tin-spa 20.14.113 → 20.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/index.d.ts +80 -10
- package/fesm2022/{tin-spa-accounting.module-Cg4vWaJE.mjs → tin-spa-accounting.module-CamqAeW_.mjs} +71 -83
- package/fesm2022/tin-spa-accounting.module-CamqAeW_.mjs.map +1 -0
- package/fesm2022/{tin-spa-agent-page.component-DJvMPjCQ.mjs → tin-spa-agent-page.component-DTYn54p0.mjs} +2 -2
- package/fesm2022/{tin-spa-agent-page.component-DJvMPjCQ.mjs.map → tin-spa-agent-page.component-DTYn54p0.mjs.map} +1 -1
- package/fesm2022/{tin-spa-agent.component-D0CGGnGZ.mjs → tin-spa-agent.component-CZKMWl3r.mjs} +2 -2
- package/fesm2022/{tin-spa-agent.component-D0CGGnGZ.mjs.map → tin-spa-agent.component-CZKMWl3r.mjs.map} +1 -1
- package/fesm2022/{tin-spa-assets.module-qIWNwJ2E.mjs → tin-spa-assets.module-BcOjlhfm.mjs} +16 -11
- package/fesm2022/tin-spa-assets.module-BcOjlhfm.mjs.map +1 -0
- package/fesm2022/tin-spa-core.mjs +426 -112
- package/fesm2022/tin-spa-core.mjs.map +1 -1
- package/fesm2022/{tin-spa-general-erp.module-CoYxceLj.mjs → tin-spa-general-erp.module-b6SjnWUM.mjs} +2 -2
- package/fesm2022/{tin-spa-general-erp.module-CoYxceLj.mjs.map → tin-spa-general-erp.module-b6SjnWUM.mjs.map} +1 -1
- package/fesm2022/{tin-spa-hr.module-CKPZ9ZOj.mjs → tin-spa-hr.module-DkGVK2lX.mjs} +2 -2
- package/fesm2022/{tin-spa-hr.module-CKPZ9ZOj.mjs.map → tin-spa-hr.module-DkGVK2lX.mjs.map} +1 -1
- package/fesm2022/tin-spa-http.mjs +32 -1
- package/fesm2022/tin-spa-http.mjs.map +1 -1
- package/fesm2022/{tin-spa-inventory.module-DRKAcNau.mjs → tin-spa-inventory.module-CYWBqSCd.mjs} +16 -11
- package/fesm2022/tin-spa-inventory.module-CYWBqSCd.mjs.map +1 -0
- package/fesm2022/{tin-spa-loans.module-BTDmgrWo.mjs → tin-spa-loans.module-BDh-T-Ng.mjs} +2 -2
- package/fesm2022/{tin-spa-loans.module-BTDmgrWo.mjs.map → tin-spa-loans.module-BDh-T-Ng.mjs.map} +1 -1
- package/fesm2022/{tin-spa-manufacturing.module-CHta6oV_.mjs → tin-spa-manufacturing.module-9Ti-BFb2.mjs} +2 -2
- package/fesm2022/{tin-spa-manufacturing.module-CHta6oV_.mjs.map → tin-spa-manufacturing.module-9Ti-BFb2.mjs.map} +1 -1
- package/fesm2022/{tin-spa-payroll.module-CZ_GO1jF.mjs → tin-spa-payroll.module-54Efumv_.mjs} +9 -6
- package/fesm2022/{tin-spa-payroll.module-CZ_GO1jF.mjs.map → tin-spa-payroll.module-54Efumv_.mjs.map} +1 -1
- package/fesm2022/{tin-spa-purchasing.module-NCdwquaF.mjs → tin-spa-purchasing.module-Bq8JnsyM.mjs} +16 -11
- package/fesm2022/tin-spa-purchasing.module-Bq8JnsyM.mjs.map +1 -0
- package/fesm2022/{tin-spa-sales.module-D0uEGJW7.mjs → tin-spa-sales.module-nyrJulzD.mjs} +40 -17
- package/fesm2022/tin-spa-sales.module-nyrJulzD.mjs.map +1 -0
- package/fesm2022/{tin-spa-tenancy-erp.module-GwIv8Wbc.mjs → tin-spa-tenancy-erp.module-DGATA-p_.mjs} +2 -2
- package/fesm2022/{tin-spa-tenancy-erp.module-GwIv8Wbc.mjs.map → tin-spa-tenancy-erp.module-DGATA-p_.mjs.map} +1 -1
- package/fesm2022/{tin-spa-tin-spa-Cqe5hYTr.mjs → tin-spa-tin-spa-BMy59BVn.mjs} +1428 -219
- package/fesm2022/tin-spa-tin-spa-BMy59BVn.mjs.map +1 -0
- package/fesm2022/tin-spa.mjs +1 -1
- package/http/index.d.ts +3 -1
- package/index.d.ts +144 -24
- package/package.json +1 -1
- package/src/tin-styles.css +27 -0
- package/fesm2022/tin-spa-accounting.module-Cg4vWaJE.mjs.map +0 -1
- package/fesm2022/tin-spa-assets.module-qIWNwJ2E.mjs.map +0 -1
- package/fesm2022/tin-spa-inventory.module-DRKAcNau.mjs.map +0 -1
- package/fesm2022/tin-spa-purchasing.module-NCdwquaF.mjs.map +0 -1
- package/fesm2022/tin-spa-sales.module-D0uEGJW7.mjs.map +0 -1
- package/fesm2022/tin-spa-tin-spa-Cqe5hYTr.mjs.map +0 -1
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { inject, Component, Injectable, EventEmitter, Output, Input, ElementRef, NgZone, HostListener, NgModule } from '@angular/core';
|
|
3
|
-
import * as i1$
|
|
3
|
+
import * as i1$2 from '@angular/router';
|
|
4
4
|
import { RouterModule } from '@angular/router';
|
|
5
|
+
import * as i1 from 'tin-spa/core';
|
|
5
6
|
import { SalesService, HttpService, silentContext, SpaMatModule } from 'tin-spa/core';
|
|
6
|
-
import { P as PageComponent, T as TilesComponent, C as ChartsComponent, e as SpaAdminModule } from './tin-spa-tin-spa-
|
|
7
|
+
import { P as PageComponent, T as TilesComponent, C as ChartsComponent, e as SpaAdminModule } from './tin-spa-tin-spa-BMy59BVn.mjs';
|
|
7
8
|
import * as i3 from '@angular/material/icon';
|
|
8
9
|
import { BreakpointObserver, LayoutModule } from '@angular/cdk/layout';
|
|
9
10
|
import { HttpClient } from '@angular/common/http';
|
|
10
11
|
import { of, from } from 'rxjs';
|
|
11
12
|
import { map, catchError, switchMap } from 'rxjs/operators';
|
|
12
|
-
import * as i1 from '@angular/common';
|
|
13
|
+
import * as i1$1 from '@angular/common';
|
|
13
14
|
import { CommonModule } from '@angular/common';
|
|
14
15
|
import * as i3$1 from '@angular/material/progress-spinner';
|
|
15
16
|
|
|
@@ -56,7 +57,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
56
57
|
|
|
57
58
|
// Sales module dashboard with revenue tracking and pipeline visibility
|
|
58
59
|
class SalesDashboardComponent {
|
|
59
|
-
constructor() {
|
|
60
|
+
constructor(dataService) {
|
|
61
|
+
this.dataService = dataService;
|
|
60
62
|
this.summaryTiles = {
|
|
61
63
|
tiles: [
|
|
62
64
|
{ name: 'salesRevenue', alias: 'Sales Revenue (MTD)', color: '#4caf50', icon: 'attach_money', style: 'icon', info: 'Total sales revenue this month' }, // Changed: Removed prefix
|
|
@@ -66,7 +68,8 @@ class SalesDashboardComponent {
|
|
|
66
68
|
{ name: 'topProduct', alias: 'Top Product', color: '#9c27b0', icon: 'star', style: 'icon', info: 'Best selling product this month' },
|
|
67
69
|
],
|
|
68
70
|
loadAction: { url: 'sales/dashboard-summary/x' }, // Changed: Use module-specific CustomCriteria endpoint
|
|
69
|
-
loadInit: true
|
|
71
|
+
loadInit: true,
|
|
72
|
+
security: { allow: [this.dataService.capSalesDashboard] } // Added (dashboard-alignment 1-D.3): cap61 (datalib.service.ts:580) — a single-module dashboard, so the gate sits on the whole config
|
|
70
73
|
};
|
|
71
74
|
// Changed: Chart-style tiles now use loadAction with dataField for dynamic data
|
|
72
75
|
this.chartTiles = {
|
|
@@ -76,7 +79,8 @@ class SalesDashboardComponent {
|
|
|
76
79
|
{ name: 'paymentSplit', alias: 'Payment Methods', color: '#9c27b0', chart: { type: 'doughnut', colors: ['#4caf50', '#2196f3', '#ff9800', '#9c27b0'], height: 130, dataField: 'paymentSplit' }, footer: 'Current distribution', footerIcon: 'payment' }, // Changed: dataField replaces inline data
|
|
77
80
|
],
|
|
78
81
|
loadAction: { url: 'sales/dashboard-chart-tiles/x' }, // Changed: Load chart tile data from API
|
|
79
|
-
loadInit: true
|
|
82
|
+
loadInit: true,
|
|
83
|
+
security: { allow: [this.dataService.capSalesDashboard] } // Added (dashboard-alignment 1-D.3): cap61
|
|
80
84
|
};
|
|
81
85
|
this.chartConfig = {
|
|
82
86
|
charts: [
|
|
@@ -87,10 +91,23 @@ class SalesDashboardComponent {
|
|
|
87
91
|
],
|
|
88
92
|
loadAction: { url: 'sales/dashboard-charts/x' }, // Changed: Use module-specific CustomCriteria endpoint
|
|
89
93
|
loadInit: true,
|
|
90
|
-
columns: 2
|
|
94
|
+
columns: 2,
|
|
95
|
+
security: { allow: [this.dataService.capSalesDashboard] } // Added (dashboard-alignment 1-D.3): cap61
|
|
96
|
+
};
|
|
97
|
+
// Added (dashboard-alignment 1-D.2): monthlyOrders MOVED here from the main overview dashboard, which was
|
|
98
|
+
// the only page showing it. It keeps its own load endpoint because sales/dashboard-chart-tiles does not
|
|
99
|
+
// carry the key; that endpoint's monthlyOrders is gated on cap61 server-side (TinErp DashboardController,
|
|
100
|
+
// 4b4290b) — the same cap declared here.
|
|
101
|
+
this.overviewChartTiles = {
|
|
102
|
+
tiles: [
|
|
103
|
+
{ name: 'monthlyOrders', alias: 'Monthly Orders', color: '#9c27b0', chart: { type: 'bar', colors: ['#9c27b0', '#ba68c8', '#ce93d8', '#e1bee7', '#9c27b0', '#ba68c8'], height: 100, dataField: 'monthlyOrders' }, footer: 'Last 6 months', footerIcon: 'bar_chart' },
|
|
104
|
+
],
|
|
105
|
+
loadAction: { url: 'dashboard/overview/chart-tiles' },
|
|
106
|
+
loadInit: true,
|
|
107
|
+
security: { allow: [this.dataService.capSalesDashboard] }
|
|
91
108
|
};
|
|
92
109
|
}
|
|
93
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SalesDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
110
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SalesDashboardComponent, deps: [{ token: i1.DataServiceLib }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
94
111
|
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SalesDashboardComponent, isStandalone: false, selector: "spa-sales-dashboard", ngImport: i0, template: `
|
|
95
112
|
<div class="dashboard-container">
|
|
96
113
|
<h4 class="dashboard-title"><mat-icon>point_of_sale</mat-icon> Sales Dashboard</h4>
|
|
@@ -99,8 +116,11 @@ class SalesDashboardComponent {
|
|
|
99
116
|
<div style="margin-top: 16px;"></div>
|
|
100
117
|
<spa-tiles [config]="chartTiles"></spa-tiles>
|
|
101
118
|
<spa-charts [config]="chartConfig"></spa-charts>
|
|
119
|
+
<!-- Added (dashboard-alignment 1-D.2): MOVED DOWN from the main overview dashboard, which was the only
|
|
120
|
+
page showing it. The module dashboard is the deeper page. -->
|
|
121
|
+
<spa-tiles [config]="overviewChartTiles"></spa-tiles>
|
|
102
122
|
</div>
|
|
103
|
-
`, isInline: true, styles: [".dashboard-container{padding:16px}.dashboard-title{display:flex;align-items:center;gap:8px;margin-bottom:16px;color:#333;font-weight:500}\n"], dependencies: [{ kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "tileClick", "tileUnClick"] }, { kind: "component", type: ChartsComponent, selector: "spa-charts", inputs: ["config", "data", "reload"] }] }); }
|
|
123
|
+
`, isInline: true, styles: [".dashboard-container{padding:16px}.dashboard-title{display:flex;align-items:center;gap:8px;margin-bottom:16px;color:#333;font-weight:500}\n"], dependencies: [{ kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "contentResolved", "tileClick", "tileUnClick"] }, { kind: "component", type: ChartsComponent, selector: "spa-charts", inputs: ["config", "data", "reload"], outputs: ["contentResolved"] }] }); }
|
|
104
124
|
}
|
|
105
125
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SalesDashboardComponent, decorators: [{
|
|
106
126
|
type: Component,
|
|
@@ -112,9 +132,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
112
132
|
<div style="margin-top: 16px;"></div>
|
|
113
133
|
<spa-tiles [config]="chartTiles"></spa-tiles>
|
|
114
134
|
<spa-charts [config]="chartConfig"></spa-charts>
|
|
135
|
+
<!-- Added (dashboard-alignment 1-D.2): MOVED DOWN from the main overview dashboard, which was the only
|
|
136
|
+
page showing it. The module dashboard is the deeper page. -->
|
|
137
|
+
<spa-tiles [config]="overviewChartTiles"></spa-tiles>
|
|
115
138
|
</div>
|
|
116
139
|
`, standalone: false, styles: [".dashboard-container{padding:16px}.dashboard-title{display:flex;align-items:center;gap:8px;margin-bottom:16px;color:#333;font-weight:500}\n"] }]
|
|
117
|
-
}] });
|
|
140
|
+
}], ctorParameters: () => [{ type: i1.DataServiceLib }] });
|
|
118
141
|
|
|
119
142
|
// Point of Sale — the shared till core (POS Phase 3).
|
|
120
143
|
//
|
|
@@ -594,7 +617,7 @@ class PosTileComponent {
|
|
|
594
617
|
this.picked.emit(this.product);
|
|
595
618
|
}
|
|
596
619
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PosTileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
597
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PosTileComponent, isStandalone: false, selector: "spa-pos-tile", inputs: { product: "product", inCart: "inCart", disabled: "disabled" }, outputs: { picked: "picked" }, ngImport: i0, template: "<!-- Added (POS Phase 4): the tile face. A real button, so Tab/Space/Enter work with no scripted key handling. -->\n<button type=\"button\" class=\"pt-tile\" [disabled]=\"disabled\" (click)=\"pick()\"\n [attr.aria-label]=\"product?.name + ', ' + (product?.unitPrice | number:'1.2-2') + ', add to sale'\">\n\n <!-- badge: how many are already on the ticket. Absent at zero \u2014 an empty counter on every tile is noise. -->\n <span class=\"pt-count\" *ngIf=\"inCart > 0\" aria-hidden=\"true\">{{ inCart }}</span>\n\n <span class=\"pt-body\">\n <span class=\"pt-name\">{{ product?.name }}</span>\n <span class=\"pt-code\" *ngIf=\"product?.code\">{{ product?.code }}</span>\n </span>\n\n <!-- the price strip: the one figure the operator and the customer both read off the screen -->\n <span class=\"pt-price\">{{ product?.unitPrice | number:'1.2-2' }}</span>\n</button>\n", styles: [":host{display:block;height:100%}.pt-tile{position:relative;display:flex;flex-direction:column;justify-content:space-between;align-items:stretch;width:100%;height:100%;min-width:88px;min-height:88px;padding:10px 10px 0;border:1px solid rgba(0,0,0,.12);border-radius:10px;background:#fff;font:inherit;text-align:left;cursor:pointer;overflow:hidden;transition:border-color .12s,box-shadow .12s,transform .06s}.pt-tile:hover:not(:disabled){border-color:var(--pos-accent, #1565c0)}.pt-tile:focus-visible{outline:2px solid var(--pos-accent, #1565c0);outline-offset:2px}.pt-tile:active:not(:disabled){transform:scale(.97)}.pt-tile:disabled{opacity:.45;cursor:default}.pt-body{display:flex;flex-direction:column;gap:2px;min-width:0;padding:0 22px 8px 0}.pt-name{font-size:14px;font-weight:600;line-height:1.25;color:#000000de;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.pt-code{font-size:11.5px;color:#00000080;font-variant-numeric:tabular-nums}.pt-price{margin:0 -10px;padding:7px 10px;background:#0000000b;border-top:1px solid rgba(0,0,0,.07);font-size:15px;font-weight:600;font-variant-numeric:tabular-nums;color:#000c;text-align:right}.pt-count{position:absolute;top:6px;right:6px;min-width:22px;height:22px;padding:0 6px;border-radius:11px;background:var(--pos-accent, #1565c0);color:#fff;font-size:12.5px;font-weight:700;line-height:22px;text-align:center;font-variant-numeric:tabular-nums}@media (prefers-reduced-motion: reduce){.pt-tile{transition:none}.pt-tile:active:not(:disabled){transform:none}}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.DecimalPipe, name: "number" }] }); }
|
|
620
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PosTileComponent, isStandalone: false, selector: "spa-pos-tile", inputs: { product: "product", inCart: "inCart", disabled: "disabled" }, outputs: { picked: "picked" }, ngImport: i0, template: "<!-- Added (POS Phase 4): the tile face. A real button, so Tab/Space/Enter work with no scripted key handling. -->\n<button type=\"button\" class=\"pt-tile\" [disabled]=\"disabled\" (click)=\"pick()\"\n [attr.aria-label]=\"product?.name + ', ' + (product?.unitPrice | number:'1.2-2') + ', add to sale'\">\n\n <!-- badge: how many are already on the ticket. Absent at zero \u2014 an empty counter on every tile is noise. -->\n <span class=\"pt-count\" *ngIf=\"inCart > 0\" aria-hidden=\"true\">{{ inCart }}</span>\n\n <span class=\"pt-body\">\n <span class=\"pt-name\">{{ product?.name }}</span>\n <span class=\"pt-code\" *ngIf=\"product?.code\">{{ product?.code }}</span>\n </span>\n\n <!-- the price strip: the one figure the operator and the customer both read off the screen -->\n <span class=\"pt-price\">{{ product?.unitPrice | number:'1.2-2' }}</span>\n</button>\n", styles: [":host{display:block;height:100%}.pt-tile{position:relative;display:flex;flex-direction:column;justify-content:space-between;align-items:stretch;width:100%;height:100%;min-width:88px;min-height:88px;padding:10px 10px 0;border:1px solid rgba(0,0,0,.12);border-radius:10px;background:#fff;font:inherit;text-align:left;cursor:pointer;overflow:hidden;transition:border-color .12s,box-shadow .12s,transform .06s}.pt-tile:hover:not(:disabled){border-color:var(--pos-accent, #1565c0)}.pt-tile:focus-visible{outline:2px solid var(--pos-accent, #1565c0);outline-offset:2px}.pt-tile:active:not(:disabled){transform:scale(.97)}.pt-tile:disabled{opacity:.45;cursor:default}.pt-body{display:flex;flex-direction:column;gap:2px;min-width:0;padding:0 22px 8px 0}.pt-name{font-size:14px;font-weight:600;line-height:1.25;color:#000000de;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.pt-code{font-size:11.5px;color:#00000080;font-variant-numeric:tabular-nums}.pt-price{margin:0 -10px;padding:7px 10px;background:#0000000b;border-top:1px solid rgba(0,0,0,.07);font-size:15px;font-weight:600;font-variant-numeric:tabular-nums;color:#000c;text-align:right}.pt-count{position:absolute;top:6px;right:6px;min-width:22px;height:22px;padding:0 6px;border-radius:11px;background:var(--pos-accent, #1565c0);color:#fff;font-size:12.5px;font-weight:700;line-height:22px;text-align:center;font-variant-numeric:tabular-nums}@media (prefers-reduced-motion: reduce){.pt-tile{transition:none}.pt-tile:active:not(:disabled){transform:none}}\n"], dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }] }); }
|
|
598
621
|
}
|
|
599
622
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PosTileComponent, decorators: [{
|
|
600
623
|
type: Component,
|
|
@@ -673,7 +696,7 @@ class PosWideComponent {
|
|
|
673
696
|
this.charge.emit();
|
|
674
697
|
}
|
|
675
698
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PosWideComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
676
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PosWideComponent, isStandalone: false, selector: "spa-pos-wide", inputs: { title: "title" }, outputs: { charge: "charge" }, ngImport: i0, template: "<!-- Added (POS Phase 4): the landscape till. Two columns \u2014 the catalogue, and the ticket. -->\n\n<div class=\"pw-till\">\n\n <!-- ================= TOP BAR: the scan/search box is the till's home key ================= -->\n <header class=\"pw-bar\">\n <div class=\"pw-scan\">\n <mat-icon class=\"pw-scan-icon\" aria-hidden=\"true\">qr_code_scanner</mat-icon>\n <input #scan class=\"pw-scan-input\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\"\n placeholder=\"Scan a barcode or search by name\" aria-label=\"Scan a barcode or search by name\"\n [value]=\"searchTerm\" (input)=\"onSearch(scan.value)\" (keyup.enter)=\"onSearchEnter()\">\n <button type=\"button\" class=\"pw-scan-clear\" *ngIf=\"searchTerm\" aria-label=\"Clear the search\"\n (click)=\"onSearch(''); scan.value = ''\">\n <mat-icon aria-hidden=\"true\">close</mat-icon>\n </button>\n </div>\n <h1 class=\"pw-title\">{{ title }}</h1>\n </header>\n\n <div class=\"pw-body\">\n\n <!-- ================= LEFT: category tabs + the product grid ================= -->\n <section class=\"pw-catalogue\" aria-label=\"Products\">\n\n <div class=\"pw-tabs\" role=\"tablist\" aria-label=\"Product categories\" *ngIf=\"session.categories.length > 0\">\n <button type=\"button\" class=\"pw-tab\" role=\"tab\" [class.active]=\"session.activeCategory === ''\"\n [attr.aria-selected]=\"session.activeCategory === ''\" (click)=\"session.setCategory('')\">All</button>\n <button type=\"button\" class=\"pw-tab\" role=\"tab\" *ngFor=\"let category of session.categories\"\n [class.active]=\"session.activeCategory === category\"\n [attr.aria-selected]=\"session.activeCategory === category\"\n (click)=\"session.setCategory(category)\">{{ category }}</button>\n </div>\n\n <div class=\"pw-grid\" *ngIf=\"session.filtered.length > 0\">\n <spa-pos-tile *ngFor=\"let product of session.filtered\" [product]=\"product\"\n [inCart]=\"countOf(product)\" (picked)=\"pick($event)\"></spa-pos-tile>\n </div>\n\n <!-- An empty screen is an invitation to act, not a shrug. It says what to do next. -->\n <div class=\"pw-empty\" *ngIf=\"session.filtered.length === 0 && !session.loadingProducts\">\n <mat-icon aria-hidden=\"true\">search_off</mat-icon>\n <p *ngIf=\"searchTerm\">Nothing matches “{{ searchTerm }}”. Clear the search to see every product.</p>\n <p *ngIf=\"!searchTerm\">No products are available on this till yet.</p>\n </div>\n\n <div class=\"pw-empty\" *ngIf=\"session.loadingProducts\">\n <mat-spinner diameter=\"28\"></mat-spinner>\n <p>Loading products</p>\n </div>\n </section>\n\n <!-- ================= RIGHT: the ticket ================= -->\n <aside class=\"pw-ticket\" aria-label=\"Current sale\">\n\n <div class=\"pw-ticket-head\">\n <span class=\"pw-ticket-title\">Current sale</span>\n <span class=\"pw-ticket-count\" *ngIf=\"session.totals.unitCount > 0\">{{ session.totals.unitCount | number:'1.0-2' }} items</span>\n <button type=\"button\" class=\"pw-clear\" *ngIf=\"session.lines.length > 0\" (click)=\"clear()\">Clear</button>\n </div>\n\n <!-- the receipt panel: one row per line, scrolling inside itself so the plinth below never moves -->\n <div class=\"pw-lines\" *ngIf=\"session.lines.length > 0\">\n <div class=\"pw-line\" *ngFor=\"let line of session.lines\">\n <div class=\"pw-line-text\">\n <span class=\"pw-line-name\">{{ line.name }}</span>\n <!-- Changed (POS Phase 5): the unit line now states the multiplication the extension beside it is\n the answer to, and says when a shelf price already contains its VAT. An operator checking the\n ticket against the counter reads across one row: this price, this many, this much. -->\n <span class=\"pw-line-unit\">\n {{ line.shelfUnitPrice | number:'1.2-2' }} each × {{ line.quantity | number:'1.0-2' }}<span *ngIf=\"line.isTaxInclusive && line.taxRate > 0\"> · incl. VAT</span>\n </span>\n </div>\n\n <div class=\"pw-qty\" role=\"group\" [attr.aria-label]=\"line.name + ' quantity'\">\n <button type=\"button\" class=\"pw-step\" [attr.aria-label]=\"'One less ' + line.name\" (click)=\"decrement(line)\">\n <mat-icon aria-hidden=\"true\">remove</mat-icon>\n </button>\n <span class=\"pw-qty-value\" aria-live=\"polite\">{{ line.quantity | number:'1.0-2' }}</span>\n <button type=\"button\" class=\"pw-step\" [attr.aria-label]=\"'One more ' + line.name\" (click)=\"increment(line)\">\n <mat-icon aria-hidden=\"true\">add</mat-icon>\n </button>\n </div>\n\n <!-- \uD83D\uDD34 Changed (POS Phase 5, Arbiter A-0831-23): was line.lineTotal, which is VAT-INCLUSIVE. Sat under\n a VAT-EXCLUSIVE \"36.00 each\" it rendered \"36.00 each\" above \"41.40\" at quantity 1 \u2014 correct\n arithmetic that reads as a mistake to the person holding the goods. The extension is now the tile\n price multiplied out, so every line multiplies visibly, and VAT is told once, in the plinth. -->\n <span class=\"pw-line-total\">{{ line.shelfAmount | number:'1.2-2' }}</span>\n\n <button type=\"button\" class=\"pw-remove\" [attr.aria-label]=\"'Remove ' + line.name\" (click)=\"remove(line)\">\n <mat-icon aria-hidden=\"true\">delete_outline</mat-icon>\n </button>\n </div>\n </div>\n\n <div class=\"pw-ticket-empty\" *ngIf=\"session.lines.length === 0\">\n <mat-icon aria-hidden=\"true\">shopping_cart</mat-icon>\n <p>Scan an item or tap a product to start the sale.</p>\n </div>\n\n <!-- ================= THE PLINTH \u2014 the one bold element on the page =================\n Quiet subtotal and VAT rows, a hairline, then the TOTAL at full size, fused to the CHARGE bar.\n Everything above it is deliberately ordinary so that this reads as the only thing shouting. -->\n <div class=\"pw-plinth\">\n <!-- Changed (POS Phase 5): these two rows now reconcile with the LINES ABOVE THEM \u2014 Subtotal is the sum\n of the extensions on screen and \"VAT added\" is only the tax this till puts on top of them, so\n Subtotal + VAT added = Total exactly, in a mixed basket as well as a plain one. For the ordinary\n all-exclusive basket these are the same two numbers as before. The full VAT breakdown, including\n the tax already inside an inclusive shelf price, is on the printed receipt. -->\n <!-- Changed (Arbiter, smalls batch 2026-08-31): \"Subtotal\" \u2192 \"Items\" \u2014 the receipt's Subtotal is NET while this\n row is the SHELF value of the goods; one word for two quantities was the collision. Value unchanged. -->\n <div class=\"pw-sum\"><span>Items</span><span class=\"pw-num\">{{ session.totals.shelfTotal | number:'1.2-2' }}</span></div>\n <div class=\"pw-sum\"><span>VAT added</span><span class=\"pw-num\">{{ session.totals.taxAdded | number:'1.2-2' }}</span></div>\n\n <div class=\"pw-total\">\n <span class=\"pw-total-label\">Total</span>\n <span class=\"pw-total-value\" aria-live=\"polite\">{{ session.totals.total | number:'1.2-2' }}</span>\n </div>\n\n <button type=\"button\" class=\"pw-charge\" [disabled]=\"!session.canCharge\" (click)=\"onCharge()\">\n <mat-icon aria-hidden=\"true\">point_of_sale</mat-icon>\n <span>Charge</span>\n </button>\n </div>\n </aside>\n </div>\n</div>\n", styles: [":host{display:block;height:100%;min-height:0}.pw-till{height:100%;min-height:0;display:flex;flex-direction:column;background:#fafafa}.pw-bar{display:flex;align-items:center;gap:16px;padding:10px 16px;background:#fff;border-bottom:1px solid var(--pos-line);flex:0 0 auto}.pw-scan{position:relative;display:flex;align-items:center;gap:8px;flex:1 1 auto;max-width:560px;min-height:48px;padding:0 8px 0 12px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;transition:border-color .12s,box-shadow .12s}.pw-scan:focus-within{border-color:var(--pos-accent);box-shadow:0 0 0 3px var(--pos-accent-soft)}.pw-scan-icon{color:#00000073;font-size:22px;width:22px;height:22px;flex:0 0 auto}.pw-scan-input{flex:1 1 auto;min-width:0;border:0;outline:0;background:transparent;font:inherit;font-size:15px;padding:12px 0;color:#000000de;min-height:48px;box-sizing:border-box}.pw-scan-clear{display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:0;background:transparent;color:#00000080;cursor:pointer;border-radius:8px}.pw-scan-clear:hover{background:#0000000d}.pw-scan-clear:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pw-title{margin:0 0 0 auto;font-size:14px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:#0000006b}.pw-body{flex:1 1 auto;min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 380px}.pw-catalogue{min-width:0;min-height:0;display:flex;flex-direction:column;padding:14px 16px;gap:12px;overflow:hidden}.pw-tabs{display:flex;gap:8px;flex-wrap:wrap;flex:0 0 auto}.pw-tab{display:inline-flex;align-items:center;min-height:48px;padding:0 18px;border:1px solid var(--pos-line);border-radius:24px;background:#fff;font:inherit;font-size:14px;color:#000000b3;cursor:pointer;transition:border-color .12s,background .12s,color .12s}.pw-tab:hover{border-color:#90a4ae}.pw-tab:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pw-tab.active{border-color:var(--pos-accent);background:var(--pos-accent-soft);color:var(--pos-accent);font-weight:600}.pw-grid{flex:1 1 auto;min-height:0;overflow-y:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:12px;align-content:start;padding-bottom:8px}.pw-empty{flex:1 1 auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:var(--pos-quiet);text-align:center}.pw-empty mat-icon{font-size:40px;width:40px;height:40px;color:#0003}.pw-empty p{margin:0;font-size:14px;max-width:34ch}.pw-ticket{min-height:0;display:flex;flex-direction:column;background:#fff;border-left:1px solid var(--pos-line)}.pw-ticket-head{flex:0 0 auto;display:flex;align-items:center;gap:10px;padding:14px 16px 10px;border-bottom:1px solid var(--pos-line)}.pw-ticket-title{font-size:13px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#0000008c}.pw-ticket-count{font-size:13px;color:var(--pos-quiet);font-variant-numeric:tabular-nums}.pw-clear{margin-left:auto;min-height:48px;padding:0 14px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;font:inherit;font-size:13.5px;color:#0009;cursor:pointer}.pw-clear:hover{border-color:#ef9a9a;color:#c62828}.pw-clear:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pw-lines{flex:1 1 auto;min-height:0;overflow-y:auto;padding:4px 0}.pw-line{display:grid;grid-template-columns:auto minmax(0,1fr) auto;grid-template-areas:\"text text text\" \"qty total remove\";align-items:center;gap:4px 8px;padding:10px 8px 10px 16px;border-bottom:1px solid rgba(0,0,0,.06)}.pw-line-text{grid-area:text;min-width:0;display:flex;flex-direction:column;gap:1px}.pw-qty{grid-area:qty}.pw-line-total{grid-area:total}.pw-remove{grid-area:remove}.pw-line-name{font-size:14px;font-weight:500;line-height:1.3;color:#000000de;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.pw-line-unit{font-size:12px;color:var(--pos-quiet);font-variant-numeric:tabular-nums}.pw-qty{display:inline-flex;align-items:center;justify-self:start}.pw-step{display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;color:#000000b3;cursor:pointer}.pw-step:hover{border-color:var(--pos-accent);color:var(--pos-accent)}.pw-step:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pw-step mat-icon{font-size:20px;width:20px;height:20px}.pw-qty-value{min-width:40px;text-align:center;font-size:16px;font-weight:600;font-variant-numeric:tabular-nums}.pw-line-total{min-width:74px;text-align:right;font-size:15px;font-weight:600;font-variant-numeric:tabular-nums;color:#000000d9}.pw-remove{display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:0;border-radius:8px;background:transparent;color:#00000061;cursor:pointer}.pw-remove:hover{background:#ffebee;color:#c62828}.pw-remove:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pw-remove mat-icon{font-size:20px;width:20px;height:20px}.pw-ticket-empty{flex:1 1 auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:var(--pos-quiet);text-align:center;padding:0 24px}.pw-ticket-empty mat-icon{font-size:36px;width:36px;height:36px;color:#00000026}.pw-ticket-empty p{margin:0;font-size:13.5px;max-width:26ch}.pw-plinth{flex:0 0 auto;border-top:1px solid var(--pos-line);background:#fff}.pw-sum{display:flex;justify-content:space-between;align-items:baseline;padding:3px 16px;font-size:13px;color:var(--pos-quiet)}.pw-num{font-variant-numeric:tabular-nums;color:#000000b3}.pw-sum:first-child{padding-top:12px}.pw-total{display:flex;justify-content:space-between;align-items:baseline;gap:12px;margin:8px 16px 14px;padding-top:12px;border-top:1px solid rgba(0,0,0,.14)}.pw-total-label{font-size:13px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#0000008c}.pw-total-value{font-size:40px;font-weight:700;line-height:1;letter-spacing:-1px;font-variant-numeric:tabular-nums;color:var(--pos-accent)}.pw-charge{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;min-height:88px;border:0;border-radius:0;background:var(--pos-accent);color:#fff;font:inherit;font-size:21px;font-weight:700;letter-spacing:.03em;cursor:pointer;transition:background .12s}.pw-charge mat-icon{font-size:26px;width:26px;height:26px}.pw-charge:hover:not(:disabled){background:#0d47a1}.pw-charge:focus-visible{outline:3px solid #0d47a1;outline-offset:-6px}.pw-charge:disabled{background:#0000001a;color:#00000059;cursor:default}@media (max-width: 1100px){.pw-body{grid-template-columns:minmax(0,1fr) 320px}.pw-total-value{font-size:34px}}@media (prefers-reduced-motion: reduce){.pw-scan,.pw-tab,.pw-charge{transition:none}}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: PosTileComponent, selector: "spa-pos-tile", inputs: ["product", "inCart", "disabled"], outputs: ["picked"] }, { kind: "pipe", type: i1.DecimalPipe, name: "number" }] }); }
|
|
699
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PosWideComponent, isStandalone: false, selector: "spa-pos-wide", inputs: { title: "title" }, outputs: { charge: "charge" }, ngImport: i0, template: "<!-- Added (POS Phase 4): the landscape till. Two columns \u2014 the catalogue, and the ticket. -->\n\n<div class=\"pw-till\">\n\n <!-- ================= TOP BAR: the scan/search box is the till's home key ================= -->\n <header class=\"pw-bar\">\n <div class=\"pw-scan\">\n <mat-icon class=\"pw-scan-icon\" aria-hidden=\"true\">qr_code_scanner</mat-icon>\n <input #scan class=\"pw-scan-input\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\"\n placeholder=\"Scan a barcode or search by name\" aria-label=\"Scan a barcode or search by name\"\n [value]=\"searchTerm\" (input)=\"onSearch(scan.value)\" (keyup.enter)=\"onSearchEnter()\">\n <button type=\"button\" class=\"pw-scan-clear\" *ngIf=\"searchTerm\" aria-label=\"Clear the search\"\n (click)=\"onSearch(''); scan.value = ''\">\n <mat-icon aria-hidden=\"true\">close</mat-icon>\n </button>\n </div>\n <h1 class=\"pw-title\">{{ title }}</h1>\n </header>\n\n <div class=\"pw-body\">\n\n <!-- ================= LEFT: category tabs + the product grid ================= -->\n <section class=\"pw-catalogue\" aria-label=\"Products\">\n\n <div class=\"pw-tabs\" role=\"tablist\" aria-label=\"Product categories\" *ngIf=\"session.categories.length > 0\">\n <button type=\"button\" class=\"pw-tab\" role=\"tab\" [class.active]=\"session.activeCategory === ''\"\n [attr.aria-selected]=\"session.activeCategory === ''\" (click)=\"session.setCategory('')\">All</button>\n <button type=\"button\" class=\"pw-tab\" role=\"tab\" *ngFor=\"let category of session.categories\"\n [class.active]=\"session.activeCategory === category\"\n [attr.aria-selected]=\"session.activeCategory === category\"\n (click)=\"session.setCategory(category)\">{{ category }}</button>\n </div>\n\n <div class=\"pw-grid\" *ngIf=\"session.filtered.length > 0\">\n <spa-pos-tile *ngFor=\"let product of session.filtered\" [product]=\"product\"\n [inCart]=\"countOf(product)\" (picked)=\"pick($event)\"></spa-pos-tile>\n </div>\n\n <!-- An empty screen is an invitation to act, not a shrug. It says what to do next. -->\n <div class=\"pw-empty\" *ngIf=\"session.filtered.length === 0 && !session.loadingProducts\">\n <mat-icon aria-hidden=\"true\">search_off</mat-icon>\n <p *ngIf=\"searchTerm\">Nothing matches “{{ searchTerm }}”. Clear the search to see every product.</p>\n <p *ngIf=\"!searchTerm\">No products are available on this till yet.</p>\n </div>\n\n <div class=\"pw-empty\" *ngIf=\"session.loadingProducts\">\n <mat-spinner diameter=\"28\"></mat-spinner>\n <p>Loading products</p>\n </div>\n </section>\n\n <!-- ================= RIGHT: the ticket ================= -->\n <aside class=\"pw-ticket\" aria-label=\"Current sale\">\n\n <div class=\"pw-ticket-head\">\n <span class=\"pw-ticket-title\">Current sale</span>\n <span class=\"pw-ticket-count\" *ngIf=\"session.totals.unitCount > 0\">{{ session.totals.unitCount | number:'1.0-2' }} items</span>\n <button type=\"button\" class=\"pw-clear\" *ngIf=\"session.lines.length > 0\" (click)=\"clear()\">Clear</button>\n </div>\n\n <!-- the receipt panel: one row per line, scrolling inside itself so the plinth below never moves -->\n <div class=\"pw-lines\" *ngIf=\"session.lines.length > 0\">\n <div class=\"pw-line\" *ngFor=\"let line of session.lines\">\n <div class=\"pw-line-text\">\n <span class=\"pw-line-name\">{{ line.name }}</span>\n <!-- Changed (POS Phase 5): the unit line now states the multiplication the extension beside it is\n the answer to, and says when a shelf price already contains its VAT. An operator checking the\n ticket against the counter reads across one row: this price, this many, this much. -->\n <span class=\"pw-line-unit\">\n {{ line.shelfUnitPrice | number:'1.2-2' }} each × {{ line.quantity | number:'1.0-2' }}<span *ngIf=\"line.isTaxInclusive && line.taxRate > 0\"> · incl. VAT</span>\n </span>\n </div>\n\n <div class=\"pw-qty\" role=\"group\" [attr.aria-label]=\"line.name + ' quantity'\">\n <button type=\"button\" class=\"pw-step\" [attr.aria-label]=\"'One less ' + line.name\" (click)=\"decrement(line)\">\n <mat-icon aria-hidden=\"true\">remove</mat-icon>\n </button>\n <span class=\"pw-qty-value\" aria-live=\"polite\">{{ line.quantity | number:'1.0-2' }}</span>\n <button type=\"button\" class=\"pw-step\" [attr.aria-label]=\"'One more ' + line.name\" (click)=\"increment(line)\">\n <mat-icon aria-hidden=\"true\">add</mat-icon>\n </button>\n </div>\n\n <!-- \uD83D\uDD34 Changed (POS Phase 5, Arbiter A-0831-23): was line.lineTotal, which is VAT-INCLUSIVE. Sat under\n a VAT-EXCLUSIVE \"36.00 each\" it rendered \"36.00 each\" above \"41.40\" at quantity 1 \u2014 correct\n arithmetic that reads as a mistake to the person holding the goods. The extension is now the tile\n price multiplied out, so every line multiplies visibly, and VAT is told once, in the plinth. -->\n <span class=\"pw-line-total\">{{ line.shelfAmount | number:'1.2-2' }}</span>\n\n <button type=\"button\" class=\"pw-remove\" [attr.aria-label]=\"'Remove ' + line.name\" (click)=\"remove(line)\">\n <mat-icon aria-hidden=\"true\">delete_outline</mat-icon>\n </button>\n </div>\n </div>\n\n <div class=\"pw-ticket-empty\" *ngIf=\"session.lines.length === 0\">\n <mat-icon aria-hidden=\"true\">shopping_cart</mat-icon>\n <p>Scan an item or tap a product to start the sale.</p>\n </div>\n\n <!-- ================= THE PLINTH \u2014 the one bold element on the page =================\n Quiet subtotal and VAT rows, a hairline, then the TOTAL at full size, fused to the CHARGE bar.\n Everything above it is deliberately ordinary so that this reads as the only thing shouting. -->\n <div class=\"pw-plinth\">\n <!-- Changed (POS Phase 5): these two rows now reconcile with the LINES ABOVE THEM \u2014 Subtotal is the sum\n of the extensions on screen and \"VAT added\" is only the tax this till puts on top of them, so\n Subtotal + VAT added = Total exactly, in a mixed basket as well as a plain one. For the ordinary\n all-exclusive basket these are the same two numbers as before. The full VAT breakdown, including\n the tax already inside an inclusive shelf price, is on the printed receipt. -->\n <!-- Changed (Arbiter, smalls batch 2026-08-31): \"Subtotal\" \u2192 \"Items\" \u2014 the receipt's Subtotal is NET while this\n row is the SHELF value of the goods; one word for two quantities was the collision. Value unchanged. -->\n <div class=\"pw-sum\"><span>Items</span><span class=\"pw-num\">{{ session.totals.shelfTotal | number:'1.2-2' }}</span></div>\n <div class=\"pw-sum\"><span>VAT added</span><span class=\"pw-num\">{{ session.totals.taxAdded | number:'1.2-2' }}</span></div>\n\n <div class=\"pw-total\">\n <span class=\"pw-total-label\">Total</span>\n <span class=\"pw-total-value\" aria-live=\"polite\">{{ session.totals.total | number:'1.2-2' }}</span>\n </div>\n\n <button type=\"button\" class=\"pw-charge\" [disabled]=\"!session.canCharge\" (click)=\"onCharge()\">\n <mat-icon aria-hidden=\"true\">point_of_sale</mat-icon>\n <span>Charge</span>\n </button>\n </div>\n </aside>\n </div>\n</div>\n", styles: [":host{display:block;height:100%;min-height:0}.pw-till{height:100%;min-height:0;display:flex;flex-direction:column;background:#fafafa}.pw-bar{display:flex;align-items:center;gap:16px;padding:10px 16px;background:#fff;border-bottom:1px solid var(--pos-line);flex:0 0 auto}.pw-scan{position:relative;display:flex;align-items:center;gap:8px;flex:1 1 auto;max-width:560px;min-height:48px;padding:0 8px 0 12px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;transition:border-color .12s,box-shadow .12s}.pw-scan:focus-within{border-color:var(--pos-accent);box-shadow:0 0 0 3px var(--pos-accent-soft)}.pw-scan-icon{color:#00000073;font-size:22px;width:22px;height:22px;flex:0 0 auto}.pw-scan-input{flex:1 1 auto;min-width:0;border:0;outline:0;background:transparent;font:inherit;font-size:15px;padding:12px 0;color:#000000de;min-height:48px;box-sizing:border-box}.pw-scan-clear{display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:0;background:transparent;color:#00000080;cursor:pointer;border-radius:8px}.pw-scan-clear:hover{background:#0000000d}.pw-scan-clear:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pw-title{margin:0 0 0 auto;font-size:14px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:#0000006b}.pw-body{flex:1 1 auto;min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 380px}.pw-catalogue{min-width:0;min-height:0;display:flex;flex-direction:column;padding:14px 16px;gap:12px;overflow:hidden}.pw-tabs{display:flex;gap:8px;flex-wrap:wrap;flex:0 0 auto}.pw-tab{display:inline-flex;align-items:center;min-height:48px;padding:0 18px;border:1px solid var(--pos-line);border-radius:24px;background:#fff;font:inherit;font-size:14px;color:#000000b3;cursor:pointer;transition:border-color .12s,background .12s,color .12s}.pw-tab:hover{border-color:#90a4ae}.pw-tab:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pw-tab.active{border-color:var(--pos-accent);background:var(--pos-accent-soft);color:var(--pos-accent);font-weight:600}.pw-grid{flex:1 1 auto;min-height:0;overflow-y:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:12px;align-content:start;padding-bottom:8px}.pw-empty{flex:1 1 auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:var(--pos-quiet);text-align:center}.pw-empty mat-icon{font-size:40px;width:40px;height:40px;color:#0003}.pw-empty p{margin:0;font-size:14px;max-width:34ch}.pw-ticket{min-height:0;display:flex;flex-direction:column;background:#fff;border-left:1px solid var(--pos-line)}.pw-ticket-head{flex:0 0 auto;display:flex;align-items:center;gap:10px;padding:14px 16px 10px;border-bottom:1px solid var(--pos-line)}.pw-ticket-title{font-size:13px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#0000008c}.pw-ticket-count{font-size:13px;color:var(--pos-quiet);font-variant-numeric:tabular-nums}.pw-clear{margin-left:auto;min-height:48px;padding:0 14px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;font:inherit;font-size:13.5px;color:#0009;cursor:pointer}.pw-clear:hover{border-color:#ef9a9a;color:#c62828}.pw-clear:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pw-lines{flex:1 1 auto;min-height:0;overflow-y:auto;padding:4px 0}.pw-line{display:grid;grid-template-columns:auto minmax(0,1fr) auto;grid-template-areas:\"text text text\" \"qty total remove\";align-items:center;gap:4px 8px;padding:10px 8px 10px 16px;border-bottom:1px solid rgba(0,0,0,.06)}.pw-line-text{grid-area:text;min-width:0;display:flex;flex-direction:column;gap:1px}.pw-qty{grid-area:qty}.pw-line-total{grid-area:total}.pw-remove{grid-area:remove}.pw-line-name{font-size:14px;font-weight:500;line-height:1.3;color:#000000de;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.pw-line-unit{font-size:12px;color:var(--pos-quiet);font-variant-numeric:tabular-nums}.pw-qty{display:inline-flex;align-items:center;justify-self:start}.pw-step{display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;color:#000000b3;cursor:pointer}.pw-step:hover{border-color:var(--pos-accent);color:var(--pos-accent)}.pw-step:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pw-step mat-icon{font-size:20px;width:20px;height:20px}.pw-qty-value{min-width:40px;text-align:center;font-size:16px;font-weight:600;font-variant-numeric:tabular-nums}.pw-line-total{min-width:74px;text-align:right;font-size:15px;font-weight:600;font-variant-numeric:tabular-nums;color:#000000d9}.pw-remove{display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:0;border-radius:8px;background:transparent;color:#00000061;cursor:pointer}.pw-remove:hover{background:#ffebee;color:#c62828}.pw-remove:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pw-remove mat-icon{font-size:20px;width:20px;height:20px}.pw-ticket-empty{flex:1 1 auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:var(--pos-quiet);text-align:center;padding:0 24px}.pw-ticket-empty mat-icon{font-size:36px;width:36px;height:36px;color:#00000026}.pw-ticket-empty p{margin:0;font-size:13.5px;max-width:26ch}.pw-plinth{flex:0 0 auto;border-top:1px solid var(--pos-line);background:#fff}.pw-sum{display:flex;justify-content:space-between;align-items:baseline;padding:3px 16px;font-size:13px;color:var(--pos-quiet)}.pw-num{font-variant-numeric:tabular-nums;color:#000000b3}.pw-sum:first-child{padding-top:12px}.pw-total{display:flex;justify-content:space-between;align-items:baseline;gap:12px;margin:8px 16px 14px;padding-top:12px;border-top:1px solid rgba(0,0,0,.14)}.pw-total-label{font-size:13px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#0000008c}.pw-total-value{font-size:40px;font-weight:700;line-height:1;letter-spacing:-1px;font-variant-numeric:tabular-nums;color:var(--pos-accent)}.pw-charge{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;min-height:88px;border:0;border-radius:0;background:var(--pos-accent);color:#fff;font:inherit;font-size:21px;font-weight:700;letter-spacing:.03em;cursor:pointer;transition:background .12s}.pw-charge mat-icon{font-size:26px;width:26px;height:26px}.pw-charge:hover:not(:disabled){background:#0d47a1}.pw-charge:focus-visible{outline:3px solid #0d47a1;outline-offset:-6px}.pw-charge:disabled{background:#0000001a;color:#00000059;cursor:default}@media (max-width: 1100px){.pw-body{grid-template-columns:minmax(0,1fr) 320px}.pw-total-value{font-size:34px}}@media (prefers-reduced-motion: reduce){.pw-scan,.pw-tab,.pw-charge{transition:none}}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: PosTileComponent, selector: "spa-pos-tile", inputs: ["product", "inCart", "disabled"], outputs: ["picked"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }] }); }
|
|
677
700
|
}
|
|
678
701
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PosWideComponent, decorators: [{
|
|
679
702
|
type: Component,
|
|
@@ -907,7 +930,7 @@ class PosTenderComponent {
|
|
|
907
930
|
this.cancelled.emit();
|
|
908
931
|
}
|
|
909
932
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PosTenderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
910
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PosTenderComponent, isStandalone: false, selector: "spa-pos-tender", outputs: { cancelled: "cancelled", completed: "completed" }, ngImport: i0, template: "<!-- Added (POS Phase 5): the tender surface. Two faces over one frame \u2014 take the money, then the sale is done.\n The frame never moves between them, so the primary button stays under the same finger. -->\n\n<div class=\"pt-scrim\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"isComplete ? 'Sale complete' : 'Take payment'\">\n <div class=\"pt-sheet\">\n\n <!-- ================= FACE 1: taking the money ================= -->\n <ng-container *ngIf=\"!isComplete\">\n\n <header class=\"pt-head\">\n <span class=\"pt-head-title\">Take payment</span>\n <button type=\"button\" class=\"pt-close\" aria-label=\"Back to the sale\" (click)=\"cancel()\">\n <mat-icon aria-hidden=\"true\">close</mat-icon>\n </button>\n </header>\n\n <div class=\"pt-body\">\n\n <!-- LEFT: the readout. Nothing here is typed into; it is what the operator reads to the customer. -->\n <section class=\"pt-readout\" aria-label=\"Payment summary\">\n <div class=\"pt-due\">\n <span class=\"pt-label\">Amount due</span>\n <span class=\"pt-due-value\">{{ amountDue | number:'1.2-2' }}</span>\n </div>\n\n <div class=\"pt-row\" *ngIf=\"isCash\">\n <span class=\"pt-label\">Cash received</span>\n <span class=\"pt-num\">{{ session.tender.amountTendered == null ? '\u2014' : (session.tender.amountTendered | number:'1.2-2') }}</span>\n </div>\n\n <!-- THE FIXED SLOT. One rectangle, four states, always in the same place on the screen.\n Changed (ARBITER RULING A-0831-29): a REFUSAL is the fourth, and it REPLACES the other three\n rather than sitting under them. A green change figure over a sale the server refused is the one\n thing this surface must never show. -->\n <div class=\"pt-change pt-change-refused\" *ngIf=\"refused\" role=\"alert\">\n <span class=\"pt-label\">Sale not completed</span>\n <span class=\"pt-refusal\">{{ refusalMessage }}</span>\n </div>\n\n <ng-container *ngIf=\"!refused\">\n <div class=\"pt-change\" *ngIf=\"isCash\" [class.pt-change-short]=\"shortfall > 0\" [class.pt-change-live]=\"shortfall === 0 && session.tender.amountTendered != null\">\n <span class=\"pt-label\">{{ shortfall > 0 ? 'Still to pay' : 'Change due' }}</span>\n <span class=\"pt-change-value\" aria-live=\"polite\">{{ (shortfall > 0 ? shortfall : session.changeDue) | number:'1.2-2' }}</span>\n </div>\n\n <p class=\"pt-settle\" *ngIf=\"!isCash\">This payment settles the sale exactly. There is no change to give.</p>\n </ng-container>\n\n <!-- The only way forward: the cart is frozen behind this overlay, so a second identical submit refuses\n identically. The button names what happens when it is used. -->\n <button type=\"button\" class=\"pt-secondary pt-back\" *ngIf=\"refused\" (click)=\"cancel()\">\n <mat-icon aria-hidden=\"true\">arrow_back</mat-icon>\n <span>Back to the sale</span>\n </button>\n\n <p class=\"pt-message\" *ngIf=\"message && !refused\" role=\"alert\">{{ message }}</p>\n </section>\n\n <!-- RIGHT: everything the operator touches. -->\n <section class=\"pt-input\" aria-label=\"Payment entry\">\n\n <div class=\"pt-methods\" role=\"group\" aria-label=\"Payment method\">\n <button type=\"button\" class=\"pt-method\" *ngFor=\"let method of methods\"\n [class.active]=\"session.tender.method === method.value\"\n [attr.aria-pressed]=\"session.tender.method === method.value\"\n (click)=\"setMethod(method.value)\">\n <mat-icon aria-hidden=\"true\">{{ method.icon }}</mat-icon>\n <span>{{ method.label }}</span>\n </button>\n </div>\n\n <ng-container *ngIf=\"isCash\">\n <!-- Quick cash first: the exact amount leads, then the notes a customer is likely to hand over.\n One tap settles most sales without the keypad being touched at all. -->\n <div class=\"pt-quick\">\n <button type=\"button\" class=\"pt-cash\" *ngFor=\"let amount of quickAmounts; let first = first\"\n [class.exact]=\"first\" (click)=\"quick(amount)\">\n <span class=\"pt-cash-tag\" *ngIf=\"first\">Exact</span>\n {{ amount | number:'1.2-2' }}\n </button>\n </div>\n\n <div class=\"pt-keypad\" role=\"group\" aria-label=\"Amount keypad\">\n <button type=\"button\" class=\"pt-key\" *ngFor=\"let key of keys\" (click)=\"press(key)\">{{ key }}</button>\n <button type=\"button\" class=\"pt-key pt-key-wide\" aria-label=\"Delete the last digit\" (click)=\"backspace()\">\n <mat-icon aria-hidden=\"true\">backspace</mat-icon>\n </button>\n <button type=\"button\" class=\"pt-key pt-key-clear\" (click)=\"clearEntry()\">Clear</button>\n </div>\n </ng-container>\n </section>\n </div>\n\n <!-- Fused to the foot of the sheet, exactly as CHARGE is fused to the plinth. Same shape, same height,\n same place \u2014 the second half of a two-tap sale. -->\n <button type=\"button\" class=\"pt-primary\" [disabled]=\"!canComplete\" (click)=\"complete()\">\n <mat-spinner diameter=\"24\" *ngIf=\"session.submitting\"></mat-spinner>\n <mat-icon aria-hidden=\"true\" *ngIf=\"!session.submitting\">check</mat-icon>\n <span>{{ session.submitting ? 'Completing' : 'Complete sale' }}</span>\n </button>\n </ng-container>\n\n <!-- ================= FACE 2: the sale is done ================= -->\n <ng-container *ngIf=\"isComplete\">\n <header class=\"pt-head pt-head-done\">\n <span class=\"pt-head-title\">Sale complete</span>\n </header>\n\n <div class=\"pt-done\">\n <!-- The change is the only thing that matters in the two seconds after a sale: it is money the operator\n owes the customer from the drawer. It keeps the slot and the size the live readout had. -->\n <div class=\"pt-change pt-change-live\" *ngIf=\"result?.changeGiven\">\n <span class=\"pt-label\">Change due</span>\n <span class=\"pt-change-value\">{{ result?.changeGiven | number:'1.2-2' }}</span>\n </div>\n\n <div class=\"pt-done-note\" *ngIf=\"!result?.changeGiven\">\n <mat-icon aria-hidden=\"true\">check_circle</mat-icon>\n <span>Paid in full. No change to give.</span>\n </div>\n\n <p class=\"pt-sale-number\" *ngIf=\"result?.saleNumber\">Sale {{ result?.saleNumber }}</p>\n\n <p class=\"pt-message\" *ngIf=\"receiptMessage\" role=\"alert\">{{ receiptMessage }}</p>\n\n <div class=\"pt-done-actions\">\n <!-- The label says what actually happens. The receipt arrives as a PDF the operator prints for the\n customer (fsd.md A5 \u2014 printing is the browser's print of the PDF, not a thermal driver), and a\n button promising \"Print\" that produces a file in a downloads folder is a small lie the operator\n pays for at the counter. -->\n <button type=\"button\" class=\"pt-secondary\" [disabled]=\"fetchingReceipt\" (click)=\"downloadReceipt()\">\n <mat-spinner diameter=\"20\" *ngIf=\"fetchingReceipt\"></mat-spinner>\n <mat-icon aria-hidden=\"true\" *ngIf=\"!fetchingReceipt\">receipt_long</mat-icon>\n <span>{{ fetchingReceipt ? 'Preparing receipt' : 'Download receipt' }}</span>\n </button>\n </div>\n\n <p class=\"pt-done-hint\">The receipt opens as a PDF. Print it for the customer.</p>\n </div>\n\n <button type=\"button\" class=\"pt-primary\" (click)=\"nextSale()\">\n <mat-icon aria-hidden=\"true\">arrow_forward</mat-icon>\n <span>Next sale</span>\n </button>\n </ng-container>\n </div>\n</div>\n", styles: [":host{display:block}.pt-scrim{position:absolute;inset:0;z-index:950;display:flex;align-items:center;justify-content:center;padding:16px;background:#00000073}.pt-sheet{display:flex;flex-direction:column;width:100%;max-width:860px;height:min(660px,100%);max-height:100%;overflow:hidden;background:#fff;border-radius:12px;box-shadow:0 18px 48px #00000052}.pt-head{flex:0 0 auto;display:flex;align-items:center;gap:12px;padding:12px 12px 12px 20px;border-bottom:1px solid var(--pos-line)}.pt-head-title{font-size:13px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#0000008c}.pt-head-done{background:var(--pos-money);border-bottom:0}.pt-head-done .pt-head-title{color:#fff}.pt-close{margin-left:auto;display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:0;border-radius:8px;background:transparent;color:#00000080;cursor:pointer}.pt-close:hover{background:#0000000d}.pt-close:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pt-body{flex:1 1 auto;min-height:0;overflow-y:auto;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.pt-readout{padding:20px;display:flex;flex-direction:column;gap:14px;border-right:1px solid var(--pos-line)}.pt-input{padding:16px 20px 20px;display:flex;flex-direction:column;gap:14px}.pt-label{font-size:12px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#00000080}.pt-num{font-size:18px;font-weight:600;font-variant-numeric:tabular-nums;color:#000c}.pt-due{display:flex;flex-direction:column;gap:2px}.pt-due-value{font-size:34px;font-weight:700;line-height:1.1;letter-spacing:-.5px;font-variant-numeric:tabular-nums;color:var(--pos-accent)}.pt-row{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-top:12px;border-top:1px solid var(--pos-line)}.pt-change{display:flex;flex-direction:column;gap:2px;justify-content:center;min-height:116px;padding:12px 16px;border-radius:10px;background:#f5f5f5;border:1px solid var(--pos-line)}.pt-change-value{font-size:64px;font-weight:700;line-height:1;letter-spacing:-2px;font-variant-numeric:tabular-nums;color:#0000004d}.pt-change-live{background:#edf7ee;border-color:#2e7d3259}.pt-change-live .pt-change-value,.pt-change-live .pt-label{color:var(--pos-money)}.pt-change-short{background:#fdecea;border-color:#c628284d}.pt-change-short .pt-change-value,.pt-change-short .pt-label{color:#c62828}.pt-change-refused{background:#fdecea;border-color:#c628284d;border-left:6px solid #c62828;gap:6px}.pt-change-refused .pt-label{color:#c62828}.pt-refusal{font-size:24px;font-weight:600;line-height:1.25;color:#8e1c1c;max-width:28ch}.pt-back{align-self:flex-start}.pt-settle{margin:0;font-size:13.5px;color:var(--pos-quiet);max-width:34ch}.pt-message{margin:0;font-size:13.5px;font-weight:500;color:#c62828}.pt-methods{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.pt-method{display:inline-flex;align-items:center;gap:8px;min-height:48px;padding:0 12px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;font:inherit;font-size:13.5px;color:#000000b3;cursor:pointer;text-align:left}.pt-method mat-icon{font-size:20px;width:20px;height:20px;flex:0 0 auto}.pt-method span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pt-method:hover{border-color:#90a4ae}.pt-method:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pt-method.active{border-color:var(--pos-accent);background:var(--pos-accent-soft);color:var(--pos-accent);font-weight:600}.pt-quick{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.pt-cash{position:relative;min-height:56px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;font:inherit;font-size:16px;font-weight:600;font-variant-numeric:tabular-nums;color:#000c;cursor:pointer}.pt-cash:hover{border-color:var(--pos-accent);color:var(--pos-accent)}.pt-cash:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pt-cash.exact{border-color:var(--pos-accent);background:var(--pos-accent-soft);color:var(--pos-accent)}.pt-cash-tag{position:absolute;top:4px;left:0;right:0;font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;opacity:.7}.pt-keypad{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.pt-key{min-height:56px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--pos-line);border-radius:8px;background:#fff;font:inherit;font-size:20px;font-weight:600;font-variant-numeric:tabular-nums;color:#000c;cursor:pointer}.pt-key:hover{border-color:var(--pos-accent)}.pt-key:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pt-key mat-icon{font-size:22px;width:22px;height:22px}.pt-key-clear{font-size:14px;letter-spacing:.04em;color:#0000008c}.pt-primary{flex:0 0 auto;display:flex;align-items:center;justify-content:center;gap:10px;width:100%;min-height:88px;border:0;background:var(--pos-money);color:#fff;font:inherit;font-size:21px;font-weight:700;letter-spacing:.03em;cursor:pointer;transition:background .12s}.pt-primary mat-icon{font-size:26px;width:26px;height:26px}.pt-primary:hover:not(:disabled){background:#1b5e20}.pt-primary:focus-visible{outline:3px solid #1b5e20;outline-offset:-6px}.pt-primary:disabled{background:#0000001a;color:#00000059;cursor:default}.pt-primary ::ng-deep circle{stroke:#fff}.pt-done{flex:1 1 auto;min-height:0;overflow-y:auto;padding:20px;display:flex;flex-direction:column;justify-content:center;gap:14px}.pt-done-note{display:flex;align-items:center;gap:10px;font-size:16px;font-weight:500;color:var(--pos-money)}.pt-done-note mat-icon{font-size:26px;width:26px;height:26px}.pt-sale-number{margin:0;font-size:13.5px;font-variant-numeric:tabular-nums;color:var(--pos-quiet)}.pt-done-actions{display:flex;gap:10px;flex-wrap:wrap}.pt-done-hint{margin:0;font-size:12.5px;color:var(--pos-quiet)}.pt-secondary{display:inline-flex;align-items:center;gap:8px;min-height:48px;padding:0 18px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;font:inherit;font-size:14.5px;font-weight:600;color:var(--pos-accent);cursor:pointer}.pt-secondary:hover:not(:disabled){border-color:var(--pos-accent);background:var(--pos-accent-soft)}.pt-secondary:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pt-secondary:disabled{color:var(--pos-quiet);cursor:default}.pt-secondary mat-icon{font-size:20px;width:20px;height:20px}@media (max-width: 720px){.pt-scrim{padding:0}.pt-sheet{max-width:none;height:100%;max-height:100%;border-radius:0}.pt-body{display:flex;flex-direction:column;overflow:hidden}.pt-readout{flex:0 0 auto;border-right:0;border-bottom:1px solid var(--pos-line);padding:14px 16px;gap:8px}.pt-input{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch}.pt-due-value{font-size:28px}.pt-row{padding-top:8px}.pt-change{min-height:88px;padding:8px 16px}.pt-change-value{font-size:52px}.pt-refusal{font-size:19px}}@media (prefers-reduced-motion: reduce){.pt-primary{transition:none}}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "pipe", type: i1.DecimalPipe, name: "number" }] }); }
|
|
933
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PosTenderComponent, isStandalone: false, selector: "spa-pos-tender", outputs: { cancelled: "cancelled", completed: "completed" }, ngImport: i0, template: "<!-- Added (POS Phase 5): the tender surface. Two faces over one frame \u2014 take the money, then the sale is done.\n The frame never moves between them, so the primary button stays under the same finger. -->\n\n<div class=\"pt-scrim\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"isComplete ? 'Sale complete' : 'Take payment'\">\n <div class=\"pt-sheet\">\n\n <!-- ================= FACE 1: taking the money ================= -->\n <ng-container *ngIf=\"!isComplete\">\n\n <header class=\"pt-head\">\n <span class=\"pt-head-title\">Take payment</span>\n <button type=\"button\" class=\"pt-close\" aria-label=\"Back to the sale\" (click)=\"cancel()\">\n <mat-icon aria-hidden=\"true\">close</mat-icon>\n </button>\n </header>\n\n <div class=\"pt-body\">\n\n <!-- LEFT: the readout. Nothing here is typed into; it is what the operator reads to the customer. -->\n <section class=\"pt-readout\" aria-label=\"Payment summary\">\n <div class=\"pt-due\">\n <span class=\"pt-label\">Amount due</span>\n <span class=\"pt-due-value\">{{ amountDue | number:'1.2-2' }}</span>\n </div>\n\n <div class=\"pt-row\" *ngIf=\"isCash\">\n <span class=\"pt-label\">Cash received</span>\n <span class=\"pt-num\">{{ session.tender.amountTendered == null ? '\u2014' : (session.tender.amountTendered | number:'1.2-2') }}</span>\n </div>\n\n <!-- THE FIXED SLOT. One rectangle, four states, always in the same place on the screen.\n Changed (ARBITER RULING A-0831-29): a REFUSAL is the fourth, and it REPLACES the other three\n rather than sitting under them. A green change figure over a sale the server refused is the one\n thing this surface must never show. -->\n <div class=\"pt-change pt-change-refused\" *ngIf=\"refused\" role=\"alert\">\n <span class=\"pt-label\">Sale not completed</span>\n <span class=\"pt-refusal\">{{ refusalMessage }}</span>\n </div>\n\n <ng-container *ngIf=\"!refused\">\n <div class=\"pt-change\" *ngIf=\"isCash\" [class.pt-change-short]=\"shortfall > 0\" [class.pt-change-live]=\"shortfall === 0 && session.tender.amountTendered != null\">\n <span class=\"pt-label\">{{ shortfall > 0 ? 'Still to pay' : 'Change due' }}</span>\n <span class=\"pt-change-value\" aria-live=\"polite\">{{ (shortfall > 0 ? shortfall : session.changeDue) | number:'1.2-2' }}</span>\n </div>\n\n <p class=\"pt-settle\" *ngIf=\"!isCash\">This payment settles the sale exactly. There is no change to give.</p>\n </ng-container>\n\n <!-- The only way forward: the cart is frozen behind this overlay, so a second identical submit refuses\n identically. The button names what happens when it is used. -->\n <button type=\"button\" class=\"pt-secondary pt-back\" *ngIf=\"refused\" (click)=\"cancel()\">\n <mat-icon aria-hidden=\"true\">arrow_back</mat-icon>\n <span>Back to the sale</span>\n </button>\n\n <p class=\"pt-message\" *ngIf=\"message && !refused\" role=\"alert\">{{ message }}</p>\n </section>\n\n <!-- RIGHT: everything the operator touches. -->\n <section class=\"pt-input\" aria-label=\"Payment entry\">\n\n <div class=\"pt-methods\" role=\"group\" aria-label=\"Payment method\">\n <button type=\"button\" class=\"pt-method\" *ngFor=\"let method of methods\"\n [class.active]=\"session.tender.method === method.value\"\n [attr.aria-pressed]=\"session.tender.method === method.value\"\n (click)=\"setMethod(method.value)\">\n <mat-icon aria-hidden=\"true\">{{ method.icon }}</mat-icon>\n <span>{{ method.label }}</span>\n </button>\n </div>\n\n <ng-container *ngIf=\"isCash\">\n <!-- Quick cash first: the exact amount leads, then the notes a customer is likely to hand over.\n One tap settles most sales without the keypad being touched at all. -->\n <div class=\"pt-quick\">\n <button type=\"button\" class=\"pt-cash\" *ngFor=\"let amount of quickAmounts; let first = first\"\n [class.exact]=\"first\" (click)=\"quick(amount)\">\n <span class=\"pt-cash-tag\" *ngIf=\"first\">Exact</span>\n {{ amount | number:'1.2-2' }}\n </button>\n </div>\n\n <div class=\"pt-keypad\" role=\"group\" aria-label=\"Amount keypad\">\n <button type=\"button\" class=\"pt-key\" *ngFor=\"let key of keys\" (click)=\"press(key)\">{{ key }}</button>\n <button type=\"button\" class=\"pt-key pt-key-wide\" aria-label=\"Delete the last digit\" (click)=\"backspace()\">\n <mat-icon aria-hidden=\"true\">backspace</mat-icon>\n </button>\n <button type=\"button\" class=\"pt-key pt-key-clear\" (click)=\"clearEntry()\">Clear</button>\n </div>\n </ng-container>\n </section>\n </div>\n\n <!-- Fused to the foot of the sheet, exactly as CHARGE is fused to the plinth. Same shape, same height,\n same place \u2014 the second half of a two-tap sale. -->\n <button type=\"button\" class=\"pt-primary\" [disabled]=\"!canComplete\" (click)=\"complete()\">\n <mat-spinner diameter=\"24\" *ngIf=\"session.submitting\"></mat-spinner>\n <mat-icon aria-hidden=\"true\" *ngIf=\"!session.submitting\">check</mat-icon>\n <span>{{ session.submitting ? 'Completing' : 'Complete sale' }}</span>\n </button>\n </ng-container>\n\n <!-- ================= FACE 2: the sale is done ================= -->\n <ng-container *ngIf=\"isComplete\">\n <header class=\"pt-head pt-head-done\">\n <span class=\"pt-head-title\">Sale complete</span>\n </header>\n\n <div class=\"pt-done\">\n <!-- The change is the only thing that matters in the two seconds after a sale: it is money the operator\n owes the customer from the drawer. It keeps the slot and the size the live readout had. -->\n <div class=\"pt-change pt-change-live\" *ngIf=\"result?.changeGiven\">\n <span class=\"pt-label\">Change due</span>\n <span class=\"pt-change-value\">{{ result?.changeGiven | number:'1.2-2' }}</span>\n </div>\n\n <div class=\"pt-done-note\" *ngIf=\"!result?.changeGiven\">\n <mat-icon aria-hidden=\"true\">check_circle</mat-icon>\n <span>Paid in full. No change to give.</span>\n </div>\n\n <p class=\"pt-sale-number\" *ngIf=\"result?.saleNumber\">Sale {{ result?.saleNumber }}</p>\n\n <p class=\"pt-message\" *ngIf=\"receiptMessage\" role=\"alert\">{{ receiptMessage }}</p>\n\n <div class=\"pt-done-actions\">\n <!-- The label says what actually happens. The receipt arrives as a PDF the operator prints for the\n customer (fsd.md A5 \u2014 printing is the browser's print of the PDF, not a thermal driver), and a\n button promising \"Print\" that produces a file in a downloads folder is a small lie the operator\n pays for at the counter. -->\n <button type=\"button\" class=\"pt-secondary\" [disabled]=\"fetchingReceipt\" (click)=\"downloadReceipt()\">\n <mat-spinner diameter=\"20\" *ngIf=\"fetchingReceipt\"></mat-spinner>\n <mat-icon aria-hidden=\"true\" *ngIf=\"!fetchingReceipt\">receipt_long</mat-icon>\n <span>{{ fetchingReceipt ? 'Preparing receipt' : 'Download receipt' }}</span>\n </button>\n </div>\n\n <p class=\"pt-done-hint\">The receipt opens as a PDF. Print it for the customer.</p>\n </div>\n\n <button type=\"button\" class=\"pt-primary\" (click)=\"nextSale()\">\n <mat-icon aria-hidden=\"true\">arrow_forward</mat-icon>\n <span>Next sale</span>\n </button>\n </ng-container>\n </div>\n</div>\n", styles: [":host{display:block}.pt-scrim{position:absolute;inset:0;z-index:950;display:flex;align-items:center;justify-content:center;padding:16px;background:#00000073}.pt-sheet{display:flex;flex-direction:column;width:100%;max-width:860px;height:min(660px,100%);max-height:100%;overflow:hidden;background:#fff;border-radius:12px;box-shadow:0 18px 48px #00000052}.pt-head{flex:0 0 auto;display:flex;align-items:center;gap:12px;padding:12px 12px 12px 20px;border-bottom:1px solid var(--pos-line)}.pt-head-title{font-size:13px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#0000008c}.pt-head-done{background:var(--pos-money);border-bottom:0}.pt-head-done .pt-head-title{color:#fff}.pt-close{margin-left:auto;display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:0;border-radius:8px;background:transparent;color:#00000080;cursor:pointer}.pt-close:hover{background:#0000000d}.pt-close:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pt-body{flex:1 1 auto;min-height:0;overflow-y:auto;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.pt-readout{padding:20px;display:flex;flex-direction:column;gap:14px;border-right:1px solid var(--pos-line)}.pt-input{padding:16px 20px 20px;display:flex;flex-direction:column;gap:14px}.pt-label{font-size:12px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#00000080}.pt-num{font-size:18px;font-weight:600;font-variant-numeric:tabular-nums;color:#000c}.pt-due{display:flex;flex-direction:column;gap:2px}.pt-due-value{font-size:34px;font-weight:700;line-height:1.1;letter-spacing:-.5px;font-variant-numeric:tabular-nums;color:var(--pos-accent)}.pt-row{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-top:12px;border-top:1px solid var(--pos-line)}.pt-change{display:flex;flex-direction:column;gap:2px;justify-content:center;min-height:116px;padding:12px 16px;border-radius:10px;background:#f5f5f5;border:1px solid var(--pos-line)}.pt-change-value{font-size:64px;font-weight:700;line-height:1;letter-spacing:-2px;font-variant-numeric:tabular-nums;color:#0000004d}.pt-change-live{background:#edf7ee;border-color:#2e7d3259}.pt-change-live .pt-change-value,.pt-change-live .pt-label{color:var(--pos-money)}.pt-change-short{background:#fdecea;border-color:#c628284d}.pt-change-short .pt-change-value,.pt-change-short .pt-label{color:#c62828}.pt-change-refused{background:#fdecea;border-color:#c628284d;border-left:6px solid #c62828;gap:6px}.pt-change-refused .pt-label{color:#c62828}.pt-refusal{font-size:24px;font-weight:600;line-height:1.25;color:#8e1c1c;max-width:28ch}.pt-back{align-self:flex-start}.pt-settle{margin:0;font-size:13.5px;color:var(--pos-quiet);max-width:34ch}.pt-message{margin:0;font-size:13.5px;font-weight:500;color:#c62828}.pt-methods{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.pt-method{display:inline-flex;align-items:center;gap:8px;min-height:48px;padding:0 12px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;font:inherit;font-size:13.5px;color:#000000b3;cursor:pointer;text-align:left}.pt-method mat-icon{font-size:20px;width:20px;height:20px;flex:0 0 auto}.pt-method span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pt-method:hover{border-color:#90a4ae}.pt-method:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pt-method.active{border-color:var(--pos-accent);background:var(--pos-accent-soft);color:var(--pos-accent);font-weight:600}.pt-quick{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.pt-cash{position:relative;min-height:56px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;font:inherit;font-size:16px;font-weight:600;font-variant-numeric:tabular-nums;color:#000c;cursor:pointer}.pt-cash:hover{border-color:var(--pos-accent);color:var(--pos-accent)}.pt-cash:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pt-cash.exact{border-color:var(--pos-accent);background:var(--pos-accent-soft);color:var(--pos-accent)}.pt-cash-tag{position:absolute;top:4px;left:0;right:0;font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;opacity:.7}.pt-keypad{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.pt-key{min-height:56px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--pos-line);border-radius:8px;background:#fff;font:inherit;font-size:20px;font-weight:600;font-variant-numeric:tabular-nums;color:#000c;cursor:pointer}.pt-key:hover{border-color:var(--pos-accent)}.pt-key:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pt-key mat-icon{font-size:22px;width:22px;height:22px}.pt-key-clear{font-size:14px;letter-spacing:.04em;color:#0000008c}.pt-primary{flex:0 0 auto;display:flex;align-items:center;justify-content:center;gap:10px;width:100%;min-height:88px;border:0;background:var(--pos-money);color:#fff;font:inherit;font-size:21px;font-weight:700;letter-spacing:.03em;cursor:pointer;transition:background .12s}.pt-primary mat-icon{font-size:26px;width:26px;height:26px}.pt-primary:hover:not(:disabled){background:#1b5e20}.pt-primary:focus-visible{outline:3px solid #1b5e20;outline-offset:-6px}.pt-primary:disabled{background:#0000001a;color:#00000059;cursor:default}.pt-primary ::ng-deep circle{stroke:#fff}.pt-done{flex:1 1 auto;min-height:0;overflow-y:auto;padding:20px;display:flex;flex-direction:column;justify-content:center;gap:14px}.pt-done-note{display:flex;align-items:center;gap:10px;font-size:16px;font-weight:500;color:var(--pos-money)}.pt-done-note mat-icon{font-size:26px;width:26px;height:26px}.pt-sale-number{margin:0;font-size:13.5px;font-variant-numeric:tabular-nums;color:var(--pos-quiet)}.pt-done-actions{display:flex;gap:10px;flex-wrap:wrap}.pt-done-hint{margin:0;font-size:12.5px;color:var(--pos-quiet)}.pt-secondary{display:inline-flex;align-items:center;gap:8px;min-height:48px;padding:0 18px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;font:inherit;font-size:14.5px;font-weight:600;color:var(--pos-accent);cursor:pointer}.pt-secondary:hover:not(:disabled){border-color:var(--pos-accent);background:var(--pos-accent-soft)}.pt-secondary:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pt-secondary:disabled{color:var(--pos-quiet);cursor:default}.pt-secondary mat-icon{font-size:20px;width:20px;height:20px}@media (max-width: 720px){.pt-scrim{padding:0}.pt-sheet{max-width:none;height:100%;max-height:100%;border-radius:0}.pt-body{display:flex;flex-direction:column;overflow:hidden}.pt-readout{flex:0 0 auto;border-right:0;border-bottom:1px solid var(--pos-line);padding:14px 16px;gap:8px}.pt-input{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch}.pt-due-value{font-size:28px}.pt-row{padding-top:8px}.pt-change{min-height:88px;padding:8px 16px}.pt-change-value{font-size:52px}.pt-refusal{font-size:19px}}@media (prefers-reduced-motion: reduce){.pt-primary{transition:none}}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }] }); }
|
|
911
934
|
}
|
|
912
935
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PosTenderComponent, decorators: [{
|
|
913
936
|
type: Component,
|
|
@@ -1028,7 +1051,7 @@ class PosCompactComponent {
|
|
|
1028
1051
|
this.charge.emit();
|
|
1029
1052
|
}
|
|
1030
1053
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PosCompactComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1031
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PosCompactComponent, isStandalone: false, selector: "spa-pos-compact", inputs: { title: "title" }, outputs: { charge: "charge" }, ngImport: i0, template: "<!-- Added (POS Phase 6): the phone-portrait till. Two views in the scrolling middle; ONE plinth docked below\n both of them. Nothing outside this file computes a figure on this screen. -->\n\n<div class=\"pc-till\" [attr.aria-label]=\"title\">\n\n <!-- ================= VIEW 1: the catalogue ================= -->\n <ng-container *ngIf=\"view === 'catalogue'\">\n\n <!-- The scan box is the till's home key and it is pinned. On a phone it takes the whole width: there is no\n page title beside it to demote, because the app chrome above already carries one. -->\n <header class=\"pc-bar\">\n <div class=\"pc-scan\">\n <mat-icon class=\"pc-scan-icon\" aria-hidden=\"true\">qr_code_scanner</mat-icon>\n <input #scan class=\"pc-scan-input\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\"\n placeholder=\"Scan or search\" aria-label=\"Scan a barcode or search by name\"\n [value]=\"searchTerm\" (input)=\"onSearch(scan.value)\" (keyup.enter)=\"onSearchEnter()\">\n <button type=\"button\" class=\"pc-scan-clear\" *ngIf=\"searchTerm\" aria-label=\"Clear the search\"\n (click)=\"onSearch(''); scan.value = ''\">\n <mat-icon aria-hidden=\"true\">close</mat-icon>\n </button>\n </div>\n </header>\n\n <!-- Category pills, the same control as the wide till's \u2014 but a single row that scrolls sideways rather\n than wrapping. Wrapping six categories on a 390px screen eats three rows of the product grid. -->\n <div class=\"pc-tabs\" role=\"tablist\" aria-label=\"Product categories\" *ngIf=\"session.categories.length > 0\">\n <button type=\"button\" class=\"pc-tab\" role=\"tab\" [class.active]=\"session.activeCategory === ''\"\n [attr.aria-selected]=\"session.activeCategory === ''\" (click)=\"session.setCategory('')\">All</button>\n <button type=\"button\" class=\"pc-tab\" role=\"tab\" *ngFor=\"let category of session.categories\"\n [class.active]=\"session.activeCategory === category\"\n [attr.aria-selected]=\"session.activeCategory === category\"\n (click)=\"session.setCategory(category)\">{{ category }}</button>\n </div>\n\n <section class=\"pc-scroll\" aria-label=\"Products\">\n <div class=\"pc-grid\" *ngIf=\"session.filtered.length > 0\">\n <spa-pos-tile *ngFor=\"let product of session.filtered\" [product]=\"product\"\n [inCart]=\"countOf(product)\" (picked)=\"pick($event)\"></spa-pos-tile>\n </div>\n\n <div class=\"pc-empty\" *ngIf=\"session.filtered.length === 0 && !session.loadingProducts\">\n <mat-icon aria-hidden=\"true\">search_off</mat-icon>\n <p *ngIf=\"searchTerm\">Nothing matches “{{ searchTerm }}”. Clear the search to see every product.</p>\n <p *ngIf=\"!searchTerm\">No products are available on this till yet.</p>\n </div>\n\n <div class=\"pc-empty\" *ngIf=\"session.loadingProducts\">\n <mat-spinner diameter=\"28\"></mat-spinner>\n <p>Loading products</p>\n </div>\n </section>\n </ng-container>\n\n <!-- ================= VIEW 2: the ticket ================= -->\n <ng-container *ngIf=\"view === 'ticket'\">\n\n <header class=\"pc-bar pc-bar-ticket\">\n <button type=\"button\" class=\"pc-back\" aria-label=\"Back to the products\" (click)=\"showCatalogue()\">\n <mat-icon aria-hidden=\"true\">arrow_back</mat-icon>\n </button>\n <span class=\"pc-bar-title\">Current sale</span>\n <button type=\"button\" class=\"pc-clear\" *ngIf=\"session.lines.length > 0\" (click)=\"clear()\">Clear</button>\n </header>\n\n <section class=\"pc-scroll\" aria-label=\"Current sale\">\n\n <ul class=\"pc-lines\" *ngIf=\"session.lines.length > 0\">\n <li class=\"pc-line\" *ngFor=\"let line of session.lines\">\n <!-- Name across the full width, controls beneath it \u2014 the two-row line Phase 4 arrived at by looking\n at the render, and the reason holds harder here: an operator who cannot read what they are\n charging for cannot check it against the goods on the counter. -->\n <!-- \uD83C\uDFA8 Changed after looking at the render: the extension sits BESIDE THE NAME, the way a paper slip\n reads \u2014 description left, money right. It was under the stepper, which on a 393px screen left a\n ~200px hole of nothing between the \"+\" button and the figure, and separated the two things the\n operator is actually comparing: what it is, and what it costs. -->\n <div class=\"pc-line-text\">\n <span class=\"pc-line-name\">{{ line.name }}</span>\n <!-- The tile price multiplied out (Arbiter A-0831-23), identical to the wide ticket: every line\n multiplies visibly, and VAT is told once, below. -->\n <span class=\"pc-line-total\">{{ line.shelfAmount | number:'1.2-2' }}</span>\n </div>\n\n <div class=\"pc-line-controls\">\n <div class=\"pc-qty\" role=\"group\" [attr.aria-label]=\"line.name + ' quantity'\">\n <button type=\"button\" class=\"pc-step\" [attr.aria-label]=\"'One less ' + line.name\" (click)=\"decrement(line)\">\n <mat-icon aria-hidden=\"true\">remove</mat-icon>\n </button>\n <span class=\"pc-qty-value\" aria-live=\"polite\">{{ line.quantity | number:'1.0-2' }}</span>\n <button type=\"button\" class=\"pc-step\" [attr.aria-label]=\"'One more ' + line.name\" (click)=\"increment(line)\">\n <mat-icon aria-hidden=\"true\">add</mat-icon>\n </button>\n </div>\n\n <span class=\"pc-line-unit\">\n {{ line.shelfUnitPrice | number:'1.2-2' }} each × {{ line.quantity | number:'1.0-2' }}<span *ngIf=\"line.isTaxInclusive && line.taxRate > 0\"> · incl. VAT</span>\n </span>\n\n <button type=\"button\" class=\"pc-remove\" [attr.aria-label]=\"'Remove ' + line.name\" (click)=\"remove(line)\">\n <mat-icon aria-hidden=\"true\">delete_outline</mat-icon>\n </button>\n </div>\n </li>\n </ul>\n\n <!-- \u2702\uFE0F The CUT totals block lands here rather than in the plinth. On the wide till Subtotal and VAT sit\n above the total; on a phone that would push CHARGE up the screen on every screen, for two numbers\n the operator only wants when they are checking the sale \u2014 which is precisely this view. -->\n <div class=\"pc-sums\" *ngIf=\"session.lines.length > 0\">\n <!-- Changed (Arbiter, smalls batch 2026-08-31): \"Subtotal\" \u2192 \"Items\" \u2014 matches pos-wide; the printed receipt keeps\n its NET Subtotal, the screen names the shelf value truthfully. Value unchanged. -->\n <div class=\"pc-sum\"><span>Items</span><span class=\"pc-num\">{{ session.totals.shelfTotal | number:'1.2-2' }}</span></div>\n <div class=\"pc-sum\"><span>VAT added</span><span class=\"pc-num\">{{ session.totals.taxAdded | number:'1.2-2' }}</span></div>\n </div>\n\n <div class=\"pc-empty\" *ngIf=\"session.lines.length === 0\">\n <mat-icon aria-hidden=\"true\">shopping_cart</mat-icon>\n <p>Nothing on this sale yet. Go back and scan or tap an item.</p>\n </div>\n </section>\n </ng-container>\n\n <!-- ================= THE PLINTH \u2014 docked, identical in both views =================\n The one fixed object on a screen where everything else scrolls. The count row is also the door to the\n ticket; the total sits beneath it; CHARGE is fused to the foot, square-topped and full-bleed. -->\n <div class=\"pc-plinth\">\n\n <button type=\"button\" class=\"pc-review\" (click)=\"toggleView()\"\n [attr.aria-label]=\"view === 'ticket' ? 'Back to the products' : 'Review the sale'\">\n <span class=\"pc-review-count\">{{ session.totals.itemCount }} {{ session.totals.itemCount === 1 ? 'line' : 'lines' }}<span *ngIf=\"session.totals.unitCount > 0\"> · {{ session.totals.unitCount | number:'1.0-2' }} items</span></span>\n <span class=\"pc-review-action\">{{ view === 'ticket' ? 'Add items' : 'Review sale' }}</span>\n <mat-icon class=\"pc-review-chevron\" aria-hidden=\"true\">{{ view === 'ticket' ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n\n <div class=\"pc-total\">\n <span class=\"pc-total-label\">Total</span>\n <span class=\"pc-total-value\" aria-live=\"polite\">{{ session.totals.total | number:'1.2-2' }}</span>\n </div>\n\n <button type=\"button\" class=\"pc-charge\" [disabled]=\"!session.canCharge\" (click)=\"onCharge()\">\n <mat-icon aria-hidden=\"true\">point_of_sale</mat-icon>\n <span>Charge</span>\n </button>\n </div>\n</div>\n", styles: [":host{display:block;height:100%;min-height:0}.pc-till{height:100%;min-height:0;display:flex;flex-direction:column;background:#fafafa}.pc-bar{flex:0 0 auto;display:flex;align-items:center;gap:8px;padding:10px 12px;background:#fff;border-bottom:1px solid var(--pos-line)}.pc-bar-ticket{padding:6px 12px 6px 4px}.pc-bar-title{font-size:13px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#0000008c}.pc-back{display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:0;border-radius:8px;background:transparent;color:#0009;cursor:pointer}.pc-back:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pc-clear{margin-left:auto;min-height:48px;padding:0 14px;border:0;border-radius:8px;background:transparent;font:inherit;font-size:14px;color:#c62828;cursor:pointer}.pc-clear:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pc-scan{position:relative;display:flex;align-items:center;gap:8px;flex:1 1 auto;min-width:0;min-height:48px;padding:0 4px 0 12px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;transition:border-color .12s,box-shadow .12s}.pc-scan:focus-within{border-color:var(--pos-accent);box-shadow:0 0 0 3px var(--pos-accent-soft)}.pc-scan-icon{flex:0 0 auto;font-size:22px;width:22px;height:22px;color:#00000073}.pc-scan-input{flex:1 1 auto;min-width:0;border:0;outline:0;background:transparent;font:inherit;font-size:16px;min-height:48px;color:#000000de}.pc-scan-clear{display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:0;border-radius:8px;background:transparent;color:#00000080;cursor:pointer}.pc-scan-clear:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pc-tabs{flex:0 0 auto;display:flex;gap:8px;padding:10px 12px;overflow-x:auto;background:#fff;border-bottom:1px solid var(--pos-line);scrollbar-width:none}.pc-tabs::-webkit-scrollbar{display:none}.pc-tab{flex:0 0 auto;display:inline-flex;align-items:center;min-height:48px;padding:0 18px;border:1px solid var(--pos-line);border-radius:24px;background:#fff;font:inherit;font-size:14px;white-space:nowrap;color:#000000b3;cursor:pointer}.pc-tab:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pc-tab.active{border-color:var(--pos-accent);background:var(--pos-accent-soft);color:var(--pos-accent);font-weight:600}.pc-scroll{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch}.pc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(144px,1fr));gap:10px;padding:12px;align-items:stretch}.pc-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:48px 24px;color:var(--pos-quiet);text-align:center}.pc-empty mat-icon{font-size:36px;width:36px;height:36px;color:#00000026}.pc-empty p{margin:0;font-size:13.5px;max-width:28ch}.pc-lines{list-style:none;margin:0;padding:0}.pc-line{display:flex;flex-direction:column;gap:6px;padding:10px 12px;background:#fff;border-bottom:1px solid var(--pos-line)}.pc-line-text{display:flex;align-items:baseline;gap:12px;min-width:0}.pc-line-name{flex:1 1 auto;min-width:0;font-size:15px;font-weight:500;line-height:1.3;color:#000000de;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.pc-line-controls{display:flex;align-items:center;gap:10px}.pc-line-unit{flex:1 1 auto;min-width:0;font-size:12.5px;color:var(--pos-quiet);font-variant-numeric:tabular-nums}.pc-qty{display:inline-flex;align-items:center;flex:0 0 auto}.pc-step{display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;color:#000000b3;cursor:pointer}.pc-step:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pc-step mat-icon{font-size:20px;width:20px;height:20px}.pc-qty-value{min-width:44px;text-align:center;font-size:16px;font-weight:600;font-variant-numeric:tabular-nums}.pc-line-total{flex:0 0 auto;text-align:right;font-size:16px;font-weight:600;font-variant-numeric:tabular-nums;color:#000000d9}.pc-remove{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:0;border-radius:8px;background:transparent;color:#00000061;cursor:pointer}.pc-remove:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pc-remove mat-icon{font-size:20px;width:20px;height:20px}.pc-sums{padding:12px 12px 20px;background:#fff}.pc-sum{display:flex;justify-content:space-between;align-items:baseline;padding:3px 0;font-size:13px;color:var(--pos-quiet)}.pc-num{font-variant-numeric:tabular-nums;color:#000000b3}.pc-plinth{flex:0 0 auto;background:#fff;box-shadow:0 -2px 10px #0000001a}.pc-review{display:flex;align-items:center;gap:8px;width:100%;min-height:52px;padding:0 8px 0 16px;border:0;border-bottom:1px solid var(--pos-line);background:transparent;font:inherit;text-align:left;cursor:pointer}.pc-review:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pc-review-count{font-size:13px;color:var(--pos-quiet);font-variant-numeric:tabular-nums}.pc-review-action{margin-left:auto;font-size:13px;font-weight:600;color:var(--pos-accent)}.pc-review-chevron{flex:0 0 auto;font-size:22px;width:22px;height:22px;color:var(--pos-accent)}.pc-total{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:10px 16px 12px}.pc-total-label{font-size:13px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#0000008c}.pc-total-value{font-size:34px;font-weight:700;line-height:1;letter-spacing:-1px;font-variant-numeric:tabular-nums;color:var(--pos-accent)}.pc-charge{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;min-height:88px;border:0;border-radius:0;background:var(--pos-accent);color:#fff;font:inherit;font-size:20px;font-weight:700;letter-spacing:.03em;cursor:pointer;transition:background .12s}.pc-charge mat-icon{font-size:25px;width:25px;height:25px}.pc-charge:focus-visible{outline:3px solid #0d47a1;outline-offset:-6px}.pc-charge:disabled{background:#0000001a;color:#00000059;cursor:default}@media (prefers-reduced-motion: reduce){.pc-scan,.pc-charge{transition:none}}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: PosTileComponent, selector: "spa-pos-tile", inputs: ["product", "inCart", "disabled"], outputs: ["picked"] }, { kind: "pipe", type: i1.DecimalPipe, name: "number" }] }); }
|
|
1054
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PosCompactComponent, isStandalone: false, selector: "spa-pos-compact", inputs: { title: "title" }, outputs: { charge: "charge" }, ngImport: i0, template: "<!-- Added (POS Phase 6): the phone-portrait till. Two views in the scrolling middle; ONE plinth docked below\n both of them. Nothing outside this file computes a figure on this screen. -->\n\n<div class=\"pc-till\" [attr.aria-label]=\"title\">\n\n <!-- ================= VIEW 1: the catalogue ================= -->\n <ng-container *ngIf=\"view === 'catalogue'\">\n\n <!-- The scan box is the till's home key and it is pinned. On a phone it takes the whole width: there is no\n page title beside it to demote, because the app chrome above already carries one. -->\n <header class=\"pc-bar\">\n <div class=\"pc-scan\">\n <mat-icon class=\"pc-scan-icon\" aria-hidden=\"true\">qr_code_scanner</mat-icon>\n <input #scan class=\"pc-scan-input\" type=\"text\" autocomplete=\"off\" spellcheck=\"false\"\n placeholder=\"Scan or search\" aria-label=\"Scan a barcode or search by name\"\n [value]=\"searchTerm\" (input)=\"onSearch(scan.value)\" (keyup.enter)=\"onSearchEnter()\">\n <button type=\"button\" class=\"pc-scan-clear\" *ngIf=\"searchTerm\" aria-label=\"Clear the search\"\n (click)=\"onSearch(''); scan.value = ''\">\n <mat-icon aria-hidden=\"true\">close</mat-icon>\n </button>\n </div>\n </header>\n\n <!-- Category pills, the same control as the wide till's \u2014 but a single row that scrolls sideways rather\n than wrapping. Wrapping six categories on a 390px screen eats three rows of the product grid. -->\n <div class=\"pc-tabs\" role=\"tablist\" aria-label=\"Product categories\" *ngIf=\"session.categories.length > 0\">\n <button type=\"button\" class=\"pc-tab\" role=\"tab\" [class.active]=\"session.activeCategory === ''\"\n [attr.aria-selected]=\"session.activeCategory === ''\" (click)=\"session.setCategory('')\">All</button>\n <button type=\"button\" class=\"pc-tab\" role=\"tab\" *ngFor=\"let category of session.categories\"\n [class.active]=\"session.activeCategory === category\"\n [attr.aria-selected]=\"session.activeCategory === category\"\n (click)=\"session.setCategory(category)\">{{ category }}</button>\n </div>\n\n <section class=\"pc-scroll\" aria-label=\"Products\">\n <div class=\"pc-grid\" *ngIf=\"session.filtered.length > 0\">\n <spa-pos-tile *ngFor=\"let product of session.filtered\" [product]=\"product\"\n [inCart]=\"countOf(product)\" (picked)=\"pick($event)\"></spa-pos-tile>\n </div>\n\n <div class=\"pc-empty\" *ngIf=\"session.filtered.length === 0 && !session.loadingProducts\">\n <mat-icon aria-hidden=\"true\">search_off</mat-icon>\n <p *ngIf=\"searchTerm\">Nothing matches “{{ searchTerm }}”. Clear the search to see every product.</p>\n <p *ngIf=\"!searchTerm\">No products are available on this till yet.</p>\n </div>\n\n <div class=\"pc-empty\" *ngIf=\"session.loadingProducts\">\n <mat-spinner diameter=\"28\"></mat-spinner>\n <p>Loading products</p>\n </div>\n </section>\n </ng-container>\n\n <!-- ================= VIEW 2: the ticket ================= -->\n <ng-container *ngIf=\"view === 'ticket'\">\n\n <header class=\"pc-bar pc-bar-ticket\">\n <button type=\"button\" class=\"pc-back\" aria-label=\"Back to the products\" (click)=\"showCatalogue()\">\n <mat-icon aria-hidden=\"true\">arrow_back</mat-icon>\n </button>\n <span class=\"pc-bar-title\">Current sale</span>\n <button type=\"button\" class=\"pc-clear\" *ngIf=\"session.lines.length > 0\" (click)=\"clear()\">Clear</button>\n </header>\n\n <section class=\"pc-scroll\" aria-label=\"Current sale\">\n\n <ul class=\"pc-lines\" *ngIf=\"session.lines.length > 0\">\n <li class=\"pc-line\" *ngFor=\"let line of session.lines\">\n <!-- Name across the full width, controls beneath it \u2014 the two-row line Phase 4 arrived at by looking\n at the render, and the reason holds harder here: an operator who cannot read what they are\n charging for cannot check it against the goods on the counter. -->\n <!-- \uD83C\uDFA8 Changed after looking at the render: the extension sits BESIDE THE NAME, the way a paper slip\n reads \u2014 description left, money right. It was under the stepper, which on a 393px screen left a\n ~200px hole of nothing between the \"+\" button and the figure, and separated the two things the\n operator is actually comparing: what it is, and what it costs. -->\n <div class=\"pc-line-text\">\n <span class=\"pc-line-name\">{{ line.name }}</span>\n <!-- The tile price multiplied out (Arbiter A-0831-23), identical to the wide ticket: every line\n multiplies visibly, and VAT is told once, below. -->\n <span class=\"pc-line-total\">{{ line.shelfAmount | number:'1.2-2' }}</span>\n </div>\n\n <div class=\"pc-line-controls\">\n <div class=\"pc-qty\" role=\"group\" [attr.aria-label]=\"line.name + ' quantity'\">\n <button type=\"button\" class=\"pc-step\" [attr.aria-label]=\"'One less ' + line.name\" (click)=\"decrement(line)\">\n <mat-icon aria-hidden=\"true\">remove</mat-icon>\n </button>\n <span class=\"pc-qty-value\" aria-live=\"polite\">{{ line.quantity | number:'1.0-2' }}</span>\n <button type=\"button\" class=\"pc-step\" [attr.aria-label]=\"'One more ' + line.name\" (click)=\"increment(line)\">\n <mat-icon aria-hidden=\"true\">add</mat-icon>\n </button>\n </div>\n\n <span class=\"pc-line-unit\">\n {{ line.shelfUnitPrice | number:'1.2-2' }} each × {{ line.quantity | number:'1.0-2' }}<span *ngIf=\"line.isTaxInclusive && line.taxRate > 0\"> · incl. VAT</span>\n </span>\n\n <button type=\"button\" class=\"pc-remove\" [attr.aria-label]=\"'Remove ' + line.name\" (click)=\"remove(line)\">\n <mat-icon aria-hidden=\"true\">delete_outline</mat-icon>\n </button>\n </div>\n </li>\n </ul>\n\n <!-- \u2702\uFE0F The CUT totals block lands here rather than in the plinth. On the wide till Subtotal and VAT sit\n above the total; on a phone that would push CHARGE up the screen on every screen, for two numbers\n the operator only wants when they are checking the sale \u2014 which is precisely this view. -->\n <div class=\"pc-sums\" *ngIf=\"session.lines.length > 0\">\n <!-- Changed (Arbiter, smalls batch 2026-08-31): \"Subtotal\" \u2192 \"Items\" \u2014 matches pos-wide; the printed receipt keeps\n its NET Subtotal, the screen names the shelf value truthfully. Value unchanged. -->\n <div class=\"pc-sum\"><span>Items</span><span class=\"pc-num\">{{ session.totals.shelfTotal | number:'1.2-2' }}</span></div>\n <div class=\"pc-sum\"><span>VAT added</span><span class=\"pc-num\">{{ session.totals.taxAdded | number:'1.2-2' }}</span></div>\n </div>\n\n <div class=\"pc-empty\" *ngIf=\"session.lines.length === 0\">\n <mat-icon aria-hidden=\"true\">shopping_cart</mat-icon>\n <p>Nothing on this sale yet. Go back and scan or tap an item.</p>\n </div>\n </section>\n </ng-container>\n\n <!-- ================= THE PLINTH \u2014 docked, identical in both views =================\n The one fixed object on a screen where everything else scrolls. The count row is also the door to the\n ticket; the total sits beneath it; CHARGE is fused to the foot, square-topped and full-bleed. -->\n <div class=\"pc-plinth\">\n\n <button type=\"button\" class=\"pc-review\" (click)=\"toggleView()\"\n [attr.aria-label]=\"view === 'ticket' ? 'Back to the products' : 'Review the sale'\">\n <span class=\"pc-review-count\">{{ session.totals.itemCount }} {{ session.totals.itemCount === 1 ? 'line' : 'lines' }}<span *ngIf=\"session.totals.unitCount > 0\"> · {{ session.totals.unitCount | number:'1.0-2' }} items</span></span>\n <span class=\"pc-review-action\">{{ view === 'ticket' ? 'Add items' : 'Review sale' }}</span>\n <mat-icon class=\"pc-review-chevron\" aria-hidden=\"true\">{{ view === 'ticket' ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n\n <div class=\"pc-total\">\n <span class=\"pc-total-label\">Total</span>\n <span class=\"pc-total-value\" aria-live=\"polite\">{{ session.totals.total | number:'1.2-2' }}</span>\n </div>\n\n <button type=\"button\" class=\"pc-charge\" [disabled]=\"!session.canCharge\" (click)=\"onCharge()\">\n <mat-icon aria-hidden=\"true\">point_of_sale</mat-icon>\n <span>Charge</span>\n </button>\n </div>\n</div>\n", styles: [":host{display:block;height:100%;min-height:0}.pc-till{height:100%;min-height:0;display:flex;flex-direction:column;background:#fafafa}.pc-bar{flex:0 0 auto;display:flex;align-items:center;gap:8px;padding:10px 12px;background:#fff;border-bottom:1px solid var(--pos-line)}.pc-bar-ticket{padding:6px 12px 6px 4px}.pc-bar-title{font-size:13px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#0000008c}.pc-back{display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:0;border-radius:8px;background:transparent;color:#0009;cursor:pointer}.pc-back:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pc-clear{margin-left:auto;min-height:48px;padding:0 14px;border:0;border-radius:8px;background:transparent;font:inherit;font-size:14px;color:#c62828;cursor:pointer}.pc-clear:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pc-scan{position:relative;display:flex;align-items:center;gap:8px;flex:1 1 auto;min-width:0;min-height:48px;padding:0 4px 0 12px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;transition:border-color .12s,box-shadow .12s}.pc-scan:focus-within{border-color:var(--pos-accent);box-shadow:0 0 0 3px var(--pos-accent-soft)}.pc-scan-icon{flex:0 0 auto;font-size:22px;width:22px;height:22px;color:#00000073}.pc-scan-input{flex:1 1 auto;min-width:0;border:0;outline:0;background:transparent;font:inherit;font-size:16px;min-height:48px;color:#000000de}.pc-scan-clear{display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:0;border-radius:8px;background:transparent;color:#00000080;cursor:pointer}.pc-scan-clear:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pc-tabs{flex:0 0 auto;display:flex;gap:8px;padding:10px 12px;overflow-x:auto;background:#fff;border-bottom:1px solid var(--pos-line);scrollbar-width:none}.pc-tabs::-webkit-scrollbar{display:none}.pc-tab{flex:0 0 auto;display:inline-flex;align-items:center;min-height:48px;padding:0 18px;border:1px solid var(--pos-line);border-radius:24px;background:#fff;font:inherit;font-size:14px;white-space:nowrap;color:#000000b3;cursor:pointer}.pc-tab:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pc-tab.active{border-color:var(--pos-accent);background:var(--pos-accent-soft);color:var(--pos-accent);font-weight:600}.pc-scroll{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch}.pc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(144px,1fr));gap:10px;padding:12px;align-items:stretch}.pc-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:48px 24px;color:var(--pos-quiet);text-align:center}.pc-empty mat-icon{font-size:36px;width:36px;height:36px;color:#00000026}.pc-empty p{margin:0;font-size:13.5px;max-width:28ch}.pc-lines{list-style:none;margin:0;padding:0}.pc-line{display:flex;flex-direction:column;gap:6px;padding:10px 12px;background:#fff;border-bottom:1px solid var(--pos-line)}.pc-line-text{display:flex;align-items:baseline;gap:12px;min-width:0}.pc-line-name{flex:1 1 auto;min-width:0;font-size:15px;font-weight:500;line-height:1.3;color:#000000de;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.pc-line-controls{display:flex;align-items:center;gap:10px}.pc-line-unit{flex:1 1 auto;min-width:0;font-size:12.5px;color:var(--pos-quiet);font-variant-numeric:tabular-nums}.pc-qty{display:inline-flex;align-items:center;flex:0 0 auto}.pc-step{display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:1px solid var(--pos-line);border-radius:8px;background:#fff;color:#000000b3;cursor:pointer}.pc-step:focus-visible{outline:2px solid var(--pos-accent);outline-offset:2px}.pc-step mat-icon{font-size:20px;width:20px;height:20px}.pc-qty-value{min-width:44px;text-align:center;font-size:16px;font-weight:600;font-variant-numeric:tabular-nums}.pc-line-total{flex:0 0 auto;text-align:right;font-size:16px;font-weight:600;font-variant-numeric:tabular-nums;color:#000000d9}.pc-remove{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;min-width:48px;min-height:48px;border:0;border-radius:8px;background:transparent;color:#00000061;cursor:pointer}.pc-remove:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pc-remove mat-icon{font-size:20px;width:20px;height:20px}.pc-sums{padding:12px 12px 20px;background:#fff}.pc-sum{display:flex;justify-content:space-between;align-items:baseline;padding:3px 0;font-size:13px;color:var(--pos-quiet)}.pc-num{font-variant-numeric:tabular-nums;color:#000000b3}.pc-plinth{flex:0 0 auto;background:#fff;box-shadow:0 -2px 10px #0000001a}.pc-review{display:flex;align-items:center;gap:8px;width:100%;min-height:52px;padding:0 8px 0 16px;border:0;border-bottom:1px solid var(--pos-line);background:transparent;font:inherit;text-align:left;cursor:pointer}.pc-review:focus-visible{outline:2px solid var(--pos-accent);outline-offset:-2px}.pc-review-count{font-size:13px;color:var(--pos-quiet);font-variant-numeric:tabular-nums}.pc-review-action{margin-left:auto;font-size:13px;font-weight:600;color:var(--pos-accent)}.pc-review-chevron{flex:0 0 auto;font-size:22px;width:22px;height:22px;color:var(--pos-accent)}.pc-total{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:10px 16px 12px}.pc-total-label{font-size:13px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#0000008c}.pc-total-value{font-size:34px;font-weight:700;line-height:1;letter-spacing:-1px;font-variant-numeric:tabular-nums;color:var(--pos-accent)}.pc-charge{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;min-height:88px;border:0;border-radius:0;background:var(--pos-accent);color:#fff;font:inherit;font-size:20px;font-weight:700;letter-spacing:.03em;cursor:pointer;transition:background .12s}.pc-charge mat-icon{font-size:25px;width:25px;height:25px}.pc-charge:focus-visible{outline:3px solid #0d47a1;outline-offset:-6px}.pc-charge:disabled{background:#0000001a;color:#00000059;cursor:default}@media (prefers-reduced-motion: reduce){.pc-scan,.pc-charge{transition:none}}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: PosTileComponent, selector: "spa-pos-tile", inputs: ["product", "inCart", "disabled"], outputs: ["picked"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }] }); }
|
|
1032
1055
|
}
|
|
1033
1056
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PosCompactComponent, decorators: [{
|
|
1034
1057
|
type: Component,
|
|
@@ -1113,7 +1136,7 @@ class PosComponent {
|
|
|
1113
1136
|
this.tenderOpen = false;
|
|
1114
1137
|
}
|
|
1115
1138
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PosComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1116
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PosComponent, isStandalone: false, selector: "spa-pos", inputs: { posConfig: "posConfig", products: "products" }, providers: [PosSessionService], ngImport: i0, template: "<!-- Added (POS Phase 4): the shell renders ONE of two presentations over the SAME provided PosSessionService.\n Nothing else is here. Anything that appeared outside this switch would have to be duplicated by whichever\n presentation is not showing, which is precisely the fork this component exists to prevent. -->\n\n<div class=\"pos-shell\" [class.pos-small]=\"smallScreen\">\n\n <spa-pos-wide *ngIf=\"!smallScreen\" [title]=\"title\" (charge)=\"openTender()\"></spa-pos-wide>\n\n <!-- Changed (POS Phase 6): the phone presentation. This replaced the Phase 4 HOLDING PANEL, and \u2014 exactly as\n that panel's own note predicted \u2014 nothing else in this file changed. Both presentations read the SAME\n provided PosSessionService and both reach the tender by emitting the SAME (charge) event, which is why\n a rotate mid-sale is a template swap over state that never moved. -->\n <spa-pos-compact *ngIf=\"smallScreen\" [title]=\"title\" (charge)=\"openTender()\"></spa-pos-compact>\n\n <!-- Added (POS Phase 5): the tender surface is hosted by the SHELL, above the presentation swap, for the same\n reason the cart is: an operator who rotates a tablet mid-tender must keep the tender. It is opened by the\n (charge) event, so the Phase 6 phone presentation reaches this identical component by emitting the same\n event \u2014 there is no second tender to write and nowhere for one to diverge. -->\n <spa-pos-tender *ngIf=\"tenderOpen\" (cancelled)=\"closeTender()\" (completed)=\"closeTender()\"></spa-pos-tender>\n</div>\n", styles: [":host{--pos-accent: #1565c0;--pos-accent-soft: #e3f2fd;--pos-money: #2e7d32;--pos-line: rgba(0, 0, 0, .12);--pos-quiet: rgba(0, 0, 0, .55);display:block;height:100%;min-height:0}.pos-shell{position:relative;height:100%;min-height:0;display:flex;flex-direction:column}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: PosWideComponent, selector: "spa-pos-wide", inputs: ["title"], outputs: ["charge"] }, { kind: "component", type: PosTenderComponent, selector: "spa-pos-tender", outputs: ["cancelled", "completed"] }, { kind: "component", type: PosCompactComponent, selector: "spa-pos-compact", inputs: ["title"], outputs: ["charge"] }] }); }
|
|
1139
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PosComponent, isStandalone: false, selector: "spa-pos", inputs: { posConfig: "posConfig", products: "products" }, providers: [PosSessionService], ngImport: i0, template: "<!-- Added (POS Phase 4): the shell renders ONE of two presentations over the SAME provided PosSessionService.\n Nothing else is here. Anything that appeared outside this switch would have to be duplicated by whichever\n presentation is not showing, which is precisely the fork this component exists to prevent. -->\n\n<div class=\"pos-shell\" [class.pos-small]=\"smallScreen\">\n\n <spa-pos-wide *ngIf=\"!smallScreen\" [title]=\"title\" (charge)=\"openTender()\"></spa-pos-wide>\n\n <!-- Changed (POS Phase 6): the phone presentation. This replaced the Phase 4 HOLDING PANEL, and \u2014 exactly as\n that panel's own note predicted \u2014 nothing else in this file changed. Both presentations read the SAME\n provided PosSessionService and both reach the tender by emitting the SAME (charge) event, which is why\n a rotate mid-sale is a template swap over state that never moved. -->\n <spa-pos-compact *ngIf=\"smallScreen\" [title]=\"title\" (charge)=\"openTender()\"></spa-pos-compact>\n\n <!-- Added (POS Phase 5): the tender surface is hosted by the SHELL, above the presentation swap, for the same\n reason the cart is: an operator who rotates a tablet mid-tender must keep the tender. It is opened by the\n (charge) event, so the Phase 6 phone presentation reaches this identical component by emitting the same\n event \u2014 there is no second tender to write and nowhere for one to diverge. -->\n <spa-pos-tender *ngIf=\"tenderOpen\" (cancelled)=\"closeTender()\" (completed)=\"closeTender()\"></spa-pos-tender>\n</div>\n", styles: [":host{--pos-accent: #1565c0;--pos-accent-soft: #e3f2fd;--pos-money: #2e7d32;--pos-line: rgba(0, 0, 0, .12);--pos-quiet: rgba(0, 0, 0, .55);display:block;height:100%;min-height:0}.pos-shell{position:relative;height:100%;min-height:0;display:flex;flex-direction:column}\n"], dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: PosWideComponent, selector: "spa-pos-wide", inputs: ["title"], outputs: ["charge"] }, { kind: "component", type: PosTenderComponent, selector: "spa-pos-tender", outputs: ["cancelled", "completed"] }, { kind: "component", type: PosCompactComponent, selector: "spa-pos-compact", inputs: ["title"], outputs: ["charge"] }] }); }
|
|
1117
1140
|
}
|
|
1118
1141
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PosComponent, decorators: [{
|
|
1119
1142
|
type: Component,
|
|
@@ -1238,7 +1261,7 @@ const SALES_ROUTES = [
|
|
|
1238
1261
|
];
|
|
1239
1262
|
class SalesRoutingModule {
|
|
1240
1263
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SalesRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
1241
|
-
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.14", ngImport: i0, type: SalesRoutingModule, imports: [i1$
|
|
1264
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.14", ngImport: i0, type: SalesRoutingModule, imports: [i1$2.RouterModule], exports: [RouterModule] }); }
|
|
1242
1265
|
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SalesRoutingModule, imports: [RouterModule.forChild(SALES_ROUTES), RouterModule] }); }
|
|
1243
1266
|
}
|
|
1244
1267
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SalesRoutingModule, decorators: [{
|
|
@@ -1343,4 +1366,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
1343
1366
|
}] });
|
|
1344
1367
|
|
|
1345
1368
|
export { SalesModule };
|
|
1346
|
-
//# sourceMappingURL=tin-spa-sales.module-
|
|
1369
|
+
//# sourceMappingURL=tin-spa-sales.module-nyrJulzD.mjs.map
|