diff --git a/app/Http/Controllers/Admin/ShipmentController.php b/app/Http/Controllers/Admin/ShipmentController.php new file mode 100644 index 0000000..28dc465 --- /dev/null +++ b/app/Http/Controllers/Admin/ShipmentController.php @@ -0,0 +1,157 @@ +toArray(); + + // 2) Load available orders (not used in any shipment) + $availableOrders = Order::whereNotIn('id', $usedOrderIds)->get(); + + // 3) Load all shipments for listing + $shipments = Shipment::latest()->get(); + + // Return your file: resources/views/admin/shipment.blade.php + return view('admin.shipments', compact('availableOrders', 'shipments')); + } + + + + /** + * Store new shipment + */ + public function store(Request $request) + { + $request->validate([ + 'origin' => 'required|string', + 'destination' => 'required|string', + 'shipment_date' => 'required|date', + 'order_ids' => 'required|array|min:1', + ]); + + // ----------------------------- + // PREVENT DUPLICATE ORDERS + // ----------------------------- + foreach ($request->order_ids as $id) { + if (ShipmentItem::where('order_id', $id)->exists()) { + return back()->with('error', "Order ID $id is already assigned to a shipment."); + } + } + + // ----------------------------- + // GENERATE UNIQUE SHIPMENT ID + // ----------------------------- + $year = date('y'); + $prefix = "SHIP-$year-"; + + $lastShipment = Shipment::latest('id')->first(); + $nextNumber = $lastShipment ? intval(substr($lastShipment->shipment_id, -8)) + 1 : 1; + + $newShipmentId = $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT); + + // ----------------------------- + // CALCULATE TOTALS + // ----------------------------- + $orders = Order::whereIn('id', $request->order_ids)->get(); + + $total_ctn = $orders->sum('ctn'); + $total_qty = $orders->sum('qty'); + $total_ttl_qty = $orders->sum('ttl_qty'); + $total_amount = $orders->sum('ttl_amount'); + $total_cbm = $orders->sum('cbm'); + $total_ttl_cbm = $orders->sum('ttl_cbm'); + $total_kg = $orders->sum('kg'); + $total_ttl_kg = $orders->sum('ttl_kg'); + + // ----------------------------- + // CREATE SHIPMENT + //------------------------------- + $shipment = Shipment::create([ + 'shipment_id' => $newShipmentId, + 'origin' => $request->origin, + 'destination' => $request->destination, + 'status' => Shipment::STATUS_PENDING, + 'shipment_date' => $request->shipment_date, + + 'total_ctn' => $total_ctn, + 'total_qty' => $total_qty, + 'total_ttl_qty' => $total_ttl_qty, + 'total_amount' => $total_amount, + 'total_cbm' => $total_cbm, + 'total_ttl_cbm' => $total_ttl_cbm, + 'total_kg' => $total_kg, + 'total_ttl_kg' => $total_ttl_kg, + ]); + + // ----------------------------- + // INSERT SHIPMENT ITEMS + // ----------------------------- + foreach ($orders as $order) { + ShipmentItem::create([ + 'shipment_id' => $shipment->id, + 'order_id' => $order->id, + 'order_ctn' => $order->ctn, + 'order_qty' => $order->qty, + 'order_ttl_qty' => $order->ttl_qty, + 'order_ttl_amount' => $order->ttl_amount, + 'order_ttl_kg' => $order->ttl_kg, + ]); + } + + return redirect()->back()->with('success', "Shipment $newShipmentId created successfully!"); + } + + + + /** + * Show shipment details (for modal popup) + */ + public function show($id) + { + $shipment = Shipment::findOrFail($id); + + // Load full order data from orders table + $orders = Order::whereIn('id', + ShipmentItem::where('shipment_id', $id)->pluck('order_id') + )->get(); + + return response()->json([ + 'shipment' => $shipment, + 'orders' => $orders + ]); + } + + + + /** + * Update Shipment status from action button + */ + public function updateStatus(Request $request) + { + $request->validate([ + 'shipment_id' => 'required|exists:shipments,id', + 'status' => 'required|string' + ]); + + $shipment = Shipment::findOrFail($request->shipment_id); + $shipment->status = $request->status; + $shipment->save(); + + return redirect()->back()->with('success', "Shipment status updated to {$shipment->statusLabel()}"); + } + +} diff --git a/app/Models/Shipment.php b/app/Models/Shipment.php new file mode 100644 index 0000000..95bb5d5 --- /dev/null +++ b/app/Models/Shipment.php @@ -0,0 +1,80 @@ + 'array', + 'shipment_date' => 'date', + ]; + + // --------------------------- + // RELATIONSHIPS + // --------------------------- + + public function items() + { + return $this->hasMany(ShipmentItem::class); + } + + public function orders() + { + return $this->belongsToMany(Order::class, 'shipment_items', 'shipment_id', 'order_id'); + } + + // --------------------------- + // STATUS CONSTANTS + // --------------------------- + + const STATUS_PENDING = 'pending'; + const STATUS_IN_TRANSIT = 'in_transit'; + const STATUS_DISPATCHED = 'dispatched'; + const STATUS_DELIVERED = 'delivered'; + + public static function statusOptions() + { + return [ + self::STATUS_PENDING => 'Pending', + self::STATUS_IN_TRANSIT => 'In Transit', + self::STATUS_DISPATCHED => 'Dispatched', + self::STATUS_DELIVERED => 'Delivered', + ]; + } + + // --------------------------- + // HELPERS + // --------------------------- + + public function totalOrdersCount() + { + return $this->items()->count(); + } + + public function statusLabel() + { + return self::statusOptions()[$this->status] ?? ucfirst($this->status); + } +} diff --git a/app/Models/ShipmentItem.php b/app/Models/ShipmentItem.php new file mode 100644 index 0000000..e004fd1 --- /dev/null +++ b/app/Models/ShipmentItem.php @@ -0,0 +1,46 @@ +belongsTo(Shipment::class); + } + + public function order() + { + return $this->belongsTo(Order::class); + } + + // Helper: return order data with fallback to snapshot + public function getDisplayQty() + { + return $this->order->qty ?? $this->order_qty; + } + + public function getDisplayAmount() + { + return $this->order->ttl_amount ?? $this->order_ttl_amount; + } +} diff --git a/database/migrations/2025_11_14_053837_create_shipments_table.php b/database/migrations/2025_11_14_053837_create_shipments_table.php new file mode 100644 index 0000000..9d81952 --- /dev/null +++ b/database/migrations/2025_11_14_053837_create_shipments_table.php @@ -0,0 +1,63 @@ +bigIncrements('id'); + + // Human-friendly auto-generated shipment id (e.g. SHIP-25-00000001) + $table->string('shipment_id')->unique(); + + // Basic details + $table->string('origin')->nullable(); + $table->string('destination')->nullable(); + + // Totals (calculated when creating shipment) + $table->integer('total_ctn')->default(0)->comment('sum of CTN of selected orders'); + $table->integer('total_qty')->default(0)->comment('sum of qty of selected orders'); + $table->integer('total_ttl_qty')->default(0)->comment('sum of ttl_qty of selected orders'); + $table->decimal('total_amount', 16, 2)->default(0.00)->comment('sum of ttl_amount of selected orders'); + $table->decimal('total_cbm', 14, 3)->default(0.000)->comment('sum cbm'); + $table->decimal('total_ttl_cbm', 14, 3)->default(0.000)->comment('sum ttl cbm'); + $table->decimal('total_kg', 14, 3)->default(0.000)->comment('sum kg'); + $table->decimal('total_ttl_kg', 14, 3)->default(0.000)->comment('sum ttl kg'); + + // status: pending (default), in_transit, dispatched, delivered, cancelled, etc. + $table->string('status')->default('pending'); + + // shipment date (admin can change) + $table->date('shipment_date')->nullable(); + + // optional meta (vehicle, driver etc) + $table->json('meta')->nullable(); + + $table->timestamps(); + + // Indexes for fast filtering + $table->index('shipment_id'); + $table->index('status'); + $table->index('shipment_date'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('shipments'); + } +} diff --git a/database/migrations/2025_11_14_053911_create_shipment_items_table.php b/database/migrations/2025_11_14_053911_create_shipment_items_table.php new file mode 100644 index 0000000..a28d8fb --- /dev/null +++ b/database/migrations/2025_11_14_053911_create_shipment_items_table.php @@ -0,0 +1,51 @@ +bigIncrements('id'); + + // Link to shipments + $table->foreignId('shipment_id')->constrained('shipments')->onDelete('cascade'); + + // Link to orders. assuming orders.id is bigIncrements + $table->foreignId('order_id')->constrained('orders')->onDelete('restrict'); + + // Snapshots (optional) — store basic order totals at time of assignment + $table->integer('order_ctn')->nullable()->default(0); + $table->integer('order_qty')->nullable()->default(0); + $table->integer('order_ttl_qty')->nullable()->default(0); + $table->decimal('order_ttl_amount', 16, 2)->nullable()->default(0.00); + $table->decimal('order_ttl_kg', 14, 3)->nullable()->default(0.000); + + $table->timestamps(); + + // Prevent duplicate assignment of same order to the same shipment + $table->unique(['shipment_id', 'order_id']); + + // We will check order_id uniqueness across shipments in app logic (see below) + $table->index('order_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('shipment_items'); + } +} diff --git a/resources/views/admin/shipments.blade.php b/resources/views/admin/shipments.blade.php index cdd1fc1..2f42e6c 100644 --- a/resources/views/admin/shipments.blade.php +++ b/resources/views/admin/shipments.blade.php @@ -1,12 +1,320 @@ @extends('admin.layouts.app') -@section('page-title', 'Dashboard') +@section('page-title', 'Shipment Management') @section('content') -
-
-

Welcome to the Admin shipment

-

Here you can manage all system modules.

-
+ +
+ + {{-- SUCCESS / ERROR MESSAGES --}} + @if(session('success')) +
{{ session('success') }}
+ @endif + + @if(session('error')) +
{{ session('error') }}
+ @endif + + + + + +
+
+
+ Create Shipment +
+
+ +
+ +
+ @csrf + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
Select Orders for Shipment
+ + {{-- Orders Table --}} +
+ + + + + + + + + + + + + + + + + @forelse($availableOrders as $order) + + + + + + + + + + + + + @empty + + + + @endforelse + +
SelectOrder IDMark NoOriginDestinationCTNQTYTTL QtyAmountKG
+ + {{ $order->order_id }}{{ $order->mark_no }}{{ $order->origin }}{{ $order->destination }}{{ $order->ctn }}{{ $order->qty }}{{ $order->ttl_qty }}₹{{ number_format($order->ttl_amount, 2) }}{{ $order->ttl_kg }}
No available orders
+
+ +
+ +
+ +
+ +
+
+ + + + + + + +
+
+
Shipments List
+
+ +
+ + + + + + + + + + + + + + + + + + + + @forelse($shipments as $ship) + + + + + + + + + + + + + + + + + + + + @empty + + + + @endforelse + + +
#Shipment IDOriginDestinationTotal QTYTotal KGTotal AmountStatusDateAction
{{ $ship->id }} + + {{ $ship->shipment_id }} + + {{ $ship->origin }}{{ $ship->destination }}{{ $ship->total_qty }}{{ $ship->total_kg }}₹{{ number_format($ship->total_amount, 2) }} + {{ ucfirst($ship->status) }} + {{ $ship->shipment_date }} +
+ @csrf + + + +
+
No shipments found
+
+
+ + + + + + + + +
+ + + + + + + + + @endsection diff --git a/routes/web.php b/routes/web.php index f21c935..abdae90 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Admin\AdminAuthController; use App\Http\Controllers\Admin\UserRequestController; use App\Http\Controllers\Admin\AdminMarkListController; use App\Http\Controllers\Admin\AdminOrderController; +use App\Http\Controllers\Admin\ShipmentController; // ------------------------- // Default Front Page @@ -29,7 +30,7 @@ Route::prefix('admin')->middleware('auth:admin')->group(function () { // Dashboard Pages // Route::get('/dashboard', fn() => view('admin.dashboard'))->name('admin.dashboard'); Route::get('/dashboard', [AdminOrderController::class, 'index'])->name('admin.dashboard'); - Route::get('/shipments', fn() => view('admin.shipments'))->name('admin.shipments'); + //Route::get('/shipments', fn() => view('admin.shipments'))->name('admin.shipments'); Route::get('/invoice', fn() => view('admin.invoice'))->name('admin.invoice'); Route::get('/customers', fn() => view('admin.customers'))->name('admin.customers'); Route::get('/reports', fn() => view('admin.reports'))->name('admin.reports'); @@ -77,4 +78,16 @@ Route::prefix('admin')->middleware('auth:admin')->group(function () { + Route::get('/shipments', [ShipmentController::class, 'index']) + ->name('admin.shipments'); + + Route::post('/shipments/store', [ShipmentController::class, 'store']) + ->name('admin.shipments.store'); + + Route::post('/shipments/update-status', [ShipmentController::class, 'updateStatus']) + ->name('admin.shipments.updateStatus'); + + Route::get('/shipments/{id}', [ShipmentController::class, 'show']) + ->name('admin.shipments.show'); + });