@@ -63,6 +63,41 @@ public function __construct(
6363 'cancelled ' => 'cancelled_at ' ,
6464 ];
6565
66+ public function getItemsData (array $ filters , array $ scope ): array
67+ {
68+ $ scopedOutletId = $ this ->outletIdFromScope ($ scope );
69+
70+ $ query = OrderItem::with ([
71+ 'order:id,order_number,order_type,outlet_id,status ' ,
72+ 'order.orderTables.diningTable:id,name ' ,
73+ 'food:id,name ' ,
74+ 'foodVariant:id,name ' ,
75+ 'preparationDepartment:id,name ' ,
76+ 'addons.addon:id,name ' ,
77+ ])
78+ ->whereHas ('order ' , function ($ q ) use ($ scopedOutletId , $ filters ) {
79+ $ q ->whereNotIn ('status ' , ['completed ' , 'cancelled ' ])
80+ ->when ($ scopedOutletId !== null , fn ($ b ) => $ b ->where ('outlet_id ' , $ scopedOutletId ))
81+ ->when ($ scopedOutletId === null && $ filters ['outlet_id ' ] !== '' , fn ($ b ) => $ b ->where ('outlet_id ' , $ filters ['outlet_id ' ]));
82+ })
83+ ->when ($ filters ['department_id ' ] !== '' , fn ($ b ) => $ b ->where ('preparation_department_id ' , $ filters ['department_id ' ]))
84+ ->when ($ filters ['status ' ] !== '' , fn ($ b ) => $ b ->where ('status ' , $ filters ['status ' ]))
85+ ->when ($ filters ['date ' ] !== '' , fn ($ b ) => $ b ->whereDate ('created_at ' , $ filters ['date ' ]))
86+ ->orderBy ('created_at ' );
87+
88+ $ items = $ query ->paginate ($ this ->perPage ($ query , $ filters ['per_page ' ]))->withQueryString ();
89+
90+ $ outlets = $ this ->scopeOutlets ($ scope );
91+
92+ $ departments = OutletDepartment::where ('can_prepare_order ' , true )
93+ ->where ('is_active ' , true )
94+ ->when ($ scopedOutletId !== null , fn ($ b ) => $ b ->where ('outlet_id ' , $ scopedOutletId ))
95+ ->orderBy ('name ' )
96+ ->get (['id ' , 'name ' ]);
97+
98+ return compact ('items ' , 'outlets ' , 'departments ' , 'filters ' );
99+ }
100+
66101 public function getIndexData (array $ filters , array $ scope ): array
67102 {
68103 $ scopedOutletId = $ this ->outletIdFromScope ($ scope );
@@ -157,7 +192,13 @@ public function getShowData(Order $order): array
157192
158193 $ availableTables = $ this ->getAvailableDiningTables ($ order ->outlet_id , $ order ->id );
159194
160- return compact ('order ' , 'availableFoods ' , 'availableTables ' );
195+ $ departments = OutletDepartment::where ('outlet_id ' , $ order ->outlet_id )
196+ ->where ('can_prepare_order ' , true )
197+ ->where ('is_active ' , true )
198+ ->orderBy ('name ' )
199+ ->get (['id ' , 'name ' ]);
200+
201+ return compact ('order ' , 'availableFoods ' , 'availableTables ' , 'departments ' );
161202 }
162203
163204 public function createOrder (array $ data ): Order
@@ -488,6 +529,109 @@ public function changeItemStatus(OrderItem $item, string $toStatus): void
488529 });
489530 }
490531
532+ public function transferItemDepartment (OrderItem $ item , int $ targetDepartmentId ): void
533+ {
534+ abort_if (
535+ ! in_array ($ item ->status , ['stock_reserved ' , 'sent_to_kitchen ' ]),
536+ 422 ,
537+ 'Item can only be transferred before preparation starts. '
538+ );
539+
540+ DB ::transaction (function () use ($ item , $ targetDepartmentId ) {
541+ $ item ->loadMissing (['order ' , 'food ' , 'addons.addon ' ]);
542+
543+ $ targetDept = OutletDepartment::where ('id ' , $ targetDepartmentId )
544+ ->where ('outlet_id ' , $ item ->order ->outlet_id )
545+ ->where ('can_prepare_order ' , true )
546+ ->where ('is_active ' , true )
547+ ->firstOrFail ();
548+
549+ abort_if (
550+ $ targetDept ->id === $ item ->preparation_department_id ,
551+ 422 ,
552+ 'Item is already assigned to this department. '
553+ );
554+
555+ $ deductions = $ this ->orderItemDeductions ($ item );
556+
557+ if ($ deductions === []) {
558+ $ item ->update (['preparation_department_id ' => $ targetDept ->id ]);
559+ return ;
560+ }
561+
562+ $ targetWh = $ this ->findDepartmentWarehouse ($ targetDept );
563+
564+ if (! $ targetWh ) {
565+ throw ValidationException::withMessages ([
566+ 'department ' => "Department \"{$ targetDept ->name }\" has no active warehouse configured. " ,
567+ ]);
568+ }
569+
570+ if (! $ this ->warehouseCanFulfill ($ targetWh , $ deductions )) {
571+ $ this ->autoTransferIngredientsToWarehouse ($ targetWh , $ deductions , $ item ->order );
572+ }
573+
574+ // Release stock reserved in the current department
575+ $ this ->releaseOrderItemStock ($ item );
576+
577+ // Assign to target department and reserve stock there
578+ $ item ->update (['preparation_department_id ' => $ targetDept ->id ]);
579+
580+ $ this ->reserveOrderItemStock ($ item ->fresh (['food ' , 'addons.addon ' , 'order ' ]));
581+ });
582+ }
583+
584+ private function autoTransferIngredientsToWarehouse (Warehouse $ targetWh , array $ deductions , Order $ order ): void
585+ {
586+ // Pass 1: outlet warehouse
587+ $ outletWarehouses = Warehouse::where ('outlet_id ' , $ order ->outlet_id )
588+ ->where ('type ' , 'outlet ' )
589+ ->where ('is_active ' , true )
590+ ->orderByDesc ('is_default ' )
591+ ->get ();
592+
593+ foreach ($ outletWarehouses as $ outletWh ) {
594+ if ($ this ->warehouseCanFulfill ($ outletWh , $ deductions )) {
595+ $ this ->autoTransferIngredients ($ outletWh , $ targetWh , $ deductions , $ order );
596+ return ;
597+ }
598+ }
599+
600+ // Pass 2: central warehouse → outlet warehouse → target
601+ $ intermediateWh = $ outletWarehouses ->first ()
602+ ?? Warehouse::where ('outlet_id ' , $ order ->outlet_id )
603+ ->where ('is_active ' , true )
604+ ->orderByDesc ('is_default ' )
605+ ->first ();
606+
607+ if ($ intermediateWh ) {
608+ $ centralWarehouses = Warehouse::where ('type ' , 'central ' )
609+ ->where ('is_active ' , true )
610+ ->orderByDesc ('is_default ' )
611+ ->get ();
612+
613+ foreach ($ centralWarehouses as $ centralWh ) {
614+ if ($ this ->warehouseCanFulfill ($ centralWh , $ deductions )) {
615+ $ this ->autoTransferIngredients ($ centralWh , $ intermediateWh , $ deductions , $ order );
616+ $ this ->autoTransferIngredients ($ intermediateWh , $ targetWh , $ deductions , $ order );
617+ return ;
618+ }
619+ }
620+ }
621+
622+ // Nothing had stock — throw detailed error
623+ foreach ($ deductions as $ ingredientId => $ deduction ) {
624+ $ ingredient = Ingredient::findOrFail ((int ) $ ingredientId );
625+ $ available = $ this ->warehouseAvailableStock ($ targetWh ->id , $ ingredientId );
626+
627+ if ($ available < (float ) $ deduction ['total_quantity ' ]) {
628+ throw ValidationException::withMessages ([
629+ 'stock ' => "Insufficient stock for \"{$ ingredient ->name }\" across all warehouses. Available: {$ available }, Requested: {$ deduction ['total_quantity ' ]}. " ,
630+ ]);
631+ }
632+ }
633+ }
634+
491635 public function syncOrderStatus (Order $ order ): void
492636 {
493637 $ items = $ order ->items ()->get (['status ' ]);
@@ -1029,59 +1173,23 @@ private function selectPreparationDepartment(OrderItem $item): OutletDepartment
10291173 }
10301174 }
10311175
1032- // Pass 2: outlet warehouse has stock — instant-transfer to department warehouse
1033- $ outletWarehouses = Warehouse::where ('outlet_id ' , $ item ->order ->outlet_id )
1034- ->where ('type ' , 'outlet ' )
1035- ->where ('is_active ' , true )
1036- ->orderByDesc ('is_default ' )
1037- ->get ();
1038-
1039- foreach ($ outletWarehouses as $ outletWh ) {
1040- if (! $ this ->warehouseCanFulfill ($ outletWh , $ deductions )) {
1176+ // Passes 2 & 3: try to get stock into a department warehouse via outlet/central transfer
1177+ foreach ($ departments as $ dept ) {
1178+ $ deptWh = $ this ->findDepartmentWarehouse ($ dept );
1179+ if (! $ deptWh ) {
10411180 continue ;
10421181 }
1043- foreach ($ departments as $ dept ) {
1044- $ deptWh = $ this ->findDepartmentWarehouse ($ dept );
1045- if (! $ deptWh ) {
1046- continue ;
1047- }
1048- $ this ->autoTransferIngredients ($ outletWh , $ deptWh , $ deductions , $ item ->order );
1182+ try {
1183+ $ this ->autoTransferIngredientsToWarehouse ($ deptWh , $ deductions , $ item ->order );
10491184 return $ dept ;
1050- }
1051- }
1052-
1053- // Pass 3: central warehouse has stock — instant-transfer central → outlet → department
1054- $ centralWarehouses = Warehouse::where ('type ' , 'central ' )
1055- ->where ('is_active ' , true )
1056- ->orderByDesc ('is_default ' )
1057- ->get ();
1058-
1059- $ intermediateOutletWh = $ outletWarehouses ->first ()
1060- ?? Warehouse::where ('outlet_id ' , $ item ->order ->outlet_id )
1061- ->where ('is_active ' , true )
1062- ->orderByDesc ('is_default ' )
1063- ->first ();
1064-
1065- if ($ intermediateOutletWh ) {
1066- foreach ($ centralWarehouses as $ centralWh ) {
1067- if (! $ this ->warehouseCanFulfill ($ centralWh , $ deductions )) {
1068- continue ;
1069- }
1070- foreach ($ departments as $ dept ) {
1071- $ deptWh = $ this ->findDepartmentWarehouse ($ dept );
1072- if (! $ deptWh ) {
1073- continue ;
1074- }
1075- $ this ->autoTransferIngredients ($ centralWh , $ intermediateOutletWh , $ deductions , $ item ->order );
1076- $ this ->autoTransferIngredients ($ intermediateOutletWh , $ deptWh , $ deductions , $ item ->order );
1077- return $ dept ;
1078- }
1185+ } catch (ValidationException ) {
1186+ continue ;
10791187 }
10801188 }
10811189
10821190 // All passes failed — report the specific shortage
1083- $ fallback = $ departments ->first ();
1084- $ fallbackWh = $ this ->findDepartmentWarehouse ($ fallback );
1191+ $ fallback = $ departments ->first ();
1192+ $ fallbackWh = $ this ->findDepartmentWarehouse ($ fallback );
10851193
10861194 foreach ($ deductions as $ ingredientId => $ deduction ) {
10871195 $ ingredient = Ingredient::findOrFail ((int ) $ ingredientId );
0 commit comments