From 1a784bbbfdaf0c62636f3c995731707918aba466 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Mon, 20 Apr 2026 14:51:53 +0200 Subject: [PATCH] Remove edge gateway components and templates associated with outdated workflows. --- .ai-workflow/manifest.json | 186 ++ .ai-workflow/workflow.md | 74 + .../rules/Creating and maintaining tests.md | 262 +-- .codex/environments/environment.toml | 37 +- .github/workflows/tests.yml | 3 + .junie/guidelines.md | 86 +- openapi.yaml | 14 +- scripts/sync-ai-workflow.mjs | 414 +++++ .../displays/boxes/WhiteBoxCard.vue | 10 +- .../department/pos/orders/ordersTable.vue | 104 ++ .../steps/mobile/PosDepartmentStepMobile2.vue | 74 +- .../PosDepartmentStepMobile2LastOrder.vue | 322 ++-- .../PosDepartmentStepMobile2Product.vue | 266 +-- .../PosDepartmentStepMobilePopupRenderer.vue | 98 +- ...epartmentStepMobilePopupSelectCustomer.vue | 559 ++++-- .../objects/PosDepartmentStepMobileFlow.vue | 91 +- .../edgeGateway/EdgeGatewayConfigurePanel.vue | 209 --- .../edgeGateway/EdgeGatewayFleetRail.vue | 121 -- .../edgeGateway/EdgeGatewayHealthSummary.vue | 187 -- .../EdgeGatewayMaintenancePanel.vue | 313 ---- .../edgeGateway/EdgeGatewayTerminal.vue | 192 -- .../edgeGateway/EdgeGatewayWorkspace.vue | 1590 +---------------- .../edgeGatewayWorkspace.helpers.js | 657 ------- .../modals/AssignDraftOrderCustomerModal.vue | 488 +++++ .../models/DepartmentPos/OrdersPagination.vue | 8 +- .../department/pos/SelectCustomerFormPOS.vue | 144 +- .../pos/input/customerSearchFieldPos.vue | 362 +++- .../items/NavigationMenuItemsAdmin.vue | 29 +- .../step1/RegistrationNumberSearchResult.vue | 39 +- src/components/session/token/SessionUser.vue | 14 + .../token/SessionUser/Objects/Orders.vue | 137 ++ .../superUser/modules/Economic/Config.vue | 10 +- src/components/shop/POSDepartmentProcess.vue | 3 +- .../page/headers/DesktopNavigationBuefy.vue | 39 +- .../headers/menu/NavigationMenuDepartment.vue | 62 +- .../useDraftTransactionCustomer.js | 69 + src/composables/useEdgeGatewayWorkspace.js | 887 --------- .../useStripeReaderAvailability.js | 179 ++ .../edgeGateways/EdgeGatewayInventoryPage.vue | 144 ++ .../edgeGateways/EdgeGatewayManagePage.vue | 106 ++ .../edgeGateways/EdgeGatewayManager.vue | 1431 ++++++++------- .../EdgeGatewayOperationsPage.vue | 130 ++ .../edgeGateways/EdgeGatewayOverviewPage.vue | 108 ++ .../edgeGateways/edgeGatewayErrors.js | 69 + src/i18n/locales/da.json | 69 +- src/i18n/locales/de.json | 55 + src/i18n/locales/en.json | 55 + src/i18n/locales/no.json | 55 + src/i18n/locales/sv.json | 55 + src/router.js | 71 +- src/services/departmentVisibility.js | 50 + src/services/edgeGateways.js | 67 +- .../modules/Pos/DepartmentPosDrafts.vue | 94 + .../modules/Pos/DepartmentPosOrder.vue | 72 +- .../superUserDashboard/EdgeGateways.vue | 18 - .../EdgeGatewaysWorkspacePage.vue | 49 + .../collectedOrderInvoiceManageEconomic.vue | 15 + .../configuration/ConfigurationEconomic.vue | 36 +- ...ue => DepartmentGatewaysWorkspacePage.vue} | 21 +- .../EconomicQueuePlaywrightHarness.vue | 1 + tests/e2e/admin-department-visibility.spec.ts | 104 +- tests/e2e/admin-pos-drafts.spec.ts | 285 +++ tests/e2e/admin-pos-orders.spec.ts | 99 + tests/e2e/economic-queue-workflow.spec.js | 17 + tests/e2e/edge-gateways.routes.spec.js | 54 +- tests/e2e/edge-gateways.smoke.spec.js | 116 +- tests/e2e/edge-gateways.visual.spec.js | 26 +- tests/e2e/pos-flow.spec.js | 184 +- tests/e2e/pos-mobile-card-payments.spec.js | 57 + tests/e2e/pos-mobile-order-flow.spec.js | 374 +++- .../release/edge-gateways.live-smoke.spec.ts | 9 +- tests/e2e/support/network.js | 556 +++--- tests/unit/edge-gateway-service.spec.js | 10 +- .../edge-gateway-workflow-helpers.spec.js | 114 +- tests/unit/edge-gateway-workspace.spec.js | 105 +- tests/unit/orders-table.spec.js | 37 + tests/unit/pos-mobile-popup-renderer.spec.js | 141 ++ tests/unit/select-vehicle-form-pos.spec.js | 2 + .../use-draft-transaction-customer.spec.js | 69 + 79 files changed, 7228 insertions(+), 6241 deletions(-) create mode 100644 .ai-workflow/manifest.json create mode 100644 .ai-workflow/workflow.md create mode 100644 scripts/sync-ai-workflow.mjs delete mode 100644 src/components/displays/edgeGateway/EdgeGatewayConfigurePanel.vue delete mode 100644 src/components/displays/edgeGateway/EdgeGatewayFleetRail.vue delete mode 100644 src/components/displays/edgeGateway/EdgeGatewayHealthSummary.vue delete mode 100644 src/components/displays/edgeGateway/EdgeGatewayMaintenancePanel.vue delete mode 100644 src/components/displays/edgeGateway/EdgeGatewayTerminal.vue delete mode 100644 src/components/displays/edgeGateway/edgeGatewayWorkspace.helpers.js create mode 100644 src/components/displays/modals/AssignDraftOrderCustomerModal.vue create mode 100644 src/composables/useDraftTransactionCustomer.js delete mode 100644 src/composables/useEdgeGatewayWorkspace.js create mode 100644 src/composables/useStripeReaderAvailability.js create mode 100644 src/features/edgeGateways/EdgeGatewayInventoryPage.vue create mode 100644 src/features/edgeGateways/EdgeGatewayManagePage.vue create mode 100644 src/features/edgeGateways/EdgeGatewayOperationsPage.vue create mode 100644 src/features/edgeGateways/EdgeGatewayOverviewPage.vue create mode 100644 src/features/edgeGateways/edgeGatewayErrors.js create mode 100644 src/views/dashboards/departmentDashboard/modules/Pos/DepartmentPosDrafts.vue delete mode 100644 src/views/dashboards/superUserDashboard/EdgeGateways.vue create mode 100644 src/views/dashboards/superUserDashboard/EdgeGatewaysWorkspacePage.vue rename src/views/dashboards/superUserDashboard/department/{DepartmentGateways.vue => DepartmentGatewaysWorkspacePage.vue} (61%) create mode 100644 tests/e2e/admin-pos-drafts.spec.ts create mode 100644 tests/unit/pos-mobile-popup-renderer.spec.js create mode 100644 tests/unit/use-draft-transaction-customer.spec.js diff --git a/.ai-workflow/manifest.json b/.ai-workflow/manifest.json new file mode 100644 index 00000000..bf868136 --- /dev/null +++ b/.ai-workflow/manifest.json @@ -0,0 +1,186 @@ +{ + "version": 1, + "snapshot_of": "front-end-vue", + "projects": { + "front-end-vue": { + "display_name": "Truckwash Frontend", + "relative_root": ".", + "commands": { + "setup": { + "default": "npm ci\nnpx playwright install" + }, + "run": { + "default": "npm run dev" + }, + "test_unit": { + "default": "npm run test:unit" + }, + "test_e2e": { + "default": "npm run test:e2e:smoke" + }, + "test_full": { + "default": "npm run test:e2e:ci" + }, + "debug": { + "default": "npx playwright test --debug" + } + }, + "codex_environment": { + "name": "pleno-vue", + "actions": [ + { + "name": "Run dev", + "icon": "run", + "command_id": "run" + }, + { + "name": "Vitest unit tests", + "icon": "test", + "command_id": "test_unit" + }, + { + "name": "Playwright smoke", + "icon": "test", + "command_id": "test_e2e" + }, + { + "name": "Playwright full suite", + "icon": "test", + "command_id": "test_full" + }, + { + "name": "Playwright debug", + "icon": "debug", + "command_id": "debug" + }, + { + "name": "AI workflow check", + "icon": "debug", + "literal_command": { + "default": "node scripts/sync-ai-workflow.mjs --check" + } + } + ] + }, + "generated_content": { + "aiassistant_tests_lines": [ + "# Frontend Testing Rules", + "", + "These rules apply to Vue changes, browser flows, and frontend test updates.", + "", + "1. Add or update tests for every new feature, bug fix, API integration change, or auth, booking, POS, or i18n workflow change.", + "2. Browser validation runs through Playwright. Legacy browser-driver tooling is outside the supported workflow.", + "3. Use `npm run test:unit` for Vitest coverage of isolated logic and component behavior.", + "4. Use `npm run test:e2e:smoke` for the narrow Playwright browser pass on changed user flows.", + "5. Use `npm run test:e2e:ci` when a change affects shared navigation, authentication, release-critical flows, or multiple browser projects.", + "6. Use `npx playwright test --debug` for interactive debugging and trace capture.", + "7. Prefer stable selectors, explicit fixtures, and assertions about user-visible behavior.", + "8. Avoid manual sleeps. Let Playwright waiting and expectations drive synchronization.", + "9. If a bug fix needs a browser regression test, prove the failure first and then keep the test with the fix.", + "", + "Canonical workflow reference: `.ai-workflow/workflow.md`." + ], + "junie_lines": [ + "# Truckwash Frontend Development Guidelines", + "", + "This file is generated from the canonical AI workflow and is the supported Junie-facing reference for the frontend repository.", + "", + "## Build And Run", + "", + "- Setup: `npm ci` and `npx playwright install`.", + "- Start development server: `npm run dev`.", + "- Production build sanity check: `npm run build`.", + "", + "## Testing", + "", + "- Vitest unit tests: `npm run test:unit`.", + "- Playwright smoke: `npm run test:e2e:smoke`.", + "- Playwright full suite: `npm run test:e2e:ci`.", + "- Playwright debug: `npx playwright test --debug`.", + "- Browser validation is Playwright-first. Legacy browser-driver tooling is not part of the supported workflow.", + "", + "## Workflow Notes", + "", + "- Generated assistant metadata is checked with `node scripts/sync-ai-workflow.mjs --check`.", + "- Keep frontend browser verification aligned with the existing Playwright projects and runner scripts in `package.json`.", + "- Use the narrowest relevant test command first, then expand to the broader suite when the change risk requires it.", + "", + "Canonical workflow reference: `.ai-workflow/workflow.md`." + ] + } + } + }, + "assistants": { + "codex": { + "description": "Codex environment files and actions." + }, + "aiassistant": { + "description": "Project-specific AI Assistant rules." + }, + "junie": { + "description": "Project-specific Junie guidance." + }, + "copilot": { + "description": "Standardized Copilot task dispatch workflow." + } + }, + "commands": { + "setup": { + "description": "Install dependencies and prepare the supported local environment." + }, + "run": { + "description": "Start the primary local development entrypoint for the project." + }, + "test_unit": { + "description": "Run the project's narrow unit-style verification command." + }, + "test_e2e": { + "description": "Run the project's targeted browser or end-to-end verification command." + }, + "test_full": { + "description": "Run the broader high-confidence verification command when the project defines one." + }, + "debug": { + "description": "Run the supported debug entrypoint." + } + }, + "generated_outputs": [ + { + "id": "frontend_codex_environment", + "template": "codex_project_environment", + "project": "front-end-vue", + "path": ".codex/environments/environment.toml" + }, + { + "id": "frontend_aiassistant_tests", + "template": "aiassistant_frontend_tests_rule", + "project": "front-end-vue", + "path": ".aiassistant/rules/Creating and maintaining tests.md" + }, + { + "id": "frontend_junie_guidelines", + "template": "junie_frontend_guidelines", + "project": "front-end-vue", + "path": ".junie/guidelines.md" + } + ], + "sync_targets": { + "front-end-vue": { + "source_root": ".", + "destination": "C:\\Users\\2jepp\\WebstormProjects\\pleno-vue", + "supported_metadata_dirs": [ + ".codex", + ".aiassistant", + ".junie", + ".github", + ".ai-workflow", + "scripts" + ] + } + }, + "unsupported": { + "mcp": [ + "front-end-vue/.ai/mcp/mcp.json" + ] + } +} diff --git a/.ai-workflow/workflow.md b/.ai-workflow/workflow.md new file mode 100644 index 00000000..ea1f02ca --- /dev/null +++ b/.ai-workflow/workflow.md @@ -0,0 +1,74 @@ + + +# Developer AI Workflow + +This directory is the canonical source of truth for the repository's developer-facing AI workflow. + +## Goals + +- Keep Codex, AI Assistant, Junie, and Copilot aligned from one maintained source. +- Make assistant metadata deterministic so the generated files can be rewritten safely and checked in CI. +- Preserve the current mirror-repo workflow for `backend-php` and `front-end-vue`. +- Keep all changes in this workflow scoped to developer tooling and documentation. Runtime OpenAI features stay out of scope. + +## Supported Assistants + +- `Codex`: local environments and actions under `.codex/environments`. +- `AI Assistant`: generated guidance under `.aiassistant/rules`. +- `Junie`: generated project guidance under `.junie/guidelines.md`. +- `Copilot`: standardized issue-dispatch workflow content under `.github/workflows/copilot.yml`. + +## Global Rules + +1. Change only `.ai-workflow/workflow.md` and `.ai-workflow/manifest.json` when updating the developer AI workflow. +2. Regenerate all derived files with `node scripts/sync-ai-workflow.mjs --write`. +3. Validate drift with `node scripts/sync-ai-workflow.mjs --check`. +4. Generated assistant files are not hand-edited. +5. Unsupported surfaces stay unsupported until they have a real owner and a real config. +6. `front-end-vue/.ai/mcp/mcp.json` is intentionally unsupported and should not be recreated until there is an actual MCP integration to maintain. + +## Command Matrix + +### `backend-php` + +- Setup: use the existing setup scripts in `backend-php/scripts`. +- Run: start the API stack with `traefik`, `redis`, `mysql-debug`, `php1`, and `caddy`. +- Debug: tail `php1` logs. +- PHP verification: always run backend validation in the `php1` container. +- Unit tests: `docker compose exec -T php1 sh -lc "cd /var/www/html && composer test:unit"`. +- Integration tests: `docker compose exec -T php1 sh -lc "cd /var/www/html && composer test:integration"`. +- API tests: `docker compose exec -T php1 sh -lc "cd /var/www/html && composer test:api"`. + +### `front-end-vue` + +- Setup: `npm ci` and `npx playwright install`. +- Run: `npm run dev`. +- Unit tests: `npm run test:unit` with Vitest. +- Browser smoke: `npm run test:e2e:smoke`. +- Browser full suite: `npm run test:e2e:ci`. +- Debug: `npx playwright test --debug`. +- Browser validation is Playwright-first. WebdriverIO and Appium are not part of the supported workflow. + +### `automation` + +- Setup: `npm ci` and `npx playwright install`. +- Run: `npx playwright test --ui`. +- Browser tests: `npx playwright test`. +- Debug: `npx playwright test --debug`. + +## Mirror Repos And Sync + +- `backend-php` mirrors to `C:\Users\2jepp\PhpstormProjects\api`. +- `front-end-vue` mirrors to `C:\Users\2jepp\WebstormProjects\pleno-vue`. +- Supported metadata directories that must stay mirrored are `.codex`, `.aiassistant`, `.junie`, `.github`, generated `.ai-workflow`, and `scripts/sync-ai-workflow.mjs`. +- Cache and build directories remain excluded from the watcher. +- The mirror repos receive generated snapshots of `.ai-workflow` and `scripts/sync-ai-workflow.mjs` so their local CI can run `--check` without depending on the combined workspace root. + +## Generated Outputs + +- Root Codex environment for combined backend and frontend entrypoints. +- Backend and frontend Codex environments. +- Backend and frontend AI Assistant guidance. +- Backend and frontend Junie guidance. +- Backend Copilot dispatcher workflow. +- Backend and frontend snapshot copies of `.ai-workflow` plus `scripts/sync-ai-workflow.mjs` for mirrored repositories. diff --git a/.aiassistant/rules/Creating and maintaining tests.md b/.aiassistant/rules/Creating and maintaining tests.md index aaf493d7..ae8ca2d0 100644 --- a/.aiassistant/rules/Creating and maintaining tests.md +++ b/.aiassistant/rules/Creating and maintaining tests.md @@ -2,256 +2,20 @@ apply: always --- -# Guidelines for Creating and Maintaining Tests + -## When to Create Tests +# Frontend Testing Rules -Tests **MUST** be created when any of the following criteria are met: +These rules apply to Vue changes, browser flows, and frontend test updates. -1. **New Feature Development**: Any new feature, component, or functionality requires accompanying tests -2. **Bug Fixes**: When fixing a bug, write a test that reproduces the issue first, then verify the fix -3. **API Changes**: Changes to APIs, services, or data-fetching logic require updated or new tests -4. **Critical User Flows**: Any modification to authentication, payments, bookings, or POS flows -5. **Component Behavior Changes**: When modifying Vue component logic, props, or events -6. **i18n/Localization Changes**: When adding or modifying translation keys +1. Add or update tests for every new feature, bug fix, API integration change, or auth, booking, POS, or i18n workflow change. +2. Browser validation runs through Playwright. Legacy browser-driver tooling is outside the supported workflow. +3. Use `npm run test:unit` for Vitest coverage of isolated logic and component behavior. +4. Use `npm run test:e2e:smoke` for the narrow Playwright browser pass on changed user flows. +5. Use `npm run test:e2e:ci` when a change affects shared navigation, authentication, release-critical flows, or multiple browser projects. +6. Use `npx playwright test --debug` for interactive debugging and trace capture. +7. Prefer stable selectors, explicit fixtures, and assertions about user-visible behavior. +8. Avoid manual sleeps. Let Playwright waiting and expectations drive synchronization. +9. If a bug fix needs a browser regression test, prove the failure first and then keep the test with the fix. -## Running Tests When Adding Tests - -When creating or modifying tests, you **MUST**: - -1. **Run the specific test file** to verify it passes: - ```bash - npx playwright test tests/your-test-file.spec.ts - ``` - -2. **Run the full test suite** before committing to ensure no regressions: - ```bash - npx playwright test - ``` - -3. **Verify test failures before fixes**: If writing a test for a bug, confirm the test fails before applying the fix - -## Testing Framework - -This project uses **Playwright** as the primary testing framework. Playwright provides reliable end-to-end testing with auto-waiting, powerful selectors, and built-in assertions. - -## Running Tests - -```bash -# Run full test suite -npx playwright test - -# Run tests in headed mode (visible browser) -npx playwright test --headed - -# Run tests in a specific browser -npx playwright test --project=chromium -npx playwright test --project=firefox -npx playwright test --project=webkit - -# Run tests with UI mode for debugging -npx playwright test --ui - -# Generate HTML report -npx playwright show-report -``` - -## Test File Location and Naming - -- **Location**: All test files should be placed in `tests/` or `tests/specs/` -- **Naming Convention**: Use kebab-case with `.spec.ts` suffix - - ✅ `i18n-settings-wheel.spec.ts` - - ✅ `user-authentication.spec.ts` - - ❌ `UserAuth.test.js` - -## Test Structure - -### Basic Template - -```typescript -import { test, expect } from '@playwright/test'; - -/** - * Brief description of what this test suite covers - */ -test.describe('Feature or Component Name', () => { - test('should describe expected behavior', async ({ page }) => { - await page.goto('/'); - // Test implementation - await expect(page).toHaveTitle(/Expected Text/); - }); -}); -``` - -### Key Conventions - -1. **Use descriptive test names**: Start with `should` to describe expected behavior -2. **Always use `async/await`**: All Playwright interactions are asynchronous -3. **Include JSDoc comments**: Add a comment block at the top describing the test purpose -4. **Group related tests**: Use `test.describe` blocks to organize related test cases - -## Writing Tests - -### Navigation - -```typescript -// Navigate to a page -await page.goto('/'); -await page.goto('/dashboard'); - -// Wait for navigation -await page.goto('/dashboard', { waitUntil: 'networkidle' }); -``` - -### Page Interactions - -```typescript -// Fill input fields -await page.fill('input[name="username"]', 'testuser'); -await page.fill('input[name="password"]', 'password123'); - -// Click elements -await page.click('button[id="login-button"]'); - -// Wait for elements -await page.waitForSelector('input[name="2fa_code"]'); - -// Get element text -const text = await page.locator('.message').textContent(); - -// Get page title -const title = await page.title(); - -// Execute JavaScript in browser context -const result = await page.evaluate(() => { - return document.querySelector('.data')?.textContent; -}); -``` - -### Locators (Recommended) - -```typescript -// Use locators for better reliability -const submitButton = page.locator('button[type="submit"]'); -await submitButton.click(); - -// Locator with text -const loginButton = page.locator('button:has-text("Login")'); - -// Get by role (accessibility-friendly) -const heading = page.getByRole('heading', { name: 'Welcome' }); - -// Get by test ID -const element = page.getByTestId('submit-button'); -``` - -### Assertions - -```typescript -// Use Playwright's built-in expect assertions -await expect(page).toHaveURL('/dashboard'); -await expect(page).toHaveTitle(/Dashboard/); -await expect(page.locator('.message')).toBeVisible(); -await expect(page.locator('.message')).toHaveText('Success'); -await expect(page.locator('input')).toHaveValue('expected value'); -await expect(page.locator('.items')).toHaveCount(5); -``` - -## Authentication Helpers - -Create reusable authentication helpers in `tests/fixtures/`: - -```typescript -import { Page, expect } from '@playwright/test'; - -/** - * Login as a customer user - * @param page - Playwright page object - * @param credentials - User credentials - */ -export async function loginAsUser( - page: Page, - credentials: { customerNumber: string; password: string } -) { - await page.goto('/login'); - await page.fill('input[name="customer_number"]', credentials.customerNumber); - await page.fill('input[name="password"]', credentials.password); - await page.click('button[id="login-button"]'); - await expect(page).toHaveURL('/user'); -} -``` - -## Best Practices - -### DO: -- ✅ Test one behavior per `test` block -- ✅ Use meaningful variable names -- ✅ Use Playwright's auto-waiting (avoid manual waits when possible) -- ✅ Use locators for element interactions -- ✅ Use `expect` assertions with auto-retry -- ✅ Clean up test state when necessary -- ✅ Use test fixtures for setup/teardown - -### DON'T: -- ❌ Write tests that depend on execution order -- ❌ Hardcode sensitive data (use environment variables) -- ❌ Use `page.waitForTimeout()` for synchronization (use proper waits) -- ❌ Leave commented-out code in test files - -## Testing i18n/Translations - -When testing internationalization: - -1. Define expected translation keys as an array -2. Fetch locale files via page context -3. Verify all required keys exist in each locale -4. Check parity between locales (same keys in all languages) - -```typescript -import { test, expect } from '@playwright/test'; - -const requiredKeys = ['key1', 'key2', 'key3']; - -test('should have all translations in English', async ({ page }) => { - await page.goto('/'); - const result = await page.evaluate(async (keys) => { - const response = await fetch('/src/i18n/locales/en.json'); - const data = await response.json(); - const missingKeys = keys.filter(key => !(key in data)); - return { success: missingKeys.length === 0, missingKeys }; - }, requiredKeys); - expect(result.success).toBe(true); -}); -``` - -## Mobile Testing - -Playwright supports mobile emulation out of the box: - -```typescript -import { test, devices } from '@playwright/test'; - -// Use device presets -test.use({ ...devices['iPhone 13'] }); - -test('should work on mobile', async ({ page }) => { - await page.goto('/'); - // Test mobile-specific behavior -}); -``` - -Configure mobile projects in `playwright.config.ts`: - -```typescript -projects: [ - { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } }, - { name: 'Mobile Safari', use: { ...devices['iPhone 12'] } }, -] -``` - -## Maintaining Tests - -1. **Keep tests updated**: When modifying features, update corresponding tests -2. **Review failing tests**: Investigate failures before marking as skipped -3. **Remove obsolete tests**: Delete tests for removed features -4. **Refactor shared logic**: Extract common test utilities to helper files in `tests/fixtures/` +Canonical workflow reference: `.ai-workflow/workflow.md`. diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml index f53215e1..7570244e 100644 --- a/.codex/environments/environment.toml +++ b/.codex/environments/environment.toml @@ -11,14 +11,41 @@ npx playwright install [[actions]] name = "Run dev" icon = "run" -command = "npm run dev" +command = ''' +npm run dev +''' [[actions]] -name = "Run all tests" +name = "Vitest unit tests" icon = "test" -command = "npm run test:all" +command = ''' +npm run test:unit +''' [[actions]] -name = "Run debug" +name = "Playwright smoke" +icon = "test" +command = ''' +npm run test:e2e:smoke +''' + +[[actions]] +name = "Playwright full suite" +icon = "test" +command = ''' +npm run test:e2e:ci +''' + +[[actions]] +name = "Playwright debug" icon = "debug" -command = "npx playwright test --debug" +command = ''' +npx playwright test --debug +''' + +[[actions]] +name = "AI workflow check" +icon = "debug" +command = ''' +node scripts/sync-ai-workflow.mjs --check +''' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 47f8aa46..cfeacf05 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,6 +27,9 @@ jobs: node-version: 22 cache: npm + - name: Check AI workflow sync + run: node scripts/sync-ai-workflow.mjs --check + - name: Install dependencies run: npm ci --legacy-peer-deps diff --git a/.junie/guidelines.md b/.junie/guidelines.md index af397d72..acb63e0e 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -1,77 +1,27 @@ -# Project Guidelines + -## Project Overview +# Truckwash Frontend Development Guidelines -This repository contains a Vue 3 single-page application built with Vite. It powers dashboard-style pages and modules such as bookings and POS flows. The UI is primarily styled with Bulma. The app leverages libraries like FullCalendar and Chart.js for scheduling and analytics. +This file is generated from the canonical AI workflow and is the supported Junie-facing reference for the frontend repository. -- Tech stack: Vue 3, Vite 5, Vue Router, Vuex, Bulma, FullCalendar, Chart.js -- Notable integrations: toast notifications, QR/Barcode utilities, and PWA/devtools plugins -- Target: modern browsers and mobile WebView contexts +## Build And Run -Local development -- Requirements: Node.js 18+ (recommended to match Vite 5), npm 9+ -- Install dependencies: `npm install` -- Start dev server: `npm run dev` (default at http://localhost:5173) +- Setup: `npm ci` and `npx playwright install`. +- Start development server: `npm run dev`. +- Production build sanity check: `npm run build`. -Build and preview -- Production build: `npm run build` (output in `dist/`) -- Preview build locally: `npm run preview` +## Testing -Testing -- Framework: WebdriverIO (wdio) with Vite service; Appium available for mobile testing -- Common commands: - - `npm run wdio` — run test suite per `wdio.conf.js` - - `npm run test:mobile` — run tests excluding Safari - - `npm run test:android` — run Android-targeted specs -- Prerequisites for mobile tests may include Android SDK/emulator or iOS tooling and Appium drivers +- Vitest unit tests: `npm run test:unit`. +- Playwright smoke: `npm run test:e2e:smoke`. +- Playwright full suite: `npm run test:e2e:ci`. +- Playwright debug: `npx playwright test --debug`. +- Browser validation is Playwright-first. Legacy browser-driver tooling is not part of the supported workflow. -Environment configuration -- Place environment variables in `.env.*` files (Vite loads `VITE_`-prefixed vars into the client) +## Workflow Notes -See the sections below for code style and detailed project structure. +- Generated assistant metadata is checked with `node scripts/sync-ai-workflow.mjs --check`. +- Keep frontend browser verification aligned with the existing Playwright projects and runner scripts in `package.json`. +- Use the narrowest relevant test command first, then expand to the broader suite when the change risk requires it. -## Code Style and Conventions - -### Vue Components - -- Use PascalCase for component names (e.g., `UserProfile.vue`) -- Implement Single File Components (SFC) pattern -- Keep components focused and single-responsibility -- Use composition API for new components -- Document component props and events - -### JavaScript/TypeScript - -- Use ES6+ features -- Maintain consistent naming conventions (camelCase for variables/methods) -- Document complex logic with clear comments -- Use TypeScript for type safety when possible - -## Project Structure - -### Root Directory Structure - -- `/src` - Source code files - - `/assets` - Static assets (images, fonts, etc.) - - `/components` - Reusable Vue components - - `/views` - Page components - - `/router` - Vue Router configuration - - `/store` - Vuex store modules - - `/services` - API and external service integrations - - `/utils` - Utility functions and helpers - - `/styles` - Global styles and variables - -### Configuration Files - -- `vite.config.js` - Vite configuration -- `.env.*` - Environment variables -- `package.json` - Project dependencies and scripts -- `jsconfig.json` - JavaScript/TypeScript tooling and path aliases - -### File Naming Conventions - -- Components: PascalCase (e.g., `UserProfile.vue`) -- Utilities: camelCase (e.g., `formatDate.js`) -- Store modules: camelCase (e.g., `userStore.js`) -- Style files: kebab-case (e.g., `main-styles.scss`) -- Test files: `*.spec.js` or `*.test.js` +Canonical workflow reference: `.ai-workflow/workflow.md`. diff --git a/openapi.yaml b/openapi.yaml index 047c5011..3e9b9a5b 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2175,6 +2175,17 @@ paths: two_factor_enabled: type: boolean description: Indicates if 2FA is enabled for this account + runtime_config: + type: object + properties: + economic: + type: object + properties: + transaction_draft_customer_number: + type: integer + nullable: true + additionalProperties: false + additionalProperties: true '400': $ref: '#/components/responses/BadRequest' '401': @@ -12011,12 +12022,13 @@ components: type: object properties: module: { type: string, enum: [economic] } - variable: { type: string, enum: [adminFeeMonthly, adminFeeOrder, feeProductId, invoiceLayoutNumber, paymentTermsNumber] } + variable: { type: string, enum: [adminFeeMonthly, adminFeeOrder, feeProductId, invoiceLayoutNumber, paymentTermsNumber, transactionDraftCustomerNumber] } type: { type: string, enum: [string, int] } value: oneOf: - type: string - type: integer + nullable: true required: [module, variable, type, value] RecaptchaConfigEntry: diff --git a/scripts/sync-ai-workflow.mjs b/scripts/sync-ai-workflow.mjs new file mode 100644 index 00000000..08c86e87 --- /dev/null +++ b/scripts/sync-ai-workflow.mjs @@ -0,0 +1,414 @@ +#!/usr/bin/env node + +import fs from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const scriptSource = await fs.readFile(__filename, "utf8"); + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const workspaceRoot = path.resolve(args.root ?? path.join(__dirname, "..")); + const workflowPath = path.join(workspaceRoot, ".ai-workflow", "workflow.md"); + const manifestPath = path.join(workspaceRoot, ".ai-workflow", "manifest.json"); + + const workflow = await fs.readFile(workflowPath, "utf8"); + const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); + + validateManifest(manifest); + + const outputs = buildOutputs({ + manifest, + workflow, + scriptSource, + }); + + if (args.mode === "check") { + const drifted = await findDriftedOutputs(workspaceRoot, outputs); + if (drifted.length > 0) { + console.error("AI workflow outputs are out of sync:"); + for (const drift of drifted) { + console.error(`- ${drift}`); + } + process.exit(1); + } + + console.log("AI workflow outputs are in sync."); + return; + } + + const changed = await writeOutputs(workspaceRoot, outputs); + if (changed.length === 0) { + console.log("AI workflow outputs are already up to date."); + return; + } + + console.log("Updated AI workflow outputs:"); + for (const changedPath of changed) { + console.log(`- ${changedPath}`); + } +} + +function parseArgs(argv) { + const args = { + mode: null, + root: null, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + + if (arg === "--write") { + args.mode = "write"; + continue; + } + + if (arg === "--check") { + args.mode = "check"; + continue; + } + + if (arg === "--root") { + args.root = argv[index + 1]; + index += 1; + continue; + } + + throw new Error(`Unsupported argument: ${arg}`); + } + + if (!args.mode) { + throw new Error("Expected --write or --check."); + } + + return args; +} + +function validateManifest(manifest) { + const requiredKeys = ["projects", "assistants", "commands", "generated_outputs", "sync_targets"]; + for (const key of requiredKeys) { + if (!(key in manifest)) { + throw new Error(`Manifest is missing required key: ${key}`); + } + } + + const seenPaths = new Set(); + for (const output of manifest.generated_outputs) { + if (!output.path || !output.template) { + throw new Error("Each generated output must define path and template."); + } + + if (seenPaths.has(output.path)) { + throw new Error(`Duplicate generated output path: ${output.path}`); + } + + seenPaths.add(output.path); + } +} + +function buildOutputs(context) { + const outputs = []; + for (const output of context.manifest.generated_outputs) { + outputs.push({ + path: normalizeRelativePath(output.path), + content: renderTemplate(output, context), + }); + } + return outputs; +} + +function renderTemplate(output, context) { + const templateMap = { + codex_workspace_environment: () => renderWorkspaceCodexEnvironment(context.manifest), + codex_project_environment: () => renderProjectCodexEnvironment(context.manifest, output.project), + aiassistant_backend_tests_rule: () => + renderAiAssistantRule(context.manifest.projects["backend-php"].generated_content.aiassistant_tests_lines), + aiassistant_backend_routes_rule: () => + renderAiAssistantRule(context.manifest.projects["backend-php"].generated_content.aiassistant_routes_lines), + aiassistant_frontend_tests_rule: () => + renderAiAssistantRule(context.manifest.projects["front-end-vue"].generated_content.aiassistant_tests_lines), + junie_backend_guidelines: () => + renderJunieGuidelines(context.manifest.projects["backend-php"].generated_content.junie_lines), + junie_frontend_guidelines: () => + renderJunieGuidelines(context.manifest.projects["front-end-vue"].generated_content.junie_lines), + copilot_dispatcher_workflow: () => renderCopilotWorkflow(context.manifest.copilot), + workflow_snapshot: () => renderWorkflowSnapshot(context.workflow, output.project), + project_snapshot_manifest: () => renderProjectSnapshotManifest(context.manifest, output.project), + project_sync_script: () => context.scriptSource, + }; + + const renderer = templateMap[output.template]; + if (!renderer) { + throw new Error(`Unsupported template: ${output.template}`); + } + + return ensureTrailingNewline(renderer()); +} + +function renderWorkspaceCodexEnvironment(manifest) { + return renderCodexEnvironment({ + name: manifest.workspace.name, + setup: manifest.workspace.codex_environment.setup, + actions: manifest.workspace.codex_environment.actions, + manifest, + workspaceScoped: true, + }); +} + +function renderProjectCodexEnvironment(manifest, projectKey) { + const project = manifest.projects[projectKey]; + return renderCodexEnvironment({ + name: project.codex_environment.name, + setup: project.commands.setup, + actions: project.codex_environment.actions, + manifest, + projectKey, + workspaceScoped: false, + }); +} + +function renderCodexEnvironment({ name, setup, actions, manifest, projectKey = null, workspaceScoped }) { + const lines = [ + "# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY", + "version = 1", + `name = "${name}"`, + "", + ]; + + appendTomlCommandBlock(lines, "setup", setup); + + for (const action of actions) { + lines.push(""); + lines.push("[[actions]]"); + lines.push(`name = "${escapeTomlString(action.name)}"`); + lines.push(`icon = "${escapeTomlString(action.icon)}"`); + lines.push(`command = ${renderTomlMultiline(resolveActionCommand(action, manifest, projectKey, workspaceScoped))}`); + + if (action.platform) { + lines.push(`platform = "${escapeTomlString(action.platform)}"`); + } + } + + return lines.join("\n"); +} + +function appendTomlCommandBlock(lines, key, commandSpec = {}) { + lines.push(`[${key}]`); + lines.push(`script = ${renderTomlMultiline(commandSpec.default ?? "")}`); + + if (commandSpec.win32 !== undefined) { + lines.push(""); + lines.push(`[${key}.win32]`); + lines.push(`script = ${renderTomlMultiline(commandSpec.win32)}`); + } +} + +function resolveActionCommand(action, manifest, projectKey, workspaceScoped) { + if (action.literal_command) { + return action.literal_command.default ?? ""; + } + + if (!action.command_id) { + throw new Error(`Action ${action.name} is missing command_id or literal_command.`); + } + + const resolvedProjectKey = action.project ?? projectKey; + if (!resolvedProjectKey) { + throw new Error(`Action ${action.name} does not resolve to a project.`); + } + + const project = manifest.projects[resolvedProjectKey]; + const command = project.commands[action.command_id]?.default ?? ""; + + if (!workspaceScoped) { + return command; + } + + return joinCommands([`cd ${project.relative_root}`, command]); +} + +function renderAiAssistantRule(lines) { + return [ + "---", + "apply: always", + "---", + "", + "", + "", + ...lines, + ].join("\n"); +} + +function renderJunieGuidelines(lines) { + return [ + "", + "", + ...lines, + ].join("\n"); +} + +function renderCopilotWorkflow(copilot) { + const body = copilot.body_lines + .map((line) => line.replaceAll("{{ACTOR}}", "$ACTOR").replaceAll("{{TASK}}", "$TASK")) + .join("\n"); + + return [ + `name: ${copilot.workflow_name}`, + "on:", + " workflow_dispatch:", + " inputs:", + " task:", + ` description: '${copilot.input_description}'`, + " required: true", + " type: string", + "", + "jobs:", + " assign-task:", + " runs-on: ubuntu-latest", + " permissions:", + " issues: write", + " steps:", + " - name: Checkout repository", + " uses: actions/checkout@v4", + "", + " - name: Create GitHub Issue for Copilot", + " env:", + " GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}", + " TASK: ${{ github.event.inputs.task }}", + " ACTOR: ${{ github.actor }}", + " run: |", + " BODY=\"$(cat < ` ${line}`), + " EOF", + " )\"", + " gh issue create \\", + ` --title "${copilot.title_prefix} $TASK" \\`, + " --body \"$BODY\" \\", + ` --label "${copilot.issue_label}"`, + ].join("\n"); +} + +function renderWorkflowSnapshot(workflow, projectKey) { + return [ + ``, + "", + workflow.trimEnd(), + ].join("\n"); +} + +function renderProjectSnapshotManifest(manifest, projectKey) { + const snapshotManifest = buildProjectSnapshotManifest(manifest, projectKey); + return `${JSON.stringify(snapshotManifest, null, 2)}\n`; +} + +function buildProjectSnapshotManifest(manifest, projectKey) { + const project = JSON.parse(JSON.stringify(manifest.projects[projectKey])); + const prefix = `${manifest.projects[projectKey].relative_root}/`; + + project.relative_root = "."; + + const snapshotOutputs = manifest.generated_outputs + .filter((output) => output.project === projectKey) + .filter((output) => output.template !== "workflow_snapshot") + .filter((output) => output.template !== "project_snapshot_manifest") + .filter((output) => output.template !== "project_sync_script") + .map((output) => ({ + ...output, + path: normalizeRelativePath(output.path.slice(prefix.length)), + })); + + const snapshotManifest = { + "version": manifest.version, + "snapshot_of": projectKey, + "projects": { + [projectKey]: project, + }, + "assistants": manifest.assistants, + "commands": manifest.commands, + "generated_outputs": snapshotOutputs, + "sync_targets": { + [projectKey]: { + "source_root": ".", + "destination": manifest.sync_targets[projectKey]?.destination ?? "", + "supported_metadata_dirs": manifest.sync_targets[projectKey]?.supported_metadata_dirs ?? [], + }, + }, + }; + + if (projectKey === "backend-php" && manifest.copilot) { + snapshotManifest.copilot = manifest.copilot; + } + + if (projectKey === "front-end-vue" && manifest.unsupported) { + snapshotManifest.unsupported = manifest.unsupported; + } + + return snapshotManifest; +} + +async function findDriftedOutputs(workspaceRoot, outputs) { + const drifted = []; + for (const output of outputs) { + const absolutePath = path.join(workspaceRoot, output.path); + const currentContent = await readIfExists(absolutePath); + if (currentContent !== output.content) { + drifted.push(output.path); + } + } + return drifted; +} + +async function writeOutputs(workspaceRoot, outputs) { + const changed = []; + for (const output of outputs) { + const absolutePath = path.join(workspaceRoot, output.path); + const currentContent = await readIfExists(absolutePath); + if (currentContent === output.content) { + continue; + } + + await fs.mkdir(path.dirname(absolutePath), { recursive: true }); + await fs.writeFile(absolutePath, output.content, "utf8"); + changed.push(output.path); + } + return changed; +} + +async function readIfExists(filePath) { + try { + return await fs.readFile(filePath, "utf8"); + } catch (error) { + if (error && error.code === "ENOENT") { + return null; + } + throw error; + } +} + +function joinCommands(commands) { + return commands.filter((command) => command && command.trim() !== "").join("\n"); +} + +function renderTomlMultiline(value) { + return `'''\n${value ?? ""}\n'''`; +} + +function escapeTomlString(value) { + return String(value).replaceAll("\\", "\\\\").replaceAll("\"", "\\\""); +} + +function normalizeRelativePath(filePath) { + return filePath.replaceAll("\\", "/"); +} + +function ensureTrailingNewline(value) { + return value.endsWith("\n") ? value : `${value}\n`; +} + +main().catch((error) => { + console.error(error.message); + process.exit(1); +}); diff --git a/src/components/displays/boxes/WhiteBoxCard.vue b/src/components/displays/boxes/WhiteBoxCard.vue index 42bfb4e8..29e20110 100644 --- a/src/components/displays/boxes/WhiteBoxCard.vue +++ b/src/components/displays/boxes/WhiteBoxCard.vue @@ -28,6 +28,10 @@ const props = defineProps({ type: Boolean, default: false }, + footerTestId: { + type: String, + default: "" + }, hasSelectedStyle: { type: Boolean, default: false @@ -103,8 +107,8 @@ watch(() => props.forceState, (newVal) => { - diff --git a/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2LastOrder.vue b/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2LastOrder.vue index a90226c5..538f3bbb 100644 --- a/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2LastOrder.vue +++ b/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2LastOrder.vue @@ -1,179 +1,235 @@ + diff --git a/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Product.vue b/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Product.vue index bf46a6c3..573bc813 100644 --- a/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Product.vue +++ b/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Product.vue @@ -1,148 +1,188 @@ diff --git a/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupRenderer.vue b/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupRenderer.vue index b6b50060..0b493649 100644 --- a/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupRenderer.vue +++ b/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupRenderer.vue @@ -1,4 +1,5 @@