diff --git a/app/Http/Controllers/Admin/AdminOrderController.php b/app/Http/Controllers/Admin/AdminOrderController.php index 662ecce..15799d4 100644 --- a/app/Http/Controllers/Admin/AdminOrderController.php +++ b/app/Http/Controllers/Admin/AdminOrderController.php @@ -7,28 +7,123 @@ use Illuminate\Http\Request; use App\Models\Order; use App\Models\OrderItem; use App\Models\MarkList; +use App\Models\Invoice; +use App\Models\InvoiceItem; +use App\Models\User; class AdminOrderController extends Controller { + // --------------------------- + // LIST / DASHBOARD + // --------------------------- public function index() { + // raw list for admin dashboard (simple) $orders = Order::latest()->get(); $markList = MarkList::where('status', 'active')->get(); return view('admin.dashboard', compact('orders', 'markList')); } - // ------------------------------------------------------------------------- - // STEP 1 : ADD TEMPORARY ITEM - // ------------------------------------------------------------------------- + /** + * Orders list (detailed) + */ + // public function orderShow() + // { + // $orders = Order::with(['markList', 'shipments', 'invoice']) + // ->latest('id') + // ->get(); - public function addTempItem(Request $request) + // return view('admin.orders', compact('orders')); + // } + + // --------------------------- + // CREATE NEW ORDER (simple DB flow) + // --------------------------- + /** + * Show create form (you can place create UI on separate view or dashboard) + */ + public function create() { - // Validate item fields - $item = $request->validate([ - 'mark_no' => 'required', - 'origin' => 'required', - 'destination' => 'required', + // return a dedicated create view - create it at resources/views/admin/orders_create.blade.php + // If you prefer create UI on dashboard, change this to redirect route('admin.orders.index') etc. + $markList = MarkList::where('status', 'active')->get(); + return view('admin.orders_create', compact('markList')); + } + + /** + * Store a new order and optionally create initial invoice + */ + public function store(Request $request) + { + $data = $request->validate([ + 'mark_no' => 'required|string', + 'origin' => 'nullable|string', + 'destination' => 'nullable|string', + // totals optional when creating without items + 'ctn' => 'nullable|numeric', + 'qty' => 'nullable|numeric', + 'ttl_qty' => 'nullable|numeric', + 'ttl_amount' => 'nullable|numeric', + 'cbm' => 'nullable|numeric', + 'ttl_cbm' => 'nullable|numeric', + 'kg' => 'nullable|numeric', + 'ttl_kg' => 'nullable|numeric', + ]); + + $order = Order::create([ + 'order_id' => $this->generateOrderId(), + 'mark_no' => $data['mark_no'], + 'origin' => $data['origin'] ?? null, + 'destination' => $data['destination'] ?? null, + 'ctn' => $data['ctn'] ?? 0, + 'qty' => $data['qty'] ?? 0, + 'ttl_qty' => $data['ttl_qty'] ?? 0, + 'ttl_amount' => $data['ttl_amount'] ?? 0, + 'cbm' => $data['cbm'] ?? 0, + 'ttl_cbm' => $data['ttl_cbm'] ?? 0, + 'kg' => $data['kg'] ?? 0, + 'ttl_kg' => $data['ttl_kg'] ?? 0, + 'status' => 'pending', + ]); + + // If you want to auto-create an invoice at order creation, uncomment: + // $this->createInvoice($order); + + return redirect()->route('admin.orders.show', $order->id) + ->with('success', 'Order created successfully.'); + } + + // --------------------------- + // SHOW / POPUP + // --------------------------- + public function show($id) + { + $order = Order::with('items', 'markList')->findOrFail($id); + $user = $this->getCustomerFromOrder($order); + + return view('admin.orders_show', compact('order', 'user')); + } + + // public function popup($id) + // { + // $order = Order::with(['items', 'markList'])->findOrFail($id); + // $user = $this->getCustomerFromOrder($order); + + // return view('admin.popup', compact('order', 'user')); + // } + + // --------------------------- + // ORDER ITEM MANAGEMENT (DB) + // --------------------------- + /** + * Add an item to an existing order + */ + public function addItem(Request $request, $orderId) + { + $order = Order::findOrFail($orderId); + + $data = $request->validate([ 'description' => 'required|string', 'ctn' => 'nullable|numeric', 'qty' => 'nullable|numeric', @@ -43,233 +138,207 @@ class AdminOrderController extends Controller 'shop_no' => 'nullable|string', ]); - // ❌ Prevent changing mark_no once first item added - if (session()->has('mark_no') && session('mark_no') != $request->mark_no) { - return redirect()->to(route('admin.orders.index') . '#createOrderForm') - ->with('error', 'You must finish or clear the current order before changing Mark No.'); - } + $data['order_id'] = $order->id; - // Save mark, origin, destination ONLY ONCE - if (!session()->has('mark_no')) { - session([ - 'mark_no' => $request->mark_no, - 'origin' => $request->origin, - 'destination' => $request->destination + OrderItem::create($data); + + // recalc totals and save to order + $this->recalcTotals($order); + + return redirect()->back()->with('success', 'Item added and totals updated.'); + } + + /** + * Soft-delete an order item and recalc totals + */ + public function deleteItem($id) + { + $item = OrderItem::findOrFail($id); + $order = $item->order; + + $item->delete(); // soft delete + + // recalc totals + $this->recalcTotals($order); + + return redirect()->back()->with('success', 'Item deleted and totals updated.'); + } + + /** + * Restore soft-deleted item and recalc totals + */ + public function restoreItem($id) + { + $item = OrderItem::withTrashed()->findOrFail($id); + $order = Order::findOrFail($item->order_id); + + $item->restore(); + + // recalc totals + $this->recalcTotals($order); + + return redirect()->back()->with('success', 'Item restored and totals updated.'); + } + + // --------------------------- + // ORDER CRUD: update / destroy + // --------------------------- + public function update(Request $request, $id) + { + $order = Order::findOrFail($id); + + $data = $request->validate([ + 'mark_no' => 'required|string', + 'origin' => 'nullable|string', + 'destination' => 'nullable|string', + ]); + + $order->update([ + 'mark_no' => $data['mark_no'], + 'origin' => $data['origin'] ?? null, + 'destination' => $data['destination'] ?? null, + ]); + + // optionally recalc totals (not necessary unless you change item-level fields here) + $this->recalcTotals($order); + + return redirect()->route('admin.orders.show', $order->id) + ->with('success', 'Order updated successfully.'); + } + + /** + * Soft-delete whole order and its items (soft-delete items first then order) + */ + public function destroy($id) + { + $order = Order::findOrFail($id); + + // soft-delete items first (so they show up in onlyTrashed for restore) + OrderItem::where('order_id', $order->id)->delete(); + + // then soft-delete order + $order->delete(); + + return redirect()->route('admin.orders.index') + ->with('success', 'Order deleted successfully.'); + } + + // --------------------------- + // HELPERS + // --------------------------- + /** + * Recalculate totals for the order from current (non-deleted) items + */ + private function recalcTotals(Order $order) + { + // make sure we re-query live items (non-deleted) + $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)), + ]); + } + + /** + * Generate order id (keeps old format) + */ + private function generateOrderId() + { + $year = date('y'); + $prefix = "KNT-$year-"; + + $lastOrder = Order::latest('id')->first(); + $nextNumber = $lastOrder ? intval(substr($lastOrder->order_id, -8)) + 1 : 1; + + return $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT); + } + + // --------------------------- + // INVOICE CREATION (optional helper used by store/finish) + // --------------------------- + private function createInvoice(Order $order) + { + $invoiceNumber = $this->generateInvoiceNumber(); + $customer = $this->getCustomerFromMarkList($order->mark_no); + $totalAmount = $order->ttl_amount; + + $invoice = Invoice::create([ + 'order_id' => $order->id, + 'customer_id' => $customer->id ?? null, + 'mark_no' => $order->mark_no, + 'invoice_number' => $invoiceNumber, + 'invoice_date' => now(), + 'due_date' => now()->addDays(10), + 'payment_method' => null, + 'reference_no' => null, + 'status' => 'pending', + 'final_amount' => $totalAmount, + 'gst_percent' => 0, + 'gst_amount' => 0, + 'final_amount_with_gst' => $totalAmount, + 'customer_name' => $customer->customer_name ?? null, + 'company_name' => $customer->company_name ?? null, + 'customer_email' => $customer->email ?? null, + 'customer_mobile' => $customer->mobile_no ?? null, + 'customer_address' => $customer->address ?? null, + 'pincode' => $customer->pincode ?? null, + 'notes' => null, + 'pdf_path' => null, + ]); + + // clone order items into invoice items + 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, ]); } - - // ❌ DO NOT overwrite these values again - // session(['mark_no' => $request->mark_no]); - // session(['origin' => $request->origin]); - // session(['destination' => $request->destination]); - - // Add new sub-item into session - session()->push('temp_order_items', $item); - - return redirect()->to(route('admin.orders.index') . '#createOrderForm') - ->with('success', 'Item added.'); } - // ------------------------------------------------------------------------- - // STEP 2 : DELETE TEMPORARY ITEM - // ------------------------------------------------------------------------- - - public function deleteTempItem(Request $request) + private function generateInvoiceNumber() { - $index = $request->index; + $lastInvoice = Invoice::latest()->first(); + $nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1; - $items = session('temp_order_items', []); + return 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT); + } - if (isset($items[$index])) { - unset($items[$index]); - session(['temp_order_items' => array_values($items)]); + private function getCustomerFromMarkList($markNo) + { + $markList = MarkList::where('mark_no', $markNo)->first(); + + if ($markList && $markList->customer_id) { + return User::where('customer_id', $markList->customer_id)->first(); } - // If no items left → reset mark_no lock - if (empty($items)) { - session()->forget(['mark_no', 'origin', 'destination']); - } - - return redirect()->to(route('admin.orders.index') . '#createOrderForm') - ->with('success', 'Item removed successfully.'); + return null; } - // ------------------------------------------------------------------------- - // STEP 3 : FINISH ORDER - // ------------------------------------------------------------------------- - - public function finishOrder(Request $request) -{ - $request->validate([ - 'mark_no' => 'required', - 'origin' => 'required', - 'destination' => 'required', - ]); - - $items = session('temp_order_items', []); - - if (empty($items)) { - return redirect()->to(route('admin.orders.index') . '#createOrderForm') - ->with('error', 'Add at least one item before finishing.'); - } - - // ======================= - // GENERATE ORDER ID - // ======================= - $year = date('y'); - $prefix = "KNT-$year-"; - - $lastOrder = Order::latest('id')->first(); - $nextNumber = $lastOrder ? intval(substr($lastOrder->order_id, -8)) + 1 : 1; - - $orderId = $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT); - - // ======================= - // TOTAL SUMS - // ======================= - $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')); - - // ======================= - // CREATE ORDER - // ======================= - $order = Order::create([ - 'order_id' => $orderId, - 'mark_no' => $request->mark_no, - 'origin' => $request->origin, - 'destination' => $request->destination, - 'ctn' => $total_ctn, - 'qty' => $total_qty, - 'ttl_qty' => $total_ttl_qty, - 'ttl_amount' => $total_amount, - 'cbm' => $total_cbm, - 'ttl_cbm' => $total_ttl_cbm, - 'kg' => $total_kg, - 'ttl_kg' => $total_ttl_kg, - 'status' => 'pending', - ]); - - // SAVE ORDER ITEMS - foreach ($items as $item) { - OrderItem::create([ - 'order_id' => $order->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'], - ]); - } - - // ======================= - // INVOICE CREATION START - // ======================= - - // 1. Auto-generate invoice number - $lastInvoice = \App\Models\Invoice::latest()->first(); - $nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1; - $invoiceNumber = 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT); - - // 2. Fetch customer (using mark list → customer_id) - $markList = MarkList::where('mark_no', $order->mark_no)->first(); - $customer = null; - - if ($markList && $markList->customer_id) { - $customer = \App\Models\User::where('customer_id', $markList->customer_id)->first(); - } - - // 3. Create Invoice Record - $invoice = \App\Models\Invoice::create([ - 'order_id' => $order->id, - 'customer_id' => $customer->id ?? null, - 'mark_no' => $order->mark_no, - - 'invoice_number' => $invoiceNumber, - 'invoice_date' => now(), - 'due_date' => now()->addDays(10), - - 'payment_method' => null, - 'reference_no' => null, - 'status' => 'pending', - - 'final_amount' => $total_amount, - 'gst_percent' => 0, - 'gst_amount' => 0, - 'final_amount_with_gst' => $total_amount, - - // snapshot customer fields - 'customer_name' => $customer->customer_name ?? null, - 'company_name' => $customer->company_name ?? null, - 'customer_email' => $customer->email ?? null, - 'customer_mobile' => $customer->mobile_no ?? null, - 'customer_address' => $customer->address ?? null, - 'pincode' => $customer->pincode ?? null, - - 'notes' => null, - ]); - - // 4. Clone order items into invoice_items - foreach ($order->items as $item) { - \App\Models\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, - ]); - } - - // 5. TODO: PDF generation (I will add this later) - $invoice->pdf_path = null; // placeholder for now - $invoice->save(); - - // ======================= - // END INVOICE CREATION - // ======================= - - // CLEAR TEMP DATA - session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']); - - return redirect()->route('admin.orders.index') - ->with('success', 'Order + Invoice created successfully.'); -} - - - // ------------------------------------------------------------------------- - // ORDER SHOW PAGE - // ------------------------------------------------------------------------- - - public function show($id) + private function getCustomerFromOrder($order) { - $order = Order::with('items', 'markList')->findOrFail($id); - - $user = null; if ($order->markList && $order->markList->customer_id) { - $user = \App\Models\User::where('customer_id', $order->markList->customer_id)->first(); + return User::where('customer_id', $order->markList->customer_id)->first(); } - return view('admin.orders_show', compact('order', 'user')); + return null; } public function popup($id) @@ -351,4 +420,129 @@ public function downloadExcel(Request $request) return Excel::download(new OrdersExport($request), 'orders-report-' . date('Y-m-d') . '.xlsx'); } +public function addTempItem(Request $request) + { + // Validate item fields + $item = $request->validate([ + 'mark_no' => 'required', + 'origin' => 'required', + 'destination' => 'required', + 'description' => 'required|string', + 'ctn' => 'nullable|numeric', + 'qty' => 'nullable|numeric', + 'ttl_qty' => 'nullable|numeric', + 'unit' => 'nullable|string', + 'price' => 'nullable|numeric', + 'ttl_amount' => 'nullable|numeric', + 'cbm' => 'nullable|numeric', + 'ttl_cbm' => 'nullable|numeric', + 'kg' => 'nullable|numeric', + 'ttl_kg' => 'nullable|numeric', + 'shop_no' => 'nullable|string', + ]); + + // ❌ Prevent changing mark_no once first item added + if (session()->has('mark_no') && session('mark_no') != $request->mark_no) { + return redirect()->to(route('admin.orders.index') . '#createOrderForm') + ->with('error', 'You must finish or clear the current order before changing Mark No.'); + } + + // Save mark, origin, destination ONLY ONCE + if (!session()->has('mark_no')) { + session([ + 'mark_no' => $request->mark_no, + 'origin' => $request->origin, + 'destination' => $request->destination + ]); + } + + // ❌ DO NOT overwrite these values again + // session(['mark_no' => $request->mark_no]); + // session(['origin' => $request->origin]); + // session(['destination' => $request->destination]); + + // Add new sub-item into session + session()->push('temp_order_items', $item); + + return redirect()->to(route('admin.orders.index') . '#createOrderForm') + ->with('success', 'Item added.'); + } + +public function finishOrder(Request $request) + { + $request->validate([ + 'mark_no' => 'required', + 'origin' => 'required', + 'destination' => 'required', + ]); + + $items = session('temp_order_items', []); + + if (empty($items)) { + return redirect()->to(route('admin.orders.index') . '#createOrderForm') + ->with('error', 'Add at least one item before finishing.'); + } + + // Generate Order ID + $year = date('y'); + $prefix = "KNT-$year-"; + + $lastOrder = Order::latest('id')->first(); + $nextNumber = $lastOrder ? intval(substr($lastOrder->order_id, -8)) + 1 : 1; + + $orderId = $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT); + + // TOTAL SUMS + $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')); + + // CREATE ORDER + $order = Order::create([ + 'order_id' => $orderId, + 'mark_no' => $request->mark_no, + 'origin' => $request->origin, + 'destination' => $request->destination, + 'ctn' => $total_ctn, + 'qty' => $total_qty, + 'ttl_qty' => $total_ttl_qty, + 'ttl_amount' => $total_amount, + 'cbm' => $total_cbm, + 'ttl_cbm' => $total_ttl_cbm, + 'kg' => $total_kg, + 'ttl_kg' => $total_ttl_kg, + 'status' => 'pending', + ]); + + // SAVE ALL SUB-ITEMS + foreach ($items as $item) { + OrderItem::create([ + 'order_id' => $order->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'], + ]); + } + + // CLEAR TEMP DATA + session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']); + + return redirect()->route('admin.orders.index') + ->with('success', 'Order saved successfully.'); + } + } diff --git a/app/Http/Controllers/Admin/ShipmentController.php b/app/Http/Controllers/Admin/ShipmentController.php index c0883fd..d281059 100644 --- a/app/Http/Controllers/Admin/ShipmentController.php +++ b/app/Http/Controllers/Admin/ShipmentController.php @@ -14,7 +14,7 @@ class ShipmentController extends Controller /** * Show shipment page (Create Shipment + Shipment List) */ - public function index() + public function index() { // 1) Get all used order IDs $usedOrderIds = ShipmentItem::pluck('order_id')->toArray(); @@ -29,8 +29,6 @@ class ShipmentController extends Controller return view('admin.shipments', compact('availableOrders', 'shipments')); } - - /** * Store new shipment */ @@ -115,8 +113,6 @@ class ShipmentController extends Controller return redirect()->back()->with('success', "Shipment $newShipmentId created successfully!"); } - - /** * Show shipment details (for modal popup) */ @@ -135,8 +131,6 @@ class ShipmentController extends Controller ]); } - - /** * Update Shipment status from action button */ @@ -164,5 +158,55 @@ class ShipmentController extends Controller ); } + /** + * Update shipment details + */ + public function update(Request $request, $id) + { + $shipment = Shipment::findOrFail($id); -} + $data = $request->validate([ + 'origin' => 'required|string', + 'destination' => 'required|string', + 'shipment_date' => 'required|date', + 'status' => 'required|string', + ]); + + $shipment->update($data); + + // If it's an AJAX request, return JSON response + if ($request->ajax() || $request->wantsJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Shipment updated successfully.' + ]); + } + + return redirect()->back()->with('success', 'Shipment updated successfully.'); + } + + /** + * Delete shipment permanently + */ + public function destroy($id, Request $request) + { + $shipment = Shipment::findOrFail($id); + + // Delete shipment items + ShipmentItem::where('shipment_id', $shipment->id)->delete(); + + // Delete shipment itself + $shipment->delete(); + + // If it's an AJAX request, return JSON response + if ($request->ajax() || $request->wantsJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Shipment deleted successfully.' + ]); + } + + return redirect()->route('admin.shipments') + ->with('success', 'Shipment deleted successfully.'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Admin/UserRequestController.php b/app/Http/Controllers/Admin/UserRequestController.php index 0b6306f..e09c56f 100644 --- a/app/Http/Controllers/Admin/UserRequestController.php +++ b/app/Http/Controllers/Admin/UserRequestController.php @@ -65,4 +65,52 @@ class UserRequestController extends Controller return redirect()->back()->with('info', 'Request rejected successfully.'); } + + public function profileUpdateRequests() +{ + $requests = \App\Models\UpdateRequest::where('status', 'pending') + ->orderBy('id', 'desc') + ->get(); + + return view('admin.profile_update_requests', compact('requests')); +} + +public function approveProfileUpdate($id) +{ + $req = \App\Models\UpdateRequest::findOrFail($id); + $user = \App\Models\User::findOrFail($req->user_id); + + // FIX: Ensure data is array + $newData = is_array($req->data) ? $req->data : json_decode($req->data, true); + + foreach ($newData as $key => $value) { + if ($value !== null && $value !== "") { + if (in_array($key, ['customer_name','company_name','designation','email','mobile_no','address','pincode'])) { + $user->$key = $value; + } + } + } + + $user->save(); + + $req->status = 'approved'; + $req->admin_note = 'Approved by admin on ' . now(); + $req->save(); + + return back()->with('success', 'Profile updated successfully.'); +} + + + +public function rejectProfileUpdate($id) +{ + $req = \App\Models\UpdateRequest::findOrFail($id); + $req->status = 'rejected'; + $req->admin_note = 'Rejected by admin on ' . now(); + $req->save(); + + return back()->with('info', 'Profile update request rejected.'); +} + + } diff --git a/app/Http/Controllers/UserAuthController.php b/app/Http/Controllers/UserAuthController.php index b6893d4..256f98a 100644 --- a/app/Http/Controllers/UserAuthController.php +++ b/app/Http/Controllers/UserAuthController.php @@ -9,6 +9,74 @@ use App\Models\User; class UserAuthController extends Controller { + + public function refreshToken() + { + \Log::info('🔄 refreshToken() called'); + + try { + // Get current token + $currentToken = JWTAuth::getToken(); + + if (!$currentToken) { + \Log::warning('⚠ No token provided in refreshToken()'); + return response()->json([ + 'success' => false, + 'message' => 'Token not provided', + ], 401); + } + + \Log::info('📥 Current Token:', ['token' => (string) $currentToken]); + + // Try refreshing token + $newToken = JWTAuth::refresh($currentToken); + + \Log::info('✅ Token refreshed successfully', ['new_token' => $newToken]); + + return response()->json([ + 'success' => true, + 'token' => $newToken, + ]); + + } catch (\Tymon\JWTAuth\Exceptions\TokenExpiredException $e) { + \Log::error('❌ TokenExpiredException in refreshToken()', [ + 'message' => $e->getMessage(), + ]); + return response()->json([ + 'success' => false, + 'message' => 'Token expired, cannot refresh.', + ], 401); + + } catch (\Tymon\JWTAuth\Exceptions\TokenInvalidException $e) { + \Log::error('❌ TokenInvalidException in refreshToken()', [ + 'message' => $e->getMessage(), + ]); + return response()->json([ + 'success' => false, + 'message' => 'Invalid token.', + ], 401); + + } catch (\Tymon\JWTAuth\Exceptions\JWTException $e) { + \Log::error('❌ JWTException in refreshToken()', [ + 'message' => $e->getMessage(), + ]); + return response()->json([ + 'success' => false, + 'message' => 'Could not refresh token.', + ], 401); + + } catch (\Exception $e) { + \Log::error('❌ General Exception in refreshToken()', [ + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + return response()->json([ + 'success' => false, + 'message' => 'Unexpected error while refreshing token.', + ], 500); + } + } + /** * User Login */ @@ -60,6 +128,8 @@ class UserAuthController extends Controller ]); } + + /** * User Logout */ diff --git a/app/Http/Controllers/user/UserOrderController.php b/app/Http/Controllers/user/UserOrderController.php new file mode 100644 index 0000000..d18f7ae --- /dev/null +++ b/app/Http/Controllers/user/UserOrderController.php @@ -0,0 +1,296 @@ +authenticate(); + + if (!$user) { + return response()->json([ + 'status' => false, + 'message' => 'Unauthorized' + ], 401); + } + + // ------------------------------------- + // Get all orders + // ------------------------------------- + $orders = $user->orders()->with('invoice')->get(); + + // ------------------------------------- + // Counts + // ------------------------------------- + $totalOrders = $orders->count(); + $delivered = $orders->where('status', 'delivered')->count(); + $inTransit = $orders->where('status', '!=', 'delivered')->count(); + $active = $totalOrders; + + // ------------------------------------- + // Total Amount = Invoice.total_with_gst + // ------------------------------------- + $totalAmount = $orders->sum(function ($o) { + return $o->invoice->final_amount_with_gst ?? 0; + }); + + // Format total amount in K, L, Cr + $formattedAmount = $this->formatIndianNumber($totalAmount); + + return response()->json([ + 'status' => true, + + 'summary' => [ + 'active_orders' => $active, + 'in_transit_orders' => $inTransit, + 'delivered_orders' => $delivered, + 'total_value' => $formattedAmount, // formatted value + 'total_raw' => $totalAmount // original value + ] + ]); + } + + /** + * Convert number into Indian Format: + * 1000 -> 1K + * 100000 -> 1L + * 10000000 -> 1Cr + */ + private function formatIndianNumber($num) + { + if ($num >= 10000000) { + return round($num / 10000000, 1) . 'Cr'; + } + + if ($num >= 100000) { + return round($num / 100000, 1) . 'L'; + } + + if ($num >= 1000) { + return round($num / 1000, 1) . 'K'; + } + + return (string)$num; + } + + public function allOrders() +{ + $user = JWTAuth::parseToken()->authenticate(); + + if (!$user) { + return response()->json([ + 'success' => false, + 'message' => 'Unauthorized' + ], 401); + } + + // Fetch orders for this user + $orders = $user->orders() + ->with(['invoice', 'shipments']) + ->orderBy('id', 'desc') + ->get() + ->map(function ($o) { + return [ + 'order_id' => $o->order_id, + 'status' => $o->status, + 'amount' => $o->ttl_amount, + 'description'=> "Order from {$o->origin} to {$o->destination}", + 'created_at' => $o->created_at, + ]; + }); + + return response()->json([ + 'success' => true, + 'orders' => $orders + ]); +} + +public function orderDetails($order_id) +{ + $user = JWTAuth::parseToken()->authenticate(); + + $order = $user->orders() + ->with(['items']) + ->where('order_id', $order_id) + ->first(); + + if (!$order) { + return response()->json(['success' => false, 'message' => 'Order not found'], 404); + } + + return response()->json([ + 'success' => true, + 'order' => $order + ]); +} + + +public function orderShipment($order_id) +{ + $user = JWTAuth::parseToken()->authenticate(); + + // Get order + $order = $user->orders()->where('order_id', $order_id)->first(); + + if (!$order) { + return response()->json(['success' => false, 'message' => 'Order not found'], 404); + } + + // Find shipment only for this order + $shipment = $order->shipments() + ->with(['items' => function ($q) use ($order) { + $q->where('order_id', $order->id); + }]) + ->first(); + + return response()->json([ + 'success' => true, + 'shipment' => $shipment + ]); +} + + +public function orderInvoice($order_id) +{ + $user = JWTAuth::parseToken()->authenticate(); + + $order = $user->orders() + ->with('invoice.items') + ->where('order_id', $order_id) + ->first(); + + if (!$order) { + return response()->json(['success' => false, 'message' => 'Order not found'], 404); + } + + return response()->json([ + 'success' => true, + 'invoice' => $order->invoice + ]); +} + +public function trackOrder($order_id) +{ + $user = JWTAuth::parseToken()->authenticate(); + + $order = $user->orders() + ->with('shipments') + ->where('order_id', $order_id) + ->first(); + + if (!$order) { + return response()->json(['success' => false, 'message' => 'Order not found'], 404); + } + + $shipment = $order->shipments()->first(); + + return response()->json([ + 'success' => true, + 'track' => [ + 'order_id' => $order->order_id, + 'shipment_status' => $shipment->status ?? 'pending', + 'shipment_date' => $shipment->shipment_date ?? null, + ] + ]); +} + +public function allInvoices() +{ + $user = JWTAuth::parseToken()->authenticate(); + + if (!$user) { + return response()->json([ + 'success' => false, + 'message' => 'Unauthorized' + ], 401); + } + + // Fetch all invoices of customer + $invoices = $user->invoices() + ->withCount('installments') + ->orderBy('id', 'desc') + ->get() + ->map(function ($invoice) { + return [ + 'invoice_id' => $invoice->id, + 'invoice_number' => $invoice->invoice_number, + 'invoice_date' => $invoice->invoice_date, + 'status' => $invoice->status, + 'amount' => $invoice->final_amount_with_gst, + 'formatted_amount' => $this->formatIndianNumber($invoice->final_amount_with_gst), + 'pdf_url' => $invoice->pdf_path ? url($invoice->pdf_path) : null, + 'installment_count' => $invoice->installments_count, + ]; + }); + + return response()->json([ + 'success' => true, + 'invoices' => $invoices + ]); +} + +public function invoiceInstallmentsById($invoice_id) +{ + $user = \PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth::parseToken()->authenticate(); + + if (! $user) { + return response()->json(['success' => false, 'message' => 'Unauthorized'], 401); + } + + // Find invoice by numeric id and ensure it belongs to logged-in user (invoice.customer_id = user.id) + $invoice = \App\Models\Invoice::where('id', (int)$invoice_id) + ->where('customer_id', $user->id) + ->with(['installments' => function($q){ + $q->orderBy('installment_date', 'ASC')->orderBy('id', 'ASC'); + }]) + ->first(); + + if (! $invoice) { + return response()->json([ + 'success' => false, + 'message' => 'Invoice not found for this customer' + ], 404); + } + + return response()->json([ + 'success' => true, + 'invoice_id' => $invoice->id, + 'invoice_number' => $invoice->invoice_number, + 'installments' => $invoice->installments + ]); +} + +public function invoiceDetails($invoice_id) +{ + $user = JWTAuth::parseToken()->authenticate(); + + if (! $user) { + return response()->json(['success' => false, 'message' => 'Unauthorized'], 401); + } + + $invoice = \App\Models\Invoice::where('id', $invoice_id) + ->where('customer_id', $user->id) + ->with('items') + ->first(); + + if (! $invoice) { + return response()->json(['success' => false, 'message' => 'Invoice not found'], 404); + } + + return response()->json([ + 'success' => true, + 'invoice' => $invoice + ]); +} + + + + + +} diff --git a/app/Http/Controllers/user/UserProfileController.php b/app/Http/Controllers/user/UserProfileController.php new file mode 100644 index 0000000..ca42ab2 --- /dev/null +++ b/app/Http/Controllers/user/UserProfileController.php @@ -0,0 +1,135 @@ +authenticate(); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Token invalid or expired', + ], 401); + } + + if (! $user) { + return response()->json([ + 'success' => false, + 'message' => 'Unauthorized' + ], 401); + } + + return response()->json([ + 'success' => true, + 'data' => [ + 'customer_id' => $user->customer_id, + 'customer_name' => $user->customer_name, + 'company_name' => $user->company_name, + 'designation' => $user->designation, + 'email' => $user->email, + 'mobile' => $user->mobile_no, + 'address' => $user->address, + 'pincode' => $user->pincode, + 'status' => $user->status, + 'customer_type' => $user->customer_type, + 'profile_image' => $user->profile_image ? url($user->profile_image) : null, + 'date' => $user->date, + 'created_at' => $user->created_at, + ] + ]); + } + + + + /** + * Update profile IMAGE only (no admin approval) + */ + public function updateProfileImage(Request $request) + { + $user = JWTAuth::parseToken()->authenticate(); + + if (! $user) { + return response()->json([ + 'success' => false, + 'message' => 'Unauthorized' + ], 401); + } + + $request->validate([ + 'profile_image' => 'required|image|mimes:jpg,jpeg,png|max:2048' + ]); + + // DELETE OLD IMAGE + if ($user->profile_image && file_exists(public_path($user->profile_image))) { + @unlink(public_path($user->profile_image)); + } + + // SAVE NEW IMAGE + $file = $request->file('profile_image'); + $filename = 'profile_' . time() . '.' . $file->getClientOriginalExtension(); + $folder = 'profile_upload/'; + $file->move(public_path($folder), $filename); + + $user->profile_image = $folder . $filename; + $user->save(); + + return response()->json([ + 'success' => true, + 'message' => 'Profile image updated successfully', + 'profile_image' => url($user->profile_image), + ]); + } + + + + /** + * Submit profile update request (requires admin approval) + */ + public function updateProfileRequest(Request $request) +{ + $user = JWTAuth::parseToken()->authenticate(); + + if (! $user) { + return response()->json([ + 'success' => false, + 'message' => 'Unauthorized' + ], 401); + } + + // Validate input + $request->validate([ + 'customer_name' => 'nullable|string|max:255', + 'company_name' => 'nullable|string|max:255', + 'designation' => 'nullable|string|max:255', + 'email' => 'nullable|email', + 'mobile_no' => 'nullable|string|max:15', + 'address' => 'nullable|string', + 'pincode' => 'nullable|string|max:10' + ]); + + // SAVE AS ARRAY (NOT JSON STRING!) + $updateReq = \App\Models\UpdateRequest::create([ + 'user_id' => $user->id, + 'data' => $request->all(), // <---- FIXED + 'status' => 'pending', + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Profile update request submitted. Waiting for admin approval.', + 'request_id' => $updateReq->id + ]); +} + +} diff --git a/app/Http/Middleware/JwtRefreshMiddleware.php b/app/Http/Middleware/JwtRefreshMiddleware.php new file mode 100644 index 0000000..5d625e2 --- /dev/null +++ b/app/Http/Middleware/JwtRefreshMiddleware.php @@ -0,0 +1,36 @@ +authenticate(); + } catch (TokenExpiredException $e) { + try { + $newToken = JWTAuth::refresh(JWTAuth::getToken()); + auth()->setToken($newToken); + + $response = $next($request); + + return $response->header('Authorization', 'Bearer ' . $newToken); + } catch (\Exception $e) { + return response()->json(['message' => 'Session expired, please login again'], 401); + } + } catch (TokenInvalidException $e) { + return response()->json(['message' => 'Invalid token'], 401); + } catch (JWTException $e) { + return response()->json(['message' => 'Token missing'], 401); + } + + return $next($request); + } +} diff --git a/app/Models/Order.php b/app/Models/Order.php index d3691c8..029cf35 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -4,11 +4,13 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; + class Order extends Model { - use HasFactory; - + use HasFactory,SoftDeletes; + protected $fillable = [ 'order_id', 'mark_no', diff --git a/app/Models/OrderItem.php b/app/Models/OrderItem.php index 75cd416..6d37939 100644 --- a/app/Models/OrderItem.php +++ b/app/Models/OrderItem.php @@ -4,10 +4,11 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; class OrderItem extends Model { - use HasFactory; + use HasFactory, SoftDeletes; protected $fillable = [ 'order_id', diff --git a/app/Models/UpdateRequest.php b/app/Models/UpdateRequest.php new file mode 100644 index 0000000..27e84a4 --- /dev/null +++ b/app/Models/UpdateRequest.php @@ -0,0 +1,30 @@ + 'array', // converts JSON to array automatically + ]; + + // Relationship: request belongs to a user + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index b5df7a5..e67ea2f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -89,4 +89,11 @@ class User extends Authenticatable implements JWTSubject { return []; } +public function invoices() +{ + return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id'); +} + + + } diff --git a/database/migrations/2025_11_24_131305_create_invoice_installments_table.php b/database/migrations/2025_11_24_131305_create_invoice_installments_table.php index f9e33cc..d1c540f 100644 --- a/database/migrations/2025_11_24_131305_create_invoice_installments_table.php +++ b/database/migrations/2025_11_24_131305_create_invoice_installments_table.php @@ -8,23 +8,16 @@ return new class extends Migration { public function up(): void { - Schema::create('invoice_installments', function (Blueprint $table) { - $table->id(); - - $table->unsignedBigInteger('invoice_id'); - $table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade'); - - $table->date('installment_date'); - $table->string('payment_method')->nullable(); // cash, bank, UPI, cheque, etc - $table->string('reference_no')->nullable(); - $table->decimal('amount', 10, 2); - - $table->timestamps(); + // Table already exists. Add updates here if needed. + Schema::table('invoice_installments', function (Blueprint $table) { + // nothing to update }); } public function down(): void { - Schema::dropIfExists('invoice_installments'); + Schema::table('invoice_installments', function (Blueprint $table) { + // nothing to rollback + }); } }; diff --git a/database/migrations/2025_11_28_182057_add_soft_deletes_to_orders_table.php b/database/migrations/2025_11_28_182057_add_soft_deletes_to_orders_table.php new file mode 100644 index 0000000..4d837c8 --- /dev/null +++ b/database/migrations/2025_11_28_182057_add_soft_deletes_to_orders_table.php @@ -0,0 +1,22 @@ +softDeletes(); + }); + } + + public function down(): void + { + Schema::table('order_items', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; diff --git a/database/migrations/2025_11_29_045236_add_deleted_at_to_order_items_table.php b/database/migrations/2025_11_29_045236_add_deleted_at_to_order_items_table.php new file mode 100644 index 0000000..660229b --- /dev/null +++ b/database/migrations/2025_11_29_045236_add_deleted_at_to_order_items_table.php @@ -0,0 +1,26 @@ +softDeletes(); // adds deleted_at (nullable timestamp) + } + }); + } + + public function down(): void + { + Schema::table('order_items', function (Blueprint $table) { + if (Schema::hasColumn('order_items', 'deleted_at')) { + $table->dropSoftDeletes(); // drops deleted_at + } + }); + } +}; diff --git a/database/migrations/2025_12_01_063142_add_deleted_at_to_orders_table.php b/database/migrations/2025_12_01_063142_add_deleted_at_to_orders_table.php new file mode 100644 index 0000000..656e68d --- /dev/null +++ b/database/migrations/2025_12_01_063142_add_deleted_at_to_orders_table.php @@ -0,0 +1,26 @@ +softDeletes(); // creates deleted_at column + }); + } + + public function down() + { + Schema::table('orders', function (Blueprint $table) { + $table->dropColumn('deleted_at'); + }); + } + +}; diff --git a/database/migrations/2025_12_02_055345_create_update_requests_table.php b/database/migrations/2025_12_02_055345_create_update_requests_table.php new file mode 100644 index 0000000..cb4c527 --- /dev/null +++ b/database/migrations/2025_12_02_055345_create_update_requests_table.php @@ -0,0 +1,37 @@ +id(); + + // The user who is requesting profile update + $table->unsignedBigInteger('user_id'); + + // JSON data of the requested profile changes + $table->json('data')->nullable(); + + // pending / approved / rejected + $table->enum('status', ['pending', 'approved', 'rejected'])->default('pending'); + + // Optional message (admin notes) + $table->text('admin_note')->nullable(); + + $table->timestamps(); + + // Foreign key constraint + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + public function down() + { + Schema::dropIfExists('update_requests'); + } +} diff --git a/public/invoices/invoice-INV-2025-000017.pdf b/public/invoices/invoice-INV-2025-000017.pdf new file mode 100644 index 0000000..1c25516 Binary files /dev/null and b/public/invoices/invoice-INV-2025-000017.pdf differ diff --git a/public/invoices/invoice-INV-2025-000019.pdf b/public/invoices/invoice-INV-2025-000019.pdf new file mode 100644 index 0000000..431b377 Binary files /dev/null and b/public/invoices/invoice-INV-2025-000019.pdf differ diff --git a/public/invoices/invoice-INV-2025-000023.pdf b/public/invoices/invoice-INV-2025-000023.pdf new file mode 100644 index 0000000..1519813 Binary files /dev/null and b/public/invoices/invoice-INV-2025-000023.pdf differ diff --git a/public/profile_upload/profile_1764568863.jpg b/public/profile_upload/profile_1764568863.jpg new file mode 100644 index 0000000..bdd8e39 Binary files /dev/null and b/public/profile_upload/profile_1764568863.jpg differ diff --git a/public/profile_upload/profile_1764645094.jpg b/public/profile_upload/profile_1764645094.jpg new file mode 100644 index 0000000..3c6a162 Binary files /dev/null and b/public/profile_upload/profile_1764645094.jpg differ diff --git a/resources/views/admin/customers.blade.php b/resources/views/admin/customers.blade.php index 17162ce..3321dc2 100644 --- a/resources/views/admin/customers.blade.php +++ b/resources/views/admin/customers.blade.php @@ -5,6 +5,14 @@ @section('content')