From 7187d1ddbeb57b057b8490387e5f805ce3213260 Mon Sep 17 00:00:00 2001 From: khaira777 <777gurkirat@gmail.com> Date: Sat, 31 Oct 2026 16:30:55 -0400 Subject: [PATCH] fix(server-app): apply configured idle locking --- frontend/e2e/server-app-ordering.spec.ts | 23 ++ frontend/src/app/server-standalone/page.tsx | 282 ++++++++++++++++++-- frontend/src/lib/i18n/messages/ar.json | 1 + frontend/src/lib/i18n/messages/bn.json | 1 + frontend/src/lib/i18n/messages/de.json | 1 + frontend/src/lib/i18n/messages/en.json | 1 + frontend/src/lib/i18n/messages/es.json | 1 + frontend/src/lib/i18n/messages/fa.json | 1 + frontend/src/lib/i18n/messages/fil.json | 1 + frontend/src/lib/i18n/messages/fr.json | 1 + frontend/src/lib/i18n/messages/hi.json | 1 + frontend/src/lib/i18n/messages/id.json | 1 + frontend/src/lib/i18n/messages/it.json | 1 + frontend/src/lib/i18n/messages/ja.json | 1 + frontend/src/lib/i18n/messages/ko.json | 1 + frontend/src/lib/i18n/messages/ne.json | 1 + frontend/src/lib/i18n/messages/nl.json | 1 + frontend/src/lib/i18n/messages/pt.json | 1 + frontend/src/lib/i18n/messages/ru.json | 1 + frontend/src/lib/i18n/messages/sq.json | 1 + frontend/src/lib/i18n/messages/th.json | 1 + frontend/src/lib/i18n/messages/tr.json | 1 + frontend/src/lib/i18n/messages/ur.json | 1 + frontend/src/lib/i18n/messages/vi.json | 1 + frontend/src/lib/i18n/messages/zh-tw.json | 1 + frontend/src/lib/i18n/messages/zh.json | 1 + main/server-app.ts | 6 + tests/server-app-server-role.test.ts | 13 + 28 files changed, 326 insertions(+), 22 deletions(-) diff --git a/frontend/e2e/server-app-ordering.spec.ts b/frontend/e2e/server-app-ordering.spec.ts index e2e5cd136..f4bf11b9b 100644 --- a/frontend/e2e/server-app-ordering.spec.ts +++ b/frontend/e2e/server-app-ordering.spec.ts @@ -732,3 +732,26 @@ test.describe('Server App tableside ordering', () => { await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible(); }); }); + +test('Server App locks locally on configured inactivity even if logout fails', async ({ page }) => { + await page.clock.install(); + await page.route('**/api/settings/auto_lock_minutes', (route) => + route.fulfill({ json: { setting: { value: '1' } } }), + ); + let logoutAttempts = 0; + await page.route('**/api/auth/logout', (route) => { + logoutAttempts += 1; + return route.fulfill({ status: 503, json: { error: 'offline' } }); + }); + const serverToken = getE2eToken('e2e-server', 'server@flo.local', 'server'); + await openServerApp(page, serverToken); + await expect(page.getByTestId('pos-product-grid')).toBeVisible(); + await page.clock.fastForward('00:59'); + await page.keyboard.press('Shift'); + await page.clock.fastForward('00:59'); + await expect(page.getByTestId('pos-product-grid')).toBeVisible(); + await page.clock.fastForward('00:02'); + await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible(); + expect(await page.evaluate((key) => localStorage.getItem(key), TOKEN_KEY)).toBeNull(); + await expect.poll(() => logoutAttempts).toBe(1); +}); diff --git a/frontend/src/app/server-standalone/page.tsx b/frontend/src/app/server-standalone/page.tsx index e5019c228..909aa2af8 100644 --- a/frontend/src/app/server-standalone/page.tsx +++ b/frontend/src/app/server-standalone/page.tsx @@ -96,7 +96,7 @@ function createApi(): AxiosInstance { const api = axios.create({ baseURL: window.location.origin, timeout: 10000 }); api.interceptors.request.use((config) => { const token = localStorage.getItem(TOKEN_KEY); - if (token) config.headers.Authorization = `Bearer ${token}`; + if (token && !config.headers.Authorization) config.headers.Authorization = `Bearer ${token}`; return config; }); api.interceptors.response.use( @@ -247,6 +247,33 @@ export default function ServerStandalonePage() { const orderLookupFailedRef = useRef(false); const orderLoadSeqRef = useRef(0); const authSessionSeqRef = useRef(0); + const activeUserIdRef = useRef(null); + const [autoLockMinutes, setAutoLockMinutes] = useState(0); + const lastActivityRef = useRef(0); + const liveDraftRef = useRef({ + tableId: '', + draft: [] as DraftLine[], + customerName: '', + customerPhone: '', + orderNotes: '', + }); + const suspendedDraftRef = useRef< + | (typeof liveDraftRef.current & { + ownerId: string; + attempt: typeof sendAttemptRef.current; + }) + | null + >(null); + + useEffect(() => { + liveDraftRef.current = { + tableId: selectedTableId, + draft, + customerName, + customerPhone, + orderNotes, + }; + }, [selectedTableId, draft, customerName, customerPhone, orderNotes]); const loadUser = useCallback(async () => { if (!api || sendInFlightRef.current) return; @@ -280,11 +307,14 @@ export default function ServerStandalonePage() { async function loadAll(options?: { allowWhileSending?: boolean }) { if (!api) return; if (sendInFlightRef.current && !options?.allowWhileSending) return; + const session = authSessionSeqRef.current; + const token = localStorage.getItem(TOKEN_KEY); const [categoriesRes, productsRes, tablesRes] = await Promise.all([ api.get('/api/categories', { params: { active: 'true' } }), api.get('/api/products', { params: { active: 'true' } }), api.get('/api/tables', { params: { active: 'true' } }), ]); + if (session !== authSessionSeqRef.current || token !== localStorage.getItem(TOKEN_KEY)) return; if (sendInFlightRef.current && !options?.allowWhileSending) return; setCategories(categoriesRes.data.categories || []); setProducts(productsRes.data.products || []); @@ -330,10 +360,13 @@ export default function ServerStandalonePage() { } const order = res.data.orders?.[0] || null; setCurrentOrder(order); + const suspended = suspendedDraftRef.current; const pendingAttempt = sendAttemptRef.current?.snapshot.tableId === tableId ? sendAttemptRef.current.snapshot - : null; + : suspended?.ownerId === activeUserIdRef.current && suspended.tableId === tableId + ? suspended + : null; if (pendingAttempt) { setCustomerName(pendingAttempt.customerName); setCustomerPhone(pendingAttempt.customerPhone); @@ -344,6 +377,7 @@ export default function ServerStandalonePage() { setCustomerName(''); setCustomerPhone(''); } + if (suspended && !sendInFlightRef.current) suspendedDraftRef.current = null; setCustomerMatch(null); setCustomerSearched(false); orderLoadedTableIdRef.current = tableId; @@ -389,6 +423,111 @@ export default function ServerStandalonePage() { const userId = user?.id; + useEffect(() => { + activeUserIdRef.current = userId ?? null; + }, [userId]); + + useEffect(() => { + if (!api || !userId) return; + let cancelled = false; + const session = authSessionSeqRef.current; + const token = localStorage.getItem(TOKEN_KEY); + const refresh = () => { + api + .get('/api/settings/auto_lock_minutes') + .then((response) => { + if ( + cancelled || + session !== authSessionSeqRef.current || + token !== localStorage.getItem(TOKEN_KEY) + ) + return; + const minutes = Number(response.data?.setting?.value); + if ([0, 1, 5, 15, 30].includes(minutes)) setAutoLockMinutes(minutes); + }) + .catch(() => {}); + }; + lastActivityRef.current = Date.now(); + refresh(); + window.addEventListener('focus', refresh); + return () => { + cancelled = true; + window.removeEventListener('focus', refresh); + }; + }, [api, userId]); + + useEffect(() => { + if (!api || !userId || autoLockMinutes <= 0) return; + const timeout = autoLockMinutes * 60_000; + let timer: ReturnType; + let locked = false; + const lock = () => { + if (locked) return; + locked = true; + const token = localStorage.getItem(TOKEN_KEY); + suspendedDraftRef.current = { + ...liveDraftRef.current, + ownerId: userId, + attempt: sendAttemptRef.current, + }; + authSessionSeqRef.current += 1; + orderLoadSeqRef.current += 1; + sendSeqRef.current += 1; + cancelPendingCustomerLookup(); + localStorage.removeItem(TOKEN_KEY); + sendAttemptRef.current = null; + selectedTableIdRef.current = ''; + orderLoadedTableIdRef.current = ''; + activeUserIdRef.current = null; + setUser(null); + setEmail(''); + setPassword(''); + setDraft([]); + setOrderNotes(''); + setCustomerName(''); + setCustomerPhone(''); + setCustomerMatch(null); + setCustomerSearched(false); + setSelectedTableId(''); + setOrderLoadedTableId(''); + setCurrentOrder(null); + setAddonModalProduct(null); + setEditingDraftLine(null); + setMoveTableModalOpen(false); + setMobileCartOpen(false); + setAutoLockMinutes(0); + if (token) + void api + .post('/api/auth/logout', {}, { headers: { Authorization: `Bearer ${token}` } }) + .catch(() => {}); + }; + const check = () => { + const remaining = timeout - (Date.now() - lastActivityRef.current); + if (remaining <= 0) lock(); + else timer = setTimeout(check, remaining); + }; + const activity = () => { + lastActivityRef.current = Date.now(); + clearTimeout(timer); + timer = setTimeout(check, timeout); + }; + const visible = () => { + if (document.visibilityState === 'visible') { + clearTimeout(timer); + check(); + } + }; + const events = ['pointerdown', 'keydown', 'touchstart', 'wheel', 'mousemove'] as const; + events.forEach((event) => window.addEventListener(event, activity, { passive: true })); + document.addEventListener('visibilitychange', visible); + check(); + return () => { + clearTimeout(timer); + events.forEach((event) => window.removeEventListener(event, activity)); + document.removeEventListener('visibilitychange', visible); + }; + }, [api, userId, autoLockMinutes]); + useEffect(() => { if (!userId || !api) return; let cancelled = false; @@ -443,10 +582,13 @@ export default function ServerStandalonePage() { } const order = res.data.orders?.[0] || null; setCurrentOrder(order); + const suspended = suspendedDraftRef.current; const pendingAttempt = sendAttemptRef.current?.snapshot.tableId === targetTableId ? sendAttemptRef.current.snapshot - : null; + : suspended?.ownerId === activeUserIdRef.current && suspended.tableId === targetTableId + ? suspended + : null; if (pendingAttempt) { setCustomerName(pendingAttempt.customerName); setCustomerPhone(pendingAttempt.customerPhone); @@ -457,6 +599,7 @@ export default function ServerStandalonePage() { setCustomerName(''); setCustomerPhone(''); } + if (suspended && !sendInFlightRef.current) suspendedDraftRef.current = null; setCustomerMatch(null); setCustomerSearched(false); orderLoadedTableIdRef.current = targetTableId; @@ -617,6 +760,28 @@ export default function ServerStandalonePage() { }); authSessionSeqRef.current += 1; localStorage.setItem(TOKEN_KEY, res.data.access_token); + const suspended = suspendedDraftRef.current; + if (suspended && suspended.ownerId === res.data.user.id) { + selectedTableIdRef.current = suspended.tableId; + setSelectedTableId(suspended.tableId); + setDraft(suspended.draft); + setCustomerName(suspended.customerName); + setCustomerPhone(suspended.customerPhone); + setOrderNotes(suspended.orderNotes); + sendAttemptRef.current = suspended.attempt; + } else if (suspended) { + const hadDraft = + suspended.draft.length > 0 || + Boolean(suspended.attempt) || + Boolean(suspended.orderNotes.trim()) || + Boolean(suspended.customerName.trim()) || + Boolean(suspended.customerPhone.trim()); + suspendedDraftRef.current = null; + if (hadDraft) { + toast(t('draftDiscardedOnUserChange')); + } + } + activeUserIdRef.current = res.data.user.id; setUser(res.data.user); } catch (error: unknown) { toastApiError(error, t('signInFailed'), apiErrorT); @@ -742,6 +907,8 @@ export default function ServerStandalonePage() { async function ensureCustomer( submittedName: string, submittedPhone: string, + token: string, + isCurrent: () => boolean, ): Promise { if (!api) return null; const name = submittedName.trim(); @@ -754,28 +921,48 @@ export default function ServerStandalonePage() { try { const lookup = await api.get('/api/crm/lookup', { params: { phone: normalizedPhone }, + headers: { Authorization: `Bearer ${token}` }, }); + if (!isCurrent()) throw new Error('Session changed'); if (lookup.data.found && lookup.data.customer?.id) return lookup.data.customer.id; - } catch {} + } catch { + if (!isCurrent()) throw new Error('Session changed'); + } } + if (!isCurrent()) throw new Error('Session changed'); const fallbackName = name || t('guestFallbackName', { last4: rawPhone.slice(-4) }); - const res = await api.post('/api/customers', { - name: fallbackName, - phone: normalizedPhone || undefined, - }); + const res = await api.post( + '/api/customers', + { + name: fallbackName, + phone: normalizedPhone || undefined, + }, + { headers: { Authorization: `Bearer ${token}` } }, + ); + if (!isCurrent()) throw new Error('Session changed'); return res.data.customer?.id || null; } // Falls back to this device's browser print dialog when no hardware printer is configured (400). - async function printKotForOrder(orderId: number, orderForPrint: Record) { - if (!api) return; + async function printKotForOrder( + orderId: number, + orderForPrint: Record, + token: string, + isCurrent: () => boolean, + ) { + if (!api || !isCurrent()) return; try { - await api.post('/api/printers/print-kot', { - orderId, - items: orderForPrint.items, - }); + await api.post( + '/api/printers/print-kot', + { + orderId, + items: orderForPrint.items, + }, + { headers: { Authorization: `Bearer ${token}` } }, + ); return; } catch (printError: unknown) { + if (!isCurrent()) return; const status = axios.isAxiosError(printError) ? printError.response?.status : undefined; if (status !== 400) { toastApiError(printError, t('kotPrintFailed'), apiErrorT); @@ -785,6 +972,7 @@ export default function ServerStandalonePage() { try { const { generateKotHtml, resolveKotTicketLanguage } = await import('@/lib/printer/kot-web-print'); + if (!isCurrent()) return; const html = generateKotHtml(orderForPrint as unknown as FullOrder, { paperWidth: 80, language: resolveKotTicketLanguage(), @@ -797,12 +985,22 @@ export default function ServerStandalonePage() { } // Same browser-print fallback as KOT above on 400; stays quiet on 403 (owner hasn't enabled server bill printing). - async function printOrderSlip(orderId: number, orderForPrint: Record) { - if (!api) return; + async function printOrderSlip( + orderId: number, + orderForPrint: Record, + token: string, + isCurrent: () => boolean, + ) { + if (!api || !isCurrent()) return; try { - await api.post('/api/printers/print-bill', { orderId }); + await api.post( + '/api/printers/print-bill', + { orderId }, + { headers: { Authorization: `Bearer ${token}` } }, + ); return; } catch (printError: unknown) { + if (!isCurrent()) return; const status = axios.isAxiosError(printError) ? printError.response?.status : undefined; if (status === 403) return; if (status !== 400) { @@ -812,6 +1010,7 @@ export default function ServerStandalonePage() { } try { const { generateOrderSlipHtml } = await import('@/lib/printer/order-slip-web-print'); + if (!isCurrent()) return; const html = generateOrderSlipHtml( orderForPrint as unknown as FullOrder, { @@ -848,6 +1047,13 @@ export default function ServerStandalonePage() { ) { return; } + const requestToken = localStorage.getItem(TOKEN_KEY); + const requestSession = authSessionSeqRef.current; + const requestOwner = user?.id; + if (!requestToken || !requestOwner) return; + const isCurrent = () => + authSessionSeqRef.current === requestSession && + localStorage.getItem(TOKEN_KEY) === requestToken; const snapshotTable = tables.find((table) => table.id === selectedTableId) || null; const candidateSnapshot: SendSnapshot = { tableId: selectedTableId, @@ -888,7 +1094,13 @@ export default function ServerStandalonePage() { sendInFlightRef.current = true; setSending(true); try { - const customerId = await ensureCustomer(snapshot.customerName, snapshot.customerPhone); + const customerId = await ensureCustomer( + snapshot.customerName, + snapshot.customerPhone, + requestToken, + isCurrent, + ); + if (!isCurrent()) return; const items = snapshot.draftLines.map((line) => cartItemToOrderItem(line)); const specialInstructions = snapshot.orderNotes.trim() || undefined; let orderId: number; @@ -899,7 +1111,7 @@ export default function ServerStandalonePage() { `/api/orders/${snapshot.orderId}/items`, { items, special_instructions: specialInstructions }, { - headers: { 'Idempotency-Key': idempotencyKey }, + headers: { 'Idempotency-Key': idempotencyKey, Authorization: `Bearer ${requestToken}` }, }, ); orderId = data.order.id; @@ -924,6 +1136,26 @@ export default function ServerStandalonePage() { rawOrder = data.order; newItems = data.order.items || []; } + if (!isCurrent()) { + const suspended = suspendedDraftRef.current; + if (suspended?.ownerId === requestOwner && suspended.attempt?.nonce === nonce) { + const submitted = new Map(snapshot.draftLines.map((line) => [line.id, line.quantity])); + const remaining = suspended.draft.flatMap((line) => { + const count = submitted.get(line.id); + if (count === undefined) return [line]; + return line.quantity > count ? [{ ...line, quantity: line.quantity - count }] : []; + }); + suspended.draft = remaining; + if (suspended.orderNotes === snapshot.orderNotes) suspended.orderNotes = ''; + suspended.attempt = null; + if (activeUserIdRef.current === requestOwner) { + setDraft(remaining); + setOrderNotes(suspended.orderNotes); + if (sendAttemptRef.current?.nonce === nonce) sendAttemptRef.current = null; + } + } + return; + } if (sendAttemptRef.current?.signature === signature) { sendAttemptRef.current = null; } @@ -947,6 +1179,7 @@ export default function ServerStandalonePage() { loadAll({ allowWhileSending: true }), loadOrder(snapshot.tableId, { allowWhileSending: true }), ]).catch(() => {}); + if (!isCurrent()) return; toast.success(t('orderSent')); // rawOrder only has table_id/customer_id; the KOT/slip renderers need the nested table/customer for display. const enrichedOrder = { @@ -959,11 +1192,16 @@ export default function ServerStandalonePage() { // Backgrounded so an unreachable printer can't block Send; sequenced (not concurrent) // since KOT and bill can share a default printer and race its socket connection. void (async () => { - await printKotForOrder(orderId, { ...enrichedOrder, items: newItems }); - await printOrderSlip(orderId, enrichedOrder); + await printKotForOrder( + orderId, + { ...enrichedOrder, items: newItems }, + requestToken, + isCurrent, + ); + if (isCurrent()) await printOrderSlip(orderId, enrichedOrder, requestToken, isCurrent); })(); } catch (error: unknown) { - toastApiError(error, t('couldNotSendOrder'), apiErrorT); + if (isCurrent()) toastApiError(error, t('couldNotSendOrder'), apiErrorT); } finally { sendInFlightRef.current = false; setSending(false); diff --git a/frontend/src/lib/i18n/messages/ar.json b/frontend/src/lib/i18n/messages/ar.json index fcbe6f45b..c39935965 100644 --- a/frontend/src/lib/i18n/messages/ar.json +++ b/frontend/src/lib/i18n/messages/ar.json @@ -1716,6 +1716,7 @@ "disabledHint": "اطلب من المالك أو المدير إعادة تفعيل ميزة «الطلب من الطاولة» من قسم «الإعدادات».", "disabledTitle": "تطبيق الخادم معطل", "draftTotal": "المجموع المبدئي", + "draftDiscardedOnUserChange": "تم تجاهل مسودة الموظف السابق.", "emailPlaceholder": "server@flo.local", "emptyDraft": "انقر على عناصر القائمة لإضافتها هنا.", "guestFallbackName": "ضيف {last4}", diff --git a/frontend/src/lib/i18n/messages/bn.json b/frontend/src/lib/i18n/messages/bn.json index da4279459..e5c10257b 100644 --- a/frontend/src/lib/i18n/messages/bn.json +++ b/frontend/src/lib/i18n/messages/bn.json @@ -1716,6 +1716,7 @@ "disabledHint": "সেটিংস থেকে টেবিলসাইড অর্ডারিং পুনরায় সক্ষম করতে মালিক বা পরিচালককে বলুন।", "disabledTitle": "সার্ভার অ্যাপ অক্ষম করা হয়েছে", "draftTotal": "খসড়া মোট", + "draftDiscardedOnUserChange": "আগের কর্মীর খসড়া বাতিল করা হয়েছে।", "emailPlaceholder": "server@flo.local", "emptyDraft": "মেনু আইটেমগুলিকে এখানে যোগ করতে আলতো চাপুন।", "guestFallbackName": "অতিথি {last4}", diff --git a/frontend/src/lib/i18n/messages/de.json b/frontend/src/lib/i18n/messages/de.json index 4b77ce544..f5e3cb4fa 100644 --- a/frontend/src/lib/i18n/messages/de.json +++ b/frontend/src/lib/i18n/messages/de.json @@ -1716,6 +1716,7 @@ "disabledHint": "Bitten Sie einen Inhaber oder Manager, die Tischbestellung in den Einstellungen wieder zu aktivieren.", "disabledTitle": "Server-App ist deaktiviert", "draftTotal": "Zwischensumme Entwurf", + "draftDiscardedOnUserChange": "Der Entwurf der vorherigen Person wurde verworfen.", "emailPlaceholder": "server@flo.local", "emptyDraft": "Tippen Sie auf Menüartikel, um sie hier hinzuzufügen.", "guestFallbackName": "Gast {last4}", diff --git a/frontend/src/lib/i18n/messages/en.json b/frontend/src/lib/i18n/messages/en.json index 9b5318116..63b4c33cf 100644 --- a/frontend/src/lib/i18n/messages/en.json +++ b/frontend/src/lib/i18n/messages/en.json @@ -1716,6 +1716,7 @@ "disabledHint": "Ask an owner or manager to re-enable Tableside Ordering from Settings.", "disabledTitle": "Server App is disabled", "draftTotal": "Draft total", + "draftDiscardedOnUserChange": "The previous staff member’s draft was discarded.", "emailPlaceholder": "server@flo.local", "emptyDraft": "Tap menu items to add them here.", "guestFallbackName": "Guest {last4}", diff --git a/frontend/src/lib/i18n/messages/es.json b/frontend/src/lib/i18n/messages/es.json index d76983325..9b0e73e76 100644 --- a/frontend/src/lib/i18n/messages/es.json +++ b/frontend/src/lib/i18n/messages/es.json @@ -1716,6 +1716,7 @@ "disabledHint": "Pedile al propietario o encargado que reactive los pedidos desde la mesa desde Ajustes.", "disabledTitle": "La App de mesero está desactivada", "draftTotal": "Total del borrador", + "draftDiscardedOnUserChange": "Se descartó el borrador del empleado anterior.", "emailPlaceholder": "server@flo.local", "emptyDraft": "Tocá los ítems del menú para agregarlos acá.", "guestFallbackName": "Invitado {last4}", diff --git a/frontend/src/lib/i18n/messages/fa.json b/frontend/src/lib/i18n/messages/fa.json index ade956714..b85b8bba3 100644 --- a/frontend/src/lib/i18n/messages/fa.json +++ b/frontend/src/lib/i18n/messages/fa.json @@ -1716,6 +1716,7 @@ "disabledHint": "از دارنده یا سرپرست بخواهید ثبت سفارش کنار میز را از تنظیمات دوباره فعال کند.", "disabledTitle": "برنامه سرور غیرفعال است", "draftTotal": "جمع پیش‌نویس", + "draftDiscardedOnUserChange": "پیش‌نویس کارمند قبلی حذف شد.", "emailPlaceholder": "server@flo.local", "emptyDraft": "برای افزودن کالاها، روی کالاهای منو بزنید.", "guestFallbackName": "میهمان {last4}", diff --git a/frontend/src/lib/i18n/messages/fil.json b/frontend/src/lib/i18n/messages/fil.json index bf1fd9b67..402458043 100644 --- a/frontend/src/lib/i18n/messages/fil.json +++ b/frontend/src/lib/i18n/messages/fil.json @@ -1716,6 +1716,7 @@ "disabledHint": "Ask an owner or manager to re-enable Tableside Ordering from Mga Setting.", "disabledTitle": "Naka-disable ang Server App", "draftTotal": "Draft na kabuuan", + "draftDiscardedOnUserChange": "Tinanggal ang draft ng nakaraang kawani.", "emailPlaceholder": "server@flo.local", "emptyDraft": "I-tap ang mga item sa menu upang idagdag ang mga ito dito.", "guestFallbackName": "Bisita {last4}", diff --git a/frontend/src/lib/i18n/messages/fr.json b/frontend/src/lib/i18n/messages/fr.json index b1a33627b..6530963ea 100644 --- a/frontend/src/lib/i18n/messages/fr.json +++ b/frontend/src/lib/i18n/messages/fr.json @@ -1716,6 +1716,7 @@ "disabledHint": "Demandez à un propriétaire ou à un responsable de réactiver la prise de commande à table dans les paramètres.", "disabledTitle": "L’application serveur est désactivée", "draftTotal": "Total provisoire", + "draftDiscardedOnUserChange": "Le brouillon du membre du personnel précédent a été supprimé.", "emailPlaceholder": "server@flo.local", "emptyDraft": "Touchez les articles du menu pour les ajouter ici.", "guestFallbackName": "Client {last4}", diff --git a/frontend/src/lib/i18n/messages/hi.json b/frontend/src/lib/i18n/messages/hi.json index 7a506ccbe..b7b42ce29 100644 --- a/frontend/src/lib/i18n/messages/hi.json +++ b/frontend/src/lib/i18n/messages/hi.json @@ -1716,6 +1716,7 @@ "disabledHint": "किसी स्वामी या प्रबंधक से सेटिंग्स से टेबलसाइड ऑर्डरिंग को पुनः सक्षम करने के लिए कहें।", "disabledTitle": "सर्वर ऐप अक्षम है", "draftTotal": "कुल ड्राफ्ट", + "draftDiscardedOnUserChange": "पिछले कर्मचारी का ड्राफ्ट हटा दिया गया।", "emailPlaceholder": "server@flo.local", "emptyDraft": "मेनू आइटम को यहां जोड़ने के लिए उन पर टैप करें।", "guestFallbackName": "अतिथि {last4}", diff --git a/frontend/src/lib/i18n/messages/id.json b/frontend/src/lib/i18n/messages/id.json index 2b70edba7..559fb56ab 100644 --- a/frontend/src/lib/i18n/messages/id.json +++ b/frontend/src/lib/i18n/messages/id.json @@ -1716,6 +1716,7 @@ "disabledHint": "Minta pemilik atau manajer untuk mengaktifkan kembali Pemesanan di Meja dari Pengaturan.", "disabledTitle": "Server App dinonaktifkan", "draftTotal": "Total draf", + "draftDiscardedOnUserChange": "Draf staf sebelumnya telah dibuang.", "emailPlaceholder": "server@flo.local", "emptyDraft": "Ketuk item menu untuk menambahkannya di sini.", "guestFallbackName": "Tamu {last4}", diff --git a/frontend/src/lib/i18n/messages/it.json b/frontend/src/lib/i18n/messages/it.json index 62bd74101..f8cb2622c 100644 --- a/frontend/src/lib/i18n/messages/it.json +++ b/frontend/src/lib/i18n/messages/it.json @@ -1716,6 +1716,7 @@ "disabledHint": "Chiedi a un proprietario o a un gestore di riattivare l'ordinazione al tavolo dalle Impostazioni.", "disabledTitle": "Server App disabilitata", "draftTotal": "Totale bozza", + "draftDiscardedOnUserChange": "La bozza del precedente membro dello staff è stata eliminata.", "emailPlaceholder": "server@flo.local", "emptyDraft": "Tocca gli articoli del menu per aggiungerli qui.", "guestFallbackName": "Ospite {last4}", diff --git a/frontend/src/lib/i18n/messages/ja.json b/frontend/src/lib/i18n/messages/ja.json index d06641457..2fbc54771 100644 --- a/frontend/src/lib/i18n/messages/ja.json +++ b/frontend/src/lib/i18n/messages/ja.json @@ -1716,6 +1716,7 @@ "disabledHint": "設定からテーブルサイド注文を再有効化するよう、オーナーまたはマネージャーに依頼してください。", "disabledTitle": "サーバーアプリは無効です", "draftTotal": "下書き合計", + "draftDiscardedOnUserChange": "前のスタッフの下書きは破棄されました。", "emailPlaceholder": "server@flo.local", "emptyDraft": "メニュー項目をタップしてここに追加します。", "guestFallbackName": "お客様 {last4}", diff --git a/frontend/src/lib/i18n/messages/ko.json b/frontend/src/lib/i18n/messages/ko.json index ae8772154..ce9912ac0 100644 --- a/frontend/src/lib/i18n/messages/ko.json +++ b/frontend/src/lib/i18n/messages/ko.json @@ -1716,6 +1716,7 @@ "disabledHint": "테이블 주문을 다시 활성화하려면 설정에서 사장님 또는 관리자에게 요청하세요.", "disabledTitle": "서버 앱이 비활성화됨", "draftTotal": "임시 합계", + "draftDiscardedOnUserChange": "이전 직원의 임시 주문이 삭제되었습니다.", "emailPlaceholder": "server@flo.local", "emptyDraft": "메뉴 항목을 눌러 여기에 추가하세요.", "guestFallbackName": "게스트 {last4}", diff --git a/frontend/src/lib/i18n/messages/ne.json b/frontend/src/lib/i18n/messages/ne.json index abebb7ca9..ee676c126 100644 --- a/frontend/src/lib/i18n/messages/ne.json +++ b/frontend/src/lib/i18n/messages/ne.json @@ -1716,6 +1716,7 @@ "disabledHint": "मालिक वा प्रबन्धकलाई सेटिङहरूबाट टेबलसाइड अर्डरिङ पुनः सक्षम गर्न सोध्नुहोस्।", "disabledTitle": "सर्भर एप असक्षम गरिएको छ", "draftTotal": "मस्यौदा कुल", + "draftDiscardedOnUserChange": "अघिल्लो कर्मचारीको ड्राफ्ट हटाइयो।", "emailPlaceholder": "server@flo.local", "emptyDraft": "मेनु वस्तुहरूलाई यहाँ थप्न ट्याप गर्नुहोस्।", "guestFallbackName": "अतिथि {last4}", diff --git a/frontend/src/lib/i18n/messages/nl.json b/frontend/src/lib/i18n/messages/nl.json index f9c9c6abc..4a1169637 100644 --- a/frontend/src/lib/i18n/messages/nl.json +++ b/frontend/src/lib/i18n/messages/nl.json @@ -1716,6 +1716,7 @@ "disabledHint": "Vraag een eigenaar of manager om bestellen aan tafel opnieuw in te schakelen via Instellingen.", "disabledTitle": "Server-app is uitgeschakeld", "draftTotal": "Concept totaal", + "draftDiscardedOnUserChange": "Het concept van de vorige medewerker is verwijderd.", "emailPlaceholder": "server@flo.local", "emptyDraft": "Tik op menu-artikelen om ze hier toe te voegen.", "guestFallbackName": "Gast {last4}", diff --git a/frontend/src/lib/i18n/messages/pt.json b/frontend/src/lib/i18n/messages/pt.json index b7eb32b02..115270f1c 100644 --- a/frontend/src/lib/i18n/messages/pt.json +++ b/frontend/src/lib/i18n/messages/pt.json @@ -1716,6 +1716,7 @@ "disabledHint": "Peça ao proprietário ou gerente para reativar os pedidos à mesa nas Definições.", "disabledTitle": "O App do Garçom está desativado", "draftTotal": "Total do rascunho", + "draftDiscardedOnUserChange": "O rascunho do funcionário anterior foi descartado.", "emailPlaceholder": "server@flo.local", "emptyDraft": "Toque nos itens do menu para adicioná-los aqui.", "guestFallbackName": "Convidado {last4}", diff --git a/frontend/src/lib/i18n/messages/ru.json b/frontend/src/lib/i18n/messages/ru.json index e7766b446..4e6cfcd3c 100644 --- a/frontend/src/lib/i18n/messages/ru.json +++ b/frontend/src/lib/i18n/messages/ru.json @@ -1716,6 +1716,7 @@ "disabledHint": "Попросите владельца или менеджера снова включить функцию заказа у стола в настройках.", "disabledTitle": "Серверное приложение отключено", "draftTotal": "Итого черновика", + "draftDiscardedOnUserChange": "Черновик предыдущего сотрудника удалён.", "emailPlaceholder": "server@flo.local", "emptyDraft": "Нажмите на пункты меню, чтобы добавить их сюда.", "guestFallbackName": "Гость {last4}", diff --git a/frontend/src/lib/i18n/messages/sq.json b/frontend/src/lib/i18n/messages/sq.json index a8a9e87c7..2eeed8742 100644 --- a/frontend/src/lib/i18n/messages/sq.json +++ b/frontend/src/lib/i18n/messages/sq.json @@ -1716,6 +1716,7 @@ "disabledHint": "Kërkoni nga pronari ose menaxheri të riaktivizojë Porositë në tavolinë nga Cilësimet.", "disabledTitle": "Server App është i çaktivizuar", "draftTotal": "Totali i draftit", + "draftDiscardedOnUserChange": "Drafti i punonjësit të mëparshëm u hoq.", "emailPlaceholder": "server@flo.local", "emptyDraft": "Prekni artikujt e menusë për t’i shtuar këtu.", "guestFallbackName": "I ftuar {last4}", diff --git a/frontend/src/lib/i18n/messages/th.json b/frontend/src/lib/i18n/messages/th.json index ca1777cca..08a5dcd67 100644 --- a/frontend/src/lib/i18n/messages/th.json +++ b/frontend/src/lib/i18n/messages/th.json @@ -1716,6 +1716,7 @@ "disabledHint": "โปรดขอให้เจ้าของหรือผู้จัดการเปิดใช้งานการสั่งซื้อที่โต๊ะอีกครั้งในการตั้งค่า", "disabledTitle": "ปิดใช้งาน Server App", "draftTotal": "ยอดรวมร่าง", + "draftDiscardedOnUserChange": "ลบร่างของพนักงานคนก่อนแล้ว", "emailPlaceholder": "server@flo.local", "emptyDraft": "แตะรายการเมนูเพื่อเพิ่มที่นี่", "guestFallbackName": "ลูกค้า {last4}", diff --git a/frontend/src/lib/i18n/messages/tr.json b/frontend/src/lib/i18n/messages/tr.json index 7dfc8137e..b0c2a0226 100644 --- a/frontend/src/lib/i18n/messages/tr.json +++ b/frontend/src/lib/i18n/messages/tr.json @@ -1716,6 +1716,7 @@ "disabledHint": "Bir işletme sahibi veya müdürden Ayarlar'dan Masadan Sipariş Alma özelliğini yeniden etkinleştirmesini isteyin.", "disabledTitle": "Garson Uygulaması Devre Dışı", "draftTotal": "Taslak Toplamı", + "draftDiscardedOnUserChange": "Önceki personelin taslağı silindi.", "emailPlaceholder": "garson@restoran.com", "emptyDraft": "Buraya eklemek için menü ürünlerine dokunun.", "guestFallbackName": "Misafir {last4}", diff --git a/frontend/src/lib/i18n/messages/ur.json b/frontend/src/lib/i18n/messages/ur.json index 04f343bce..9ef0c07cb 100644 --- a/frontend/src/lib/i18n/messages/ur.json +++ b/frontend/src/lib/i18n/messages/ur.json @@ -1716,6 +1716,7 @@ "disabledHint": "مالک یا منیجر سے ترتیبات میں ٹیبل سائیڈ آرڈرنگ دوبارہ فعال کرنے کو کہیں۔", "disabledTitle": "Server App غیر فعال ہے", "draftTotal": "مسودے کی کل رقم", + "draftDiscardedOnUserChange": "پچھلے ملازم کا مسودہ حذف کر دیا گیا۔", "emailPlaceholder": "server@flo.local", "emptyDraft": "مینو آئٹمز پر کلک کر کے انہیں یہاں شامل کریں۔", "guestFallbackName": "مہمان {last4}", diff --git a/frontend/src/lib/i18n/messages/vi.json b/frontend/src/lib/i18n/messages/vi.json index 03264b049..8c40952ae 100644 --- a/frontend/src/lib/i18n/messages/vi.json +++ b/frontend/src/lib/i18n/messages/vi.json @@ -1716,6 +1716,7 @@ "disabledHint": "Hãy nhờ chủ cửa hàng hoặc quản lý bật lại Đặt hàng tại bàn trong Cài đặt.", "disabledTitle": "Server App đã bị tắt", "draftTotal": "Tổng tạm tính", + "draftDiscardedOnUserChange": "Bản nháp của nhân viên trước đã bị xóa.", "emailPlaceholder": "server@flo.local", "emptyDraft": "Chạm vào món trong thực đơn để thêm vào đây.", "guestFallbackName": "Khách {last4}", diff --git a/frontend/src/lib/i18n/messages/zh-tw.json b/frontend/src/lib/i18n/messages/zh-tw.json index 36fad8eae..3b0f7cf6f 100644 --- a/frontend/src/lib/i18n/messages/zh-tw.json +++ b/frontend/src/lib/i18n/messages/zh-tw.json @@ -1716,6 +1716,7 @@ "disabledHint": "請讓店主或經理在設定中重新啟用桌邊點餐。", "disabledTitle": "服務員應用程式已禁用", "draftTotal": "草稿合計", + "draftDiscardedOnUserChange": "已捨棄上一位員工的草稿。", "emailPlaceholder": "server@flo.local", "emptyDraft": "點按菜單商品將其新增到這裡。", "guestFallbackName": "客人 {last4}", diff --git a/frontend/src/lib/i18n/messages/zh.json b/frontend/src/lib/i18n/messages/zh.json index 2c805bc5e..f281fb6cb 100644 --- a/frontend/src/lib/i18n/messages/zh.json +++ b/frontend/src/lib/i18n/messages/zh.json @@ -1716,6 +1716,7 @@ "disabledHint": "请让店主或经理在设置中重新启用桌边点单。", "disabledTitle": "服务员应用已禁用", "draftTotal": "草稿合计", + "draftDiscardedOnUserChange": "已丢弃上一位员工的草稿。", "emailPlaceholder": "server@flo.local", "emptyDraft": "点按菜单商品将其添加到这里。", "guestFallbackName": "客人 {last4}", diff --git a/main/server-app.ts b/main/server-app.ts index be0a7e897..74c12e38a 100644 --- a/main/server-app.ts +++ b/main/server-app.ts @@ -371,6 +371,12 @@ export function startServerApp(): Promise { }, ); + app.get( + '/api/settings/auto_lock_minutes', + serverAppApiRateLimit, + requireServerAppAuth, + (req, res) => forwardToMainApi(req, res, '/settings/auto_lock_minutes'), + ); app.get('/api/categories', serverAppApiRateLimit, requireServerAppAuth, (req, res) => forwardToMainApi(req, res, '/categories'), ); diff --git a/tests/server-app-server-role.test.ts b/tests/server-app-server-role.test.ts index 524b6a993..4e9692f6c 100644 --- a/tests/server-app-server-role.test.ts +++ b/tests/server-app-server-role.test.ts @@ -194,6 +194,19 @@ async function main() { assert.equal(me.body.user.role, role, `/api/auth/me returns ${role} role`); } + setSetting('auto_lock_minutes', '1'); + const unauthIdleSetting = await fetch(`${baseUrl}/api/settings/auto_lock_minutes`); + assert.equal(unauthIdleSetting.status, 401, 'Server App idle setting requires authentication'); + for (const role of ['server', 'manager', 'owner']) { + const setting = await getJson(baseUrl, '/api/settings/auto_lock_minutes', roleTokens[role]); + assert.equal(setting.status, 200, `${role} can read the idle timeout`); + assert.equal(setting.body.setting.value, '1', `${role} receives the configured timeout`); + } + const otherSetting = await fetch(`${baseUrl}/api/settings/country`, { + headers: { Authorization: `Bearer ${roleTokens.server}` }, + }); + assert.equal(otherSetting.status, 404, 'Server App does not proxy general settings'); + const liveServerLogin = await postJson(baseUrl, '/api/auth/login', { email: 'server@server-app.test', password: 'ServerPass123!',