"use client"; import { useCallback, useEffect, useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; import * as signalR from "@microsoft/signalr"; import { Plus, QrCode, Pencil, Video, Trash2, Copy, ExternalLink } from "lucide-react"; import { DemoDataBanner } from "@/components/demo/demo-data-banner"; import { notify } from "@/lib/notify"; import { useApiError } from "@/lib/use-api-error"; import { MediaPairUpload } from "@/components/media/media-pair-upload"; import { PageHeader } from "@/components/layout/page-header"; import { apiGet, apiGetBlob, apiPatch, apiPost, ApiClientError, openBlobInNewTab, resolveMediaUrl, } from "@/lib/api/client"; import { createBranchTable, deleteBranchTable, fetchCafeTableBoard, patchBranchTable, setTableCleaning, } from "@/lib/api/branch-tables"; import { useBranchStore } from "@/lib/stores/branch.store"; import type { TableBoardItem } from "@/lib/api/types"; import { useAuthStore } from "@/lib/stores/auth.store"; import { formatCurrency, formatNumber } from "@/lib/format"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { LabeledField } from "@/components/ui/labeled-field"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { useConfirm } from "@/components/providers/confirm-provider"; import { Alert } from "@/components/ui/alert"; const statusStyles: Record = { Free: "bg-[#E1F5EE] text-[#0F6E56] border-[#0F6E56]/30", Busy: "bg-blue-50 text-[#0C447C] border-blue-200", Reserved: "bg-amber-50 text-[#BA7517] border-amber-200", Cleaning: "bg-slate-100 text-slate-600 border-slate-300", }; export function TablesScreen() { const t = useTranslations("tables"); const tCommon = useTranslations("common"); const cafeId = useAuthStore((s) => s.user?.cafeId); const role = useAuthStore((s) => s.user?.role); const branchId = useBranchStore((s) => s.branchId); const queryClient = useQueryClient(); const confirmDialog = useConfirm(); const apiError = useApiError(); const [actionMessage, setActionMessage] = useState(null); const [showForm, setShowForm] = useState(false); const [number, setNumber] = useState(""); const [capacity, setCapacity] = useState("4"); const [floor, setFloor] = useState(""); const [editingId, setEditingId] = useState(null); const [editNumber, setEditNumber] = useState(""); const [editCapacity, setEditCapacity] = useState(""); const [editFloor, setEditFloor] = useState(""); const [editImageUrl, setEditImageUrl] = useState(""); const [editVideoUrl, setEditVideoUrl] = useState(""); const apiBase = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:5080"; const canManage = role === "Owner" || role === "Manager"; const { data: tables = [], isLoading } = useQuery({ queryKey: ["tables-board", cafeId, branchId, "manage"], queryFn: () => fetchCafeTableBoard(cafeId!, branchId), enabled: !!cafeId, }); const refresh = useCallback(() => { queryClient.invalidateQueries({ queryKey: ["tables-board", cafeId] }); }, [queryClient, cafeId]); useEffect(() => { if (!cafeId) return; const token = typeof window !== "undefined" ? localStorage.getItem("meezi_access_token") : null; const connection = new signalR.HubConnectionBuilder() .withUrl(`${apiBase}/hubs/kds`, { accessTokenFactory: () => token ?? "" }) .withAutomaticReconnect() .build(); connection .start() .then(() => connection.invoke("JoinCafe", cafeId)) .catch(() => undefined); connection.on("TableStatusChanged", () => refresh()); connection.on("OrderCreated", () => refresh()); connection.on("OrderStatusChanged", () => refresh()); return () => { void connection.stop(); }; }, [cafeId, apiBase, refresh]); const createTable = useMutation({ mutationFn: async () => { const cap = parseInt(capacity, 10); if (branchId) { await createBranchTable(cafeId!, branchId, { number, capacity: cap, floor: floor || null, }); return; } await apiPost(`/api/cafes/${cafeId}/tables`, { number, capacity: cap, floor: floor || null, }); }, onSuccess: () => { setShowForm(false); setNumber(""); setFloor(""); setActionMessage(null); refresh(); }, onError: (err) => { const msg = apiError(err, t("createError")); setActionMessage(msg); notify.error(msg); }, }); const setCleaning = useMutation({ mutationFn: ({ table, isCleaning, }: { table: TableBoardItem; isCleaning: boolean; }) => setTableCleaning(cafeId!, table.id, isCleaning, branchId ?? table.branchId), onSuccess: () => { setActionMessage(null); refresh(); }, onError: (err) => { setActionMessage(apiError(err, t("cleaningError"))); }, }); const deleteTable = useMutation({ mutationFn: (table: TableBoardItem) => deleteBranchTable(cafeId!, table.branchId, table.id), onSuccess: () => { setActionMessage(null); refresh(); }, onError: (err) => { if (err instanceof ApiClientError && err.code === "TABLE_HAS_OPEN_ORDER") { setActionMessage(t("tableHasOpenOrder")); return; } setActionMessage(apiError(err, t("deleteError"))); }, }); const patchTable = useMutation({ mutationFn: ({ table, body, }: { table: TableBoardItem; body: { number?: string; capacity?: number; floor?: string | null; imageUrl?: string; videoUrl?: string; isActive?: boolean; }; }) => { if (branchId) { return patchBranchTable(cafeId!, branchId, table.id, body); } return apiPatch(`/api/cafes/${cafeId}/tables/${table.id}`, body); }, onSuccess: () => { setEditingId(null); setActionMessage(null); refresh(); }, onError: (err) => { const msg = apiError(err, t("createError")); setActionMessage(msg); notify.error(msg); }, }); const startEdit = (table: TableBoardItem) => { setEditingId(table.id); setEditNumber(table.number); setEditCapacity(String(table.capacity)); setEditFloor(table.floor ?? ""); setEditImageUrl(table.imageUrl ?? ""); setEditVideoUrl(table.videoUrl ?? ""); }; const mediaField = (url: string) => (url.trim() === "" ? "" : url); const openQr = async (tableId: string) => { try { const blob = await apiGetBlob(`/api/cafes/${cafeId}/tables/${tableId}/qr`); openBlobInNewTab(blob); } catch { /* ignore */ } }; const copyQrUrl = async (url: string) => { try { await navigator.clipboard.writeText(url); notify.success(t("qrUrlCopied")); } catch { notify.error(t("qrUrlCopyFailed")); } }; if (!cafeId) return null; return (
setShowForm((v) => !v)} > {t("addTable")} ) : undefined } /> {showForm && ( setNumber(e.target.value)} /> setCapacity(e.target.value)} dir="ltr" className="text-end" /> setFloor(e.target.value)} />
)} {editingId && (

{t("editTable")}

setEditNumber(e.target.value)} /> setEditCapacity(e.target.value)} dir="ltr" className="text-end" /> setEditFloor(e.target.value)} /> setEditImageUrl(url ?? "")} onVideoChange={(url) => setEditVideoUrl(url ?? "")} />
)} {isLoading ? (

{tCommon("loading")}

) : tables.length === 0 ? (

{branchId ? t("emptyBranch") : t("empty")}

) : ( <> {actionMessage ? ( setActionMessage(null)}> {actionMessage} ) : null}
{tables.map((table) => ( {(table.imageUrl || table.videoUrl) && (
{resolveMediaUrl(table.imageUrl) ? ( // eslint-disable-next-line @next/next/no-img-element ) : (
)} {table.videoUrl ? ( ) : null}
)}

{t("tableLabel", { number: table.number })}

{t("meta", { capacity: formatNumber(table.capacity), floor: table.floor ?? "—", })}

{!table.isActive ? ( {t("inactive")} ) : null}
{t(`status.${table.status}`)}
{table.currentOrder && table.status === "Busy" && (

{table.currentOrder.guestLabel ?? t("activeOrder")} —{" "} {formatCurrency(table.currentOrder.total)}

)} {table.currentOrder && table.status === "Reserved" && (

{table.currentOrder.guestLabel} — {t("reserved")}

)} {table.qrCodeUrl ? (

{t("qrMenuUrl")}

{table.qrCodeUrl}
) : null}
{canManage ? ( ) : null} {canManage ? ( ) : null} {canManage && table.isActive ? ( ) : canManage && !table.isActive ? ( ) : null}

{t("reprintHint")}

))}
)}
); }