select('orders.*') ->leftJoin('customers', 'orders.customer_id', '=', 'customers.id') ->leftJoin('users as waiters', 'orders.waiter_id', '=', 'waiters.id') ->with([ 'items.variation', 'items.food', 'customer', 'table', 'paymentMethod', 'waiter', ]); /** * 🔍 GLOBAL SEARCH */ if ($search = request('search')) { $search = strtolower(trim($search)); $query->where(function ($q) use ($search) { $q->whereRaw('LOWER(orders.order_number) LIKE ?', ["%{$search}%"]) ->orWhereRaw('LOWER(orders.order_type) LIKE ?', ["%{$search}%"]) ->orWhereRaw('LOWER(orders.status) LIKE ?', ["%{$search}%"]) ->orWhereRaw('LOWER(customers.name) LIKE ?', ["%{$search}%"]) ->orWhereRaw('LOWER(customers.phone) LIKE ?', ["%{$search}%"]) ->orWhereRaw('LOWER(waiters.first_name) LIKE ?', ["%{$search}%"]); }); } /** * 📌 STATUS FILTER */ if ($status = request('status')) { $query->where('orders.status', $status); } /** * 🍽 ORDER TYPE FILTER */ if ($orderType = request('order_type')) { $query->where('orders.order_type', $orderType); } /** * 👨‍🍳 WAITER FILTER */ if ($waiterId = request('waiter_id')) { $query->where('orders.waiter_id', $waiterId); } /** * 👤 CUSTOMER FILTER */ if ($customerId = request('customer_id')) { $query->where('orders.customer_id', $customerId); } /** * 📆 DATE RANGE FILTER */ if (request('from_date') && request('to_date')) { $query->whereBetween('orders.created_at', [ request('from_date').' 00:00:00', request('to_date').' 23:59:59', ]); } /** * 🔃 SORTING (SAFE WHITELIST) */ $allowedSorts = [ 'created_at', 'order_number', 'status', 'order_type', 'grand_total', ]; $sortBy = request('sort_by', 'created_at'); $sortOrder = request('sort_order', 'desc'); if (! in_array($sortBy, $allowedSorts)) { $sortBy = 'created_at'; } if (! in_array($sortOrder, ['asc', 'desc'])) { $sortOrder = 'desc'; } /** * 📄 PAGINATION */ $orders = $query ->orderBy("orders.$sortBy", $sortOrder) ->paginate(request('perPage', 10)); /** * 📦 APPLY RESOURCE (KEEP PAGINATION STRUCTURE) */ $orders->getCollection()->transform(function ($order) { return new OrderResource($order); }); return $this->responseSuccess([ $orders, ], 'Order fetch successfully.'); } catch (\Throwable $e) { return $this->responseError([], 'Order fetch failed: '.$e->getMessage()); } } public function store(CustomerOrderStoreRequest $request, FirebaseService $firebase): JsonResponse { DB::beginTransaction(); try { $restaurantId = $this->getCurrentRestaurantId(); $orderNumber = OrderHelper::generateOrderNumber($restaurantId); // 🔹 Calculate totals $subtotal = collect($request->items)->sum(fn ($i) => $i['quantity'] * $i['price']); $grandTotal = $subtotal - ($request->discount ?? 0) + ($request->tax ?? 0); // 🔹 Create Order $order = Order::create([ 'restaurant_id' => $restaurantId, 'order_number' => $orderNumber, 'order_type' => $request->order_type, 'status' => 'pending', 'table_id' => $request->table_id, // 'waiter_id' => $request->waiter_id, 'customer_id' => getUserId(), 'payment_method_id' => $request->payment_method_id, 'payment_status' => $request->payment_status ?? false, 'subtotal' => $subtotal, 'discount' => $request->discount ?? 0, 'tax' => $request->tax ?? 0, 'grand_total' => $grandTotal, 'order_date' => Carbon::now(), 'added_by' => getUserId(), ]); // 🔹 Create Order Items foreach ($request->items as $item) { $orderItem = $order->items()->create([ 'restaurant_id' => $restaurantId, 'food_item_id' => $item['food_item_id'], 'food_variant_id' => $item['food_variant_id'] ?? null, 'quantity' => $item['quantity'], 'price' => $item['price'], 'total' => $item['quantity'] * $item['price'], 'addons' => isset($item['addons']) ? json_encode($item['addons']) : null, ]); /** * 🔽 Ingredient deduction (Commented out for reference) * * foreach ($item['ingredients'] ?? [] as $ingredient) { * $ingredientModel = Ingredient::find($ingredient['id']); * if ($ingredientModel) { * $deductQty = $ingredient['quantity_required'] * $item['quantity']; * $ingredientModel->decrement('available_quantity', $deductQty); * } * } */ } $customer = auth()->user(); // Action Log info $this->trackAction(ActionStatus::OrderCreate, Customer::class, $customer->id, $order); DB::commit(); // Push Notification if ($customer?->fcm_token) { $result = $firebase->sendNotification( $customer->fcm_token, 'Order Confirmed', "Your order #{$order->id} has been confirmed!", ['order_id' => $order->id] ); if ($result === true) { return response()->json(['message' => 'Notification sent successfully']); } else { return response()->json(['message' => 'Failed to send notification', 'error' => $result], 500); } } // Directly send email if ($order->customer?->email) { try { $result = shop_mailer_send($order->restaurant_id, $order->customer?->email, new OrderInvoiceMail($order)); if (! $result['status']) { Log::warning("Order invoice email not sent for shop {$order->restaurant_id}: ".$result['message']); } Mail::to($order->customer?->email)->send(new OrderInvoiceMail($order)); } catch (Exception $ex) { Log::error("Invoice email failed for order {$order->id}: ".$ex->getMessage()); } } return $this->responseSuccess([ 'order' => $order->load('items'), ], 'Order created successfully.'); } catch (Exception $e) { DB::rollBack(); return $this->responseError([], 'Order creation failed: '.$e->getMessage()); } } public function show(int $id): JsonResponse { try { $order = Order::with([ 'items.food', // If you have relation: OrderItem → Food 'customer', 'table', 'paymentMethod', ])->findOrFail($id); return $this->responseSuccess($order, 'Order has been fetched successfully.'); } catch (Exception $e) { return $this->responseError([], $e->getMessage()); } } public function update(CustomerOrderUpdateRequest $request, int $id): JsonResponse { DB::beginTransaction(); try { $order = Order::with('items')->findOrFail($id); // Recalculate total $totalAmount = collect($request->items)->sum(function ($item) { return $item['quantity'] * $item['price']; }); // Update order (order_number stays untouched) $order->update([ 'order_type' => $request->order_type, // 'status' => $request->status, 'table_id' => $request->table_id, 'payment_method_id' => $request->payment_method_id, 'payment_status' => $request->payment_status ?? false, 'waiter_id' => $request->waiter_id, // 'customer_id' => $request->customer_id, 'grand_total' => $totalAmount, 'updated_by' => getUserId(), ]); $existingItemIds = $order->items->pluck('id')->toArray(); $requestItemIds = collect($request->items)->pluck('id')->filter()->toArray(); // 1️⃣ Remove items that are not in request anymore $itemsToDelete = array_diff($existingItemIds, $requestItemIds); if (! empty($itemsToDelete)) { $order->items()->whereIn('id', $itemsToDelete)->delete(); } // 2️⃣ Add or update items foreach ($request->items as $item) { if (isset($item['id']) && in_array($item['id'], $existingItemIds)) { // Update existing item $order->items()->where('id', $item['id'])->update([ 'restaurant_id' => $order->restaurant_id, 'food_item_id' => $item['food_item_id'], 'food_variant_id' => $item['food_variant_id'] ?? null, 'quantity' => $item['quantity'], 'price' => $item['price'], 'total' => $item['quantity'] * $item['price'], ]); } else { // Add new item $order->items()->create([ 'restaurant_id' => $order->restaurant_id, 'food_item_id' => $item['food_item_id'], 'food_variant_id' => $item['food_variant_id'] ?? null, 'quantity' => $item['quantity'], 'price' => $item['price'], 'total' => $item['quantity'] * $item['price'], ]); } } DB::commit(); return $this->responseSuccess($order->load('items'), 'Order has been updated successfully.'); } catch (\Throwable $e) { DB::rollBack(); return $this->responseError([], 'Failed to update order: '.$e->getMessage()); } } public function destroy(int $id): JsonResponse { DB::beginTransaction(); try { $order = Order::with('items')->findOrFail($id); // Delete related order items foreach ($order->items as $item) { $item->delete(); } // Delete the order $order->delete(); DB::commit(); return $this->responseSuccess([], 'Order and related items have been deleted successfully.'); } catch (\Throwable $e) { DB::rollBack(); return $this->responseError([], 'Failed to delete order: '.$e->getMessage()); } } public function cancel(Request $request, int $orderId, FirebaseService $firebase): JsonResponse { $request->validate([ 'cancel_reason' => 'nullable|string|max:255', ]); DB::beginTransaction(); try { $restaurantId = $this->getCurrentRestaurantId(); $order = Order::where('restaurant_id', $restaurantId) ->where('id', $orderId) ->first(); if (! $order) { return $this->responseError([], 'Order not found.'); } // 🚫 Block if already cancelled or completed if (in_array($order->status, ['cancelled', 'completed'])) { return $this->responseError([], "This order is already {$order->status} and cannot be cancelled."); } // 🕒 Check time difference (5 minutes) $minutesSinceOrder = now()->diffInMinutes($order->created_at); $cancelLimit = env('ORDER_CANCEL_LIMIT', 5); if ($minutesSinceOrder > $cancelLimit) { return $this->responseError([], "You can only cancel an order within {$cancelLimit} minutes of placement."); } // 🔹 Update order cancellation info $order->update([ 'status' => OrderStatus::CANCELLED, 'cancelled_at' => now(), 'cancel_reason' => $request->cancel_reason ?? 'No reason provided', 'cancelled_by' => getUserId(), ]); $customer = auth()->user(); // Action Log info $this->trackAction(ActionStatus::OrderCreate, Customer::class, $customer->id, $order); DB::commit(); // TODO // Order Cancel Mail & Notification // // Push Notification // if ($customer?->fcm_token) { // $result = $firebase->sendNotification( // $customer->fcm_token, // 'Order Confirmed', // "Your order #{$order->id} has been confirmed!", // ['order_id' => $order->id] // ); // if ($result === true) { // return response()->json(['message' => 'Notification sent successfully']); // } else { // return response()->json(['message' => 'Failed to send notification', 'error' => $result], 500); // } // } // // Directly send email // if ($order->customer?->email) { // try { // $result = shop_mailer_send($order->restaurant_id, $order->customer?->email, new OrderInvoiceMail($order)); // if (! $result['status']) { // Log::warning("Order invoice email not sent for shop {$order->restaurant_id}: " . $result['message']); // } // Mail::to($order->customer?->email)->send(new OrderInvoiceMail($order)); // } catch (Exception $ex) { // Log::error("Invoice email failed for order {$order->id}: " . $ex->getMessage()); // } // } return $this->responseSuccess([ 'order' => $order->fresh(), ], 'Order cancelled successfully.'); } catch (Exception $e) { DB::rollBack(); return $this->responseError([], 'Order cancellation failed: '.$e->getMessage()); } } public function myBooking(): JsonResponse { try { $filters = request()->all(); $query = Booking::where('customer_id', getUserId()) ->with([ 'customer', 'hotel', 'bookingItems.room', ]); // ----------------------------- // 🔍 SEARCHING // ----------------------------- if (! empty($filters['search'])) { $search = $filters['search']; $query->whereHas('customer', function ($q) use ($search) { $q->where('name', 'LIKE', "%$search%") ->orWhere('phone', 'LIKE', "%$search%"); })->orWhere('id', $search); } // ----------------------------- // 🎯 FILTERS // ----------------------------- if (isset($filters['status'])) { $query->where('status', $filters['status']); } if (! empty($filters['customer_id'])) { $query->where('customer_id', $filters['customer_id']); } if (! empty($filters['hotel_id'])) { $query->where('hotel_id', $filters['hotel_id']); } if (! empty($filters['check_in_from']) && ! empty($filters['check_in_to'])) { $query->whereBetween('check_in', [$filters['check_in_from'], $filters['check_in_to']]); } if (! empty($filters['check_out_from']) && ! empty($filters['check_out_to'])) { $query->whereBetween('check_out', [$filters['check_out_from'], $filters['check_out_to']]); } // ----------------------------- // 🔽 SORTING // ----------------------------- $sortBy = $filters['sort_by'] ?? 'check_in'; $sortOrder = $filters['sort_order'] ?? 'desc'; $allowedSortColumns = ['id', 'check_in', 'check_out', 'status', 'created_at']; if (! in_array($sortBy, $allowedSortColumns)) { $sortBy = 'check_in'; } $query->orderBy($sortBy, $sortOrder); // ----------------------------- // 📄 PAGINATION // ----------------------------- $perPage = $filters['per_page'] ?? 20; $bookings = $query->paginate($perPage); return $this->responseSuccess($bookings, 'Bookings fetched successfully.'); } catch (Exception $e) { return $this->responseError([], $e->getMessage()); } } public function booking(CustomerBookingStoreRequest $request): JsonResponse { $data = $request->validated(); DB::beginTransaction(); try { // Create main booking $booking = Booking::create([ 'restaurant_id' => getUserRestaurantId(), 'hotel_id' => $data['hotel_id'], 'customer_id' => getUserId(), 'check_in' => $data['check_in'], 'check_out' => $data['check_out'], 'check_in_time' => $data['check_in_time'] ?? null, 'check_out_time' => $data['check_out_time'] ?? null, 'total_adults' => $data['total_adults'], 'total_children' => $data['total_children'], 'subtotal_amount' => $data['subtotal_amount'], 'tax_amount' => $data['tax_amount'] ?? 0, 'discount_amount' => $data['discount_amount'] ?? 0, 'total_amount' => $data['total_amount'], 'payment_status' => $data['payment_status'], 'payment_method' => $data['payment_method'] ?? null, 'channel' => $data['channel'] ?? null, 'status' => $data['status'], 'remarks' => $data['remarks'] ?? null, ]); // Create booking items foreach ($data['booking_items'] as $item) { BookingItem::create([ 'restaurant_id' => getUserRestaurantId(), 'booking_id' => $booking->id, 'room_id' => $item['room_id'], 'adults' => $item['adults'], 'children' => $item['children'], 'room_price' => $item['room_price'], 'nights' => $item['nights'], 'tax_amount' => $item['tax_amount'] ?? 0, 'total_amount' => $item['total_amount'], 'status' => $item['status'], ]); } DB::commit(); return $this->responseSuccess($booking->load('bookingItems'), 'Booking has been created successfully.'); } catch (Exception $e) { DB::rollBack(); return $this->responseError([], $e->getMessage()); } } }