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 ==='); } $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++; if ($this->isMetaRow($row)) { $this->statsSkipped++; $bar->advance(); continue; } $chunk[] = $row; if (count($chunk) >= $chunkSize) { $this->processChunk($chunk, $dryRun); $chunk = []; } $bar->advance(); } if (! empty($chunk)) { $this->processChunk($chunk, $dryRun); } $bar->finish(); $this->newLine(2); $this->printSummary($rowCount, $dryRun); return self::SUCCESS; } private function isMetaRow(array $row): bool { $code = $row['Mã căn hộ'] ?? ''; if (empty($code) || is_numeric($code)) { return true; } if ($code === 'Ngày TT' || $code === 'Tình trạng' || $code === 'Tên DN') { return true; } return false; } private function processChunk(array $rows, bool $dryRun): void { if ($dryRun) { foreach ($rows as $row) { $this->processRow($row, true); } return; } DB::transaction(function () use ($rows): void { foreach ($rows as $row) { $this->processRow($row, false); } }); } private function processRow(array $row, bool $dryRun): void { $productCode = trim($row['Mã căn hộ'] ?? ''); $customerName = trim($row['Họ tên KH'] ?? ''); if (empty($productCode) || empty($customerName)) { $this->statsSkipped++; 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], [$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], ] ); } }