produzione 1 #9

Merged
JoeKung merged 135 commits from dev into main 2026-03-03 09:58:04 +01:00
Showing only changes of commit cecdfacd33 - Show all commits

View File

@@ -1,4 +1,4 @@
import { Component, input, output, signal } from '@angular/core';
import { Component, input, output, signal, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
@@ -9,6 +9,11 @@ import { AppButtonComponent } from '../../../../shared/components/app-button/app
import { StlViewerComponent } from '../../../../shared/components/stl-viewer/stl-viewer.component';
import { QuoteRequest } from '../../services/quote-estimator.service';
interface FormItem {
file: File;
quantity: number;
}
@Component({
selector: 'app-upload-form',
standalone: true,
@@ -20,28 +25,53 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
@if (selectedFile()) {
<div class="viewer-wrapper">
<app-stl-viewer [file]="selectedFile()"></app-stl-viewer>
<button type="button" class="btn-clear" (click)="clearFiles()">
X
<button type="button" class="btn-clear-viewer" (click)="selectedFile.set(null)">
Close Viewer
</button>
</div>
<div class="file-list">
@for (f of files(); track f.name) {
<div class="file-item" [class.active]="f === selectedFile()" (click)="selectFile(f)">
{{ f.name }}
</div>
}
</div>
} @else {
<app-dropzone
}
<!-- Dropzone always available if we want to add more, or hide if list not empty? -->
<!-- User wants to "add more", so dropzone should remain or be available -->
<app-dropzone
[label]="'CALC.UPLOAD_LABEL' | translate"
[subtext]="'CALC.UPLOAD_SUB' | translate"
[accept]="acceptedFormats"
[multiple]="true"
(filesDropped)="onFilesDropped($event)">
</app-dropzone>
</app-dropzone>
<!-- New File List with Details -->
@if (items().length > 0) {
<div class="items-list">
@for (item of items(); track item.file.name; let i = $index) {
<div class="file-row" [class.active]="item.file === selectedFile()">
<div class="file-info" (click)="selectFile(item.file)">
<span class="file-name">{{ item.file.name }}</span>
<span class="file-size">{{ (item.file.size / 1024 / 1024) | number:'1.1-2' }} MB</span>
</div>
<div class="file-actions">
<div class="qty-control">
<label>Qtà</label>
<input
type="number"
min="1"
[value]="item.quantity"
(change)="updateItemQuantity(i, $event)"
class="qty-input">
</div>
<button type="button" class="btn-remove" (click)="removeItem(i)" title="Remove file">
X
</button>
</div>
</div>
}
</div>
}
@if (form.get('files')?.invalid && form.get('files')?.touched) {
@if (items().length === 0 && form.get('itemsTouched')?.value) {
<div class="error-msg">{{ 'CALC.ERR_FILE_REQUIRED' | translate }}</div>
}
</div>
@@ -60,11 +90,7 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
></app-select>
</div>
<app-input
formControlName="quantity"
type="number"
[label]="'CALC.QUANTITY' | translate"
></app-input>
<!-- Global quantity removed, now per item -->
@if (mode() === 'advanced') {
<div class="grid">
@@ -113,7 +139,7 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
<app-button
type="submit"
[disabled]="form.invalid || loading()"
[disabled]="form.invalid || items().length === 0 || loading()"
[fullWidth]="true">
{{ loading() ? (uploadProgress() < 100 ? 'Uploading...' : 'Processing...') : ('CALC.CALCULATE' | translate) }}
</app-button>
@@ -127,40 +153,92 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
.error-msg { color: var(--color-danger-500); font-size: 0.875rem; margin-top: var(--space-2); text-align: center; }
.viewer-wrapper { position: relative; margin-bottom: var(--space-4); }
.btn-clear {
.btn-clear-viewer {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.5);
color: white;
border: none;
width: 32px;
height: 32px;
border-radius: 50%;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
z-index: 10;
&:hover { background: rgba(0,0,0,0.7); }
}
.file-list {
.items-list {
display: flex;
flex-direction: column;
gap: var(--space-2);
overflow-x: auto;
padding-bottom: var(--space-2);
margin-top: var(--space-4);
}
.file-item {
padding: 0.5rem 1rem;
.file-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3);
background: var(--color-neutral-100);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: 0.85rem;
cursor: pointer;
white-space: nowrap;
&:hover { background: var(--color-neutral-200); }
transition: all 0.2s;
&.active {
border-color: var(--color-brand);
background: rgba(250, 207, 10, 0.1);
font-weight: 600;
border-color: var(--color-brand);
background: rgba(250, 207, 10, 0.05);
}
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
cursor: pointer;
}
.file-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); }
.file-size { font-size: 0.75rem; color: var(--color-text-muted); }
.file-actions {
display: flex;
align-items: center;
gap: var(--space-3);
}
.qty-control {
display: flex;
align-items: center;
gap: var(--space-2);
label { font-size: 0.8rem; color: var(--color-text-muted); }
}
.qty-input {
width: 50px;
padding: 4px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
text-align: center;
font-size: 0.9rem;
&:focus { outline: none; border-color: var(--color-brand); }
}
.btn-remove {
width: 28px;
height: 28px;
border-radius: 50%;
border: none;
background: transparent; // var(--color-neutral-200);
color: var(--color-text-muted); // var(--color-danger-500);
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: var(--color-danger-100);
color: var(--color-danger-500);
}
}
@@ -185,9 +263,6 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
/* Progress Bar */
.progress-container {
margin-bottom: var(--space-3);
/* padding: var(--space-2); */
/* background: var(--color-neutral-100); */
/* border-radius: var(--radius-md); */
text-align: center;
width: 100%;
}
@@ -206,7 +281,6 @@ import { QuoteRequest } from '../../services/quote-estimator.service';
width: 0%;
transition: width 0.2s ease-out;
}
.progress-text { font-size: 0.875rem; color: var(--color-text-muted); }
`]
})
export class UploadFormComponent {
@@ -217,7 +291,7 @@ export class UploadFormComponent {
form: FormGroup;
files = signal<File[]>([]);
items = signal<FormItem[]>([]);
selectedFile = signal<File | null>(null);
materials = [
@@ -252,10 +326,9 @@ export class UploadFormComponent {
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
files: [[], Validators.required],
itemsTouched: [false], // Hack to track touched state for custom items list
material: ['PLA', Validators.required],
quality: ['Standard', Validators.required],
quantity: [1, [Validators.required, Validators.min(1)]],
notes: [''],
// Advanced fields
color: ['Black'],
@@ -267,14 +340,14 @@ export class UploadFormComponent {
onFilesDropped(newFiles: File[]) {
const MAX_SIZE = 200 * 1024 * 1024; // 200MB
const validFiles: File[] = [];
const validItems: FormItem[] = [];
let hasError = false;
for (const file of newFiles) {
if (file.size > MAX_SIZE) {
hasError = true;
} else {
validFiles.push(file);
validItems.push({ file, quantity: 1 });
}
}
@@ -282,32 +355,55 @@ export class UploadFormComponent {
alert("Alcuni file superano il limite di 200MB e non sono stati aggiunti.");
}
if (validFiles.length > 0) {
this.files.update(current => [...current, ...validFiles]);
this.form.patchValue({ files: this.files() });
this.form.get('files')?.markAsTouched();
this.selectedFile.set(validFiles[validFiles.length - 1]);
if (validItems.length > 0) {
this.items.update(current => [...current, ...validItems]);
this.form.get('itemsTouched')?.setValue(true);
// Auto select last added
this.selectedFile.set(validItems[validItems.length - 1].file);
}
}
selectFile(file: File) {
this.selectedFile.set(file);
if (this.selectedFile() === file) {
// toggle off? no, keep active
} else {
this.selectedFile.set(file);
}
}
clearFiles() {
this.files.set([]);
this.selectedFile.set(null);
this.form.patchValue({ files: [] });
updateItemQuantity(index: number, event: Event) {
const input = event.target as HTMLInputElement;
let val = parseInt(input.value, 10);
if (isNaN(val) || val < 1) val = 1;
this.items.update(current => {
const updated = [...current];
updated[index] = { ...updated[index], quantity: val };
return updated;
});
}
removeItem(index: number) {
this.items.update(current => {
const updated = [...current];
const removed = updated.splice(index, 1)[0];
if (this.selectedFile() === removed.file) {
this.selectedFile.set(null);
}
return updated;
});
}
onSubmit() {
if (this.form.valid) {
if (this.form.valid && this.items().length > 0) {
this.submitRequest.emit({
items: this.items(), // Pass the items array
...this.form.value,
mode: this.mode()
});
} else {
this.form.markAllAsTouched();
this.form.get('itemsTouched')?.setValue(true);
}
}
}