Skip to content
Closed
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
717 changes: 388 additions & 329 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 16 additions & 2 deletions src/Layout/AppLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,28 @@ import { Outlet, useNavigate } from "react-router-dom";
import SideCartDrawer from "../components/OrderFlow/SideCartDrawer";
import { Toaster } from "sonner";
import { useAuthStore } from "../store";
import { isStaffUser } from "../utils/roleUtils";
import { isStaffUser, isSuperAdmin, isKitchenOnlyUser } from "../utils/roleUtils";

function AppLayout() {
const { user, isAuthenticated } = useAuthStore();
const navigate = useNavigate();

useEffect(() => {
if (isAuthenticated && isStaffUser(user)) {
if (!isAuthenticated || !user) return;

// 1. Admin can navigate to everything in the app (do not restrict or redirect)
if (isSuperAdmin(user)) {
return;
}

// 2. Chief can navigate to only Live Kitchen
if (isKitchenOnlyUser(user)) {
navigate('/dashboard/live-kitchen', { replace: true });
return;
}

// 3. Manager navigates to only the dashboard pages
if (isStaffUser(user)) {
navigate('/dashboard', { replace: true });
}
}, [isAuthenticated, user, navigate]);
Expand Down
11 changes: 7 additions & 4 deletions src/components/Dashboard/DashboardSidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "react-icons/md";
import { FiShoppingBag, FiLogOut, FiUsers } from "react-icons/fi";
import useAuthStore from "../../store/authStore";
import { isKitchenOnlyUser, isAdminUser } from "../../utils/roleUtils";
import { isKitchenOnlyUser, isAdminUser, isSuperAdmin } from "../../utils/roleUtils";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the unused isAdminUser import.

isAdminUser is no longer referenced in this file (replaced by isSuperAdmin), and ESLint flags it as an error, which can fail the lint gate.

🧹 Proposed fix
-import { isKitchenOnlyUser, isAdminUser, isSuperAdmin } from "../../utils/roleUtils";
+import { isKitchenOnlyUser, isSuperAdmin } from "../../utils/roleUtils";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { isKitchenOnlyUser, isAdminUser, isSuperAdmin } from "../../utils/roleUtils";
import { isKitchenOnlyUser, isSuperAdmin } from "../../utils/roleUtils";
🧰 Tools
🪛 ESLint

[error] 13-13: 'isAdminUser' is defined but never used. Allowed unused vars must match /^[A-Z_]/u.

(no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Dashboard/DashboardSidebar.jsx` at line 13, Remove the unused
isAdminUser named import from the roleUtils import in DashboardSidebar.jsx,
while retaining isKitchenOnlyUser and isSuperAdmin.

Source: Linters/SAST tools


const navItems = [
{ to: "/dashboard", label: "Dashboard", icon: MdDashboard, end: true },
Expand All @@ -27,7 +27,7 @@ function DashboardSidebar() {
const { user, logout } = useAuthStore();
const navigate = useNavigate();
const isChief = isKitchenOnlyUser(user);
const isAdmin = isAdminUser(user);
const isSuper = isSuperAdmin(user);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);

useEffect(() => {
Expand All @@ -39,8 +39,11 @@ function DashboardSidebar() {
let visibleNavItems = navItems;
if (isChief) {
visibleNavItems = navItems.filter((item) => item.to === "/dashboard/live-kitchen");
} else if (!isAdmin) {
visibleNavItems = navItems.filter((item) => item.to !== "/dashboard/staff-management");
} else if (isSuper) {
visibleNavItems = [
...navItems,
{ to: "/", label: "Customer App", icon: MdRestaurantMenu, end: true },
];
}

const handleLogout = () => {
Expand Down
19 changes: 9 additions & 10 deletions src/components/Dashboard/DashboardView.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useState } from "react";
import DashboardHeader from "./DashboardHeader";
import MetricCards from "./MetricCards";
import RevenueChart from "./RevenueChart";
Expand All @@ -6,7 +7,6 @@ import OrdersOverviewChart from "./shared/OrdersOverviewChart";
import OrderTypes from "./OrderTypes";
import TrendingMenus from "./TrendingMenus";
import InventoryAlerts from "./InventoryAlerts";
import RecentActivity from "./RecentActivity";
import CustomerReviews from "./CustomerReviews";

import {
Expand All @@ -17,22 +17,23 @@ import {
useOrderTypes,
useTrendingMenus,
useInventoryAlerts,
useRecentActivity,
useCustomerReviews,
} from "../../hooks/dashboard/useDashboard";

import { MetricCardSkeleton, ChartSkeleton } from "./shared/DashboardSkeleton";
import ErrorState from "./shared/ErrorState";

function DashboardView() {
const [overviewPeriod, setOverviewPeriod] = useState("This Week");
const [revenuePeriod, setRevenuePeriod] = useState("This Month");

const { data: metrics, isLoading: loadingMetrics, error: errMetrics } = useDashboardMetrics();
const { data: revenue, isLoading: loadingRev, error: errRev } = useRevenueData();
const { data: revenue, isLoading: loadingRev, error: errRev } = useRevenueData(revenuePeriod);
const { data: categories, isLoading: loadingCats, error: errCats } = useTopCategories();
const { data: ordersOverview, isLoading: loadingOrdOv, error: errOrdOv } = useOrdersOverview();
const { data: ordersOverview, isLoading: loadingOrdOv, error: errOrdOv } = useOrdersOverview(overviewPeriod);
const { data: orderTypes, isLoading: loadingTypes, error: errTypes } = useOrderTypes();
const { data: trending, isLoading: loadingTrend, error: errTrend } = useTrendingMenus();
const { data: inventory, isLoading: loadingInv, error: errInv } = useInventoryAlerts();
const { data: activity, isLoading: loadingAct, error: errAct } = useRecentActivity();
const { data: reviews, isLoading: loadingRevw, error: errRevw } = useCustomerReviews();

const safeMetrics = errMetrics ? {} : (metrics || {});
Expand All @@ -42,7 +43,6 @@ function DashboardView() {
const safeOrderTypes = errTypes ? [] : (orderTypes || []);
const safeTrending = errTrend ? [] : (trending || []);
const safeInventory = errInv ? {} : (inventory || {});
const safeActivity = errAct ? [] : (activity || []);
const safeReviews = errRevw ? [] : (reviews || []);

return (
Expand All @@ -64,22 +64,21 @@ function DashboardView() {
)}

<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-6">
{loadingRev ? <ChartSkeleton height={320} /> : <RevenueChart data={safeRevenue} totalRevenue={safeMetrics?.totalRevenue?.value} />}
{loadingRev ? <ChartSkeleton height={320} /> : <RevenueChart data={safeRevenue} totalRevenue={safeMetrics?.totalRevenue?.value} period={revenuePeriod} onPeriodChange={setRevenuePeriod} />}
{loadingCats ? <ChartSkeleton height={320} /> : <TopCategories data={safeCategories} />}
</div>

<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-6">
{loadingOrdOv ? <ChartSkeleton height={280} /> : <OrdersOverviewChart data={safeOrdersOverview} />}
{loadingOrdOv ? <ChartSkeleton height={280} /> : <OrdersOverviewChart data={safeOrdersOverview} period={overviewPeriod} onPeriodChange={setOverviewPeriod} />}
{loadingTypes ? <ChartSkeleton height={280} /> : <OrderTypes data={safeOrderTypes} />}
</div>

{loadingInv ? <ChartSkeleton height={220} /> : <InventoryAlerts data={safeInventory} />}
</div>

{/* Right Column */}
<div className="w-full xl:w-[340px] flex flex-col gap-6">
<div className="w-full xl:w-[280px] shrink-0 flex flex-col gap-6">
{loadingTrend ? <ChartSkeleton height={280} /> : <TrendingMenus data={safeTrending} />}
{loadingAct ? <ChartSkeleton height={380} /> : <RecentActivity data={safeActivity} />}
</div>
</div>

Expand Down
16 changes: 9 additions & 7 deletions src/components/Dashboard/LiveKitchen/KitchenTicketsTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ export function KitchenTicketsTable({ tickets, isLoading, error, isFetching, onR
const tabFiltered = activeTab === "All"
? [...allTickets]
: allTickets.filter((t) => {
if (activeTab === "Cancelled") return t.status === "Cancelled" || t.status === "CANCELED" || t.status === "CANCELLED";
return t.status === activeTab;
});
if (activeTab === "Cancelled") return t.status === "Cancelled" || t.status === "CANCELED" || t.status === "CANCELLED";
return t.status === activeTab;
});
const totalCount = allTickets.length;
const countsByStatus = allTickets.reduce((acc, t) => {
const key = (t.status === "CANCELED" || t.status === "CANCELLED") ? "Cancelled" : t.status;
Expand Down Expand Up @@ -78,6 +78,8 @@ export function KitchenTicketsTable({ tickets, isLoading, error, isFetching, onR
</tr>
) : tabFiltered.map((ticket) => {
const flow = STATUS_FLOW[ticket.status] || STATUS_FLOW[ticket.status?.toUpperCase()];
// Always use the orderId for status changes so the Kanban board + ticket stay in sync
const actionId = ticket.orderId || ticket.id;
return (
<tr key={ticket.id} className="hover:bg-gray-50/50 transition-colors">
<td className="py-3.5 px-4 text-[13px] font-bold text-[#1a1a1a]">#{ticket.id}</td>
Expand All @@ -89,22 +91,22 @@ export function KitchenTicketsTable({ tickets, isLoading, error, isFetching, onR
{(ticket.status === "Queue" || ticket.status === "QUEUED") && isAdmin && (
<button
type="button"
onClick={() => onAction(ticket.id, "Cancelled", "Cancel Ticket")}
onClick={() => onAction(actionId, "cancelled", "Cancel Ticket")}
className="px-3 py-1.5 rounded-xl text-[12px] font-bold transition-all shadow-sm cursor-pointer border border-rose-200 bg-rose-50 text-rose-600 hover:bg-rose-100"
>
Cancel
</button>
)}
{flow ? (
<button type="button" onClick={() => onAction(ticket.id, flow.next, flow.label)}
<button type="button" onClick={() => onAction(actionId, flow.next.toLowerCase(), flow.label)}
className={`px-4 py-1.5 rounded-xl text-[12px] font-bold transition-all shadow-sm cursor-pointer border-none ${getTicketActionStyle(ticket.status)}`}>
{flow.label}
</button>
) : (ticket.status === "Done" || ticket.status === "DONE") ? (
isAdmin ? (
<button
type="button"
onClick={() => onAction(ticket.id, "Ready", "Move back to Ready")}
onClick={() => onAction(actionId, "ready", "Move back to Ready")}
className="px-3 py-1.5 rounded-xl text-[12px] font-bold transition-all shadow-sm cursor-pointer border border-gray-200 bg-gray-100 text-gray-600 hover:bg-orange-50 hover:text-orange-600 hover:border-orange-200 flex items-center gap-1"
>
<span className="font-extrabold">&lt;</span> Ready
Expand All @@ -116,7 +118,7 @@ export function KitchenTicketsTable({ tickets, isLoading, error, isFetching, onR
isAdmin ? (
<button
type="button"
onClick={() => onAction(ticket.id, "Queue", "Restore to Queue")}
onClick={() => onAction(actionId, "queue", "Restore to Queue")}
className="px-3 py-1.5 rounded-xl text-[12px] font-bold transition-all shadow-sm cursor-pointer border border-rose-200 bg-rose-50 text-rose-600 hover:bg-rose-100 flex items-center gap-1"
>
<span className="font-extrabold">&lt;</span> Queue
Expand Down
97 changes: 55 additions & 42 deletions src/components/Dashboard/LiveKitchenView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import {
useRealtimeKitchen,
useUpdateKitchenStatus,
useActiveTickets,
useUpdateTicketStatus,
useUpdateChefStatus,
useUpdateChefStation,
useUpdateChefDisplayName,
} from "../../hooks/dashboard/useKitchenOrders";
import { KanbanCardSkeleton } from "./shared/DashboardSkeleton";
import ErrorState from "./shared/ErrorState";
Expand All @@ -18,28 +14,23 @@ import { COLUMNS } from "./LiveKitchen/constants";
import { LiveIndicator } from "./LiveKitchen/LiveIndicator";
import { OrderCard, DoneCard, EmptyColumn } from "./LiveKitchen/OrderCards";
import { KitchenTicketsTable } from "./LiveKitchen/KitchenTicketsTable";
import { ChefManagement } from "./LiveKitchen/ChefManagement";

function LiveKitchenView() {
const [viewingOrder, setViewingOrder] = useState(null);
const [orderToCancel, setOrderToCancel] = useState(null);
const [orderToMarkDone, setOrderToMarkDone] = useState(null);
const [orderToRevert, setOrderToRevert] = useState(null);

// ── Kitchen-service state ──
const [ticketConfirm, setTicketConfirm] = useState(null); // { ticketId, status, label }
// ── Ticket confirm state ── { orderId, status, label }
const [ticketConfirm, setTicketConfirm] = useState(null);

const { boards, isFetching, error, refetch } = useRealtimeKitchen();
const { mutate: updateStatus } = useUpdateKitchenStatus();

// ── Kitchen-service hooks ──
const { data: tickets, isLoading: ticketsLoading, error: ticketsError, refetch: refetchTickets, isFetching: ticketsFetching } = useActiveTickets();
const { mutate: mutateTicketStatus } = useUpdateTicketStatus();
const { mutate: mutateChefStatus } = useUpdateChefStatus();
const { mutate: mutateChefStation } = useUpdateChefStation();
const { mutate: mutateChefName } = useUpdateChefDisplayName();

// ── Action handler — routes to confirmation modals for destructive actions ──
// ── Kanban action handler — routes destructive actions to confirmation modals ──
const handleAction = useCallback((orderId, nextStatus) => {
if (nextStatus === "cancelled") { setOrderToCancel(orderId); return; }
if (nextStatus === "done") { setOrderToMarkDone(orderId); return; }
Expand All @@ -50,10 +41,49 @@ function LiveKitchenView() {
const confirmMarkDone = () => { if (orderToMarkDone) { updateStatus({ orderId: orderToMarkDone, nextStatus: "done" }); setOrderToMarkDone(null); } };
const confirmRevert = () => { if (orderToRevert) { updateStatus({ orderId: orderToRevert, nextStatus: "ready" }); setOrderToRevert(null); } };

const handleTicketAction = useCallback((ticketId, status, label) => {
setTicketConfirm({ ticketId, status, label });
// ── Ticket table action handler ──
// All ticket actions go through confirmation, then use the SAME updateStatus
// (which updates the order + syncs the ticket) so both views stay in sync.
const handleTicketAction = useCallback((orderId, status, label) => {
setTicketConfirm({ orderId, status, label });
}, []);

const confirmTicketAction = () => {
if (ticketConfirm) {
updateStatus({ orderId: ticketConfirm.orderId, nextStatus: ticketConfirm.status.toLowerCase() });
setTicketConfirm(null);
}
};

// ── Build a unified tickets list from Kanban board + API tickets ──
// Map board column keys to the status labels used in the table
const BOARD_STATUS_MAP = { queue: "Queue", preparing: "Preparing", ready: "Ready", done: "Done" };

// Synthesize ticket-shaped objects from ALL Kanban board columns
const boardTickets = Object.entries(BOARD_STATUS_MAP).flatMap(([colKey, statusLabel]) =>
(boards[colKey] || []).map(order => {
const numericId = String(order.orderId || order.id).replace('#', '');
return {
id: `board-${colKey}-${numericId}`,
orderId: numericId,
status: statusLabel,
createdAt: order.createdAt || order.time || new Date().toISOString(),
assignedChefId: null,
chefDisplayName: "",
chefStation: "UNASSIGNED",
chefStatus: "ACTIVE",
items: order.items || [],
};
})
);

// Merge: prefer real API tickets when they exist, fill gaps with board-synthesized ones
const apiTicketOrderIds = new Set((tickets || []).map(t => String(t.orderId)));
const mergedTickets = [
...(tickets || []),
...boardTickets.filter(bt => !apiTicketOrderIds.has(bt.orderId)),
];

const doneCount = boards.done?.length ?? 0;

return (
Expand Down Expand Up @@ -136,33 +166,21 @@ function LiveKitchenView() {
)}

{/* ══════════════════════════════════════════════════════
SECTION: Kitchen Service — Active Tickets
SECTION: Kitchen Tickets Table
═══════════════════════════════════════════════════════ */}
<KitchenTicketsTable
tickets={tickets}
tickets={mergedTickets}
isLoading={ticketsLoading}
error={ticketsError}
isFetching={ticketsFetching}
onRetry={refetchTickets}
onRetry={() => { refetchTickets(); refetch(); }}
onAction={handleTicketAction}
/>

{/* ══════════════════════════════════════════════════════
SECTION: Chef Management
═══════════════════════════════════════════════════════ */}
<ChefManagement
tickets={tickets}
isLoading={ticketsLoading}
error={ticketsError}
onUpdateStatus={mutateChefStatus}
onUpdateStation={mutateChefStation}
onUpdateName={mutateChefName}
/>

</div>
</div>

{/* ── Confirm modals ── */}
{/* ── Kanban confirm modals ── */}
<ConfirmModal
isOpen={!!orderToCancel}
onClose={() => setOrderToCancel(null)}
Expand Down Expand Up @@ -193,21 +211,16 @@ function LiveKitchenView() {
<ConfirmModal
isOpen={!!ticketConfirm}
onClose={() => setTicketConfirm(null)}
onConfirm={() => {
if (ticketConfirm) {
mutateTicketStatus({ ticketId: ticketConfirm.ticketId, status: ticketConfirm.status });
setTicketConfirm(null);
}
}}
title={ticketConfirm?.label || "Update Status?"}
message={`Are you sure you want to move ticket #${ticketConfirm?.ticketId} to ${ticketConfirm?.status}?`}
onConfirm={confirmTicketAction}
title={ticketConfirm?.label || "Update Ticket"}
message={`Are you sure you want to change this order to "${ticketConfirm?.status}"?`}
confirmLabel={ticketConfirm?.label || "Confirm"}
confirmClassName={
ticketConfirm?.status?.toUpperCase() === "CANCELLED" || ticketConfirm?.status?.toUpperCase() === "CANCELED"
ticketConfirm?.status?.toLowerCase() === "cancelled" || ticketConfirm?.status?.toLowerCase() === "canceled"
? "bg-rose-600 hover:bg-rose-700 shadow-lg shadow-rose-500/30"
: ticketConfirm?.status?.toUpperCase() === "READY" || ticketConfirm?.status?.toUpperCase() === "PREPARING"
? "bg-[#F97316] hover:bg-orange-600 shadow-lg shadow-orange-500/30"
: "bg-[#16A34A] hover:bg-green-700 shadow-lg shadow-green-500/30"
: ticketConfirm?.status?.toLowerCase() === "ready" || ticketConfirm?.status?.toLowerCase() === "preparing"
? "bg-[#F97316] hover:bg-orange-600 shadow-lg shadow-orange-500/30"
: "bg-[#16A34A] hover:bg-green-700 shadow-lg shadow-green-500/30"
}
/>

Expand Down
Loading