produzione 1 #9
@@ -26,6 +26,8 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/quote-sessions")
|
||||
@@ -309,4 +311,34 @@ public class QuoteSessionController {
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
// 6. Download Line Item Content
|
||||
@GetMapping(value = "/{sessionId}/line-items/{lineItemId}/content")
|
||||
public ResponseEntity<org.springframework.core.io.Resource> downloadLineItemContent(
|
||||
@PathVariable UUID sessionId,
|
||||
@PathVariable UUID lineItemId
|
||||
) throws IOException {
|
||||
QuoteLineItem item = lineItemRepo.findById(lineItemId)
|
||||
.orElseThrow(() -> new RuntimeException("Item not found"));
|
||||
|
||||
if (!item.getQuoteSession().getId().equals(sessionId)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
if (item.getStoredPath() == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
Path path = Paths.get(item.getStoredPath());
|
||||
if (!Files.exists(path)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
org.springframework.core.io.Resource resource = new org.springframework.core.io.UrlResource(path.toUri());
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + item.getOriginalFilename() + "\"")
|
||||
.body(resource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component, signal, ViewChild, ElementRef, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
|
||||
import { AppAlertComponent } from '../../shared/components/app-alert/app-alert.component';
|
||||
@@ -43,6 +45,95 @@ export class CalculatorPageComponent implements OnInit {
|
||||
this.mode.set(data['mode']);
|
||||
}
|
||||
});
|
||||
|
||||
this.route.queryParams.subscribe(params => {
|
||||
const sessionId = params['session'];
|
||||
if (sessionId) {
|
||||
this.loadSession(sessionId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadSession(sessionId: string) {
|
||||
this.loading.set(true);
|
||||
this.estimator.getQuoteSession(sessionId).subscribe({
|
||||
next: (data) => {
|
||||
// 1. Map to Result
|
||||
const result = this.estimator.mapSessionToQuoteResult(data);
|
||||
this.result.set(result);
|
||||
this.step.set('quote');
|
||||
|
||||
// 2. Determine Mode (Heuristic)
|
||||
// If we have custom settings, maybe Advanced?
|
||||
// For now, let's stick to current mode or infer from URL if possible.
|
||||
// Actually, we can check if settings deviate from Easy defaults.
|
||||
// But let's leave it as is or default to Advanced if not sure.
|
||||
// data.session.materialCode etc.
|
||||
|
||||
// 3. Download Files & Restore Form
|
||||
this.restoreFilesAndSettings(data.session, data.items);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to load session', err);
|
||||
this.error.set(true);
|
||||
this.loading.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
restoreFilesAndSettings(session: any, items: any[]) {
|
||||
if (!items || items.length === 0) {
|
||||
this.loading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download all files
|
||||
const downloads = items.map(item =>
|
||||
this.estimator.getLineItemContent(session.id, item.id).pipe(
|
||||
map((blob: Blob) => {
|
||||
return {
|
||||
blob,
|
||||
fileName: item.originalFilename,
|
||||
// We need to match the file object to the item so we can set colors ideally.
|
||||
// UploadForm.setFiles takes File[].
|
||||
// We might need to handle matching but UploadForm just pushes them.
|
||||
// If order is preserved, we are good. items from backend are list.
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
forkJoin(downloads).subscribe({
|
||||
next: (results: any[]) => {
|
||||
const files = results.map(res => new File([res.blob], res.fileName, { type: 'application/octet-stream' }));
|
||||
|
||||
if (this.uploadForm) {
|
||||
this.uploadForm.setFiles(files);
|
||||
this.uploadForm.patchSettings(session);
|
||||
|
||||
// Also restore colors?
|
||||
// setFiles inits with 'Black'. We need to update them if they differ.
|
||||
// items has colorCode.
|
||||
setTimeout(() => {
|
||||
if (this.uploadForm) {
|
||||
items.forEach((item, index) => {
|
||||
// Assuming index matches.
|
||||
// Need to be careful if items order changed, but usually ID sort or insert order.
|
||||
if (item.colorCode) {
|
||||
this.uploadForm.updateItemColor(index, item.colorCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: (err: any) => {
|
||||
console.error('Failed to download files', err);
|
||||
this.loading.set(false);
|
||||
// Still show result? Yes.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCalculate(req: QuoteRequest) {
|
||||
@@ -67,10 +158,21 @@ export class CalculatorPageComponent implements OnInit {
|
||||
this.uploadProgress.set(event);
|
||||
} else {
|
||||
// It's the result
|
||||
this.result.set(event as QuoteResult);
|
||||
const res = event as QuoteResult;
|
||||
this.result.set(res);
|
||||
this.loading.set(false);
|
||||
this.uploadProgress.set(100);
|
||||
this.step.set('quote');
|
||||
|
||||
// Update URL with session ID without reloading
|
||||
if (res.sessionId) {
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { session: res.sessionId },
|
||||
queryParamsHandling: 'merge', // merge with existing params like 'mode' if any
|
||||
replaceUrl: true // prevent cluttering history, or false if we want back button to work. replaceUrl seems safer for "state update"
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
|
||||
@@ -232,6 +232,59 @@ export class UploadFormComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
setFiles(files: File[]) {
|
||||
const validItems: FormItem[] = [];
|
||||
for (const file of files) {
|
||||
// Default color is Black or derive from somewhere if possible, but here we just init
|
||||
validItems.push({ file, quantity: 1, color: 'Black' });
|
||||
}
|
||||
|
||||
if (validItems.length > 0) {
|
||||
this.items.set(validItems);
|
||||
this.form.get('itemsTouched')?.setValue(true);
|
||||
// Auto select last added
|
||||
this.selectedFile.set(validItems[validItems.length - 1].file);
|
||||
}
|
||||
}
|
||||
|
||||
patchSettings(settings: any) {
|
||||
if (!settings) return;
|
||||
// settings object matches keys in our form?
|
||||
// Session has: materialCode, etc. derived from QuoteSession entity properties
|
||||
// We need to map them if names differ.
|
||||
|
||||
const patch: any = {};
|
||||
if (settings.materialCode) patch.material = settings.materialCode;
|
||||
|
||||
// Heuristic for Quality if not explicitly stored as "draft/standard/high"
|
||||
// But we stored it in session creation?
|
||||
// QuoteSession entity does NOT store "quality" string directly, only layerHeight/infill.
|
||||
// So we might need to deduce it or just set Custom/Advanced.
|
||||
// But for Easy mode, we want to show "Standard" etc.
|
||||
|
||||
// Actually, let's look at what we have in QuoteSession.
|
||||
// layerHeightMm, infillPercent, etc.
|
||||
// If we are in Easy mode, we might just set the "quality" dropdown to match approx?
|
||||
// Or if we stored "quality" in notes or separate field? We didn't.
|
||||
|
||||
// Let's try to reverse map or defaults.
|
||||
if (settings.layerHeightMm) {
|
||||
if (settings.layerHeightMm >= 0.28) patch.quality = 'draft';
|
||||
else if (settings.layerHeightMm <= 0.12) patch.quality = 'high';
|
||||
else patch.quality = 'standard';
|
||||
|
||||
patch.layerHeight = settings.layerHeightMm;
|
||||
}
|
||||
|
||||
if (settings.nozzleDiameterMm) patch.nozzleDiameter = settings.nozzleDiameterMm;
|
||||
if (settings.infillPercent) patch.infillDensity = settings.infillPercent;
|
||||
if (settings.infillPattern) patch.infillPattern = settings.infillPattern;
|
||||
if (settings.supportsEnabled !== undefined) patch.supportEnabled = settings.supportsEnabled;
|
||||
if (settings.notes) patch.notes = settings.notes;
|
||||
|
||||
this.form.patchValue(patch);
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
console.log('UploadFormComponent: onSubmit triggered');
|
||||
console.log('Form Valid:', this.form.valid, 'Items:', this.items().length);
|
||||
|
||||
@@ -306,4 +306,45 @@ export class QuoteEstimatorService {
|
||||
this.pendingConsultation.set(null); // Clear after reading
|
||||
return data;
|
||||
}
|
||||
|
||||
// Session File Retrieval
|
||||
getLineItemContent(sessionId: string, lineItemId: string): Observable<Blob> {
|
||||
const headers: any = {};
|
||||
// @ts-ignore
|
||||
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth);
|
||||
return this.http.get(`${environment.apiUrl}/api/quote-sessions/${sessionId}/line-items/${lineItemId}/content`, {
|
||||
headers,
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
mapSessionToQuoteResult(sessionData: any): QuoteResult {
|
||||
const session = sessionData.session;
|
||||
const items = sessionData.items || [];
|
||||
const totalTime = items.reduce((acc: number, item: any) => acc + (item.printTimeSeconds || 0) * item.quantity, 0);
|
||||
const totalWeight = items.reduce((acc: number, item: any) => acc + (item.materialGrams || 0) * item.quantity, 0);
|
||||
|
||||
return {
|
||||
sessionId: session.id,
|
||||
items: items.map((item: any) => ({
|
||||
id: item.id,
|
||||
fileName: item.originalFilename,
|
||||
unitPrice: item.unitPriceChf,
|
||||
unitTime: item.printTimeSeconds,
|
||||
unitWeight: item.materialGrams,
|
||||
quantity: item.quantity,
|
||||
material: session.materialCode, // Assumption: session has one material for all? or items have it?
|
||||
// Backend model QuoteSession has materialCode.
|
||||
// But line items might have different colors.
|
||||
color: item.colorCode
|
||||
})),
|
||||
setupCost: session.setupCostChf,
|
||||
currency: 'CHF', // Fixed for now
|
||||
totalPrice: sessionData.grandTotalChf,
|
||||
totalTimeHours: Math.floor(totalTime / 3600),
|
||||
totalTimeMinutes: Math.ceil((totalTime % 3600) / 60),
|
||||
totalWeight: Math.ceil(totalWeight),
|
||||
notes: session.notes
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user