From 78648a0b7f74c5aaec2264552d48f86a511ce03b Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Thu, 9 Apr 2026 11:35:18 +0200 Subject: [PATCH] Add customer complaints, edge gateway terminal, and mobile POS functionalities with e2e and unit test coverage: - Implemented `departmentDailyReportComplaintCustomers.js` module for handling customer complaint data, including search, normalization, and API integration. - Added `EdgeGatewayTerminal.vue` for root shell access with session approval, dynamic status handling, and user feedback. - Developed `mobilePos.js` for simulating mobile POS fixtures and workflows in e2e tests. - Introduced relevant e2e and unit tests to ensure functionality and error handling across modules. --- openapi.yaml | 720 +++++++- package-lock.json | 17 +- package.json | 5 +- src/assets/pos.css | 502 ++++++ .../pos/PosLastScannedLicensePlatesV2.vue | 103 +- .../department/pos/PosOrderItemsCurrent.vue | 673 +++++-- .../department/pos/PosSelectedCustomer.vue | 219 ++- .../department/pos/order/POSOrderNote.vue | 214 +-- .../pos/order/POSOrderReference.vue | 211 +-- .../pos/order/PosOrderLicensePlates.vue | 110 +- .../pos/steps/PosDepartmentStep1.vue | 57 +- .../pos/steps/PosDepartmentStep2.vue | 14 +- .../pos/steps/PosDepartmentStep3.vue | 17 +- .../pos/steps/PosDepartmentStep4.vue | 36 +- .../steps/mobile/PosDepartmentStepMobile1.vue | 56 +- .../steps/mobile/PosDepartmentStepMobile2.vue | 77 +- ...tepMobile1RegistrationNumberInputField.vue | 7 +- ...osDepartmentStepMobile2AdditionalItems.vue | 35 +- .../PosDepartmentStepMobile2Addons.vue | 4 +- .../PosDepartmentStepMobile2Categories.vue | 4 +- ...osDepartmentStepMobile2CategoryProduct.vue | 8 +- .../PosDepartmentStepMobile2LastOrder.vue | 4 +- .../PosDepartmentStepMobile2Products.vue | 1 + .../PosDepartmentStepMobileAttachments.vue | 20 +- .../PosDepartmentStepMobileButtonClearAll.vue | 3 +- .../PosDepartmentStepMobileButtonNextStep.vue | 50 +- ...DepartmentStepMobileFixedBottomControl.vue | 64 +- ...partmentStepMobilePopupCompleteBooking.vue | 6 +- .../PosDepartmentStepMobilePopupRenderer.vue | 4 +- ...epartmentStepMobilePopupSelectCustomer.vue | 6 +- .../objects/PosDepartmentStepMobileFlow.vue | 16 +- ...osDepartmentStep1MobileChangeReference.vue | 3 +- .../PosDepartmentStep1MobileManualInput.vue | 15 +- ...sDepartmentStep2MobileVehicleSelection.vue | 4 +- .../edgeGateway/EdgeGatewayTerminal.vue | 500 ++++++ .../edgeGateway/EdgeGatewayWorkspace.vue | 300 ++-- .../department/pos/SelectVehicleFormPOS.vue | 156 +- .../search/economic/customerSearchField.vue | 11 +- .../DepartmentDailyReportComplaints.vue | 372 +++- .../SessionUser/Objects/ObjectsGlobal.vue | 20 +- .../token/SessionUser/Objects/Orders.vue | 40 +- .../elements/controls/ControlSelectAmount.vue | 10 +- .../controls/fields/ControlFieldInput.vue | 10 +- .../search/ControlFieldInputSearchResults.vue | 10 +- .../scanner/graphics/ScannerInstructions.vue | 26 +- src/composables/useEdgeGatewayTerminal.js | 491 +++++ src/i18n/locales/da.json | 27 + src/i18n/locales/de.json | 27 + src/i18n/locales/en.json | 27 + src/i18n/locales/no.json | 27 + src/i18n/locales/sv.json | 27 + src/main.js | 2 + src/services/AutoTableExportService.js | 19 +- ...departmentDailyReportComplaintCustomers.js | 75 + .../modules/Pos/DepartmentPosOrder.vue | 644 +++++-- .../modules/Pos/posRouteState.js | 19 +- ...partmentDashboardDailyReportComplaints.vue | 312 +++- ...partmentDashboardDailyReportNavigation.vue | 72 +- tests/e2e/admin-daily-report.spec.ts | 68 +- tests/e2e/admin-department-visibility.spec.ts | 27 +- tests/e2e/admin-pos-orders.spec.ts | 287 ++- .../e2e/adminModulePosMobileOrderFlow.spec.ts | 499 +---- tests/e2e/edge-gateways.smoke.spec.js | 84 +- tests/e2e/pos-flow.spec.js | 37 +- tests/e2e/pos-mobile-card-payments.spec.js | 979 +--------- tests/e2e/pos-mobile-order-flow.spec.js | 797 ++++++++ tests/e2e/pos.visual.spec.js | 266 +++ ...os-mobile-step-2-chromium-mobile-win32.png | Bin 0 -> 28234 bytes ...os-mobile-step-3-chromium-mobile-win32.png | Bin 0 -> 38851 bytes ...-detail-desktop-chromium-desktop-win32.png | Bin 0 -> 47349 bytes ...ustomer-desktop-chromium-desktop-win32.png | Bin 0 -> 52966 bytes ...-step-1-desktop-chromium-desktop-win32.png | Bin 0 -> 26421 bytes .../e2e/superuser-customer-complaints.spec.ts | 37 +- tests/e2e/support/mobilePos.js | 1599 +++++++++++++++++ tests/e2e/support/network.js | 1024 ++++++++++- ...department-daily-report-complaints.spec.js | 59 +- tests/unit/edge-gateway-workspace.spec.js | 26 +- tests/unit/use-edge-gateway-terminal.spec.js | 255 +++ 78 files changed, 10019 insertions(+), 2534 deletions(-) create mode 100644 src/assets/pos.css create mode 100644 src/components/displays/edgeGateway/EdgeGatewayTerminal.vue create mode 100644 src/composables/useEdgeGatewayTerminal.js create mode 100644 src/services/departmentDailyReportComplaintCustomers.js create mode 100644 tests/e2e/pos-mobile-order-flow.spec.js create mode 100644 tests/e2e/pos.visual.spec.js create mode 100644 tests/e2e/pos.visual.spec.js-snapshots/pos-mobile-step-2-chromium-mobile-win32.png create mode 100644 tests/e2e/pos.visual.spec.js-snapshots/pos-mobile-step-3-chromium-mobile-win32.png create mode 100644 tests/e2e/pos.visual.spec.js-snapshots/pos-order-detail-desktop-chromium-desktop-win32.png create mode 100644 tests/e2e/pos.visual.spec.js-snapshots/pos-step-1-customer-desktop-chromium-desktop-win32.png create mode 100644 tests/e2e/pos.visual.spec.js-snapshots/pos-step-1-desktop-chromium-desktop-win32.png create mode 100644 tests/e2e/support/mobilePos.js create mode 100644 tests/unit/use-edge-gateway-terminal.spec.js diff --git a/openapi.yaml b/openapi.yaml index 1f4a960c..41a53da2 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -901,14 +901,16 @@ paths: summary: Process inbound Bird voice call lifecycle operationId: birdInboundVoiceCallWebhook description: > - Stateful inbound-call webhook that answers the call immediately, tracks DTMF input, and enforces a timeout hangup flow. - During the first 300 seconds from call start, DTMF input is extracted from fixed payload fields - (`dtmf`, `digit`, `digits`, `keys`, and known nested variants). A Slack message is sent whenever any DTMF - value is captured, the call acknowledges input, and gather is re-issued with retry-loop semantics - checking for input every 2 seconds until a digit is entered. At or after 300 seconds, the webhook - says `timeout reached`, waits 10 - seconds, sends a hangup command once, and polls Bird call status until terminal. Call lifecycle - state is only finalized and cleared after terminal status is observed. + Stateful inbound-call webhook that answers the call immediately, runs a DTMF-driven IVR for + phone-controlled entrance and exit gates, and enforces a timeout hangup flow. During the + first 300 seconds from call start, DTMF input is extracted from payload fields such as + `dtmf`, `digit`, `digits`, `keys`, `result.keys`, and nested `conditions[].value`. Values + like `1#` are normalized to a single menu digit before the route resolves the department + selection and then the gate type selection (`1=entrance`, `2=exit`). When a valid gate is + resolved, the webhook triggers the corresponding `department_gates` phone-call gate, announces + the result, sends a hangup command, and keeps the completed state cached until Bird reports a + terminal call status. At or after 300 seconds, the webhook says `timeout reached`, waits 10 + seconds, sends a hangup command once, and polls Bird call status until terminal. parameters: - in: query name: workspaceId @@ -9878,6 +9880,33 @@ paths: '403': $ref: '#/components/responses/Forbidden' + /superuser/system/status: + get: + tags: + - Superuser + summary: Aggregated system status snapshot + description: Returns a read-only snapshot of runtime health, dependency connectivity, module configuration/probe status, and active user session activity for the superuser dashboard. + operationId: getSuperuserSystemStatus + parameters: + - in: query + name: force + required: false + schema: + type: boolean + default: false + description: Bypass cached external module probes for this request. + responses: + '200': + description: System status snapshot returned successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuperuserSystemStatusResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + # Configuration Endpoints /economic/config: get: @@ -10888,6 +10917,34 @@ paths: application/json: schema: {} + /departments/daily-reports/overview: + get: + tags: + - Departments + summary: Get daily report overview + operationId: getDailyReportOverview + parameters: + - name: date + in: query + required: true + schema: {type: string} + - name: date_to + in: query + required: false + schema: {type: string} + - name: department_ids + in: query + required: true + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/DepartmentDailyReportOverviewResponse' + /departments/daily-reports/get: get: tags: @@ -10910,6 +10967,127 @@ paths: application/json: schema: {} + /departments/daily-reports/complaints: + get: + tags: + - Departments + summary: List or fetch daily report customer complaints + operationId: listDailyReportComplaints + parameters: + - name: id + in: query + required: false + schema: {type: integer} + - name: page + in: query + required: false + schema: {type: integer} + - name: limit + in: query + required: false + schema: {type: integer} + - name: search + in: query + required: false + schema: {type: string} + - name: filters + in: query + required: false + schema: {type: string} + - name: order + in: query + required: false + schema: {type: string} + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/DepartmentDailyReportComplaintCollectionResponse' + post: + tags: + - Departments + summary: Create daily report customer complaint + operationId: createDailyReportComplaint + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DepartmentDailyReportComplaintCreateRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/DepartmentDailyReportComplaintResponse' + put: + tags: + - Departments + summary: Update daily report customer complaint + operationId: updateDailyReportComplaint + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DepartmentDailyReportComplaintUpdateRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/DepartmentDailyReportComplaintResponse' + delete: + tags: + - Departments + summary: Delete daily report customer complaint + operationId: deleteDailyReportComplaint + parameters: + - name: id + in: query + required: true + schema: {type: integer} + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/DepartmentDailyReportComplaintDeleteResponse' + + /departments/daily-reports/complaints/customers: + get: + tags: + - Departments + summary: Search selectable customers for daily report complaints + operationId: searchDailyReportComplaintCustomers + parameters: + - name: search + in: query + required: true + schema: + type: string + minLength: 2 + - name: limit + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 20 + default: 10 + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/DepartmentDailyReportComplaintCustomerSearchResponse' + /departments/daily-reports/product-count: get: tags: @@ -10964,7 +11142,36 @@ paths: description: Success content: application/json: - schema: {} + schema: + $ref: '#/components/schemas/DepartmentDailyReportTransactionCountResponse' + + /departments/daily-reports/outside-hours-trend: + get: + tags: + - Departments + summary: Get outside-hours trend for daily reports + operationId: getDailyReportOutsideHoursTrend + parameters: + - name: date + in: query + required: true + schema: {type: string} + - name: date_to + in: query + required: true + schema: {type: string} + - name: department_ids + in: query + required: true + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendResponse' /departments/daily-reports/bookings-count: get: @@ -11205,6 +11412,245 @@ components: type: integer description: HTTP status code + SuperuserSystemStatusResponse: + type: object + properties: + success: + type: boolean + data: + $ref: '#/components/schemas/SuperuserSystemStatusPayload' + meta: + type: object + additionalProperties: true + includes: + type: object + additionalProperties: true + required: + - success + - data + - meta + - includes + + SuperuserSystemStatusPayload: + type: object + properties: + overall_status: + $ref: '#/components/schemas/SuperuserSystemStatusEnum' + generated_at: + type: string + format: date-time + refresh_after_seconds: + type: integer + runtime: + type: object + properties: + cpu: + $ref: '#/components/schemas/SuperuserRuntimeMetric' + memory: + $ref: '#/components/schemas/SuperuserRuntimeMetric' + disk: + $ref: '#/components/schemas/SuperuserRuntimeMetric' + dependencies: + type: object + properties: + database: + $ref: '#/components/schemas/SuperuserDependencyStatus' + redis: + $ref: '#/components/schemas/SuperuserDependencyStatus' + minio: + $ref: '#/components/schemas/SuperuserMinioDependencyStatus' + modules: + type: array + items: + $ref: '#/components/schemas/SuperuserModuleStatus' + sessions: + $ref: '#/components/schemas/SuperuserSessionStatus' + warnings: + type: array + items: + type: string + required: + - overall_status + - generated_at + - refresh_after_seconds + - runtime + - dependencies + - modules + - sessions + - warnings + + SuperuserSystemStatusEnum: + type: string + enum: [ok, degraded, down] + + SuperuserModuleStatusEnum: + type: string + enum: [disabled, not_configured, configured, ok, degraded, down] + + SuperuserRuntimeMetric: + type: object + properties: + status: + $ref: '#/components/schemas/SuperuserSystemStatusEnum' + usage_percent: + type: number + format: float + nullable: true + used_bytes: + type: integer + nullable: true + free_bytes: + type: integer + nullable: true + total_bytes: + type: integer + nullable: true + path: + type: string + nullable: true + source: + type: string + nullable: true + checked_at: + type: string + format: date-time + + SuperuserDependencyStatus: + type: object + properties: + status: + $ref: '#/components/schemas/SuperuserSystemStatusEnum' + latency_ms: + type: number + format: float + nullable: true + database: + oneOf: + - type: integer + - type: string + nullable: true + server_version: + type: string + nullable: true + http_status: + type: integer + nullable: true + checked_at: + type: string + format: date-time + error: + type: string + nullable: true + + SuperuserMinioDependencyStatus: + type: object + properties: + status: + $ref: '#/components/schemas/SuperuserSystemStatusEnum' + latency_ms: + type: number + format: float + nullable: true + endpoint: + type: string + nullable: true + http_status: + type: integer + nullable: true + buckets: + type: array + items: + type: object + properties: + name: + type: string + status: + type: string + error: + type: string + nullable: true + checked_at: + type: string + format: date-time + error: + type: string + nullable: true + + SuperuserModuleStatus: + type: object + properties: + key: + type: string + enabled: + type: boolean + configured: + type: boolean + probe_supported: + type: boolean + status: + $ref: '#/components/schemas/SuperuserModuleStatusEnum' + status_reason: + type: string + nullable: true + checked_at: + type: string + format: date-time + required: + - key + - enabled + - configured + - probe_supported + - status + - checked_at + + SuperuserSessionStatus: + type: object + properties: + active_window_minutes: + type: integer + active_users: + type: integer + active_sessions: + type: integer + recent_sessions: + type: array + items: + type: object + properties: + session_kind: + type: string + principal_id: + type: integer + display_name: + type: string + context_label: + type: string + nullable: true + customer_number_context: + type: integer + nullable: true + device_type: + type: string + user_agent: + type: string + last_route: + type: string + first_seen_at: + type: string + format: date-time + nullable: true + last_seen_at: + type: string + format: date-time + nullable: true + active: + type: boolean + required: + - active_window_minutes + - active_users + - active_sessions + - recent_sessions + SystemSearchEntityType: type: string enum: @@ -16020,16 +16466,20 @@ components: example: ongoing dtmf: type: string - description: DTMF value when present + description: DTMF value when present, for example `1` or `1#` example: "5" digit: type: string - description: Alternate DTMF field + description: Alternate DTMF field, also accepts values such as `1#` example: "5" digits: type: string description: Alternate DTMF field example: "5" + keys: + type: string + description: Alternate DTMF field returned by gather results + example: "1#" call: type: object additionalProperties: true @@ -16053,7 +16503,11 @@ components: properties: phase: type: string - enum: [lock_not_acquired, input_window, timeout_window, terminal_completion] + enum: [lock_not_acquired, input_window, timeout_window, completed, terminal_completion] + stage: + type: string + nullable: true + enum: [department_select, gate_type_select, completed] call_id: type: string example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09" @@ -16074,6 +16528,19 @@ components: last_input: type: string nullable: true + selected_department_id: + type: integer + nullable: true + selected_gate_type: + type: string + nullable: true + enum: [entrance, exit] + gate_id: + type: integer + nullable: true + gate_opened: + type: boolean + nullable: true answered_at: type: integer nullable: true @@ -16516,4 +16983,233 @@ components: additionalProperties: true example: [] + DepartmentDailyReportOutsideHoursBreakdown: + type: object + properties: + orders: { type: integer } + xlvask: { type: integer } + selfserve: { type: integer } + + DepartmentDailyReportOutsideHoursSummary: + type: object + properties: + department_ids: + type: array + items: { type: integer } + date: { type: string } + date_to: { type: string } + total: { type: integer } + by_source: + $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursBreakdown' + has_missing_opening_hours: { type: boolean } + missing_department_ids: + type: array + items: { type: integer } + + DepartmentDailyReportMetric: + type: object + properties: + state: { type: string } + value: + type: number + nullable: true + out_of: + type: number + nullable: true + message: + type: string + nullable: true + by_source: + $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursBreakdown' + has_missing_opening_hours: { type: boolean } + missing_department_ids: + type: array + items: { type: integer } + + DepartmentDailyReportComplaintCreateRequest: + type: object + required: [department_id, description] + properties: + department_id: { type: integer } + customer_number: + type: integer + nullable: true + description: + type: string + minLength: 1 + maxLength: 4000 + + DepartmentDailyReportComplaintUpdateRequest: + type: object + required: [id] + properties: + id: { type: integer } + department_id: { type: integer } + customer_number: + type: integer + nullable: true + description: + type: string + minLength: 1 + maxLength: 4000 + + DepartmentDailyReportComplaintCustomerSearchResult: + type: object + properties: + customer_number: + type: integer + customer_name: + type: string + nullable: true + + DepartmentDailyReportComplaint: + type: object + properties: + id: { type: integer } + department_id: { type: integer } + department_name: + type: string + nullable: true + customer_number: + type: integer + nullable: true + customer_name: + type: string + nullable: true + description: { type: string } + created_by: { type: integer } + created_by_name: + type: string + nullable: true + created_at: + type: string + format: date-time + + DepartmentDailyReportComplaintResponse: + type: object + properties: + success: { type: boolean, example: true } + data: + $ref: '#/components/schemas/DepartmentDailyReportComplaint' + + DepartmentDailyReportComplaintCollectionResponse: + type: object + properties: + success: { type: boolean, example: true } + data: + oneOf: + - $ref: '#/components/schemas/DepartmentDailyReportComplaint' + - type: array + items: + $ref: '#/components/schemas/DepartmentDailyReportComplaint' + + DepartmentDailyReportComplaintCustomerSearchResponse: + type: object + properties: + success: { type: boolean, example: true } + data: + type: array + items: + $ref: '#/components/schemas/DepartmentDailyReportComplaintCustomerSearchResult' + + DepartmentDailyReportComplaintDeleteResponse: + type: object + properties: + success: { type: boolean, example: true } + data: + type: object + properties: + message: { type: string } + + DepartmentDailyReportProductTile: + type: object + properties: + product_id: { type: integer } + slug: { type: string } + title: { type: string } + state: { type: string } + value: { type: integer } + out_of: { type: integer } + + DepartmentDailyReportOverviewPayload: + type: object + properties: + department_ids: + type: array + items: { type: integer } + date: { type: string } + date_to: { type: string } + metrics: + type: object + additionalProperties: + $ref: '#/components/schemas/DepartmentDailyReportMetric' + products: + type: array + items: + $ref: '#/components/schemas/DepartmentDailyReportProductTile' + + DepartmentDailyReportOverviewResponse: + type: object + properties: + success: { type: boolean, example: true } + data: + $ref: '#/components/schemas/DepartmentDailyReportOverviewPayload' + + DepartmentDailyReportTransactionCountPayload: + type: object + properties: + quantity: { type: integer } + products: { type: integer } + earnings: { type: integer } + washes: { type: integer } + water_usage: { type: integer } + date: { type: string } + date_to: { type: string } + department_id: { type: integer } + outside_hours: + $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursSummary' + + DepartmentDailyReportTransactionCountResponse: + type: object + properties: + success: { type: boolean, example: true } + data: + $ref: '#/components/schemas/DepartmentDailyReportTransactionCountPayload' + + DepartmentDailyReportOutsideHoursTrendPoint: + type: object + properties: + date: { type: string } + total: { type: integer } + by_source: + $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursBreakdown' + has_missing_opening_hours: { type: boolean } + missing_department_ids: + type: array + items: { type: integer } + + DepartmentDailyReportOutsideHoursTrendPayload: + type: object + properties: + department_ids: + type: array + items: { type: integer } + date: { type: string } + date_to: { type: string } + points: + type: array + items: + $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendPoint' + has_missing_opening_hours: { type: boolean } + missing_department_ids: + type: array + items: { type: integer } + + DepartmentDailyReportOutsideHoursTrendResponse: + type: object + properties: + success: { type: boolean, example: true } + data: + $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendPayload' + diff --git a/package-lock.json b/package-lock.json index b4e40fe5..47c8b384 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@vueuse/core": "^12.7.0", "@vueuse/head": "^2.0.0", "@vueuse/integrations": "^13.7.0", + "@xterm/addon-fit": "^0.11.0", "animate.css": "^4.1.1", "apexcharts": "^5.10.4", "axios": "1.13.5", @@ -56,7 +57,8 @@ "vue3-apexcharts": "^1.11.1", "vue3-cookies": "^1.0.6", "vuex": "^4.1.0", - "xlsx": "^0.18.5" + "xlsx": "^0.18.5", + "xterm": "^5.3.0" }, "devDependencies": { "@creativebulma/bulma-divider": "^1.1.0", @@ -5296,6 +5298,12 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -15071,6 +15079,13 @@ "node": ">=0.4" } }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", + "license": "MIT" + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index fb7b6494..e9f37683 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "test:e2e": "playwright test", "test:e2e:ci": "playwright test --reporter=line,html", "test:e2e:smoke": "playwright test --grep @smoke --project=chromium-desktop --project=chromium-mobile", + "test:e2e:pos-mobile-live": "node -e \"const { spawnSync } = require('child_process'); const result = spawnSync('npx', ['playwright', 'test', 'tests/e2e/adminModulePosMobileOrderFlow.spec.ts', '--project=chromium-mobile'], { stdio: 'inherit', shell: true, env: { ...process.env, PLAYWRIGHT_LIVE: '1' } }); process.exit(result.status ?? 1);\"", "test:all": "npm run test:unit && npm run test:e2e:smoke", "twa:build": "bubblewrap build", "twa:update": "bubblewrap update" @@ -33,6 +34,7 @@ "@vueuse/core": "^12.7.0", "@vueuse/head": "^2.0.0", "@vueuse/integrations": "^13.7.0", + "@xterm/addon-fit": "^0.11.0", "animate.css": "^4.1.1", "apexcharts": "^5.10.4", "axios": "1.13.5", @@ -65,7 +67,8 @@ "vue3-apexcharts": "^1.11.1", "vue3-cookies": "^1.0.6", "vuex": "^4.1.0", - "xlsx": "^0.18.5" + "xlsx": "^0.18.5", + "xterm": "^5.3.0" }, "devDependencies": { "@creativebulma/bulma-divider": "^1.1.0", diff --git a/src/assets/pos.css b/src/assets/pos.css new file mode 100644 index 00000000..c0591192 --- /dev/null +++ b/src/assets/pos.css @@ -0,0 +1,502 @@ +:root { + --pos-shell-gap: 1.25rem; + --pos-card-padding: 1.25rem; + --pos-card-radius: 1rem; + --pos-card-border: #dce5f1; + --pos-card-shadow: 0 14px 36px rgba(6, 54, 81, 0.08); + --pos-muted: #6b7a90; +} + +.pos-shell { + display: grid; + gap: var(--pos-shell-gap); + align-items: start; + grid-template-columns: minmax(0, 1fr) minmax(320px, 380px); +} + +.pos-main, +.pos-rail { + min-width: 0; +} + +.pos-rail { + display: flex; + flex-direction: column; + gap: var(--pos-shell-gap); +} + +.pos-shell-actions { + display: grid; + grid-column: 1 / -1; + gap: var(--pos-shell-gap); + align-items: start; + grid-template-columns: minmax(0, 1fr) minmax(320px, 380px); +} + +.pos-shell-actions__rail { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +@media screen and (min-width: 1024px) { + .pos-rail--sticky { + position: sticky; + top: 1rem; + } +} + +@media screen and (max-width: 1023px) { + .pos-shell { + grid-template-columns: minmax(0, 1fr); + } + + .pos-shell-actions { + grid-template-columns: minmax(0, 1fr); + } +} + +.box.has-sharp-edges.pos-card { + padding: var(--pos-card-padding); + border-radius: var(--pos-card-radius); + border: 1px solid var(--pos-card-border); + box-shadow: var(--pos-card-shadow); + background: #ffffff; +} + +.box.has-sharp-edges.pos-card.pos-card--flush { + padding: 0; + overflow: hidden; +} + +.box.has-sharp-edges.pos-card.pos-card--compact { + padding: 1rem; +} + +.pos-card-tabs .tabs { + margin: 0; + padding: 0.85rem 1rem 0.35rem; + border-bottom: 1px solid #e6edf7; +} + +.pos-card-tabs .tabs ul { + gap: 0.35rem; + flex-wrap: wrap; + border-bottom: none; +} + +.pos-card-tabs .tabs li a { + min-height: 2.4rem; + padding: 0.45rem 0.9rem; + border: none; + border-radius: 999px; + color: #56657a; + font-weight: 600; +} + +.pos-card-tabs .tabs li.is-active a { + background: #eef4ff; + color: #21446d; +} + +.buttons.pos-actions { + margin: 0; + gap: 0.75rem; +} + +.buttons.pos-actions > * { + margin: 0; + min-width: 0; +} + +.buttons.pos-actions .button { + margin: 0; +} + +.buttons.pos-actions.pos-actions--desktop { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); +} + +.buttons.pos-actions.pos-actions--stacked, +.buttons.pos-actions.pos-actions--rail { + display: grid; + grid-template-columns: minmax(0, 1fr); +} + +.auto-excel-export-inline, +[data-auto-excel-export-button="1"] { + display: flex; + align-items: center; +} + +.auto-excel-export-inline .button, +[data-auto-excel-export-button="1"] .button { + margin: 0; +} + +.pos-inline-tags { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; +} + +.pos-inline-tag { + display: inline-flex; + align-items: center; + padding: 0.35rem 0.7rem; + border-radius: 999px; + background: #f3f7fc; + color: #43566f; + font-size: 0.78rem; + font-weight: 600; + line-height: 1; +} + +.pos-desktop-step { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.pos-order-metadata-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.pos-order-metadata-card { + display: flex; + flex-direction: column; + gap: 0.75rem; + min-width: 0; + min-height: 100%; + padding: 1rem; + border: 1px solid #dce5f1; + border-radius: 0.95rem; + background: linear-gradient(180deg, #fbfdff 0%, #f6faff 100%); +} + +.pos-order-metadata-card--full { + grid-column: 1 / -1; +} + +.pos-order-metadata-label { + margin: 0; + color: #607086; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.pos-order-field { + display: flex; + flex-direction: column; + gap: 0.5rem; + min-width: 0; +} + +.pos-order-field__control { + width: 100%; + min-height: 3.25rem; + padding: 0.8rem 0.95rem; + border: 1px solid #d7e2ef; + border-radius: 0.85rem; + background: #ffffff; + color: #223347; + font-size: 0.95rem; + line-height: 1.4; + text-align: left; + transition: border-color 120ms ease, box-shadow 120ms ease; +} + +.pos-order-field__control--interactive { + cursor: text; + display: flex; + align-items: center; + justify-content: flex-start; +} + +.pos-order-field__control--interactive:hover, +.pos-order-field__control--interactive:focus-visible { + border-color: #93b3d8; + box-shadow: 0 0 0 3px rgba(93, 142, 191, 0.12); + outline: none; +} + +.pos-order-field__control--empty { + color: #8ea0b6; +} + +.pos-order-field__control--editing { + padding: 0; +} + +.pos-order-field__preview { + flex: 1 1 auto; + min-width: 0; + display: -webkit-box; + overflow: hidden; + white-space: pre-wrap; + word-break: break-word; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.pos-order-field__empty-state { + display: flex; + align-items: center; + width: 100%; + min-width: 0; +} + +.pos-order-field__empty-pill { + display: inline-flex; + align-items: center; + max-width: 100%; + padding: 0.32rem 0.7rem; + border-radius: 999px; + background: #f3f6fa; + color: #4c5f76; + font-size: 0.79rem; + font-weight: 700; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.pos-order-note-editor .pos-order-field__control { + min-height: 7.25rem; +} + +.pos-order-note-editor .pos-order-field__control--interactive { + align-items: flex-start; +} + +.pos-order-note-editor .pos-order-field__preview { + -webkit-line-clamp: 5; +} + +.pos-order-note-editor__empty { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 0.55rem; + width: 100%; + min-width: 0; + min-height: 100%; +} + +.pos-order-note-editor__empty-copy { + color: #7a8ba1; + font-size: 0.92rem; + line-height: 1.35; +} + +.pos-order-field__input { + width: 100%; + min-height: 3.25rem; + resize: vertical; + border: 0; + box-shadow: none; + background: transparent; + color: inherit; + padding: 0.8rem 0.95rem; +} + +.pos-order-note-editor .pos-order-field__input { + min-height: 7.25rem; +} + +.pos-order-field__input:focus { + outline: none; + box-shadow: none; +} + +.pos-registration-grid { + display: grid; + gap: 0.75rem; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.pos-registration-slot { + min-width: 0; +} + +.pos-registration-field { + display: flex; + flex-direction: column; + gap: 0.35rem; + width: 100%; + min-height: 4.25rem; + padding: 0.75rem 0.9rem; + border: 1px solid #d7e2ef; + border-radius: 0.85rem; + background: #ffffff; + color: #223347; + text-align: left; + transition: border-color 120ms ease, box-shadow 120ms ease, transform 120ms ease; +} + +.pos-registration-field:hover, +.pos-registration-field:focus-visible { + border-color: #93b3d8; + box-shadow: 0 0 0 3px rgba(93, 142, 191, 0.12); + outline: none; + transform: translateY(-1px); +} + +.pos-registration-field--empty { + color: #8ea0b6; +} + +.pos-registration-field__label { + color: #607086; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.pos-registration-field__value { + min-width: 0; + font-weight: 600; + line-height: 1.35; + word-break: break-word; +} + +.pos-registration-field--add { + border-style: dashed; + background: #fbfcfe; +} + +.pos-registration-field--add:hover, +.pos-registration-field--add:focus-visible { + border-color: #93b3d8; + box-shadow: 0 0 0 3px rgba(93, 142, 191, 0.12); + background: #fbfdff; + outline: none; + transform: none; +} + +.pos-registration-field__value--muted { + color: #63768f; +} + +.pos-order-items--order-detail .pos-order-metadata-grid { + gap: 0.9rem; + grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.9fr); +} + +.pos-order-items--order-detail .pos-order-metadata-card { + gap: 0.65rem; + padding: 0.9rem 0.95rem; + border-color: #e2e8f0; + border-radius: 0.9rem; + background: linear-gradient(180deg, #ffffff 0%, #fafbfd 100%); + box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04); +} + +.pos-order-items--order-detail .pos-order-metadata-label { + color: #738397; + font-size: 0.68rem; + letter-spacing: 0.08em; +} + +.pos-order-items--order-detail .pos-order-field__control { + min-height: 2.85rem; + padding: 0.65rem 0.8rem; + border-color: #dbe3eb; + border-radius: 0.8rem; + box-shadow: none; +} + +.pos-order-items--order-detail .pos-order-field__empty-pill { + background: #f4f6f9; + color: #445466; +} + +.pos-order-items--order-detail .pos-order-field__input { + min-height: 2.85rem; + padding: 0.65rem 0.8rem; +} + +.pos-order-items--order-detail .pos-order-reference-editor .pos-order-field__preview { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.pos-order-items--order-detail .pos-order-note-editor .pos-order-field__control { + min-height: 5rem; +} + +.pos-order-items--order-detail .pos-order-note-editor .pos-order-field__preview { + -webkit-line-clamp: 3; +} + +.pos-order-items--order-detail .pos-order-note-editor .pos-order-field__input { + min-height: 5rem; +} + +.pos-order-items--order-detail .pos-order-note-editor__empty { + gap: 0.35rem; +} + +.pos-order-items--order-detail .pos-order-note-editor__empty-copy { + font-size: 0.88rem; +} + +.pos-order-items--order-detail .pos-registration-grid { + gap: 0.55rem; +} + +.pos-order-items--order-detail .pos-registration-field { + min-height: 3.35rem; + padding: 0.62rem 0.78rem; + border-color: #dbe3eb; + border-radius: 0.8rem; + background: #ffffff; + box-shadow: none; +} + +.pos-order-items--order-detail .pos-registration-field:hover, +.pos-order-items--order-detail .pos-registration-field:focus-visible { + transform: none; + background: #fcfdff; +} + +.pos-order-items--order-detail .pos-registration-field__label { + color: #738397; + font-size: 0.68rem; + letter-spacing: 0.08em; +} + +.pos-order-items--order-detail .pos-registration-field__value { + font-size: 0.95rem; +} + +.pos-order-items--order-detail .pos-registration-field--add { + border-color: #d2dbe5; +} + +@media screen and (max-width: 768px) { + .box.has-sharp-edges.pos-card { + padding: 1rem; + border-radius: 0.9rem; + } + + .pos-card-tabs .tabs { + padding: 0.75rem 0.75rem 0.35rem; + } + + .pos-order-metadata-grid, + .pos-registration-grid { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/src/components/displays/department/pos/PosLastScannedLicensePlatesV2.vue b/src/components/displays/department/pos/PosLastScannedLicensePlatesV2.vue index e9680b03..8a8d40ff 100644 --- a/src/components/displays/department/pos/PosLastScannedLicensePlatesV2.vue +++ b/src/components/displays/department/pos/PosLastScannedLicensePlatesV2.vue @@ -577,24 +577,21 @@ const isDropdownContentLoaded = (scan) => { \ No newline at end of file +.pos-selected-customer__panel { + display: flex; + flex-direction: column; + gap: 0; + min-width: 0; +} + +.pos-selected-customer__header { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 0.75rem; + padding: 1rem 1rem 0.8rem; + border-bottom: 1px solid #e6edf7; +} + +.pos-selected-customer__title-wrap { + min-width: 0; +} + +.pos-selected-customer__title { + margin: 0; + font-size: 1.2rem; + line-height: 1.2; + color: #26384c; +} + +.pos-selected-customer__tabs { + margin: 0 0 0.35rem; +} + +.pos-selected-customer__tabs ul { + justify-content: flex-start; + margin: 0; + gap: 0.35rem; + flex-wrap: wrap; + border-bottom: none; +} + +.pos-selected-customer__tabs li a { + border: none; + border-radius: 999px; + min-height: 2.1rem; + padding: 0.45rem 0.85rem; + color: #5a6980; + font-weight: 600; +} + +.pos-selected-customer__tabs li.is-active a { + background: #eef4ff; + color: #21446d; +} + +.pos-selected-customer__row { + display: grid; + grid-template-columns: 1rem minmax(0, 1fr) auto; + align-items: start; + gap: 0.75rem; + padding: 0.85rem 1rem; + border-bottom: 1px solid #eef3f8; + text-decoration: none; +} + +.pos-selected-customer__icon { + margin-right: 0; + color: #62748a; + width: 1rem; + align-self: center; +} + +.pos-selected-customer__label { + min-width: 0; + color: #4b5a6d; + font-weight: 600; + align-self: center; +} + +.pos-selected-customer__value { + min-width: 0; + max-width: 14rem; + justify-self: end; + text-align: right; + color: #243446; + word-break: break-word; +} + +.pos-selected-customer__shortcut { + color: #21446d; + font-weight: 600; +} + +.pos-selected-customer__discounts { + padding: 1rem; +} + +.pos-selected-customer--sidebar .pos-selected-customer__title, +.pos-selected-customer--order-detail .pos-selected-customer__title { + font-size: 1.05rem; +} + +.pos-selected-customer--sidebar .pos-selected-customer__header, +.pos-selected-customer--order-detail .pos-selected-customer__header { + padding: 0.95rem 1rem 0.75rem; +} + +.pos-selected-customer :deep(.column.is-12-desktop) { + padding: 0.35rem 1rem 0.5rem; +} + +.pos-selected-customer :deep(.has-text-centered) { + margin: 0; +} + +.pos-selected-customer :deep(.field) { + margin-bottom: 0; +} + +@media screen and (max-width: 768px) { + .pos-selected-customer__row { + grid-template-columns: 1rem minmax(0, 1fr); + } + + .pos-selected-customer__value { + grid-column: 2; + justify-self: start; + text-align: left; + max-width: none; + } +} + + diff --git a/src/components/displays/department/pos/order/POSOrderNote.vue b/src/components/displays/department/pos/order/POSOrderNote.vue index 3cf8e643..a3e91e2c 100644 --- a/src/components/displays/department/pos/order/POSOrderNote.vue +++ b/src/components/displays/department/pos/order/POSOrderNote.vue @@ -1,5 +1,6 @@ \ No newline at end of file + diff --git a/src/components/displays/department/pos/order/POSOrderReference.vue b/src/components/displays/department/pos/order/POSOrderReference.vue index c30d75ab..5c8ef52b 100644 --- a/src/components/displays/department/pos/order/POSOrderReference.vue +++ b/src/components/displays/department/pos/order/POSOrderReference.vue @@ -1,5 +1,6 @@ \ No newline at end of file + diff --git a/src/components/displays/department/pos/order/PosOrderLicensePlates.vue b/src/components/displays/department/pos/order/PosOrderLicensePlates.vue index cb9f58c7..ad929ab6 100644 --- a/src/components/displays/department/pos/order/PosOrderLicensePlates.vue +++ b/src/components/displays/department/pos/order/PosOrderLicensePlates.vue @@ -1,15 +1,52 @@