@retalia/pos-components 0.0.4 → 0.0.5
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/fesm2022/retalia-pos-components.mjs +1089 -0
- package/fesm2022/retalia-pos-components.mjs.map +1 -0
- package/package.json +39 -17
- package/types/retalia-pos-components.d.ts +300 -0
- package/eslint.config.js +0 -32
- package/ng-package.json +0 -14
- package/src/lib/basket/pos-basket.html +0 -124
- package/src/lib/basket/pos-basket.scss +0 -285
- package/src/lib/basket/pos-basket.spec.ts +0 -198
- package/src/lib/basket/pos-basket.stories.ts +0 -114
- package/src/lib/basket/pos-basket.ts +0 -142
- package/src/lib/item-entry/pos-item-entry.html +0 -41
- package/src/lib/item-entry/pos-item-entry.scss +0 -145
- package/src/lib/item-entry/pos-item-entry.spec.ts +0 -123
- package/src/lib/item-entry/pos-item-entry.stories.ts +0 -31
- package/src/lib/item-entry/pos-item-entry.ts +0 -81
- package/src/lib/login/pos-login.html +0 -82
- package/src/lib/login/pos-login.scss +0 -238
- package/src/lib/login/pos-login.spec.ts +0 -89
- package/src/lib/login/pos-login.stories.ts +0 -36
- package/src/lib/login/pos-login.ts +0 -150
- package/src/lib/receipt/pos-receipt.html +0 -116
- package/src/lib/receipt/pos-receipt.scss +0 -367
- package/src/lib/receipt/pos-receipt.spec.ts +0 -188
- package/src/lib/receipt/pos-receipt.stories.ts +0 -163
- package/src/lib/receipt/pos-receipt.ts +0 -129
- package/src/lib/tender/pos-tender.html +0 -79
- package/src/lib/tender/pos-tender.scss +0 -318
- package/src/lib/tender/pos-tender.spec.ts +0 -150
- package/src/lib/tender/pos-tender.stories.ts +0 -47
- package/src/lib/tender/pos-tender.ts +0 -154
- package/src/lib/tokens/pos-theme-css.spec.ts +0 -75
- package/src/lib/tokens/pos-theme-css.ts +0 -123
- package/src/lib/tokens/pos-token-catalog.ts +0 -514
- package/src/lib/tokens/pos-tokens.html +0 -351
- package/src/lib/tokens/pos-tokens.scss +0 -437
- package/src/lib/tokens/pos-tokens.spec.ts +0 -164
- package/src/lib/tokens/pos-tokens.stories.ts +0 -22
- package/src/lib/tokens/pos-tokens.ts +0 -208
- package/src/public-api.ts +0 -37
- package/tsconfig.lib.json +0 -14
- package/tsconfig.lib.prod.json +0 -11
- package/tsconfig.spec.json +0 -11
- /package/{src/styles → styles}/styles.css +0 -0
- /package/{src/styles → styles}/tokens.css +0 -0
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeDetectionStrategy,
|
|
3
|
-
Component,
|
|
4
|
-
computed,
|
|
5
|
-
input,
|
|
6
|
-
output,
|
|
7
|
-
} from '@angular/core';
|
|
8
|
-
|
|
9
|
-
export type PosBasketIntent =
|
|
10
|
-
| {
|
|
11
|
-
readonly type: 'basket.changeQuantity';
|
|
12
|
-
readonly lineId: number;
|
|
13
|
-
readonly qty: number;
|
|
14
|
-
}
|
|
15
|
-
| { readonly type: 'basket.voidLine'; readonly lineId: number }
|
|
16
|
-
| { readonly type: 'basket.clear' };
|
|
17
|
-
|
|
18
|
-
/** Display-only VAT from the host envelope — not calculated here. */
|
|
19
|
-
export interface PosBasketVat {
|
|
20
|
-
readonly rate: number;
|
|
21
|
-
readonly amount: number;
|
|
22
|
-
readonly included: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Display-only basket line from the host envelope — not an API DTO. */
|
|
26
|
-
export interface PosBasketLine {
|
|
27
|
-
readonly lineId: number;
|
|
28
|
-
readonly sku: string;
|
|
29
|
-
readonly name: string;
|
|
30
|
-
readonly qty: number;
|
|
31
|
-
readonly unitPrice: number;
|
|
32
|
-
readonly lineTotal: number;
|
|
33
|
-
readonly vat: PosBasketVat;
|
|
34
|
-
readonly struckThrough: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Display-only totals from the host envelope — not calculated here. */
|
|
38
|
-
export interface PosBasketTotals {
|
|
39
|
-
readonly gross: number;
|
|
40
|
-
readonly net: number;
|
|
41
|
-
readonly vat: number;
|
|
42
|
-
readonly discount: number;
|
|
43
|
-
readonly due: number;
|
|
44
|
-
readonly currency: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const EMPTY_TOTALS: PosBasketTotals = {
|
|
48
|
-
gross: 0,
|
|
49
|
-
net: 0,
|
|
50
|
-
vat: 0,
|
|
51
|
-
discount: 0,
|
|
52
|
-
due: 0,
|
|
53
|
-
currency: 'EUR',
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
@Component({
|
|
57
|
-
selector: 'pos-basket',
|
|
58
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
59
|
-
templateUrl: './pos-basket.html',
|
|
60
|
-
styleUrl: './pos-basket.scss',
|
|
61
|
-
host: {
|
|
62
|
-
class: 'pos-basket',
|
|
63
|
-
},
|
|
64
|
-
})
|
|
65
|
-
export class PosBasket {
|
|
66
|
-
readonly lines = input<readonly PosBasketLine[]>([]);
|
|
67
|
-
readonly totals = input<PosBasketTotals>(EMPTY_TOTALS);
|
|
68
|
-
readonly disabled = input(false);
|
|
69
|
-
|
|
70
|
-
readonly title = input('Basket');
|
|
71
|
-
readonly emptyMessage = input('Basket is empty');
|
|
72
|
-
readonly clearLabel = input('Clear basket');
|
|
73
|
-
|
|
74
|
-
readonly action = output<PosBasketIntent>();
|
|
75
|
-
|
|
76
|
-
protected readonly hasLines = computed(() => this.lines().length > 0);
|
|
77
|
-
|
|
78
|
-
protected readonly canClear = computed(
|
|
79
|
-
() => this.hasLines() && !this.disabled(),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
protected formatMoney(amount: number): string {
|
|
83
|
-
return new Intl.NumberFormat(undefined, {
|
|
84
|
-
style: 'currency',
|
|
85
|
-
currency: this.totals().currency || 'EUR',
|
|
86
|
-
minimumFractionDigits: 2,
|
|
87
|
-
maximumFractionDigits: 2,
|
|
88
|
-
}).format(amount);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
protected canChangeQuantity(line: PosBasketLine): boolean {
|
|
92
|
-
return !this.disabled() && !line.struckThrough;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
protected canDecrement(line: PosBasketLine): boolean {
|
|
96
|
-
return this.canChangeQuantity(line) && line.qty > 1;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
protected canVoid(line: PosBasketLine): boolean {
|
|
100
|
-
return this.canChangeQuantity(line);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
protected onIncrement(line: PosBasketLine): void {
|
|
104
|
-
if (!this.canChangeQuantity(line)) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
this.action.emit({
|
|
109
|
-
type: 'basket.changeQuantity',
|
|
110
|
-
lineId: line.lineId,
|
|
111
|
-
qty: line.qty + 1,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
protected onDecrement(line: PosBasketLine): void {
|
|
116
|
-
if (!this.canDecrement(line)) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
this.action.emit({
|
|
121
|
-
type: 'basket.changeQuantity',
|
|
122
|
-
lineId: line.lineId,
|
|
123
|
-
qty: line.qty - 1,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
protected onVoid(line: PosBasketLine): void {
|
|
128
|
-
if (!this.canVoid(line)) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
this.action.emit({ type: 'basket.voidLine', lineId: line.lineId });
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
protected onClear(): void {
|
|
136
|
-
if (!this.canClear()) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
this.action.emit({ type: 'basket.clear' });
|
|
141
|
-
}
|
|
142
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
<section
|
|
2
|
-
class="pos-item-entry__panel"
|
|
3
|
-
[attr.aria-busy]="disabled() ? true : null"
|
|
4
|
-
>
|
|
5
|
-
<form class="pos-item-entry__form" (submit)="onSubmit($event)">
|
|
6
|
-
<label class="pos-item-entry__field">
|
|
7
|
-
<span class="pos-item-entry__label">{{ label() }}</span>
|
|
8
|
-
<input
|
|
9
|
-
#itemInput
|
|
10
|
-
class="pos-item-entry__input"
|
|
11
|
-
type="text"
|
|
12
|
-
name="itemReference"
|
|
13
|
-
autocomplete="off"
|
|
14
|
-
spellcheck="false"
|
|
15
|
-
[attr.autocorrect]="'off'"
|
|
16
|
-
[attr.autocapitalize]="'none'"
|
|
17
|
-
enterkeyhint="done"
|
|
18
|
-
[placeholder]="placeholder()"
|
|
19
|
-
[value]="reference()"
|
|
20
|
-
[disabled]="disabled()"
|
|
21
|
-
[attr.aria-invalid]="errorMessage() ? true : null"
|
|
22
|
-
(input)="onInput($event)"
|
|
23
|
-
/>
|
|
24
|
-
</label>
|
|
25
|
-
|
|
26
|
-
<button
|
|
27
|
-
type="submit"
|
|
28
|
-
class="pos-item-entry__add"
|
|
29
|
-
[class.pos-item-entry__add--disabled]="!canSubmit()"
|
|
30
|
-
[disabled]="!canSubmit()"
|
|
31
|
-
>
|
|
32
|
-
{{ addLabel() }}
|
|
33
|
-
</button>
|
|
34
|
-
</form>
|
|
35
|
-
|
|
36
|
-
<div class="pos-item-entry__status" aria-live="polite">
|
|
37
|
-
@if (errorMessage()) {
|
|
38
|
-
<p class="pos-item-entry__error">{{ errorMessage() }}</p>
|
|
39
|
-
}
|
|
40
|
-
</div>
|
|
41
|
-
</section>
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
:host {
|
|
2
|
-
display: block;
|
|
3
|
-
width: 100%;
|
|
4
|
-
color: var(--pos-color-text);
|
|
5
|
-
font-family: var(--pos-font-family);
|
|
6
|
-
font-size: var(--pos-font-size-md);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
.pos-item-entry__panel {
|
|
10
|
-
display: flex;
|
|
11
|
-
flex-direction: column;
|
|
12
|
-
gap: var(--pos-space-sm);
|
|
13
|
-
padding: var(--pos-space-lg);
|
|
14
|
-
border: 1px solid var(--pos-color-border);
|
|
15
|
-
border-radius: var(--pos-radius-lg);
|
|
16
|
-
background: var(--pos-color-panel);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
.pos-item-entry__form {
|
|
20
|
-
display: flex;
|
|
21
|
-
flex-direction: column;
|
|
22
|
-
gap: var(--pos-space-sm);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.pos-item-entry__field {
|
|
26
|
-
display: flex;
|
|
27
|
-
flex: 1 1 auto;
|
|
28
|
-
flex-direction: column;
|
|
29
|
-
gap: var(--pos-space-xs);
|
|
30
|
-
min-width: 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.pos-item-entry__label {
|
|
34
|
-
color: var(--pos-color-text-muted);
|
|
35
|
-
font-size: var(--pos-font-size-sm);
|
|
36
|
-
font-weight: var(--pos-font-weight-semibold);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.pos-item-entry__input {
|
|
40
|
-
box-sizing: border-box;
|
|
41
|
-
width: 100%;
|
|
42
|
-
min-height: var(--pos-button-min-height);
|
|
43
|
-
margin: 0;
|
|
44
|
-
padding: 0 var(--pos-space-md);
|
|
45
|
-
border: 1px solid var(--pos-color-border);
|
|
46
|
-
border-radius: var(--pos-radius-md);
|
|
47
|
-
background: var(--pos-color-panel-elevated);
|
|
48
|
-
color: var(--pos-color-text);
|
|
49
|
-
font: inherit;
|
|
50
|
-
font-family: var(--pos-font-family-display);
|
|
51
|
-
font-size: var(--pos-font-size-lg);
|
|
52
|
-
font-weight: var(--pos-font-weight-semibold);
|
|
53
|
-
outline: none;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.pos-item-entry__input::placeholder {
|
|
57
|
-
color: var(--pos-color-text-muted);
|
|
58
|
-
font-weight: var(--pos-font-weight-semibold);
|
|
59
|
-
opacity: 0.8;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.pos-item-entry__input:disabled {
|
|
63
|
-
cursor: not-allowed;
|
|
64
|
-
background: var(--pos-color-disabled);
|
|
65
|
-
color: var(--pos-color-on-disabled);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.pos-item-entry__input[aria-invalid='true'] {
|
|
69
|
-
border-color: var(--pos-color-danger);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.pos-item-entry__add {
|
|
73
|
-
flex: 0 0 auto;
|
|
74
|
-
min-height: var(--pos-button-min-height);
|
|
75
|
-
padding: 0 var(--pos-space-xl);
|
|
76
|
-
border: 0;
|
|
77
|
-
border-radius: var(--pos-radius-md);
|
|
78
|
-
background: var(--pos-color-primary);
|
|
79
|
-
color: var(--pos-color-on-primary);
|
|
80
|
-
font: inherit;
|
|
81
|
-
font-weight: var(--pos-font-weight-bold);
|
|
82
|
-
letter-spacing: 0.04em;
|
|
83
|
-
text-transform: uppercase;
|
|
84
|
-
cursor: pointer;
|
|
85
|
-
-webkit-tap-highlight-color: transparent;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.pos-item-entry__add:not(:disabled):active {
|
|
89
|
-
transform: translateY(1px);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.pos-item-entry__add--disabled,
|
|
93
|
-
.pos-item-entry__add:disabled {
|
|
94
|
-
background: var(--pos-color-disabled);
|
|
95
|
-
color: var(--pos-color-on-disabled);
|
|
96
|
-
cursor: not-allowed;
|
|
97
|
-
transform: none;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.pos-item-entry__input:focus-visible {
|
|
101
|
-
border-color: var(--pos-color-primary);
|
|
102
|
-
outline: 2px solid var(--pos-color-primary);
|
|
103
|
-
outline-offset: 2px;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.pos-item-entry__add:focus-visible {
|
|
107
|
-
outline: 2px solid var(--pos-color-primary);
|
|
108
|
-
outline-offset: 2px;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.pos-item-entry__status {
|
|
112
|
-
min-height: 0;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.pos-item-entry__error {
|
|
116
|
-
margin: 0;
|
|
117
|
-
padding: var(--pos-space-sm) var(--pos-space-md);
|
|
118
|
-
border-left: 3px solid var(--pos-color-danger);
|
|
119
|
-
background: color-mix(in srgb, var(--pos-color-danger) 12%, transparent);
|
|
120
|
-
color: var(--pos-color-danger);
|
|
121
|
-
font-size: var(--pos-font-size-sm);
|
|
122
|
-
font-weight: var(--pos-font-weight-semibold);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
@media (min-width: 768px) {
|
|
126
|
-
.pos-item-entry__panel {
|
|
127
|
-
padding: var(--pos-space-xl);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.pos-item-entry__form {
|
|
131
|
-
flex-direction: row;
|
|
132
|
-
align-items: flex-end;
|
|
133
|
-
gap: var(--pos-space-md);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.pos-item-entry__add {
|
|
137
|
-
min-width: 7.5rem;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
@media (prefers-reduced-motion: reduce) {
|
|
142
|
-
.pos-item-entry__add {
|
|
143
|
-
transform: none;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
-
|
|
3
|
-
import { PosItemEntry, PosItemEntryIntent } from './pos-item-entry';
|
|
4
|
-
|
|
5
|
-
describe('PosItemEntry', () => {
|
|
6
|
-
let fixture: ComponentFixture<PosItemEntry>;
|
|
7
|
-
let component: PosItemEntry;
|
|
8
|
-
|
|
9
|
-
beforeEach(async () => {
|
|
10
|
-
await TestBed.configureTestingModule({
|
|
11
|
-
imports: [PosItemEntry],
|
|
12
|
-
}).compileComponents();
|
|
13
|
-
|
|
14
|
-
fixture = TestBed.createComponent(PosItemEntry);
|
|
15
|
-
component = fixture.componentInstance;
|
|
16
|
-
await fixture.whenStable();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
function typeReference(value: string): void {
|
|
20
|
-
const input = (fixture.nativeElement as HTMLElement).querySelector(
|
|
21
|
-
'.pos-item-entry__input',
|
|
22
|
-
) as HTMLInputElement;
|
|
23
|
-
input.value = value;
|
|
24
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
it('should render a scan-or-type field by default', () => {
|
|
28
|
-
const compiled = fixture.nativeElement as HTMLElement;
|
|
29
|
-
expect(compiled.querySelector('.pos-item-entry__label')?.textContent).toContain('Item');
|
|
30
|
-
expect(
|
|
31
|
-
(compiled.querySelector('.pos-item-entry__input') as HTMLInputElement).placeholder,
|
|
32
|
-
).toBe('Scan or enter item');
|
|
33
|
-
expect(
|
|
34
|
-
(compiled.querySelector('.pos-item-entry__add') as HTMLButtonElement).disabled,
|
|
35
|
-
).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should emit item-entry with the typed reference and then clear the field', async () => {
|
|
39
|
-
const intents: PosItemEntryIntent[] = [];
|
|
40
|
-
component.action.subscribe((intent) => intents.push(intent));
|
|
41
|
-
|
|
42
|
-
typeReference('SKU-001');
|
|
43
|
-
await fixture.whenStable();
|
|
44
|
-
|
|
45
|
-
const add = (fixture.nativeElement as HTMLElement).querySelector(
|
|
46
|
-
'.pos-item-entry__add',
|
|
47
|
-
) as HTMLButtonElement;
|
|
48
|
-
add.click();
|
|
49
|
-
await fixture.whenStable();
|
|
50
|
-
|
|
51
|
-
expect(intents).toEqual([{ type: 'itemEntry.submit', reference: 'SKU-001' }]);
|
|
52
|
-
expect(
|
|
53
|
-
(fixture.nativeElement as HTMLElement).querySelector(
|
|
54
|
-
'.pos-item-entry__input',
|
|
55
|
-
) as HTMLInputElement,
|
|
56
|
-
).toHaveProperty('value', '');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should emit item-entry when a scanner (or cashier) submits the form with Enter', async () => {
|
|
60
|
-
const intents: PosItemEntryIntent[] = [];
|
|
61
|
-
component.action.subscribe((intent) => intents.push(intent));
|
|
62
|
-
|
|
63
|
-
typeReference('5012345678900');
|
|
64
|
-
await fixture.whenStable();
|
|
65
|
-
|
|
66
|
-
const form = (fixture.nativeElement as HTMLElement).querySelector(
|
|
67
|
-
'.pos-item-entry__form',
|
|
68
|
-
) as HTMLFormElement;
|
|
69
|
-
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
70
|
-
await fixture.whenStable();
|
|
71
|
-
|
|
72
|
-
expect(intents).toEqual([{ type: 'itemEntry.submit', reference: '5012345678900' }]);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should trim the reference and ignore empty or whitespace-only input', async () => {
|
|
76
|
-
const intents: PosItemEntryIntent[] = [];
|
|
77
|
-
component.action.subscribe((intent) => intents.push(intent));
|
|
78
|
-
|
|
79
|
-
typeReference(' ');
|
|
80
|
-
await fixture.whenStable();
|
|
81
|
-
|
|
82
|
-
const form = (fixture.nativeElement as HTMLElement).querySelector(
|
|
83
|
-
'.pos-item-entry__form',
|
|
84
|
-
) as HTMLFormElement;
|
|
85
|
-
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
86
|
-
await fixture.whenStable();
|
|
87
|
-
|
|
88
|
-
expect(intents).toEqual([]);
|
|
89
|
-
|
|
90
|
-
typeReference(' SKU-014 ');
|
|
91
|
-
await fixture.whenStable();
|
|
92
|
-
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
93
|
-
await fixture.whenStable();
|
|
94
|
-
|
|
95
|
-
expect(intents).toEqual([{ type: 'itemEntry.submit', reference: 'SKU-014' }]);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should not emit while disabled, and should show host error text as given', async () => {
|
|
99
|
-
const intents: PosItemEntryIntent[] = [];
|
|
100
|
-
component.action.subscribe((intent) => intents.push(intent));
|
|
101
|
-
|
|
102
|
-
fixture.componentRef.setInput('disabled', true);
|
|
103
|
-
fixture.componentRef.setInput('errorMessage', 'Item not found');
|
|
104
|
-
await fixture.whenStable();
|
|
105
|
-
|
|
106
|
-
typeReference('SKU-001');
|
|
107
|
-
const add = (fixture.nativeElement as HTMLElement).querySelector(
|
|
108
|
-
'.pos-item-entry__add',
|
|
109
|
-
) as HTMLButtonElement;
|
|
110
|
-
add.click();
|
|
111
|
-
await fixture.whenStable();
|
|
112
|
-
|
|
113
|
-
const compiled = fixture.nativeElement as HTMLElement;
|
|
114
|
-
expect(add.disabled).toBe(true);
|
|
115
|
-
expect(
|
|
116
|
-
(compiled.querySelector('.pos-item-entry__input') as HTMLInputElement).disabled,
|
|
117
|
-
).toBe(true);
|
|
118
|
-
expect(compiled.querySelector('.pos-item-entry__error')?.textContent).toContain(
|
|
119
|
-
'Item not found',
|
|
120
|
-
);
|
|
121
|
-
expect(intents).toEqual([]);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/angular-vite';
|
|
2
|
-
|
|
3
|
-
import { PosItemEntry } from './pos-item-entry';
|
|
4
|
-
|
|
5
|
-
const meta: Meta<PosItemEntry> = {
|
|
6
|
-
title: 'Item entry',
|
|
7
|
-
component: PosItemEntry,
|
|
8
|
-
args: {
|
|
9
|
-
disabled: false,
|
|
10
|
-
errorMessage: '',
|
|
11
|
-
autofocus: false,
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export default meta;
|
|
16
|
-
|
|
17
|
-
type Story = StoryObj<PosItemEntry>;
|
|
18
|
-
|
|
19
|
-
export const Default: Story = {};
|
|
20
|
-
|
|
21
|
-
export const Disabled: Story = {
|
|
22
|
-
args: {
|
|
23
|
-
disabled: true,
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export const Error: Story = {
|
|
28
|
-
args: {
|
|
29
|
-
errorMessage: 'Item not found',
|
|
30
|
-
},
|
|
31
|
-
};
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
afterNextRender,
|
|
3
|
-
ChangeDetectionStrategy,
|
|
4
|
-
Component,
|
|
5
|
-
computed,
|
|
6
|
-
ElementRef,
|
|
7
|
-
input,
|
|
8
|
-
output,
|
|
9
|
-
signal,
|
|
10
|
-
viewChild,
|
|
11
|
-
} from '@angular/core';
|
|
12
|
-
|
|
13
|
-
export type PosItemEntryIntent =
|
|
14
|
-
| { readonly type: 'itemEntry.submit'; readonly reference: string };
|
|
15
|
-
|
|
16
|
-
@Component({
|
|
17
|
-
selector: 'pos-item-entry',
|
|
18
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19
|
-
templateUrl: './pos-item-entry.html',
|
|
20
|
-
styleUrl: './pos-item-entry.scss',
|
|
21
|
-
host: {
|
|
22
|
-
class: 'pos-item-entry',
|
|
23
|
-
},
|
|
24
|
-
})
|
|
25
|
-
export class PosItemEntry {
|
|
26
|
-
readonly disabled = input(false);
|
|
27
|
-
readonly errorMessage = input('');
|
|
28
|
-
readonly autofocus = input(true);
|
|
29
|
-
|
|
30
|
-
readonly label = input('Item');
|
|
31
|
-
readonly placeholder = input('Scan or enter item');
|
|
32
|
-
readonly addLabel = input('Add');
|
|
33
|
-
|
|
34
|
-
readonly action = output<PosItemEntryIntent>();
|
|
35
|
-
|
|
36
|
-
private readonly itemInput = viewChild<ElementRef<HTMLInputElement>>('itemInput');
|
|
37
|
-
|
|
38
|
-
protected readonly reference = signal('');
|
|
39
|
-
|
|
40
|
-
protected readonly canSubmit = computed(
|
|
41
|
-
() => this.reference().trim().length > 0 && !this.disabled(),
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
constructor() {
|
|
45
|
-
afterNextRender(() => {
|
|
46
|
-
if (this.autofocus() && !this.disabled()) {
|
|
47
|
-
this.focusInput();
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
protected onInput(event: Event): void {
|
|
53
|
-
const target = event.target;
|
|
54
|
-
if (!(target instanceof HTMLInputElement)) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
this.reference.set(target.value);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
protected onSubmit(event: Event): void {
|
|
62
|
-
event.preventDefault();
|
|
63
|
-
|
|
64
|
-
if (this.disabled()) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const reference = this.reference().trim();
|
|
69
|
-
if (!reference) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
this.action.emit({ type: 'itemEntry.submit', reference });
|
|
74
|
-
this.reference.set('');
|
|
75
|
-
this.focusInput();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private focusInput(): void {
|
|
79
|
-
this.itemInput()?.nativeElement.focus();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
<section class="pos-login__panel" [attr.aria-busy]="session().status === 'busy' ? true : null">
|
|
2
|
-
<header class="pos-login__header">
|
|
3
|
-
<p class="pos-login__kicker">Adaptive POS</p>
|
|
4
|
-
<h1 class="pos-login__title">{{ title() }}</h1>
|
|
5
|
-
</header>
|
|
6
|
-
|
|
7
|
-
<div class="pos-login__status" aria-live="polite">
|
|
8
|
-
@if (sessionStatusText()) {
|
|
9
|
-
<p>{{ sessionStatusText() }}</p>
|
|
10
|
-
}
|
|
11
|
-
@if (errorMessage()) {
|
|
12
|
-
<p class="pos-login__error">{{ errorMessage() }}</p>
|
|
13
|
-
}
|
|
14
|
-
</div>
|
|
15
|
-
|
|
16
|
-
<div class="pos-login__body">
|
|
17
|
-
<div class="pos-login__display">
|
|
18
|
-
<input
|
|
19
|
-
class="pos-login__input"
|
|
20
|
-
[attr.type]="maskInput() ? 'password' : 'text'"
|
|
21
|
-
[value]="inputValue()"
|
|
22
|
-
maxlength="12"
|
|
23
|
-
readonly
|
|
24
|
-
tabindex="-1"
|
|
25
|
-
placeholder="· · · ·"
|
|
26
|
-
(keydown)="$event.preventDefault()"
|
|
27
|
-
aria-label="Login code"
|
|
28
|
-
/>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<div class="pos-login__keypad" role="group" aria-label="Keypad">
|
|
32
|
-
@for (row of keypadRows; track $index) {
|
|
33
|
-
<div class="pos-login__keypad-row">
|
|
34
|
-
@for (key of row; track key) {
|
|
35
|
-
<button
|
|
36
|
-
type="button"
|
|
37
|
-
class="pos-login__key"
|
|
38
|
-
[class.pos-login__key--action]="key === '←' || key === '.'"
|
|
39
|
-
[disabled]="interactionLocked()"
|
|
40
|
-
(click)="onKeypad(key)"
|
|
41
|
-
>
|
|
42
|
-
{{ key }}
|
|
43
|
-
</button>
|
|
44
|
-
}
|
|
45
|
-
</div>
|
|
46
|
-
}
|
|
47
|
-
</div>
|
|
48
|
-
|
|
49
|
-
<div class="pos-login__actions">
|
|
50
|
-
@if (showBack()) {
|
|
51
|
-
<button
|
|
52
|
-
type="button"
|
|
53
|
-
class="pos-login__back"
|
|
54
|
-
aria-label="Back"
|
|
55
|
-
[disabled]="interactionLocked()"
|
|
56
|
-
(click)="onBack()"
|
|
57
|
-
>
|
|
58
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
59
|
-
<path
|
|
60
|
-
d="M15 6 9 12l6 6"
|
|
61
|
-
stroke="currentColor"
|
|
62
|
-
stroke-width="2"
|
|
63
|
-
stroke-linecap="round"
|
|
64
|
-
stroke-linejoin="round"
|
|
65
|
-
/>
|
|
66
|
-
</svg>
|
|
67
|
-
<span>Back</span>
|
|
68
|
-
</button>
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
<button
|
|
72
|
-
type="button"
|
|
73
|
-
class="pos-login__primary"
|
|
74
|
-
[class.pos-login__primary--disabled]="!primaryEnabled()"
|
|
75
|
-
[disabled]="!primaryEnabled()"
|
|
76
|
-
(click)="onPrimary()"
|
|
77
|
-
>
|
|
78
|
-
{{ primaryLabel() }}
|
|
79
|
-
</button>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
</section>
|