feat(front-end): multiple file upload
All checks were successful
Build, Test and Deploy / test-backend (push) Successful in 22s
Build, Test and Deploy / build-and-push (push) Successful in 20s
Build, Test and Deploy / deploy (push) Successful in 5s

This commit is contained in:
2026-02-05 17:21:52 +01:00
parent fcf439e369
commit 99ae6db064
3 changed files with 79 additions and 37 deletions

View File

@@ -1,4 +1,4 @@
import { Component, signal } from '@angular/core'; import { Component, signal, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@@ -37,6 +37,7 @@ import { Router } from '@angular/router';
</div> </div>
<app-upload-form <app-upload-form
#uploadForm
[mode]="mode()" [mode]="mode()"
[loading]="loading()" [loading]="loading()"
[uploadProgress]="uploadProgress()" [uploadProgress]="uploadProgress()"
@@ -60,7 +61,11 @@ import { Router } from '@angular/router';
</div> </div>
</app-card> </app-card>
} @else if (result()) { } @else if (result()) {
<app-quote-result [result]="result()!" (consult)="onConsult()"></app-quote-result> <app-quote-result
[result]="result()!"
(consult)="onConsult()"
(itemChange)="uploadForm.updateItemQuantityByName($event.fileName, $event.quantity)"
></app-quote-result>
} @else { } @else {
<app-card> <app-card>
<h3>{{ 'CALC.BENEFITS_TITLE' | translate }}</h3> <h3>{{ 'CALC.BENEFITS_TITLE' | translate }}</h3>
@@ -178,6 +183,8 @@ export class CalculatorPageComponent {
result = signal<QuoteResult | null>(null); result = signal<QuoteResult | null>(null);
error = signal<boolean>(false); error = signal<boolean>(false);
@ViewChild('uploadForm') uploadForm!: UploadFormComponent;
constructor(private estimator: QuoteEstimatorService, private router: Router) {} constructor(private estimator: QuoteEstimatorService, private router: Router) {}
onCalculate(req: QuoteRequest) { onCalculate(req: QuoteRequest) {

View File

@@ -15,7 +15,32 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
<app-card> <app-card>
<h3 class="title">{{ 'CALC.RESULT' | translate }}</h3> <h3 class="title">{{ 'CALC.RESULT' | translate }}</h3>
<!-- Detailed Items List --> <!-- Summary Grid (NOW ON TOP) -->
<div class="result-grid">
<app-summary-card
class="item full-width"
[label]="'CALC.COST' | translate"
[large]="true"
[highlight]="true">
{{ totals().price | currency:result().currency }}
</app-summary-card>
<app-summary-card [label]="'CALC.TIME' | translate">
{{ totals().hours }}h {{ totals().minutes }}m
</app-summary-card>
<app-summary-card [label]="'CALC.MATERIAL' | translate">
{{ totals().weight }}g
</app-summary-card>
</div>
<div class="setup-note">
<small>* Include {{ result().setupCost | currency:result().currency }} Setup Cost</small>
</div>
<div class="divider"></div>
<!-- Detailed Items List (NOW ON BOTTOM) -->
<div class="items-list"> <div class="items-list">
@for (item of items(); track item.fileName; let i = $index) { @for (item of items(); track item.fileName; let i = $index) {
<div class="item-row"> <div class="item-row">
@@ -44,31 +69,6 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
} }
</div> </div>
<div class="divider"></div>
<!-- Summary Grid -->
<div class="result-grid">
<app-summary-card
class="item full-width"
[label]="'CALC.COST' | translate"
[large]="true"
[highlight]="true">
{{ totals().price | currency:result().currency }}
</app-summary-card>
<app-summary-card [label]="'CALC.TIME' | translate">
{{ totals().hours }}h {{ totals().minutes }}m
</app-summary-card>
<app-summary-card [label]="'CALC.MATERIAL' | translate">
{{ totals().weight }}g
</app-summary-card>
</div>
<div class="setup-note">
<small>* Include {{ result().setupCost | currency:result().currency }} Setup Cost</small>
</div>
<div class="actions"> <div class="actions">
<app-button variant="primary" [fullWidth]="true">{{ 'CALC.ORDER' | translate }}</app-button> <app-button variant="primary" [fullWidth]="true">{{ 'CALC.ORDER' | translate }}</app-button>
<app-button variant="outline" [fullWidth]="true" (click)="consult.emit()">{{ 'CALC.CONSULT' | translate }}</app-button> <app-button variant="outline" [fullWidth]="true" (click)="consult.emit()">{{ 'CALC.CONSULT' | translate }}</app-button>
@@ -159,6 +159,7 @@ import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
export class QuoteResultComponent { export class QuoteResultComponent {
result = input.required<QuoteResult>(); result = input.required<QuoteResult>();
consult = output<void>(); consult = output<void>();
itemChange = output<{fileName: string, quantity: number}>();
// Local mutable state for items to handle quantity changes // Local mutable state for items to handle quantity changes
items = signal<QuoteItem[]>([]); items = signal<QuoteItem[]>([]);
@@ -180,6 +181,11 @@ export class QuoteResultComponent {
updated[index] = { ...updated[index], quantity: qty }; updated[index] = { ...updated[index], quantity: qty };
return updated; return updated;
}); });
this.itemChange.emit({
fileName: this.items()[index].fileName,
quantity: qty
});
} }
totals = computed(() => { totals = computed(() => {

View File

@@ -31,15 +31,16 @@ interface FormItem {
</div> </div>
} }
<!-- Dropzone always available if we want to add more, or hide if list not empty? --> <!-- Initial Dropzone (Visible only when no files) -->
<!-- User wants to "add more", so dropzone should remain or be available --> @if (items().length === 0) {
<app-dropzone <app-dropzone
[label]="'CALC.UPLOAD_LABEL' | translate" [label]="'CALC.UPLOAD_LABEL' | translate"
[subtext]="'CALC.UPLOAD_SUB' | translate" [subtext]="'CALC.UPLOAD_SUB' | translate"
[accept]="acceptedFormats" [accept]="acceptedFormats"
[multiple]="true" [multiple]="true"
(filesDropped)="onFilesDropped($event)"> (filesDropped)="onFilesDropped($event)">
</app-dropzone> </app-dropzone>
}
<!-- New File List with Details --> <!-- New File List with Details -->
@if (items().length > 0) { @if (items().length > 0) {
@@ -69,6 +70,14 @@ interface FormItem {
</div> </div>
} }
</div> </div>
<!-- "Add Files" Button (Visible only when files exist) -->
<div class="add-more-container">
<input #additionalInput type="file" [accept]="acceptedFormats" multiple hidden (change)="onAdditionalFilesSelected($event)">
<app-button variant="outline" [fullWidth]="true" (click)="additionalInput.click()">
+ {{ 'CALC.ADD_FILES' | translate }}
</app-button>
</div>
} }
@if (items().length === 0 && form.get('itemsTouched')?.value) { @if (items().length === 0 && form.get('itemsTouched')?.value) {
@@ -363,6 +372,26 @@ export class UploadFormComponent {
} }
} }
onAdditionalFilesSelected(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
this.onFilesDropped(Array.from(input.files));
// Reset input so same files can be selected again if needed
input.value = '';
}
}
updateItemQuantityByName(fileName: string, quantity: number) {
this.items.update(current => {
return current.map(item => {
if (item.file.name === fileName) {
return { ...item, quantity };
}
return item;
});
});
}
selectFile(file: File) { selectFile(file: File) {
if (this.selectedFile() === file) { if (this.selectedFile() === file) {
// toggle off? no, keep active // toggle off? no, keep active