Skip to content
Merged
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
29 changes: 28 additions & 1 deletion docs/architecture/frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,33 @@ racing lookups are ignored by lookup id, rapid clicks open at most one allocator
explicit split-opening request is in flight competing payment and split mutations are disabled until
it finally releases.

### Add-on groups have one editor

Add-on groups are managed only in Products, on the tab selected by `/products?tab=addons`. The
Products page reads `tab` (`products`, `categories`, or `addons`; anything else selects Products)
inside a Suspense boundary and writes tab clicks back with `router.replace`, keeping other query
parameters. The tab is shown to restaurants and to any business whose role grants `catalog.manage`.
The legacy `/addon-groups` route is kept for bookmarks and the static export; it makes no API call and
client-redirects to `/products?tab=addons` with `router.replace`.

The group editor echoes each saved add-on's `tax_category_id`, `tax_behavior`, and
`inherit_parent_tax_category` back on save, and sends `stock_quantity` only when the stock field was
edited, so an ordinary save cannot overwrite stock moved since the editor opened.

### Charge identity is chosen, not typed

[`ChargesSettingsCard`](../../frontend/src/components/settings/ChargesSettingsCard.tsx) offers Service,
Packaging, and Custom kinds for a new charge. Service and Packaging use the reserved IDs
`service_charge` and `packaging_charge` (which feed the standard report columns); if that ID already
exists, choosing the kind opens the saved definition instead of applying defaults. Custom charges get
an opaque `charge-<32 hex>` ID from `crypto.getRandomValues`, generated once per draft. IDs are never
derived from names or editable, so renaming a charge keeps its identity. Choosing a kind writes
nothing; only Save persists. Editing an existing charge reloads definitions before building the
replacement list. An edit therefore does not recreate a charge already deleted on the server or
replace other definitions from its old snapshot. A failed reload retains the draft and blocks saving
until Retry succeeds. Creation retries reuse the draft's stable ID, including after a committed
attempt's response is lost.

### The KDS live feed is a WebSocket, not a request

Two surfaces open a socket to the same `/kds` path. The base ports for that path are `:3001` and
Expand Down Expand Up @@ -597,7 +624,7 @@ frontend/src/
│ ├── products/ Product image uploader
│ ├── providers/ I18nProvider
│ ├── settings/ The settings screen's building blocks:
│ │ BetaChannelToggle, CurrencyResetDialog, DatabaseSettingsTab,
│ │ BetaChannelToggle, ChargesSettingsCard, CurrencyResetDialog, DatabaseSettingsTab,
│ │ GeneralSettingsTab, HealthCheckDialog,
│ │ InitializeDatabaseDialog, LocalePreferencesPanel, MasterPinPrompt,
│ │ PaymentMethodsSettings, PermissionAuditLog, PermissionMatrix,
Expand Down
36 changes: 30 additions & 6 deletions frontend/e2e/i18n-batch-5e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ async function login(page: Page, email: string): Promise<void> {
}

test.describe('Batch 5E Migrated Pages & Components E2E Validation', () => {
test('Batch 5E Pages render correctly in English, Spanish, and Persian with useTranslations and valid leaf keys', async ({ page }) => {
test('Batch 5E Pages render correctly in English, Spanish, and Persian with useTranslations and valid leaf keys', async ({
page,
}) => {
await login(page, 'owner@flo.local');

// ==========================================
Expand Down Expand Up @@ -58,8 +60,15 @@ test.describe('Batch 5E Migrated Pages & Components E2E Validation', () => {

// 1c. Addon Groups (EN)
await page.goto(`${BASE}/addon-groups`);
await page.waitForURL(
(url) => url.pathname.startsWith('/products') && url.searchParams.get('tab') === 'addons',
);
await expect(page.locator('html')).toHaveAttribute('dir', 'ltr');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Addon Groups');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Products');
await expect(page.getByRole('tab', { name: 'Addon Groups' })).toHaveAttribute(
'data-state',
'active',
);
await captureScreenshot(page, 'batch-5e-addon-groups-en.png');

// 1d. Staff (EN)
Expand Down Expand Up @@ -107,8 +116,15 @@ test.describe('Batch 5E Migrated Pages & Components E2E Validation', () => {

// 2c. Addon Groups (ES)
await page.goto(`${BASE}/addon-groups`);
await page.waitForURL(
(url) => url.pathname.startsWith('/products') && url.searchParams.get('tab') === 'addons',
);
await expect(page.locator('html')).toHaveAttribute('dir', 'ltr');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Grupos de adicionales');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Productos');
await expect(page.getByRole('tab', { name: 'Grupos de adicionales' })).toHaveAttribute(
'data-state',
'active',
);
await captureScreenshot(page, 'batch-5e-addon-groups-es.png');

// 2d. Staff (ES)
Expand All @@ -132,7 +148,9 @@ test.describe('Batch 5E Migrated Pages & Components E2E Validation', () => {
// 2g. Print Test (ES)
await page.goto(`${BASE}/print-test`);
await expect(page.locator('html')).toHaveAttribute('dir', 'ltr');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Página de prueba de impresión');
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'Página de prueba de impresión',
);
await captureScreenshot(page, 'batch-5e-print-test-es.png');

// ==========================================
Expand All @@ -156,8 +174,15 @@ test.describe('Batch 5E Migrated Pages & Components E2E Validation', () => {

// 3c. Addon Groups (FA)
await page.goto(`${BASE}/addon-groups`);
await page.waitForURL(
(url) => url.pathname.startsWith('/products') && url.searchParams.get('tab') === 'addons',
);
await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('گروه‌های افزونه');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('کالاها');
await expect(page.getByRole('tab', { name: 'گروه‌های افزودنی' })).toHaveAttribute(
'data-state',
'active',
);
await captureScreenshot(page, 'batch-5e-addon-groups-fa.png');

// 3d. Staff (FA)
Expand All @@ -183,7 +208,6 @@ test.describe('Batch 5E Migrated Pages & Components E2E Validation', () => {
await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('صفحه آزمون چاپ');
await captureScreenshot(page, 'batch-5e-print-test-fa.png');

} finally {
await setLanguage(page, 'en');
}
Expand Down
Loading
Loading