Skip to content
Merged

Dev #17

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions dashboard/src/components/MenuPage/Cart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -410,19 +410,21 @@ const Cart = () => {
<div
key={item.name}
className={cn(
"flex justify-between bg-secondary-background p-2 rounded",
"flex justify-between gap-2 bg-secondary-background p-2 rounded min-w-0",
isActive && "border-2 border-primary bg-primary/10"
)}
>
<div className="flex gap-4 font-bold">
<p>x{item.quantity}</p>
<p className="max-w-30 truncate">{item.item_name || item.name}</p>
<i>
<div className="flex gap-2 font-bold min-w-0 flex-1 text-sm">
<p className="shrink-0">x{item.quantity}</p>
<p className="min-w-0 flex-1 break-words whitespace-normal text-xs">
{item.item_name || item.name}
</p>
<i className="shrink-0">
{formatCurrency(item.price ?? item.standard_rate ?? 0)}
</i>
</div>

<div className="flex">
<div className="flex shrink-0">
<Trash2
onClick={() => removeFromCart(item)}
className="cursor-pointer text-red-600"
Expand Down
99 changes: 92 additions & 7 deletions dashboard/src/components/MenuPage/Menu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { useEffect, useState, useRef } from "react";
import MenuItemCard from "@/components/MenuPage/MenuItemCard";
import { useMenuContext } from "@/contexts/MenuContext";

import { useAgents } from "@/hooks";
import { useAgents, useRooms } from "@/hooks";
import { isRoomDirectBookingsEnabled } from "@/lib/utils";
import ShiftDialog from "@/components/ui/ShiftDialog"; // adjust path if needed
import {
Select,
Expand All @@ -27,6 +28,8 @@ const Menu = () => {
const [shiftDialogOpen, setShiftDialogOpen] = useState(false);
const [shiftType, setShiftType] = useState("open"); // "open", "continue", "close"
const [hasActiveShift, setHasActiveShift] = useState(true);
const [roomBookingsEnabled, setRoomBookingsEnabled] = useState(false);
const [selectedRoom, setSelectedRoom] = useState(null);

const {
fetchMenuItems,
Expand All @@ -48,6 +51,7 @@ const Menu = () => {
target,
setTarget,
menuGridRef,
currentIndex,
} = useMenuContext();

const {
Expand All @@ -56,6 +60,8 @@ const Menu = () => {
fetchAgents,
} = useAgents();

const { rooms, loading: loadingRooms, fetchRooms } = useRooms();

const [openMixDialog, setOpenMixDialog] = useState(false);

const searchInputRef = useRef(null);
Expand Down Expand Up @@ -96,6 +102,20 @@ const Menu = () => {
fetchMenuItems();
}, [fetchMenuItems]);

useEffect(() => {
const checkRoomBookings = async () => {
const enabled = await isRoomDirectBookingsEnabled();
setRoomBookingsEnabled(Boolean(enabled));
};
checkRoomBookings();
}, []);

useEffect(() => {
if (selectedRoom && selectedRoom.customer) {
setCustomer(selectedRoom.customer);
}
}, [selectedRoom, setCustomer]);

useEffect(() => {
const handleF3Click = (event) => {
if (event.key === "F3") {
Expand Down Expand Up @@ -166,7 +186,7 @@ useEffect(() => {
});

const data = await res.json();
console.log("Shift status response:", data);
// console.log("Shift status response:", data);

const status = data.message?.status || "open";
setShiftType(status);
Expand All @@ -191,10 +211,10 @@ useEffect(() => {
return (
<>
<NumPad isOpen={false} setIsOpen={() => {}} />
<div className="flex items-center justify-between gap-4">
<p className="text-2xl my-4">{selectedCategory?.name || "Menu"}</p>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 justify-between min-w-0">
<p className="text-2xl my-4 shrink-0">{selectedCategory?.name || "Menu"}</p>

<div className="flex items-center gap-2 flex-1 justify-end">
<div className="flex flex-wrap items-center gap-2 min-w-0 flex-1 justify-end">
<Button
variant={"outline"}
onClick={() => setOpenMixDialog(true)}
Expand Down Expand Up @@ -263,7 +283,13 @@ useEffect(() => {
label: cust.customer_name || cust.name,
}))}
value={customer}
onValueChange={setCustomer}
onValueChange={(value) => {
setCustomer(value);
// Clear room selection when customer is manually changed
if (value && selectedRoom) {
setSelectedRoom(null);
}
}}
placeholder={loadingCustomers ? "Loading..." : "Select customer"}
searchPlaceholder="Search customers..."
disabled={loadingCustomers}
Expand All @@ -281,6 +307,47 @@ useEffect(() => {
}
}}
/>
{roomBookingsEnabled && (
<Combobox
type="room"
options={rooms.map((room) => ({
value: room.name,
name: room.name,
label: `Room ${room.room_number}${room.guest_name ? ` - ${room.guest_name}` : ""}`,
room_number: room.room_number,
customer: room.customer,
customer_name: room.customer_name,
}))}
value={selectedRoom?.name || ""}
onValueChange={(value) => {
if (value) {
const room = rooms.find((r) => r.name === value);
if (room) {
setSelectedRoom(room);
if (room.customer) {
setCustomer(room.customer);
}
}
} else {
setSelectedRoom(null);
}
}}
placeholder={loadingRooms ? "Loading..." : "Select room"}
searchPlaceholder="Search rooms..."
disabled={loadingRooms}
className="w-[200px]"
onOpenChange={(open) => {
if (open) {
fetchRooms();
}
if (!open && target === "menu") {
requestAnimationFrame(() => {
searchInputRef.current?.focus();
});
}
}}
/>
)}
<div className="flex items-center bg-background px-2 py-1 rounded-sm focus-within:ring-2 focus-within:ring-primary focus-within:border-primary">
<input
type="text"
Expand All @@ -290,6 +357,24 @@ useEffect(() => {
className="w-[200px] focus:outline-none focus:ring-0 focus:border-transparent"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && filteredItems.length > 0) {
e.preventDefault();
e.stopPropagation();
const index = Math.min(currentIndex, filteredItems.length - 1);
if (index < 0) return;
const item = filteredItems[index];
addToCart({
name: item.name,
item_name: item.item_name,
custom_menu_category: item.custom_menu_category,
quantity: 1,
price: item.standard_rate ?? item.price ?? 0,
standard_rate: item.standard_rate ?? item.price ?? 0,
remark: "",
});
}
}}
onBlur={(e) => {
// if (target !== "menu") return;

Expand Down Expand Up @@ -343,7 +428,7 @@ useEffect(() => {
open={shiftDialogOpen}
type={shiftType}
onOpenChange={setShiftDialogOpen}
onShiftAction={(action, msg) => console.log(action, msg)}
// onShiftAction={(action, msg) => console.log(action, msg)}
/>
</>
);
Expand Down
128 changes: 80 additions & 48 deletions dashboard/src/components/MenuPage/MenuItemCard.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, useMemo } from "react";
import { cn } from "@/lib/utils";
import { useCartStore } from "@/stores/useCartStore";
import { checkStock, negativeStock, getItemUoms } from "@/lib/utils";
import { checkStock, getItemUoms, negativeStock } from "@/lib/utils";
import { toast } from "sonner";

import { useMenuContext } from "@/contexts/MenuContext";
Expand All @@ -12,8 +12,9 @@ const MenuItemCard = ({ item, index }) => {
const [showUomModal, setShowUomModal] = useState(false);
const [pendingItem, setPendingItem] = useState(null);
const [dynamicUoms, setDynamicUoms] = useState([]);
const [addingItemName, setAddingItemName] = useState(null);

const { currentIndex, setCurrentIndex, target, setTarget } = useMenuContext();
const { currentIndex, setCurrentIndex, target, setTarget, allowNegativeStock } = useMenuContext();
const addToCart = useCartStore((state) => state.addToCart);
const cartItems = useCartStore((state) => state.cart || []);

Expand All @@ -34,53 +35,83 @@ const MenuItemCard = ({ item, index }) => {
const isActive = currentIndex === index && target === "menu";

const handleAddToCart = async () => {
const stockData = await checkStock(item.name);
const allowNegative = await negativeStock();

if (!allowNegative && stockData?.stock <= 0) {
toast.error("Error", { description: `No stock available for ${item.item_name}` });
return;
}

const existingCartItem = cartItems.find((ci) => ci.name === item.name);

if (existingCartItem) {
// Already in cart → increment quantity
addToCart({
...existingCartItem,
quantity: existingCartItem.quantity + 1,
// Prevent double-add: only one add-in-progress per item at a time
if (addingItemName === item.name) return;
setAddingItemName(item.name);

try {
// can_use_negative_stock is loaded once on menu open (MenuContext). Only check stock when negative not allowed.
if (allowNegativeStock === false) {
const stockData = await checkStock(item.name);
if (stockData?.stock <= 0) {
toast.error("Error", { description: `No stock available for ${item.item_name}` });
return;
}
} else if (allowNegativeStock === null) {
// Not yet loaded: one-time fallback so we don't add when stock is 0
const [stockData, allowNegative] = await Promise.all([
checkStock(item.name),
negativeStock(),
]);
if (!allowNegative && stockData?.stock <= 0) {
toast.error("Error", { description: `No stock available for ${item.item_name}` });
return;
}
}
// allowNegativeStock === true: skip stock check entirely

// Use fresh cart from store in case another add completed while we awaited
const currentCart = useCartStore.getState().cart || [];
const existingCartItem = currentCart.find((ci) => ci.name === item.name);

if (existingCartItem) {
addToCart({
...existingCartItem,
quantity: existingCartItem.quantity + 1,
});
return;
}

// Not in cart → fetch UOMs (single call)
const fetchedUoms = await getItemUoms(item.name);

if (!fetchedUoms || fetchedUoms.length === 0) {
toast.error(`No UOMs found for ${item.item_name}`);
return;
}

// Re-check cart after fetch; user might have added same item via another click
const cartAfterFetch = useCartStore.getState().cart || [];
const existingNow = cartAfterFetch.find((ci) => ci.name === item.name);
if (existingNow) {
addToCart({ ...existingNow, quantity: existingNow.quantity + 1 });
return;
}

if (fetchedUoms.length === 1) {
addToCart({
name: item.name,
item_name: item.item_name,
custom_menu_category: item.custom_menu_category,
quantity: 1,
uom: fetchedUoms[0],
price: item.standard_rate ?? item.price ?? 0,
standard_rate: item.standard_rate ?? item.price ?? 0,
remark: "No stock override",
});
return;
}

setPendingItem(item);
setDynamicUoms(fetchedUoms);
setShowUomModal(true);
} catch (err) {
toast.error("Could not add item", {
description: err?.message || "Please try again.",
});
return;
}

// Not in cart → fetch UOMs dynamically
const fetchedUoms = await getItemUoms(item.name);
console.log("fetched uoms", fetchedUoms);

if (!fetchedUoms || fetchedUoms.length === 0) {
toast.error(`No UOMs found for ${item.item_name}`);
return;
} finally {
setAddingItemName(null);
}

if (fetchedUoms.length === 1) {
// Only one UOM → add directly
addToCart({
name: item.name,
item_name: item.item_name,
custom_menu_category: item.custom_menu_category,
quantity: 1,
uom: fetchedUoms[0],
price: item.standard_rate ?? item.price ?? 0,
standard_rate: item.standard_rate ?? item.price ?? 0,
remark: "No stock override",
});
return;
}

// Multiple UOMs → show modal
setPendingItem(item);
setDynamicUoms(fetchedUoms);
setShowUomModal(true);
};


Expand Down Expand Up @@ -118,7 +149,8 @@ const handleAddToCart = async () => {
}}
className={cn(
"menu-item cursor-pointer rounded-lg border shadow-sm transition transform hover:shadow-md hover:scale-[1.02] active:scale-[0.98] active:bg-gray-50 py-2 gap-2",
isActive && "border-primary bg-primary/10"
isActive && "border-primary bg-primary/10",
addingItemName === item.name && "pointer-events-none opacity-70"
)}
>
<CardHeader className="flex items-start justify-between px-3 py-1">
Expand Down
6 changes: 3 additions & 3 deletions dashboard/src/components/MenuPage/MultiCurrencyDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default function MultiCurrencyDialog({
const itemsToUse = useMemo(() => {
const items = cartItems && cartItems.length > 0 ? cartItems : cartStoreItems;
if (!items || items.length === 0) {
console.warn("MultiCurrencyDialog: No cart items found", { cartItems, cartStoreItems });
// console.warn("MultiCurrencyDialog: No cart items found", { cartItems, cartStoreItems });
return [];
}
// Ensure items are in the correct format for the API
Expand All @@ -62,7 +62,7 @@ export default function MultiCurrencyDialog({
price: item.price || item.rate || item.standard_rate || 0,
rate: item.rate || item.price || item.standard_rate || 0,
}));
console.log("MultiCurrencyDialog: Formatted cart items", formattedItems);
// console.log("MultiCurrencyDialog: Formatted cart items", formattedItems);
return formattedItems;
}, [cartItems, cartStoreItems]);

Expand Down Expand Up @@ -471,7 +471,7 @@ export default function MultiCurrencyDialog({
cartItems: itemsToUse,
orderPayload: orderPayload,
});
console.log("still muilti",paymentData);
// console.log("still muilti",paymentData);
} catch (err) {
// Log error but don't block user (payment already shown as successful)
console.error("Payment processing error (background):", err);
Expand Down
Loading
Loading