duplicateGroups(); $loose = $this->option('loose') ? $deduplicator->looseCandidateGroups() : []; if ($certain === [] && $loose === []) { $this->info('Nincs duplikált gyártónév ebben az adatbázisban.'); return self::SUCCESS; } $path = $this->option('path') ?: storage_path( 'app/private/gyarto_duplikaciok_' . now()->format('Y-m-d') . '.xlsx' ); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Gyártó duplikációk'); $this->writeIntro($sheet, $loose !== []); $headerRow = 9; $this->writeHeader($sheet, $headerRow); $lastRow = $this->writeGroups($sheet, $headerRow, $certain, $loose); $this->finishLayout($sheet, $headerRow, $lastRow, $certain, $loose); $directory = dirname($path); if (! is_dir($directory)) { mkdir($directory, 0775, true); } (new Xlsx($spreadsheet))->save($path); $this->info('Döntési lap elkészült: ' . $path); $this->line(' Biztos duplikáció: ' . count($certain) . ' csoport, ' . array_sum(array_map('count', $certain)) . ' rekord'); if ($this->option('loose')) { $this->line(' Ellenőrizendő javaslat: ' . count($loose) . ' csoport, ' . array_sum(array_map('count', $loose)) . ' rekord'); } else { $this->comment(' A bizonytalan jelöltekhez (elírás, kötőjel, egybeírás, ékezet): --loose'); } return self::SUCCESS; } private function writeIntro($sheet, bool $hasLoose): void { $sheet->setCellValue('A1', 'Gyártó nevek összevonása – döntési lap'); $sheet->getStyle('A1')->getFont()->setBold(true)->setSize(16); $sheet->setCellValue('A2', 'EV3-357 · Árlista feltöltés és feldolgozás újragondolása'); $sheet->setCellValue('A3', 'Adatforrás: ' . DB::connection()->getDatabaseName() . ' adatbázis · Készült: ' . now()->format('Y-m-d H:i')); $sheet->setCellValue('A5', 'A rendszerben ugyanaz a gyártó több néven is szerepel – például ' . '"Danone" és "Danone " egy záró szóközzel, "Békás Kft." és "Békás Kft", vagy ' . '"Gast Food" és "Gast-Food". Ez két gondot okoz: az árlista feldolgozó valódi változás ' . 'nélkül is módosulást jelez, a statisztikában pedig egy gyártóra szűrve a másik név alá ' . 'könyvelt tételek kimaradnak a riportból.'); $sheet->mergeCells('A5:J5'); $sheet->getStyle('A5')->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP); $sheet->getRowDimension(5)->setRowHeight(46); $sheet->setCellValue('A6', 'TEENDŐ: minden csoportnál töltsd ki a sárga "VÉGLEGES NÉV" mezőt – ez lesz ' . 'a megmaradó, egységes név. A többi név nem vész el: összevonjuk őket a véglegesbe, a ' . 'termékek és a rendelési előzmények megmaradnak. Az "Azonosító" oszlopot kérjük ne módosítsd.'); $sheet->mergeCells('A6:J6'); $sheet->getStyle('A6')->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP); $sheet->getStyle('A6')->getFont()->setBold(true); $sheet->getRowDimension(6)->setRowHeight(46); if ($hasLoose) { $sheet->setCellValue('A7', 'A "Típus" oszlop megmutatja, miben térnek el a nevek – így kötegelve ' . 'tudsz haladni. A "' . ProducerDeduplicator::DIFF_PUNCTUATION . '" csoportoknál csak pont, ' . 'kötőjel vagy vessző a különbség ("Békás Kft." és "Békás Kft"), ott már beírtuk a ' . 'javaslatot. A többinél ("' . ProducerDeduplicator::DIFF_SPACING . '", "' . ProducerDeduplicator::DIFF_ACCENT . '", "' . ProducerDeduplicator::DIFF_COMPANY_FORM . '") NEM töltöttük ki a végleges nevet, mert ott valódi döntés kell: ' . 'ha ugyanaz a cég, írd be a nevet (a javaslat a "Megjegyzés" oszlopban van); ' . 'HA NEM UGYANAZ A CÉG, HAGYD ÜRESEN – akkor nem nyúlunk hozzájuk.'); $sheet->mergeCells('A7:J7'); $sheet->getStyle('A7')->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP); $sheet->getStyle('A7')->getFont()->setBold(true)->getColor()->setARGB('FF9C5700'); $sheet->getRowDimension(7)->setRowHeight(62); } } private function writeHeader($sheet, int $headerRow): void { $headers = [self::COLUMN_GROUP, self::COLUMN_ID, 'Típus', 'Gyártó neve a rendszerben', 'Eltérés', 'Termékek', 'Rendelési tételek', 'Létrehozva', self::COLUMN_FINAL_NAME, 'Megjegyzés']; foreach ($headers as $i => $text) { $sheet->setCellValueByColumnAndRow($i + 1, $headerRow, $text); } $range = 'A' . $headerRow . ':J' . $headerRow; $sheet->getStyle($range)->getFont()->setBold(true)->getColor()->setARGB('FFFFFFFF'); $sheet->getStyle($range)->getFill()->setFillType(Fill::FILL_SOLID) ->getStartColor()->setARGB('FF44546A'); $sheet->getStyle($range)->getAlignment()->setWrapText(true) ->setVertical(Alignment::VERTICAL_CENTER); $sheet->getRowDimension($headerRow)->setRowHeight(32); } /** * A laza javaslatok sorrendje: elöl a gyakorlatilag eldöntött esetek, hátul azok, * amik valódi cégismeretet igényelnek. Így a megrendelő kötegelve tud haladni. */ private const CATEGORY_ORDER = [ ProducerDeduplicator::DIFF_PUNCTUATION, ProducerDeduplicator::DIFF_SPACING, ProducerDeduplicator::DIFF_ACCENT, ProducerDeduplicator::DIFF_COMPANY_FORM, ProducerDeduplicator::DIFF_MIXED, ]; private function writeGroups($sheet, int $headerRow, array $certain, array $loose): int { $row = $headerRow + 1; $groupIndex = 0; foreach ($certain as $members) { $groupIndex++; $row = $this->writeGroup($sheet, $row, $groupIndex, self::TYPE_CERTAIN, $members, false); } $deduplicator = app(ProducerDeduplicator::class); $byCategory = []; foreach ($loose as $key => $members) { $byCategory[$deduplicator->differenceCategory($members)][$key] = $members; } foreach (self::CATEGORY_ORDER as $category) { foreach ($byCategory[$category] ?? [] as $members) { $groupIndex++; // Az írásjel-eltérésnél nincs mérlegelnivaló (a "Kft." és a "Kft" ugyanaz), // ezért ott előre beírjuk a javaslatot - a megrendelő így csak a valóban // kérdéses eseteket nézi át tételesen. $prefill = $category === ProducerDeduplicator::DIFF_PUNCTUATION; $row = $this->writeGroup($sheet, $row, $groupIndex, $category, $members, ! $prefill); } } return $row - 1; } private function writeGroup($sheet, int $row, int $groupIndex, string $type, array $members, bool $isReview): int { $proposed = trim((string) $members[0]['producer']->name); $shade = $isReview ? 'FFFDF2E9' : ($groupIndex % 2 === 0 ? 'FFF2F2F2' : 'FFFFFFFF'); $groupFirstRow = $row; foreach ($members as $index => $member) { $producer = $member['producer']; $name = (string) $producer->name; // A csoport sorszáma MINDEN soron szerepel: így a lap szűrhető és // rendezhető anélkül, hogy a csoportok összekeverednének. $sheet->setCellValue('A' . $row, $groupIndex); $sheet->setCellValue('B' . $row, $producer->id); $sheet->setCellValue('C' . $row, $type); $sheet->setCellValueExplicit('D' . $row, '"' . $name . '"', DataType::TYPE_STRING); $sheet->setCellValue('E' . $row, $index === 0 ? ($isReview ? 'legtöbb termék' : 'javasolt megtartani') : implode(', ', $this->describeDifference($name, $proposed))); $sheet->setCellValue('F' . $row, $member['counts']['products']); $sheet->setCellValue('G' . $row, $member['counts']['order_archives_items']); $sheet->setCellValue('H' . $row, substr((string) $producer->created_at, 0, 10)); $sheet->getStyle('A' . $row . ':J' . $row)->getFill() ->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB($shade); if ($index === 0) { $sheet->getStyle('D' . $row)->getFont()->setBold(true); if ($isReview) { // Bizonytalan javaslatnál a végleges név ÜRESEN marad: az alapértelmezett // viselkedés a "nem nyúlunk hozzá", a döntés pedig tudatos kitöltés. $sheet->setCellValue('J' . $row, 'ha ugyanaz a cég: ' . $proposed); $sheet->getStyle('J' . $row)->getFont()->setItalic(true); } else { $sheet->setCellValueExplicit('I' . $row, $proposed, DataType::TYPE_STRING); $sheet->getStyle('I' . $row)->getFont()->setBold(true); } $sheet->getStyle('I' . $row)->getFill()->setFillType(Fill::FILL_SOLID) ->getStartColor()->setARGB($isReview ? 'FFFFE0B2' : 'FFFFF2CC'); } $row++; } $sheet->getStyle('A' . $groupFirstRow . ':J' . ($row - 1))->getBorders()->getTop() ->setBorderStyle(Border::BORDER_THIN)->getColor()->setARGB('FFBFBFBF'); return $row; } /** * @return array */ private function describeDifference(string $name, string $proposed): array { $differences = []; if ($name !== trim($name)) { $differences[] = 'szóköz a név szélén'; } if (trim($name) !== $proposed && mb_strtoupper(trim($name)) === mb_strtoupper($proposed)) { $differences[] = 'eltérő kis/nagybetű'; } if (str_contains($name, '/') || str_contains($name, '_')) { $differences[] = 'eltérő elválasztó'; } if (preg_match('/\s{2,}/', $name)) { $differences[] = 'dupla szóköz'; } return $differences === [] ? ['eltérő írásmód'] : $differences; } private function finishLayout($sheet, int $headerRow, int $lastRow, array $certain, array $loose): void { $first = $headerRow + 1; $sheet->getStyle('F' . $first . ':G' . $lastRow)->getNumberFormat()->setFormatCode('# ##0'); $sheet->getStyle('A' . $first . ':C' . $lastRow)->getAlignment() ->setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet->getStyle('I' . $first . ':I' . $lastRow)->getBorders()->getAllBorders() ->setBorderStyle(Border::BORDER_THIN)->getColor()->setARGB('FFBF8F00'); $widths = ['A' => 9, 'B' => 11, 'C' => 15, 'D' => 34, 'E' => 20, 'F' => 11, 'G' => 17, 'H' => 13, 'I' => 34, 'J' => 34]; foreach ($widths as $column => $width) { $sheet->getColumnDimension($column)->setWidth($width); } $sheet->freezePane('A' . $first); $sheet->setAutoFilter('A' . $headerRow . ':J' . $lastRow); $summaryRow = $lastRow + 2; $summary = 'Biztos duplikáció: ' . count($certain) . ' csoport, ' . array_sum(array_map('count', $certain)) . ' rekord.'; if ($loose !== []) { $summary .= ' Ellenőrizendő javaslat: ' . count($loose) . ' csoport, ' . array_sum(array_map('count', $loose)) . ' rekord – ezeknél az üresen hagyott ' . 'végleges név azt jelenti, hogy nem vonjuk össze őket.'; } $sheet->setCellValue('A' . $summaryRow, $summary); $sheet->mergeCells('A' . $summaryRow . ':J' . $summaryRow); $sheet->getStyle('A' . $summaryRow)->getFont()->setItalic(true); } }