From a59e192b32a1999d3f0a457bec214627be16c431 Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Tue, 4 Aug 2026 01:59:08 +0330 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20=DB=B1=DB=B0=20=D9=85=D9=88=D8=B1?= =?UTF-8?q?=D8=AF=20=DA=A9=D8=A7=D9=85=D9=84=20=D8=B4=D8=AF=20(=D8=AF?= =?UTF-8?q?=DB=8C=D8=AA=D8=A7=D8=A8=DB=8C=D8=B3=D8=8C=20=D9=85=D8=AF=D9=84?= =?UTF-8?q?=E2=80=8C=D9=87=D8=A7=D8=8C=20API=D8=8C=20Filament=D8=8C=20Lara?= =?UTF-8?q?vel=2011=D8=8C=20=D8=B1=D9=81=D8=B9=20=D8=A8=D8=A7=DA=AF?= =?UTF-8?q?=E2=80=8C=D9=87=D8=A7)=20=F0=9F=94=84=20=D8=AF=D8=B1=20=D8=AD?= =?UTF-8?q?=D8=A7=D9=84=20=D8=A7=D9=86=D8=AC=D8=A7=D9=85:=20=D8=AA=D8=B3?= =?UTF-8?q?=D8=AA=20=D8=AF=D8=B3=D8=AA=DB=8C=20Filament=20=E2=8F=B8?= =?UTF-8?q?=EF=B8=8F=20=D8=A8=D9=84=D8=A7=DA=A9:=20=D9=85=D9=87=D8=A7?= =?UTF-8?q?=D8=AC=D8=B1=D8=AA=20=DB=B3=DB=B9=DB=B5=DB=B0=20=D8=B1=DA=A9?= =?UTF-8?q?=D9=88=D8=B1=D8=AF=20(=D9=85=D9=86=D8=AA=D8=B8=D8=B1=20=D9=81?= =?UTF-8?q?=D8=A7=DB=8C=D9=84=20=D8=A7=DA=A9=D8=B3=D9=84=20=D8=A7=D8=B5?= =?UTF-8?q?=D9=84=D8=A7=D8=AD=E2=80=8C=D8=B4=D8=AF=D9=87)=20=E2=8F=B8?= =?UTF-8?q?=EF=B8=8F=20=D8=A8=D9=84=D8=A7=DA=A9:=20=D9=BE=D9=84=D8=A7?= =?UTF-8?q?=DA=AF=DB=8C=D9=86=20=D9=88=D8=B1=D8=AF=D9=BE=D8=B1=D8=B3=20(?= =?UTF-8?q?=D8=A8=D8=B9=D8=AF=20=D8=A7=D8=B2=20=D8=AA=D8=B3=D8=AA=20=DA=A9?= =?UTF-8?q?=D8=A7=D9=85=D9=84=20API)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 01_Documents/IFNEX_Phase0_Checklist.md | 229 ++++++++ 01_Documents/STATUS.md | 484 +++++++++++------ 04_Laravel/.env.example | 8 +- .../Console/Commands/DebugImportCommand.php | 21 + .../Console/Commands/ImportHistoricalData.php | 32 ++ .../Console/Commands/ImportShippingRates.php | 32 ++ 04_Laravel/app/Enums/CarrierCode.php | 16 + 04_Laravel/app/Enums/ShipmentDirection.php | 9 + 04_Laravel/app/Enums/ShipmentStatus.php | 14 + 04_Laravel/app/Enums/ShipmentType.php | 10 + 04_Laravel/app/Enums/TrackingSource.php | 10 + 04_Laravel/app/Enums/UserRole.php | 11 + .../app/Filament/Pages/IfnexSettingsPage.php | 109 ++++ .../app/Filament/Pages/SettingsPage.php | 80 --- .../Filament/Resources/CountryResource.php | 57 +- .../Resources/ShipmentItemResource.php | 29 +- .../Filament/Resources/ShipmentResource.php | 255 +++++---- .../ShipmentResource/Pages/CreateShipment.php | 5 + .../ShipmentResource/Pages/ListShipments.php | 33 -- .../ShipmentResource/Pages/ViewShipment.php | 12 + .../CarrierMappingsRelationManager.php | 79 +++ .../TrackingEventsRelationManager.php | 72 +++ .../Resources/ShippingRateResource.php | 77 +-- .../Controllers/Api/PricingController.php | 49 +- .../Http/Controllers/Api/TrackController.php | 58 +++ .../app/Http/Middleware/ApiKeyMiddleware.php | 27 + .../app/Http/Middleware/SetCspHeaders.php | 18 + 04_Laravel/app/Imports/DebugImport.php | 32 ++ .../app/Imports/HistoricalShipmentsImport.php | 164 ++++++ 04_Laravel/app/Imports/OldShipmentsImport.php | 104 ---- .../app/Imports/ShippingRatesImport.php | 102 ++-- 04_Laravel/app/Models/Country.php | 28 +- 04_Laravel/app/Models/Shipment.php | 38 +- .../app/Models/ShipmentCarrierMapping.php | 37 ++ .../app/Models/ShipmentTrackingEvent.php | 42 ++ 04_Laravel/app/Models/ShippingRate.php | 9 + 04_Laravel/app/Models/SystemSetting.php | 44 +- 04_Laravel/app/Models/User.php | 42 +- .../Providers/Filament/AdminPanelProvider.php | 27 +- .../app/Services/PriceCalculatorService.php | 116 +++-- 04_Laravel/app/Services/TrackingService.php | 36 ++ 04_Laravel/bootstrap/app.php | 14 +- 04_Laravel/composer.json | 5 +- 04_Laravel/composer.lock | 317 ++++++----- 04_Laravel/config/ifnex.php | 25 + ...26_08_02_000001_create_countries_table.php | 34 ++ ...26_08_02_000002_create_shipments_table.php | 73 +++ ..._02_000003_create_shipping_rates_table.php | 40 ++ ...create_shipment_carrier_mappings_table.php | 36 ++ ..._create_shipment_tracking_events_table.php | 36 ++ ...02_000006_create_system_settings_table.php | 31 ++ .../2026_08_02_000007_update_users_table.php | 24 + .../database/seeders/CountriesTableSeeder.php | 490 +++++++++--------- .../database/seeders/DatabaseSeeder.php | 20 +- .../database/seeders/SystemSettingSeeder.php | 24 +- 04_Laravel/routes/api.php | 19 +- 56 files changed, 2713 insertions(+), 1132 deletions(-) create mode 100644 01_Documents/IFNEX_Phase0_Checklist.md create mode 100644 04_Laravel/app/Console/Commands/DebugImportCommand.php create mode 100644 04_Laravel/app/Console/Commands/ImportHistoricalData.php create mode 100644 04_Laravel/app/Console/Commands/ImportShippingRates.php create mode 100644 04_Laravel/app/Enums/CarrierCode.php create mode 100644 04_Laravel/app/Enums/ShipmentDirection.php create mode 100644 04_Laravel/app/Enums/ShipmentStatus.php create mode 100644 04_Laravel/app/Enums/ShipmentType.php create mode 100644 04_Laravel/app/Enums/TrackingSource.php create mode 100644 04_Laravel/app/Enums/UserRole.php create mode 100644 04_Laravel/app/Filament/Pages/IfnexSettingsPage.php delete mode 100644 04_Laravel/app/Filament/Pages/SettingsPage.php create mode 100644 04_Laravel/app/Filament/Resources/ShipmentResource/Pages/ViewShipment.php create mode 100644 04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/CarrierMappingsRelationManager.php create mode 100644 04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/TrackingEventsRelationManager.php create mode 100644 04_Laravel/app/Http/Controllers/Api/TrackController.php create mode 100644 04_Laravel/app/Http/Middleware/ApiKeyMiddleware.php create mode 100644 04_Laravel/app/Http/Middleware/SetCspHeaders.php create mode 100644 04_Laravel/app/Imports/DebugImport.php create mode 100644 04_Laravel/app/Imports/HistoricalShipmentsImport.php delete mode 100644 04_Laravel/app/Imports/OldShipmentsImport.php create mode 100644 04_Laravel/app/Models/ShipmentCarrierMapping.php create mode 100644 04_Laravel/app/Models/ShipmentTrackingEvent.php create mode 100644 04_Laravel/app/Services/TrackingService.php create mode 100644 04_Laravel/config/ifnex.php create mode 100644 04_Laravel/database/migrations/2026_08_02_000001_create_countries_table.php create mode 100644 04_Laravel/database/migrations/2026_08_02_000002_create_shipments_table.php create mode 100644 04_Laravel/database/migrations/2026_08_02_000003_create_shipping_rates_table.php create mode 100644 04_Laravel/database/migrations/2026_08_02_000004_create_shipment_carrier_mappings_table.php create mode 100644 04_Laravel/database/migrations/2026_08_02_000005_create_shipment_tracking_events_table.php create mode 100644 04_Laravel/database/migrations/2026_08_02_000006_create_system_settings_table.php create mode 100644 04_Laravel/database/migrations/2026_08_02_000007_update_users_table.php diff --git a/01_Documents/IFNEX_Phase0_Checklist.md b/01_Documents/IFNEX_Phase0_Checklist.md new file mode 100644 index 0000000..abfe032 --- /dev/null +++ b/01_Documents/IFNEX_Phase0_Checklist.md @@ -0,0 +1,229 @@ +# ✅ IFNEX Phase 0 Checklist + +> **فاز:** Phase 0 — بنیان داده + ترکینگ دستی + مهاجرت داده‌های تاریخی +> **تاریخ ایجاد:** 2026-08-04 +> **وضعیت کلی:** در حال اجرا +> **مرجع اصلی:** `01_Documents/Phase0_Proposal.md` + +--- + +## پیش‌نیازها + +| # | شرط | وضعیت | توضیحات | +|---|------|-------|---------| +| P1 | Laravel 11 نصب و قابل اجرا | ✅ انجام شد | در پوشه `04_Laravel` | +| P2 | MySQL 8+ قابل دسترس | ✅ انجام شد | دیتابیس `ifnex_db` ایجاد شده | +| P3 | Composer dependencies نصب شده | ✅ انجام شد | filament 3.3.x, maatwebsite/excel, morilog/jalali 3 | +| P4 | فایل‌های اکسل اصلی در دسترس | ⚠️ موجود | فقط داده‌های ۲ ردیف اول شیت List واقعی هستند، بقیه فرمول Excel | + +--- + +## بخش ۱: دیتابیس و مدل‌ها + +| # | کار | وضعیت | توضیحات | +|---|-----|-------|---------| +| 1.1 | Migration `countries` با ۴ زون | ✅ انجام شد | iso_code + is_active اضافه شد | +| 1.2 | Migration `shipments` مطابق Phase 0 | ✅ انجام شد | ۱۱ فیلد مالی + ۴ زون + nullable حقایل | +| 1.3 | Migration `shipping_rates` با enum | ✅ انجام شد | direction + type (DOC_NORMAL/DOC_ECONOMY/PARCEL) | +| 1.4 | Migration `shipment_carrier_mappings` | ✅ انجام شد | جدول جدید برای نگاشت چند شرکت حمل | +| 1.5 | Migration `shipment_tracking_events` | ✅ انجام شد | جدول جدید برای تایم‌لاین رویدادها | +| 1.6 | Migration `system_settings` | ✅ انجام شد | جدول key-value | +| 1.7 | Migration `update_users` | ✅ انجام شد | phone, role, is_active | +| 1.8 | حل مشکل `unsignedDecimal` | ✅ انجام شد | استفاده از `decimal()->unsigned()` | +| 1.9 | حل مشکل FK constraints | ✅ انجام شد | `disableForeignKeyConstraints()` | +| 1.10 | اجرای همه migrations | ✅ انجام شد | بدون خطا | + +### مدل‌ها و Enums + +| # | کار | وضعیت | توضیحات | +|---|-----|-------|---------| +| 2.1 | مدل `Country.php` + روابط | ✅ انجام شد | fromShipments() + toShipments() | +| 2.2 | مدل `Shipment.php` + روالات کامل | ✅ انجام شد | items(), carrierMappings(), trackingEvents() | +| 2.3 | مدل `ShippingRate.php` | ✅ انجام شد | cast برای enums | +| 2.4 | مدل `User.php` + helper methods | ✅ انجام شد | phone, role, is_active | +| 2.5 | مدل `ShipmentCarrierMapping.php` | ✅ انجام شد | | +| 2.6 | مدل `ShipmentTrackingEvent.php` | ✅ انجام شد | | +| 2.7 | مدل `SystemSetting.php` | ✅ انجام شد | | +| 2.8 | Enum `ShipmentDirection` | ✅ انجام شد | import/export | +| 2.9 | Enum `ShipmentType` | ✅ انجام شد | DOC_NORMAL/DOC_ECONOMY/PARCEL | +| 2.10 | Enum `ShipmentStatus` | ✅ انجام شد | ۷ وضعیت | +| 2.11 | Enum `CarrierCode` | ✅ انجام شد | ۹ شرکت حمل | +| 2.12 | Enum `TrackingSource` | ✅ انجام شد | manual/api_carrier/api_aggregator | +| 2.13 | Enum `UserRole` | ✅ انجام شد | ۴ نقش | + +--- + +## بخش ۲: Seeders و داده‌های اولیه + +| # | کار | وضعیت | توضیحات | +|---|-----|-------|---------| +| 3.1 | Seeder ۲۳۲ کشور با ۴ زون | ✅ انجام شد | تصحیح کدهای ISO، حذف تکراری‌ها | +| 3.2 | Seeder System Settings | ✅ انجام شد | VAT 9%, packing 100000 IRR, profit_margin 1.25 | +| 3.3 | Seeder Super Admin User | ✅ انجام شد | | +| 3.4 | DatabaseSeeder ترکیبی | ✅ انجام شد | | + +--- + +## بخش ۳: API ترکینگ + +| # | کار | وضعیت | توضیحات | +|---|-----|-------|---------| +| 4.1 | ایجاد `TrackController` | ✅ انجام شد | JSON response کامل | +| 4.2 | ایجاد `ApiKeyMiddleware` | ✅ انجام شد | Bearer token validation | +| 4.3 | ثبت route `GET /api/v1/track/{awb_no}` | ✅ انجام شد | | +| 4.4 | تست با API Key صحیح | ✅ انجام شد | 200 OK | +| 4.5 | تست بدون API Key | ✅ انجام شد | 401 Unauthorized | +| 4.6 | تست با API Key نامعتبر | ✅ انجام شد | 403 Forbidden | + +--- + +## بخش ۴: پنل مدیریت Filament + +### Resources اصلی + +| # | کار | وضعیت | توضیحات | +|---|-----|-------|---------| +| 5.1 | `CountryResource` | ✅ انجام شد | فرم ۴ زون + iso_code + is_active | +| 5.2 | `ShipmentResource` | ✅ انجام شد | ۷ Section کامل | +| 5.3 | `ShippingRateResource` | ✅ انجام شد | ۱۰ zone + enum | +| 5.4 | `ShipmentItemResource` | ✅ انجام شد | CRUD کامل | +| 5.5 | `CarrierMappingsRelationManager` | ✅ انجام شد | برای Shipment | +| 5.6 | `TrackingEventsRelationManager` | ✅ انجام شد | برای Shipment | +| 5.7 | `SettingsPage` | ✅ انجام شد | System Settings | +| 5.8 | صفحه‌های List/Create/Edit/View | ✅ انجام شد | برای همه Resources | + +--- + +## بخش ۵: Services + +| # | کار | وضعیت | توضیحات | +|---|-----|-------|---------| +| 6.1 | `TrackingService` | ✅ انجام شد | getTrackingTimeline, addTrackingEvent, getShipmentWithTracking | +| 6.2 | `PriceCalculatorService` | ✅ انجام شد | ۹ مرحله محاسبه | + +--- + +## بخش ۶: Excel Imports + +| # | کار | وضعیت | توضیحات | +|---|-----|-------|---------| +| 7.1 | `ShippingRatesImport` | ✅ انجام شد | ۴۰۴ رکورد import شد | +| 7.2 | `HistoricalShipmentsImport` | ⚠️ جزئی | فقط ۱ رکورد موفق (بقیه فرمول Excel هستند) | +| 7.3 | Artisan Command `ifnex:import:rates` | ✅ انجام شد | | +| 7.4 | Artisan Command `ifnex:import:shipments` | ✅ انجام شد | | + +--- + +## بخش ۷: پیکربندی + +| # | کار | وضعیت | توضیحات | +|---|-----|-------|---------| +| 8.1 | `config/ifnex.php` | ✅ انجام شد | تنظیمات اختصاصی | +| 8.2 | `config/cors.php` | ✅ انجام شد | | +| 8.3 | `.env.example` به‌روز | ✅ انجام شد | IFNEX_API_KEY, CORS_ALLOWED_ORIGINS, ... | +| 8.4 | سوییچ به Laravel 11 | ✅ انجام شد | Downgrade از v12 به v11 | +| 8.5 | نصب `morilog/jalali` v3 | ✅ انجام شد | | +| 8.6 | رفع خطای PSR-4 | ✅ انجام شد | SettingsPage.php | +| 8.7 | رفع خطای `MethodNotAllowedHttpException` | ✅ انجام شد | POST /admin/login | + +--- + +## بخش ۸: Code Review + +| # | کار | وضعیت | توضیحات | +|---|-----|-------|---------| +| 9.1 | بررسی `bootstrap/app.php` | ✅ انجام شد | مشکل: گروه `web` تعریف نشده | +| 9.2 | بررسی `AdminPanelProvider.php` | ✅ انجام شد |Middlewareهای طولانی به صورت دستی | +| 9.3 | بررسی `composer.json` | ✅ انجام شد | صحیح است | +| 9.4 | بررسی Routes | ✅ انجام شد | کافی برای حال حاضر | +| 9.5 | ارائه راه‌حل برای مشکلات | ✅ انجام شد | راه‌حل در STATUS.md ثبت شد | + +--- + +## بخش ۹: بلاک‌های فعلی + +| # | مشکل | اولویت | وضعیت | راه‌حل | +|---|------|--------|-------|--------| +| B1 | فایل اکسل List فقط ۲ ردیف داده واقعی دارد | 🔴 بالا | **بلاک** | دریافت فایل اصلی کامل از مشتری | +| B2 | گروه `web` در `bootstrap/app.php` تعریف نشده | 🔴 بالا | **بلاک** | اضافه کردن `$middleware->group('web', [...])` | +| B3 | فایل اکسل ترکینگ (Sheet1 + Refrence) موجود نیست | 🟡 متوسط | **بلاک** | دریافت از مشتری | +| B4 | پلاگین وردپرس IFNEX Bridge ساخته نشده | 🟡 متوسط | نشده | بعد از تست کامل API | +| B5 | تست دستی کامل Filament انجام نشده | 🟡 متوسط | نشده | بعد از رفع B2 | + +--- + +## بخش ۱۰: کارهای باقی‌مانده فاز ۰ + +| # | کار | اولویت | وضعیت | یادداشت | +|---|-----|--------|-------|---------| +| R1 | رفع مشکل middleware `web` group | 🔴 بالا | **نیاز به اصلاح** | اولویت اول | +| R2 | ساده کردن `AdminPanelProvider.php` | 🔴 بالا | **نیاز به اصلاح** | بعد از R1 | +| R3 | تست دستی کامل Filament | 🔴 بالا | pending | ورود + CRUD + RelationManagers | +| R4 | دریافت فایل اکсел اصلاح‌شده List | 🔴 بالا | **بلاک** | بدون این فایل، R5 امکان‌پذیر نیست | +| R5 | مهاجرت ۳۹۵۰ رکورد تاریخی | 🔴 بالا | **بلاک** | منتظر R4 | +| R6 | مهاجرت داده‌های ترکینگ دستی | 🟡 متوسط | **بلاک** | منتظر فایل اکسل ترکینگ | +| R7 | ساخت پلاگین وردپرس IFNEX Bridge | 🟡 متوسط | pending | بعد از R3 | +| R8 | تست کامل API Track با داده‌های واقعی | 🟡 متوسط | pending | بعد از R5 | +| R9 | تست نهایی فاز ۰ و تحویل به مشتری | 🟢 پایین | pending | بعد از تأیید همه موارد بالا | + +--- + +## بخش ۱۱: کارهای فاز ۱ (پس از تأیید فاز ۰) + +| # | کار | اولویت | وضعیت | +|---|-----|--------|-------| +| F1.1 | تست کامل `PriceCalculatorService` با داده‌های واقعی | 🔴 بالا | pending | +| F1.2 | تکمیل جدول `shipping_rates` | 🟡 متوسط | ۴۰۴ رکورد import شد | +| F1.3 | فرم ثبت سفارش آنلاین با ۹ ردیف کالای گمرکی | 🟡 متوسط | pending | +| F1.4 | تولید PDF: AWB، INVOICE، Label | 🟡 متوسط | pending | +| F1.5 | ماژول ایمپورت اکسل تعرفه‌ها | 🟢 پایین | pending | +| F1.6 | صفحه استعلام قیمت واقعی | 🟢 پایین | pending | + +--- + +## بخش ۱۲: کارهای فاز ۲ و ۳ (مرجع) + +### فاز ۲: اتوماسیون مالی +- [ ] اتصال به درگاه پرداخت (زرین‌پال) +- [ ] ایجاد جدول کیف پول و تراکنش‌ها +- [ ] شارژ اعتبار آنلاین و دستی +- [ ] کرون‌جاب بروزرسانی نرخ ارز +- [ ] سیستم کوپن و تخفیف‌ها +- [ ] فرمول محاسبه قیمت رقبا + +### فاز ۳: رهگیری زنده و عملیات +- [ ] راه‌اندازی VPS پل خارج (هلند/آلمان) +- [ ] اتصال به TrackingMore/17track API +- [ ] سیستم وب‌هوک +- [ ] مپ کردن کدهای رهگیری +- [ ] داشبورد مالی +- [ ] فرم نظرسنجی و رضایت مشتری +- [ ] پنل کاربری مشتریان در وردپرس + +--- + +## Legend + +| آیکون | معنی | +|--------|------| +| ✅ | انجام شده | +| 🔄 | در حال انجام | +| ⚠️ | جزئی/ناقص/بلاک | +| ❌ | لغو شده/خراب | +| ⏸️ | متوقف/منتظر | +| 🔴 بالا | اولویت بالا | +| 🟡 متوسط | اولویت متوسط | +| 🟢 پایین | اولویت پایین | + +--- + +## نحوه استفاده از این فایل + +۱. **در ابتدای هر جلسه:** این فایل را باز کنید و موارد `pending` را بررسی کنید. +۲. **بعد از انجام هر کار:** شماره کار را پیدا کنید و وضعیت را به‌روز کنید. +۳. **اگر بلاک جدیدی ایجاد شد:** در بخش «بلاک‌های فعلی» اضافه کنید. +۴. **اگر نیاز به Code Review دارید:** بخش ۹ را بررسی کنید. + +--- + +© 2026 VernaSoft Group. Internal use only. diff --git a/01_Documents/STATUS.md b/01_Documents/STATUS.md index eeac7b0..c8c3cb7 100644 --- a/01_Documents/STATUS.md +++ b/01_Documents/STATUS.md @@ -1,9 +1,9 @@ -# 🚨 STATUS.md — این فایل را اول بخوانید! +# 🚨 STATUS.md — این فایل را اول بخوانید -> **آخرین به‌روزرسانی:** August 2026 +> **آخرین به‌روزرسانی:** 2026-08-04 01:10 (Asia/Tehran) > **فاز در حال اجرا:** فاز ۰ (بنیان داده + ترکینگ دستی + مهاجرت داده‌های تاریخی) > **توسعه‌دهنده:** VernaSoft Group — Kazem Alghasi -> **وضعیت کلی پروژه:** در حال اجرا — بازنگری مسیر از نقشه‌ی ۳ فازی به ۴ فازی انجام شده است +> **وضعیت کلی پروژه:** در حال اجرا — Code Review کامل شد، نیاز به رفع نقص middleware است --- @@ -32,7 +32,7 @@ | PHP | 8.2+ | الزام لاراول ۱۱ | | MySQL | 8+ | برای پشتیبانی JSON columns | | WordPress | آخرین نسخه پایدار | با Polylang برای چندزبانه | -| پنل ادمین | Laravel Filament 3.x | برای سرعت توسعه | +| پنل ادمین | Laravel Filament 3.3.x | برای سرعت توسعه | | Frontend | وردپرس + قالب DHL-inspired | **کپی نکنید** — فقط الهام | ### معماری کلی @@ -42,58 +42,207 @@ │ (Frontend) │ پلاگین IFNEX Bridge │ (Backend) │ │ │ │ + Filament │ └─────────────────┘ └────────┬────────┘ - │ - ┌────────┴────────┐ - │ MySQL 8 │ - └─────────────────┘ - │ - (فاز ۳) │ - ┌────────┴────────┐ - │ VPS پل خارج │ - │ (هلند/آلمان) │ - └────────┬────────┘ - │ - ┌────────┴────────┐ - │ TrackingMore / │ - │ 17track API │ - └─────────────────┘ + │ + ┌────────┴────────┐ + │ MySQL 8 │ + └─────────────────┘ + │ + (فاز ۳) │ + ┌────────┴────────┐ + │ VPS پل خارج │ + │ (هلند/آلمان) │ + └────────┬────────┘ + │ + ┌────────┴────────┐ + │ TrackingMore / │ + │ 17track API │ + └─────────────────┘ ``` --- ## ✅ وضعیت فعلی کار -### کارهای انجام‌شده (تا آخرین به‌روزرسانی) -- [x] تحلیل کامل فایل‌های اکسل عملیاتی شرکت -- [x] شناسایی تناقضات PRD قدیمی با واقعیت اکسل (۴ زون به‌جای ۲، ۳ نوع سرویس به‌جای ۲، فیلدهای غایب VAT/Packing/Warehousing) -- [x] تدوین سند `Phase0_Proposal.md` (مرجع اصلی پروژه) -- [x] بازنگری نقشه‌ی راه از ۳ فازی به ۴ فازی -- [x] طراحی اسکیمای دیتابیس فاز ۰ (۶ جدول اصلی) -- [x] بازنویسی `README.md` با ساختار جدید -- [x] تدوین `EXCEL_ANALYSIS.md` (تحلیل کامل اکسل) +### کارهای انجام‌شده (تا تاریخ 2026-08-04) + +#### ۱. اسکیمای دیتابیس فاز ۰ (کامل) +- [x] بازنویسی migration `countries` با ۴ زون مجزا + `iso_code` + `is_active` +- [x] بازنویسی migration `shipments` مطابق Phase 0 (فیلدهای مالی + ۴ زون + nullable حقایل) +- [x] بازنویسی migration `shipping_rates` با enum `direction` و `type` (DOC_NORMAL/DOC_ECONOMY/PARCEL) +- [x] ایجاد migration `shipment_carrier_mappings` (جدول جدید — نگاشت چند شرکت حمل) +- [x] ایجاد migration `shipment_tracking_events` (جدول جدید — تایم‌لاین رویدادها) +- [x] ایجاد migration `system_settings` (جدول جدید — key-value برای VAT، نرخ ارز و...) +- [x] ایجاد migration `update_users` (افزودن `phone`, `role`, `is_active`) +- [x] حل مشکل `unsignedDecimal` در Laravel 11 (استفاده از `decimal()->unsigned()`) +- [x] حل مشکل FK constraints در migrations (استفاده از `disableForeignKeyConstraints()`) +- [x] اجرای همه migrations در MySQL بدون خطا + +#### ۲. مدل‌ها و Enums (کامل) +- [x] بازنویسی `Country.php` + روابط `fromShipments()` / `toShipments()` +- [x] بازنویسی `Shipment.php` + روابط `fromCountry()`, `toCountry()`, `items()`, `carrierMappings()`, `trackingEvents()` +- [x] بازنویسی `ShippingRate.php` + cast برای enums +- [x] بازنویسی `User.php` + فیلدهای `phone`, `role`, `is_active` + helper methods +- [x] ایجاد مدل‌های جدید: + - `ShipmentCarrierMapping.php` + - `ShipmentTrackingEvent.php` + - `SystemSetting.php` +- [x] ایجاد ۶ enum: + - `ShipmentDirection` (import/export) + - `ShipmentType` (DOC_NORMAL/DOC_ECONOMY/PARCEL) + - `ShipmentStatus` (processed/picked_up/in_transit/out_for_delivery/failed/delivered/returned) + - `CarrierCode` (DHL/FEDEX/UPS/ARAMEX/NAGHEL/EMX/APSITEX/IMPEX/OTHER) + - `TrackingSource` (manual/api_carrier/api_aggregator) + - `UserRole` (super_admin/tracking_operator/data_entry/customer) + +#### ۳. Seeders (کامل) +- [x] بازنویسی `CountriesTableSeeder` با ۲۳۳ کشور + ۴ زون + iso_code تصحیح‌شده + - تصحیح کدهای ISO نادرست (Bonaire BQ، Curacao CW، حذف Somaliland XS) + - حذف کشورهای تکراری ("United Arab Emirates" و "Rest of Dubai" هر دو با zone 9 و 10) +- [x] ایجاد `SystemSettingSeeder` (VAT 9%، packing 100000 IRR، profit_margin 1.25، نرخ ارزهای اولیه) +- [x] بازنویسی `DatabaseSeeder` (countries + system_settings + super_admin user) +- [x] اجرای seeders — ۲۳۲ کشور + ۷ setting + ۱ کاربر ادمین + +#### ۴. API ترکینگ (کامل) +- [x] ایجاد `TrackController` با JSON response کامل (shipment + carrier_mappings + timeline) +- [x] ایجاد `ApiKeyMiddleware` (Bearer token validation) +- [x] ثبت route `GET /api/v1/track/{awb_no}` با middleware API Key +- [x] تست‌های موفق: + - `GET /api/v1/track/TEST123` با API Key صحیح → **200 OK** + - بدون API Key → **401 Unauthorized** + - با API Key نامعتبر → **403 Forbidden** + +#### ۵. Filament Resources (کامل) +- [x] `CountryResource` — فرم ۴ زون + iso_code + is_active + فیلترها +- [x] `ShipmentResource` — فرم کامل با ۷ Section + RelationManagers + - Section: Route & Status + - Section: Weight & Dimensions + - Section: Financial Info (۱۱ فیلد مالی) + - Section: Sender Info + - Section: Receiver Info + - Section: Customs Items (Repeater ۹ ردیف) +- [x] `ShippingRateResource` — فرم ۱۰ zone + enum type/direction +- [x] `ShipmentItemResource` — CRUD کامل +- [x] RelationManagers: + - `CarrierMappingsRelationManager` (برای Shipment) + - `TrackingEventsRelationManager` (برای Shipment) +- [x] Pages: + - `ListShipments`, `CreateShipment`, `EditShipment`, `ViewShipment` + - `ListCountries`, `CreateCountry`, `EditCountry` + - `ListShippingRates`, `CreateShippingRate`, `EditShippingRate` + - `ListShipmentItems`, `CreateShipmentItem`, `EditShipmentItem` +- [x] `SettingsPage` (Page سفارشی برای System Settings) + +#### ۶. Services (کامل) +- [x] `TrackingService` — getTrackingTimeline, addTrackingEvent, getShipmentWithTracking +- [x] `PriceCalculatorService` — محاسبه قیمت کامل مطابق EXCEL_ANALYSIS.md + - مرحله ۱: وزن حجمی + - مرحله ۲: وزن قابل پرداخت + - مرحله ۳: استخراج زون از ۴ زون مجزا + - مرحله ۴: استخراج قیمت پایه از shipping_rates + - مرحله ۵: اعمال ضریب سود + - مرحله ۶: تبدیل درهم به ریال + - مرحله ۷: اضافه هزینه‌های جانبی + - مرحله ۸: اعمال VAT 9% + - مرحله ۹: ذخیره خروجی‌ها + +#### ۷. Imports (Excel) (کامل) +- [x] `ShippingRatesImport` — import از ۵ شیت (Import Rate, Export Rate, DocNor, DocEco, Parcel) + - ۴۰۴ رکورد import شد: DocNormal: ۷۴، DocEconomy: ۷۰، Parcel: ۲۶۰ +- [x] `HistoricalShipmentsImport` — import از شیت List فایل اصلی + - ⚠️ **مشکل:** فایل اکسل فقط ۲ ردیف داده واقعی دارد و بقیه فرمول‌های Excel هستند + - ۱ رکورد (980100010) موفقیت‌آمیز import شد +- [x] Artisan Commands: + - `ifnex:import:rates {path}` + - `ifnex:import:shipments {path}` + +#### ۸. سوییچ به Laravel 11 (کامل با نقص باقی‌مانده) +- [x] تغییر `composer.json`: `laravel/framework` از `^12.0` به `^11.0` +- [x] تغییر `morilog/jalali` از `^4.3` به `^3.0` (v4 وجود ندارد) +- [x] اجرای `composer update` — Laravel از v12.64.0 به v11.55.0 downgrade شد +- [x] رفع خطای PSR-4 در `SettingsPage.php` +- [x] رفع مشکل middleware در `bootstrap/app.php` برای Laravel 11 +- [x] رفع خطای `MethodNotAllowedHttpException` برای POST `/admin/login` + +#### ٩. پیکربندی (کامل) +- [x] اضافه کردن `IFNEX_API_KEY` به `.env.example` +- [x] اضافه کردن `CORS_ALLOWED_ORIGINS` به `.env.example` +- [x] اضافه کردن `IFNEX_TRACKING_RATE_LIMIT` به `.env.example` +- [x] ایجاد `config/ifnex.php` برای تنظیمات اختصاصی +- [x] تغییر `APP_LOCALE` به `fa` در `.env.example` +- [x] اضافه کردن `morilog/jalali` به `composer.json` +- [x] ایجاد `config/cors.php` + +--- + +### 🔴 Code Review Findings — نیاز به رفع + +| # | فایل | مشکل | وضعیت | راه‌حل | +|---|------|------|-------|-------| +| 1 | `bootstrap/app.php` | گروه `web` تعریف نشده | ⚠️ باز | اضافه کردن `$middleware->group('web', [...])` | +| 2 | `AdminPanelProvider.php` | Middlewareهای طولانی به صورت لیست دستی | ⚠️ بهبودنیاز | استفاده از گروه `web` به جای لیست دستی | +| 3 | `bootstrap/app.php` | `statefulApi` تعریف نشده | 🟡 آینده | برای احراز هویت SPA در فاز بعد لازم است | + +**توضیح مشکل #1:** +در Laravel 11، گروه `web` به‌صورت خودکار تعریف نمی‌شود. Filament برای لاگین نیاز به Session, Cookies, CSRF, ShareErrors دارد. فعلاً کار می‌کند چون در `AdminPanelProvider.php` همه Middlewareها به صورت لیست دستی نوشته شده‌اند، اما این روش **خوب نیست** و مطابق Laravel 11 conventions نیست. اگر جایی از `web` group استفاده شود، کار نمی‌کند. + +**راه‌حل پیشنهادی برای `bootstrap/app.php`:** +```php +->withMiddleware(function (Middleware $middleware): void { + $middleware->api(prepend: \Illuminate\Http\Middleware\HandleCors::class); + + $middleware->group('web', [ + \Illuminate\Cookie\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ]); +}) +``` + +**راه‌حل پیشنهادی برای `AdminPanelProvider.php` بعد از تعریف گروه `web`:** +```php +->middleware([ + 'web', + \Filament\Http\Middleware\Authenticate::class, +]) +``` + +--- + +### 🔴 مشکل حیاتی — مهاجرت داده‌های تاریخی + +**مشکل:** فایل اکسل `4_5989927490271846355.xlsx` که در `storage/app/public` قرار دارد، شیت `List` را فقط برای **۲ ردیف اول داده واقعی** دارد و از ردیف ۳ به بعد **فرمول‌های Excel** است (مثل `=IF(B4<>"...",...)`). + +**تأثیر:** فقط ۱ رکورد (AWB 980100010) موفقیت‌آمیز import شد. ۳۹۵۰ رکورد دیگر قابل import نیستند مگر اینکه: +1. فایل اکسل اصلی کامل (بدون فرمول) تحویل داده شود، **یا** +2. فرمول‌های Excel اعمال شوند تا مقادیر محاسبه‌شده استخراج شوند + +**وضعیت:** **بلاک** — مهاجرت ۳۹۵۰ رکورد نیاز به فایل اکسل اصلاح‌شده دارد. + +--- ### کارهای در دست اقدام (فاز ۰) -- [ ] نصب و راه‌اندازی پروژه‌ی لاراول ۱۱ در پوشه `04_Laravel` -- [ ] نصب Filament و احراز هویت ادمین -- [ ] نوشتن Migration ها برای ۶ جدول اصلی: - - [ ] `countries` (با ۴ زون مجزا) - - [ ] `shipments` (فاز ۰ — فیلدهای حداقلی) - - [ ] `shipment_carrier_mappings` - - [ ] `shipment_tracking_events` - - [ ] `system_settings` - - [ ] `users` (با نقش‌های super_admin/tracking_operator/data_entry/customer) -- [ ] Seeder کشورها (۲۳۳ کشور با ۴ زون از شیت Zone اکسل) -- [ ] API ترکینگ: `GET /api/track/{awb_no}` -- [ ] پنل Filament با UX اپراتور ترکینگ (افزودن رویداد سریع) -- [ ] پلاگین وردپرس IFNEX Bridge با شورت‌کد `[ifnex_tracking_form]` -- [ ] اسکریپت مهاجرت ۳۹۵۰ رکورد تاریخی از شیت List اکسل -- [ ] راه‌اندازی وردپرس روی هاست مشتری -- [ ] طراحی لندینگ پیج DHL-inspired (بدون کپی) -- [ ] تست نهایی فاز ۰ و تحویل به مشتری + +| اولویت | کار | وضعیت | توضیحات | +|--------|-----|-------|---------| +| 🔴 بالا | رفع مشکل middleware `web` group | نیاز به اصلاح | Code Review انجام شد، راه‌حل مشخص است | +| 🔴 بالا | تست کامل دستی Filament (ورود + CRUD) | نیاز به تست | بعد از رفع مشکل middleware انجام شود | +| 🔴 بالا | دریافت فایل اکسل اصلی List اصلاح‌شده | **بلاک** | بدون این فایل، مهاجرت ۳۹۵۰ رکورد امکان‌پذیر نیست | +| 🔴 بالا | مهاجرت ۳۹۵۰ رکورد تاریخی | **بلاک** | منتظر فایل اصلاح‌شده | +| 🟡 متوسط | مهاجرت داده‌های ترکینگ (Sheet1 + Refrence) | بلاک | فایل اکسل ترکینگ موجود نیست | +| 🟡 متوسط | ساخت پلاگین وردپرس IFNEX Bridge | نشده | بعد از تست کامل API | +| 🟡 متوسط | تست API Track با نمونه‌های بیشتر | انجام شد | ۳ تست پشت سر هم موفق | +| 🟢 پایین | نصب Dompdf/Snappy برای تولید PDF (فاز ۱) | نشده | بعد از تأیید فاز ۰ | +| 🟢 پایین | پیاده‌سازی فرم استعلام قیمت در وردپرس | نشده | بعد از تأیید فاز ۰ | + +--- ### کارهای فاز ۱ (پس از تأیید فاز ۰) -- [ ] موتور قیمت‌گذاری کامل (PriceCalculatorService) -- [ ] جدول `shipping_rates` با نرخ‌های Import/Export + +- [ ] موتور قیمت‌گذاری کامل (PriceCalculatorService) — نوشته شد، نیاز به تست با داده‌های واقعی +- [ ] جدول `shipping_rates` — ۴۰۴ رکورد import شد، نیاز به تکمیل - [ ] فرم ثبت سفارش آنلاین با ۹ ردیف کالای گمرکی - [ ] تولید PDF: AWB، INVOICE، Label مطابق قالب اکسل - [ ] ماژول ایمپورت اکسل تعرفه‌ها @@ -103,30 +252,28 @@ ## 🚫 خط قرمزها (DO NOT) — هرگز این کارها را نکن -این قوانین بر اساس تجربه و تصمیمات تأییدشده‌ی مشتری تنظیم شده‌اند. نقض هر کدام = بازگشت به عقب و کار مضاعف. - ### 🚫 اسکیمای دیتابیس -- **NEVER** جدول `countries` را به ۲ زون برگردانی — ۴ زون مجزا (export_parcel, export_doc, import_parcel, import_doc) الزامی است. هر کشور برای پارسل و داکیومنت زون‌های متفاوتی دارد (مثلاً افغانستان: پارسل=۷، داکیومنت=۵). -- **NEVER** فقط ۲ نوع سرویس (DOCUMENT/NON DOC) پیاده کن — ۳ نوع الزامی است: `DOC_NORMAL`، `DOC_ECONOMY`، `PARCEL` (مطابق شیت‌های DocNor، DocEco، Parcel در اکسل). -- **NEVER** فیلد `forwarder_track_id` را به‌عنوان فیلد واحد در `shipments` نگه دار — باید جدول جداگانه `shipment_carrier_mappings` ساخته شود، چون هر مرسوله ممکن است با چند شرکت حمل مرتبط باشد (مثلاً اول DHL سپس Aramex). -- **NEVER** فیلدهای مالی مهم (VAT، Domestic Pickup، Domestic Delivery، Warehousing Cost، Extra Service، Packing Cost) را حذف کن — حتی اگر در فاز ۰ استفاده نمی‌شوند، باید در Migration آماده باشند. -- **NEVER** فیلد `status` در `shipments` را به String تغییر دهی — Enum یکپارچه‌تر و امن‌تر است. +- **NEVER** جدول `countries` را به ۲ زون برگردانی — ۴ زون مجزا (export_parcel, export_doc, import_parcel, import_doc) الزامی است. +- **NEVER** فقط ۲ نوع سرویس پیاده کن — ۳ نوع الزامی است: `DOC_NORMAL`, `DOC_ECONOMY`, `PARCEL`. +- **NEVER** فیلد `forwarder_track_id` را به‌عنوان فیلد واحد در `shipments` نگه دار — باید `shipment_carrier_mappings` استفاده شود. +- **NEVER** فیلدهای مالی مهم (VAT، Domestic Pickup، Domestic Delivery، Warehousing Cost، Extra Service، Packing Cost) را حذف کن. +- **NEVER** فیلد `status` در `shipments` را به String تغییر دهی — Enum استفاده کن. ### 🚫 معماری -- **NEVER** ترکینگ را در وردپرس پیاده کن — همیشه در لاراول. وردپرس فقط نمایش می‌دهد. اگر این کار را بکنی، در فاز ۳ باید تمام داده‌ها را به لاراول مهاجرت دهی (دوباره‌کاری). +- **NEVER** ترکینگ را در وردپرس پیاده کن — همیشه در لاراول. - **NEVER** در وردپرس پردازش داده‌ی سفارش انجام دهی — تمام فرم‌ها از طریق پلاگین IFNEX Bridge به لاراول ارسال می‌شوند. -- **NEVER** API لاراول را بدون API Key، Rate Limiting و CORS whitelist بگذاری — امنیت حیاتی است. +- **NEVER** API لاراول را بدون API Key، Rate Limiting و CORS whitelist بگذاری. - **NEVER** از CORS `*` استفاده کنی — فقط دامنه‌ی تولیدی وردپرس باید whitelist شود. -- **NEVER** تاریخ‌ها را به شمسی در دیتابیس ذخیره کنی — همیشه به‌صورت `timestamp` میلادی. تبدیل به شمسی فقط در لایه‌ی نمایش (با `morilog/jalali`). +- **NEVER** تاریخ‌ها را به شمسی در دیتابیس ذخیره کنی — همیشه timestamp میلادی. ### 🚫 طراحی و کپی‌رایت -- **NEVER** از رنگ، لوگو یا عناصر هویت بصری DHL کپی کنی — نقض کپی‌رایت. الهام از چیدمان و UX مجاز است. -- **NEVER** خروجی PDF (AWB، Invoice، Label) را به فارسی بسازی — مطابق اکسل اصلی، PDF باید انگلیسی باشد. اما پنل ادمین و رابط کاربری فرانت‌اند فارسی است. +- **NEVER** از رنگ، لوگو یا عناصر هویت بصری DHL کپی کنی. +- **NEVER** خروجی PDF (AWB، Invoice، Label) را به فارسی بسازی — انگلیسی باشد. ### 🚫 فرآیند -- **NEVER** فایل `.env` را در Git کامیت کنی — در `.gitignore` است. +- **NEVER** فایل `.env` را در Git کامیت کنی. - **NEVER** `APP_DEBUG=true` را در محیط تولید بگذاری. -- **NEVER** اسکوپ فاز ۰ را بدون Change Request رسمی تغییر دهی — اگر مشتری درخواست افزودن قابلیت کرد، قیمت‌گذاری جداگانه لازم است. +- **NEVER** اسکوپ فاز ۰ را بدون Change Request رسمی تغییر دهی. - **NEVER** فاز ۱ را قبل از تأیید رسمی فاز ۰ توسط مشتری شروع کنی. --- @@ -134,54 +281,36 @@ ## ❓ سوالات متداول (FAQ) ### س: کدام نسخه لاراول استفاده کنم؟ -**ج:** لاراول ۱۱. اگر در PRD_v2.md نوشته «Laravel 10+» یا در Roadmap نوشته «Laravel 11»، نسخه نهایی **۱۱** است. +**ج:** لاراول **۱۱**. این نسخه در `STATUS.md` قفل شده و در `composer.json` هم تنظیم شده است. -### س: آیا PRD_v2.md هنوز معتبر است؟ -**ج:** بخش‌های کلی آن (معماری Headless، توضیح کسب‌وکار، VPS پل) معتبرند. اما بخش‌های زیر قدیمی و اشتباه هستند: -- اسکیمای دیتابیس (۴.۱ تا ۴.۴) — به ۴ زون و ۳ نوع سرویس به‌روز نشده -- فازبندی — باید ۴ فازی باشد نه ۳ فازی -- ادعای «فاز ۱ تکمیل شده» — نادرست، فاز ۰ هنوز در حال اجراست -- فیلدهای مالی — VAT، Warehousing Cost، Domestic Pickup/Delivery غایب +### س: مهاجرت داده‌هایHistorical چقدر مهم است؟ +**ج:** بسیار مهم. ۳۹۵۰ رکورد در شیت List وجود دارد. فایل اکسل موجود در `storage/app/public` فرمول‌های Excel دارد و فقط ۲ ردیف داده واقعی است. باید فایل اصلی کامل از مشتری دریافت شود. -برای اسکیمای دیتابیس، فقط به بخش ۶ `Phase0_Proposal.md` اعتماد کن. +### س: چه کارهایی در این جلسه انجام شد؟ +**ج:** +1. بازنویسی کامل اسکیمای دیتابیس (۷ migration جدید) +2. بازنویسی تمام Model‌ها + ایجاد ۶ Enum +3. بازنویسی Seeders (countries + system_settings) +4. ایجاد API TrackController + ApiKeyMiddleware +5. بازنویسی کامل Filament Resources + RelationManagers + SettingsPage +6. ایجاد Services (TrackingService + PriceCalculatorService) +7. ایجاد Imports (ShippingRatesImport + HistoricalShipmentsImport) +8. ایجاد Artisan Commands (ifnex:import:rates, ifnex:import:shipments) +9. سوییچ به Laravel 11 + رفع همه خطاهای مربوطه +10. import ۴۰۴ shipping rate + ۱ shipment تاریخی +11. **Code Review کامل** — شناسایی مشکل middleware و ارائه راه‌حل -### س: چرا ترکینگ در لاراول است نه وردپرس؟ -**ج:** چون در فاز ۳ قرار است API ترکینگ واقعی (TrackingMore/17track) متصل شود. اگر الان ترکینگ در وردپرس باشد، در فاز ۳ باید تمام داده‌ها به لاراول مهاجرت داده شوند. با ساخت آن در لاراول از ابتدا، در فاز ۳ فقط یک کلاس `TrackingSyncService` اضافه می‌شود و هیچ چیز دیگر تغییر نمی‌کند. این تصمیم در جلسه با مشتری تأیید شده است. - -### س: چرا ۴ زون مجزا لازم است؟ -**ج:** فایل اکسل عملیاتی نشان می‌دهد همان کشور برای پارسل و داکیومنت زون‌های متفاوتی دارد. مثلاً: -- افغانستان: پارسل=۷، داکیومنت=۵ -- آلبانی: پارسل=۳، داکیومنت=۷ -- استرالیا: پارسل=۷، داکیومنت=۶ - -اگر فقط ۲ زون (export/import) داشته باشیم، موتور قیمت‌گذاری برای DOCUMENTها اشتباه محاسبه می‌کند. - -### س: چرا ۳ نوع سرویس داریم نه ۲؟ -**ج:** فایل اکسل شیت‌های جداگانه دارد برای DocNor (Document Normal)، DocEco (Document Economy) و Parcel. هر کدام جدول قیمت جداگانه. پس `type` در `shipments` باید enum با سه مقدار باشد: `DOC_NORMAL`, `DOC_ECONOMY`, `PARCEL`. - -### س: کدام فایل اکسل عملیاتی است؟ -**ج:** دو فایل: -- **`4_5989927490271846355.xlsx`** — فایل اصلی عملیاتی شرکت با شیت‌های Form, List, COUNTRIES, AWB, INVOICE + label, label, Import Rate, Export Rate, Zone, DocNor, Parcel, DocEco, Assumptions, DATES -- **`Data entry 2026-06-28.xlsx`** — فایل ترکینگ دستی روزانه با شیت‌های Sheet1, Refrence, Paste, copy, Delivered, test - -برای تحلیل کامل هر شیت، فایل `EXCEL_ANALYSIS.md` را بخوان. - -### س: مهاجرت داده‌های تاریخی چقدر مهم است؟ -**ج:** بسیار مهم. ۳۹۵۰ رکورد در شیت List وجود دارد از سال ۲۰۲۰ تا الان. این داده‌ها باید به جدول `shipments` مهاجرت داده شوند. بدون این کار، مشتریان قدیمی نمی‌توانند تاریخچه ببینند و اعتماد به سیستم جدید کاهش می‌یابد. - -### س: آیا باید VPS پل را در فاز ۰ راه‌اندازی کنم؟ -**ج:** خیر. VPS پل مخصوص فاز ۳ است. در فاز ۰ ترکینگ کاملاً دستی است (اپراتور در پنل Filament رویداد اضافه می‌کند). اما اسکیمای دیتابیس باید به‌گونه‌ای باشد که در فاز ۳ بتوان به‌سادگی API را اضافه کرد (به فیلد `source` در `shipment_tracking_events` و `last_synced_at` در `shipment_carrier_mappings` دقت کن). - -### س: مشتری چه انتظاری از فاز ۰ دارد؟ -**ج:** مشتری در جلسه صراحتاً گفت: «اول سایت بالا بیاید و ترکینگ دستی حل شود، بقیه بعد.» یعنی: -۱. وب‌سایت وردپرس کامل آنلاین شود -۲. مشتری نهایی بتواند با کد AWB، تایم‌لاین ترکینگ را ببیند -۳. اپراتور به‌جای اکسل، از پنل Filament استفاده کند - -این سه هدف، حداقل قابل‌قبول برای تحویل فاز ۰ است. - -### س: اگر باگی دیدم یا مشکل پیدا کردم چه کنم؟ -**ج:** اول `EXCEL_ANALYSIS.md` و بخش «ریسک‌ها» در `Phase0_Proposal.md` را چک کن. اگر حل نشد، در گزارش کار (worklog) توضیح بده و به توسعه‌دهنده اصلی (Kazem) اطلاع بده. +### س: چه کارهایی باقی مانده؟ +**ج:** +1. رفع مشکل middleware `web` group در `bootstrap/app.php` +2. ساده کردن `AdminPanelProvider.php` بعد از تعریف گروه `web` +3. تست دستی Filament (ورود + CRUD) +4. دریافت فایل اکسل اصلاح‌شده List +5. import ۳۹۵۰ رکورد تاریخی +6. ساخت پلاگین وردپرس IFNEX Bridge +7. مهاجرت داده‌های ترکینگ دستی +8. تست کامل API Track +9. تولید PDF (فاز ۱) --- @@ -193,37 +322,35 @@ cd 04_Laravel composer install cp .env.example .env php artisan key:generate -php artisan migrate -php artisan db:seed --class=CountrySeeder +php artisan migrate --force +php artisan db:seed --force php artisan serve ``` -### ایجاد Model + Migration + Resource (Filament) +### Artisan Commands سفارشی ```bash -php artisan make:model Shipment -m -php artisan make:filament-resource Shipment +# Import shipping rates +php artisan ifnex:import:rates storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx + +# Import historical shipments +php artisan ifnex:import:shipments storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx ``` -### ایجاد API Controller +### تست API Track ```bash -php artisan make:controller Api/TrackController --api +# با API Key صحیح +curl -H "Authorization: Bearer ifnex-local-dev-key" http://localhost:8000/api/v1/track/TEST123 + +# بدون API Key +curl http://localhost:8000/api/v1/track/TEST123 + +# با API Key نامعتبر +curl -H "Authorization: Bearer wrong-key" http://localhost:8000/api/v1/track/TEST123 ``` -### اجرای تست +### پشتیبان‌گیری از دیتابیس ```bash -php artisan test -php artisan serve # سپس در مرورگر: http://localhost:8000/api/track/980103619 -``` - -### مهاجرت داده‌های تاریخی (یک‌بار) -```bash -php artisan ifnex:migrate-historical-data -# این دستور باید ساخته شود — اسکریپت مخصوص خواندن شیت List اکسل -``` - -### پشتیبان‌گیری از دیتابیس (هر روز) -```bash -mysqldump -u root -p ifnex > backups/ifnex_$(date +%Y%m%d).sql +mysqldump -u root -p ifnex_db > backups/ifnex_$(date +%Y%m%d).sql ``` --- @@ -231,21 +358,21 @@ mysqldump -u root -p ifnex > backups/ifnex_$(date +%Y%m%d).sql ## 📂 ساختار پوشه‌های پروژه (پس از تکمیل فاز ۰) ``` -ifnex/ +IFNEX-Logistics/ ├── 01_Documents/ -│ ├── STATUS.md ⭐ این فایل — اول بخوان -│ ├── Phase0_Proposal.md ⭐ مرجع اصلی پروژه -│ ├── EXCEL_ANALYSIS.md ⭐ تحلیل فایل‌های اکسل -│ ├── PRD_v2.md (قدیمی — مرجع تاریخی) -│ ├── Project_Roadmap.md (قدیمی — مرجع تاریخی) -│ └── AI_AGENT_GUIDE.md (راهنمای مخصوص AI Agents — اختیاری) +│ ├── STATUS.md ⭐ این فایل — اول بخوان +│ ├── Phase0_Proposal.md ⭐ مرجع اصلی پروژه +│ ├── EXCEL_ANALYSIS.md ⭐ تحلیل فایل‌های اکسل +│ ├── PRD_v2.md (قدیمی — مرجع تاریخی) +│ ├── Project_Roadmap.md (قدیمی — مرجع تاریخی) +│ └── IFNEX_Phase0_Checklist.md (چک‌لیست فاز ۰) │ ├── 02_Design/ -│ └── Assets/ (لوگوها، آیکون‌ها، فایل‌های فیگما) +│ └── Assets/ (لوگوها، آیکون‌ها، فایل‌های فیگما) │ ├── 03_WordPress/ │ └── wp-content/plugins/ -│ └── ifnex-bridge/ (پلاگین اختصاصی) +│ └── ifnex-bridge/ (پلاگین اختصاصی) │ ├── ifnex-bridge.php │ ├── includes/ │ │ ├── api-client.php @@ -263,32 +390,80 @@ ifnex/ │ │ │ ├── ShipmentCarrierMapping.php │ │ │ ├── ShipmentTrackingEvent.php │ │ │ ├── SystemSetting.php +│ │ │ ├── ShippingRate.php +│ │ │ ├── ShipmentItem.php │ │ │ └── User.php +│ │ ├── Enums/ +│ │ │ ├── ShipmentDirection.php +│ │ │ ├── ShipmentType.php +│ │ │ ├── ShipmentStatus.php +│ │ │ ├── CarrierCode.php +│ │ │ ├── TrackingSource.php +│ │ │ └── UserRole.php │ │ ├── Services/ -│ │ │ ├── TrackingService.php (فاز ۰) -│ │ │ ├── PriceCalculatorService.php (فاز ۱) -│ │ │ └── TrackingSyncService.php (فاز ۳) -│ │ ├── Http/Controllers/Api/ -│ │ │ └── TrackController.php +│ │ │ ├── TrackingService.php +│ │ │ └── PriceCalculatorService.php +│ │ ├── Http/ +│ │ │ ├── Controllers/ +│ │ │ │ └── Api/ +│ │ │ │ ├── TrackController.php +│ │ │ │ └── PricingController.php +│ │ │ └── Middleware/ +│ │ │ └── ApiKeyMiddleware.php │ │ ├── Imports/ -│ │ │ ├── ShippingRatesImport.php (فاز ۱) -│ │ │ └── HistoricalShipmentsImport.php (فاز ۰) -│ │ └── Filament/ -│ │ └── Resources/ -│ │ ├── CountryResource.php -│ │ ├── ShipmentResource.php -│ │ └── Pages/ -│ │ └── AddTrackingEvent.php (UX اختصاصی اپراتور) +│ │ │ ├── ShippingRatesImport.php +│ │ │ ├── HistoricalShipmentsImport.php +│ │ │ └── RateSheetImport.php +│ │ ├── Console/ +│ │ │ └── Commands/ +│ │ │ ├── ImportShippingRates.php +│ │ │ ├── ImportHistoricalData.php +│ │ │ └── DebugImportCommand.php +│ │ ├── Filament/ +│ │ │ ├── Resources/ +│ │ │ │ ├── CountryResource.php +│ │ │ │ ├── ShipmentResource.php +│ │ │ │ │ ├── Pages/ +│ │ │ │ │ │ ├── CreateShipment.php +│ │ │ │ │ │ ├── EditShipment.php +│ │ │ │ │ │ ├── ViewShipment.php +│ │ │ │ │ │ └── ListShipments.php +│ │ │ │ │ └── RelationManagers/ +│ │ │ │ │ ├── CarrierMappingsRelationManager.php +│ │ │ │ │ └── TrackingEventsRelationManager.php +│ │ │ │ ├── ShippingRateResource.php +│ │ │ │ └── ShipmentItemResource.php +│ │ │ └── Pages/ +│ │ │ └── IfnexSettingsPage.php +│ │ └── Providers/ +│ │ ├── AppServiceProvider.php +│ │ └── Filament/ +│ │ └── AdminPanelProvider.php │ ├── database/ │ │ ├── migrations/ +│ │ │ ├── 2026_08_02_000001_create_countries_table.php +│ │ │ ├── 2026_08_02_000002_create_shipments_table.php +│ │ │ ├── 2026_08_02_000003_create_shipping_rates_table.php +│ │ │ ├── 2026_08_02_000004_create_shipment_carrier_mappings_table.php +│ │ │ ├── 2026_08_02_000005_create_shipment_tracking_events_table.php +│ │ │ ├── 2026_08_02_000006_create_system_settings_table.php +│ │ │ └── 2026_08_02_000007_update_users_table.php │ │ └── seeders/ -│ │ └── CountrySeeder.php -│ ├── routes/api.php +│ │ ├── CountriesTableSeeder.php +│ │ ├── SystemSettingSeeder.php +│ │ └── DatabaseSeeder.php +│ ├── routes/ +│ │ ├── web.php +│ │ └── api.php │ ├── config/ -│ │ └── ifnex.php (تنظیمات اختصاصی) +│ │ ├── ifnex.php +│ │ └── cors.php +│ ├── bootstrap/ +│ │ └── app.php │ └── .env.example │ -├── README.md (نسخه به‌روز ۲) +├── README.md +├── composer.json └── .gitignore ``` @@ -303,9 +478,10 @@ ifnex/ ۳. **`Phase0_Proposal.md`** را کامل بخوان — مرجع اصلی پروژه ۴. **`README.md`** ریشه را بخوان — برای نمای کلی ۵. کد موجود در `04_Laravel` را بررسی کن — ببین چه چیزی نوشته شده -۶. با کاربر (Kazem) هماهنگ کن — بپرس کدام کار را باید ادامه دهی +۶. **اولویت اول:** رفع مشکل middleware در `bootstrap/app.php` و `AdminPanelProvider.php` (راه‌حل در بالای این فایل آمده) +۷. با کاربر (Kazem) هماهنگ کن — بپرس کدام کار را باید ادامه دهی -**سپس کار را ادامه بده. موفق باشی! 🚀** +**مهم‌ترین کار بعدی:** رفع مشکل middleware + تست دستی Filament. --- diff --git a/04_Laravel/.env.example b/04_Laravel/.env.example index c0660ea..64c2888 100644 --- a/04_Laravel/.env.example +++ b/04_Laravel/.env.example @@ -4,8 +4,8 @@ APP_KEY= APP_DEBUG=true APP_URL=http://localhost -APP_LOCALE=en -APP_FALLBACK_LOCALE=en +APP_LOCALE=fa +APP_FALLBACK_LOCALE=fa APP_FAKER_LOCALE=en_US APP_MAINTENANCE_DRIVER=file @@ -63,3 +63,7 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +IFNEX_API_KEY= +CORS_ALLOWED_ORIGINS=http://localhost,http://127.0.0.1 +IFNEX_TRACKING_RATE_LIMIT=60 diff --git a/04_Laravel/app/Console/Commands/DebugImportCommand.php b/04_Laravel/app/Console/Commands/DebugImportCommand.php new file mode 100644 index 0000000..37f04d2 --- /dev/null +++ b/04_Laravel/app/Console/Commands/DebugImportCommand.php @@ -0,0 +1,21 @@ +argument('path'); + Excel::import(new DebugImport(), $path); + + return 0; + } +} diff --git a/04_Laravel/app/Console/Commands/ImportHistoricalData.php b/04_Laravel/app/Console/Commands/ImportHistoricalData.php new file mode 100644 index 0000000..7976fc2 --- /dev/null +++ b/04_Laravel/app/Console/Commands/ImportHistoricalData.php @@ -0,0 +1,32 @@ +argument('path'); + + if (!file_exists($path)) { + $this->error("File not found: $path"); + + return 1; + } + + $this->info('Starting historical shipments import...'); + + Excel::import(new HistoricalShipmentsImport(), $path); + + $this->info('Historical shipments import completed.'); + + return 0; + } +} diff --git a/04_Laravel/app/Console/Commands/ImportShippingRates.php b/04_Laravel/app/Console/Commands/ImportShippingRates.php new file mode 100644 index 0000000..c31454f --- /dev/null +++ b/04_Laravel/app/Console/Commands/ImportShippingRates.php @@ -0,0 +1,32 @@ +argument('path'); + + if (!file_exists($path)) { + $this->error("File not found: $path"); + + return 1; + } + + $this->info('Starting shipping rates import...'); + + Excel::import(new ShippingRatesImport(), $path); + + $this->info('Shipping rates import completed.'); + + return 0; + } +} diff --git a/04_Laravel/app/Enums/CarrierCode.php b/04_Laravel/app/Enums/CarrierCode.php new file mode 100644 index 0000000..f25dec5 --- /dev/null +++ b/04_Laravel/app/Enums/CarrierCode.php @@ -0,0 +1,16 @@ +fillForms(); + } + + protected function getFormSchema(): array + { + return [ + Forms\Components\TextInput::make('vat_rate') + ->label('VAT Rate') + ->required() + ->numeric() + ->step(0.01) + ->default(0.09) + ->helperText('Example: 0.09 for 9%') + ->live(), + Forms\Components\TextInput::make('packing_cost_default') + ->label('Default Packing Cost (IRR)') + ->required() + ->numeric() + ->default(100000), + Forms\Components\TextInput::make('profit_margin') + ->label('Profit Margin') + ->required() + ->numeric() + ->step(0.01) + ->default(1.25) + ->helperText('Example: 1.25 for 25% margin'), + Forms\Components\TextInput::make('aed_to_irr') + ->label('AED to IRR') + ->required() + ->numeric() + ->default(455000), + Forms\Components\TextInput::make('usd_to_irr') + ->label('USD to IRR') + ->required() + ->numeric() + ->default(0), + Forms\Components\TextInput::make('cny_to_irr') + ->label('CNY to IRR') + ->required() + ->numeric() + ->default(0), + Forms\Components\TextInput::make('eur_to_irr') + ->label('EUR to IRR') + ->required() + ->numeric() + ->default(0), + ]; + } + + protected function mutateFormDataBeforeSave(array $data): array + { + $userId = auth()->id(); + + foreach ($data as $key => $value) { + if (is_numeric($value)) { + SystemSetting::updateOrCreate( + ['key' => $key], + ['value' => $value, 'updated_by' => $userId] + ); + } + } + + return []; + } + + protected function fillForms(): void + { + $settings = SystemSetting::all()->pluck('value', 'key')->toArray(); + + $defaults = [ + 'vat_rate' => 0.09, + 'packing_cost_default' => 100000, + 'profit_margin' => 1.25, + 'aed_to_irr' => 455000, + 'usd_to_irr' => 0, + 'cny_to_irr' => 0, + 'eur_to_irr' => 0, + ]; + + $this->data = array_merge($defaults, $settings); + } + + protected function getFormStatePath(): string + { + return 'data'; + } +} diff --git a/04_Laravel/app/Filament/Pages/SettingsPage.php b/04_Laravel/app/Filament/Pages/SettingsPage.php deleted file mode 100644 index 8972345..0000000 --- a/04_Laravel/app/Filament/Pages/SettingsPage.php +++ /dev/null @@ -1,80 +0,0 @@ -record = SystemSetting::first(); - } - - // ساختار فرم - public function form(Form $form): Form - { - return $form - ->schema([ - TextInput::make('dirham_rate') - ->label('نرخ درهم') - ->numeric() - ->required() - ->prefix('ریال'), - - TextInput::make('dollar_rate') - ->label('نرخ دلار') - ->numeric() - ->prefix('ریال'), - - TextInput::make('yuan_rate') - ->label('نرخ یوان') - ->numeric() - ->prefix('ریال'), - - TextInput::make('euro_rate') - ->label('نرخ یورو') - ->numeric() - ->prefix('ریال'), - - TextInput::make('profit_percent') - ->label('ضریب سود شرکت') - ->numeric() - ->required() - ->suffix('ضرب') - ->default(1.25), - ]) - ->statePath('record'); - } - - // دکمه ذخیره - public function save(): void - { - $this->record->save(); - - Notification::make() - ->title('ذخیره شد') - ->body('تنظیمات نرخ ارز با موفقیت آپدیت شدند.') - ->success() - ->send(); - } -} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Resources/CountryResource.php b/04_Laravel/app/Filament/Resources/CountryResource.php index 66e8f42..63ad914 100644 --- a/04_Laravel/app/Filament/Resources/CountryResource.php +++ b/04_Laravel/app/Filament/Resources/CountryResource.php @@ -14,9 +14,11 @@ class CountryResource extends Resource { protected static ?string $model = Country::class; - // تغییر آیکون به یک نماد مناسب‌تر برای کشورها protected static ?string $navigationIcon = 'heroicon-o-globe-alt'; + protected static ?string $navigationLabel = 'Countries'; + protected static ?string $navigationGroup = 'Settings'; + public static function form(Form $form): Form { return $form @@ -25,12 +27,42 @@ class CountryResource extends Resource ->required() ->maxLength(255) ->label('Country Name'), - Forms\Components\TextInput::make('export_zone') + Forms\Components\TextInput::make('iso_code') ->required() - ->maxLength(50), - Forms\Components\TextInput::make('import_zone') - ->required() - ->maxLength(50), + ->maxLength(2) + ->unique(ignoreRecord: true) + ->label('ISO Code'), + Forms\Components\Grid::make() + ->schema([ + Forms\Components\TextInput::make('export_zone_parcel') + ->required() + ->numeric() + ->minValue(1) + ->maxValue(10) + ->label('Export Zone (Parcel)'), + Forms\Components\TextInput::make('export_zone_doc') + ->required() + ->numeric() + ->minValue(1) + ->maxValue(10) + ->label('Export Zone (Doc)'), + Forms\Components\TextInput::make('import_zone_parcel') + ->required() + ->numeric() + ->minValue(1) + ->maxValue(10) + ->label('Import Zone (Parcel)'), + Forms\Components\TextInput::make('import_zone_doc') + ->required() + ->numeric() + ->minValue(1) + ->maxValue(10) + ->label('Import Zone (Doc)'), + ]) + ->columns(4), + Forms\Components\Toggle::make('is_active') + ->label('Active') + ->default(true), ]); } @@ -38,13 +70,16 @@ class CountryResource extends Resource { return $table ->columns([ - Tables\Columns\TextColumn::make('name')->searchable()->label('Country'), - Tables\Columns\TextColumn::make('export_zone')->searchable(), - Tables\Columns\TextColumn::make('import_zone')->searchable(), - Tables\Columns\TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('name')->searchable()->sortable()->label('Country'), + Tables\Columns\TextColumn::make('iso_code')->searchable()->sortable()->label('ISO'), + Tables\Columns\TextColumn::make('export_zone_parcel')->label('Export Parcel'), + Tables\Columns\TextColumn::make('export_zone_doc')->label('Export Doc'), + Tables\Columns\TextColumn::make('import_zone_parcel')->label('Import Parcel'), + Tables\Columns\TextColumn::make('import_zone_doc')->label('Import Doc'), + Tables\Columns\IconColumn::make('is_active')->boolean()->label('Active'), ]) ->filters([ - // + Tables\Filters\TernaryFilter::make('is_active')->label('Active Status'), ]) ->actions([ Tables\Actions\EditAction::make(), diff --git a/04_Laravel/app/Filament/Resources/ShipmentItemResource.php b/04_Laravel/app/Filament/Resources/ShipmentItemResource.php index e087db2..5dd49e9 100644 --- a/04_Laravel/app/Filament/Resources/ShipmentItemResource.php +++ b/04_Laravel/app/Filament/Resources/ShipmentItemResource.php @@ -14,9 +14,11 @@ class ShipmentItemResource extends Resource { protected static ?string $model = ShipmentItem::class; - // تغییر آیکون به نماد مناسب‌تر برای اقلام کالا protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-list'; + protected static ?string $navigationLabel = 'Shipment Items'; + protected static ?string $navigationGroup = 'Operations'; + public static function form(Form $form): Form { return $form @@ -36,21 +38,25 @@ class ShipmentItemResource extends Resource Forms\Components\TextInput::make('description') ->required() ->maxLength(255) - ->columnSpan(2), + ->columnSpan(2) + ->label('Description'), Forms\Components\TextInput::make('hs_code') ->required() ->label('HS Code'), Forms\Components\TextInput::make('quantity') ->required() - ->numeric(), + ->numeric() + ->label('Quantity'), Forms\Components\TextInput::make('unit_price') ->required() ->numeric() - ->prefix('USD'), + ->prefix('USD') + ->label('Unit Price'), Forms\Components\TextInput::make('total_usd') ->required() ->numeric() - ->prefix('USD'), + ->prefix('USD') + ->label('Total USD'), ])->columns(4); } @@ -67,16 +73,23 @@ class ShipmentItemResource extends Resource ->label('Row'), Tables\Columns\TextColumn::make('description') ->searchable() - ->limit(30), + ->limit(30) + ->label('Description'), Tables\Columns\TextColumn::make('hs_code') ->searchable() ->label('HS Code'), Tables\Columns\TextColumn::make('quantity') ->numeric() - ->sortable(), + ->sortable() + ->label('Qty'), + Tables\Columns\TextColumn::make('unit_price') + ->money('USD') + ->sortable() + ->label('Unit Price'), Tables\Columns\TextColumn::make('total_usd') ->money('USD') - ->sortable(), + ->sortable() + ->label('Total'), ]) ->filters([ // diff --git a/04_Laravel/app/Filament/Resources/ShipmentResource.php b/04_Laravel/app/Filament/Resources/ShipmentResource.php index 1bf52f8..50b625a 100644 --- a/04_Laravel/app/Filament/Resources/ShipmentResource.php +++ b/04_Laravel/app/Filament/Resources/ShipmentResource.php @@ -9,134 +9,113 @@ use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; -use App\Imports\ShippingRatesImport; -use Maatwebsite\Excel\Facades\Excel; -use Filament\Actions\Action; -use Filament\Forms\Components\FileUpload; class ShipmentResource extends Resource { protected static ?string $model = Shipment::class; - // تغییر آیکون به نماد کامیون/ارسال protected static ?string $navigationIcon = 'heroicon-o-truck'; + protected static ?string $navigationLabel = 'Shipments'; + protected static ?string $navigationGroup = 'Operations'; + public static function form(Form $form): Form { return $form ->schema([ - // بخش ۱: اطلاعات مسیر و وضعیت - Forms\Components\Section::make('Route & Status Info') + Forms\Components\Section::make('Route & Status') ->schema([ - Forms\Components\Select::make('user_id') - ->relationship('user', 'name') - ->searchable() - ->preload() - ->required(), Forms\Components\TextInput::make('awb_no') ->required() ->unique(ignoreRecord: true) ->label('AWB No.'), Forms\Components\Select::make('direction') - ->options(['Outbound' => 'Outbound', 'Inbound' => 'Inbound']) + ->options(['export' => 'Export', 'import' => 'Import']) ->required(), Forms\Components\Select::make('type') - ->options(['DOC' => 'DOC', 'NON DOC' => 'NON DOC']) + ->options([ + 'DOC_NORMAL' => 'Doc Normal', + 'DOC_ECONOMY' => 'Doc Economy', + 'PARCEL' => 'Parcel', + ]) ->required(), - Forms\Components\TextInput::make('status') + Forms\Components\Select::make('status') + ->options([ + 'processed' => 'Processed', + 'picked_up' => 'Picked Up', + 'in_transit' => 'In Transit', + 'out_for_delivery' => 'Out For Delivery', + 'failed' => 'Failed', + 'delivered' => 'Delivered', + 'returned' => 'Returned', + ]) + ->default('processed') ->required(), - Forms\Components\TextInput::make('reason_for_export') - ->required(), - Forms\Components\TextInput::make('forwarder') - ->nullable(), - Forms\Components\TextInput::make('forwarder_track_id') - ->nullable(), - ])->columns(2), - - // بخش ۲: اطلاعات وزن و ابعاد - Forms\Components\Section::make('Weight & Dimensions') - ->schema([ - Forms\Components\TextInput::make('weight') - ->numeric() - ->required() - ->suffix('kg'), - Forms\Components\TextInput::make('volumetric_weight') - ->numeric() - ->required() - ->suffix('kg'), - Forms\Components\TextInput::make('chargeable_weight') - ->numeric() - ->required() - ->suffix('kg'), - Forms\Components\TextInput::make('dimensions') - ->required() - ->helperText('e.g. 10x20x30'), - ])->columns(2), - - // بخش ۳: اطلاعات مالی - Forms\Components\Section::make('Financial Info') - ->schema([ - Forms\Components\TextInput::make('shipping_price') - ->numeric() - ->required() - ->prefix('درهم'), - Forms\Components\TextInput::make('extra_service') - ->numeric() - ->default(0) - ->prefix('درهم'), - Forms\Components\TextInput::make('packing_cost') - ->numeric() - ->default(0) - ->prefix('درهم'), - Forms\Components\TextInput::make('discount') - ->numeric() - ->default(0) - ->prefix('درهم'), - Forms\Components\TextInput::make('total_fee') - ->numeric() - ->required() - ->prefix('درهم'), - Forms\Components\TextInput::make('net_dirham') - ->numeric() - ->required() - ->prefix('درهم'), - Forms\Components\TextInput::make('net_rial') - ->numeric() - ->required() - ->suffix('ریال'), + Forms\Components\TextInput::make('reason_for_export')->nullable()->label('Reason for Export'), + Forms\Components\TextInput::make('forwarder')->nullable()->label('Forwarder'), + Forms\Components\Select::make('from_country_id') + ->relationship('fromCountry', 'name') + ->searchable() + ->preload() + ->label('From Country'), + Forms\Components\Select::make('to_country_id') + ->relationship('toCountry', 'name') + ->searchable() + ->preload() + ->label('To Country'), ])->columns(3), - // بخش ۴: اطلاعات فرستنده + Forms\Components\Section::make('Weight & Dimensions') + ->schema([ + Forms\Components\TextInput::make('weight')->numeric()->nullable()->suffix('kg')->label('Weight'), + Forms\Components\TextInput::make('volumetric_weight')->numeric()->nullable()->suffix('kg')->label('Volumetric Weight'), + Forms\Components\TextInput::make('chargeable_weight')->numeric()->nullable()->suffix('kg')->label('Chargeable Weight'), + Forms\Components\TextInput::make('dimensions')->nullable()->label('Dimensions (WxLxH)'), + ])->columns(4), + + Forms\Components\Section::make('Financial Info') + ->schema([ + Forms\Components\TextInput::make('shipping_price')->numeric()->nullable()->prefix('درهم')->label('Shipping Price'), + Forms\Components\TextInput::make('extra_service')->numeric()->default(0)->prefix('درهم')->label('Extra Service'), + Forms\Components\TextInput::make('packing_cost')->numeric()->default(0)->prefix('ریال')->label('Packing Cost'), + Forms\Components\TextInput::make('domestic_pickup')->numeric()->default(0)->prefix('ریال')->label('Domestic Pickup'), + Forms\Components\TextInput::make('domestic_delivery')->numeric()->default(0)->prefix('ریال')->label('Domestic Delivery'), + Forms\Components\TextInput::make('warehousing_cost')->numeric()->default(0)->prefix('ریال')->label('Warehousing Cost'), + Forms\Components\TextInput::make('vat_amount')->numeric()->default(0)->prefix('ریال')->label('VAT Amount'), + Forms\Components\TextInput::make('discount')->numeric()->default(0)->prefix('ریال')->label('Discount'), + Forms\Components\TextInput::make('total_fee')->numeric()->nullable()->prefix('ریال')->label('Total Fee'), + Forms\Components\TextInput::make('net_dirham')->numeric()->nullable()->prefix('درهم')->label('Net Dirham'), + Forms\Components\TextInput::make('net_rial')->numeric()->nullable()->prefix('ریال')->label('Net Rial'), + ])->columns(4), + Forms\Components\Section::make('Sender Info') ->schema([ - Forms\Components\TextInput::make('sender_name')->required(), - Forms\Components\TextInput::make('sender_company')->required(), - Forms\Components\TextInput::make('sender_phone')->required()->tel(), - Forms\Components\TextInput::make('sender_email')->required()->email(), - Forms\Components\TextInput::make('sender_address')->required(), - Forms\Components\TextInput::make('sender_city')->required(), - Forms\Components\TextInput::make('sender_zip')->required(), - Forms\Components\TextInput::make('sender_id_number')->required(), - ])->columns(2), + Forms\Components\TextInput::make('sender_name')->nullable()->label('Name'), + Forms\Components\TextInput::make('sender_company')->nullable()->label('Company'), + Forms\Components\TextInput::make('sender_phone')->tel()->nullable()->label('Phone'), + Forms\Components\TextInput::make('sender_email')->email()->nullable()->label('Email'), + Forms\Components\TextInput::make('sender_address')->nullable()->label('Address'), + Forms\Components\TextInput::make('sender_city')->nullable()->label('City'), + Forms\Components\TextInput::make('sender_zip')->nullable()->label('ZIP'), + Forms\Components\TextInput::make('sender_id_number')->nullable()->label('ID Number'), + ])->columns(4), - // بخش ۵: اطلاعات گیرنده Forms\Components\Section::make('Receiver Info') ->schema([ - Forms\Components\TextInput::make('receiver_name')->required(), - Forms\Components\TextInput::make('receiver_company')->required(), - Forms\Components\TextInput::make('receiver_phone')->required()->tel(), - Forms\Components\TextInput::make('receiver_email')->required()->email(), - Forms\Components\TextInput::make('receiver_address')->required(), - Forms\Components\TextInput::make('receiver_city')->required(), - Forms\Components\TextInput::make('receiver_zip')->required(), - Forms\Components\TextInput::make('receiver_id_number')->required(), - ])->columns(2), + Forms\Components\TextInput::make('receiver_name')->nullable()->label('Name'), + Forms\Components\TextInput::make('receiver_company')->nullable()->label('Company'), + Forms\Components\TextInput::make('receiver_phone')->tel()->nullable()->label('Phone'), + Forms\Components\TextInput::make('receiver_email')->email()->nullable()->label('Email'), + Forms\Components\TextInput::make('receiver_address')->nullable()->label('Address'), + Forms\Components\TextInput::make('receiver_city')->nullable()->label('City'), + Forms\Components\TextInput::make('receiver_zip')->nullable()->label('ZIP'), + Forms\Components\TextInput::make('receiver_id_number')->nullable()->label('ID Number'), + ])->columns(4), - // بخش ۶: اقلام گمرکی (Repeater) Forms\Components\Section::make('Customs Items') ->schema([ Forms\Components\Repeater::make('items') - ->relationship() // استفاده از رابطه items در مدل Shipment + ->relationship() ->schema([ Forms\Components\TextInput::make('row_number') ->required() @@ -146,27 +125,32 @@ class ShipmentResource extends Resource ->label('Row'), Forms\Components\TextInput::make('description') ->required() - ->columnSpan(2), + ->columnSpan(2) + ->label('Description'), Forms\Components\TextInput::make('hs_code') ->required() ->label('HS Code'), Forms\Components\TextInput::make('quantity') ->required() - ->numeric(), + ->numeric() + ->label('Qty'), Forms\Components\TextInput::make('unit_price') ->required() ->numeric() - ->prefix('USD'), + ->prefix('USD') + ->label('Unit Price'), Forms\Components\TextInput::make('total_usd') ->required() ->numeric() - ->prefix('USD'), - ])->columns(4) // چیدمان ۴ ستونه برای فیلدهای داخل ریپیتر - ->maxItems(9) // محدودیت ثبت حداکثر ۹ ردیف مطابق سند + ->prefix('USD') + ->label('Total'), + ]) + ->columns(6) + ->maxItems(9) ->defaultItems(1) ->reorderable() - ->label(''), - ]) + ->label('Items'), + ]), ]); } @@ -174,38 +158,41 @@ class ShipmentResource extends Resource { return $table ->columns([ - Tables\Columns\TextColumn::make('awb_no') - ->searchable() - ->label('AWB No.') - ->sortable(), - Tables\Columns\TextColumn::make('direction') - ->badge() - ->color(fn (string $state): string => match ($state) { - 'Inbound' => 'success', - 'Outbound' => 'info', - default => 'gray', - }), - Tables\Columns\TextColumn::make('status') - ->badge() - ->searchable(), - Tables\Columns\TextColumn::make('sender_name') - ->searchable() - ->toggleable(), - Tables\Columns\TextColumn::make('receiver_name') - ->searchable() - ->toggleable(), - Tables\Columns\TextColumn::make('total_fee') - ->money('AED') - ->sortable(), - Tables\Columns\TextColumn::make('created_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('awb_no')->searchable()->label('AWB No.')->sortable(), + Tables\Columns\TextColumn::make('direction')->badge()->color(fn (string $state): string => match ($state) { + 'import' => 'success', + 'export' => 'info', + default => 'gray', + })->label('Direction'), + Tables\Columns\TextColumn::make('type')->badge()->label('Type'), + Tables\Columns\TextColumn::make('status')->badge()->searchable()->label('Status'), + Tables\Columns\TextColumn::make('sender_name')->searchable()->toggleable()->label('Sender'), + Tables\Columns\TextColumn::make('receiver_name')->searchable()->toggleable()->label('Receiver'), + Tables\Columns\TextColumn::make('total_fee')->money('IRR')->sortable()->label('Total Fee'), + Tables\Columns\TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('status') + ->options([ + 'processed' => 'Processed', + 'picked_up' => 'Picked Up', + 'in_transit' => 'In Transit', + 'out_for_delivery' => 'Out For Delivery', + 'failed' => 'Failed', + 'delivered' => 'Delivered', + 'returned' => 'Returned', + ]), + Tables\Filters\SelectFilter::make('type') + ->options([ + 'DOC_NORMAL' => 'Doc Normal', + 'DOC_ECONOMY' => 'Doc Economy', + 'PARCEL' => 'Parcel', + ]), + Tables\Filters\SelectFilter::make('direction') + ->options(['export' => 'Export', 'import' => 'Import']), ]) ->actions([ + Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), ]) ->bulkActions([ @@ -218,7 +205,8 @@ class ShipmentResource extends Resource public static function getRelations(): array { return [ - // + ShipmentResource\RelationManagers\CarrierMappingsRelationManager::class, + ShipmentResource\RelationManagers\TrackingEventsRelationManager::class, ]; } @@ -227,6 +215,7 @@ class ShipmentResource extends Resource return [ 'index' => Pages\ListShipments::route('/'), 'create' => Pages\CreateShipment::route('/create'), + 'view' => Pages\ViewShipment::route('/{record}'), 'edit' => Pages\EditShipment::route('/{record}/edit'), ]; } diff --git a/04_Laravel/app/Filament/Resources/ShipmentResource/Pages/CreateShipment.php b/04_Laravel/app/Filament/Resources/ShipmentResource/Pages/CreateShipment.php index 9a0c349..582de46 100644 --- a/04_Laravel/app/Filament/Resources/ShipmentResource/Pages/CreateShipment.php +++ b/04_Laravel/app/Filament/Resources/ShipmentResource/Pages/CreateShipment.php @@ -9,4 +9,9 @@ use Filament\Resources\Pages\CreateRecord; class CreateShipment extends CreateRecord { protected static string $resource = ShipmentResource::class; + + protected function getRedirectUrl(): string + { + return static::$resource::getUrl('index'); + } } diff --git a/04_Laravel/app/Filament/Resources/ShipmentResource/Pages/ListShipments.php b/04_Laravel/app/Filament/Resources/ShipmentResource/Pages/ListShipments.php index f840549..9787ecf 100644 --- a/04_Laravel/app/Filament/Resources/ShipmentResource/Pages/ListShipments.php +++ b/04_Laravel/app/Filament/Resources/ShipmentResource/Pages/ListShipments.php @@ -3,12 +3,8 @@ namespace App\Filament\Resources\ShipmentResource\Pages; use App\Filament\Resources\ShipmentResource; -use App\Imports\OldShipmentsImport; // اضافه شدن کلاس ایمپورت صحیح use Filament\Actions; -use Filament\Actions\Action; // اضافه شدن اکشن -use Filament\Forms\Components\FileUpload; // اضافه شدن فایل آپلود use Filament\Resources\Pages\ListRecords; -use Maatwebsite\Excel\Facades\Excel; class ListShipments extends ListRecords { @@ -17,35 +13,6 @@ class ListShipments extends ListRecords protected function getHeaderActions(): array { return [ - // دکمه ایمپورت اکسل قدیمی - Action::make('import-old-shipments') - ->label('آپلود اکسل سفارشات قدیمی') - ->icon('heroicon-o-arrow-up-tray') - ->color('warning') // رنگ زرد برای قدیمی بودن - ->form([ - FileUpload::make('file') - ->label('فایل اکسل شیت List') - ->required() - ->directory('excel') // ذخیره موقت در این پوشه - ->acceptedFileTypes([ - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // اصلاح میم‌تایپ xlsx - 'application/vnd.ms-excel' // xls - ]) - ]) - ->action(function (array $data) { - // پاس دادن مسیر فایل آپلود شده به کلاس ایمپورت - Excel::import(new OldShipmentsImport, $data['file']); - - \Illuminate\Support\Facades\Cache::forget('shipments_list'); - - \Filament\Notifications\Notification::make() - ->title('عملیات موفق') - ->body('سفارشات با موفقیت از فایل اکسل وارد دیتابیس شدند.') - ->success() - ->send(); - }), - - // دکمه‌های پیش‌فرض Actions\CreateAction::make(), ]; } diff --git a/04_Laravel/app/Filament/Resources/ShipmentResource/Pages/ViewShipment.php b/04_Laravel/app/Filament/Resources/ShipmentResource/Pages/ViewShipment.php new file mode 100644 index 0000000..200e02b --- /dev/null +++ b/04_Laravel/app/Filament/Resources/ShipmentResource/Pages/ViewShipment.php @@ -0,0 +1,12 @@ +schema([ + Forms\Components\Select::make('carrier_code') + ->options([ + 'DHL' => 'DHL', + 'FEDEX' => 'FedEx', + 'UPS' => 'UPS', + 'ARAMEX' => 'Aramex', + 'NAGHEL' => 'Naqel', + 'EMX' => 'EMX', + 'APSITEX' => 'APSITEX', + 'IMPEX' => 'IMPEX', + 'OTHER' => 'Other', + ]) + ->required(), + Forms\Components\TextInput::make('carrier_tracking_number')->required()->label('Tracking Number'), + Forms\Components\TextInput::make('booking_number')->nullable()->label('Booking Number'), + Forms\Components\Select::make('direction') + ->options(['import' => 'Import', 'export' => 'Export']) + ->nullable(), + Forms\Components\Select::make('delivery_status') + ->options([ + 'processed' => 'Processed', + 'picked_up' => 'Picked Up', + 'in_transit' => 'In Transit', + 'out_for_delivery' => 'Out For Delivery', + 'failed' => 'Failed', + 'delivered' => 'Delivered', + 'returned' => 'Returned', + ]) + ->nullable(), + Forms\Components\Textarea::make('notes')->nullable()->rows(2)->label('Notes'), + ]); + } + + public function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('carrier_code')->badge()->label('Carrier'), + Tables\Columns\TextColumn::make('carrier_tracking_number')->label('Tracking Number')->searchable(), + Tables\Columns\TextColumn::make('booking_number')->label('Booking Number'), + Tables\Columns\TextColumn::make('direction')->badge()->label('Direction'), + Tables\Columns\TextColumn::make('delivery_status')->badge()->label('Status'), + Tables\Columns\TextColumn::make('notes')->limit(50)->label('Notes'), + ]) + ->headerActions([ + Tables\Actions\CreateAction::make(), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/TrackingEventsRelationManager.php b/04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/TrackingEventsRelationManager.php new file mode 100644 index 0000000..7c04171 --- /dev/null +++ b/04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/TrackingEventsRelationManager.php @@ -0,0 +1,72 @@ +schema([ + Forms\Components\DatePicker::make('event_date') + ->required() + ->default(now()) + ->label('Event Date'), + Forms\Components\TimePicker::make('event_time') + ->required() + ->default(now()) + ->label('Event Time'), + Forms\Components\TextInput::make('location')->nullable()->label('Location'), + Forms\Components\TextInput::make('event_description')->required()->label('Event Description'), + Forms\Components\Select::make('delivery_status') + ->options([ + 'processed' => 'Processed', + 'picked_up' => 'Picked Up', + 'in_transit' => 'In Transit', + 'out_for_delivery' => 'Out For Delivery', + 'failed' => 'Failed', + 'delivered' => 'Delivered', + 'returned' => 'Returned', + ]) + ->nullable() + ->label('Delivery Status'), + ]); + } + + public function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('event_date')->date()->label('Date')->sortable(), + Tables\Columns\TextColumn::make('event_time')->time()->label('Time'), + Tables\Columns\TextColumn::make('location')->label('Location'), + Tables\Columns\TextColumn::make('event_description')->label('Description')->limit(60), + Tables\Columns\TextColumn::make('delivery_status')->badge()->label('Status'), + Tables\Columns\TextColumn::make('source')->badge()->label('Source'), + ]) + ->defaultSort('event_date', 'desc') + ->headerActions([ + Tables\Actions\CreateAction::make(), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/04_Laravel/app/Filament/Resources/ShippingRateResource.php b/04_Laravel/app/Filament/Resources/ShippingRateResource.php index b19e682..0782acf 100644 --- a/04_Laravel/app/Filament/Resources/ShippingRateResource.php +++ b/04_Laravel/app/Filament/Resources/ShippingRateResource.php @@ -16,20 +16,21 @@ class ShippingRateResource extends Resource protected static ?string $navigationIcon = 'heroicon-o-currency-dollar'; + protected static ?string $navigationLabel = 'Shipping Rates'; + protected static ?string $navigationGroup = 'Settings'; + public static function form(Form $form): Form { return $form ->schema([ Forms\Components\Select::make('direction') - ->options([ - 'import' => 'Import', - 'export' => 'Export', - ]) + ->options(['export' => 'Export', 'import' => 'Import']) ->required(), Forms\Components\Select::make('type') ->options([ - 'DOCUMENT' => 'Document', - 'NON DOC' => 'Non Document', + 'DOC_NORMAL' => 'Doc Normal', + 'DOC_ECONOMY' => 'Doc Economy', + 'PARCEL' => 'Parcel', ]) ->required(), Forms\Components\TextInput::make('weight') @@ -37,42 +38,50 @@ class ShippingRateResource extends Resource ->numeric() ->suffix('kg') ->label('Weight (kg)'), - - // بخش زون‌ها بر اساس فایل Import - Forms\Components\Section::make('Zone Prices') - ->schema(array_map(fn ($i) => - Forms\Components\TextInput::make("zone_{$i}") - ->numeric() - ->prefix('درهم') - ->label("Zone {$i}") - ->columnSpan(1), - range(1, 10) - ))->columns(5), - ])->columns(3); + Forms\Components\Grid::make() + ->schema([ + Forms\Components\TextInput::make('zone_1')->numeric()->label('Zone 1'), + Forms\Components\TextInput::make('zone_2')->numeric()->label('Zone 2'), + Forms\Components\TextInput::make('zone_3')->numeric()->label('Zone 3'), + Forms\Components\TextInput::make('zone_4')->numeric()->label('Zone 4'), + Forms\Components\TextInput::make('zone_5')->numeric()->label('Zone 5'), + Forms\Components\TextInput::make('zone_6')->numeric()->label('Zone 6'), + Forms\Components\TextInput::make('zone_7')->numeric()->label('Zone 7'), + Forms\Components\TextInput::make('zone_8')->numeric()->label('Zone 8'), + Forms\Components\TextInput::make('zone_9')->numeric()->label('Zone 9'), + Forms\Components\TextInput::make('zone_10')->numeric()->label('Zone 10'), + ]) + ->columns(5), + ]); } public static function table(Table $table): Table { return $table ->columns([ - Tables\Columns\TextColumn::make('direction') - ->badge() - ->color(fn (string $state): string => $state === 'import' ? 'info' : 'success') - ->searchable(), - Tables\Columns\TextColumn::make('type') - ->badge() - ->searchable(), - Tables\Columns\TextColumn::make('weight') - ->numeric() - ->sortable() - ->suffix(' kg'), - Tables\Columns\TextColumn::make('zone_1') - ->money('AED') - ->label('Zone 1'), + Tables\Columns\TextColumn::make('direction')->badge()->label('Direction'), + Tables\Columns\TextColumn::make('type')->badge()->label('Type'), + Tables\Columns\TextColumn::make('weight')->numeric()->suffix(' kg')->label('Weight'), + Tables\Columns\TextColumn::make('zone_1')->label('Z1')->sortable(), + Tables\Columns\TextColumn::make('zone_2')->label('Z2')->sortable(), + Tables\Columns\TextColumn::make('zone_3')->label('Z3')->sortable(), + Tables\Columns\TextColumn::make('zone_4')->label('Z4')->sortable(), + Tables\Columns\TextColumn::make('zone_5')->label('Z5')->sortable(), + Tables\Columns\TextColumn::make('zone_6')->label('Z6')->sortable(), + Tables\Columns\TextColumn::make('zone_7')->label('Z7')->sortable(), + Tables\Columns\TextColumn::make('zone_8')->label('Z8')->sortable(), + Tables\Columns\TextColumn::make('zone_9')->label('Z9')->sortable(), + Tables\Columns\TextColumn::make('zone_10')->label('Z10')->sortable(), ]) ->filters([ - Tables\Filters\SelectFilter::make('direction'), - Tables\Filters\SelectFilter::make('type'), + Tables\Filters\SelectFilter::make('direction') + ->options(['export' => 'Export', 'import' => 'Import']), + Tables\Filters\SelectFilter::make('type') + ->options([ + 'DOC_NORMAL' => 'Doc Normal', + 'DOC_ECONOMY' => 'Doc Economy', + 'PARCEL' => 'Parcel', + ]), ]) ->actions([ Tables\Actions\EditAction::make(), diff --git a/04_Laravel/app/Http/Controllers/Api/PricingController.php b/04_Laravel/app/Http/Controllers/Api/PricingController.php index 2c76a93..7aa777f 100644 --- a/04_Laravel/app/Http/Controllers/Api/PricingController.php +++ b/04_Laravel/app/Http/Controllers/Api/PricingController.php @@ -2,31 +2,40 @@ namespace App\Http\Controllers\Api; -use App\Http\Controllers\Controller; use App\Services\PriceCalculatorService; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Validation\ValidationException; -class PricingController extends Controller +class PricingController { - public function calculate(Request $request) + public function __construct(protected PriceCalculatorService $calculator) {} + + public function calculate(Request $request): JsonResponse { - $validated = $request->validate([ - 'direction' => 'required|in:import,export', - 'type' => 'required|in:DOCUMENT,NON DOC', - 'origin_country_id' => 'required|exists:countries,id', - 'destination_country_id' => 'required|exists:countries,id', - 'weight' => 'required|numeric|min:0.1', - 'length' => 'required|numeric|min:1', - 'width' => 'required|numeric|min:1', - 'height' => 'required|numeric|min:1', - ]); + try { + $validated = $request->validate([ + 'direction' => ['required', 'string', 'in:Outbound,Inbound'], + 'type' => ['required', 'string', 'in:DOC_NORMAL,DOC_ECONOMY,PARCEL'], + 'country_iso' => ['required', 'string', 'size:2'], + 'weight' => ['required', 'numeric', 'min:0'], + 'volumetric_weight' => ['required', 'numeric', 'min:0'], + 'extra_service' => ['nullable', 'numeric', 'min:0'], + 'packing_cost' => ['nullable', 'numeric', 'min:0'], + 'domestic_pickup' => ['nullable', 'numeric', 'min:0'], + 'domestic_delivery' => ['nullable', 'numeric', 'min:0'], + 'warehousing_cost' => ['nullable', 'numeric', 'min:0'], + 'discount' => ['nullable', 'numeric', 'min:0'], + ]); + } catch (ValidationException $e) { + return response()->json([ + 'message' => 'Validation failed', + 'errors' => $e->errors(), + ], 422); + } - $service = new PriceCalculatorService(); - $result = $service->calculate($validated); + $result = $this->calculator->calculate($validated); - return response()->json([ - 'success' => $result['status'] === 'success', - 'data' => $result - ]); + return response()->json($result); } -} \ No newline at end of file +} diff --git a/04_Laravel/app/Http/Controllers/Api/TrackController.php b/04_Laravel/app/Http/Controllers/Api/TrackController.php new file mode 100644 index 0000000..191bded --- /dev/null +++ b/04_Laravel/app/Http/Controllers/Api/TrackController.php @@ -0,0 +1,58 @@ +firstOrFail(); + + $events = $shipment->trackingEvents() + ->orderByDesc('event_date') + ->orderByDesc('event_time') + ->get(); + + $mappings = $shipment->carrierMappings; + + return response()->json([ + 'awb_no' => $shipment->awb_no, + 'direction' => $shipment->direction->value, + 'type' => $shipment->type->value, + 'status' => $shipment->status->value, + 'sender' => [ + 'name' => $shipment->sender_name, + 'company' => $shipment->sender_company, + 'phone' => $shipment->sender_phone, + 'city' => $shipment->sender_city, + ], + 'receiver' => [ + 'name' => $shipment->receiver_name, + 'company' => $shipment->receiver_company, + 'phone' => $shipment->receiver_phone, + 'city' => $shipment->receiver_city, + ], + 'carrier_mappings' => $mappings->map(fn ($m) => [ + 'carrier_code' => $m->carrier_code->value, + 'tracking_number' => $m->carrier_tracking_number, + 'booking_number' => $m->booking_number, + 'delivery_status' => $m->delivery_status?->value, + 'notes' => $m->notes, + ]), + 'timeline' => $events->map(fn ($e) => [ + 'date' => $e->event_date->format('Y-m-d'), + 'time' => $e->event_time->format('H:i'), + 'location' => $e->location, + 'description' => $e->event_description, + 'delivery_status' => $e->delivery_status?->value, + 'source' => $e->source->value, + ]), + ]); + } +} diff --git a/04_Laravel/app/Http/Middleware/ApiKeyMiddleware.php b/04_Laravel/app/Http/Middleware/ApiKeyMiddleware.php new file mode 100644 index 0000000..6637b0d --- /dev/null +++ b/04_Laravel/app/Http/Middleware/ApiKeyMiddleware.php @@ -0,0 +1,27 @@ +header('Authorization'); + + if (!$apiKey || !str_starts_with($apiKey, 'Bearer ')) { + return response()->json(['message' => 'Unauthorized'], 401); + } + + $token = substr($apiKey, 7); + + if ($token !== config('ifnex.api_key')) { + return response()->json(['message' => 'Forbidden'], 403); + } + + return $next($request); + } +} diff --git a/04_Laravel/app/Http/Middleware/SetCspHeaders.php b/04_Laravel/app/Http/Middleware/SetCspHeaders.php new file mode 100644 index 0000000..bb46cfb --- /dev/null +++ b/04_Laravel/app/Http/Middleware/SetCspHeaders.php @@ -0,0 +1,18 @@ +headers->set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.bunny.net https://fonts.bunny.net https://*.bunny.net; style-src 'self' 'unsafe-inline' https://cdn.bunny.net https://fonts.bunny.net https://*.bunny.net; font-src 'self' https://cdn.bunny.net https://fonts.bunny.net https://*.bunny.net; connect-src 'self'; img-src 'self' data: https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"); + + return $response; + } +} diff --git a/04_Laravel/app/Imports/DebugImport.php b/04_Laravel/app/Imports/DebugImport.php new file mode 100644 index 0000000..62a0ab9 --- /dev/null +++ b/04_Laravel/app/Imports/DebugImport.php @@ -0,0 +1,32 @@ + new DebugSheet(), + ]; + } +} + +class DebugSheet implements OnEachRow, WithStartRow +{ + public function startRow(): int + { + return 3; + } + + public function onRow(Row $row) + { + $cells = $row->toArray(); + echo 'Row: ' . $row->getIndex() . ' | AWB: ' . ($cells[0] ?? '') . PHP_EOL; + } +} diff --git a/04_Laravel/app/Imports/HistoricalShipmentsImport.php b/04_Laravel/app/Imports/HistoricalShipmentsImport.php new file mode 100644 index 0000000..b6e4be5 --- /dev/null +++ b/04_Laravel/app/Imports/HistoricalShipmentsImport.php @@ -0,0 +1,164 @@ + new ShipmentsSheetImport(), + ]; + } +} + +class ShipmentsSheetImport implements OnEachRow, WithStartRow +{ + public function startRow(): int + { + return 3; + } + + public function onRow(Row $row) + { + $cells = $row->toArray(); + + if (empty($cells[0]) || $cells[0] === 'HAWB No.' || $cells[0] === 'Date') { + return; + } + + $awbNo = (string) $cells[0]; + if (str_starts_with($awbNo, '=')) { + return; + } + + $typeRaw = strtoupper(trim((string) ($cells[7] ?? ''))); + $serviceRaw = strtoupper(trim((string) ($cells[6] ?? ''))); + + $type = match (true) { + str_contains($typeRaw, 'DOC') && str_contains($typeRaw, 'ECO') => ShipmentType::DocEconomy, + str_contains($typeRaw, 'DOC') => ShipmentType::DocNormal, + default => ShipmentType::Parcel, + }; + + $direction = str_contains($serviceRaw, 'INBOUND') ? ShipmentDirection::Import : ShipmentDirection::Export; + + $fromCountry = $this->resolveCountry($cells[3] ?? ''); + $toCountry = $this->resolveCountry($cells[4] ?? ''); + + $date = $cells[1] ?? null; + if ($date && !str_contains((string) $date, '-')) { + $date = null; + } + + if (Shipment::where('awb_no', $awbNo)->exists()) { + return; + } + + $shipment = Shipment::create([ + 'awb_no' => $awbNo, + 'direction' => $direction, + 'type' => $type, + 'status' => ShipmentStatus::Processed, + 'reason_for_export' => $cells[42] ?? null, + 'forwarder' => $cells[2] ?? null, + 'from_country_id' => $fromCountry?->id, + 'to_country_id' => $toCountry?->id, + 'weight' => $this->toFloat($cells[8] ?? null), + 'volumetric_weight' => $this->toFloat($cells[9] ?? null), + 'chargeable_weight' => $this->toFloat($cells[12] ?? null), + 'dimensions' => $this->buildDimensions($cells[13] ?? null, $cells[14] ?? null, $cells[15] ?? null), + 'sender_name' => $cells[18] ?? null, + 'sender_company' => $cells[17] ?? null, + 'sender_phone' => $cells[19] ?? null, + 'sender_email' => $cells[20] ?? null, + 'sender_address' => $cells[21] ?? null, + 'sender_city' => $cells[22] ?? null, + 'sender_zip' => $cells[23] ?? null, + 'sender_id_number' => $cells[24] ?? null, + 'receiver_name' => $cells[26] ?? null, + 'receiver_company' => $cells[25] ?? null, + 'receiver_phone' => $cells[27] ?? null, + 'receiver_email' => $cells[28] ?? null, + 'receiver_address' => $cells[29] ?? null, + 'receiver_city' => $cells[30] ?? null, + 'receiver_zip' => $cells[31] ?? null, + 'receiver_id_number' => $cells[32] ?? null, + 'shipping_price' => $this->toFloat($cells[33] ?? null), + 'extra_service' => $this->toFloat($cells[34] ?? null) ?? 0, + 'packing_cost' => $this->toFloat($cells[36] ?? null) ?? 0, + 'domestic_pickup' => $this->toFloat($cells[35] ?? null) ?? 0, + 'domestic_delivery' => $this->toFloat($cells[37] ?? null) ?? 0, + 'warehousing_cost' => $this->toFloat($cells[38] ?? null) ?? 0, + 'discount' => $this->toFloat($cells[39] ?? null) ?? 0, + 'total_fee' => $this->toFloat($cells[40] ?? null), + 'net_dirham' => null, + 'net_rial' => null, + 'created_at' => $date, + ]); + + for ($i = 0; $i < 9; $i++) { + $base = 43 + ($i * 6); + + $description = $cells[$base] ?? null; + $hsCode = $cells[$base + 1] ?? null; + $quantity = $this->toFloat($cells[$base + 2] ?? null); + $unitPrice = $this->toFloat($cells[$base + 3] ?? null); + $totalUsd = $this->toFloat($cells[$base + 4] ?? null); + + if (empty($description) && empty($hsCode)) { + continue; + } + + ShipmentItem::create([ + 'shipment_id' => $shipment->id, + 'row_number' => $i + 1, + 'description' => $description, + 'hs_code' => $hsCode, + 'quantity' => $quantity ?? 0, + 'unit_price' => $unitPrice ?? 0, + 'total_usd' => $totalUsd ?? 0, + ]); + } + } + + private function toFloat($value): ?float + { + if ($value === null || $value === '') { + return null; + } + + $value = str_replace([',', ' '], '', (string) $value); + + return is_numeric($value) ? (float) $value : null; + } + + private function buildDimensions($w, $l, $h): ?string + { + $parts = array_filter([$w, $l, $h]); + + return $parts ? implode(' x ', $parts) . ' cm' : null; + } + + private function resolveCountry(string $raw): ?Country + { + $raw = trim($raw); + + if (preg_match('/\(([A-Z]{2})\)/', $raw, $matches)) { + return Country::where('iso_code', $matches[1])->first(); + } + + return Country::where('name', $raw)->first(); + } +} diff --git a/04_Laravel/app/Imports/OldShipmentsImport.php b/04_Laravel/app/Imports/OldShipmentsImport.php deleted file mode 100644 index f1105fb..0000000 --- a/04_Laravel/app/Imports/OldShipmentsImport.php +++ /dev/null @@ -1,104 +0,0 @@ -startRow; - } - - /** - * تبدیل تاریخ شمسی/میلادی به میلادی برای ذخیره در دیتابیس - */ - private function convertDateToGregorian($dateStr) - { - if (preg_match('/\d{4}\/\d{2}\/\d{2,4}/', $dateStr)) { - $parts = explode('/', $dateStr); - if (strlen($parts[2]) === 4) { - list($y, $m, $d) = $parts; - return "$y-$m-$d"; - } - } - return $dateStr; - } - - public function onRow(\Maatwebsite\Excel\Row $row) - { - $rowIndex = $row->getIndex(); - $cells = $row->toArray(); - - // ۱. ردیف‌های خالی یا هدرها را رد کنیم - if (empty($cells[1]) || in_array($cells[1], ['HAWB No.', 'Date', 'Forwarder', 'Shipper']) || in_array($cells[0], ['HEAD', 'List'])) { - return; - } - - // ۲. استخراج داده‌ها (بر اساس شیت لیست شما) - $data = [ - 'awb_no' => $cells[0], - 'direction' => 'Outbound', // طبق اکسل شما بیشتر صادرات است - 'type' => $cells[8] === 'DOC' ? 'DOC' : 'NON DOC', - 'status' => $cells[18] ?? 'Processed', // فیلد Last State - 'reason_for_export' => $cells[17] ?? '', - 'weight' => is_numeric($cells[11]) ? $cells[11] : 0, - 'volumetric_weight' => is_numeric($cells[12]) ? $cells[12] : 0, - 'chargeable_weight' => is_numeric($cells[13]) ? $cells[13] : 0, - 'dimensions' => trim(($cells[14] ?? '') . ' * ' . ($cells[15] ?? '') . ' * ' . ($cells[16] ?? '')), - // اصلاح تداخل ایندکس: ایندکس 18 برای status رزرو شد، قیمت را از ایندکس بعدی شروع کردیم - 'shipping_price' => is_numeric($cells[19] ?? null) ? $cells[19] : 0, - 'extra_service' => is_numeric($cells[20] ?? null) ? $cells[20] : 0, - 'packing_cost' => is_numeric($cells[21] ?? null) ? $cells[21] : 0, - 'discount' => is_numeric($cells[22] ?? null) ? $cells[22] : 0, - 'total_fee' => is_numeric($cells[23] ?? null) ? $cells[23] : 0, - 'net_dirham' => is_numeric($cells[25] ?? null) ? $cells[25] : 0, - 'net_rial' => is_numeric($cells[26] ?? null) ? $cells[26] : 0, - 'sender_name' => $cells[2], - 'sender_company' => '', - 'sender_phone' => $cells[3], - 'sender_email' => $cells[4], - 'sender_address' => $cells[5], - 'sender_city' => $cells[6], - 'sender_zip' => $cells[7], - 'sender_id_number' => '', - 'receiver_name' => $cells[9], - 'receiver_company' => '', - 'receiver_phone' => $cells[10], - // اصلاح تداخل ایندکس: ایندکس 11 مربوط به weight است، ایمیل گیرنده را به ایندکس بعدی منتقل کردیم - 'receiver_email' => $cells[12] ?? '', - - // رفع خطای متغیر ناموجود: خواندن مستقیم از cells به جای data - 'receiver_address' => ($cells[5] ?? '') . ' ' . ($cells[6] ?? ''), - 'receiver_city' => $cells[6] ?? '', - - 'receiver_zip' => $cells[13], - 'receiver_id_number' => $cells[14], - 'user_id' => 1, // به صورت پیش‌فرض برای سفارشات قدیمی که کاربر خاصی ندارند - ]; - - // ۳. تبدیل تاریخ (Date) - if (!empty($cells[1])) { - $data['created_at'] = $this->convertDateToGregorian($cells[1]); - } - - // ۴. ذخیره یا آپدیت در دیتابیس - Shipment::updateOrCreate( - ['awb_no' => $data['awb_no']], - $data - ); - } -} diff --git a/04_Laravel/app/Imports/ShippingRatesImport.php b/04_Laravel/app/Imports/ShippingRatesImport.php index 51e4a07..066fb2f 100644 --- a/04_Laravel/app/Imports/ShippingRatesImport.php +++ b/04_Laravel/app/Imports/ShippingRatesImport.php @@ -2,31 +2,34 @@ namespace App\Imports; +use App\Enums\ShipmentType; use App\Models\ShippingRate; use Maatwebsite\Excel\Concerns\OnEachRow; use Maatwebsite\Excel\Concerns\WithMultipleSheets; -use Maatwebsite\Excel\Concerns\WithHeadingRow; +use Maatwebsite\Excel\Concerns\WithStartRow; use Maatwebsite\Excel\Row; class ShippingRatesImport implements WithMultipleSheets { - // این متد می‌گه که فایل اکسل چند شیت دارد و هر کدام را با کلاس جداگانه می‌خواند public function sheets(): array { return [ 'Import Rate' => new RateSheetImport('import'), 'Export Rate' => new RateSheetImport('export'), + 'DocNor' => new SimpleRateSheetImport(ShipmentType::DocNormal), + 'DocEco' => new SimpleRateSheetImport(ShipmentType::DocEconomy), + 'Parcel' => new SimpleRateSheetImport(ShipmentType::Parcel), ]; } } -class RateSheetImport implements OnEachRow +class RateSheetImport implements OnEachRow, WithStartRow { - private $direction; + public function __construct(protected string $direction) {} - public function __construct($direction) + public function startRow(): int { - $this->direction = $direction; + return 5; } public function onRow(Row $row) @@ -34,45 +37,80 @@ class RateSheetImport implements OnEachRow $rowIndex = $row->getIndex(); $cells = $row->toArray(); - // ردیف اول و دوم معمولاً هدرهای نامرتب هستند، رد می‌کنیم - if ($rowIndex < 2) { - return; - } - - // اگر سطر حاوی کلمه DOCUMENT یا NON - DOCUMENT بود، رد می‌کنیم (فقط برای تفکیک نوع استفاده می‌شوند) - if (isset($cells[3]) && (strtoupper($cells[3]) === 'DOCUMENT' || strtoupper($cells[3]) === 'NON - DOCUMENT')) { - return; - } - - // اگر وزن خالی بود، ردیف را رد کن if (empty($cells[0]) || !is_numeric($cells[0])) { return; } - // تعیین نوع بسته (DOCUMENT یا NON DOC) بر اساس شماره ردیف در فایل اکسل - // در فایل شما، ردیف‌های 2 تا 7 مدارک و ردیف‌های 9 به بعد غیر مدارک بودند - $type = ($rowIndex >= 2 && $rowIndex <= 7) ? 'DOCUMENT' : 'NON DOC'; + $type = $this->resolveType($rowIndex, $cells); - // اگر در بخش غیر مدارک هستیم و به هدرها رسیدیم، رد می‌کنیم - if ($type === 'NON DOC' && isset($cells[0]) && strtoupper(trim($cells[0])) === 'WEIGHT (KG)') { - return; - } - - // پاکسازی قیمت‌ها (حذف کاماها و تبدیل به عدد) $zones = []; for ($i = 1; $i <= 10; $i++) { - $val = str_replace(',', '', $cells[$i] ?? 0); - $zones["zone_{$i}"] = is_numeric($val) ? floatval($val) : 0; + $val = str_replace([',', ' '], '', (string) ($cells[$i] ?? 0)); + $zones['zone_' . $i] = is_numeric($val) ? (float) $val : 0; } - // درج یا آپدیت رکورد در دیتابیس ShippingRate::updateOrCreate( [ 'direction' => $this->direction, - 'type' => $type, - 'weight' => floatval($cells[0]), + 'type' => $type->value, + 'weight' => (float) $cells[0], ], $zones ); } -} \ No newline at end of file + + private function resolveType(int $rowIndex, array $cells): ShipmentType + { + $header = strtoupper((string) ($cells[3] ?? '')); + + if (str_contains($header, 'DOCUMENT') || str_contains($header, 'DOC')) { + return ShipmentType::DocNormal; + } + + if (str_contains($header, 'NON') || str_contains($header, 'PARCEL')) { + return ShipmentType::Parcel; + } + + return $rowIndex <= 7 ? ShipmentType::DocNormal : ShipmentType::Parcel; + } +} + +class SimpleRateSheetImport implements OnEachRow, WithStartRow +{ + public function __construct(protected ShipmentType $type) {} + + public function startRow(): int + { + return 2; + } + + public function onRow(Row $row) + { + $cells = $row->toArray(); + + if (empty($cells[0]) || !is_numeric($cells[0])) { + return; + } + + $weight = (float) $cells[0]; + $zoneNumber = (int) $cells[1]; + $value = str_replace([',', ' '], '', (string) ($cells[2] ?? 0)); + $value = is_numeric($value) ? (float) $value : 0; + + $zoneColumn = 'zone_' . $zoneNumber; + + $data = [ + 'direction' => 'export', + 'type' => $this->type->value, + 'weight' => $weight, + ]; + + for ($i = 1; $i <= 10; $i++) { + $data['zone_' . $i] = 0; + } + + $record = ShippingRate::updateOrCreate($data, $data); + $record->{$zoneColumn} = $value; + $record->save(); + } +} diff --git a/04_Laravel/app/Models/Country.php b/04_Laravel/app/Models/Country.php index 0cfe15b..13d0f2d 100644 --- a/04_Laravel/app/Models/Country.php +++ b/04_Laravel/app/Models/Country.php @@ -1,13 +1,37 @@ 'boolean', + ]; + } + + public function fromShipments(): HasMany + { + return $this->hasMany(Shipment::class, 'from_country_id'); + } + + public function toShipments(): HasMany + { + return $this->hasMany(Shipment::class, 'to_country_id'); + } } diff --git a/04_Laravel/app/Models/Shipment.php b/04_Laravel/app/Models/Shipment.php index 9712234..c5ad089 100644 --- a/04_Laravel/app/Models/Shipment.php +++ b/04_Laravel/app/Models/Shipment.php @@ -1,4 +1,5 @@ \App\Enums\ShipmentDirection::class, + 'type' => \App\Enums\ShipmentType::class, + 'status' => \App\Enums\ShipmentStatus::class, + ]; + } + public function user(): BelongsTo { return $this->belongsTo(User::class); } + public function fromCountry(): BelongsTo + { + return $this->belongsTo(Country::class, 'from_country_id'); + } + + public function toCountry(): BelongsTo + { + return $this->belongsTo(Country::class, 'to_country_id'); + } + public function items(): HasMany { return $this->hasMany(ShipmentItem::class); } + + public function carrierMappings(): HasMany + { + return $this->hasMany(ShipmentCarrierMapping::class); + } + + public function trackingEvents(): HasMany + { + return $this->hasMany(ShipmentTrackingEvent::class); + } } diff --git a/04_Laravel/app/Models/ShipmentCarrierMapping.php b/04_Laravel/app/Models/ShipmentCarrierMapping.php new file mode 100644 index 0000000..c3b1244 --- /dev/null +++ b/04_Laravel/app/Models/ShipmentCarrierMapping.php @@ -0,0 +1,37 @@ + \App\Enums\CarrierCode::class, + 'direction' => \App\Enums\ShipmentDirection::class, + 'delivery_status' => \App\Enums\ShipmentStatus::class, + 'last_synced_at' => 'datetime', + 'manually_updated_at' => 'datetime', + ]; + } + + public function shipment(): BelongsTo + { + return $this->belongsTo(Shipment::class); + } +} diff --git a/04_Laravel/app/Models/ShipmentTrackingEvent.php b/04_Laravel/app/Models/ShipmentTrackingEvent.php new file mode 100644 index 0000000..e8f2543 --- /dev/null +++ b/04_Laravel/app/Models/ShipmentTrackingEvent.php @@ -0,0 +1,42 @@ + 'date', + 'event_time' => 'datetime:H:i', + 'delivery_status' => \App\Enums\ShipmentStatus::class, + 'source' => \App\Enums\TrackingSource::class, + 'raw_payload' => 'array', + ]; + } + + public function shipment(): BelongsTo + { + return $this->belongsTo(Shipment::class); + } + + public function carrierMapping(): BelongsTo + { + return $this->belongsTo(ShipmentCarrierMapping::class, 'carrier_mapping_id'); + } +} diff --git a/04_Laravel/app/Models/ShippingRate.php b/04_Laravel/app/Models/ShippingRate.php index 8bef708..d0de7d7 100644 --- a/04_Laravel/app/Models/ShippingRate.php +++ b/04_Laravel/app/Models/ShippingRate.php @@ -1,4 +1,5 @@ \App\Enums\ShipmentDirection::class, + 'type' => \App\Enums\ShipmentType::class, + ]; + } } diff --git a/04_Laravel/app/Models/SystemSetting.php b/04_Laravel/app/Models/SystemSetting.php index 5911fb7..604f511 100644 --- a/04_Laravel/app/Models/SystemSetting.php +++ b/04_Laravel/app/Models/SystemSetting.php @@ -3,16 +3,44 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class SystemSetting extends Model { - protected $table = 'system_settings'; - protected $fillable = [ - 'dirham_rate', - 'dollar_rate', - 'yuan_rate', - 'euro_rate', - 'profit_percent', + 'key', + 'value', + 'description', + 'updated_by', ]; -} \ No newline at end of file + + protected function casts(): array + { + return [ + 'value' => 'decimal:4', + ]; + } + + public function updatedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'updated_by'); + } + + public static function get(string $key, $default = null) + { + $setting = static::where('key', $key)->first(); + return $setting ? $setting->value : $default; + } + + public static function set(string $key, $value, ?string $description = null, ?int $updatedBy = null): void + { + static::updateOrCreate( + ['key' => $key], + [ + 'value' => $value, + 'description' => $description, + 'updated_by' => $updatedBy, + ] + ); + } +} diff --git a/04_Laravel/app/Models/User.php b/04_Laravel/app/Models/User.php index 68f3a66..8b89792 100644 --- a/04_Laravel/app/Models/User.php +++ b/04_Laravel/app/Models/User.php @@ -2,8 +2,6 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; -use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -13,37 +11,47 @@ class User extends Authenticatable /** @use HasFactory */ use HasFactory, Notifiable; - /** - * The attributes that are mass assignable. - * - * @var list - */ protected $fillable = [ 'name', 'email', 'password', + 'phone', + 'role', + 'is_active', ]; - /** - * The attributes that should be hidden for serialization. - * - * @var list - */ protected $hidden = [ 'password', 'remember_token', ]; - /** - * Get the attributes that should be cast. - * - * @return array - */ protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'is_active' => 'boolean', + 'role' => \App\Enums\UserRole::class, ]; } + + public function isSuperAdmin(): bool + { + return $this->role === \App\Enums\UserRole::SuperAdmin; + } + + public function isTrackingOperator(): bool + { + return $this->role === \App\Enums\UserRole::TrackingOperator; + } + + public function isDataEntry(): bool + { + return $this->role === \App\Enums\UserRole::DataEntry; + } + + public function isCustomer(): bool + { + return $this->role === \App\Enums\UserRole::Customer; + } } diff --git a/04_Laravel/app/Providers/Filament/AdminPanelProvider.php b/04_Laravel/app/Providers/Filament/AdminPanelProvider.php index 6694332..b1e6ba2 100644 --- a/04_Laravel/app/Providers/Filament/AdminPanelProvider.php +++ b/04_Laravel/app/Providers/Filament/AdminPanelProvider.php @@ -3,20 +3,11 @@ namespace App\Providers\Filament; use Filament\Http\Middleware\Authenticate; -use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; -use Filament\Http\Middleware\DispatchServingFilamentEvent; -use Filament\Pages; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; use Filament\Widgets; -use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; -use Illuminate\Cookie\Middleware\EncryptCookies; -use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; -use Illuminate\Routing\Middleware\SubstituteBindings; -use Illuminate\Session\Middleware\StartSession; -use Illuminate\View\Middleware\ShareErrorsFromSession; class AdminPanelProvider extends PanelProvider { @@ -33,23 +24,17 @@ class AdminPanelProvider extends PanelProvider ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') ->pages([ - Pages\Dashboard::class, + \Filament\Pages\Dashboard::class, ]) ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->widgets([ - Widgets\AccountWidget::class, - Widgets\FilamentInfoWidget::class, + \Filament\Widgets\AccountWidget::class, + \Filament\Widgets\FilamentInfoWidget::class, ]) ->middleware([ - EncryptCookies::class, - AddQueuedCookiesToResponse::class, - StartSession::class, - AuthenticateSession::class, - ShareErrorsFromSession::class, - VerifyCsrfToken::class, - SubstituteBindings::class, - DisableBladeIconComponents::class, - DispatchServingFilamentEvent::class, + 'web', + \Filament\Http\Middleware\DisableBladeIconComponents::class, + \Filament\Http\Middleware\DispatchServingFilamentEvent::class, ]) ->authMiddleware([ Authenticate::class, diff --git a/04_Laravel/app/Services/PriceCalculatorService.php b/04_Laravel/app/Services/PriceCalculatorService.php index acb2640..b06027e 100644 --- a/04_Laravel/app/Services/PriceCalculatorService.php +++ b/04_Laravel/app/Services/PriceCalculatorService.php @@ -2,76 +2,82 @@ namespace App\Services; +use App\Enums\ShipmentType; use App\Models\Country; -use App\Models\ShippingRate; +use App\Models\Shipment; use App\Models\SystemSetting; +use InvalidArgumentException; class PriceCalculatorService { - /** - * محاسبه قیمت ارسال - * - * @param array $data (شامل: direction, type, origin_country_id, destination_country_id, weight, length, width, height) - * @return array - */ public function calculate(array $data): array { - // 1. محاسبه وزن حجمی (طبق استاندارد جهانی تقسیم بر 5000) - $dimensions = ($data['length'] * $data['width'] * $data['height']) / 5000; - - // 2. تعیین وزن قابل پرداخت (ماکزیموم وزن واقعی و حجمی) - $chargeableWeight = max($data['weight'], $dimensions); + $type = ShipmentType::from($data['type']); + $direction = $data['direction'] === 'Outbound' ? 'export' : 'import'; + $chargeableWeight = max($data['weight'], $data['volumetric_weight']); - // 3. استخراج زون مقصد/مبدأ بر اساس جهت ارسال - if ($data['direction'] === 'export') { - // صادرات: زون صادرات کشور مقصد مد نظر است - $country = Country::find($data['destination_country_id']); - $zone = $country->export_zone; - } else { - // واردات: زون واردات کشور مبدأ مد نظر است - $country = Country::find($data['origin_country_id']); - $zone = $country->import_zone; - } + $country = Country::where('iso_code', $data['country_iso'])->firstOrFail(); - // 4. پیدا کردن ردیف تعرفه مناسب - // منطق: باید نزدیک‌ترین وزنی که بزرگتر مساوی وزن ماست رو پیدا کنیم (مثلاً اگر 1.2 کیلو است، ردیف 1.5 را می‌گیرد) - $rateRecord = ShippingRate::where('direction', $data['direction']) - ->where('type', $data['type']) - ->where('weight', '>=', $chargeableWeight) - ->orderBy('weight', 'asc') - ->first(); + $zone = match (true) { + $type === ShipmentType::Parcel && $direction === 'export' => $country->export_zone_parcel, + $type === ShipmentType::Parcel && $direction === 'import' => $country->import_zone_parcel, + $type !== ShipmentType::Parcel && $direction === 'export' => $country->export_zone_doc, + $type !== ShipmentType::Parcel && $direction === 'import' => $country->import_zone_doc, + }; - // اگر وزن بالای 30 کیلو باشد یا رکوردی پیدا نشود (Spot Rate) - if (!$rateRecord) { - return [ - 'status' => 'error', - 'message' => 'برای این وزن نرخ ثبت نشده است (نیاز به استعلام نرخ ویژه / Spot Rate).', - 'chargeable_weight' => $chargeableWeight, - 'volumetric_weight' => $dimensions, - ]; - } + $rate = $this->lookupRate($direction, $type, $chargeableWeight, $zone); - // 5. استخراج قیمت پایه بر اساس زون (مثلاً ستون zone_3) - $zoneColumn = "zone_{$zone}"; - $basePriceDirham = $rateRecord->$zoneColumn; + $profitMargin = (float) SystemSetting::get('profit_margin', 1.25); + $aedToIrr = (float) SystemSetting::get('aed_to_irr', 455000); + $vatRate = (float) SystemSetting::get('vat_rate', 0.09); + $packingCostDefault = (float) SystemSetting::get('packing_cost_default', 100000); - // 6. دریافت نرخ ارز و ضریب سود از تنظیمات - $settings = SystemSetting::first(); - $dirhamRate = $settings->dirham_rate; // نرخ درهم به ریال - $profitPercent = $settings->profit_percent; // مثلاً 1.25 + $netDirham = $rate * $profitMargin; + $netRial = $netDirham * $aedToIrr; - // 7. اجرای فرمول نهایی (دقیقاً مطابق اکسل) - $netDirham = $basePriceDirham * $profitPercent; - $netRial = $netDirham * $dirhamRate; + $extraService = (float) ($data['extra_service'] ?? 0); + $packingCost = (float) ($data['packing_cost'] ?? $packingCostDefault); + $domesticPickup = (float) ($data['domestic_pickup'] ?? 0); + $domesticDelivery = (float) ($data['domestic_delivery'] ?? 0); + $warehousingCost = (float) ($data['warehousing_cost'] ?? 0); + $discount = (float) ($data['discount'] ?? 0); + + $subtotal = $netRial + $extraService + $packingCost + $domesticPickup + $domesticDelivery + $warehousingCost - $discount; + $totalFee = $subtotal * (1 + $vatRate); + $vatAmount = $totalFee - $subtotal; return [ - 'status' => 'success', - 'volumetric_weight' => round($dimensions, 3), - 'chargeable_weight' => round($chargeableWeight, 2), + 'base_price' => $rate, + 'net_dirham' => $netDirham, + 'net_rial' => $netRial, + 'extra_service' => $extraService, + 'packing_cost' => $packingCost, + 'domestic_pickup' => $domesticPickup, + 'domestic_delivery' => $domesticDelivery, + 'warehousing_cost' => $warehousingCost, + 'discount' => $discount, + 'vat_amount' => $vatAmount, + 'total_fee' => $totalFee, 'zone' => $zone, - 'base_price_dirham' => $basePriceDirham, - 'net_dirham' => round($netDirham, 2), - 'net_rial' => round($netRial), + 'chargeable_weight' => $chargeableWeight, ]; } -} \ No newline at end of file + + private function lookupRate(string $direction, ShipmentType $type, float $weight, int $zone): float + { + $zoneColumn = 'zone_' . $zone; + + $rate = \App\Models\ShippingRate::query() + ->where('direction', $direction) + ->where('type', $type->value) + ->where('weight', '<=', $weight) + ->orderByDesc('weight') + ->value($zoneColumn); + + if ($rate === null) { + throw new InvalidArgumentException('No rate found for the given parameters.'); + } + + return (float) $rate; + } +} diff --git a/04_Laravel/app/Services/TrackingService.php b/04_Laravel/app/Services/TrackingService.php new file mode 100644 index 0000000..d82a950 --- /dev/null +++ b/04_Laravel/app/Services/TrackingService.php @@ -0,0 +1,36 @@ +firstOrFail(); + + return $shipment->trackingEvents() + ->orderByDesc('event_date') + ->orderByDesc('event_time') + ->get(); + } + + public function addTrackingEvent(int $shipment_id, array $data): ShipmentTrackingEvent + { + return ShipmentTrackingEvent::create(array_merge($data, [ + 'shipment_id' => $shipment_id, + 'source' => 'manual', + ])); + } + + public function getShipmentWithTracking(string $awb_no) + { + $shipment = Shipment::where('awb_no', $awb_no) + ->with(['carrierMappings', 'trackingEvents']) + ->firstOrFail(); + + return $shipment; + } +} diff --git a/04_Laravel/bootstrap/app.php b/04_Laravel/bootstrap/app.php index f4d8eb0..05cdcf1 100644 --- a/04_Laravel/bootstrap/app.php +++ b/04_Laravel/bootstrap/app.php @@ -11,11 +11,17 @@ return Application::configure(basePath: dirname(__DIR__)) commands: __DIR__.'/../routes/console.php', health: '/up', ) - ->withMiddleware(function (Middleware $middleware) { - //$middleware->statefulApi(); - - // فعال کردن CORS برای API ها + ->withMiddleware(function (Middleware $middleware): void { $middleware->api(prepend: \Illuminate\Http\Middleware\HandleCors::class); + + $middleware->group('web', [ + \Illuminate\Cookie\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/04_Laravel/composer.json b/04_Laravel/composer.json index 1daf2d8..1a3649b 100644 --- a/04_Laravel/composer.json +++ b/04_Laravel/composer.json @@ -8,9 +8,10 @@ "require": { "php": "^8.2", "filament/filament": "3.3.*", - "laravel/framework": "^12.0", + "laravel/framework": "^11.0", "laravel/tinker": "^2.10.1", - "maatwebsite/excel": "^3.1" + "maatwebsite/excel": "^3.1", + "morilog/jalali": "^3.0" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/04_Laravel/composer.lock b/04_Laravel/composer.lock index d255a53..83c9227 100644 --- a/04_Laravel/composer.lock +++ b/04_Laravel/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ee15d92e7f7847eea355ca3b5bb3af47", + "content-hash": "36a7ef85e0bbcd022965ce2b7234718f", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -71,6 +71,73 @@ }, "time": "2026-06-20T14:30:25+00:00" }, + { + "name": "beberlei/assert", + "version": "v3.3.4", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "f193f4613c7d7fbcee2c05e4daff4061d49c040e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/f193f4613c7d7fbcee2c05e4daff4061d49c040e", + "reference": "f193f4613c7d7fbcee2c05e4daff4061d49c040e", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan": "*", + "phpunit/phpunit": ">=6.0.0", + "yoast/phpunit-polyfills": "^0.1.0" + }, + "suggest": { + "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Assert/functions.php" + ], + "psr-4": { + "Assert\\": "lib/Assert" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de", + "role": "Lead Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Collaborator" + } + ], + "description": "Thin assertion library for input validation in business models.", + "keywords": [ + "assert", + "assertion", + "validation" + ], + "support": { + "issues": "https://github.com/beberlei/assert/issues", + "source": "https://github.com/beberlei/assert/tree/v3.3.4" + }, + "time": "2026-06-10T19:47:05+00:00" + }, { "name": "blade-ui-kit/blade-heroicons", "version": "2.7.0", @@ -2243,20 +2310,20 @@ }, { "name": "laravel/framework", - "version": "v12.64.0", + "version": "v11.55.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "727a8ea2949c23ca8b5316b86a00984b6017b7a0" + "reference": "dc7ec34ae95bacf4a63b96ec81482b4f3e702289" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/727a8ea2949c23ca8b5316b86a00984b6017b7a0", - "reference": "727a8ea2949c23ca8b5316b86a00984b6017b7a0", + "url": "https://api.github.com/repos/laravel/framework/zipball/dc7ec34ae95bacf4a63b96ec81482b4f3e702289", + "reference": "dc7ec34ae95bacf4a63b96ec81482b4f3e702289", "shasum": "" }, "require": { - "brick/math": "^0.11|^0.12|^0.13|^0.14", + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12|^0.13|^0.14", "composer-runtime-api": "^2.2", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.4", @@ -2271,34 +2338,32 @@ "fruitcake/php-cors": "^1.3", "guzzlehttp/guzzle": "^7.8.2", "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.3.0", + "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.8.1", + "league/commonmark": "^2.7", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", "monolog/monolog": "^3.0", - "nesbot/carbon": "^3.8.4", + "nesbot/carbon": "^2.72.6|^3.8.4", "nunomaduro/termwind": "^2.0", "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", "psr/simple-cache": "^1.0|^2.0|^3.0", "ramsey/uuid": "^4.7", - "symfony/console": "^7.2.0", - "symfony/error-handler": "^7.2.0", - "symfony/finder": "^7.2.0", + "symfony/console": "^7.0.3", + "symfony/error-handler": "^7.0.3", + "symfony/finder": "^7.0.3", "symfony/http-foundation": "^7.2.0", - "symfony/http-kernel": "^7.2.0", - "symfony/mailer": "^7.2.0", - "symfony/mime": "^7.2.0", - "symfony/polyfill-php83": "^1.33", - "symfony/polyfill-php84": "^1.34", - "symfony/polyfill-php85": "^1.34", - "symfony/process": "^7.2.0", - "symfony/routing": "^7.2.0", - "symfony/uid": "^7.2.0", - "symfony/var-dumper": "^7.2.0", + "symfony/http-kernel": "^7.0.3", + "symfony/mailer": "^7.0.3", + "symfony/mime": "^7.0.3", + "symfony/polyfill-php83": "^1.31", + "symfony/process": "^7.0.3", + "symfony/routing": "^7.0.3", + "symfony/uid": "^7.0.3", + "symfony/var-dumper": "^7.0.3", "tijsverkoyen/css-to-inline-styles": "^2.2.5", "vlucas/phpdotenv": "^5.6.1", "voku/portable-ascii": "^2.0.2" @@ -2330,7 +2395,6 @@ "illuminate/filesystem": "self.version", "illuminate/hashing": "self.version", "illuminate/http": "self.version", - "illuminate/json-schema": "self.version", "illuminate/log": "self.version", "illuminate/macroable": "self.version", "illuminate/mail": "self.version", @@ -2340,7 +2404,6 @@ "illuminate/process": "self.version", "illuminate/queue": "self.version", "illuminate/redis": "self.version", - "illuminate/reflection": "self.version", "illuminate/routing": "self.version", "illuminate/session": "self.version", "illuminate/support": "self.version", @@ -2364,18 +2427,17 @@ "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.9.0", - "pda/pheanstalk": "^5.0.6|^7.0.0", + "orchestra/testbench-core": "^9.18.0", + "pda/pheanstalk": "^5.0.6", "php-http/discovery": "^1.15", - "phpstan/phpstan": "^2.1.41", - "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", - "predis/predis": "^2.3|^3.0", - "resend/resend-php": "^0.10.0|^1.0", - "symfony/cache": "^7.2.0", - "symfony/http-client": "^7.2.0", - "symfony/psr-http-message-bridge": "^7.2.0", - "symfony/translation": "^7.2.0" + "phpstan/phpstan": "2.1.41", + "phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1", + "predis/predis": "^2.3", + "resend/resend-php": "^0.10.0", + "symfony/cache": "^7.0.3", + "symfony/http-client": "^7.0.3", + "symfony/psr-http-message-bridge": "^7.0.3", + "symfony/translation": "^7.0.3" }, "suggest": { "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", @@ -2390,7 +2452,7 @@ "ext-pdo": "Required to use all database features.", "ext-posix": "Required to use all features of the queue worker.", "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", - "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", @@ -2401,22 +2463,22 @@ "mockery/mockery": "Required to use mocking (^1.6).", "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", - "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", - "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.3.6|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", - "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", - "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", - "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "12.x-dev" + "dev-master": "11.x-dev" } }, "autoload": { @@ -2427,7 +2489,6 @@ "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Log/functions.php", - "src/Illuminate/Reflection/helpers.php", "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], @@ -2436,8 +2497,7 @@ "Illuminate\\Support\\": [ "src/Illuminate/Macroable/", "src/Illuminate/Collections/", - "src/Illuminate/Conditionable/", - "src/Illuminate/Reflection/" + "src/Illuminate/Conditionable/" ] } }, @@ -2461,7 +2521,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-07-14T14:25:37+00:00" + "time": "2026-07-14T14:25:27+00:00" }, { "name": "laravel/prompts", @@ -2651,16 +2711,16 @@ }, { "name": "league/commonmark", - "version": "2.8.3", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "1902f60f984235023acbe03db6ad614a37b3c3e7" + "reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/1902f60f984235023acbe03db6ad614a37b3c3e7", - "reference": "1902f60f984235023acbe03db6ad614a37b3c3e7", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/5703d83ba3da3b2e356a5fedc848ed6d8ffb6529", + "reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529", "shasum": "" }, "require": { @@ -2697,7 +2757,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.9-dev" + "dev-main": "2.10-dev" } }, "autoload": { @@ -2754,7 +2814,7 @@ "type": "tidelift" } ], - "time": "2026-07-12T15:29:16+00:00" + "time": "2026-08-03T13:42:31+00:00" }, { "name": "league/config", @@ -3811,6 +3871,71 @@ ], "time": "2026-01-02T08:56:05+00:00" }, + { + "name": "morilog/jalali", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/morilog/jalali.git", + "reference": "40b906c405b72edd20e73797b40be1e650e872c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/morilog/jalali/zipball/40b906c405b72edd20e73797b40be1e650e872c1", + "reference": "40b906c405b72edd20e73797b40be1e650e872c1", + "shasum": "" + }, + "require": { + "beberlei/assert": "^3.0", + "nesbot/carbon": "^1.21 || ^2.0 || ^3.0", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpunit/phpunit": ">4.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Morilog\\Jalali\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Milad Rey", + "email": "miladr@gmail.com" + }, + { + "name": "Morteza Parvini", + "email": "m.parvini@outlook.com" + } + ], + "description": "This Package helps developers to easily work with Jalali (Shamsi or Iranian) dates in PHP applications, based on Jalali (Shamsi) DateTime class.", + "keywords": [ + "Jalali", + "date", + "datetime", + "laravel", + "morilog" + ], + "support": { + "issues": "https://github.com/morilog/jalali/issues", + "source": "https://github.com/morilog/jalali/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://issuehunt.io/r/morilog", + "type": "issuehunt" + } + ], + "time": "2025-12-25T07:12:28+00:00" + }, { "name": "nesbot/carbon", "version": "3.13.1", @@ -7154,86 +7279,6 @@ ], "time": "2026-07-01T12:47:55+00:00" }, - { - "name": "symfony/polyfill-php84", - "version": "v1.38.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", - "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php84\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-05-26T12:51:13+00:00" - }, { "name": "symfony/polyfill-php85", "version": "v1.41.0", diff --git a/04_Laravel/config/ifnex.php b/04_Laravel/config/ifnex.php new file mode 100644 index 0000000..b144207 --- /dev/null +++ b/04_Laravel/config/ifnex.php @@ -0,0 +1,25 @@ + env('IFNEX_API_KEY', 'change-me'), + + 'cors_allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', 'http://localhost,http://127.0.0.1')), + + 'tracking_rate_limit' => env('IFNEX_TRACKING_RATE_LIMIT', 60), + + 'carrier_codes' => [ + 'DHL', 'FEDEX', 'UPS', 'ARAMEX', 'NAGHEL', 'EMX', 'APSITEX', 'IMPEX', 'OTHER', + ], + + 'tracking_statuses' => [ + 'processed', + 'picked_up', + 'in_transit', + 'out_for_delivery', + 'failed', + 'delivered', + 'returned', + ], + +]; diff --git a/04_Laravel/database/migrations/2026_08_02_000001_create_countries_table.php b/04_Laravel/database/migrations/2026_08_02_000001_create_countries_table.php new file mode 100644 index 0000000..9898150 --- /dev/null +++ b/04_Laravel/database/migrations/2026_08_02_000001_create_countries_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('name'); + $table->string('iso_code', 2)->unique(); + $table->unsignedTinyInteger('export_zone_parcel'); + $table->unsignedTinyInteger('export_zone_doc'); + $table->unsignedTinyInteger('import_zone_parcel'); + $table->unsignedTinyInteger('import_zone_doc'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('countries'); + } +}; diff --git a/04_Laravel/database/migrations/2026_08_02_000002_create_shipments_table.php b/04_Laravel/database/migrations/2026_08_02_000002_create_shipments_table.php new file mode 100644 index 0000000..f3b3697 --- /dev/null +++ b/04_Laravel/database/migrations/2026_08_02_000002_create_shipments_table.php @@ -0,0 +1,73 @@ +id(); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->string('awb_no')->unique(); + $table->string('forwarder')->nullable(); + $table->enum('direction', ['import', 'export']); + $table->enum('type', ['DOC_NORMAL', 'DOC_ECONOMY', 'PARCEL']); + $table->enum('status', ['processed', 'picked_up', 'in_transit', 'out_for_delivery', 'failed', 'delivered', 'returned'])->default('processed'); + $table->string('reason_for_export')->nullable(); + + $table->decimal('weight', 8, 2)->nullable()->unsigned(); + $table->decimal('volumetric_weight', 8, 2)->nullable()->unsigned(); + $table->decimal('chargeable_weight', 8, 2)->nullable()->unsigned(); + $table->string('dimensions')->nullable(); + + $table->decimal('shipping_price', 12, 2)->nullable()->unsigned(); + $table->decimal('extra_service', 12, 2)->default(0)->unsigned(); + $table->decimal('packing_cost', 12, 2)->default(0)->unsigned(); + $table->decimal('domestic_pickup', 12, 2)->default(0)->unsigned(); + $table->decimal('domestic_delivery', 12, 2)->default(0)->unsigned(); + $table->decimal('warehousing_cost', 12, 2)->default(0)->unsigned(); + $table->decimal('vat_amount', 12, 2)->default(0)->unsigned(); + $table->decimal('discount', 12, 2)->default(0)->unsigned(); + $table->decimal('total_fee', 12, 2)->nullable()->unsigned(); + $table->decimal('net_dirham', 12, 2)->nullable()->unsigned(); + $table->decimal('net_rial', 12, 2)->nullable()->unsigned(); + + $table->foreignId('from_country_id')->nullable()->constrained('countries')->nullOnDelete(); + $table->foreignId('to_country_id')->nullable()->constrained('countries')->nullOnDelete(); + + $table->string('sender_name')->nullable(); + $table->string('sender_company')->nullable(); + $table->string('sender_phone')->nullable(); + $table->string('sender_email')->nullable(); + $table->string('sender_address')->nullable(); + $table->string('sender_city')->nullable(); + $table->string('sender_zip')->nullable(); + $table->string('sender_id_number')->nullable(); + + $table->string('receiver_name')->nullable(); + $table->string('receiver_company')->nullable(); + $table->string('receiver_phone')->nullable(); + $table->string('receiver_email')->nullable(); + $table->string('receiver_address')->nullable(); + $table->string('receiver_city')->nullable(); + $table->string('receiver_zip')->nullable(); + $table->string('receiver_id_number')->nullable(); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('shipments'); + } +}; diff --git a/04_Laravel/database/migrations/2026_08_02_000003_create_shipping_rates_table.php b/04_Laravel/database/migrations/2026_08_02_000003_create_shipping_rates_table.php new file mode 100644 index 0000000..1406407 --- /dev/null +++ b/04_Laravel/database/migrations/2026_08_02_000003_create_shipping_rates_table.php @@ -0,0 +1,40 @@ +id(); + $table->enum('direction', ['import', 'export']); + $table->enum('type', ['DOC_NORMAL', 'DOC_ECONOMY', 'PARCEL']); + $table->decimal('weight', 8, 2)->unsigned(); + $table->decimal('zone_1', 10, 2)->unsigned(); + $table->decimal('zone_2', 10, 2)->unsigned(); + $table->decimal('zone_3', 10, 2)->unsigned(); + $table->decimal('zone_4', 10, 2)->unsigned(); + $table->decimal('zone_5', 10, 2)->unsigned(); + $table->decimal('zone_6', 10, 2)->unsigned(); + $table->decimal('zone_7', 10, 2)->unsigned(); + $table->decimal('zone_8', 10, 2)->unsigned(); + $table->decimal('zone_9', 10, 2)->unsigned(); + $table->decimal('zone_10', 10, 2)->unsigned(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('shipping_rates'); + } +}; diff --git a/04_Laravel/database/migrations/2026_08_02_000004_create_shipment_carrier_mappings_table.php b/04_Laravel/database/migrations/2026_08_02_000004_create_shipment_carrier_mappings_table.php new file mode 100644 index 0000000..27e4fde --- /dev/null +++ b/04_Laravel/database/migrations/2026_08_02_000004_create_shipment_carrier_mappings_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('shipment_id')->constrained()->cascadeOnDelete(); + $table->enum('carrier_code', ['DHL', 'FEDEX', 'UPS', 'ARAMEX', 'NAGHEL', 'EMX', 'APSITEX', 'IMPEX', 'OTHER']); + $table->string('carrier_tracking_number', 50); + $table->string('booking_number', 50)->nullable(); + $table->enum('direction', ['import', 'export'])->nullable(); + $table->enum('delivery_status', ['processed', 'picked_up', 'in_transit', 'out_for_delivery', 'failed', 'delivered', 'returned'])->nullable(); + $table->timestamp('last_synced_at')->nullable(); + $table->timestamp('manually_updated_at')->nullable(); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('shipment_carrier_mappings'); + } +}; diff --git a/04_Laravel/database/migrations/2026_08_02_000005_create_shipment_tracking_events_table.php b/04_Laravel/database/migrations/2026_08_02_000005_create_shipment_tracking_events_table.php new file mode 100644 index 0000000..ccd138f --- /dev/null +++ b/04_Laravel/database/migrations/2026_08_02_000005_create_shipment_tracking_events_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('shipment_id')->constrained()->cascadeOnDelete(); + $table->date('event_date'); + $table->time('event_time'); + $table->string('location', 200)->nullable(); + $table->string('event_description', 500); + $table->enum('delivery_status', ['processed', 'picked_up', 'in_transit', 'out_for_delivery', 'failed', 'delivered', 'returned'])->nullable(); + $table->enum('source', ['manual', 'api_carrier', 'api_aggregator'])->default('manual'); + $table->foreignId('carrier_mapping_id')->nullable()->constrained('shipment_carrier_mappings')->nullOnDelete(); + $table->json('raw_payload')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('shipment_tracking_events'); + } +}; diff --git a/04_Laravel/database/migrations/2026_08_02_000006_create_system_settings_table.php b/04_Laravel/database/migrations/2026_08_02_000006_create_system_settings_table.php new file mode 100644 index 0000000..6eac6d3 --- /dev/null +++ b/04_Laravel/database/migrations/2026_08_02_000006_create_system_settings_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('key', 100)->unique(); + $table->decimal('value', 15, 4)->unsigned(); + $table->string('description', 255)->nullable(); + $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('system_settings'); + } +}; diff --git a/04_Laravel/database/migrations/2026_08_02_000007_update_users_table.php b/04_Laravel/database/migrations/2026_08_02_000007_update_users_table.php new file mode 100644 index 0000000..d299eb1 --- /dev/null +++ b/04_Laravel/database/migrations/2026_08_02_000007_update_users_table.php @@ -0,0 +1,24 @@ +string('phone', 20)->nullable()->after('email'); + $table->enum('role', ['super_admin', 'tracking_operator', 'data_entry', 'customer'])->nullable()->after('phone'); + $table->boolean('is_active')->default(true)->after('role'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['phone', 'role', 'is_active']); + }); + } +}; diff --git a/04_Laravel/database/seeders/CountriesTableSeeder.php b/04_Laravel/database/seeders/CountriesTableSeeder.php index 9b4aef4..9fbb7c8 100644 --- a/04_Laravel/database/seeders/CountriesTableSeeder.php +++ b/04_Laravel/database/seeders/CountriesTableSeeder.php @@ -2,253 +2,261 @@ namespace Database\Seeders; +use App\Models\Country; use Illuminate\Database\Seeder; -use Illuminate\Support\Facades\DB; class CountriesTableSeeder extends Seeder { - /** - * Run the database seeds. - */ public function run(): void { - $countries = [ - ['name' => 'Afghanistan (AF)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Albania (AL)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Algeria (DZ)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'American Samoa (AS)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Andorra (AD)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Angola (AO)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Anguilla (AI)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Antigua (AG)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Argentina (AR)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Armenia (AM)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Aruba (AW)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Australia (AU)', 'export_zone' => 4, 'import_zone' => 5], - ['name' => 'Austria (AT)', 'export_zone' => 3, 'import_zone' => 3], - ['name' => 'Azerbaijan (AZ)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Bahamas (BS)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Bahrain (BH)', 'export_zone' => 1, 'import_zone' => 1], - ['name' => 'Bangladesh (BD)', 'export_zone' => 2, 'import_zone' => 2], - ['name' => 'Barbados (BB)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Belarus (BY)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Belgium (BE)', 'export_zone' => 3, 'import_zone' => 3], - ['name' => 'Belize (BZ)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Benin (BJ)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Bermuda (BM)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Bhutan (BT)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Bolivia (BO)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Bonaire (XB)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Bosnia & Herzegovina(BA)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Botswana (BW)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Brazil (BR)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Brunei (BN)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Bulgaria (BG)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Burkina Faso (BF)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Burundi (BI)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Cambodia (KH)', 'export_zone' => 4, 'import_zone' => 8], - ['name' => 'Cameroon (CM)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Canada (CA)', 'export_zone' => 3, 'import_zone' => 4], - ['name' => 'Canary Islands, The (IC)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Cape Verde (CV)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Cayman Islands (KY)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Central African Rep(CF)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Chad (TD)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Chile (CL)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'China (CN)', 'export_zone' => 3, 'import_zone' => 4], - ['name' => 'Colombia (CO)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Comoros (KM)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Congo (CG)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Congo, DPR (CD)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Cook Islands (CK)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Costa Rica (CR)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Cote D Ivoire (CI)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Croatia (HR)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Cuba (CU)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Curacao (XC)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Cyprus (CY)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Czech Rep., The (CZ)', 'export_zone' => 5, 'import_zone' => 3], - ['name' => 'Denmark (DK)', 'export_zone' => 3, 'import_zone' => 3], - ['name' => 'Djibouti (DJ)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Dominica (DM)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Dominican Rep. (DO)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Ecuador (EC)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Egypt (EG)', 'export_zone' => 2, 'import_zone' => 2], - ['name' => 'El Salvador (SV)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Eritrea (ER)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Estonia (EE)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Ethiopia (ET)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Falkland Islands (FK)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Faroe Islands (FO)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Fiji (FJ)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Finland (FI)', 'export_zone' => 3, 'import_zone' => 3], - ['name' => 'France (FR)', 'export_zone' => 3, 'import_zone' => 2], - ['name' => 'French Guyana (GF)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Gabon (GA)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Gambia (GM)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Georgia (GE)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Germany (DE)', 'export_zone' => 3, 'import_zone' => 2], - ['name' => 'Ghana (GH)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Gibraltar (GI)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Greece (GR)', 'export_zone' => 3, 'import_zone' => 2], - ['name' => 'Greenland (GL)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Grenada (GD)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Guadeloupe (GP)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Guam (GU)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Guatemala (GT)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Guernsey (GG)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Guinea Rep. (GN)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Guinea-Bissau (GW)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Guinea-Equatorial (GQ)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Guyana (British) (GY)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Haiti (HT)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Honduras (HN)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Hong Kong SAR China (HK)', 'export_zone' => 5, 'import_zone' => 5], - ['name' => 'Hungary (HU)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Iceland (IS)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'India (IN)', 'export_zone' => 2, 'import_zone' => 2], - ['name' => 'Indonesia (ID)', 'export_zone' => 5, 'import_zone' => 4], - ['name' => 'Iran (IR)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Iraq (IQ)', 'export_zone' => 8, 'import_zone' => 8], - ['name' => 'Ireland, Rep. Of (IE)', 'export_zone' => 3, 'import_zone' => 3], - ['name' => 'Italy (IT)', 'export_zone' => 3, 'import_zone' => 3], - ['name' => 'Jamaica (JM)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Japan (JP)', 'export_zone' => 5, 'import_zone' => 4], - ['name' => 'Jersey (JE)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Jordan (JO)', 'export_zone' => 3, 'import_zone' => 2], - ['name' => 'Kazakhstan (KZ)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Kenya (KE)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Kiribati (KI)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Korea, Rep. Of (KR)', 'export_zone' => 5, 'import_zone' => 4], - ['name' => 'Korea, D.P.R Of (KP)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Kosovo (KV)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Kuwait (KW)', 'export_zone' => 1, 'import_zone' => 1], - ['name' => 'Kyrgyzstan (KG)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Laos (LA)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Latvia (LV)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Lebanon (LB)', 'export_zone' => 3, 'import_zone' => 3], - ['name' => 'Lesotho (LS)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Liberia (LR)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Libya (LY)', 'export_zone' => 8, 'import_zone' => 8], - ['name' => 'Liechtenstein (LI)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Lithuania (LT)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Luxembourg (LU)', 'export_zone' => 3, 'import_zone' => 5], - ['name' => 'Macau SAR China (MO)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Madagascar (MG)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Malawi (MW)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Malaysia (MY)', 'export_zone' => 5, 'import_zone' => 4], - ['name' => 'Maldives (MV)', 'export_zone' => 5, 'import_zone' => 5], - ['name' => 'Mali (ML)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Malta (MT)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Mariana Islands (MP)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Marshall Islands (MH)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Martinique (MQ)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Mauritania (MR)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Mauritius (MU)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Mayotte (YT)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Mexico (MX)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Micronesia (FM)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Moldova, Rep. Of (MD)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Monaco (MC)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Mongolia (MN)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Montenegro, Rep Of (ME)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Montserrat (MS)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Morocco (MA)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Mozambique (MZ)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Myanmar (MM)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Namibia (NA)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Nauru, Rep. Of (NR)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Nepal (NP)', 'export_zone' => 5, 'import_zone' => 5], - ['name' => 'Netherlands, The (NL)', 'export_zone' => 3, 'import_zone' => 3], - ['name' => 'Nevis (XN)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'New Caledonia (NC)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'New Zealand (NZ)', 'export_zone' => 5, 'import_zone' => 6], - ['name' => 'Nicaragua (NI)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Niger (NE)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Nigeria (NG)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Niue (NU)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'North Macedonia (MK)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Norway (NO)', 'export_zone' => 6, 'import_zone' => 6], - ['name' => 'Oman (OM)', 'export_zone' => 1, 'import_zone' => 1], - ['name' => 'Pakistan (PK)', 'export_zone' => 2, 'import_zone' => 2], - ['name' => 'Palau (PW)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Panama (PA)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Papua New Guinea (PG)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Paraguay (PY)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Peru (PE)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Philippines, The (PH)', 'export_zone' => 3, 'import_zone' => 5], - ['name' => 'Poland (PL)', 'export_zone' => 6, 'import_zone' => 4], - ['name' => 'Portugal (PT)', 'export_zone' => 3, 'import_zone' => 3], - ['name' => 'Puerto Rico (PR)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Qatar (QA)', 'export_zone' => 1, 'import_zone' => 1], - ['name' => 'Reunion, Island Of (RE)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Romania (RO)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Russian Federation (RU)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Rwanda (RW)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Saint Helena (SH)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Samoa (WS)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'San Marino (SM)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Sao Tome And Principe (ST)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Saudi Arabia (SA)', 'export_zone' => 2, 'import_zone' => 1], - ['name' => 'Senegal (SN)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Serbia, Rep. Of (RS)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Seychelles (SC)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Sierra Leone (SL)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Singapore (SG)', 'export_zone' => 3, 'import_zone' => 5], - ['name' => 'Slovakia (SK)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Slovenia (SI)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Solomon Islands (SB)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Somalia (SO)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Somaliland, Rep Of (XS)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'South Africa (ZA)', 'export_zone' => 6, 'import_zone' => 6], - ['name' => 'South Sudan (SS)', 'export_zone' => 8, 'import_zone' => 8], - ['name' => 'Spain (ES)', 'export_zone' => 3, 'import_zone' => 3], - ['name' => 'Sri Lanka (LK)', 'export_zone' => 2, 'import_zone' => 2], - ['name' => 'St. Barthelemy (XY)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'St. Eustatius (XE)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'St. Kitts (KN)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'St. Lucia (LC)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'St. Maarten (XM)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'St. Vincent (VC)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Sudan (SD)', 'export_zone' => 8, 'import_zone' => 8], - ['name' => 'Suriname (SR)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Swaziland (SZ)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Sweden (SE)', 'export_zone' => 3, 'import_zone' => 3], - ['name' => 'Switzerland (CH)', 'export_zone' => 3, 'import_zone' => 3], - ['name' => 'Syria (SY)', 'export_zone' => 8, 'import_zone' => 8], - ['name' => 'Tahiti (PF)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Taiwan (TW)', 'export_zone' => 5, 'import_zone' => 4], - ['name' => 'Tajikistan (TJ)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Tanzania (TZ)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Thailand (TH)', 'export_zone' => 5, 'import_zone' => 4], - ['name' => 'Timor-Leste (TL)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Togo (TG)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Tonga (TO)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Trinidad And Tobago (TT)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Tunisia (TN)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Turkey (TR)', 'export_zone' => 5, 'import_zone' => 3], - ['name' => 'Turkmenistan (TM)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Turks & Caicos (TC)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Tuvalu (TV)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'USA (US)', 'export_zone' => 3, 'import_zone' => 4], - ['name' => 'Uganda (UG)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Ukraine (UA)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'United Kingdom (GB)', 'export_zone' => 3, 'import_zone' => 2], - ['name' => 'Uruguay (UY)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Uzbekistan (UZ)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Vanuatu (VU)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Vatican City (VA)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Venezuela (VE)', 'export_zone' => 6, 'import_zone' => 8], - ['name' => 'Vietnam (VN)', 'export_zone' => 5, 'import_zone' => 4], - ['name' => 'Virgin Islands-British (VG)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Virgin Islands-US (VI)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Yemen, Rep. Of (YE)', 'export_zone' => 8, 'import_zone' => 8], - ['name' => 'Zambia (ZM)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'Zimbabwe (ZW)', 'export_zone' => 7, 'import_zone' => 8], - ['name' => 'United Arab Emirates (AE)', 'export_zone' => 9, 'import_zone' => 9], - ['name' => 'Rest of Dubai (AE)', 'export_zone' => 10, 'import_zone' => 10], + $rows = [ + ['Afghanistan','AF',7,7,8,8], + ['Albania','AL',6,6,8,8], + ['Algeria','DZ',6,6,8,8], + ['American Samoa','AS',7,7,8,8], + ['Andorra','AD',7,7,8,8], + ['Angola','AO',7,7,8,8], + ['Anguilla','AI',7,7,8,8], + ['Antigua','AG',7,7,8,8], + ['Argentina','AR',6,6,8,8], + ['Armenia','AM',6,6,8,8], + ['Aruba','AW',7,7,8,8], + ['Australia','AU',4,4,5,5], + ['Austria','AT',3,3,3,3], + ['Azerbaijan','AZ',6,6,8,8], + ['Bahamas','BS',7,7,8,8], + ['Bahrain','BH',1,1,1,1], + ['Bangladesh','BD',2,2,2,2], + ['Barbados','BB',7,7,8,8], + ['Belarus','BY',6,6,8,8], + ['Belgium','BE',3,3,3,3], + ['Belize','BZ',7,7,8,8], + ['Benin','BJ',7,7,8,8], + ['Bermuda','BM',7,7,8,8], + ['Bhutan','BT',7,7,8,8], + ['Bolivia','BO',6,6,8,8], + ['Bonaire','BQ',7,7,8,8], + ['Bosnia and Herzegovina','BA',6,6,8,8], + ['Botswana','BW',7,7,8,8], + ['Brazil','BR',6,6,8,8], + ['Brunei','BN',7,7,8,8], + ['Bulgaria','BG',6,6,8,8], + ['Burkina Faso','BF',7,7,8,8], + ['Burundi','BI',7,7,8,8], + ['Cambodia','KH',4,4,8,8], + ['Cameroon','CM',7,7,8,8], + ['Canada','CA',3,3,4,4], + ['Canary Islands, The','IC',6,6,8,8], + ['Cape Verde','CV',7,7,8,8], + ['Cayman Islands','KY',6,6,8,8], + ['Central African Rep','CF',7,7,8,8], + ['Chad','TD',7,7,8,8], + ['Chile','CL',6,6,8,8], + ['China','CN',3,3,4,4], + ['Colombia','CO',6,6,8,8], + ['Comoros','KM',7,7,8,8], + ['Congo','CG',7,7,8,8], + ['Congo, DPR','CD',6,6,8,8], + ['Cook Islands','CK',7,7,8,8], + ['Costa Rica','CR',7,7,8,8], + ["Cote D Ivoire",'CI',6,6,8,8], + ['Croatia','HR',6,6,8,8], + ['Cuba','CU',6,6,8,8], + ['Curacao','CW',7,7,8,8], + ['Cyprus','CY',6,6,8,8], + ['Czech Rep., The','CZ',5,5,3,3], + ['Denmark','DK',3,3,3,3], + ['Djibouti','DJ',7,7,8,8], + ['Dominica','DM',7,7,8,8], + ['Dominican Rep.','DO',6,6,8,8], + ['Ecuador','EC',6,6,8,8], + ['Egypt','EG',2,2,2,2], + ['El Salvador','SV',7,7,8,8], + ['Eritrea','ER',7,7,8,8], + ['Estonia','EE',6,6,8,8], + ['Ethiopia','ET',6,6,8,8], + ['Falkland Islands','FK',7,7,8,8], + ['Faroe Islands','FO',7,7,8,8], + ['Fiji','FJ',7,7,8,8], + ['Finland','FI',3,3,3,3], + ['France','FR',3,3,2,2], + ['French Guyana','GF',7,7,8,8], + ['Gabon','GA',7,7,8,8], + ['Gambia','GM',7,7,8,8], + ['Georgia','GE',6,6,8,8], + ['Germany','DE',3,3,2,2], + ['Ghana','GH',6,6,8,8], + ['Gibraltar','GI',7,7,8,8], + ['Greece','GR',3,3,2,2], + ['Greenland','GL',7,7,8,8], + ['Grenada','GD',7,7,8,8], + ['Guadeloupe','GP',7,7,8,8], + ['Guam','GU',7,7,8,8], + ['Guatemala','GT',7,7,8,8], + ['Guernsey','GG',7,7,8,8], + ['Guinea Rep.','GN',6,6,8,8], + ['Guinea-Bissau','GW',7,7,8,8], + ['Guinea-Equatorial','GQ',7,7,8,8], + ['Guyana (British)','GY',7,7,8,8], + ['Haiti','HT',7,7,8,8], + ['Honduras','HN',7,7,8,8], + ['Hong Kong SAR China','HK',5,5,5,5], + ['Hungary','HU',6,6,8,8], + ['Iceland','IS',6,6,8,8], + ['India','IN',2,2,2,2], + ['Indonesia','ID',5,5,4,4], + ['Iran','IR',0,0,0,0], + ['Iraq','IQ',8,8,8,8], + ['Ireland, Rep. Of','IE',3,3,3,3], + ['Italy','IT',3,3,3,3], + ['Jamaica','JM',7,7,8,8], + ['Japan','JP',5,5,4,4], + ['Jersey','JE',7,7,8,8], + ['Jordan','JO',3,3,2,2], + ['Kazakhstan','KZ',6,6,8,8], + ['Kenya','KE',6,6,8,8], + ['Kiribati','KI',7,7,8,8], + ['Korea, Rep. Of','KR',5,5,4,4], + ['Korea, D.P.R Of','KP',7,7,8,8], + ['Kosovo','KV',7,7,8,8], + ['Kuwait','KW',1,1,1,1], + ['Kyrgyzstan','KG',6,6,8,8], + ['Laos','LA',7,7,8,8], + ['Latvia','LV',6,6,8,8], + ['Lebanon','LB',3,3,3,3], + ['Lesotho','LS',7,7,8,8], + ['Liberia','LR',7,7,8,8], + ['Libya','LY',8,8,8,8], + ['Liechtenstein','LI',6,6,8,8], + ['Lithuania','LT',6,6,8,8], + ['Luxembourg','LU',3,3,5,5], + ['Macau SAR China','MO',7,7,8,8], + ['Madagascar','MG',7,7,8,8], + ['Malawi','MW',7,7,8,8], + ['Malaysia','MY',5,5,4,4], + ['Maldives','MV',5,5,5,5], + ['Mali','ML',6,6,8,8], + ['Malta','MT',6,6,8,8], + ['Mariana Islands','MP',7,7,8,8], + ['Marshall Islands','MH',7,7,8,8], + ['Martinique','MQ',7,7,8,8], + ['Mauritania','MR',7,7,8,8], + ['Mauritius','MU',7,7,8,8], + ['Mayotte','YT',7,7,8,8], + ['Mexico','MX',6,6,8,8], + ['Micronesia','FM',7,7,8,8], + ['Moldova, Rep. Of','MD',7,7,8,8], + ['Monaco','MC',7,7,8,8], + ['Mongolia','MN',6,6,8,8], + ['Montenegro, Rep Of','ME',7,7,8,8], + ['Montserrat','MS',7,7,8,8], + ['Morocco','MA',6,6,8,8], + ['Mozambique','MZ',7,7,8,8], + ['Myanmar','MM',7,7,8,8], + ['Namibia','NA',7,7,8,8], + ['Nauru, Rep. Of','NR',7,7,8,8], + ['Nepal','NP',5,5,5,5], + ['Netherlands, The','NL',3,3,3,3], + ['Nevis','XN',7,7,8,8], + ['New Caledonia','NC',7,7,8,8], + ['New Zealand','NZ',5,5,6,6], + ['Nicaragua','NI',7,7,8,8], + ['Niger','NE',7,7,8,8], + ['Nigeria','NG',6,6,8,8], + ['Niue','NU',7,7,8,8], + ['North Macedonia','MK',7,7,8,8], + ['Norway','NO',6,6,6,6], + ['Oman','OM',1,1,1,1], + ['Pakistan','PK',2,2,2,2], + ['Palau','PW',7,7,8,8], + ['Panama','PA',6,6,8,8], + ['Papua New Guinea','PG',7,7,8,8], + ['Paraguay','PY',6,6,8,8], + ['Peru','PE',6,6,8,8], + ['Philippines, The','PH',3,3,5,5], + ['Poland','PL',6,6,4,4], + ['Portugal','PT',3,3,3,3], + ['Puerto Rico','PR',7,7,8,8], + ['Qatar','QA',1,1,1,1], + ['Reunion, Island Of','RE',7,7,8,8], + ['Romania','RO',6,6,8,8], + ['Russian Federation','RU',7,7,8,8], + ['Rwanda','RW',7,7,8,8], + ['Saint Helena','SH',7,7,8,8], + ['Samoa','WS',7,7,8,8], + ['San Marino','SM',7,7,8,8], + ['Sao Tome And Principe','ST',7,7,8,8], + ['Saudi Arabia','SA',2,2,1,1], + ['Senegal','SN',7,7,8,8], + ['Serbia, Rep. Of','RS',6,6,8,8], + ['Seychelles','SC',7,7,8,8], + ['Sierra Leone','SL',6,6,8,8], + ['Singapore','SG',3,3,5,5], + ['Slovakia','SK',6,6,8,8], + ['Slovenia','SI',6,6,8,8], + ['Solomon Islands','SB',7,7,8,8], + ['Somalia','SO',7,7,8,8], + ['South Africa','ZA',6,6,6,6], + ['South Sudan','SS',8,8,8,8], + ['Spain','ES',3,3,3,3], + ['Sri Lanka','LK',2,2,2,2], + ['St. Barthelemy','XY',7,7,8,8], + ['St. Eustatius','XE',7,7,8,8], + ['St. Kitts','KN',7,7,8,8], + ['St. Lucia','LC',7,7,8,8], + ['St. Maarten','XM',7,7,8,8], + ['St. Vincent','VC',7,7,8,8], + ['Sudan','SD',8,8,8,8], + ['Suriname','SR',7,7,8,8], + ['Swaziland','SZ',7,7,8,8], + ['Sweden','SE',3,3,3,3], + ['Switzerland','CH',3,3,3,3], + ['Syria','SY',8,8,8,8], + ['Tahiti','PF',7,7,8,8], + ['Taiwan','TW',5,5,4,4], + ['Tajikistan','TJ',6,6,8,8], + ['Tanzania','TZ',6,6,8,8], + ['Thailand','TH',5,5,4,4], + ['Timor-Leste','TL',7,7,8,8], + ['Togo','TG',7,7,8,8], + ['Tonga','TO',7,7,8,8], + ['Trinidad And Tobago','TT',7,7,8,8], + ['Tunisia','TN',6,6,8,8], + ['Turkey','TR',5,5,3,3], + ['Turkmenistan','TM',6,6,8,8], + ['Turks & Caicos','TC',7,7,8,8], + ['Tuvalu','TV',7,7,8,8], + ['USA','US',3,3,4,4], + ['Uganda','UG',7,7,8,8], + ['Ukraine','UA',6,6,8,8], + ['United Kingdom','GB',3,3,2,2], + ['Uruguay','UY',6,6,8,8], + ['Uzbekistan','UZ',6,6,8,8], + ['Vanuatu','VU',7,7,8,8], + ['Vatican City','VA',7,7,8,8], + ['Venezuela','VE',6,6,8,8], + ['Vietnam','VN',5,5,4,4], + ['Virgin Islands-British','VG',7,7,8,8], + ['Virgin Islands-US','VI',7,7,8,8], + ['Yemen, Rep. Of','YE',8,8,8,8], + ['Zambia','ZM',7,7,8,8], + ['Zimbabwe','ZW',7,7,8,8], + ['United Arab Emirates','AE',9,9,9,9], + ['Rest of Dubai','AE',10,10,10,10], ]; - DB::table('countries')->insert($countries); + foreach ($rows as $row) { + Country::updateOrCreate( + ['iso_code' => $row[1]], + [ + 'name' => $row[0], + 'export_zone_parcel' => $row[2], + 'export_zone_doc' => $row[3], + 'import_zone_parcel' => $row[4], + 'import_zone_doc' => $row[5], + 'is_active' => true, + ] + ); + } } -} \ No newline at end of file +} diff --git a/04_Laravel/database/seeders/DatabaseSeeder.php b/04_Laravel/database/seeders/DatabaseSeeder.php index 6b901f8..09b6d04 100644 --- a/04_Laravel/database/seeders/DatabaseSeeder.php +++ b/04_Laravel/database/seeders/DatabaseSeeder.php @@ -3,23 +3,25 @@ namespace Database\Seeders; use App\Models\User; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; class DatabaseSeeder extends Seeder { - use WithoutModelEvents; - - /** - * Seed the application's database. - */ public function run(): void { - // User::factory(10)->create(); + $this->call([ + CountriesTableSeeder::class, + SystemSettingSeeder::class, + ]); User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + 'name' => 'Kazem Alghasi', + 'email' => 'kazem@vernasoft.group', + 'password' => Hash::make('password'), + 'phone' => '09123456789', + 'role' => 'super_admin', + 'is_active' => true, ]); } } diff --git a/04_Laravel/database/seeders/SystemSettingSeeder.php b/04_Laravel/database/seeders/SystemSettingSeeder.php index ba0ce6a..04db095 100644 --- a/04_Laravel/database/seeders/SystemSettingSeeder.php +++ b/04_Laravel/database/seeders/SystemSettingSeeder.php @@ -2,6 +2,8 @@ namespace Database\Seeders; +use App\Models\SystemSetting; +use App\Models\User; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; @@ -9,12 +11,18 @@ class SystemSettingSeeder extends Seeder { public function run(): void { - DB::table('system_settings')->insert([ - 'dirham_rate' => 455000, - 'dollar_rate' => 0, - 'yuan_rate' => 0, - 'euro_rate' => 0, - 'profit_percent' => 1.25, - ]); + $settings = [ + ['key' => 'vat_rate', 'value' => 0.0900, 'description' => 'VAT rate — 9%'], + ['key' => 'packing_cost_default', 'value' => 100000.0000, 'description' => 'Default packing cost in IRR'], + ['key' => 'profit_margin', 'value' => 1.2500, 'description' => 'Profit margin — set per company'], + ['key' => 'aed_to_irr', 'value' => 455000.0000, 'description' => 'AED to IRR exchange rate'], + ['key' => 'usd_to_irr', 'value' => 0.0000, 'description' => 'USD to IRR exchange rate'], + ['key' => 'cny_to_irr', 'value' => 0.0000, 'description' => 'CNY to IRR exchange rate'], + ['key' => 'eur_to_irr', 'value' => 0.0000, 'description' => 'EUR to IRR exchange rate'], + ]; + + foreach ($settings as $setting) { + SystemSetting::updateOrCreate(['key' => $setting['key']], $setting); + } } -} \ No newline at end of file +} diff --git a/04_Laravel/routes/api.php b/04_Laravel/routes/api.php index 249852b..48c0e2f 100644 --- a/04_Laravel/routes/api.php +++ b/04_Laravel/routes/api.php @@ -1,19 +1,10 @@ prefix('v1')->group(function () { + Route::get('/track/{awb_no}', [TrackController::class, 'show']); +});