Files
hub/app/Http/Controllers/Seller/Processing/ProcessingShipmentController.php
kelly c7d6ee5e21 feat: multiple UI/UX improvements and case-insensitive search
- Refactor New Quote page to enterprise data-entry layout (2-column, dense)
- Add Payment Terms dropdown (COD, NET 15, NET 30, NET 60)
- Fix sidebar menu active states and route names
- Fix brand filter badge visibility on brands page
- Remove company_name references (use business instead)
- Polish Promotions page layout
- Fix double-click issue on sidebar menu collapse
- Make all searches case-insensitive (like -> ilike for PostgreSQL)
2025-12-14 15:36:00 -07:00

259 lines
9.2 KiB
PHP

<?php
namespace App\Http\Controllers\Seller\Processing;
use App\Http\Controllers\Controller;
use App\Models\Business;
use App\Models\Processing\ProcMaterialLot;
use App\Models\Processing\ProcMaterialMovement;
use App\Models\Processing\ProcSalesOrder;
use App\Models\Processing\ProcShipment;
use App\Models\Processing\ProcShipmentLine;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ProcessingShipmentController extends Controller
{
public function index(Request $request, Business $business)
{
$query = ProcShipment::forBusiness($business->id)
->with(['salesOrder.customer'])
->orderByDesc('created_at');
if ($request->filled('status')) {
$query->where('status', $request->status);
}
if ($request->filled('search')) {
$query->where('shipment_number', 'ilike', '%'.$request->search.'%');
}
$shipments = $query->paginate(25);
return view('seller.processing.shipments.index', compact('business', 'shipments'));
}
public function create(Request $request, Business $business)
{
$salesOrders = ProcSalesOrder::forBusiness($business->id)
->whereIn('status', ['confirmed', 'processing'])
->with('customer')
->get();
return view('seller.processing.shipments.create', compact('business', 'salesOrders'));
}
public function store(Request $request, Business $business)
{
$validated = $request->validate([
'shipment_number' => 'required|string|max:100',
'sales_order_id' => 'required|exists:proc_sales_orders,id',
'carrier' => 'nullable|string|max:100',
'tracking_number' => 'nullable|string|max:200',
'ship_date' => 'nullable|date',
'notes' => 'nullable|string',
'lines' => 'required|array|min:1',
'lines.*.material_lot_id' => 'required|exists:proc_material_lots,id',
'lines.*.quantity' => 'required|numeric|min:0',
]);
// Verify sales order belongs to business
$salesOrder = ProcSalesOrder::where('business_id', $business->id)
->findOrFail($validated['sales_order_id']);
DB::transaction(function () use ($validated, $business) {
$shipment = ProcShipment::create([
'business_id' => $business->id,
'sales_order_id' => $validated['sales_order_id'],
'shipment_number' => $validated['shipment_number'],
'carrier' => $validated['carrier'],
'tracking_number' => $validated['tracking_number'],
'ship_date' => $validated['ship_date'],
'status' => 'pending',
'notes' => $validated['notes'],
]);
foreach ($validated['lines'] as $line) {
// Verify material lot belongs to business
$materialLot = ProcMaterialLot::where('business_id', $business->id)
->findOrFail($line['material_lot_id']);
ProcShipmentLine::create([
'shipment_id' => $shipment->id,
'material_lot_id' => $line['material_lot_id'],
'quantity' => $line['quantity'],
'uom' => $materialLot->uom ?? 'g',
]);
}
});
return redirect()
->route('seller.processing.shipments.index', $business)
->with('success', 'Shipment created successfully.');
}
public function show(Request $request, Business $business, ProcShipment $shipment)
{
$this->authorizeForBusiness($shipment, $business);
$shipment->load(['salesOrder.customer', 'lines.materialLot']);
return view('seller.processing.shipments.show', compact('business', 'shipment'));
}
public function edit(Request $request, Business $business, ProcShipment $shipment)
{
$this->authorizeForBusiness($shipment, $business);
if ($shipment->status === 'shipped' || $shipment->status === 'delivered') {
return back()->with('error', 'Cannot edit shipped or delivered shipments.');
}
$salesOrders = ProcSalesOrder::forBusiness($business->id)
->whereIn('status', ['confirmed', 'processing'])
->with('customer')
->get();
$shipment->load('lines.materialLot');
return view('seller.processing.shipments.edit', compact('business', 'shipment', 'salesOrders'));
}
public function update(Request $request, Business $business, ProcShipment $shipment)
{
$this->authorizeForBusiness($shipment, $business);
if ($shipment->status === 'shipped' || $shipment->status === 'delivered') {
return back()->with('error', 'Cannot edit shipped or delivered shipments.');
}
$validated = $request->validate([
'shipment_number' => 'required|string|max:100',
'carrier' => 'nullable|string|max:100',
'tracking_number' => 'nullable|string|max:200',
'ship_date' => 'nullable|date',
'status' => 'required|in:pending,packed,shipped,delivered,cancelled',
'notes' => 'nullable|string',
]);
$shipment->update($validated);
return redirect()
->route('seller.processing.shipments.show', [$business, $shipment])
->with('success', 'Shipment updated successfully.');
}
public function destroy(Request $request, Business $business, ProcShipment $shipment)
{
$this->authorizeForBusiness($shipment, $business);
if ($shipment->status === 'shipped' || $shipment->status === 'delivered') {
return back()->with('error', 'Cannot delete shipped or delivered shipments.');
}
$shipment->lines()->delete();
$shipment->delete();
return redirect()
->route('seller.processing.shipments.index', $business)
->with('success', 'Shipment deleted successfully.');
}
/**
* Mark shipment as packed.
*/
public function pack(Request $request, Business $business, ProcShipment $shipment)
{
$this->authorizeForBusiness($shipment, $business);
if ($shipment->status !== 'pending') {
return back()->with('error', 'Only pending shipments can be packed.');
}
$shipment->update(['status' => 'packed']);
return back()->with('success', 'Shipment marked as packed.');
}
/**
* Ship the shipment and deduct inventory.
*/
public function ship(Request $request, Business $business, ProcShipment $shipment)
{
$this->authorizeForBusiness($shipment, $business);
if ($shipment->status !== 'packed') {
return back()->with('error', 'Only packed shipments can be shipped.');
}
DB::transaction(function () use ($shipment, $business) {
// Deduct inventory from material lots
foreach ($shipment->lines as $line) {
$materialLot = $line->materialLot;
if ($materialLot->weight_available < $line->quantity) {
throw new \Exception("Insufficient quantity in lot {$materialLot->lot_number}");
}
$materialLot->decrement('weight_available', $line->quantity);
// Record material movement
ProcMaterialMovement::create([
'business_id' => $business->id,
'material_lot_id' => $materialLot->id,
'movement_type' => 'ship',
'quantity' => -$line->quantity,
'uom' => $line->uom,
'reference_type' => 'shipment',
'reference_id' => $shipment->id,
'reason' => 'Shipped via '.$shipment->shipment_number,
]);
// Update lot status if depleted
if ($materialLot->weight_available <= 0) {
$materialLot->update(['status' => 'depleted']);
}
}
$shipment->update([
'status' => 'shipped',
'ship_date' => $shipment->ship_date ?? now(),
]);
// Update sales order status
$shipment->salesOrder->update(['status' => 'shipped']);
});
return back()->with('success', 'Shipment marked as shipped. Inventory deducted.');
}
/**
* Mark shipment as delivered.
*/
public function deliver(Request $request, Business $business, ProcShipment $shipment)
{
$this->authorizeForBusiness($shipment, $business);
if ($shipment->status !== 'shipped') {
return back()->with('error', 'Only shipped shipments can be marked as delivered.');
}
$shipment->update([
'status' => 'delivered',
'delivered_at' => now(),
]);
// Complete the sales order
$shipment->salesOrder->update(['status' => 'completed']);
return back()->with('success', 'Shipment marked as delivered.');
}
protected function authorizeForBusiness(ProcShipment $shipment, Business $business): void
{
if ($shipment->business_id !== $business->id) {
abort(403, 'Unauthorized access to this shipment.');
}
}
}