From d01a1fbb3456b69d65fd7211e951a42c5fa49293 Mon Sep 17 00:00:00 2001 From: Amelia <77553571+Fesaa@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:12:05 +0200 Subject: [PATCH 1/2] Defaults values, nicer table sizes --- UI/Web/public/assets/i18n/en.json | 1 + .../browse-deliveries.component.html | 24 +++++++++++++------ .../client-modal/client-modal.component.html | 14 +++++------ .../client-modal/client-modal.component.ts | 4 +++- .../management-clients.component.html | 10 +++++--- .../management-clients.component.ts | 2 ++ UI/Web/src/theme/components/table.scss | 6 +++++ 7 files changed, 43 insertions(+), 18 deletions(-) diff --git a/UI/Web/public/assets/i18n/en.json b/UI/Web/public/assets/i18n/en.json index f16214c..620a10a 100644 --- a/UI/Web/public/assets/i18n/en.json +++ b/UI/Web/public/assets/i18n/en.json @@ -89,6 +89,7 @@ "from": "From", "to": "Recipient", "state": "Delivery state", + "size": "Size", "created": "Created on", "actions": "Actions", "transition": "Transition" diff --git a/UI/Web/src/app/browse-deliveries/browse-deliveries.component.html b/UI/Web/src/app/browse-deliveries/browse-deliveries.component.html index c29ea71..18a8810 100644 --- a/UI/Web/src/app/browse-deliveries/browse-deliveries.component.html +++ b/UI/Web/src/app/browse-deliveries/browse-deliveries.component.html @@ -12,6 +12,7 @@ {{t('from')}} {{t('to')}} {{t('state')}} + {{t('size')}} {{t('created')}} {{t('actions')}} @@ -23,16 +24,25 @@ {{delivery.state | deliveryState}} + + {{delivery.lines.length}} + {{ delivery.createdUtc ?? '—' | utcToLocalTime:'shortDate' }} - + + +
+ - + + + - - - + +
diff --git a/UI/Web/src/app/management/management-clients/_components/client-modal/client-modal.component.html b/UI/Web/src/app/management/management-clients/_components/client-modal/client-modal.component.html index 86903a5..e000fad 100644 --- a/UI/Web/src/app/management/management-clients/_components/client-modal/client-modal.component.html +++ b/UI/Web/src/app/management/management-clients/_components/client-modal/client-modal.component.html @@ -22,7 +22,7 @@ @if (clientForm.get('name'); as control) { - {{control.value}} + {{control.value | defaultValue}} @@ -35,7 +35,7 @@ @if (clientForm.get('address'); as control) { - {{control.value}} + {{control.value | defaultValue}} @@ -48,7 +48,7 @@ @if (clientForm.get('companyNumber'); as control) { - {{control.value}} + {{control.value | defaultValue}} @@ -61,7 +61,7 @@ @if (clientForm.get('contactEmail'); as control) { - {{control.value}} + {{control.value | defaultValue}} @@ -74,7 +74,7 @@ @if (clientForm.get('contactName'); as control) { - {{control.value}} + {{control.value | defaultValue}} @@ -87,7 +87,7 @@ @if (clientForm.get('contactNumber'); as control) { - {{control.value}} + {{control.value | defaultValue}} @@ -100,7 +100,7 @@ @if (clientForm.get('invoiceEmail'); as control) { - {{control.value}} + {{control.value | defaultValue}} diff --git a/UI/Web/src/app/management/management-clients/_components/client-modal/client-modal.component.ts b/UI/Web/src/app/management/management-clients/_components/client-modal/client-modal.component.ts index ad3210a..916dfc4 100644 --- a/UI/Web/src/app/management/management-clients/_components/client-modal/client-modal.component.ts +++ b/UI/Web/src/app/management/management-clients/_components/client-modal/client-modal.component.ts @@ -6,6 +6,7 @@ import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} fr import {ModalDismissReasons, NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; import {ClientService} from '../../../../_services/client.service'; import {SettingsItemComponent} from '../../../../shared/components/settings-item/settings-item.component'; +import {DefaultValuePipe} from '../../../../_pipes/default-value.pipe'; @Component({ selector: 'app-client-modal', @@ -14,7 +15,8 @@ import {SettingsItemComponent} from '../../../../shared/components/settings-item LoadingSpinnerComponent, FormsModule, ReactiveFormsModule, - SettingsItemComponent + SettingsItemComponent, + DefaultValuePipe ], templateUrl: './client-modal.component.html', styleUrl: './client-modal.component.scss', diff --git a/UI/Web/src/app/management/management-clients/management-clients.component.html b/UI/Web/src/app/management/management-clients/management-clients.component.html index 87e5f52..3e05c6a 100644 --- a/UI/Web/src/app/management/management-clients/management-clients.component.html +++ b/UI/Web/src/app/management/management-clients/management-clients.component.html @@ -67,13 +67,17 @@

{{t('no-clients.title')}}

{{item.name}} - {{item.companyNumber}} + {{item.companyNumber | defaultValue}} - {{item.contactEmail}} + @if (item.contactEmail) { + {{item.contactEmail}} + } @else { + {{null | defaultValue}} + } - {{item.contactNumber}} + {{item.contactNumber | defaultValue}} + + + + + + + diff --git a/UI/Web/src/app/management/management-clients/_components/import-client-modal/import-client-modal.component.scss b/UI/Web/src/app/management/management-clients/_components/import-client-modal/import-client-modal.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/UI/Web/src/app/management/management-clients/_components/import-client-modal/import-client-modal.component.ts b/UI/Web/src/app/management/management-clients/_components/import-client-modal/import-client-modal.component.ts new file mode 100644 index 0000000..3dfe792 --- /dev/null +++ b/UI/Web/src/app/management/management-clients/_components/import-client-modal/import-client-modal.component.ts @@ -0,0 +1,224 @@ +import {ChangeDetectionStrategy, Component, computed, inject, signal} from '@angular/core'; +import {LoadingSpinnerComponent} from '../../../../shared/components/loading-spinner/loading-spinner.component'; +import {translate, TranslocoDirective} from '@jsverse/transloco'; +import {ClientService} from '../../../../_services/client.service'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {FormArray, FormControl, FormGroup, NonNullableFormBuilder, ReactiveFormsModule} from '@angular/forms'; +import {FileUploadComponent, FileUploadValidators} from '@iplab/ngx-file-upload'; +import {map} from 'rxjs'; +import {toSignal} from '@angular/core/rxjs-interop'; +import {ToastrService} from 'ngx-toastr'; +import Papa from 'papaparse'; +import {SettingsItemComponent} from '../../../../shared/components/settings-item/settings-item.component'; +import {ClientFieldPipe} from '../../../../_pipes/client-field-pipe'; +import {Client} from '../../../../_models/client'; +import {TableComponent} from '../../../../shared/components/table/table.component'; +import {DefaultValuePipe} from '../../../../_pipes/default-value.pipe'; +import {ClientsTableComponent} from '../clients-table/clients-table.component'; + +enum StageId { + FileImport = 'file-import', + HeaderMatch = 'header-match', + Confirm = 'confirm', +} + +export enum ClientField { + Name = 0, + Address = 1, + CompanyNumber = 2, + InvoiceEmail = 3, + ContactName = 4, + ContactEmail = 5, + ContactNumber = 6, +} + +const fields: ClientField[] = [ClientField.Name, ClientField.Address, ClientField.CompanyNumber, +ClientField.InvoiceEmail, ClientField.ContactName, ClientField.ContactEmail, ClientField.ContactNumber]; + +type HeaderMappingControl = FormGroup<{ + header: FormControl, + field: FormControl, + index: FormControl +}>; + +@Component({ + selector: 'app-import-client-modal', + imports: [ + LoadingSpinnerComponent, + TranslocoDirective, + ReactiveFormsModule, + FileUploadComponent, + SettingsItemComponent, + ClientFieldPipe, + TableComponent, + DefaultValuePipe, + ClientsTableComponent, + ], + templateUrl: './import-client-modal.component.html', + styleUrl: './import-client-modal.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ImportClientModalComponent { + + private readonly clientService = inject(ClientService); + protected readonly modal = inject(NgbActiveModal); + private readonly toastr = inject(ToastrService); + private readonly fb = inject(NonNullableFormBuilder); + + fileUploadControl = new FormControl>(undefined, [ + FileUploadValidators.accept(['.csv']), FileUploadValidators.filesLimit(1) + ]); + + uploadForm = new FormGroup({ + files: this.fileUploadControl, + }); + + headerMatchForm: FormGroup<{headers: FormArray}> = new FormGroup({ + headers: new FormArray([]) + }); + + isSaving = signal(false); + currentStage = signal(StageId.FileImport); + // CSV data without header + data = signal([]); + clients = signal([]); + + isFileSelected = toSignal(this.uploadForm.get('files')!.valueChanges + .pipe(map((files) => !!files && files.length == 1)), {initialValue: false}); + + buttonLabel = computed(() => { + switch (this.currentStage()) { + case StageId.FileImport: + return translate('import-client-modal.next'); + case StageId.HeaderMatch: + return translate('import-client-modal.next'); + case StageId.Confirm : + return translate('import-client-modal.import'); + } + }); + canMoveToNext = computed(() => { + switch (this.currentStage()) { + case StageId.FileImport: + return this.isFileSelected(); + case StageId.HeaderMatch: + return this.headerMatchForm.valid; + case StageId.Confirm : + return true; + } + }); + + get headerArray(): FormArray { + return this.headerMatchForm.get('headers')! as FormArray; + } + + async nextStep() { + switch (this.currentStage()) { + case StageId.FileImport: + await this.handleFileImport(); + break; + case StageId.HeaderMatch: + await this.constructClients(); + break; + case StageId.Confirm: + this.import(); + break; + } + } + + private async handleFileImport() { + const files = this.fileUploadControl.value; + if (!files || files.length === 0) { + this.toastr.error(translate('import-client-modal.select-files-warning')); + return; + } + + const file = files[0]; + const text = await file.text(); + const res = Papa.parse(text, {header: false, delimiter: ','}); + if (res.errors.length > 0) { + console.log(res); + this.toastr.error(translate('import-client-modal.parse-error')); + return; + } + + const data = res.data; + if (data.length < 2) { + this.toastr.error(translate('import-client-modal.header-only-or-none')); + return; + } + + const headers = data[0]; + this.headerArray.setValue([]); + + headers.forEach((header, idx) => { + this.headerArray.push(this.fb.group({ + header: this.fb.control(header), + field: this.fb.control(fields[idx % fields.length]), + index: this.fb.control(idx), + })); + }); + + this.data.set(res.data.slice(1).filter(d => d.length === headers.length)); + this.currentStage.set(StageId.HeaderMatch); + } + + private async constructClients() { + const mappings = this.headerArray.controls.map(c => c.value); + + const clients: Client[] = []; + + for (const dataRow of this.data()) { + + const client: Partial = {}; + + + for (const field of fields) { + const mapping = mappings.find(mapping => mapping.field === field); + const value = (mapping !== undefined && mapping.index !== undefined) ? dataRow[mapping.index] : ''; + + switch (field) { + case ClientField.Name: + client.name = value; + break; + case ClientField.Address: + client.address = value; + break; + case ClientField.ContactEmail: + client.contactEmail = value; + break; + case ClientField.CompanyNumber: + client.companyNumber = value; + break; + case ClientField.InvoiceEmail: + client.invoiceEmail = value; + break; + case ClientField.ContactNumber: + client.contactNumber = value; + break; + case ClientField.ContactName: + client.contactName = value; + break; + } + } + + clients.push(client as Client); + } + + this.clients.set(clients); + this.currentStage.set(StageId.Confirm); + } + + private import() { + this.clientService.createBulk(this.clients()).subscribe({ + next: () => this.close(), + }); + } + + close() { + this.modal.close(); + } + + protected readonly StageId = StageId; + protected readonly fields = fields; + protected readonly JSON = JSON; +} diff --git a/UI/Web/src/app/management/management-clients/management-clients.component.html b/UI/Web/src/app/management/management-clients/management-clients.component.html index 3e05c6a..f0808e7 100644 --- a/UI/Web/src/app/management/management-clients/management-clients.component.html +++ b/UI/Web/src/app/management/management-clients/management-clients.component.html @@ -5,7 +5,8 @@

{{t('title')}}

@@ -42,43 +43,8 @@

{{t('no-clients.title')}}

} @else {
- - - - - {{t('header.name')}} - - - {{t('header.companyNumber')}} - - - {{t('header.contactEmail')}} - - - {{t('header.contactNumber')}} - - - {{t('header.actions')}} - - - - - - {{item.name}} - - - {{item.companyNumber | defaultValue}} - - - @if (item.contactEmail) { - {{item.contactEmail}} - } @else { - {{null | defaultValue}} - } - - - {{item.contactNumber | defaultValue}} - + + - +
} diff --git a/UI/Web/src/app/management/management-clients/management-clients.component.ts b/UI/Web/src/app/management/management-clients/management-clients.component.ts index 2f76342..add2a3a 100644 --- a/UI/Web/src/app/management/management-clients/management-clients.component.ts +++ b/UI/Web/src/app/management/management-clients/management-clients.component.ts @@ -8,6 +8,8 @@ import {TableComponent} from '../../shared/components/table/table.component'; import {ClientModalComponent} from './_components/client-modal/client-modal.component'; import {DefaultModalOptions} from '../../_models/default-modal-options'; import {DefaultValuePipe} from '../../_pipes/default-value.pipe'; +import {ImportClientModalComponent} from './_components/import-client-modal/import-client-modal.component'; +import {ClientsTableComponent} from './_components/clients-table/clients-table.component'; @Component({ selector: 'app-management-clients', @@ -16,6 +18,7 @@ import {DefaultValuePipe} from '../../_pipes/default-value.pipe'; LoadingSpinnerComponent, TableComponent, DefaultValuePipe, + ClientsTableComponent, ], templateUrl: './management-clients.component.html', styleUrl: './management-clients.component.scss', @@ -47,13 +50,19 @@ export class ManagementClientsComponent implements OnInit { return `${client.id}` } + importClients() { + const [modal, component] = this.modalService.open(ImportClientModalComponent, DefaultModalOptions); + + modal.closed.subscribe(() => this.loadClients()); + } + createOrUpdateClient(client?: Client) { const [modal, component] = this.modalService.open(ClientModalComponent, DefaultModalOptions); if (client) { component.client.set(client); } - modal.closed.subscribe(() => this.loadClients()) + modal.closed.subscribe(() => this.loadClients()); } async deleteClient(client: Client) {