| @@ -7,12 +7,27 @@ import FormatListNumbered from "@mui/icons-material/FormatListNumbered"; | |||||
| import ShowChart from "@mui/icons-material/ShowChart"; | import ShowChart from "@mui/icons-material/ShowChart"; | ||||
| import Download from "@mui/icons-material/Download"; | import Download from "@mui/icons-material/Download"; | ||||
| import Hub from "@mui/icons-material/Hub"; | import Hub from "@mui/icons-material/Hub"; | ||||
| import Settings from "@mui/icons-material/Settings"; | |||||
| import Clear from "@mui/icons-material/Clear"; | |||||
| import { CircularProgress } from "@mui/material"; | import { CircularProgress } from "@mui/material"; | ||||
| import PageTitleBar from "@/components/PageTitleBar"; | import PageTitleBar from "@/components/PageTitleBar"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | import { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | ||||
| type ItemDailyOutRow = { | |||||
| itemCode: string; | |||||
| itemName: string; | |||||
| unit?: string; | |||||
| onHandQty?: number | null; | |||||
| fakeOnHandQty?: number | null; | |||||
| avgQtyLastMonth?: number; | |||||
| dailyQty?: number | null; | |||||
| isCoffee?: number; | |||||
| isTea?: number; | |||||
| isLemon?: number; | |||||
| }; | |||||
| export default function ProductionSchedulePage() { | export default function ProductionSchedulePage() { | ||||
| const [searchDate, setSearchDate] = useState(dayjs().format("YYYY-MM-DD")); | const [searchDate, setSearchDate] = useState(dayjs().format("YYYY-MM-DD")); | ||||
| const [schedules, setSchedules] = useState<any[]>([]); | const [schedules, setSchedules] = useState<any[]>([]); | ||||
| @@ -33,6 +48,15 @@ export default function ProductionSchedulePage() { | |||||
| dayjs().format("YYYY-MM-DD") | dayjs().format("YYYY-MM-DD") | ||||
| ); | ); | ||||
| const [isDailyOutPanelOpen, setIsDailyOutPanelOpen] = useState(false); | |||||
| const [itemDailyOutList, setItemDailyOutList] = useState<ItemDailyOutRow[]>([]); | |||||
| const [itemDailyOutLoading, setItemDailyOutLoading] = useState(false); | |||||
| const [dailyOutSavingCode, setDailyOutSavingCode] = useState<string | null>(null); | |||||
| const [dailyOutClearingCode, setDailyOutClearingCode] = useState<string | null>(null); | |||||
| const [coffeeOrTeaUpdating, setCoffeeOrTeaUpdating] = useState<string | null>(null); | |||||
| const [fakeOnHandSavingCode, setFakeOnHandSavingCode] = useState<string | null>(null); | |||||
| const [fakeOnHandClearingCode, setFakeOnHandClearingCode] = useState<string | null>(null); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| handleSearch(); | handleSearch(); | ||||
| }, []); | }, []); | ||||
| @@ -182,12 +206,228 @@ export default function ProductionSchedulePage() { | |||||
| } | } | ||||
| }; | }; | ||||
| const fromDateDefault = dayjs().subtract(29, "day").format("YYYY-MM-DD"); | |||||
| const toDateDefault = dayjs().format("YYYY-MM-DD"); | |||||
| const fetchItemDailyOut = async () => { | |||||
| setItemDailyOutLoading(true); | |||||
| try { | |||||
| const params = new URLSearchParams({ | |||||
| fromDate: fromDateDefault, | |||||
| toDate: toDateDefault, | |||||
| }); | |||||
| const response = await clientAuthFetch( | |||||
| `${NEXT_PUBLIC_API_URL}/ps/itemDailyOut.json?${params.toString()}`, | |||||
| { method: "GET" } | |||||
| ); | |||||
| if (response.status === 401 || response.status === 403) return; | |||||
| const data = await response.json(); | |||||
| const rows: ItemDailyOutRow[] = (Array.isArray(data) ? data : []).map( | |||||
| (r: any) => ({ | |||||
| itemCode: r.itemCode ?? "", | |||||
| itemName: r.itemName ?? "", | |||||
| unit: r.unit != null ? String(r.unit) : "", | |||||
| onHandQty: r.onHandQty != null ? Number(r.onHandQty) : null, | |||||
| fakeOnHandQty: | |||||
| r.fakeOnHandQty != null && r.fakeOnHandQty !== "" | |||||
| ? Number(r.fakeOnHandQty) | |||||
| : null, | |||||
| avgQtyLastMonth: | |||||
| r.avgQtyLastMonth != null ? Number(r.avgQtyLastMonth) : undefined, | |||||
| dailyQty: | |||||
| r.dailyQty != null && r.dailyQty !== "" | |||||
| ? Number(r.dailyQty) | |||||
| : null, | |||||
| isCoffee: r.isCoffee != null ? Number(r.isCoffee) : 0, | |||||
| isTea: r.isTea != null ? Number(r.isTea) : 0, | |||||
| isLemon: r.isLemon != null ? Number(r.isLemon) : 0, | |||||
| }) | |||||
| ); | |||||
| setItemDailyOutList(rows); | |||||
| } catch (e) { | |||||
| console.error("itemDailyOut Error:", e); | |||||
| setItemDailyOutList([]); | |||||
| } finally { | |||||
| setItemDailyOutLoading(false); | |||||
| } | |||||
| }; | |||||
| const openSettingsPanel = () => { | |||||
| setIsDailyOutPanelOpen(true); | |||||
| fetchItemDailyOut(); | |||||
| }; | |||||
| const handleSaveDailyQty = async (itemCode: string, dailyQty: number) => { | |||||
| setDailyOutSavingCode(itemCode); | |||||
| try { | |||||
| const response = await clientAuthFetch( | |||||
| `${NEXT_PUBLIC_API_URL}/ps/setDailyQtyOut`, | |||||
| { | |||||
| method: "POST", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| body: JSON.stringify({ itemCode, dailyQty }), | |||||
| } | |||||
| ); | |||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (response.ok) { | |||||
| setItemDailyOutList((prev) => | |||||
| prev.map((r) => | |||||
| r.itemCode === itemCode ? { ...r, dailyQty } : r | |||||
| ) | |||||
| ); | |||||
| } else { | |||||
| alert("儲存失敗"); | |||||
| } | |||||
| } catch (e) { | |||||
| console.error("setDailyQtyOut Error:", e); | |||||
| alert("儲存失敗"); | |||||
| } finally { | |||||
| setDailyOutSavingCode(null); | |||||
| } | |||||
| }; | |||||
| const handleClearDailyQty = async (itemCode: string) => { | |||||
| if (!confirm(`確定要清除${itemCode}的設定排期每天出貨量嗎?`)) return; | |||||
| setDailyOutClearingCode(itemCode); | |||||
| try { | |||||
| const response = await clientAuthFetch( | |||||
| `${NEXT_PUBLIC_API_URL}/ps/clearDailyQtyOut`, | |||||
| { | |||||
| method: "POST", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| body: JSON.stringify({ itemCode }), | |||||
| } | |||||
| ); | |||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (response.ok) { | |||||
| setItemDailyOutList((prev) => | |||||
| prev.map((r) => | |||||
| r.itemCode === itemCode ? { ...r, dailyQty: null } : r | |||||
| ) | |||||
| ); | |||||
| } else { | |||||
| alert("清除失敗"); | |||||
| } | |||||
| } catch (e) { | |||||
| console.error("clearDailyQtyOut Error:", e); | |||||
| alert("清除失敗"); | |||||
| } finally { | |||||
| setDailyOutClearingCode(null); | |||||
| } | |||||
| }; | |||||
| const handleSetCoffeeOrTea = async ( | |||||
| itemCode: string, | |||||
| systemType: "coffee" | "tea" | "lemon", | |||||
| enabled: boolean | |||||
| ) => { | |||||
| const key = `${itemCode}-${systemType}`; | |||||
| setCoffeeOrTeaUpdating(key); | |||||
| try { | |||||
| const response = await clientAuthFetch( | |||||
| `${NEXT_PUBLIC_API_URL}/ps/setCoffeeOrTea`, | |||||
| { | |||||
| method: "POST", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| body: JSON.stringify({ itemCode, systemType, enabled }), | |||||
| } | |||||
| ); | |||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (response.ok) { | |||||
| setItemDailyOutList((prev) => | |||||
| prev.map((r) => { | |||||
| if (r.itemCode !== itemCode) return r; | |||||
| const next = { ...r }; | |||||
| if (systemType === "coffee") next.isCoffee = enabled ? 1 : 0; | |||||
| if (systemType === "tea") next.isTea = enabled ? 1 : 0; | |||||
| if (systemType === "lemon") next.isLemon = enabled ? 1 : 0; | |||||
| return next; | |||||
| }) | |||||
| ); | |||||
| } else { | |||||
| alert("設定失敗"); | |||||
| } | |||||
| } catch (e) { | |||||
| console.error("setCoffeeOrTea Error:", e); | |||||
| alert("設定失敗"); | |||||
| } finally { | |||||
| setCoffeeOrTeaUpdating(null); | |||||
| } | |||||
| }; | |||||
| const handleSetFakeOnHand = async (itemCode: string, onHandQty: number) => { | |||||
| setFakeOnHandSavingCode(itemCode); | |||||
| try { | |||||
| const response = await clientAuthFetch( | |||||
| `${NEXT_PUBLIC_API_URL}/ps/setFakeOnHand`, | |||||
| { | |||||
| method: "POST", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| body: JSON.stringify({ itemCode, onHandQty }), | |||||
| } | |||||
| ); | |||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (response.ok) { | |||||
| setItemDailyOutList((prev) => | |||||
| prev.map((r) => | |||||
| r.itemCode === itemCode ? { ...r, fakeOnHandQty: onHandQty } : r | |||||
| ) | |||||
| ); | |||||
| } else { | |||||
| alert("設定失敗"); | |||||
| } | |||||
| } catch (e) { | |||||
| console.error("setFakeOnHand Error:", e); | |||||
| alert("設定失敗"); | |||||
| } finally { | |||||
| setFakeOnHandSavingCode(null); | |||||
| } | |||||
| }; | |||||
| const handleClearFakeOnHand = async (itemCode: string) => { | |||||
| if (!confirm("確定要清除此物料的設定排期庫存嗎?")) return; | |||||
| setFakeOnHandClearingCode(itemCode); | |||||
| try { | |||||
| const response = await clientAuthFetch( | |||||
| `${NEXT_PUBLIC_API_URL}/ps/setFakeOnHand`, | |||||
| { | |||||
| method: "POST", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| body: JSON.stringify({ itemCode, onHandQty: null }), | |||||
| } | |||||
| ); | |||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (response.ok) { | |||||
| setItemDailyOutList((prev) => | |||||
| prev.map((r) => | |||||
| r.itemCode === itemCode ? { ...r, fakeOnHandQty: null } : r | |||||
| ) | |||||
| ); | |||||
| } else { | |||||
| alert("清除失敗"); | |||||
| } | |||||
| } catch (e) { | |||||
| console.error("clearFakeOnHand Error:", e); | |||||
| alert("清除失敗"); | |||||
| } finally { | |||||
| setFakeOnHandClearingCode(null); | |||||
| } | |||||
| }; | |||||
| return ( | return ( | ||||
| <div className="space-y-4"> | <div className="space-y-4"> | ||||
| <PageTitleBar | <PageTitleBar | ||||
| title="排程" | title="排程" | ||||
| actions={ | actions={ | ||||
| <> | <> | ||||
| <button | |||||
| type="button" | |||||
| onClick={openSettingsPanel} | |||||
| className="inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 shadow-sm transition hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700" | |||||
| > | |||||
| <Settings sx={{ fontSize: 16 }} /> | |||||
| 排期設定 | |||||
| </button> | |||||
| <button | <button | ||||
| type="button" | type="button" | ||||
| onClick={() => setIsExportDialogOpen(true)} | onClick={() => setIsExportDialogOpen(true)} | ||||
| @@ -557,6 +797,237 @@ export default function ProductionSchedulePage() { | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| {/* 排期設定 Dialog */} | |||||
| {isDailyOutPanelOpen && ( | |||||
| <div | |||||
| className="fixed inset-0 z-[1300] flex items-center justify-center p-4" | |||||
| role="dialog" | |||||
| aria-modal="true" | |||||
| aria-labelledby="settings-panel-title" | |||||
| > | |||||
| <div | |||||
| className="absolute inset-0 bg-black/50" | |||||
| onClick={() => setIsDailyOutPanelOpen(false)} | |||||
| /> | |||||
| <div className="relative z-10 flex max-h-[90vh] w-full max-w-6xl flex-col overflow-hidden rounded-lg border border-slate-200 bg-white shadow-xl dark:border-slate-700 dark:bg-slate-800"> | |||||
| <div className="flex items-center justify-between border-b border-slate-200 bg-slate-100 px-4 py-3 dark:border-slate-700 dark:bg-slate-700/50"> | |||||
| <h2 id="settings-panel-title" className="text-lg font-semibold text-slate-900 dark:text-white"> | |||||
| 排期設定 | |||||
| </h2> | |||||
| <button | |||||
| type="button" | |||||
| onClick={() => setIsDailyOutPanelOpen(false)} | |||||
| className="rounded p-1 text-slate-500 hover:bg-slate-200 hover:text-slate-700 dark:hover:bg-slate-600 dark:hover:text-slate-200" | |||||
| > | |||||
| 關閉 | |||||
| </button> | |||||
| </div> | |||||
| <p className="px-4 py-2 text-sm text-slate-600 dark:text-slate-400"> | |||||
| 預設為過去 30 天(含今日)。設定排期每天出貨量、設定排期庫存可編輯並按列儲存。 | |||||
| </p> | |||||
| <div className="max-h-[60vh] overflow-auto"> | |||||
| {itemDailyOutLoading ? ( | |||||
| <div className="flex items-center justify-center py-12"> | |||||
| <CircularProgress /> | |||||
| </div> | |||||
| ) : ( | |||||
| <table className="w-full min-w-[900px] text-left text-sm"> | |||||
| <thead className="sticky top-0 bg-slate-50 dark:bg-slate-700"> | |||||
| <tr> | |||||
| <th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">物料編號</th> | |||||
| <th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">物料名稱</th> | |||||
| <th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">單位</th> | |||||
| <th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">庫存</th> | |||||
| <th className="px-4 py-2 text-left font-bold text-slate-700 dark:text-slate-200">設定排期庫存</th> | |||||
| <th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">過去平均出貨量</th> | |||||
| <th className="px-4 py-2 text-left font-bold text-slate-700 dark:text-slate-200">設定排期每天出貨量</th> | |||||
| <th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">咖啡</th> | |||||
| <th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">茶</th> | |||||
| <th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">檸檬</th> | |||||
| </tr> | |||||
| </thead> | |||||
| <tbody> | |||||
| {itemDailyOutList.map((row, idx) => ( | |||||
| <DailyOutRow | |||||
| key={`${row.itemCode}-${idx}`} | |||||
| row={row} | |||||
| onSave={handleSaveDailyQty} | |||||
| onClear={handleClearDailyQty} | |||||
| onSetCoffeeOrTea={handleSetCoffeeOrTea} | |||||
| onSetFakeOnHand={handleSetFakeOnHand} | |||||
| onClearFakeOnHand={handleClearFakeOnHand} | |||||
| saving={dailyOutSavingCode === row.itemCode} | |||||
| clearing={dailyOutClearingCode === row.itemCode} | |||||
| coffeeOrTeaUpdating={coffeeOrTeaUpdating} | |||||
| fakeOnHandSaving={fakeOnHandSavingCode === row.itemCode} | |||||
| fakeOnHandClearing={fakeOnHandClearingCode === row.itemCode} | |||||
| formatNum={formatNum} | |||||
| /> | |||||
| ))} | |||||
| </tbody> | |||||
| </table> | |||||
| )} | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| )} | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| function DailyOutRow({ | |||||
| row, | |||||
| onSave, | |||||
| onClear, | |||||
| onSetCoffeeOrTea, | |||||
| onSetFakeOnHand, | |||||
| onClearFakeOnHand, | |||||
| saving, | |||||
| clearing, | |||||
| coffeeOrTeaUpdating, | |||||
| fakeOnHandSaving, | |||||
| fakeOnHandClearing, | |||||
| formatNum, | |||||
| }: { | |||||
| row: ItemDailyOutRow; | |||||
| onSave: (itemCode: string, dailyQty: number) => void; | |||||
| onClear: (itemCode: string) => void; | |||||
| onSetCoffeeOrTea: (itemCode: string, systemType: "coffee" | "tea" | "lemon", enabled: boolean) => void; | |||||
| onSetFakeOnHand: (itemCode: string, onHandQty: number) => void; | |||||
| onClearFakeOnHand: (itemCode: string) => void; | |||||
| saving: boolean; | |||||
| clearing: boolean; | |||||
| coffeeOrTeaUpdating: string | null; | |||||
| fakeOnHandSaving: boolean; | |||||
| fakeOnHandClearing: boolean; | |||||
| formatNum: (n: any) => string; | |||||
| }) { | |||||
| const [editQty, setEditQty] = useState<string>( | |||||
| row.dailyQty != null ? String(row.dailyQty) : "" | |||||
| ); | |||||
| const [editFakeOnHand, setEditFakeOnHand] = useState<string>( | |||||
| row.fakeOnHandQty != null ? String(row.fakeOnHandQty) : "" | |||||
| ); | |||||
| useEffect(() => { | |||||
| setEditQty(row.dailyQty != null ? String(row.dailyQty) : ""); | |||||
| }, [row.dailyQty]); | |||||
| useEffect(() => { | |||||
| setEditFakeOnHand(row.fakeOnHandQty != null ? String(row.fakeOnHandQty) : ""); | |||||
| }, [row.fakeOnHandQty]); | |||||
| const numVal = parseFloat(editQty); | |||||
| const isValid = !Number.isNaN(numVal) && numVal >= 0; | |||||
| const hasSetQty = row.dailyQty != null; | |||||
| const fakeOnHandNum = parseFloat(editFakeOnHand); | |||||
| const isValidFakeOnHand = !Number.isNaN(fakeOnHandNum) && fakeOnHandNum >= 0; | |||||
| const hasSetFakeOnHand = row.fakeOnHandQty != null; | |||||
| const isCoffee = (row.isCoffee ?? 0) > 0; | |||||
| const isTea = (row.isTea ?? 0) > 0; | |||||
| const isLemon = (row.isLemon ?? 0) > 0; | |||||
| const updatingCoffee = coffeeOrTeaUpdating === `${row.itemCode}-coffee`; | |||||
| const updatingTea = coffeeOrTeaUpdating === `${row.itemCode}-tea`; | |||||
| const updatingLemon = coffeeOrTeaUpdating === `${row.itemCode}-lemon`; | |||||
| return ( | |||||
| <tr className="border-t border-slate-200 text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-700/30"> | |||||
| <td className="px-4 py-2 font-medium">{row.itemCode}</td> | |||||
| <td className="px-4 py-2">{row.itemName}</td> | |||||
| <td className="px-4 py-2">{row.unit ?? ""}</td> | |||||
| <td className="px-4 py-2 text-right">{formatNum(row.onHandQty)}</td> | |||||
| <td className="px-4 py-2 text-left"> | |||||
| <div className="flex items-center justify-start gap-0.5"> | |||||
| <input | |||||
| type="number" | |||||
| min={0} | |||||
| step={1} | |||||
| value={editFakeOnHand} | |||||
| onChange={(e) => setEditFakeOnHand(e.target.value)} | |||||
| onBlur={() => { | |||||
| if (isValidFakeOnHand) onSetFakeOnHand(row.itemCode, fakeOnHandNum); | |||||
| }} | |||||
| onKeyDown={(e) => { | |||||
| if (e.key === "Enter" && isValidFakeOnHand) onSetFakeOnHand(row.itemCode, fakeOnHandNum); | |||||
| }} | |||||
| className="w-24 rounded border border-slate-300 bg-white px-2 py-1 text-left text-slate-900 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100" | |||||
| /> | |||||
| {hasSetFakeOnHand && ( | |||||
| <button | |||||
| type="button" | |||||
| disabled={fakeOnHandClearing} | |||||
| onClick={() => onClearFakeOnHand(row.itemCode)} | |||||
| title="清除設定排期庫存" | |||||
| className="rounded p-0.5 text-slate-400 hover:bg-slate-200 hover:text-slate-600 disabled:opacity-50 dark:hover:bg-slate-600 dark:hover:text-slate-200" | |||||
| > | |||||
| {fakeOnHandClearing ? <CircularProgress size={14} sx={{ display: "block" }} /> : <Clear sx={{ fontSize: 18 }} />} | |||||
| </button> | |||||
| )} | |||||
| </div> | |||||
| </td> | |||||
| <td className="px-4 py-2 text-right">{formatNum(row.avgQtyLastMonth)}</td> | |||||
| <td className="px-4 py-2 text-left"> | |||||
| <div className="flex items-center justify-start gap-0.5"> | |||||
| <input | |||||
| type="number" | |||||
| min={0} | |||||
| step={1} | |||||
| value={editQty} | |||||
| onChange={(e) => setEditQty(e.target.value)} | |||||
| onBlur={() => { | |||||
| if (isValid) onSave(row.itemCode, numVal); | |||||
| }} | |||||
| onKeyDown={(e) => { | |||||
| if (e.key === "Enter" && isValid) onSave(row.itemCode, numVal); | |||||
| }} | |||||
| className="w-24 rounded border border-slate-300 bg-white px-2 py-1 text-left text-slate-900 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100" | |||||
| /> | |||||
| {hasSetQty && ( | |||||
| <button | |||||
| type="button" | |||||
| disabled={clearing} | |||||
| onClick={() => onClear(row.itemCode)} | |||||
| title="清除設定排期每天出貨量" | |||||
| className="rounded p-0.5 text-slate-400 hover:bg-slate-200 hover:text-slate-600 disabled:opacity-50 dark:hover:bg-slate-600 dark:hover:text-slate-200" | |||||
| > | |||||
| {clearing ? <CircularProgress size={14} sx={{ display: "block" }} /> : <Clear sx={{ fontSize: 18 }} />} | |||||
| </button> | |||||
| )} | |||||
| </div> | |||||
| </td> | |||||
| <td className="px-4 py-2 text-center"> | |||||
| <label className="inline-flex cursor-pointer items-center gap-1"> | |||||
| <input | |||||
| type="checkbox" | |||||
| checked={isCoffee} | |||||
| disabled={updatingCoffee} | |||||
| onChange={(e) => onSetCoffeeOrTea(row.itemCode, "coffee", e.target.checked)} | |||||
| className="h-4 w-4 rounded border-slate-300 text-blue-500 focus:ring-blue-500" | |||||
| /> | |||||
| {updatingCoffee && <CircularProgress size={14} sx={{ display: "block" }} />} | |||||
| </label> | |||||
| </td> | |||||
| <td className="px-4 py-2 text-center"> | |||||
| <label className="inline-flex cursor-pointer items-center gap-1"> | |||||
| <input | |||||
| type="checkbox" | |||||
| checked={isTea} | |||||
| disabled={updatingTea} | |||||
| onChange={(e) => onSetCoffeeOrTea(row.itemCode, "tea", e.target.checked)} | |||||
| className="h-4 w-4 rounded border-slate-300 text-blue-500 focus:ring-blue-500" | |||||
| /> | |||||
| {updatingTea && <CircularProgress size={14} sx={{ display: "block" }} />} | |||||
| </label> | |||||
| </td> | |||||
| <td className="px-4 py-2 text-center"> | |||||
| <label className="inline-flex cursor-pointer items-center gap-1"> | |||||
| <input | |||||
| type="checkbox" | |||||
| checked={isLemon} | |||||
| disabled={updatingLemon} | |||||
| onChange={(e) => onSetCoffeeOrTea(row.itemCode, "lemon", e.target.checked)} | |||||
| className="h-4 w-4 rounded border-slate-300 text-blue-500 focus:ring-blue-500" | |||||
| /> | |||||
| {updatingLemon && <CircularProgress size={14} sx={{ display: "block" }} />} | |||||
| </label> | |||||
| </td> | |||||
| </tr> | |||||
| ); | |||||
| } | |||||