- 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)
259 lines
9.2 KiB
PHP
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.');
|
|
}
|
|
}
|
|
}
|