import { DatePipe, DecimalPipe } from '@angular/common'; import { httpResource } from '@angular/common/http'; import { ChangeDetectionStrategy, Component, computed, effect, inject, signal } from '@angular/core'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatOptionModule } from '@angular/material/core'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTableModule } from '@angular/material/table'; import { MatTooltipModule } from '@angular/material/tooltip'; import moment from 'moment'; import { ErrorStateComponent } from '../shared/error-state/error-state.component'; import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component'; import { DayPlan, ImportRequest, MappingEntry, PreviewResponse, SaleLine, VoucherPlan } from './sales-import'; import { SalesImportService } from './sales-import.service'; interface SalesImportFormData { startDate: Date; finishDate: Date; } @Component({ selector: 'app-sales-import', templateUrl: './sales-import.component.html', styleUrls: ['./sales-import.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ FormField, FormRoot, MatFormFieldModule, MatInputModule, MatDatepickerModule, MatOptionModule, MatSelectModule, MatButtonModule, MatIconModule, MatExpansionModule, MatTableModule, MatTooltipModule, MatSnackBarModule, DatePipe, DecimalPipe, SkeletonLoaderComponent, ErrorStateComponent, ], }) export class SalesImportComponent { private ser = inject(SalesImportService); private snackBar = inject(MatSnackBar); model = signal({ startDate: moment().startOf('month').toDate(), finishDate: moment().toDate(), }); form = createForm(this.model); previewRequest = signal(null); previewResource = httpResource(() => { const request = this.previewRequest(); return request === null ? undefined : { url: '/api/sales-import/preview', method: 'POST', body: request }; }); error = computed(() => this.previewResource.error()); loading = computed(() => this.previewResource.isLoading()); preview = computed(() => this.previewResource.value() ?? null); days = computed(() => this.preview()?.days ?? []); costCentres = computed(() => this.preview()?.costCentres ?? []); mappingEntries = computed(() => this.preview()?.mapping ?? []); newProducts = computed(() => this.preview()?.newProducts ?? []); unmappedCategories = computed( () => this.mappingEntries().filter((entry) => this.mappingSelection()[entry.saleCategoryId] == null).length, ); canImport = computed(() => this.days().length > 0 && this.unmappedCategories() === 0); mappingSelection = signal>({}); importing = signal(false); expanded = signal(null); displayedColumns = ['expand', 'businessDate', 'action', 'voucherCount', 'amount']; lineColumns = ['productName', 'quantity', 'rate', 'taxRate', 'discount', 'amount', 'happyHour']; constructor() { effect(() => { const response = this.preview(); if (response) { this.mappingSelection.set( Object.fromEntries(response.mapping.map((entry) => [entry.saleCategoryId, entry.costCentre?.id ?? null])), ); } }); } show(): void { const model = this.model(); this.previewRequest.set({ startDate: moment(model.startDate).format('YYYY-MM-DD'), finishDate: moment(model.finishDate).format('YYYY-MM-DD'), }); } onMappingChange(saleCategoryId: string, costCentreId: string | null): void { this.mappingSelection.update((current) => ({ ...current, [saleCategoryId]: costCentreId })); } saveMapping(): void { const entries: MappingEntry[] = this.mappingEntries(); const selection = this.mappingSelection(); const mapping = entries.map((entry) => ({ saleCategoryId: entry.saleCategoryId, costCentreId: selection[entry.saleCategoryId] ?? null, })); this.ser.saveMapping({ mapping }).subscribe({ next: () => { this.snackBar.open('Sale category mapping saved', 'Close', { duration: 3000 }); this.show(); }, error: (err) => this.snackBar.open('Error saving mapping: ' + err.message, 'Close', { duration: 5000 }), }); } import(): void { const model = this.model(); const request: ImportRequest = { startDate: moment(model.startDate).format('YYYY-MM-DD'), finishDate: moment(model.finishDate).format('YYYY-MM-DD'), }; this.importing.set(true); this.ser.execute(request).subscribe({ next: (result) => { this.importing.set(false); this.snackBar.open(result.message, 'Close', { duration: 5000 }); this.show(); }, error: (err) => { this.importing.set(false); this.snackBar.open('Import failed: ' + (err.error?.detail ?? err.message), 'Close', { duration: 8000 }); }, }); } toggleExpanded(day: DayPlan): void { this.expanded.update((current) => (current === day ? null : day)); } actionClass(action: string): string { return `action action-${action}`; } trackDay(_index: number, day: DayPlan): string { return day.businessDate; } trackVoucher(_index: number, voucher: VoucherPlan): string { return `${voucher.saleCategoryId}-${voucher.costCentre.id}`; } trackLine(_index: number, line: SaleLine): string { return `${line.skuId ?? line.productName}-${line.rate}-${line.isHappyHour}`; } }