Skip to content

Commit 7b7c5fa

Browse files
committed
cahsier
1 parent b3e654c commit 7b7c5fa

17 files changed

Lines changed: 869 additions & 152 deletions

.github/copilot-instructions.md

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ export class ExampleComponent implements OnInit, OnDestroy {
7676
- **Cart State**: Managed by `CartStateService` with signals (e.g., `cartCount()`, `cartTotal()`)
7777
- **Search State**: `SearchStateService` shares search query across components
7878
- **User State**: `AuthService.getCurrentUser()` returns current user or null
79-
- **Register State**: `RegisterService` manages active register (cashiers must open register)
79+
- **Register State**: `RegisterService` manages active register with device binding (cashiers must open register)
80+
- **Device Identification**: `DeviceService` generates unique device fingerprints for register binding
8081

8182
## Common Workflows
8283

@@ -89,6 +90,49 @@ export class ExampleComponent implements OnInit, OnDestroy {
8990
5. **Add route** in `src/app/app.routes.ts` if needed
9091
6. **Apply theme styles** using mixins from `_theme.scss`
9192

93+
### Creating Reusable Modal Components
94+
95+
**Pattern Example: OpenRegisterComponent**
96+
97+
1. **Create component** with `@Output()` events for parent communication:
98+
99+
```typescript
100+
@Output() registerOpened = new EventEmitter<void>();
101+
@Output() cancelled = new EventEmitter<void>();
102+
```
103+
104+
2. **Add to parent component imports**:
105+
106+
```typescript
107+
imports: [OpenRegisterComponent, ...]
108+
```
109+
110+
3. **Use signals for modal state**:
111+
112+
```typescript
113+
showOpenRegisterModal = signal<boolean>(false);
114+
```
115+
116+
4. **Template usage**:
117+
118+
```html
119+
<app-open-register
120+
*ngIf="showOpenRegisterModal()"
121+
(registerOpened)="onRegisterOpened()"
122+
(cancelled)="closeRegisterModal()"
123+
>
124+
</app-open-register>
125+
```
126+
127+
5. **Handle events in parent**:
128+
129+
```typescript
130+
onRegisterOpened(): void {
131+
this.showOpenRegisterModal.set(false);
132+
// Additional logic after register opens
133+
}
134+
```
135+
92136
### Adding Backend API Endpoint
93137

94138
1. **Create/update Mongoose model** in `server/models/{Model}.js`
@@ -111,13 +155,56 @@ export class ExampleComponent implements OnInit, OnDestroy {
111155
- Dashboard shows incomplete products card for follow-up editing
112156
- Always set `active: true` and `available: true` for newly created products
113157

158+
### QR Code Generation & Thermal Printing
159+
160+
- **QR Badge Generation**: `settings.component.ts` handles employee QR badge creation
161+
- **Display Size**: 150x150 pixels for on-screen preview (`generateQrCode()`)
162+
- **Print Size**: 120x120 pixels optimized for 58mm/80mm thermal printers (`printQrBadge()`)
163+
- **Badge Layout**: Simplified HTML/CSS with monospace fonts, minimal padding, no shadows for thermal printing
164+
- **QZ Tray Integration**: Uses QZ Tray service for direct thermal printer communication
165+
114166
### Scale Integration
115167

116168
- `ScaleService.connectScale()` uses Web Serial API (Chrome/Edge only)
117169
- Products with `requiresScale: true` prompt for weight entry
118170
- Weight modal displays current scale reading in real-time
119171
- Manual weight entry fallback if scale not connected
120172

173+
### Register Management & Device Binding
174+
175+
**OpenRegisterComponent** (`open-register/`) - Dedicated component for opening registers with smart device binding:
176+
177+
- **Device Auto-Selection**: Automatically selects register bound to current device using `DeviceService`
178+
- **Role-Based Access**:
179+
- Admins/Managers: Can select any register, create new registers, manage all devices
180+
- Cashiers/Employees: Can only open registers bound to their device or unbound registers
181+
- **Device Binding**: Registers are automatically bound to devices when opened (stored in `deviceId` and `deviceName`)
182+
- **Visual Indicators**: Shows which registers are linked to current device (⭐) or other devices (🔒)
183+
- **Used in**: POS component and Cashier component
184+
- **Backend API**: `/api/registers/available`, `/api/registers/device/:deviceId`
185+
186+
**Implementation Pattern:**
187+
188+
```typescript
189+
// In parent component
190+
showOpenRegisterModal = signal<boolean>(false);
191+
192+
openRegisterModal(): void {
193+
this.showOpenRegisterModal.set(true);
194+
}
195+
196+
onRegisterOpened(): void {
197+
this.showOpenRegisterModal.set(false);
198+
// Register opened successfully, component auto-focuses input
199+
}
200+
201+
// In template
202+
<app-open-register
203+
(registerOpened)="onRegisterOpened()"
204+
(cancelled)="closeRegisterModal()"
205+
></app-open-register>
206+
```
207+
121208
## Important Patterns
122209

123210
### ViewChild References
@@ -146,7 +233,7 @@ confirmAdd(): void {
146233

147234
### Calculator Component (Keyboard Support)
148235

149-
The calculator component supports full keyboard input for faster data entry:
236+
The calculator component (`calculator/`) supports full keyboard input for faster data entry with signals:
150237

151238
**Keyboard Shortcuts:**
152239

@@ -165,28 +252,13 @@ The calculator component supports full keyboard input for faster data entry:
165252
- Multiply button spans 2 columns, styled with `$warning` color
166253
- Add button spans 2 columns, styled with `$success` color
167254

168-
**Implementation Pattern:**
255+
**Signals Usage:**
169256

170257
```typescript
171-
// Add keyboard handler to container
172-
<div #calculatorContainer class="calculator" tabindex="-1" (keydown)="onKeydown($event)">
173-
174-
// Handle keyboard events
175-
onKeydown(event: KeyboardEvent): void {
176-
if (event.key >= '0' && event.key <= '9') {
177-
event.preventDefault();
178-
this.appendNumber(event.key);
179-
}
180-
else if (event.key === 'Enter' || event.key === '+') {
181-
event.preventDefault();
182-
this.handleEnter(); // Adds item or confirms multiply
183-
}
184-
else if (event.key === '*') {
185-
event.preventDefault();
186-
this.multiplyItem();
187-
}
188-
// ... other key handlers
189-
}
258+
display = signal<string>("0");
259+
isMultiplying = signal<boolean>(false);
260+
multiplyMode = signal<"add" | "update" | null>(null);
261+
pendingMultiplyValue = signal<number | null>(null);
190262
```
191263

192264
### RxJS Cleanup
@@ -220,7 +292,7 @@ this.toastService.show("Warning", "warning");
220292
- **Sale**: `saleNumber`, `items[]`, `subtotal`, `discount`, `tax`, `total`, `paymentMethod`, `cashier`, `register`, `status`, `isInternal`
221293
- **Cart**: `user`, `items[]`, `register` (carts are session-specific)
222294
- **User**: `username`, `fullName`, `role`, `permissions[]`, `active`
223-
- **Register**: `name`, `location`, `status` ('open', 'closed'), `openedBy`, `openingBalance`, `currentBalance`
295+
- **Register**: `name`, `location`, `status` ('open', 'closed'), `openedBy`, `openingBalance`, `currentBalance`, `deviceId`, `deviceName` (device binding)
224296

225297
## Security & Permissions
226298

src/app/components/calculator/calculator.component.ts

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ export class CalculatorComponent implements AfterViewInit {
9797
onAddItem(): void {
9898
const value = parseFloat(this.display());
9999
if (!isNaN(value) && value > 0) {
100+
if (this.isMultiplying()) {
101+
this.confirmMultiply();
102+
return;
103+
}
100104
this.addItem.emit({ value });
101105
this.display.set("0");
102106
}
@@ -176,38 +180,4 @@ export class CalculatorComponent implements AfterViewInit {
176180
handleMultiply(): void {
177181
this.multiplyItem();
178182
}
179-
180-
// Keyboard event handler
181-
onKeydown(event: KeyboardEvent): void {
182-
// Numbers 0-9
183-
if (event.key >= "0" && event.key <= "9") {
184-
event.preventDefault();
185-
this.appendNumber(event.key);
186-
}
187-
// Decimal point
188-
else if (event.key === "." || event.key === ",") {
189-
event.preventDefault();
190-
this.appendDecimal();
191-
}
192-
// Backspace
193-
else if (event.key === "Backspace") {
194-
event.preventDefault();
195-
this.backspace();
196-
}
197-
// Clear
198-
else if (event.key === "Escape" || event.key === "c" || event.key === "C") {
199-
event.preventDefault();
200-
this.clear();
201-
}
202-
// Enter or + (add item or confirm multiply)
203-
else if (event.key === "Enter" || event.key === "+") {
204-
event.preventDefault();
205-
this.handleEnter();
206-
}
207-
// * (multiply)
208-
else if (event.key === "*") {
209-
event.preventDefault();
210-
this.confirmMultiply();
211-
}
212-
}
213183
}

src/app/components/cashier/cashier.component.html

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
<div class="register-warning" *ngIf="!currentRegister()">
44
<i class="fas fa-exclamation-triangle"></i>
55
<span>{{ "CASHIER.REGISTER_WARNING" | translate }}</span>
6-
<a routerLink="/registers" class="open-register-link">
6+
<button class="open-register-btn" (click)="openRegisterModal()">
7+
<i class="fas fa-door-open"></i>
78
{{ "CASHIER.OPEN_REGISTER" | translate }}
8-
</a>
9+
</button>
910
</div>
1011

1112
<div class="cashier-content">
@@ -471,3 +472,17 @@ <h2>
471472
</div>
472473
</div>
473474
</div>
475+
476+
<!-- Open Register Modal -->
477+
<div
478+
class="modal-overlay"
479+
*ngIf="showOpenRegisterModal()"
480+
(click)="closeRegisterModal()"
481+
>
482+
<div (click)="$event.stopPropagation()">
483+
<app-open-register
484+
(registerOpened)="onRegisterOpened()"
485+
(cancelled)="closeRegisterModal()"
486+
></app-open-register>
487+
</div>
488+
</div>

src/app/components/cashier/cashier.component.scss

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
.cashier-container {
77
padding: 0.75rem;
88
background-color: var(--bg-secondary);
9-
height: 100%;
9+
height: calc(100% - 25px);
1010
overflow: hidden;
1111
display: flex;
1212
flex-direction: column;
@@ -36,18 +36,33 @@
3636
flex: 1;
3737
}
3838

39-
.open-register-link {
40-
color: $primary;
41-
text-decoration: none;
42-
font-weight: $font-weight-semibold;
39+
.open-register-btn {
40+
display: flex;
41+
align-items: center;
42+
gap: $spacing-xs;
4343
padding: $spacing-xs $spacing-md;
4444
background: $white;
45+
color: $primary;
46+
border: 1px solid $primary;
4547
border-radius: $radius-md;
48+
font-weight: $font-weight-semibold;
49+
font-size: $font-size-sm;
50+
cursor: pointer;
4651
transition: all 0.2s ease;
4752

53+
i {
54+
font-size: $font-size-base;
55+
}
56+
4857
&:hover {
4958
background: $primary;
5059
color: $white;
60+
transform: translateY(-1px);
61+
box-shadow: 0 2px 8px rgba($primary, 0.3);
62+
}
63+
64+
&:active {
65+
transform: translateY(0);
5166
}
5267
}
5368
}

src/app/components/cashier/cashier.component.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,19 @@ import {
2929
} from "../calculator/calculator.component";
3030
import { TranslatePipe } from "../../pipes/translate.pipe";
3131
import { CurrencyPipe } from "../../pipes/currency.pipe";
32-
import { RouterLink } from "@angular/router";
3332
import { environment } from "@environments/environment";
33+
import { OpenRegisterComponent } from "../open-register/open-register.component";
3434

3535
@Component({
3636
selector: "app-cashier",
3737
standalone: true,
3838
imports: [
3939
CommonModule,
4040
FormsModule,
41-
RouterLink,
4241
CalculatorComponent,
4342
TranslatePipe,
4443
CurrencyPipe,
44+
OpenRegisterComponent,
4545
],
4646
templateUrl: "./cashier.component.html",
4747
styleUrls: ["./cashier.component.scss"],
@@ -86,6 +86,7 @@ export class CashierComponent implements OnInit, AfterViewInit {
8686

8787
// Register status
8888
currentRegister = signal<Register | null>(null);
89+
showOpenRegisterModal = signal<boolean>(false);
8990

9091
private itemIdCounter = 0;
9192
private deleteKeyPressCount = 0;
@@ -106,6 +107,20 @@ export class CashierComponent implements OnInit, AfterViewInit {
106107
target?.blur();
107108
}
108109

110+
openRegisterModal(): void {
111+
this.showOpenRegisterModal.set(true);
112+
}
113+
114+
closeRegisterModal(): void {
115+
this.showOpenRegisterModal.set(false);
116+
}
117+
118+
onRegisterOpened(): void {
119+
this.closeRegisterModal();
120+
// Focus calculator after register is opened
121+
setTimeout(() => this.calculator?.focusCalculator(), 300);
122+
}
123+
109124
constructor(
110125
private saleService: SaleService,
111126
private cartService: CartService,

src/app/components/dashboard/dashboard.component.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
.dashboard-container {
44
padding: 2rem;
55
background-color: var(--bg-secondary);
6-
min-height: calc(100vh - 60px);
6+
min-height: calc(100vh - 65px);
77
overflow-y: auto;
88

99
.dashboard-header {

src/app/components/inventory/inventory.component.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
padding: 0 $spacing-sm;
55
max-width: 1500px;
66
margin: 0 auto;
7-
min-height: 100%;
7+
height: calc(100% - 65px);
88
border-radius: 0;
99
}
1010

src/app/components/layout/layout.component.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
.app-layout {
44
display: flex;
5-
height: 100vh;
5+
height: calc(100vh - 70px);
66
overflow: hidden;
77
background: var(--bg-primary);
88
}

src/app/components/login/login.component.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
.login-container {
44
display: flex;
55
align-items: stretch;
6-
min-height: 100vh;
6+
height: calc(100% - 65px);
77
background: var(--bg-primary);
88

99
// Mobile: constrain to 100vh max

0 commit comments

Comments
 (0)