diff --git a/API/Controllers/ClientController.cs b/API/Controllers/ClientController.cs index 4fe6061..d32df58 100644 --- a/API/Controllers/ClientController.cs +++ b/API/Controllers/ClientController.cs @@ -52,6 +52,18 @@ public async Task CreateClient(ClientDto dto) return Ok(); } + /// + /// Create several clients at once + /// + /// + /// + [HttpPost("create-bulk")] + public async Task CreateClientBulk(IList dtos) + { + await clientService.CreateClients(dtos); + return Ok(); + } + /// /// Update an existing client /// diff --git a/API/Services/ClientService.cs b/API/Services/ClientService.cs index 2a4d44b..c6ff431 100644 --- a/API/Services/ClientService.cs +++ b/API/Services/ClientService.cs @@ -10,6 +10,7 @@ namespace API.Services; public interface IClientService { Task CreateClient(ClientDto dto); + Task CreateClients(IList dtos); Task UpdateClient(ClientDto dto); Task DeleteClient(int id); } @@ -18,6 +19,22 @@ public class ClientService(IUnitOfWork unitOfWork, ILogger logger { public async Task CreateClient(ClientDto dto) + { + await CreateClientFromDto(dto); + await unitOfWork.CommitAsync(); + } + + public async Task CreateClients(IList dtos) + { + foreach (var dto in dtos) + { + await CreateClientFromDto(dto); + } + + await unitOfWork.CommitAsync(); + } + + private async Task CreateClientFromDto(ClientDto dto) { if (!string.IsNullOrWhiteSpace(dto.CompanyNumber)) { @@ -39,7 +56,6 @@ public async Task CreateClient(ClientDto dto) }; unitOfWork.ClientRepository.Add(client); - await unitOfWork.CommitAsync(); } public async Task UpdateClient(ClientDto dto) diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index dd04610..c286309 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -16,6 +16,7 @@ "@angular/forms": "^20.1.1", "@angular/platform-browser": "^20.1.1", "@angular/router": "^20.1.1", + "@iplab/ngx-file-upload": "^20.0.0", "@jsverse/transloco": "^7.6.1", "@ng-bootstrap/ng-bootstrap": "^19.0.1", "@popperjs/core": "^2.11.8", @@ -23,6 +24,7 @@ "bootstrap": "^5.3.6", "luxon": "^3.7.1", "ngx-toastr": "^19.0.0", + "papaparse": "^5.5.3", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -34,6 +36,7 @@ "@angular/localize": "^20.1.1", "@types/jasmine": "~5.1.0", "@types/luxon": "^3.6.2", + "@types/papaparse": "^5.3.16", "jasmine-core": "~5.7.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", @@ -1744,6 +1747,22 @@ } } }, + "node_modules/@iplab/ngx-file-upload": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@iplab/ngx-file-upload/-/ngx-file-upload-20.0.0.tgz", + "integrity": "sha512-cBUg9Y3WgIWcumS6ei6JX77EaZL9lR/jsu9T4FEE0kbZd6N3xcwFYscWcqNHHRhImeKHPHFMfVU4oW9CuiDFxQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^20.0.0", + "@angular/common": "^20.0.0", + "@angular/core": "^20.0.0", + "@angular/forms": "^20.0.0", + "rxjs": "^7.0.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3891,6 +3910,16 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/papaparse": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.16.tgz", + "integrity": "sha512-T3VuKMC2H0lgsjI9buTB3uuKj3EMD2eap1MOuEQuBQ44EnDx/IkGhU6EwiTf9zG3za4SKlmwKAImdDKdNnCsXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", @@ -8137,6 +8166,12 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/UI/Web/package.json b/UI/Web/package.json index b45d0a3..caecbef 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -28,6 +28,7 @@ "@angular/forms": "^20.1.1", "@angular/platform-browser": "^20.1.1", "@angular/router": "^20.1.1", + "@iplab/ngx-file-upload": "^20.0.0", "@jsverse/transloco": "^7.6.1", "@ng-bootstrap/ng-bootstrap": "^19.0.1", "@popperjs/core": "^2.11.8", @@ -35,6 +36,7 @@ "bootstrap": "^5.3.6", "luxon": "^3.7.1", "ngx-toastr": "^19.0.0", + "papaparse": "^5.5.3", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -46,6 +48,7 @@ "@angular/localize": "^20.1.1", "@types/jasmine": "~5.1.0", "@types/luxon": "^3.6.2", + "@types/papaparse": "^5.3.16", "jasmine-core": "~5.7.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", diff --git a/UI/Web/public/assets/i18n/en.json b/UI/Web/public/assets/i18n/en.json index f16214c..5ab1ea9 100644 --- a/UI/Web/public/assets/i18n/en.json +++ b/UI/Web/public/assets/i18n/en.json @@ -58,6 +58,26 @@ "invoiceEmail-tooltip": "Email to receive invoices and billing-related communications" }, + "import-client-modal": { + "title": "Import clients from CSV", + "cancel": "{{common.cancel}}", + "create": "{{common.create}}", + "close": "{{common.close}}", + "import": "Import", + "next": "Next", + "import-description": "Select your csv export, to mass import clients from a known source" + }, + + "client-field-pipe": { + "name": "Name", + "address": "Address", + "company-number": "Company Number", + "contact-email": "Contact Email", + "contact-name": "Contact Name", + "contact-number": "Contact Number", + "invoice-email": "Invoice email" + }, + "manage-delivery": { "loading": "Loading...", "new-delivery": "New Delivery", @@ -89,6 +109,7 @@ "from": "From", "to": "Recipient", "state": "Delivery state", + "size": "Size", "created": "Created on", "actions": "Actions", "transition": "Transition" @@ -304,7 +325,10 @@ "companyNumber": "Company number", "contactEmail": "Email", "contactNumber": "Phone number", - "actions": "{{common.actions}}" + "actions": "{{common.actions}}", + "address": "Address", + "invoiceEmail": "Invoice Email", + "contactName": "Contact Name" } } } diff --git a/UI/Web/src/app/_pipes/client-field-pipe.ts b/UI/Web/src/app/_pipes/client-field-pipe.ts new file mode 100644 index 0000000..5519a94 --- /dev/null +++ b/UI/Web/src/app/_pipes/client-field-pipe.ts @@ -0,0 +1,31 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import { + ClientField +} from '../management/management-clients/_components/import-client-modal/import-client-modal.component'; +import {translate} from '@jsverse/transloco'; + +@Pipe({ + name: 'clientField' +}) +export class ClientFieldPipe implements PipeTransform { + + transform(value: ClientField): string { + switch (value) { + case ClientField.Name: + return translate('client-field-pipe.name'); + case ClientField.Address: + return translate('client-field-pipe.address'); + case ClientField.CompanyNumber: + return translate('client-field-pipe.company-number'); + case ClientField.ContactEmail: + return translate('client-field-pipe.contact-email'); + case ClientField.ContactName: + return translate('client-field-pipe.contact-name'); + case ClientField.ContactNumber: + return translate('client-field-pipe.contact-number'); + case ClientField.InvoiceEmail: + return translate('client-field-pipe.invoice-email'); + } + } + +} diff --git a/UI/Web/src/app/_services/client.service.ts b/UI/Web/src/app/_services/client.service.ts index b972d68..69f5349 100644 --- a/UI/Web/src/app/_services/client.service.ts +++ b/UI/Web/src/app/_services/client.service.ts @@ -31,6 +31,10 @@ export class ClientService { return this.http.post(this.baseUrl, dto); } + createBulk(dtos: Client[]): Observable { + return this.http.post(this.baseUrl+"create-bulk", dtos); + } + update(dto: Client): Observable { return this.http.put(this.baseUrl, dto); } 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/_components/clients-table/clients-table.component.html b/UI/Web/src/app/management/management-clients/_components/clients-table/clients-table.component.html new file mode 100644 index 0000000..7353244 --- /dev/null +++ b/UI/Web/src/app/management/management-clients/_components/clients-table/clients-table.component.html @@ -0,0 +1,66 @@ + + + + + {{t('header.name')}} + + + {{t('header.address')}} + + + {{t('header.companyNumber')}} + + + {{t('header.invoiceEmail')}} + + + {{t('header.contactName')}} + + + {{t('header.contactEmail')}} + + + {{t('header.contactNumber')}} + + @if (showActions()) { + + {{t('header.actions')}} + + } + + + + + {{item.name}} + + + {{item.address | defaultValue}} + + + {{item.companyNumber | defaultValue}} + + + @if (item.invoiceEmail) { + {{item.invoiceEmail}} + } @else { + {{null | defaultValue}} + } + + + {{item.contactName | defaultValue}} + + + @if (item.contactEmail) { + {{item.contactEmail}} + } @else { + {{null | defaultValue}} + } + + + {{item.contactNumber | defaultValue}} + + @if (showActions()) { + + } + + diff --git a/UI/Web/src/app/management/management-clients/_components/clients-table/clients-table.component.scss b/UI/Web/src/app/management/management-clients/_components/clients-table/clients-table.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/UI/Web/src/app/management/management-clients/_components/clients-table/clients-table.component.ts b/UI/Web/src/app/management/management-clients/_components/clients-table/clients-table.component.ts new file mode 100644 index 0000000..c85ad5b --- /dev/null +++ b/UI/Web/src/app/management/management-clients/_components/clients-table/clients-table.component.ts @@ -0,0 +1,31 @@ +import {ChangeDetectionStrategy, Component, ContentChild, input, TemplateRef, ViewChild} from '@angular/core'; +import {TableComponent} from '../../../../shared/components/table/table.component'; +import {Client} from '../../../../_models/client'; +import {TranslocoDirective} from '@jsverse/transloco'; +import {DefaultValuePipe} from '../../../../_pipes/default-value.pipe'; +import {NgTemplateOutlet} from '@angular/common'; + +@Component({ + selector: 'app-clients-table', + imports: [ + TableComponent, + TranslocoDirective, + DefaultValuePipe, + NgTemplateOutlet + ], + templateUrl: './clients-table.component.html', + styleUrl: './clients-table.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ClientsTableComponent { + + @ContentChild("actions") actionsRef!: TemplateRef; + + showActions = input.required(); + clients = input.required(); + + clientTracker(idx: number, client: Client): string { + return `${client.id}` + } + +} diff --git a/UI/Web/src/app/management/management-clients/_components/import-client-modal/import-client-modal.component.html b/UI/Web/src/app/management/management-clients/_components/import-client-modal/import-client-modal.component.html new file mode 100644 index 0000000..0a93e45 --- /dev/null +++ b/UI/Web/src/app/management/management-clients/_components/import-client-modal/import-client-modal.component.html @@ -0,0 +1,61 @@ + + + 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 87e5f52..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,39 +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}} - - - {{item.contactEmail}} - - - {{item.contactNumber}} - + + - +
} 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 8b1daf8..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 @@ -7,6 +7,9 @@ import {LoadingSpinnerComponent} from '../../shared/components/loading-spinner/l 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', @@ -14,6 +17,8 @@ import {DefaultModalOptions} from '../../_models/default-modal-options'; TranslocoDirective, LoadingSpinnerComponent, TableComponent, + DefaultValuePipe, + ClientsTableComponent, ], templateUrl: './management-clients.component.html', styleUrl: './management-clients.component.scss', @@ -45,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) { diff --git a/UI/Web/src/theme/components/table.scss b/UI/Web/src/theme/components/table.scss index 2f22de8..8e9d32e 100644 --- a/UI/Web/src/theme/components/table.scss +++ b/UI/Web/src/theme/components/table.scss @@ -39,3 +39,9 @@ font-weight: 500; } } + +.table-header-cell:last-child, +.table-cell:last-child { + width: 1%; + white-space: nowrap; +}