447 lines
13 KiB
PHP
447 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Contract;
|
|
use App\Models\Customer;
|
|
use App\Models\CustomerProduct;
|
|
use App\Models\Feedback;
|
|
use App\Models\FeedbackInteraction;
|
|
use App\Models\Handover;
|
|
use App\Models\Product;
|
|
use App\Models\ProductService;
|
|
use App\Models\User;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Spatie\SimpleExcel\SimpleExcelReader;
|
|
|
|
class ImportCskh extends Command
|
|
{
|
|
protected $signature = 'app:import-cskh
|
|
{file_path : Path to CSV/Excel file}
|
|
{--dry-run : Analyze only, do not write to database}';
|
|
|
|
protected $description = 'Import dữ liệu CSKH từ file Excel/CSV vào database relational';
|
|
|
|
private int $statsProducts = 0;
|
|
private int $statsCustomers = 0;
|
|
private int $statsContracts = 0;
|
|
private int $statsHandovers = 0;
|
|
private int $statsServices = 0;
|
|
private int $statsFeedbacks = 0;
|
|
private int $statsInteractions = 0;
|
|
private int $statsSkipped = 0;
|
|
|
|
public function handle(): int
|
|
{
|
|
$filePath = $this->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],
|
|
]
|
|
);
|
|
}
|
|
}
|