170 lines
5.9 KiB
TypeScript
170 lines
5.9 KiB
TypeScript
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}`;
|
|
}
|
|
}
|