diff --git a/.env.example b/.env.example index c187aef..16a85a2 100644 --- a/.env.example +++ b/.env.example @@ -20,12 +20,12 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=sqlite -# DB_HOST=127.0.0.1 -# DB_PORT=3306 -# DB_DATABASE=laravel -# DB_USERNAME=root -# DB_PASSWORD= +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=kent_logistics6 +DB_USERNAME=root +DB_PASSWORD= SESSION_DRIVER=database SESSION_LIFETIME=120 diff --git a/app/Exports/InvoicesExport.php b/app/Exports/InvoicesExport.php new file mode 100644 index 0000000..d43606f --- /dev/null +++ b/app/Exports/InvoicesExport.php @@ -0,0 +1,63 @@ +request = $request; + } + + public function view(): View + { + $request = $this->request; + + $invoices = DB::table('invoices') + ->leftJoin('containers', 'containers.id', '=', 'invoices.container_id') + ->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no') + ->select( + 'invoices.invoice_number', + 'invoices.invoice_date', + 'invoices.mark_no', + 'containers.container_number', + 'containers.container_date', + DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'), + DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name'), + 'invoices.final_amount', + 'invoices.final_amount_with_gst', + 'invoices.status as invoice_status' + ) + ->when($request->filled('search'), function ($q) use ($request) { + $search = trim($request->search); + $q->where(function ($qq) use ($search) { + $qq->where('invoices.invoice_number', 'like', "%{$search}%") + ->orWhere('invoices.mark_no', 'like', "%{$search}%") + ->orWhere('containers.container_number', 'like', "%{$search}%") + ->orWhere('mark_list.company_name', 'like', "%{$search}%") + ->orWhere('mark_list.customer_name', 'like', "%{$search}%"); + }); + }) + ->when($request->filled('status'), function ($q) use ($request) { + $q->where('invoices.status', $request->status); + }) + ->when($request->filled('from_date'), function ($q) use ($request) { + $q->whereDate('invoices.invoice_date', '>=', $request->from_date); + }) + ->when($request->filled('to_date'), function ($q) use ($request) { + $q->whereDate('invoices.invoice_date', '<=', $request->to_date); + }) + ->orderByDesc('containers.container_date') + ->orderByDesc('invoices.id') + ->get(); + + return view('admin.pdf.invoices_excel', compact('invoices')); + } +} diff --git a/app/Http/Controllers/Admin/AdminInvoiceController.php b/app/Http/Controllers/Admin/AdminInvoiceController.php index 19d313e..00cbe82 100644 --- a/app/Http/Controllers/Admin/AdminInvoiceController.php +++ b/app/Http/Controllers/Admin/AdminInvoiceController.php @@ -3,21 +3,51 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; -use Illuminate\Http\Request; use App\Models\Invoice; use App\Models\InvoiceItem; -use Mpdf\Mpdf; use App\Models\InvoiceInstallment; +use App\Models\InvoiceChargeGroup; +use App\Models\InvoiceChargeGroupItem; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Storage; +use Mpdf\Mpdf; class AdminInvoiceController extends Controller { // ------------------------------------------------------------- // INVOICE LIST PAGE // ------------------------------------------------------------- - public function index() + public function index(Request $request) { - $invoices = Invoice::with(['order.shipments'])->latest()->get(); + $query = Invoice::with(['items', 'customer', 'container']); + + // Search (जर पुढे form मधून पाठवलंस तर) + if ($request->filled('search')) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('invoice_number', 'like', "%{$search}%") + ->orWhere('customer_name', 'like', "%{$search}%"); + }); + } + + // Status filter + if ($request->filled('status') && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Date range filter (invoice_date वर) + if ($request->filled('start_date')) { + $query->whereDate('invoice_date', '>=', $request->start_date); + } + + if ($request->filled('end_date')) { + $query->whereDate('invoice_date', '<=', $request->end_date); + } + + // Latest first + $invoices = $query->latest()->get(); + return view('admin.invoice', compact('invoices')); } @@ -26,13 +56,16 @@ class AdminInvoiceController extends Controller // ------------------------------------------------------------- public function popup($id) { - $invoice = Invoice::with(['items', 'order', 'installments'])->findOrFail($id); + $invoice = Invoice::with([ + 'items', + 'customer', + 'container', + 'chargeGroups.items.item', + ])->findOrFail($id); - $shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) { - $q->where('order_id', $invoice->order_id); - })->first(); + $shipment = null; - return view('admin.popup_invoice', compact('invoice', 'shipment')); + return view('admin.popup_invoice', compact('invoice', 'shipment')); } // ------------------------------------------------------------- @@ -40,76 +73,102 @@ class AdminInvoiceController extends Controller // ------------------------------------------------------------- public function edit($id) { - $invoice = Invoice::with(['order.shipments'])->findOrFail($id); - $shipment = $invoice->order?->shipments?->first(); + $invoice = Invoice::with([ + 'items', + 'customer', + 'container', + 'chargeGroups.items', + ])->findOrFail($id); - return view('admin.invoice_edit', compact('invoice', 'shipment')); + $shipment = null; + + $groupedItemIds = $invoice->chargeGroups + ->flatMap(function ($group) { + return $group->items->pluck('invoice_item_id'); + }) + ->unique() + ->values() + ->toArray(); + + return view('admin.invoice_edit', compact('invoice', 'shipment', 'groupedItemIds')); } // ------------------------------------------------------------- - // UPDATE INVOICE + // UPDATE INVOICE (HEADER LEVEL) // ------------------------------------------------------------- public function update(Request $request, $id) { - Log::info("🟡 Invoice Update Request Received", [ + Log::info('🟡 Invoice Update Request Received', [ 'invoice_id' => $id, - 'request' => $request->all() + 'request' => $request->all(), ]); $invoice = Invoice::findOrFail($id); $data = $request->validate([ - 'invoice_date' => 'required|date', - 'due_date' => 'required|date|after_or_equal:invoice_date', - 'final_amount' => 'required|numeric|min:0', - 'tax_type' => 'required|in:gst,igst', - 'tax_percent' => 'required|numeric|min:0|max:28', - 'status' => 'required|in:pending,paid,overdue', - 'notes' => 'nullable|string', + 'invoice_date' => 'required|date', + 'due_date' => 'required|date|after_or_equal:invoice_date', + 'final_amount' => 'required|numeric|min:0', + 'tax_type' => 'required|in:gst,igst', + 'tax_percent' => 'required|numeric|min:0|max:28', + 'status' => 'required|in:pending,paid,overdue', + 'notes' => 'nullable|string', ]); - Log::info("✅ Validated Invoice Update Data", $data); + Log::info('✅ Validated Invoice Update Data', $data); - $finalAmount = floatval($data['final_amount']); - $taxPercent = floatval($data['tax_percent']); - $taxAmount = 0; + $finalAmount = (float) $data['final_amount']; + $taxPercent = (float) $data['tax_percent']; if ($data['tax_type'] === 'gst') { - Log::info("🟢 GST Selected", compact('taxPercent')); + Log::info('🟢 GST Selected', compact('taxPercent')); + $data['cgst_percent'] = $taxPercent / 2; $data['sgst_percent'] = $taxPercent / 2; $data['igst_percent'] = 0; } else { - Log::info("🔵 IGST Selected", compact('taxPercent')); + Log::info('🔵 IGST Selected', compact('taxPercent')); + $data['cgst_percent'] = 0; $data['sgst_percent'] = 0; $data['igst_percent'] = $taxPercent; } - $taxAmount = ($finalAmount * $taxPercent) / 100; + $gstAmount = ($finalAmount * $taxPercent) / 100; + $data['gst_amount'] = $gstAmount; + $data['final_amount_with_gst'] = $finalAmount + $gstAmount; + $data['gst_percent'] = $taxPercent; - $data['gst_amount'] = $taxAmount; - $data['final_amount_with_gst'] = $finalAmount + $taxAmount; - $data['gst_percent'] = $taxPercent; - - Log::info("📌 Final Calculated Invoice Values", [ - 'invoice_id' => $invoice->id, - 'final_amount' => $finalAmount, - 'gst_amount' => $data['gst_amount'], - 'final_amount_with_gst' => $data['final_amount_with_gst'], - 'tax_type' => $data['tax_type'], - 'cgst_percent' => $data['cgst_percent'], - 'sgst_percent' => $data['sgst_percent'], - 'igst_percent' => $data['igst_percent'], + Log::info('📌 Final Calculated Invoice Values', [ + 'invoice_id' => $invoice->id, + 'final_amount' => $finalAmount, + 'gst_amount' => $data['gst_amount'], + 'final_amount_with_gst' => $data['final_amount_with_gst'], + 'tax_type' => $data['tax_type'], + 'cgst_percent' => $data['cgst_percent'], + 'sgst_percent' => $data['sgst_percent'], + 'igst_percent' => $data['igst_percent'], ]); $invoice->update($data); - Log::info("✅ Invoice Updated Successfully", [ - 'invoice_id' => $invoice->id + Log::info('✅ Invoice Updated Successfully', [ + 'invoice_id' => $invoice->id, + ]); + + $invoice->refresh(); + Log::info('🔍 Invoice AFTER UPDATE (DB values)', [ + 'invoice_id' => $invoice->id, + 'final_amount' => $invoice->final_amount, + 'gst_percent' => $invoice->gst_percent, + 'gst_amount' => $invoice->gst_amount, + 'final_amount_with_gst' => $invoice->final_amount_with_gst, + 'tax_type' => $invoice->tax_type, + 'cgst_percent' => $invoice->cgst_percent, + 'sgst_percent' => $invoice->sgst_percent, + 'igst_percent' => $invoice->igst_percent, ]); - // regenerate PDF $this->generateInvoicePDF($invoice); return redirect() @@ -117,50 +176,120 @@ class AdminInvoiceController extends Controller ->with('success', 'Invoice updated & PDF generated successfully.'); } + // ------------------------------------------------------------- + // 🔹 UPDATE INVOICE ITEMS (price + ttl_amount) + // ------------------------------------------------------------- + public function updateItems(Request $request, Invoice $invoice) + { + Log::info('🟡 Invoice Items Update Request', [ + 'invoice_id' => $invoice->id, + 'payload' => $request->all(), + ]); + + $data = $request->validate([ + 'items' => ['required', 'array'], + 'items.*.price' => ['required', 'numeric', 'min:0'], + 'items.*.ttl_amount' => ['required', 'numeric', 'min:0'], + ]); + + $itemsInput = $data['items']; + + foreach ($itemsInput as $itemId => $itemData) { + $item = InvoiceItem::where('id', $itemId) + ->where('invoice_id', $invoice->id) + ->first(); + + if (!$item) { + Log::warning('Invoice item not found or mismatched invoice', [ + 'invoice_id' => $invoice->id, + 'item_id' => $itemId, + ]); + continue; + } + + $item->price = $itemData['price']; + $item->ttl_amount = $itemData['ttl_amount']; + $item->save(); + } + + $newBaseAmount = InvoiceItem::where('invoice_id', $invoice->id) + ->sum('ttl_amount'); + + $taxType = $invoice->tax_type; + $cgstPercent = (float) ($invoice->cgst_percent ?? 0); + $sgstPercent = (float) ($invoice->sgst_percent ?? 0); + $igstPercent = (float) ($invoice->igst_percent ?? 0); + + $gstPercent = 0; + if ($taxType === 'gst') { + $gstPercent = $cgstPercent + $sgstPercent; + } elseif ($taxType === 'igst') { + $gstPercent = $igstPercent; + } + + $gstAmount = $newBaseAmount * $gstPercent / 100; + $finalWithGst = $newBaseAmount + $gstAmount; + + $invoice->final_amount = $newBaseAmount; + $invoice->gst_amount = $gstAmount; + $invoice->final_amount_with_gst = $finalWithGst; + $invoice->gst_percent = $gstPercent; + $invoice->save(); + + Log::info('✅ Invoice items updated & totals recalculated', [ + 'invoice_id' => $invoice->id, + 'final_amount' => $invoice->final_amount, + 'gst_amount' => $invoice->gst_amount, + 'final_amount_with_gst' => $invoice->final_amount_with_gst, + 'tax_type' => $invoice->tax_type, + 'cgst_percent' => $invoice->cgst_percent, + 'sgst_percent' => $invoice->sgst_percent, + 'igst_percent' => $invoice->igst_percent, + ]); + + return back()->with('success', 'Invoice items updated successfully.'); + } + // ------------------------------------------------------------- // PDF GENERATION USING mPDF // ------------------------------------------------------------- public function generateInvoicePDF($invoice) { - $invoice->load(['items', 'order.shipments']); - $shipment = $invoice->order?->shipments?->first(); + $invoice->load(['items', 'customer', 'container']); + $shipment = null; + $fileName = 'invoice-' . $invoice->invoice_number . '.pdf'; - $folder = public_path('invoices/'); + $folder = public_path('invoices/'); if (!file_exists($folder)) { mkdir($folder, 0777, true); } $filePath = $folder . $fileName; + if (file_exists($filePath)) { unlink($filePath); } - $mpdf = new Mpdf(['mode' => 'utf-8', 'format' => 'A4', 'default_font' => 'sans-serif']); - $html = view('admin.pdf.invoice', ['invoice' => $invoice, 'shipment' => $shipment])->render(); + $mpdf = new Mpdf([ + 'mode' => 'utf-8', + 'format' => 'A4', + 'default_font' => 'sans-serif', + ]); + + $html = view('admin.pdf.invoice', [ + 'invoice' => $invoice, + 'shipment' => $shipment, + ])->render(); + $mpdf->WriteHTML($html); $mpdf->Output($filePath, 'F'); + $invoice->update(['pdf_path' => 'invoices/' . $fileName]); } - public function downloadInvoice($id) -{ - $invoice = Invoice::findOrFail($id); - - // Generate PDF if missing - if ( - !$invoice->pdf_path || - !file_exists(public_path($invoice->pdf_path)) - ) { - $this->generateInvoicePDF($invoice); - $invoice->refresh(); - } - - return response()->download(public_path($invoice->pdf_path)); -} - // ------------------------------------------------------------- - // INSTALLMENTS (ADD/DELETE) + // INSTALLMENTS (ADD) // ------------------------------------------------------------- public function storeInstallment(Request $request, $invoice_id) { @@ -171,16 +300,14 @@ class AdminInvoiceController extends Controller 'amount' => 'required|numeric|min:1', ]); - $invoice = Invoice::findOrFail($invoice_id); - + $invoice = Invoice::findOrFail($invoice_id); $paidTotal = $invoice->installments()->sum('amount'); - // Use GST-inclusive total for all calculations/checks $remaining = $invoice->final_amount_with_gst - $paidTotal; if ($request->amount > $remaining) { return response()->json([ - 'status' => 'error', - 'message' => 'Installment amount exceeds remaining balance.' + 'status' => 'error', + 'message' => 'Installment amount exceeds remaining balance.', ], 422); } @@ -194,52 +321,96 @@ class AdminInvoiceController extends Controller $newPaid = $paidTotal + $request->amount; - // Mark as 'paid' if GST-inclusive total is cleared if ($newPaid >= $invoice->final_amount_with_gst) { $invoice->update(['status' => 'paid']); - - $this->generateInvoicePDF($invoice); } return response()->json([ - 'status' => 'success', - 'message' => 'Installment added successfully.', - 'installment' => $installment, - 'totalPaid' => $newPaid, - 'gstAmount' => $invoice->gst_amount, - 'finalAmountWithGst' => $invoice->final_amount_with_gst, - 'baseAmount' => $invoice->final_amount, - 'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid), - 'isCompleted' => $newPaid >= $invoice->final_amount_with_gst + 'status' => 'success', + 'message' => 'Installment added successfully.', + 'installment' => $installment, + 'totalPaid' => $newPaid, + 'gstAmount' => $invoice->gst_amount, + 'finalAmountWithGst' => $invoice->final_amount_with_gst, + 'baseAmount' => $invoice->final_amount, + 'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid), + 'isCompleted' => $newPaid >= $invoice->final_amount_with_gst, ]); } + + // ------------------------------------------------------------- + // INSTALLMENTS (DELETE) + // ------------------------------------------------------------- public function deleteInstallment($id) { $installment = InvoiceInstallment::findOrFail($id); - $invoice = $installment->invoice; - + $invoice = $installment->invoice; + $installment->delete(); - + $paidTotal = $invoice->installments()->sum('amount'); $remaining = $invoice->final_amount_with_gst - $paidTotal; - - // Update status if not fully paid anymore - if ($remaining > 0 && $invoice->status === "paid") { - $invoice->update(['status' => 'pending']); - $this->generateInvoicePDF($invoice); + if ($remaining > 0 && $invoice->status === 'paid') { + $invoice->update(['status' => 'pending']); } - + return response()->json([ - 'status' => 'success', - 'message' => 'Installment deleted.', - 'totalPaid' => $paidTotal, - 'gstAmount' => $invoice->gst_amount, - 'finalAmountWithGst' => $invoice->final_amount_with_gst, - 'baseAmount' => $invoice->final_amount, - 'remaining' => $remaining, - 'isZero' => $paidTotal == 0 + 'status' => 'success', + 'message' => 'Installment deleted.', + 'totalPaid' => $paidTotal, + 'gstAmount' => $invoice->gst_amount, + 'finalAmountWithGst' => $invoice->final_amount_with_gst, + 'baseAmount' => $invoice->final_amount, + 'remaining' => $remaining, + 'isZero' => $paidTotal == 0, ]); } - + + // ------------------------------------------------------------- + // CHARGE GROUP SAVE (NEW) + // ------------------------------------------------------------- + public function storeChargeGroup(Request $request, $invoiceId) + { + $invoice = Invoice::with('items')->findOrFail($invoiceId); + + $data = $request->validate([ + 'group_name' => 'nullable|string|max:255', + 'basis_type' => 'required|in:ttl_qty,amount,ttl_cbm,ttl_kg', + 'basis_value' => 'required|numeric', + 'rate' => 'required|numeric|min:0.0001', + 'auto_total' => 'required|numeric|min:0.01', + 'item_ids' => 'required|array', + 'item_ids.*' => 'integer|exists:invoice_items,id', + ]); + + $group = InvoiceChargeGroup::create([ + 'invoice_id' => $invoice->id, + 'group_name' => $data['group_name'] ?? null, + 'basis_type' => $data['basis_type'], + 'basis_value' => $data['basis_value'], + 'rate' => $data['rate'], + 'total_charge' => $data['auto_total'], + ]); + + foreach ($data['item_ids'] as $itemId) { + InvoiceChargeGroupItem::create([ + 'group_id' => $group->id, + 'invoice_item_id' => $itemId, + ]); + } + + return redirect() + ->back() + ->with('success', 'Charge group saved successfully.'); + } + + public function download(Invoice $invoice) + { + if (!$invoice->pdf_path || !Storage::exists($invoice->pdf_path)) { + return back()->with('error', 'PDF not found.'); + } + + return Storage::download($invoice->pdf_path, $invoice->invoice_number . '.pdf'); + } } diff --git a/app/Http/Controllers/Admin/AdminOrderController.php b/app/Http/Controllers/Admin/AdminOrderController.php index 8d887ff..9bfaa3d 100644 --- a/app/Http/Controllers/Admin/AdminOrderController.php +++ b/app/Http/Controllers/Admin/AdminOrderController.php @@ -10,26 +10,89 @@ use App\Models\MarkList; use App\Models\Invoice; use App\Models\InvoiceItem; use App\Models\User; +use App\Models\Container; +use App\Models\Admin; +use App\Models\Shipment; use PDF; use Maatwebsite\Excel\Facades\Excel; use App\Exports\OrdersExport; use App\Imports\OrderItemsPreviewImport; - - use Illuminate\Validation\ValidationException; - +use Illuminate\Support\Facades\DB; +use App\Exports\InvoicesExport; class AdminOrderController extends Controller { /* --------------------------- - * LIST / DASHBOARD + * DASHBOARD (old UI: stats + recent orders) * ---------------------------*/ - public function index() + public function dashboard() { - $orders = Order::latest()->get(); - $markList = MarkList::where('status', 'active')->get(); + $totalOrders = Order::count(); + $pendingOrders = Order::where('status', 'pending')->count(); + $totalShipments = Shipment::count(); + $totalItems = OrderItem::count(); + $totalRevenue = Invoice::sum('final_amount_with_gst'); + $activeCustomers = User::where('status', 'active')->count(); + $inactiveCustomers = User::where('status', 'inactive')->count(); + $totalStaff = Admin::where('type', 'staff')->count(); - return view('admin.dashboard', compact('orders', 'markList')); + $markList = MarkList::where('status', 'active')->get(); + $orders = Order::latest()->get(); + + return view('admin.dashboard', compact( + 'totalOrders', + 'pendingOrders', + 'totalShipments', + 'totalItems', + 'totalRevenue', + 'activeCustomers', + 'inactiveCustomers', + 'totalStaff', + 'orders', + 'markList' + )); + } + + /* --------------------------- + * LIST (new: Invoices Management for Orders page) + * ---------------------------*/ + public function index(Request $request) + { + $invoices = DB::table('invoices') + ->leftJoin('containers', 'containers.id', '=', 'invoices.container_id') + ->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no') + ->select( + 'invoices.id', + 'invoices.invoice_number', + 'invoices.invoice_date', + 'invoices.final_amount', + 'invoices.final_amount_with_gst', + 'invoices.status as invoice_status', + 'invoices.mark_no', + 'containers.container_number', + 'containers.container_date', + DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'), + DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name') + ) + ->when($request->filled('search'), function ($q) use ($request) { + $search = trim($request->search); + $q->where(function ($qq) use ($search) { + $qq->where('invoices.invoice_number', 'like', "%{$search}%") + ->orWhere('containers.container_number', 'like', "%{$search}%") + ->orWhere('invoices.mark_no', 'like', "%{$search}%") + ->orWhere('mark_list.company_name', 'like', "%{$search}%") + ->orWhere('mark_list.customer_name', 'like', "%{$search}%"); + }); + }) + ->when($request->filled('status'), function ($q) use ($request) { + $q->where('invoices.status', $request->status); + }) + ->orderByDesc('invoices.invoice_date') // इथे बदल + ->orderByDesc('invoices.id') // same-date साठी tie-breaker + ->get(); + + return view('admin.orders', compact('invoices')); } /* --------------------------- @@ -68,42 +131,39 @@ class AdminOrderController extends Controller * ORDER ITEM MANAGEMENT (existing orders) * ---------------------------*/ public function addItem(Request $request, $orderId) -{ - $order = Order::findOrFail($orderId); + { + $order = Order::findOrFail($orderId); - $data = $request->validate([ - 'description' => 'required|string', - 'ctn' => 'nullable|numeric', - 'qty' => 'nullable|numeric', - 'unit' => 'nullable|string', - 'price' => 'nullable|numeric', - 'cbm' => 'nullable|numeric', - 'kg' => 'nullable|numeric', - 'shop_no' => 'nullable|string', - ]); + $data = $request->validate([ + 'description' => 'required|string', + 'ctn' => 'nullable|numeric', + 'qty' => 'nullable|numeric', + 'unit' => 'nullable|string', + 'price' => 'nullable|numeric', + 'cbm' => 'nullable|numeric', + 'kg' => 'nullable|numeric', + 'shop_no' => 'nullable|string', + ]); - // ✅ BACKEND CALCULATION - $ctn = (float) ($data['ctn'] ?? 0); - $qty = (float) ($data['qty'] ?? 0); - $price = (float) ($data['price'] ?? 0); - $cbm = (float) ($data['cbm'] ?? 0); - $kg = (float) ($data['kg'] ?? 0); + $ctn = (float) ($data['ctn'] ?? 0); + $qty = (float) ($data['qty'] ?? 0); + $price = (float) ($data['price'] ?? 0); + $cbm = (float) ($data['cbm'] ?? 0); + $kg = (float) ($data['kg'] ?? 0); - $data['ttl_qty'] = $ctn * $qty; - $data['ttl_amount'] = $data['ttl_qty'] * $price; - $data['ttl_cbm'] = $cbm * $ctn; - $data['ttl_kg'] = $ctn * $kg; + $data['ttl_qty'] = $ctn * $qty; + $data['ttl_amount'] = $data['ttl_qty'] * $price; + $data['ttl_cbm'] = $cbm * $ctn; + $data['ttl_kg'] = $ctn * $kg; - $data['order_id'] = $order->id; + $data['order_id'] = $order->id; - OrderItem::create($data); + OrderItem::create($data); - $this->recalcTotals($order); - $this->updateInvoiceFromOrder($order); - - return redirect()->back()->with('success', 'Item added and totals updated.'); -} + $this->recalcTotals($order); + return redirect()->back()->with('success', 'Item added and totals updated.'); + } public function deleteItem($id) { @@ -113,7 +173,6 @@ class AdminOrderController extends Controller $item->delete(); $this->recalcTotals($order); - $this->updateInvoiceFromOrder($order); return redirect()->back()->with('success', 'Item deleted and totals updated.'); } @@ -126,7 +185,6 @@ class AdminOrderController extends Controller $item->restore(); $this->recalcTotals($order); - $this->updateInvoiceFromOrder($order); return redirect()->back()->with('success', 'Item restored and totals updated.'); } @@ -175,14 +233,14 @@ class AdminOrderController extends Controller $items = $order->items()->get(); $order->update([ - 'ctn' => (int) $items->sum(fn($i) => (int) ($i->ctn ?? 0)), - 'qty' => (int) $items->sum(fn($i) => (int) ($i->qty ?? 0)), - 'ttl_qty' => (int) $items->sum(fn($i) => (int) ($i->ttl_qty ?? 0)), - 'ttl_amount' => (float) $items->sum(fn($i) => (float) ($i->ttl_amount ?? 0)), - 'cbm' => (float) $items->sum(fn($i) => (float) ($i->cbm ?? 0)), - 'ttl_cbm' => (float) $items->sum(fn($i) => (float) ($i->ttl_cbm ?? 0)), - 'kg' => (float) $items->sum(fn($i) => (float) ($i->kg ?? 0)), - 'ttl_kg' => (float) $items->sum(fn($i) => (float) ($i->ttl_kg ?? 0)), + 'ctn' => (int) $items->sum(fn($i) => (int) ($i->ctn ?? 0)), + 'qty' => (int) $items->sum(fn($i) => (int) ($i->qty ?? 0)), + 'ttl_qty' => (int) $items->sum(fn($i) => (int) ($i->ttl_qty ?? 0)), + 'ttl_amount'=> (float) $items->sum(fn($i) => (float) ($i->ttl_amount ?? 0)), + 'cbm' => (float) $items->sum(fn($i) => (float) ($i->cbm ?? 0)), + 'ttl_cbm' => (float) $items->sum(fn($i) => (float) ($i->ttl_cbm ?? 0)), + 'kg' => (float) $items->sum(fn($i) => (float) ($i->kg ?? 0)), + 'ttl_kg' => (float) $items->sum(fn($i) => (float) ($i->ttl_kg ?? 0)), ]); } @@ -285,7 +343,6 @@ class AdminOrderController extends Controller $order = Order::with([ 'markList', 'items', - 'invoice.items', 'shipments' => function ($q) use ($id) { $q->whereHas('orders', function ($oq) use ($id) { $oq->where('orders.id', $id) @@ -304,14 +361,14 @@ class AdminOrderController extends Controller 'order_id' => $order->order_id, 'status' => $order->status, 'totals' => [ - 'ctn' => $order->ctn, - 'qty' => $order->qty, - 'ttl_qty' => $order->ttl_qty, - 'cbm' => $order->cbm, - 'ttl_cbm' => $order->ttl_cbm, - 'kg' => $order->kg, - 'ttl_kg' => $order->ttl_kg, - 'amount' => $order->ttl_amount, + 'ctn' => $order->ctn, + 'qty' => $order->qty, + 'ttl_qty' => $order->ttl_qty, + 'cbm' => $order->cbm, + 'ttl_cbm' => $order->ttl_cbm, + 'kg' => $order->kg, + 'ttl_kg' => $order->ttl_kg, + 'amount' => $order->ttl_amount, ], 'items' => $order->items, ]; @@ -365,29 +422,6 @@ class AdminOrderController extends Controller } $invoiceData = null; - if ($order->invoice) { - $invoice = $order->invoice; - $invoiceData = [ - 'invoice_no' => $invoice->invoice_number, - 'status' => $invoice->status, - 'invoice_date' => $invoice->invoice_date, - 'due_date' => $invoice->due_date, - 'customer' => [ - 'name' => $invoice->customer_name, - 'mobile' => $invoice->customer_mobile, - 'email' => $invoice->customer_email, - 'address' => $invoice->customer_address, - 'pincode' => $invoice->pincode, - ], - 'items' => $invoice->items, - 'summary' => [ - 'amount' => $invoice->final_amount, - 'cgst' => 0, - 'sgst' => 0, - 'total' => $invoice->final_amount_with_gst, - ], - ]; - } return view('admin.see_order', compact( 'order', @@ -398,14 +432,13 @@ class AdminOrderController extends Controller } /* --------------------------- - * FILTERED LIST + EXPORTS + * FILTERED LIST + EXPORTS (old orders listing) * ---------------------------*/ public function orderShow() { $orders = Order::with([ 'markList', 'shipments', - 'invoice' ])->latest('id')->get(); return view('admin.orders', compact('orders')); @@ -414,7 +447,7 @@ class AdminOrderController extends Controller private function buildOrdersQueryFromRequest(Request $request) { $query = Order::query() - ->with(['markList', 'invoice', 'shipments']); + ->with(['markList', 'shipments']); if ($request->filled('search')) { $search = trim($request->search); @@ -427,23 +460,12 @@ class AdminOrderController extends Controller ->orWhere('origin', 'like', "%{$search}%") ->orWhere('destination', 'like', "%{$search}%"); }) - ->orWhereHas('invoice', function ($q3) use ($search) { - $q3->where('invoice_number', 'like', "%{$search}%"); - }) ->orWhereHas('shipments', function ($q4) use ($search) { $q4->where('shipments.shipment_id', 'like', "%{$search}%"); }); }); } - if ($request->filled('status')) { - $query->where(function ($q) use ($request) { - $q->whereHas('invoice', function ($q2) use ($request) { - $q2->where('status', $request->status); - })->orWhereDoesntHave('invoice'); - }); - } - if ($request->filled('shipment')) { $query->where(function ($q) use ($request) { $q->whereHas('shipments', function ($q2) use ($request) { @@ -465,62 +487,83 @@ class AdminOrderController extends Controller public function downloadPdf(Request $request) { - $orders = $this->buildOrdersQueryFromRequest($request)->get(); - $filters = [ - 'search' => $request->search, - 'status' => $request->status, - 'shipment' => $request->shipment, - 'from' => $request->from_date, - 'to' => $request->to_date, - ]; - - $pdf = PDF::loadView('admin.orders.pdf', compact('orders', 'filters')) + $invoices = DB::table('invoices') + ->leftJoin('containers', 'containers.id', '=', 'invoices.container_id') + ->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no') + ->select( + 'invoices.invoice_number', + 'invoices.invoice_date', + 'invoices.mark_no', + 'containers.container_number', + 'containers.container_date', + DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'), + DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name'), + 'invoices.final_amount', + 'invoices.final_amount_with_gst', + 'invoices.status as invoice_status' + ) + ->when($request->filled('search'), function ($q) use ($request) { + $search = trim($request->search); + $q->where(function ($qq) use ($search) { + $qq->where('invoices.invoice_number', 'like', "%{$search}%") + ->orWhere('invoices.mark_no', 'like', "%{$search}%") + ->orWhere('containers.container_number', 'like', "%{$search}%") + ->orWhere('mark_list.company_name', 'like', "%{$search}%") + ->orWhere('mark_list.customer_name', 'like', "%{$search}%"); + }); + }) + ->when($request->filled('status'), function ($q) use ($request) { + $q->where('invoices.status', $request->status); + }) + ->when($request->filled('from_date'), function ($q) use ($request) { + $q->whereDate('invoices.invoice_date', '>=', $request->from_date); + }) + ->when($request->filled('to_date'), function ($q) use ($request) { + $q->whereDate('invoices.invoice_date', '<=', $request->to_date); + }) + ->orderByDesc('containers.container_date') + ->orderByDesc('invoices.id') + ->get(); + + $pdf = PDF::loadView('admin.pdf.invoices_report', compact('invoices')) ->setPaper('a4', 'landscape'); - + return $pdf->download( - 'orders-report-' . now()->format('Y-m-d') . '.pdf' + 'invoices-report-' . now()->format('Y-m-d') . '.pdf' ); } - + public function downloadExcel(Request $request) { return Excel::download( - new OrdersExport($request), - 'orders-report-' . now()->format('Y-m-d') . '.xlsx' + new InvoicesExport($request), + 'invoices-report-' . now()->format('Y-m-d') . '.xlsx' ); } /* -------------------------------------------------- * NEW: Create Order + Invoice directly from popup - * route: admin.orders.temp.add (Create New Order form) * --------------------------------------------------*/ public function addTempItem(Request $request) { - // 1) order-level fields $request->validate([ 'mark_no' => 'required', - 'origin' => 'required', - 'destination' => 'required', + 'origin' => 'nullable', + 'destination' => 'nullable', ]); - // 2) multi-row items $items = $request->validate([ 'items' => 'required|array', 'items.*.description' => 'required|string', 'items.*.ctn' => 'nullable|numeric', 'items.*.qty' => 'nullable|numeric', - 'items.*.unit' => 'nullable|string', 'items.*.price' => 'nullable|numeric', - 'items.*.cbm' => 'nullable|numeric', - 'items.*.kg' => 'nullable|numeric', - 'items.*.shop_no' => 'nullable|string', ])['items']; - // रिकामे rows काढा $items = array_filter($items, function ($row) { return trim($row['description'] ?? '') !== ''; }); @@ -529,38 +572,31 @@ class AdminOrderController extends Controller return back()->with('error', 'Add at least one item.'); } - // ✅ BACKEND CALCULATION (DO NOT TRUST FRONTEND) foreach ($items as &$item) { - $ctn = (float) ($item['ctn'] ?? 0); $qty = (float) ($item['qty'] ?? 0); $price = (float) ($item['price'] ?? 0); $cbm = (float) ($item['cbm'] ?? 0); $kg = (float) ($item['kg'] ?? 0); - // Calculated fields $item['ttl_qty'] = $ctn * $qty; $item['ttl_amount'] = $item['ttl_qty'] * $price; $item['ttl_cbm'] = $cbm * $ctn; $item['ttl_kg'] = $ctn * $kg; } - unset($item); // VERY IMPORTANT + unset($item); + $total_ctn = array_sum(array_column($items, 'ctn')); + $total_qty = array_sum(array_column($items, 'qty')); + $total_ttl_qty = array_sum(array_column($items, 'ttl_qty')); + $total_amount = array_sum(array_column($items, 'ttl_amount')); + $total_cbm = array_sum(array_column($items, 'cbm')); + $total_ttl_cbm = array_sum(array_column($items, 'ttl_cbm')); + $total_kg = array_sum(array_column($items, 'kg')); + $total_ttl_kg = array_sum(array_column($items, 'ttl_kg')); - // 3) totals - $total_ctn = array_sum(array_column($items, 'ctn')); - $total_qty = array_sum(array_column($items, 'qty')); - $total_ttl_qty = array_sum(array_column($items, 'ttl_qty')); - $total_amount = array_sum(array_column($items, 'ttl_amount')); - $total_cbm = array_sum(array_column($items, 'cbm')); - $total_ttl_cbm = array_sum(array_column($items, 'ttl_cbm')); - $total_kg = array_sum(array_column($items, 'kg')); - $total_ttl_kg = array_sum(array_column($items, 'ttl_kg')); - - // 4) order id generate $orderId = $this->generateOrderId(); - // 5) order create $order = Order::create([ 'order_id' => $orderId, 'mark_no' => $request->mark_no, @@ -577,7 +613,6 @@ class AdminOrderController extends Controller 'status' => 'order_placed', ]); - // 6) order items foreach ($items as $item) { OrderItem::create([ 'order_id' => $order->id, @@ -596,17 +631,14 @@ class AdminOrderController extends Controller ]); } - // 7) invoice number $invoiceNumber = $this->generateInvoiceNumber(); - // 8) customer fetch $markList = MarkList::where('mark_no', $order->mark_no)->first(); $customer = null; if ($markList && $markList->customer_id) { $customer = User::where('customer_id', $markList->customer_id)->first(); } - // 9) invoice create $invoice = Invoice::create([ 'order_id' => $order->id, 'customer_id' => $customer->id ?? null, @@ -631,7 +663,6 @@ class AdminOrderController extends Controller 'pdf_path' => null, ]); - // 10) invoice items foreach ($order->items as $item) { InvoiceItem::create([ 'invoice_id' => $invoice->id, @@ -658,114 +689,72 @@ class AdminOrderController extends Controller * UPDATE ORDER ITEM (existing orders) * ---------------------------*/ public function updateItem(Request $request, $id) -{ - $item = OrderItem::findOrFail($id); - $order = $item->order; - - $request->validate([ - 'description' => 'required|string', - 'ctn' => 'nullable|numeric', - 'qty' => 'nullable|numeric', - 'unit' => 'nullable|string', - 'price' => 'nullable|numeric', - 'cbm' => 'nullable|numeric', - 'kg' => 'nullable|numeric', - 'shop_no' => 'nullable|string', - ]); - - // ✅ BACKEND CALCULATION - $ctn = (float) ($request->ctn ?? 0); - $qty = (float) ($request->qty ?? 0); - $price = (float) ($request->price ?? 0); - $cbm = (float) ($request->cbm ?? 0); - $kg = (float) ($request->kg ?? 0); - - $item->update([ - 'description' => $request->description, - 'ctn' => $ctn, - 'qty' => $qty, - 'ttl_qty' => $ctn * $qty, - 'unit' => $request->unit, - 'price' => $price, - 'ttl_amount' => ($ctn * $qty) * $price, - 'cbm' => $cbm, - 'ttl_cbm' => $cbm * $ctn, - 'kg' => $kg, - 'ttl_kg' => $ctn * $kg, - 'shop_no' => $request->shop_no, - ]); - - $this->recalcTotals($order); - $this->updateInvoiceFromOrder($order); - - return back()->with('success', 'Item updated successfully'); -} - - - private function updateInvoiceFromOrder(Order $order) { - $invoice = Invoice::where('order_id', $order->id)->first(); + $item = OrderItem::findOrFail($id); + $order = $item->order; - if (!$invoice) { - return; - } - - $invoice->final_amount = $order->ttl_amount; - $invoice->gst_percent = 0; - $invoice->gst_amount = 0; - $invoice->final_amount_with_gst = $order->ttl_amount; - $invoice->save(); - - InvoiceItem::where('invoice_id', $invoice->id)->delete(); - - foreach ($order->items as $item) { - InvoiceItem::create([ - 'invoice_id' => $invoice->id, - 'description' => $item->description, - 'ctn' => $item->ctn, - 'qty' => $item->qty, - 'ttl_qty' => $item->ttl_qty, - 'unit' => $item->unit, - 'price' => $item->price, - 'ttl_amount' => $item->ttl_amount, - 'cbm' => $item->cbm, - 'ttl_cbm' => $item->ttl_cbm, - 'kg' => $item->kg, - 'ttl_kg' => $item->ttl_kg, - 'shop_no' => $item->shop_no, - ]); - } - } - - - -public function uploadExcelPreview(Request $request) -{ - try { $request->validate([ - 'excel' => 'required|file|mimes:xlsx,xls' + 'description' => 'required|string', + 'ctn' => 'nullable|numeric', + 'qty' => 'nullable|numeric', + 'unit' => 'nullable|string', + 'price' => 'nullable|numeric', + 'cbm' => 'nullable|numeric', + 'kg' => 'nullable|numeric', + 'shop_no' => 'nullable|string', ]); - $import = new OrderItemsPreviewImport(); - Excel::import($import, $request->file('excel')); + $ctn = (float) ($request->ctn ?? 0); + $qty = (float) ($request->qty ?? 0); + $price = (float) ($request->price ?? 0); + $cbm = (float) ($request->cbm ?? 0); + $kg = (float) ($request->kg ?? 0); - return response()->json([ - 'success' => true, - 'items' => $import->rows + $item->update([ + 'description' => $request->description, + 'ctn' => $ctn, + 'qty' => $qty, + 'ttl_qty' => $ctn * $qty, + 'unit' => $request->unit, + 'price' => $price, + 'ttl_amount' => ($ctn * $qty) * $price, + 'cbm' => $cbm, + 'ttl_cbm' => $cbm * $ctn, + 'kg' => $kg, + 'ttl_kg' => $ctn * $kg, + 'shop_no' => $request->shop_no, ]); - } catch (ValidationException $e) { - return response()->json([ - 'success' => false, - 'message' => 'Invalid Excel file format' - ], 422); - } catch (\Throwable $e) { - \Log::error($e); - return response()->json([ - 'success' => false, - 'message' => 'Server error' - ], 500); + + $this->recalcTotals($order); + + return back()->with('success', 'Item updated successfully'); + } + + public function uploadExcelPreview(Request $request) + { + try { + $request->validate([ + 'excel' => 'required|file|mimes:xlsx,xls' + ]); + + $import = new OrderItemsPreviewImport(); + Excel::import($import, $request->file('excel')); + + return response()->json([ + 'success' => true, + 'items' => $import->rows + ]); + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid Excel file format' + ], 422); + } catch (\Throwable $e) { + \Log::error($e); + return response()->json([ + 'success' => false, + 'message' => 'Server error' + ], 500); + } } } - - -} diff --git a/app/Http/Controllers/Admin/AdminReportController.php b/app/Http/Controllers/Admin/AdminReportController.php index c47cb62..60f0dab 100644 --- a/app/Http/Controllers/Admin/AdminReportController.php +++ b/app/Http/Controllers/Admin/AdminReportController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; -use App\Models\Order; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -12,45 +11,98 @@ class AdminReportController extends Controller /** * Display the reports page with joined data */ - public function index(Request $request) - { - // ------------------------------- - // FETCH REPORT DATA - // ONLY orders that have BOTH: - // 1. Invoice - // 2. Shipment - // ------------------------------- + // public function index(Request $request) + // { + /********************************************************* + * OLD FLOW (Order + Shipment + Invoice) + * फक्त reference साठी ठेवलेला, वापरत नाही. + *********************************************************/ + + /* $reports = DB::table('orders') ->join('shipment_items', 'shipment_items.order_id', '=', 'orders.id') ->join('shipments', 'shipments.id', '=', 'shipment_items.shipment_id') ->join('invoices', 'invoices.order_id', '=', 'orders.id') ->leftJoin('mark_list', 'mark_list.mark_no', '=', 'orders.mark_no') ->leftJoin('users', 'users.customer_id', '=', 'mark_list.customer_id') + ->select(...) + ->orderBy('shipments.shipment_date', 'desc') + ->get(); + */ + /********************************************************* + * NEW FLOW (Container + Invoice + MarkList) + *********************************************************/ + + // $reports = DB::table('invoices') + // ->join('containers', 'containers.id', '=', 'invoices.containerid') + // ->leftJoin('mark_list', 'mark_list.markno', '=', 'invoices.markno') + // ->select( + // 'invoices.id as invoicepk', + // 'invoices.invoicenumber', + // 'invoices.invoicedate', + // 'invoices.finalamount', + // 'invoices.finalamountwithgst', + // 'invoices.gstpercent', + // 'invoices.gstamount', + // 'invoices.status as invoicestatus', + // 'invoices.markno', + + // 'containers.id as containerpk', + // 'containers.containernumber', + // 'containers.containerdate', + // 'containers.containername', + + // 'mark_list.companyname', + // 'mark_list.customername' + // ) + // ->orderBy('containers.containerdate', 'desc') + // ->get(); + + // return view('admin.reports', compact('reports')); + // } + + + + public function index(Request $request) + { + $reports = DB::table('invoices') + ->join('containers', 'containers.id', '=', 'invoices.container_id') + ->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no') ->select( - 'orders.id as order_pk', - 'orders.order_id', - 'orders.mark_no', - 'orders.origin', - 'orders.destination', - - 'shipments.id as shipment_pk', - 'shipments.shipment_id', - 'shipments.status as shipment_status', - 'shipments.shipment_date', - + // INVOICE + 'invoices.id as invoicepk', 'invoices.invoice_number', 'invoices.invoice_date', 'invoices.final_amount', - 'invoices.status as invoice_status', - - 'mark_list.company_name', - 'mark_list.customer_name' + 'invoices.final_amount_with_gst', + 'invoices.gst_percent', + 'invoices.gst_amount', + 'invoices.status as invoicestatus', + 'invoices.mark_no', + + // CONTAINER + 'containers.id as containerpk', + 'containers.container_number', + 'containers.container_date', + 'containers.container_name', + + // RAW FIELDS (for reference/debug if needed) + 'invoices.company_name as inv_company_name', + 'invoices.customer_name as inv_customer_name', + 'mark_list.company_name as ml_company_name', + 'mark_list.customer_name as ml_customer_name', + + // FINAL FIELDS (automatically pick invoice first, else mark_list) + DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'), + DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name') ) + ->orderBy('invoices.invoice_date', 'desc') + ->orderBy('invoices.id', 'desc') - ->orderBy('shipments.shipment_date', 'desc') ->get(); - + return view('admin.reports', compact('reports')); } + } diff --git a/app/Http/Controllers/ContainerController.php b/app/Http/Controllers/ContainerController.php new file mode 100644 index 0000000..af3ecd8 --- /dev/null +++ b/app/Http/Controllers/ContainerController.php @@ -0,0 +1,755 @@ +latest()->get(); + + $containers->each(function ($container) { + $rows = $container->rows; + + $totalCtn = 0; + $totalQty = 0; + $totalCbm = 0; + $totalKg = 0; + + $ctnKeys = ['CTN', 'CTNS']; + $qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES']; + $cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM']; + $kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT']; + + $getFirstNumeric = function (array $data, array $possibleKeys) { + $normalizedMap = []; + + foreach ($data as $key => $value) { + if ($key === null || $key === '') continue; + + $normKey = strtoupper((string)$key); + $normKey = str_replace([' ', '/', '-', '.'], '', $normKey); + $normalizedMap[$normKey] = $value; + } + + foreach ($possibleKeys as $search) { + $normSearch = strtoupper($search); + $normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch); + + foreach ($normalizedMap as $nKey => $value) { + if ( + strpos($nKey, $normSearch) !== false && + (is_numeric($value) || (is_string($value) && is_numeric(trim($value)))) + ) { + return (float) trim($value); + } + } + } + + return 0; + }; + + foreach ($rows as $row) { + $data = $row->data ?? []; + + $totalCtn += $getFirstNumeric($data, $ctnKeys); + $totalQty += $getFirstNumeric($data, $qtyKeys); + $totalCbm += $getFirstNumeric($data, $cbmKeys); + $totalKg += $getFirstNumeric($data, $kgKeys); + } + + $container->summary = [ + 'total_ctn' => round($totalCtn, 2), + 'total_qty' => round($totalQty, 2), + 'total_cbm' => round($totalCbm, 3), + 'total_kg' => round($totalKg, 2), + ]; + }); + + return view('admin.container', compact('containers')); + } + + public function create() + { + return view('admin.container_create'); + } + + private function isValidExcelFormat($rows, $header) + { + if (empty($header) || count($rows) < 2) return false; + + $validKeywords = [ + 'MARK', 'DESCRIPTION', 'DESC', 'CTN', 'CTNS', 'QTY', 'TOTALQTY', 'ITLQTY', 'ITL QTY', + 'UNIT', 'CBM', 'TOTAL CBM', 'KG', 'TOTAL KG', + 'METAL BUCKLE', 'WATCH MOVEMENT', 'STEEL BOTTLE', + 'MEHULPAID', 'ITEM NO', 'ITEM NO.', 'SAHILPAID', 'PINAKIN', 'GST', + 'MOON LAMP', 'TRANSPARENT BOTTLE', 'PLASTIC FONDANT', + ]; + + $headerText = implode(' ', array_map('strtoupper', $header)); + $requiredHeaders = ['CTN', 'QTY', 'DESCRIPTION', 'DESC']; + + $hasValidHeaders = false; + foreach ($requiredHeaders as $key) { + if (stripos($headerText, $key) !== false) { + $hasValidHeaders = true; + break; + } + } + + if (!$hasValidHeaders) return false; + + $dataPreview = ''; + for ($i = 0; $i < min(5, count($rows)); $i++) { + $rowText = implode(' ', array_slice($rows[$i], 0, 10)); + $dataPreview .= ' ' . strtoupper((string)$rowText); + } + + $validMatches = 0; + foreach ($validKeywords as $keyword) { + if (stripos($headerText . $dataPreview, strtoupper($keyword)) !== false) { + $validMatches++; + } + } + + return $validMatches >= 3; + } + + private function normalizeKey($value): string + { + $norm = strtoupper((string)$value); + return str_replace([' ', '/', '-', '.'], '', $norm); + } + + public function store(Request $request) + { + $request->validate([ + 'container_name' => 'required|string', + 'container_number' => 'required|string|unique:containers,container_number', + 'container_date' => 'required|date', + 'excel_file' => 'required|file|mimes:xls,xlsx', + ]); + + $file = $request->file('excel_file'); + $sheets = Excel::toArray([], $file); + $rows = $sheets[0] ?? []; + + if (count($rows) < 2) { + return back() + ->withErrors(['excel_file' => 'Excel file is empty.']) + ->withInput(); + } + + // HEADER DETECTION + $headerRowIndex = null; + $header = []; + + foreach ($rows as $i => $row) { + $trimmed = array_map(fn($v) => trim((string)$v), $row); + $nonEmpty = array_filter($trimmed, fn($v) => $v !== ''); + if (empty($nonEmpty)) continue; + + if (count($nonEmpty) >= 4) { + $headerRowIndex = $i; + $header = $trimmed; + break; + } + } + + if ($headerRowIndex === null) { + return back() + ->withErrors(['excel_file' => 'Header row not found in Excel.']) + ->withInput(); + } + + if (!$this->isValidExcelFormat($rows, $header)) { + return back() + ->withErrors(['excel_file' => 'Only MEHUL / SAHIL / PINAKIN / GST loading list formats allowed.']) + ->withInput(); + } + + // COLUMN INDEXES + $essentialColumns = [ + 'desc_col' => null, + 'ctn_col' => null, + 'qty_col' => null, + 'totalqty_col' => null, + 'unit_col' => null, + 'price_col' => null, + 'amount_col' => null, + 'cbm_col' => null, + 'totalcbm_col' => null, + 'kg_col' => null, + 'totalkg_col' => null, + 'itemno_col' => null, + ]; + + foreach ($header as $colIndex => $headingText) { + if (empty($headingText)) continue; + + $normalized = $this->normalizeKey($headingText); + + if (strpos($normalized, 'DESCRIPTION') !== false || strpos($normalized, 'DESC') !== false) { + $essentialColumns['desc_col'] = $colIndex; + } elseif (strpos($normalized, 'CTN') !== false || strpos($normalized, 'CTNS') !== false) { + $essentialColumns['ctn_col'] = $colIndex; + } elseif ( + strpos($normalized, 'ITLQTY') !== false || + strpos($normalized, 'TOTALQTY') !== false || + strpos($normalized, 'TTLQTY') !== false + ) { + $essentialColumns['totalqty_col'] = $colIndex; + } elseif (strpos($normalized, 'QTY') !== false) { + $essentialColumns['qty_col'] = $colIndex; + } elseif (strpos($normalized, 'UNIT') !== false) { + $essentialColumns['unit_col'] = $colIndex; + } elseif (strpos($normalized, 'PRICE') !== false) { + $essentialColumns['price_col'] = $colIndex; + } elseif (strpos($normalized, 'AMOUNT') !== false) { + $essentialColumns['amount_col'] = $colIndex; + } elseif (strpos($normalized, 'TOTALCBM') !== false || strpos($normalized, 'ITLCBM') !== false) { + $essentialColumns['totalcbm_col'] = $colIndex; + } elseif (strpos($normalized, 'CBM') !== false) { + $essentialColumns['cbm_col'] = $colIndex; + } elseif (strpos($normalized, 'TOTALKG') !== false || strpos($normalized, 'TTKG') !== false) { + $essentialColumns['totalkg_col'] = $colIndex; + } elseif (strpos($normalized, 'KG') !== false) { + $essentialColumns['kg_col'] = $colIndex; + } elseif ( + strpos($normalized, 'MARKNO') !== false || + strpos($normalized, 'MARK') !== false || + strpos($normalized, 'ITEMNO') !== false || + strpos($normalized, 'ITEM') !== false + ) { + $essentialColumns['itemno_col'] = $colIndex; + } + } + + if (is_null($essentialColumns['itemno_col'])) { + return back() + ->withErrors(['excel_file' => 'Mark / Item column not found in Excel (expected headers like MARK NO / Mark_No / Item_No).']) + ->withInput(); + } + + // ROWS CLEANING + $dataRows = array_slice($rows, $headerRowIndex + 1); + $cleanedRows = []; + $unmatchedRowsData = []; + + foreach ($dataRows as $offset => $row) { + $trimmedRow = array_map(fn($v) => trim((string)$v), $row); + $nonEmptyCells = array_filter($trimmedRow, fn($v) => $v !== ''); + if (count($nonEmptyCells) < 2) continue; + + $rowText = strtoupper(implode(' ', $trimmedRow)); + if ( + stripos($rowText, 'TOTAL') !== false || + stripos($rowText, 'TTL') !== false || + stripos($rowText, 'GRAND') !== false + ) { + continue; + } + + $descValue = ''; + if ($essentialColumns['desc_col'] !== null) { + $descValue = trim($row[$essentialColumns['desc_col']] ?? ''); + } + + if ($essentialColumns['desc_col'] !== null && $descValue === '' && count($nonEmptyCells) >= 1) { + continue; + } + + $cleanedRows[] = [ + 'row' => $row, + 'offset' => $offset, + ]; + } + + if (empty($cleanedRows)) { + return back() + ->withErrors(['excel_file' => 'No valid item rows found in Excel.']) + ->withInput(); + } + + /* + * FORMULA CHECK – UPDATED WITH AMOUNT FIX + NUMBER SANITIZER + */ + + $cleanNumber = function ($value) { + if (is_string($value)) { + $value = str_replace(',', '', trim($value)); + } + return is_numeric($value) ? (float)$value : 0; + }; + + $formulaErrors = []; + + foreach ($cleanedRows as $item) { + $row = $item['row']; + $offset = $item['offset']; + + $ctn = $essentialColumns['ctn_col'] !== null ? $cleanNumber($row[$essentialColumns['ctn_col']] ?? 0) : 0; + $qty = $essentialColumns['qty_col'] !== null ? $cleanNumber($row[$essentialColumns['qty_col']] ?? 0) : 0; + $ttlQ = $essentialColumns['totalqty_col'] !== null ? $cleanNumber($row[$essentialColumns['totalqty_col']] ?? 0) : 0; + $cbm = $essentialColumns['cbm_col'] !== null ? $cleanNumber($row[$essentialColumns['cbm_col']] ?? 0) : 0; + $ttlC = $essentialColumns['totalcbm_col'] !== null ? $cleanNumber($row[$essentialColumns['totalcbm_col']] ?? 0) : 0; + $kg = $essentialColumns['kg_col'] !== null ? $cleanNumber($row[$essentialColumns['kg_col']] ?? 0) : 0; + $ttlK = $essentialColumns['totalkg_col'] !== null ? $cleanNumber($row[$essentialColumns['totalkg_col']] ?? 0) : 0; + + $price = $essentialColumns['price_col'] !== null ? $cleanNumber($row[$essentialColumns['price_col']] ?? 0) : 0; + $ttlAmount = $essentialColumns['amount_col'] !== null ? $cleanNumber($row[$essentialColumns['amount_col']] ?? 0) : 0; + + $desc = $essentialColumns['desc_col'] !== null ? (string)($row[$essentialColumns['desc_col']] ?? '') : ''; + $mark = $essentialColumns['itemno_col'] !== null ? (string)($row[$essentialColumns['itemno_col']] ?? '') : ''; + + // expected + $expTtlQty = $qty * $ctn; + $expTtlCbm = $cbm * $ctn; + $expTtlKg = $kg * $ctn; + $expTtlAmount = ($qty * $ctn) * $price; + + $rowErrors = []; + + if (abs($ttlQ - $expTtlQty) > 0.01) { + $rowErrors['TOTAL QTY'] = [ + 'actual' => $ttlQ, + 'expected' => $expTtlQty, + ]; + } + + if (abs($ttlC - $expTtlCbm) > 0.0005) { + $rowErrors['TOTAL CBM'] = [ + 'actual' => $ttlC, + 'expected' => $expTtlCbm, + ]; + } + + if (abs($ttlK - $expTtlKg) > 0.01) { + $rowErrors['TOTAL KG'] = [ + 'actual' => $ttlK, + 'expected' => $expTtlKg, + ]; + } + + if ($essentialColumns['amount_col'] !== null && $essentialColumns['price_col'] !== null) { + if (abs($ttlAmount - $expTtlAmount) > 0.01) { + $rowErrors['TOTAL AMOUNT'] = [ + 'actual' => $ttlAmount, + 'expected' => $expTtlAmount, + ]; + } + } + + if (!empty($rowErrors)) { + // full row data map for excel table + $rowData = []; + foreach ($header as $colIndex => $headingText) { + $value = $row[$colIndex] ?? null; + if (is_string($value)) $value = trim($value); + $rowData[$headingText] = $value; + } + + $formulaErrors[] = [ + 'excel_row' => $headerRowIndex + 1 + $offset, + 'mark_no' => $mark, + 'description' => $desc, + 'errors' => $rowErrors, + 'data' => $rowData, + ]; + } + } + + // MARK CHECK: strict - collect ALL marks + unmatched rows + $marksFromExcel = []; + foreach ($cleanedRows as $item) { + $row = $item['row']; + $rawMark = $row[$essentialColumns['itemno_col']] ?? null; + $mark = trim((string)($rawMark ?? '')); + if ($mark !== '') { + $marksFromExcel[] = $mark; + } + } + + $marksFromExcel = array_values(array_unique($marksFromExcel)); + + if (empty($marksFromExcel)) { + return back() + ->withErrors(['excel_file' => 'No mark numbers found in Excel file.']) + ->withInput(); + } + + $validMarks = MarkList::whereIn('mark_no', $marksFromExcel) + ->where('status', 'active') + ->pluck('mark_no') + ->toArray(); + + $unmatchedMarks = array_values(array_diff($marksFromExcel, $validMarks)); + + $markErrors = []; + + if (!empty($unmatchedMarks)) { + + foreach ($cleanedRows as $item) { + $row = $item['row']; + $offset = $item['offset']; + $rowMark = trim((string)($row[$essentialColumns['itemno_col']] ?? '')); + + if ($rowMark === '' || !in_array($rowMark, $unmatchedMarks)) { + continue; + } + + $rowData = []; + foreach ($header as $colIndex => $headingText) { + $value = $row[$colIndex] ?? null; + if (is_string($value)) $value = trim($value); + $rowData[$headingText] = $value; + } + + $markErrors[] = [ + 'excel_row' => $headerRowIndex + 1 + $offset, + 'mark_no' => $rowMark, + 'data' => $rowData, + ]; + } + } + + if (!empty($formulaErrors) || !empty($markErrors)) { + return back() + ->withInput() + ->with([ + 'formula_errors' => $formulaErrors, + 'mark_errors' => $markErrors, + ]); + } + + // STEP 1: Marks → customers mapping + grouping + $markRecords = MarkList::whereIn('mark_no', $marksFromExcel) + ->where('status', 'active') + ->get(); + + $markToCustomerId = []; + $markToSnapshot = []; + + foreach ($markRecords as $mr) { + $markToCustomerId[$mr->mark_no] = $mr->customer_id; + + $markToSnapshot[$mr->mark_no] = [ + 'customer_name' => $mr->customer_name, + 'company_name' => $mr->company_name, + 'mobile_no' => $mr->mobile_no, + ]; + } + + $groupedByCustomer = []; + + foreach ($cleanedRows as $item) { + $row = $item['row']; + $offset = $item['offset']; + + $rawMark = $row[$essentialColumns['itemno_col']] ?? null; + $mark = trim((string)($rawMark ?? '')); + + if ($mark === '') { + continue; + } + + $customerId = $markToCustomerId[$mark] ?? null; + if (!$customerId) { + continue; + } + + if (!isset($groupedByCustomer[$customerId])) { + $groupedByCustomer[$customerId] = []; + } + + $groupedByCustomer[$customerId][] = [ + 'row' => $row, + 'offset' => $offset, + 'mark' => $mark, + ]; + } + + // STEP 2: Container + ContainerRows save + $container = Container::create([ + 'container_name' => $request->container_name, + 'container_number' => $request->container_number, + 'container_date' => $request->container_date, + 'status' => 'pending', + ]); + + $path = $file->store('containers'); + $container->update(['excel_file' => $path]); + + $savedCount = 0; + + foreach ($cleanedRows as $item) { + $row = $item['row']; + $offset = $item['offset']; + + $data = []; + foreach ($header as $colIndex => $headingText) { + $value = $row[$colIndex] ?? null; + if (is_string($value)) $value = trim($value); + $data[$headingText] = $value; + } + + ContainerRow::create([ + 'container_id' => $container->id, + 'row_index' => $headerRowIndex + 1 + $offset, + 'data' => $data, + ]); + + $savedCount++; + } + + // STEP 3: per-customer invoices + invoice items + $invoiceCount = 0; + + foreach ($groupedByCustomer as $customerId => $rowsForCustomer) { + if (empty($rowsForCustomer)) { + continue; + } + + $firstMark = $rowsForCustomer[0]['mark']; + $snap = $markToSnapshot[$firstMark] ?? null; + + $invoice = new Invoice(); + $invoice->container_id = $container->id; + // $invoice->customer_id = $customerId; + + // इथे Mark No सेट करतो + $invoice->mark_no = $firstMark; + + $invoice->invoice_number = $this->generateInvoiceNumber(); + $invoice->invoice_date = now()->toDateString(); + $invoice->due_date = null; + + if ($snap) { + $invoice->customer_name = $snap['customer_name'] ?? null; + $invoice->company_name = $snap['company_name'] ?? null; + $invoice->customer_mobile = $snap['mobile_no'] ?? null; + } + + $invoice->final_amount = 0; + $invoice->gst_percent = 0; + $invoice->gst_amount = 0; + $invoice->final_amount_with_gst = 0; + + $invoice->customer_email = null; + $invoice->customer_address = null; + $invoice->pincode = null; + + $uniqueMarks = array_unique(array_column($rowsForCustomer, 'mark')); + $invoice->notes = 'Auto-created from Container ' . $container->container_number + . ' for Mark(s): ' . implode(', ', $uniqueMarks); + $invoice->pdf_path = null; + $invoice->status = 'pending'; + + $invoice->save(); + $invoiceCount++; + + $totalAmount = 0; + + foreach ($rowsForCustomer as $item) { + $row = $item['row']; + $offset = $item['offset']; + + $description = $essentialColumns['desc_col'] !== null ? ($row[$essentialColumns['desc_col']] ?? null) : null; + $ctn = $essentialColumns['ctn_col'] !== null ? (int) ($row[$essentialColumns['ctn_col']] ?? 0) : 0; + $qty = $essentialColumns['qty_col'] !== null ? (int) ($row[$essentialColumns['qty_col']] ?? 0) : 0; + $ttlQty = $essentialColumns['totalqty_col'] !== null ? (int) ($row[$essentialColumns['totalqty_col']] ?? 0) : $qty; + $unit = $essentialColumns['unit_col'] !== null ? ($row[$essentialColumns['unit_col']] ?? null) : null; + $price = $essentialColumns['price_col'] !== null ? (float) ($row[$essentialColumns['price_col']] ?? 0) : 0; + $ttlAmount = $essentialColumns['amount_col'] !== null ? (float) ($row[$essentialColumns['amount_col']] ?? 0) : 0; + $cbm = $essentialColumns['cbm_col'] !== null ? (float) ($row[$essentialColumns['cbm_col']] ?? 0) : 0; + $ttlCbm = $essentialColumns['totalcbm_col'] !== null ? (float) ($row[$essentialColumns['totalcbm_col']] ?? $cbm) : $cbm; + $kg = $essentialColumns['kg_col'] !== null ? (float) ($row[$essentialColumns['kg_col']] ?? 0) : 0; + $ttlKg = $essentialColumns['totalkg_col'] !== null ? (float) ($row[$essentialColumns['totalkg_col']] ?? $kg) : $kg; + + $rowIndex = $headerRowIndex + 1 + $offset; + + InvoiceItem::create([ + 'invoice_id' => $invoice->id, + 'container_id' => $container->id, + 'container_row_index' => $rowIndex, + 'description' => $description, + 'ctn' => $ctn, + 'qty' => $qty, + 'ttl_qty' => $ttlQty, + 'unit' => $unit, + 'price' => $price, + 'ttl_amount' => $ttlAmount, + 'cbm' => $cbm, + 'ttl_cbm' => $ttlCbm, + 'kg' => $kg, + 'ttl_kg' => $ttlKg, + 'shop_no' => null, + ]); + + $totalAmount += $ttlAmount; + } + + $invoice->final_amount = $totalAmount; + $invoice->gst_percent = 0; + $invoice->gst_amount = 0; + $invoice->final_amount_with_gst = $totalAmount; + + $invoice->save(); + } + + $msg = "Container '{$container->container_number}' created with {$savedCount} rows and {$invoiceCount} customer invoice(s)."; + return redirect()->route('containers.index')->with('success', $msg); + } + + public function show(Container $container) + { + $container->load('rows'); + return view('admin.container_show', compact('container')); + } + + public function updateRows(Request $request, Container $container) + { + $rowsInput = $request->input('rows', []); + + foreach ($rowsInput as $rowId => $cols) { + $row = ContainerRow::where('container_id', $container->id) + ->where('id', $rowId) + ->first(); + + if (!$row) continue; + + // original update + $data = $row->data ?? []; + foreach ($cols as $colHeader => $value) { + $data[$colHeader] = $value; + } + $row->update([ + 'data' => $data, + ]); + + // extra: update linked invoice items & invoice totals + $rowIndex = $row->row_index; + + $ctn = (float) ($data['CTN'] ?? $data['CTNS'] ?? 0); + $qty = (float) ($data['QTY'] ?? 0); + $ttlQ = (float) ($data['TTLQTY'] ?? $data['TOTALQTY'] ?? $data['TTL/QTY'] ?? ($ctn * $qty)); + $price = (float) ($data['PRICE'] ?? 0); + $cbm = (float) ($data['CBM'] ?? 0); + $ttlC = (float) ($data['TOTALCBM'] ?? $data['TTL CBM'] ?? ($cbm * $ctn)); + $kg = (float) ($data['KG'] ?? 0); + $ttlK = (float) ($data['TOTALKG'] ?? $data['TTL KG'] ?? ($kg * $ctn)); + $amount = (float) ($data['AMOUNT'] ?? ($price * $ttlQ)); + $desc = $data['DESCRIPTION'] ?? $data['DESC'] ?? null; + + $items = InvoiceItem::where('container_id', $container->id) + ->where('container_row_index', $rowIndex) + ->get(); + + foreach ($items as $item) { + $item->description = $desc; + $item->ctn = $ctn; + $item->qty = $qty; + $item->ttl_qty = $ttlQ; + $item->price = $price; + $item->ttl_amount = $amount; + $item->cbm = $cbm; + $item->ttl_cbm = $ttlC; + $item->kg = $kg; + $item->ttl_kg = $ttlK; + $item->save(); + + $invoice = $item->invoice; + if ($invoice) { + $newBaseAmount = InvoiceItem::where('invoice_id', $invoice->id) + ->sum('ttl_amount'); + + $taxType = $invoice->tax_type; + $cgstPercent = (float) ($invoice->cgst_percent ?? 0); + $sgstPercent = (float) ($invoice->sgst_percent ?? 0); + $igstPercent = (float) ($invoice->igst_percent ?? 0); + + $gstPercent = 0; + if ($taxType === 'gst') { + $gstPercent = $cgstPercent + $sgstPercent; + } elseif ($taxType === 'igst') { + $gstPercent = $igstPercent; + } + + $gstAmount = $newBaseAmount * $gstPercent / 100; + $finalWithGst = $newBaseAmount + $gstAmount; + + $invoice->final_amount = $newBaseAmount; + $invoice->gst_amount = $gstAmount; + $invoice->final_amount_with_gst = $finalWithGst; + $invoice->gst_percent = $gstPercent; + $invoice->save(); + } + } + } + + return redirect() + ->route('containers.show', $container->id) + ->with('success', 'Excel rows updated successfully.'); + } + + // app/Http/Controllers/ContainerController.php +public function updateStatus(Request $request, Container $container) +{ + $request->validate([ + 'status' => 'required|in:pending,in-progress,completed,cancelled', + ]); + + $container->status = $request->status; + $container->save(); + + // जर AJAX असेल तर JSON दे + if ($request->wantsJson() || $request->ajax()) { + return response()->json([ + 'success' => true, + 'status' => $container->status, + ]); + } + + // normal form submit असेल तर redirect + return back()->with('success', 'Container status updated.'); +} + + + public function destroy(Container $container) + { + $container->delete(); + return redirect()->route('containers.index')->with('success', 'Container deleted.'); + } + + private function generateInvoiceNumber(): string + { + $year = now()->format('Y'); + + $last = Invoice::whereYear('created_at', $year) + ->orderBy('id', 'desc') + ->first(); + + if ($last) { + $parts = explode('-', $last->invoice_number); + $seq = 0; + + if (count($parts) === 3) { + $seq = (int) $parts[2]; + } + + $nextSeq = $seq + 1; + } else { + $nextSeq = 1; + } + + return 'INV-' . $year . '-' . str_pad($nextSeq, 6, '0', STR_PAD_LEFT); + } +} diff --git a/app/Models/Container.php b/app/Models/Container.php new file mode 100644 index 0000000..eedeeb0 --- /dev/null +++ b/app/Models/Container.php @@ -0,0 +1,30 @@ + 'date', + ]; + + public function rows() + { + return $this->hasMany(ContainerRow::class); + } + + public function invoices() + { + return $this->hasMany(Invoice::class); + } +} diff --git a/app/Models/ContainerRow.php b/app/Models/ContainerRow.php new file mode 100644 index 0000000..9d6059b --- /dev/null +++ b/app/Models/ContainerRow.php @@ -0,0 +1,23 @@ + 'array', + ]; + + public function container() + { + return $this->belongsTo(Container::class); + } +} diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index d9dc394..367766f 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -9,41 +9,29 @@ class Invoice extends Model { use HasFactory; - protected $fillable = [ - 'order_id', - 'customer_id', - 'mark_no', - - 'invoice_number', - 'invoice_date', - 'due_date', - - 'payment_method', - 'reference_no', - 'status', - - 'final_amount', // without tax - - 'tax_type', // gst / igst - 'gst_percent', // only used for gst UI input - 'cgst_percent', - 'sgst_percent', - 'igst_percent', - - 'gst_amount', // total tax amount - 'final_amount_with_gst', - - 'customer_name', - 'company_name', - 'customer_email', - 'customer_mobile', - 'customer_address', - 'pincode', - - 'pdf_path', - 'notes', -]; - + protected $fillable = [ + 'container_id', + 'customer_id', + 'mark_no', + 'invoice_number', + 'invoice_date', + 'due_date', + 'payment_method', + 'reference_no', + 'status', + 'final_amount', + 'gst_percent', + 'gst_amount', + 'final_amount_with_gst', + 'customer_name', + 'company_name', + 'customer_email', + 'customer_mobile', + 'customer_address', + 'pincode', + 'pdf_path', + 'notes', + ]; /**************************** * Relationships @@ -54,9 +42,9 @@ class Invoice extends Model return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC'); } - public function order() + public function container() { - return $this->belongsTo(Order::class); + return $this->belongsTo(Container::class); } public function customer() @@ -64,19 +52,28 @@ class Invoice extends Model return $this->belongsTo(User::class, 'customer_id'); } + public function installments() + { + return $this->hasMany(InvoiceInstallment::class); + } + + // ✅ SINGLE, correct relation + public function chargeGroups() + { + return $this->hasMany(\App\Models\InvoiceChargeGroup::class, 'invoice_id'); + } + /**************************** * Helper Functions ****************************/ - // Auto calculate GST fields (you can call this in controller before saving) public function calculateTotals() { $gst = ($this->final_amount * $this->gst_percent) / 100; - $this->gst_amount = $gst; + $this->gst_amount = $gst; $this->final_amount_with_gst = $this->final_amount + $gst; } - // Check overdue status condition public function isOverdue() { return $this->status === 'pending' && now()->gt($this->due_date); @@ -84,27 +81,31 @@ class Invoice extends Model public function getShipment() { - return $this->order?->shipments?->first(); + return null; } - public function installments() + // ✅ Charge groups total accessor + public function getChargeGroupsTotalAttribute() + { + // relation already loaded असेल तर collection वरून sum होईल + return (float) $this->chargeGroups->sum('total_charge'); + } + + // ✅ Grand total accessor (items + GST + charge groups) + public function getGrandTotalWithChargesAttribute() + { + return (float) ($this->final_amount_with_gst ?? 0) + $this->charge_groups_total; + } + + public function totalPaid() { - return $this->hasMany(InvoiceInstallment::class); + return $this->installments->sum('amount'); } -// App\Models\Invoice.php - -public function totalPaid() -{ - return $this->installments()->sum('amount'); -} public function remainingAmount() { - return max( - ($this->final_amount_with_gst ?? 0) - $this->totalPaid(), - 0 - ); + return $this->grand_total_with_charges - $this->totalPaid(); } diff --git a/app/Models/InvoiceChargeGroup.php b/app/Models/InvoiceChargeGroup.php new file mode 100644 index 0000000..ee7175c --- /dev/null +++ b/app/Models/InvoiceChargeGroup.php @@ -0,0 +1,27 @@ +belongsTo(Invoice::class); + } + + public function items() + { + return $this->hasMany(InvoiceChargeGroupItem::class, 'group_id'); + } +} diff --git a/app/Models/InvoiceChargeGroupItem.php b/app/Models/InvoiceChargeGroupItem.php new file mode 100644 index 0000000..ab3f18e --- /dev/null +++ b/app/Models/InvoiceChargeGroupItem.php @@ -0,0 +1,23 @@ +belongsTo(InvoiceChargeGroup::class, 'group_id'); + } + + public function item() + { + return $this->belongsTo(InvoiceItem::class, 'invoice_item_id'); + } +} diff --git a/app/Models/InvoiceItem.php b/app/Models/InvoiceItem.php index 9e3a6ff..d56ee0a 100644 --- a/app/Models/InvoiceItem.php +++ b/app/Models/InvoiceItem.php @@ -37,4 +37,81 @@ class InvoiceItem extends Model { return $this->belongsTo(Invoice::class); } + + + public function chargeGroupItems() + { + return $this->hasMany(InvoiceChargeGroupItem::class, 'invoice_item_id'); + } + + // हे helper: पहिला group fetch करून त्यावरून rate/total काढणे + public function getChargeRateAttribute() + { + $pivot = $this->chargeGroupItems->first(); + if (!$pivot || !$pivot->group) { + return 0; + } + + $group = $pivot->group; + + // basis नुसार या item चा basis value + $basis = 0; + switch ($group->basis_type) { + case 'ttl_qty': + $basis = $this->ttl_qty; + break; + case 'amount': + $basis = $this->ttl_amount; + break; + case 'ttl_cbm': + $basis = $this->ttl_cbm; + break; + case 'ttl_kg': + $basis = $this->ttl_kg; + break; + } + + if ($basis <= 0 || ($group->basis_value ?? 0) <= 0) { + return 0; + } + + // group चा rate field आधीच आहे, ते direct वापरू + return (float) $group->rate; + } + + public function getChargeTotalAttribute() + { + $pivot = $this->chargeGroupItems->first(); + if (!$pivot || !$pivot->group) { + return 0; + } + + $group = $pivot->group; + + $basis = 0; + switch ($group->basis_type) { + case 'ttl_qty': + $basis = $this->ttl_qty; + break; + case 'amount': + $basis = $this->ttl_amount; + break; + case 'ttl_cbm': + $basis = $this->ttl_cbm; + break; + case 'ttl_kg': + $basis = $this->ttl_kg; + break; + } + + if ($basis <= 0 || ($group->basis_value ?? 0) <= 0) { + return 0; + } + + // per unit rate + $rate = (float) $group->rate; + // item total = basis * rate + return $basis * $rate; + } } + diff --git a/app/Models/Order.php b/app/Models/Order.php index 9362328..3eca49d 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -58,10 +58,10 @@ class Order extends Model return $this->belongsToMany(\App\Models\Shipment::class, 'shipment_items', 'order_id', 'shipment_id'); } - public function invoice() - { - return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id'); - } + // public function invoice() + // { + // return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id'); + // } const STATUS_LABELS = [ diff --git a/database/migrations/2025_11_15_120425_create_invoice_items_table.php b/database/migrations/2025_11_15_120425_create_invoice_items_table.php index cc4da76..c0ca2e3 100644 --- a/database/migrations/2025_11_15_120425_create_invoice_items_table.php +++ b/database/migrations/2025_11_15_120425_create_invoice_items_table.php @@ -33,6 +33,9 @@ class CreateInvoiceItemsTable extends Migration $table->string('shop_no')->nullable(); $table->timestamps(); + + $table->unsignedBigInteger('container_id')->nullable()->after('invoice_id'); + $table->integer('container_row_index')->nullable()->after('container_id'); // FK $table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade'); @@ -49,4 +52,6 @@ class CreateInvoiceItemsTable extends Migration }); Schema::dropIfExists('invoice_items'); } + + } diff --git a/database/migrations/2026_02_07_061825_create_containers_table.php b/database/migrations/2026_02_07_061825_create_containers_table.php new file mode 100644 index 0000000..4e4cbc6 --- /dev/null +++ b/database/migrations/2026_02_07_061825_create_containers_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('container_name'); + $table->string('container_number')->unique(); + $table->date('container_date'); + $table->string('excel_file')->nullable(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('containers'); + } +}; diff --git a/database/migrations/2026_02_07_071829_create_loading_list_items_table.php b/database/migrations/2026_02_07_071829_create_loading_list_items_table.php new file mode 100644 index 0000000..7f144d8 --- /dev/null +++ b/database/migrations/2026_02_07_071829_create_loading_list_items_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('container_id') + ->constrained('containers') + ->onDelete('cascade'); + + $table->string('mark')->nullable(); // MARK / ITEM NO + $table->string('description')->nullable(); + $table->integer('ctn')->nullable(); + $table->integer('qty')->nullable(); + $table->integer('total_qty')->nullable(); + $table->string('unit')->nullable(); + $table->decimal('price', 15, 3)->nullable(); // SAHIL format साठी + $table->decimal('cbm', 15, 5)->nullable(); + $table->decimal('total_cbm', 15, 5)->nullable(); + $table->decimal('kg', 15, 3)->nullable(); + $table->decimal('total_kg', 15, 3)->nullable(); + + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('loading_list_items'); + } +}; diff --git a/database/migrations/2026_02_07_113151_create_container_rows_table.php b/database/migrations/2026_02_07_113151_create_container_rows_table.php new file mode 100644 index 0000000..3df78ac --- /dev/null +++ b/database/migrations/2026_02_07_113151_create_container_rows_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('container_id') + ->constrained('containers') + ->onDelete('cascade'); + + // Excel मधल्या row क्रमांकासाठी (optional) + $table->unsignedInteger('row_index')->nullable(); + + // या row चा full data: "heading text" => "cell value" + $table->json('data'); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('container_rows'); + } +}; diff --git a/database/migrations/2026_02_07_132057_add_status_to_containers_table.php b/database/migrations/2026_02_07_132057_add_status_to_containers_table.php new file mode 100644 index 0000000..314efcf --- /dev/null +++ b/database/migrations/2026_02_07_132057_add_status_to_containers_table.php @@ -0,0 +1,24 @@ +string('status', 20) + ->default('pending') + ->after('container_date'); + }); + } + + public function down(): void + { + Schema::table('containers', function (Blueprint $table) { + $table->dropColumn('status'); + }); + } +}; diff --git a/database/migrations/2026_02_16_060157_update_invoices_for_container_relation.php b/database/migrations/2026_02_16_060157_update_invoices_for_container_relation.php new file mode 100644 index 0000000..17ebecb --- /dev/null +++ b/database/migrations/2026_02_16_060157_update_invoices_for_container_relation.php @@ -0,0 +1,43 @@ +dropForeign(['order_id']); + + // 2) order_id column काढा + $table->dropColumn('order_id'); + + // 3) container_id add करा + $table->unsignedBigInteger('container_id')->nullable()->after('id'); + + // 4) container_id FK + $table->foreign('container_id') + ->references('id') + ->on('containers') + ->onDelete('cascade'); + }); + } + + public function down(): void + { + Schema::table('invoices', function (Blueprint $table) { + // rollback: container_id काढून order_id परत add + $table->dropForeign(['container_id']); + $table->dropColumn('container_id'); + + $table->unsignedBigInteger('order_id')->index(); + $table->foreign('order_id') + ->references('id') + ->on('orders') + ->onDelete('cascade'); + }); + } +}; diff --git a/database/migrations/2026_02_17_163431_create_invoice_charge_groups_table.php b/database/migrations/2026_02_17_163431_create_invoice_charge_groups_table.php new file mode 100644 index 0000000..f23ef2e --- /dev/null +++ b/database/migrations/2026_02_17_163431_create_invoice_charge_groups_table.php @@ -0,0 +1,37 @@ +id(); + $table->unsignedBigInteger('invoice_id'); + $table->string('group_name')->nullable(); // उदा. "FREIGHT", "HANDLING" + $table->enum('basis_type', ['ttl_qty', 'amount', 'ttl_cbm', 'ttl_kg']); + $table->decimal('basis_value', 15, 3)->default(0); // auto calculate केलेला total basis + $table->decimal('rate', 15, 3)->default(0); // per basis rate (helper) + $table->decimal('total_charge', 15, 2); // admin नी manually टाकलेला total + $table->timestamps(); + + $table->foreign('invoice_id') + ->references('id')->on('invoices') + ->onDelete('cascade'); + }); + } + + public function down(): void + { + Schema::dropIfExists('invoice_charge_groups'); + } + + + +}; diff --git a/database/migrations/2026_02_17_163544_create_invoice_charge_group_items_table.php b/database/migrations/2026_02_17_163544_create_invoice_charge_group_items_table.php new file mode 100644 index 0000000..d86234f --- /dev/null +++ b/database/migrations/2026_02_17_163544_create_invoice_charge_group_items_table.php @@ -0,0 +1,35 @@ +id(); + $table->unsignedBigInteger('group_id'); + $table->unsignedBigInteger('invoice_item_id'); + $table->timestamps(); + + $table->foreign('group_id') + ->references('id')->on('invoice_charge_groups') + ->onDelete('cascade'); + + $table->foreign('invoice_item_id') + ->references('id')->on('invoice_items') + ->onDelete('cascade'); + }); + } + + public function down(): void + { + Schema::dropIfExists('invoice_charge_group_items'); + } + +}; diff --git a/database/migrations/2026_02_21_101207_add_container_link_to_invoice_items.php b/database/migrations/2026_02_21_101207_add_container_link_to_invoice_items.php new file mode 100644 index 0000000..2cfb73d --- /dev/null +++ b/database/migrations/2026_02_21_101207_add_container_link_to_invoice_items.php @@ -0,0 +1,27 @@ +unsignedBigInteger('container_id')->nullable()->after('invoice_id'); + $table->integer('container_row_index')->nullable()->after('container_id'); + }); + } + + public function down() + { + Schema::table('invoice_items', function (Blueprint $table) { + $table->dropColumn(['container_id', 'container_row_index']); + }); + } + +}; diff --git a/resources/views/admin/account.blade.php b/resources/views/admin/account.blade.php index 14d0b08..db330cf 100644 --- a/resources/views/admin/account.blade.php +++ b/resources/views/admin/account.blade.php @@ -6,6 +6,7 @@
@@ -1323,7 +1547,7 @@ html, body {
- -