argument('file_path'); $dryRun = $this->option('dry-run'); if (! file_exists($filePath)) { $this->error("File not found: {$filePath}"); return self::FAILURE; } if ($dryRun) { $this->warn('=== DRY RUN MODE — No data will be written ==='); } $startTime = now(); $rows = SimpleExcelReader::create($filePath)->getRows(); $total = iterator_count($rows); $rows = SimpleExcelReader::create($filePath)->getRows(); $this->info("Total rows in file: {$total}"); $bar = $this->output->createProgressBar($total); $bar->start(); $chunkSize = 100; $chunk = []; $rowCount = 0; foreach ($rows as $row) { $rowCount++; $metaReason = $this->getMetaRowReason($row); if ($metaReason !== null) { $this->recordSkip($rowCount, $metaReason, $row); $bar->advance(); continue; } $chunk[] = $row; if (count($chunk) >= $chunkSize) { $this->processChunk($chunk, $dryRun, $rowCount); $chunk = []; } $bar->advance(); } if (! empty($chunk)) { $this->processChunk($chunk, $dryRun, $rowCount); } $bar->finish(); $this->newLine(2); $this->printSummary($rowCount, $dryRun); $duration = now()->diffInSeconds($startTime); $modeLabel = $dryRun ? '[DRY RUN] ' : ''; ActivityLogger::log( 'import', $modeLabel . __('feedback.log_import_done', ['file' => basename($filePath), 'rows' => $rowCount]), null, [ 'file' => $filePath, 'total_rows' => $rowCount, 'skipped' => $this->statsSkipped, 'products' => $this->statsProducts, 'customers' => $this->statsCustomers, 'contracts' => $this->statsContracts, 'handovers' => $this->statsHandovers, 'services' => $this->statsServices, 'feedbacks' => $this->statsFeedbacks, 'interactions' => $this->statsInteractions, 'duration_seconds' => $duration, 'dry_run' => $dryRun, ], ); return self::SUCCESS; } private function isMetaRow(array $row): bool { return $this->getMetaRowReason($row) !== null; } private function getMetaRowReason(array $row): ?string { $code = $row['Mã căn hộ'] ?? ''; if (empty($code)) { return 'empty_code'; } if (is_numeric($code)) { return 'numeric_code'; } if ($code === 'Ngày TT' || $code === 'Tình trạng' || $code === 'Tên DN') { return 'header_row'; } return null; } private function recordSkip(int $rowNum, string $reason, array $row): void { $this->statsSkipped++; match ($reason) { 'empty_code' => $this->statsSkippedEmptyCode++, 'numeric_code' => $this->statsSkippedNumericCode++, 'header_row' => $this->statsSkippedHeaderRow++, default => null, }; $code = $row['Mã căn hộ'] ?? ''; $this->skippedRowDetails[] = [ 'row' => $rowNum, 'reason' => $reason, 'code' => $code, ]; } private function processChunk(array $rows, bool $dryRun, int $currentRowNum): void { if ($dryRun) { foreach ($rows as $i => $row) { $this->processRow($row, true, $currentRowNum - count($rows) + $i + 1); } return; } DB::transaction(function () use ($rows, $currentRowNum): void { foreach ($rows as $i => $row) { $this->processRow($row, false, $currentRowNum - count($rows) + $i + 1); } }); } private function processRow(array $row, bool $dryRun, int $rowNum): void { $productCode = trim($row['Mã căn hộ'] ?? ''); $customerName = trim($row['Họ tên KH'] ?? ''); if (empty($productCode) || empty($customerName)) { $this->statsSkipped++; $this->statsSkippedEmptyCustomer++; $this->skippedRowDetails[] = [ 'row' => $rowNum, 'reason' => 'empty_customer', 'code' => $productCode, ]; return; } if ($dryRun) { $this->statsProducts++; $this->statsCustomers++; return; } $product = $this->findOrCreateProduct($productCode); $customer = $this->findOrCreateCustomer($customerName); $this->ensureCustomerProduct($product, $customer); $this->processContracts($row, $product, $customer); $this->processHandover($row, $product, $customer); $this->processServices($row, $product); $content = $this->extractContent($row); if (! empty($content)) { $this->processFeedback($content, $product, $customer); } } private function findOrCreateProduct(string $code): Product { $p = Product::firstOrCreate( ['name' => $code], ['status' => 'active'] ); if ($p->wasRecentlyCreated) { $this->statsProducts++; } return $p; } private function findOrCreateCustomer(string $name): Customer { $c = Customer::firstOrCreate( ['name' => $name], ['status' => 'active'] ); if ($c->wasRecentlyCreated) { $this->statsCustomers++; } return $c; } private function ensureCustomerProduct(Product $product, Customer $customer): void { CustomerProduct::firstOrCreate([ 'customer_id' => $customer->id, 'product_id' => $product->id, ]); } private function processContracts(array $row, Product $product, Customer $customer): void { $leaseValue = $row['HỢP ĐỒNG THUÊ'] ?? null; $leaseStr = is_object($leaseValue) ? null : (string) ($leaseValue ?? ''); if (! empty($leaseStr) && ! is_numeric($leaseStr) && $leaseStr !== 'Tình trạng') { $exists = Contract::where('product_id', $product->id) ->where('customer_id', $customer->id) ->where('type', 'lease') ->exists(); if (! $exists) { Contract::create([ 'product_id' => $product->id, 'customer_id' => $customer->id, 'type' => 'lease', 'status' => 'active', ]); $this->statsContracts++; } } $bccValue = $row['HĐ HỢP TÁC KINH DOANH'] ?? null; $bccStr = is_object($bccValue) ? null : (string) ($bccValue ?? ''); if (! empty($bccStr) && ! is_numeric($bccStr) && $bccStr !== 'Tình trạng HĐ') { $exists = Contract::where('product_id', $product->id) ->where('customer_id', $customer->id) ->where('type', 'bcc') ->exists(); if (! $exists) { Contract::create([ 'product_id' => $product->id, 'customer_id' => $customer->id, 'type' => 'bcc', 'status' => 'active', 'signed_status' => $bccStr, ]); $this->statsContracts++; } } } private function processHandover(array $row, Product $product, Customer $customer): void { $handoverDate = $this->parseHandoverDate($row['BÀN GIÀO CĂN HỘ'] ?? null); $statusNote = $row['TÌNH TRẠNG'] ?? null; $statusStr = is_object($statusNote) ? null : $statusNote; if (empty($handoverDate) && empty($statusStr)) { return; } $exists = Handover::where('product_id', $product->id) ->where('customer_id', $customer->id) ->exists(); if ($exists) { return; } Handover::create([ 'product_id' => $product->id, 'customer_id' => $customer->id, 'handover_date' => $handoverDate, 'status' => $handoverDate ? 'da_ban_giao' : 'chua_du_dieu_kien', 'remark' => $this->truncateRemark($statusStr), ]); $this->statsHandovers++; } private function processServices(array $row, Product $product): void { $selfBiz = $row['TỰ DOANH'] ?? null; $selfBizStr = is_object($selfBiz) ? null : (string) ($selfBiz ?? ''); if (! empty($selfBizStr) && ! is_numeric($selfBizStr) && $selfBizStr !== 'Tên DN') { $exists = ProductService::where('product_id', $product->id) ->where('service_type', 'self_business') ->exists(); if (! $exists) { ProductService::create([ 'product_id' => $product->id, 'service_type' => 'self_business', 'status' => 'active', 'remark' => $selfBizStr, ]); $this->statsServices++; } } } private function extractContent(array $row): ?string { $sources = [ $row['Lịch Sử khách hàng'] ?? null, $row['Nội dung'] ?? null, $row['TÌNH TRẠNG'] ?? null, ]; foreach ($sources as $src) { if (is_object($src)) { continue; } if (! empty($src) && ! is_numeric($src)) { return (string) $src; } } return null; } private function processFeedback(string $content, Product $product, Customer $customer): void { $activeContract = Contract::where('product_id', $product->id) ->where('status', 'active') ->first(); $lines = array_values(array_filter( explode("\n", str_replace("\r\n", "\n", $content)), fn ($l) => trim($l) !== '' )); if (empty($lines)) { return; } $title = 'Ticket từ dữ liệu lịch sử'; $customerProduct = CustomerProduct::where('customer_id', $customer->id) ->where('product_id', $product->id) ->first(); $feedback = Feedback::firstOrCreate( [ 'customer_id' => $customer->id, 'customer_product_id' => $customerProduct?->id, 'title' => $title, ], [ 'content' => $content, 'status' => 'pending', 'contract_id' => $activeContract?->id, 'feedback_channel_id' => 1, ] ); if ($feedback->wasRecentlyCreated) { $this->statsFeedbacks++; } $defaultUser = User::first(); foreach ($lines as $line) { $exists = FeedbackInteraction::where('feedback_id', $feedback->id) ->where('content', $line) ->exists(); if ($exists) { continue; } FeedbackInteraction::create([ 'feedback_id' => $feedback->id, 'user_id' => $defaultUser?->id, 'type' => 'note', 'content' => mb_strlen($line) > 500 ? mb_substr($line, 0, 500) : $line, ]); $this->statsInteractions++; } } /** * Parse the PHP DateTime JSON format: {"date":"2021-06-09 00:00:00.000000","timezone_type":3,"timezone":"UTC"} */ private function parseHandoverDate(mixed $value): ?string { if (empty($value)) { return null; } if ($value instanceof \DateTimeInterface) { return $value->format('Y-m-d'); } if (is_object($value)) { return null; } if (is_string($value) && str_starts_with($value, '{')) { $decoded = json_decode($value, true); if (is_array($decoded) && ! empty($decoded['date'])) { $timestamp = strtotime($decoded['date']); return $timestamp ? date('Y-m-d', $timestamp) : null; } return null; } return $this->parseDateString((string) $value); } private function parseDateString(string $value): ?string { $value = trim($value); $patterns = [ '/^\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4}$/', '/^\d{4}[\/\-\.]\d{1,2}[\/\-\.]\d{1,2}$/', ]; foreach ($patterns as $pattern) { if (preg_match($pattern, $value)) { $cleaned = str_replace('/', '-', $value); $timestamp = strtotime($cleaned); return $timestamp ? date('Y-m-d', $timestamp) : null; } } return null; } private function truncateRemark(?string $value): ?string { if (empty($value) || is_numeric($value)) { return null; } return mb_strlen($value) > 500 ? mb_substr($value, 0, 500) : $value; } private function printSummary(int $totalRows, bool $dryRun): void { $mode = $dryRun ? '[DRY RUN] ' : ''; $this->table( ['Metric', 'Count'], [ ['Total rows in file', $totalRows], ['Skipped (meta/empty)', $this->statsSkipped], [' ├─ Empty code (row trống)', $this->statsSkippedEmptyCode], [' ├─ Numeric code (hàng tổng)', $this->statsSkippedNumericCode], [' ├─ Header row (tiêu đề)', $this->statsSkippedHeaderRow], [' └─ Empty customer name', $this->statsSkippedEmptyCustomer], [$mode . 'New Products', $this->statsProducts], [$mode . 'New Customers', $this->statsCustomers], [$mode . 'New Contracts', $this->statsContracts], [$mode . 'New Handovers', $this->statsHandovers], [$mode . 'New Services', $this->statsServices], [$mode . 'New Feedbacks', $this->statsFeedbacks], [$mode . 'New Interactions', $this->statsInteractions], ] ); if ($dryRun && ! empty($this->skippedRowDetails)) { $this->newLine(); $this->warn('=== Chi tiết các dòng bị bỏ qua ==='); $this->newLine(); $reasonLabels = [ 'empty_code' => 'Trống mã căn hộ', 'numeric_code' => 'Hàng tổng (numeric)', 'header_row' => 'Hàng tiêu đề', 'empty_customer' => 'Trống tên KH', ]; $reasonColors = [ 'empty_code' => 'red', 'numeric_code' => 'yellow', 'header_row' => 'cyan', 'empty_customer' => 'magenta', ]; $grouped = []; foreach ($this->skippedRowDetails as $detail) { $grouped[$detail['reason']][] = $detail; } foreach ($grouped as $reason => $details) { $color = $reasonColors[$reason] ?? 'white'; $label = $reasonLabels[$reason] ?? $reason; $count = count($details); $this->line(" {$label} — {$count} dòng:"); $showDetails = array_slice($details, 0, 30); $rowNumbers = array_column($showDetails, 'row'); $codes = array_column($showDetails, 'code'); $chunks = array_chunk($rowNumbers, 15); foreach ($chunks as $chunk) { $this->line(" Rows: " . implode(', ', $chunk) . ""); } if ($count > 30) { $this->line(" ... và " . ($count - 30) . " dòng nữa"); } $this->newLine(); } $this->line('Gợi ý: Các dòng "Empty customer name" thường là dòng trống trong Excel chỉ có mã căn hộ nhưng không có dữ liệu khác.'); $this->line('Chúng được bỏ qua vì thiếu thông tin bắt buộc (Họ tên KH).'); } } }