From bd9258cb2af912cbe6846f0aa922d7a17e4f2612 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Thu, 1 Oct 2026 14:00:21 +0530 Subject: [PATCH 1/3] Keep order details readable in phone dark mode --- assets/common/design.css | 2 + assets/order-history/theme.css | 89 +++++++++++++++++++++++++++++++ order-history.html | 3 ++ tests/order-details-theme.test.js | 62 +++++++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 assets/order-history/theme.css create mode 100644 tests/order-details-theme.test.js diff --git a/assets/common/design.css b/assets/common/design.css index 500b065..4c340ef 100644 --- a/assets/common/design.css +++ b/assets/common/design.css @@ -36,6 +36,7 @@ */ :root { + color-scheme: light; /* Ink, from strongest to faintest. Named by ROLE, so a screen never has to know which grey it is asking for. */ --ink: #0f172a; @@ -96,6 +97,7 @@ */ @media (prefers-color-scheme: dark) { :root { + color-scheme: dark; --ink: #f1f5f9; --ink-soft: #cbd5e1; --ink-faint: #94a3b8; diff --git a/assets/order-history/theme.css b/assets/order-history/theme.css new file mode 100644 index 0000000..aaa0640 --- /dev/null +++ b/assets/order-history/theme.css @@ -0,0 +1,89 @@ +/* Order history and every dialog it opens use paired surface/text colours. + design.css supplies the same light/dark palette as the menu. */ +:root { + color-scheme: light; + --light-bg: var(--ground); + --secondary-color: var(--ink); + --border-color: var(--line-strong); + --bs-body-color: var(--ink); + --bs-body-bg: var(--surface); + --bs-secondary-color: var(--ink-soft); + --bs-border-color: var(--line-strong); + --bs-light-rgb: 248, 250, 252; +} + +body { background: var(--ground); color: var(--ink); } +.modal-content { + --bs-modal-bg: var(--surface); + --bs-modal-color: var(--ink); + --bs-modal-border-color: var(--line-strong); + --bs-modal-header-border-color: var(--line-strong); + --bs-modal-footer-border-color: var(--line-strong); + background: var(--surface); + color: var(--ink); +} +.table { + --bs-table-color: var(--ink); + --bs-table-bg: var(--surface); + --bs-table-border-color: var(--line-strong); + --bs-table-striped-color: var(--ink); + --bs-table-striped-bg: var(--surface-sunk); + color: var(--ink); +} +.table > :not(caption) > * > * { color: var(--ink); border-color: var(--line-strong); } +.text-muted { color: var(--ink-soft) !important; } +.status-cancelled { background: #f8d7da; color: #842029; } +.order-items-table tfoot tr:last-child { border-top: 2px solid var(--line-strong) !important; } + +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --bs-light-rgb: 13, 21, 38; + --bs-primary-rgb: 147, 197, 253; + --bs-success-rgb: 134, 239, 172; + --bs-danger-rgb: 252, 165, 165; + } + .mobile-header, .table-card:not(.all-tables), .order-card, + .search-filter-container, .product-suggestions, + #editItemNotesModal .modal-content { + background: var(--surface); + color: var(--ink); + border-color: var(--line-strong); + } + .order-item-card, .item-preview, .empty-state, .product-suggestion:hover { + background: var(--surface-sunk); + color: var(--ink); + border-color: var(--line-strong); + } + .screen-header p, .order-meta, .order-items-preview, .item-info .item-price, + .edit-notes-hint, .search-box i, .table-number, + .order-items-table .order-item-notes { color: var(--ink-soft); } + .modal h6, .modal .modal-title, .qty-display, .item-info h6, + #editOrderModal h6, #editOrderModal .modal-title, + #editItemNotesModal .edit-notes-header .modal-title { color: var(--ink); } + input:not([type=checkbox]):not([type=radio]), textarea, select, .form-control, + .qty-btn, .filter-btn:not(.active), .close-btn, .cancel-notes-btn, + #editOrderModal .modal-footer .close-btn { + color: var(--ink); + background-color: var(--surface-sunk); + border-color: var(--line-strong); + } + input::placeholder, textarea::placeholder { color: var(--ink-soft); opacity: 1; } + .form-control:focus, input:focus, textarea:focus, select:focus { + color: var(--ink); + background-color: var(--surface-sunk); + border-color: #93c5fd; + } + .modal-header, .modal-footer, .order-actions, .order-header, + #editOrderModal .modal-header { border-color: var(--line-strong); } + .btn-close { filter: invert(1) grayscale(100%) brightness(200%); opacity: .85; } + .text-dark { color: var(--ink) !important; } + .order-count { background: #16304e; color: #bfdbfe; } + .order-count.no-orders { background: #1e293b; color: #cbd5e1; } + .status-pending, .order-count.has-pending { background: #422d0b; color: #fde68a; } + .status-completed { background: #123c36; color: #a7f3d0; } + .status-cancelled { background: #491f2b; color: #fecdd3; } + .edit-btn, #editOrderModal .modal-footer .edit-btn { background: #1d4ed8; color: #fff; } + .cancel-btn, .remove-btn { background: #b91c1c; color: #fff; } + #page-loader { background: var(--ground); } +} diff --git a/order-history.html b/order-history.html index 3ac29c0..2e54b4a 100644 --- a/order-history.html +++ b/order-history.html @@ -4,6 +4,7 @@ + Order History - Restaurant @@ -16,6 +17,8 @@ + + diff --git a/tests/order-details-theme.test.js b/tests/order-details-theme.test.js new file mode 100644 index 0000000..9dc9320 --- /dev/null +++ b/tests/order-details-theme.test.js @@ -0,0 +1,62 @@ +'use strict'; +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { chromium } = require('@playwright/test'); +const root = path.join(__dirname, '..'); +const screenshots = path.join(root, 'test-results', 'order-details-theme'); + +test('order details remain readable in both phone themes, including a live theme change', async () => { + const browser = await chromium.launch({ channel: process.env.CI ? undefined : 'msedge' }); + try { + const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); + await page.route('https://captain.test/**', async route => { + const name = new URL(route.request().url()).pathname.slice(1); + const file = path.resolve(root, name); + if (!file.startsWith(root + path.sep) || !fs.existsSync(file)) return route.abort(); + let body = fs.readFileSync(file); + if (name.endsWith('.html')) body = body.toString().replace(/]*>[\s\S]*?<\/script>/gi, ''); + await route.fulfill({ body, contentType: name.endsWith('.html') ? 'text/html' : name.endsWith('.css') ? 'text/css' : 'application/octet-stream' }); + }); + await page.goto('https://captain.test/order-history.html'); + const script = fs.readFileSync(path.join(root, 'assets/order-history/script.js'), 'utf8'); + const start = script.indexOf('function viewOrderDetails('); + const end = script.indexOf('\n// Edit order', start); + assert.ok(start >= 0 && end > start); + await page.evaluate(source => { + window.currentOrderId = null; + window.formatDateTime = () => '01 Oct, 13:00'; + window.allOrders = [{ _id: 'sample', order_id: 'KOT-126', table_number: '8', status: 'pending', customer_name: 'Walk-in', person_count: 2, items: [{ name: 'Chicken Fried Rice', quantity: 1, price: 250, item_description: 'Less spicy' }], tax: 12.5, total_amount: 262.5, discount_description: 'Serve together' }]; + (0, eval)(source); + window.viewOrderDetails('sample'); + document.getElementById('page-loader').remove(); + const modal = document.getElementById('orderDetailsModal'); + modal.classList.add('show'); + modal.style.display = 'block'; + }, script.slice(start, end)); + for (const colorScheme of ['light', 'dark', 'light']) { + await page.emulateMedia({ colorScheme }); + for (const selector of ['.modal-title', '#order-details-content strong', '.table tbody td', '.table tfoot th', '.order-notes-section .text-muted', '.order-item-notes', '.status-badge']) { + const contrast = await page.locator('#orderDetailsModal').locator(selector).first().evaluate(el => { + const rgb = value => value.match(/[\d.]+/g).slice(0, 3).map(Number); + const luminance = values => values.map(v => { v /= 255; return v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4; }).reduce((n, v, i) => n + v * [.2126, .7152, .0722][i], 0); + const fg = luminance(rgb(getComputedStyle(el).color)); + let parent = el; + while (parent && ['rgba(0, 0, 0, 0)', 'transparent'].includes(getComputedStyle(parent).backgroundColor)) parent = parent.parentElement; + const bg = luminance(rgb(getComputedStyle(parent || document.body).backgroundColor)); + return (Math.max(fg, bg) + .05) / (Math.min(fg, bg) + .05); + }); + assert.ok(contrast >= 4.5, `${colorScheme} ${selector}: contrast ${contrast}`); + } + fs.mkdirSync(screenshots, { recursive: true }); + await page.screenshot({ path: path.join(screenshots, `order-details-${colorScheme}.png`), fullPage: true }); + } + await page.emulateMedia({ colorScheme: 'dark' }); + for (const status of ['completed', 'cancelled']) { + await page.evaluate(value => { allOrders[0].status = value; viewOrderDetails('sample'); }, status); + assert.equal(await page.locator('.status-badge').textContent(), status); + assert.equal(await page.locator('#edit-order-btn').isVisible(), false); + } + } finally { await browser.close(); } +}); From f304f8649694ca41442636f927a14dd630659771 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Thu, 1 Oct 2026 14:04:57 +0530 Subject: [PATCH 2/3] Exercise current order-detail helpers in theme regression --- tests/order-details-theme.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/order-details-theme.test.js b/tests/order-details-theme.test.js index 9dc9320..4a20281 100644 --- a/tests/order-details-theme.test.js +++ b/tests/order-details-theme.test.js @@ -24,6 +24,11 @@ test('order details remain readable in both phone themes, including a live theme const start = script.indexOf('function viewOrderDetails('); const end = script.indexOf('\n// Edit order', start); assert.ok(start >= 0 && end > start); + await page.addScriptTag({ content: fs.readFileSync(path.join(root, 'assets/common/money.js'), 'utf8') }); + const cancelledStart = script.indexOf('function lineIsCancelled('); + const struckStart = script.indexOf('function struck('); + const struckEnd = script.indexOf('\n}', struckStart) + 2; + await page.addScriptTag({ content: script.slice(cancelledStart, struckEnd) }); await page.evaluate(source => { window.currentOrderId = null; window.formatDateTime = () => '01 Oct, 13:00'; From 434422e14fbfe6cb35eebf51895278cde095f539 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Thu, 1 Oct 2026 21:38:52 +0530 Subject: [PATCH 3/3] Submit catalogue base price when adding items to orders --- assets/order-history/script.js | 3 ++- tests/order-base-price.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/order-base-price.test.js diff --git a/assets/order-history/script.js b/assets/order-history/script.js index 5b3aa5e..a233188 100644 --- a/assets/order-history/script.js +++ b/assets/order-history/script.js @@ -391,7 +391,8 @@ async function searchProducts(query) { .map(p => ({ _id: p.id, name: p.name, - selling_price: p.final_price || p.price || 0, + selling_price: p.price ?? 0, + final_price: p.final_price, available_quantity: p.available_quantity || 0 })); diff --git a/tests/order-base-price.test.js b/tests/order-base-price.test.js new file mode 100644 index 0000000..0e4a590 --- /dev/null +++ b/tests/order-base-price.test.js @@ -0,0 +1,25 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const vm = require('node:vm'); +const source = fs.readFileSync(require('node:path').join(__dirname, '../assets/order-history/script.js'), 'utf8'); +const search = source.slice(source.indexOf('async function searchProducts(query)'), source.indexOf('// Store products for reference')); +const add = source.slice(source.indexOf('function addProductToOrder(productId,'), source.indexOf('// Update order total')); +const save = source.slice(source.indexOf('function linesForSave(items)'), source.indexOf('function confirmRemoveItem()')); +test('search, add and save send the menu base price, never final_price', async () => { + let matches; + const ctx = vm.createContext({ + console, crypto: require('node:crypto'), window: {}, STORE_NAME: 'products', + getData: async () => [{ id: 'paratha', name: 'Paratha', price: 45, final_price: 47.25 }], + document: { getElementById: () => ({}) }, + renderProductSuggestions: products => { matches = products; }, + editingOrder: { items: [] }, renderCurrentOrderItems() {}, updateOrderTotal() {}, + lineIsCancelled: () => false, lineQuantity: item => item.quantity, + }); + vm.runInContext(search + '\n' + add + '\n' + save, ctx); + await ctx.searchProducts('Paratha'); + ctx.addProductToOrder('paratha', matches[0].name, matches[0].selling_price); + const request = ctx.linesForSave(ctx.editingOrder.items); + assert.equal(request[0].price, 45); + assert.equal(matches[0].final_price, 47.25); +});