diff --git a/04_Laravel/app/Console/Commands/ImportTrackingData.php b/04_Laravel/app/Console/Commands/ImportTrackingData.php new file mode 100644 index 0000000..006097e --- /dev/null +++ b/04_Laravel/app/Console/Commands/ImportTrackingData.php @@ -0,0 +1,77 @@ +argument('path'); + + if (!file_exists($path)) { + $this->error("File not found: {$path}"); + return 1; + } + + $this->info("Starting tracking data import from: {$path}"); + $this->newLine(); + + try { + $import = new TrackingDataImport(); + Excel::import($import, $path); + + $imported = $import->getImportedEvents(); + $skipped = $import->getSkippedEvents(); + $errors = $import->getErrors(); + $createdShipments = $import->getCreatedShipments(); + + $this->info("Import completed successfully!"); + $this->newLine(); + $this->table( + ['Metric', 'Count'], + [ + ['Imported Events', $imported], + ['Skipped Events', $skipped], + ['Created Shipments', $createdShipments], + ['Errors', count($errors)], + ] + ); + + if ($errors) { + $this->newLine(); + $this->warn('Errors:'); + foreach (array_slice($errors, 0, 20) as $error) { + $this->error(" - {$error}"); + } + if (count($errors) > 20) { + $this->warn(" ... and " . (count($errors) - 20) . " more errors"); + } + } + + Log::info('Tracking import command completed', [ + 'path' => $path, + 'imported' => $imported, + 'skipped' => $skipped, + 'errors' => count($errors), + ]); + + return 0; + } catch (\Throwable $e) { + $this->error("Import failed: " . $e->getMessage()); + Log::error('Tracking import command failed', [ + 'path' => $path, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + return 1; + } + } +} diff --git a/04_Laravel/app/Imports/TrackingDataImport.php b/04_Laravel/app/Imports/TrackingDataImport.php new file mode 100644 index 0000000..797d199 --- /dev/null +++ b/04_Laravel/app/Imports/TrackingDataImport.php @@ -0,0 +1,224 @@ + $rows->count()]); + + if ($rows->count() === 0) { + Log::warning('No rows found'); + return; + } + + $firstRow = $rows->first(); + Log::info('First row keys', ['keys' => $firstRow->keys()->toArray()]); + Log::info('First row awb', ['awb' => $firstRow['awb'] ?? 'N/A']); + + foreach ($rows as $index => $row) { + try { + $awbNo = $this->normalizeAwb($row['awb'] ?? null); + $eventDate = $this->normalizeDate($row['date'] ?? null); + $eventTime = $this->normalizeTime($row['time'] ?? null); + $eventDescription = $this->normalizeDescription($row['state'] ?? null); + $location = $this->normalizeLocation($row['country'] ?? null); + $deliveryStatus = $this->normalizeDeliveryStatus($row['last_state'] ?? null); + + if (!$awbNo || !$eventDate || !$eventDescription) { + $this->skippedEvents++; + continue; + } + + $shipment = Shipment::where('awb_no', $awbNo)->first(); + + if (!$shipment) { + $shipment = $this->createShipmentFromTrackingRow($row, $awbNo, $location); + $this->createdShipments++; + } + + ShipmentTrackingEvent::create([ + 'shipment_id' => $shipment->id, + 'event_date' => $eventDate, + 'event_time' => $eventTime, + 'location' => $location, + 'event_description' => $eventDescription, + 'delivery_status' => $deliveryStatus, + 'source' => 'manual', + ]); + + $this->importedEvents++; + } catch (\Throwable $e) { + $this->skippedEvents++; + $this->errors[] = "Row {$index}: " . $e->getMessage(); + Log::error('Tracking import error', ['row' => $index, 'error' => $e->getMessage()]); + } + } + + Log::info('Tracking import completed', [ + 'imported' => $this->importedEvents, + 'skipped' => $this->skippedEvents, + 'created' => $this->createdShipments, + 'errors' => count($this->errors), + ]); + } + + protected function createShipmentFromTrackingRow(array $row, string $awbNo, ?string $location): Shipment + { + $countryName = $location ? explode(' - ', $location)[0] : null; + $country = Country::where('name', 'like', "%{$countryName}%")->first(); + + $lastState = strtolower($row['last_state'] ?? ''); + + $status = 'processed'; + if (str_contains($lastState, 'delivered')) $status = 'delivered'; + elseif (str_contains($lastState, 'in transit')) $status = 'in_transit'; + elseif (str_contains($lastState, 'picked')) $status = 'picked_up'; + elseif (str_contains($lastState, 'out for delivery')) $status = 'out_for_delivery'; + elseif (str_contains($lastState, 'failed')) $status = 'failed'; + + $shipment = Shipment::create([ + 'awb_no' => $awbNo, + 'direction' => 'export', + 'type' => 'PARCEL', + 'status' => $status, + 'weight' => 0, + 'volumetric_weight' => 0, + 'chargeable_weight' => 0, + 'shipping_price' => 0, + 'extra_service' => 0, + 'packing_cost' => 0, + 'domestic_pickup' => 0, + 'domestic_delivery' => 0, + 'warehousing_cost' => 0, + 'vat_amount' => 0, + 'discount' => 0, + 'total_fee' => 0, + 'net_dirham' => 0, + 'net_rial' => 0, + 'from_country_id' => Country::where('name', 'Iran')->first()?->id, + 'to_country_id' => $country?->id, + 'sender_name' => 'Unknown', + 'receiver_name' => 'Unknown', + ]); + + return $shipment; + } + + public function getImportedEvents(): int + { + return $this->importedEvents; + } + + public function getSkippedEvents(): int + { + return $this->skippedEvents; + } + + public function getErrors(): array + { + return $this->errors; + } + + public function getCreatedShipments(): int + { + return $this->createdShipments; + } + + protected function normalizeAwb($value): ?string + { + if (!$value) return null; + $value = trim($value); + return $value === '' || $value === '0' ? null : $value; + } + + protected function normalizeDate($value): ?string + { + if (!$value) return null; + + if ($value instanceof \DateTime) { + return $value->format('Y-m-d'); + } + + $value = trim($value); + if ($value === '' || $value === '1899-12-31') return null; + + try { + return date('Y-m-d', strtotime($value)); + } catch (\Throwable $e) { + return null; + } + } + + protected function normalizeTime($value): ?string + { + if (!$value) return null; + + if ($value instanceof \DateTime) { + return $value->format('H:i:s'); + } + + $value = trim($value); + if ($value === '' || $value === '1899-12-31') return null; + + try { + return date('H:i:s', strtotime($value)); + } catch (\Throwable $e) { + return null; + } + } + + protected function normalizeDescription($value): ?string + { + if (!$value) return null; + $value = trim($value); + return $value === '' ? null : $value; + } + + protected function normalizeLocation($value): ?string + { + if (!$value) return null; + $value = trim($value); + return $value === '' || $value === '.' ? null : $value; + } + + protected function normalizeDeliveryStatus($value): ?string + { + if (!$value) return null; + $value = trim($value); + if ($value === '') return null; + + $statuses = [ + 'processed' => 'processed', + 'picked up' => 'picked_up', + 'in transit' => 'in_transit', + 'out for delivery' => 'out_for_delivery', + 'failed' => 'failed', + 'delivered' => 'delivered', + 'returned' => 'returned', + ]; + + $lower = strtolower($value); + foreach ($statuses as $key => $status) { + if (str_contains($lower, $key)) { + return $status; + } + } + + return null; + } +} diff --git a/04_Laravel/bootstrap/app.php b/04_Laravel/bootstrap/app.php index 05cdcf1..e5c4b91 100644 --- a/04_Laravel/bootstrap/app.php +++ b/04_Laravel/bootstrap/app.php @@ -11,6 +11,10 @@ return Application::configure(basePath: dirname(__DIR__)) commands: __DIR__.'/../routes/console.php', health: '/up', ) + ->withCommands([ + \App\Console\Commands\ImportShippingRates::class, + \App\Console\Commands\ImportTrackingData::class, + ]) ->withMiddleware(function (Middleware $middleware): void { $middleware->api(prepend: \Illuminate\Http\Middleware\HandleCors::class); diff --git a/04_Laravel/check_shipments.php b/04_Laravel/check_shipments.php new file mode 100644 index 0000000..fb72617 --- /dev/null +++ b/04_Laravel/check_shipments.php @@ -0,0 +1,11 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +$shipments = App\Models\Shipment::all(); +echo 'Total shipments: ' . $shipments->count() . PHP_EOL; +foreach ($shipments as $s) { + echo 'AWB: ' . $s->awb_no . PHP_EOL; +} diff --git a/04_Laravel/check_tracking.php b/04_Laravel/check_tracking.php new file mode 100644 index 0000000..9a80dbe --- /dev/null +++ b/04_Laravel/check_tracking.php @@ -0,0 +1,15 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +$shipment = App\Models\Shipment::where('awb_no', '980103619')->first(); +echo 'Shipment: ' . ($shipment ? $shipment->awb_no : 'Not found') . PHP_EOL; + +if ($shipment) { + echo 'Events: ' . $shipment->trackingEvents()->count() . PHP_EOL; + foreach ($shipment->trackingEvents()->orderBy('event_date', 'desc')->limit(3)->get() as $e) { + echo $e->event_date . ' - ' . $e->event_description . PHP_EOL; + } +} diff --git a/04_Laravel/debug_import.php b/04_Laravel/debug_import.php new file mode 100644 index 0000000..8af73da --- /dev/null +++ b/04_Laravel/debug_import.php @@ -0,0 +1,16 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +use Maatwebsite\Excel\Facades\Excel; +use App\Imports\TrackingDataImport; + +Excel::import(new TrackingDataImport(), '../01_Documents/Data entry 2026-06-28.xlsx'); + +$import = app(TrackingDataImport::class); +echo 'Imported: ' . $import->getImportedEvents() . PHP_EOL; +echo 'Skipped: ' . $import->getSkippedEvents() . PHP_EOL; +echo 'Created: ' . $import->getCreatedShipments() . PHP_EOL; +echo 'Errors: ' . count($import->getErrors()) . PHP_EOL; diff --git a/04_Laravel/debug_import2.php b/04_Laravel/debug_import2.php new file mode 100644 index 0000000..5035a02 --- /dev/null +++ b/04_Laravel/debug_import2.php @@ -0,0 +1,24 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +use Maatwebsite\Excel\Facades\Excel; +use Maatwebsite\Excel\Concerns\ToCollection; +use Maatwebsite\Excel\Concerns\WithHeadingRow; +use Illuminate\Support\Collection; + +class DebugImport implements ToCollection, WithHeadingRow +{ + public function collection(Collection $rows) + { + echo 'Total rows: ' . $rows->count() . PHP_EOL; + if ($rows->count() > 0) { + echo 'First row keys: ' . implode(', ', $rows->first()->keys()->toArray()) . PHP_EOL; + echo 'First row AWB: ' . ($rows->first()['AWB'] ?? 'N/A') . PHP_EOL; + } + } +} + +Excel::import(new DebugImport(), '../01_Documents/Data entry 2026-06-28.xlsx'); diff --git a/04_Laravel/routes/console.php b/04_Laravel/routes/console.php index 3c9adf1..8e249da 100644 --- a/04_Laravel/routes/console.php +++ b/04_Laravel/routes/console.php @@ -6,3 +6,7 @@ use Illuminate\Support\Facades\Artisan; Artisan::command('inspire', function () { $this->comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); + +Artisan::command('ifnex:import:tracking {path : Path to Excel file}', function ($path) { + $this->call('import:tracking', ['path' => $path]); +})->purpose('Import tracking events from Excel file'); diff --git a/04_Laravel/test_excel.php b/04_Laravel/test_excel.php new file mode 100644 index 0000000..79cc4a1 --- /dev/null +++ b/04_Laravel/test_excel.php @@ -0,0 +1,13 @@ +getSheetNames(); +echo 'Sheets: ' . implode(', ', $sheetNames) . PHP_EOL; +$sheet = $spreadsheet->getSheet(0); +echo 'Sheet 0 name: ' . $sheet->getTitle() . PHP_EOL; +echo 'Row 1: ' . implode(', ', $sheet->rangeToArray('A1:H1')[0]) . PHP_EOL; +echo 'Row 2: ' . implode(', ', $sheet->rangeToArray('A2:H2')[0]) . PHP_EOL; +echo 'Row 3: ' . implode(', ', $sheet->rangeToArray('A3:H3')[0]) . PHP_EOL; +echo 'Total rows: ' . $sheet->getHighestRow() . PHP_EOL;