In progress Sales import and Recipe import.

First I will finish Sales Import. Then move on to recipe import
This commit is contained in:
tanshu committed 2026-09-11 10:43:23 +00:00
1 parent 75ccbdc6f9
commit b425b299bf
50 files changed
+5306 -123

No files matched your search

@@ -0,0 +1,169 @@
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<SalesImportFormData>({
startDate: moment().startOf('month').toDate(),
finishDate: moment().toDate(),
});
form = createForm(this.model);
previewRequest = signal<ImportRequest | null>(null);
previewResource = httpResource<PreviewResponse>(() => {
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<Record<string, string | null>>({});
importing = signal(false);
expanded = signal<DayPlan | null>(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}`;
}
}