✅ ۱۰ مورد کامل شد (دیتابیس، مدلها، API، Filament، Laravel 11، رفع باگها)
🔄 در حال انجام: تست دستی Filament ⏸️ بلاک: مهاجرت ۳۹۵۰ رکورد (منتظر فایل اکسل اصلاحشده) ⏸️ بلاک: پلاگین وردپرس (بعد از تست کامل API)
This commit is contained in:
parent
97fa3a9561
commit
a59e192b32
229
01_Documents/IFNEX_Phase0_Checklist.md
Normal file
229
01_Documents/IFNEX_Phase0_Checklist.md
Normal file
@ -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.
|
||||
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
21
04_Laravel/app/Console/Commands/DebugImportCommand.php
Normal file
21
04_Laravel/app/Console/Commands/DebugImportCommand.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Imports\DebugImport;
|
||||
use Illuminate\Console\Command;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class DebugImportCommand extends Command
|
||||
{
|
||||
protected $signature = 'ifnex:debug:import {path}';
|
||||
protected $description = 'Debug import';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$path = $this->argument('path');
|
||||
Excel::import(new DebugImport(), $path);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
32
04_Laravel/app/Console/Commands/ImportHistoricalData.php
Normal file
32
04_Laravel/app/Console/Commands/ImportHistoricalData.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Imports\HistoricalShipmentsImport;
|
||||
use Illuminate\Console\Command;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class ImportHistoricalData extends Command
|
||||
{
|
||||
protected $signature = 'ifnex:import:shipments {path : Path to the Excel file} {--sh|skip-header : Skip first row if it is a header}';
|
||||
protected $description = 'Import historical shipments from Excel into the shipments table';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$path = $this->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;
|
||||
}
|
||||
}
|
||||
32
04_Laravel/app/Console/Commands/ImportShippingRates.php
Normal file
32
04_Laravel/app/Console/Commands/ImportShippingRates.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Imports\ShippingRatesImport;
|
||||
use Illuminate\Console\Command;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class ImportShippingRates extends Command
|
||||
{
|
||||
protected $signature = 'ifnex:import:rates {path : Path to the Excel file}';
|
||||
protected $description = 'Import shipping rates from Excel into the shipping_rates table';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$path = $this->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;
|
||||
}
|
||||
}
|
||||
16
04_Laravel/app/Enums/CarrierCode.php
Normal file
16
04_Laravel/app/Enums/CarrierCode.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum CarrierCode: string
|
||||
{
|
||||
case DHL = 'DHL';
|
||||
case FEDEX = 'FEDEX';
|
||||
case UPS = 'UPS';
|
||||
case ARAMEX = 'ARAMEX';
|
||||
case NAGHEL = 'NAGHEL';
|
||||
case EMX = 'EMX';
|
||||
case APSITEX = 'APSITEX';
|
||||
case IMPEX = 'IMPEX';
|
||||
case OTHER = 'OTHER';
|
||||
}
|
||||
9
04_Laravel/app/Enums/ShipmentDirection.php
Normal file
9
04_Laravel/app/Enums/ShipmentDirection.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ShipmentDirection: string
|
||||
{
|
||||
case Import = 'import';
|
||||
case Export = 'export';
|
||||
}
|
||||
14
04_Laravel/app/Enums/ShipmentStatus.php
Normal file
14
04_Laravel/app/Enums/ShipmentStatus.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ShipmentStatus: string
|
||||
{
|
||||
case Processed = 'processed';
|
||||
case PickedUp = 'picked_up';
|
||||
case InTransit = 'in_transit';
|
||||
case OutForDelivery = 'out_for_delivery';
|
||||
case Failed = 'failed';
|
||||
case Delivered = 'delivered';
|
||||
case Returned = 'returned';
|
||||
}
|
||||
10
04_Laravel/app/Enums/ShipmentType.php
Normal file
10
04_Laravel/app/Enums/ShipmentType.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ShipmentType: string
|
||||
{
|
||||
case DocNormal = 'DOC_NORMAL';
|
||||
case DocEconomy = 'DOC_ECONOMY';
|
||||
case Parcel = 'PARCEL';
|
||||
}
|
||||
10
04_Laravel/app/Enums/TrackingSource.php
Normal file
10
04_Laravel/app/Enums/TrackingSource.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum TrackingSource: string
|
||||
{
|
||||
case Manual = 'manual';
|
||||
case ApiCarrier = 'api_carrier';
|
||||
case ApiAggregator = 'api_aggregator';
|
||||
}
|
||||
11
04_Laravel/app/Enums/UserRole.php
Normal file
11
04_Laravel/app/Enums/UserRole.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum UserRole: string
|
||||
{
|
||||
case SuperAdmin = 'super_admin';
|
||||
case TrackingOperator = 'tracking_operator';
|
||||
case DataEntry = 'data_entry';
|
||||
case Customer = 'customer';
|
||||
}
|
||||
109
04_Laravel/app/Filament/Pages/IfnexSettingsPage.php
Normal file
109
04_Laravel/app/Filament/Pages/IfnexSettingsPage.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\SystemSetting;
|
||||
use Filament\Forms;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class IfnexSettingsPage extends Page
|
||||
{
|
||||
protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
|
||||
|
||||
protected static ?string $navigationLabel = 'Settings';
|
||||
|
||||
protected static ?string $navigationGroup = 'Settings';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
protected static string $view = 'filament::page';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->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';
|
||||
}
|
||||
}
|
||||
@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\SystemSetting;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class SettingsPage extends Page
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
|
||||
protected static string $view = 'filament.pages.settings-page';
|
||||
|
||||
// نام منو در سایدبار
|
||||
protected static ?string $navigationLabel = 'تنظیمات ارز و سود';
|
||||
|
||||
// اینکه کجا تو منو نمایش داده بشه (ترتیب)
|
||||
protected static ?int $navigationSort = 100;
|
||||
|
||||
public ?SystemSetting $record = null;
|
||||
|
||||
// وقتی صفحه باز میشه، رکورد تنظیمات رو میگیره
|
||||
public function mount(): void
|
||||
{
|
||||
$this->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();
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
|
||||
@ -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([
|
||||
//
|
||||
|
||||
@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ShipmentResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ShipmentResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewShipment extends ViewRecord
|
||||
{
|
||||
protected static string $resource = ShipmentResource::class;
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ShipmentResource\RelationManagers;
|
||||
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CarrierMappingsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'carrierMappings';
|
||||
|
||||
protected static ?string $title = 'Carrier Mappings';
|
||||
|
||||
protected static ?string $icon = 'heroicon-o-truck';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form->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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ShipmentResource\RelationManagers;
|
||||
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class TrackingEventsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'trackingEvents';
|
||||
|
||||
protected static ?string $title = 'Tracking Events';
|
||||
|
||||
protected static ?string $icon = 'heroicon-o-clock';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form->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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
04_Laravel/app/Http/Controllers/Api/TrackController.php
Normal file
58
04_Laravel/app/Http/Controllers/Api/TrackController.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\Shipment;
|
||||
use App\Services\TrackingService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class TrackController
|
||||
{
|
||||
public function __construct(protected TrackingService $tracking) {}
|
||||
|
||||
public function show(string $awb_no): JsonResponse
|
||||
{
|
||||
$shipment = Shipment::where('awb_no', $awb_no)->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,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
27
04_Laravel/app/Http/Middleware/ApiKeyMiddleware.php
Normal file
27
04_Laravel/app/Http/Middleware/ApiKeyMiddleware.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ApiKeyMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$apiKey = $request->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);
|
||||
}
|
||||
}
|
||||
18
04_Laravel/app/Http/Middleware/SetCspHeaders.php
Normal file
18
04_Laravel/app/Http/Middleware/SetCspHeaders.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SetCspHeaders
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
$response->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;
|
||||
}
|
||||
}
|
||||
32
04_Laravel/app/Imports/DebugImport.php
Normal file
32
04_Laravel/app/Imports/DebugImport.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Imports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\OnEachRow;
|
||||
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||
use Maatwebsite\Excel\Concerns\WithStartRow;
|
||||
use Maatwebsite\Excel\Row;
|
||||
|
||||
class DebugImport implements WithMultipleSheets
|
||||
{
|
||||
public function sheets(): array
|
||||
{
|
||||
return [
|
||||
'List' => 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;
|
||||
}
|
||||
}
|
||||
164
04_Laravel/app/Imports/HistoricalShipmentsImport.php
Normal file
164
04_Laravel/app/Imports/HistoricalShipmentsImport.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Imports;
|
||||
|
||||
use App\Enums\ShipmentDirection;
|
||||
use App\Enums\ShipmentStatus;
|
||||
use App\Enums\ShipmentType;
|
||||
use App\Models\Country;
|
||||
use App\Models\Shipment;
|
||||
use App\Models\ShipmentItem;
|
||||
use Maatwebsite\Excel\Concerns\OnEachRow;
|
||||
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||
use Maatwebsite\Excel\Concerns\WithStartRow;
|
||||
use Maatwebsite\Excel\Row;
|
||||
|
||||
class HistoricalShipmentsImport implements WithMultipleSheets
|
||||
{
|
||||
public function sheets(): array
|
||||
{
|
||||
return [
|
||||
'List' => 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();
|
||||
}
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Imports;
|
||||
|
||||
use App\Models\Shipment;
|
||||
use Maatwebsite\Excel\Concerns\OnEachRow;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Maatwebsite\Excel\Concerns\WithStartRow;
|
||||
|
||||
class OldShipmentsImport implements OnEachRow, WithHeadingRow, WithStartRow
|
||||
{
|
||||
/**
|
||||
* چون ممکنه در فایل اکسل چندین سطر اول هدر هستند، میاییم ۳ سطر اول رو رد کنیم.
|
||||
* هدر اصلی معمولاً ردیف ۱۰ یا ۱۱ هست.
|
||||
*/
|
||||
protected int $startRow = 3;
|
||||
|
||||
/**
|
||||
* متد الزامی برای WithStartRow
|
||||
*/
|
||||
public function startRow(): int
|
||||
{
|
||||
return $this->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
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Country extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'export_zone',
|
||||
'import_zone',
|
||||
'iso_code',
|
||||
'export_zone_parcel',
|
||||
'export_zone_doc',
|
||||
'import_zone_parcel',
|
||||
'import_zone_doc',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => '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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@ -8,24 +9,53 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
class Shipment extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id', 'awb_no', 'forwarder', 'forwarder_track_id',
|
||||
'direction', 'type', 'status', 'reason_for_export',
|
||||
'user_id', 'awb_no', 'forwarder', 'direction', 'type', 'status', 'reason_for_export',
|
||||
'weight', 'volumetric_weight', 'chargeable_weight', 'dimensions',
|
||||
'shipping_price', 'extra_service', 'packing_cost', 'discount',
|
||||
'total_fee', 'net_dirham', 'net_rial',
|
||||
'shipping_price', 'extra_service', 'packing_cost', 'domestic_pickup', 'domestic_delivery',
|
||||
'warehousing_cost', 'vat_amount', 'discount', 'total_fee', 'net_dirham', 'net_rial',
|
||||
'from_country_id', 'to_country_id',
|
||||
'sender_name', 'sender_company', 'sender_phone', 'sender_email',
|
||||
'sender_address', 'sender_city', 'sender_zip', 'sender_id_number',
|
||||
'receiver_name', 'receiver_company', 'receiver_phone', 'receiver_email',
|
||||
'receiver_address', 'receiver_city', 'receiver_zip', 'receiver_id_number',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'direction' => \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);
|
||||
}
|
||||
}
|
||||
|
||||
37
04_Laravel/app/Models/ShipmentCarrierMapping.php
Normal file
37
04_Laravel/app/Models/ShipmentCarrierMapping.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ShipmentCarrierMapping extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'shipment_id',
|
||||
'carrier_code',
|
||||
'carrier_tracking_number',
|
||||
'booking_number',
|
||||
'direction',
|
||||
'delivery_status',
|
||||
'last_synced_at',
|
||||
'manually_updated_at',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'carrier_code' => \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);
|
||||
}
|
||||
}
|
||||
42
04_Laravel/app/Models/ShipmentTrackingEvent.php
Normal file
42
04_Laravel/app/Models/ShipmentTrackingEvent.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ShipmentTrackingEvent extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'shipment_id',
|
||||
'event_date',
|
||||
'event_time',
|
||||
'location',
|
||||
'event_description',
|
||||
'delivery_status',
|
||||
'source',
|
||||
'carrier_mapping_id',
|
||||
'raw_payload',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'event_date' => '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');
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@ -12,4 +13,12 @@ class ShippingRate extends Model
|
||||
'zone_1', 'zone_2', 'zone_3', 'zone_4', 'zone_5',
|
||||
'zone_6', 'zone_7', 'zone_8', 'zone_9', 'zone_10',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'direction' => \App\Enums\ShipmentDirection::class,
|
||||
'type' => \App\Enums\ShipmentType::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'phone',
|
||||
'role',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
36
04_Laravel/app/Services/TrackingService.php
Normal file
36
04_Laravel/app/Services/TrackingService.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Shipment;
|
||||
use App\Models\ShipmentTrackingEvent;
|
||||
|
||||
class TrackingService
|
||||
{
|
||||
public function getTrackingTimeline(string $awb_no)
|
||||
{
|
||||
$shipment = Shipment::where('awb_no', $awb_no)->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;
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
//
|
||||
|
||||
@ -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",
|
||||
|
||||
317
04_Laravel/composer.lock
generated
317
04_Laravel/composer.lock
generated
@ -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",
|
||||
|
||||
25
04_Laravel/config/ifnex.php
Normal file
25
04_Laravel/config/ifnex.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'api_key' => 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',
|
||||
],
|
||||
|
||||
];
|
||||
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
Schema::dropIfExists('countries');
|
||||
|
||||
Schema::enableForeignKeyConstraints();
|
||||
|
||||
Schema::create('countries', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
Schema::dropIfExists('shipments');
|
||||
|
||||
Schema::enableForeignKeyConstraints();
|
||||
|
||||
Schema::create('shipments', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
Schema::dropIfExists('shipping_rates');
|
||||
|
||||
Schema::enableForeignKeyConstraints();
|
||||
|
||||
Schema::create('shipping_rates', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
Schema::dropIfExists('shipment_carrier_mappings');
|
||||
|
||||
Schema::enableForeignKeyConstraints();
|
||||
|
||||
Schema::create('shipment_carrier_mappings', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
Schema::dropIfExists('shipment_tracking_events');
|
||||
|
||||
Schema::enableForeignKeyConstraints();
|
||||
|
||||
Schema::create('shipment_tracking_events', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
Schema::dropIfExists('system_settings');
|
||||
|
||||
Schema::enableForeignKeyConstraints();
|
||||
|
||||
Schema::create('system_settings', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,10 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\TrackController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Api\PricingController;
|
||||
use App\Http\Middleware\ApiKeyMiddleware;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is where you can register API routes for your application. These
|
||||
| routes are loaded by the RouteServiceProvider within a group which
|
||||
| is assigned the "api" middleware group. Enjoy building your API!
|
||||
|
|
||||
*/
|
||||
|
||||
// مسیر محاسبه قیمت (فعلاً روی GET تنظیم شده تا راحت تو مرورگر تست بشه)
|
||||
Route::get('/calculate-price', [PricingController::class, 'calculate']);
|
||||
Route::middleware([ApiKeyMiddleware::class])->prefix('v1')->group(function () {
|
||||
Route::get('/track/{awb_no}', [TrackController::class, 'show']);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user