fix(back-end): update profile inheritance
This commit is contained in:
@@ -1,37 +1,74 @@
|
|||||||
import { Component, input, output } from '@angular/core';
|
import { Component, input, output, signal, computed, effect } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { AppCardComponent } from '../../../../shared/components/app-card/app-card.component';
|
import { AppCardComponent } from '../../../../shared/components/app-card/app-card.component';
|
||||||
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
|
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
|
||||||
import { SummaryCardComponent } from '../../../../shared/components/summary-card/summary-card.component';
|
import { SummaryCardComponent } from '../../../../shared/components/summary-card/summary-card.component';
|
||||||
import { QuoteResult } from '../../services/quote-estimator.service';
|
import { QuoteResult, QuoteItem } from '../../services/quote-estimator.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-quote-result',
|
selector: 'app-quote-result',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, TranslateModule, AppCardComponent, AppButtonComponent, SummaryCardComponent],
|
imports: [CommonModule, FormsModule, TranslateModule, AppCardComponent, AppButtonComponent, SummaryCardComponent],
|
||||||
template: `
|
template: `
|
||||||
<app-card>
|
<app-card>
|
||||||
<h3 class="title">{{ 'CALC.RESULT' | translate }}</h3>
|
<h3 class="title">{{ 'CALC.RESULT' | translate }}</h3>
|
||||||
|
|
||||||
|
<!-- Detailed Items List -->
|
||||||
|
<div class="items-list">
|
||||||
|
@for (item of items(); track item.fileName; let i = $index) {
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-info">
|
||||||
|
<span class="file-name">{{ item.fileName }}</span>
|
||||||
|
<span class="file-details">
|
||||||
|
{{ (item.unitTime / 3600) | number:'1.1-1' }}h | {{ item.unitWeight | number:'1.0-0' }}g
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-controls">
|
||||||
|
<div class="qty-control">
|
||||||
|
<label>Qtà:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
[ngModel]="item.quantity"
|
||||||
|
(ngModelChange)="updateQuantity(i, $event)"
|
||||||
|
class="qty-input">
|
||||||
|
</div>
|
||||||
|
<div class="item-price">
|
||||||
|
{{ (item.unitPrice * item.quantity) | currency:result().currency }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- Summary Grid -->
|
||||||
<div class="result-grid">
|
<div class="result-grid">
|
||||||
<app-summary-card
|
<app-summary-card
|
||||||
class="item full-width"
|
class="item full-width"
|
||||||
[label]="'CALC.COST' | translate"
|
[label]="'CALC.COST' | translate"
|
||||||
[large]="true"
|
[large]="true"
|
||||||
[highlight]="true">
|
[highlight]="true">
|
||||||
{{ result().price | currency:result().currency }}
|
{{ totals().price | currency:result().currency }}
|
||||||
</app-summary-card>
|
</app-summary-card>
|
||||||
|
|
||||||
<app-summary-card [label]="'CALC.TIME' | translate">
|
<app-summary-card [label]="'CALC.TIME' | translate">
|
||||||
{{ result().printTimeHours }}h {{ result().printTimeMinutes }}m
|
{{ totals().hours }}h {{ totals().minutes }}m
|
||||||
</app-summary-card>
|
</app-summary-card>
|
||||||
|
|
||||||
<app-summary-card [label]="'CALC.MATERIAL' | translate">
|
<app-summary-card [label]="'CALC.MATERIAL' | translate">
|
||||||
{{ result().materialUsageGrams }}g
|
{{ totals().weight }}g
|
||||||
</app-summary-card>
|
</app-summary-card>
|
||||||
</div>
|
</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>
|
||||||
@@ -40,18 +77,133 @@ import { QuoteResult } from '../../services/quote-estimator.service';
|
|||||||
`,
|
`,
|
||||||
styles: [`
|
styles: [`
|
||||||
.title { margin-bottom: var(--space-6); text-align: center; }
|
.title { margin-bottom: var(--space-6); text-align: center; }
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--color-border);
|
||||||
|
margin: var(--space-4) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--color-neutral-50);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name { font-weight: 500; font-size: 0.9rem; color: var(--color-text); }
|
||||||
|
.file-details { font-size: 0.8rem; color: var(--color-text-muted); }
|
||||||
|
|
||||||
|
.item-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
|
||||||
|
label { font-size: 0.8rem; color: var(--color-text-muted); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-input {
|
||||||
|
width: 60px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
text-align: center;
|
||||||
|
&:focus { outline: none; border-color: var(--color-brand); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-price {
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.result-grid {
|
.result-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
margin-bottom: var(--space-6);
|
margin-bottom: var(--space-2);
|
||||||
}
|
}
|
||||||
.full-width { grid-column: span 2; }
|
.full-width { grid-column: span 2; }
|
||||||
|
|
||||||
|
.setup-note {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.actions { display: flex; flex-direction: column; gap: var(--space-3); }
|
.actions { display: flex; flex-direction: column; gap: var(--space-3); }
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class QuoteResultComponent {
|
export class QuoteResultComponent {
|
||||||
result = input.required<QuoteResult>();
|
result = input.required<QuoteResult>();
|
||||||
consult = output<void>();
|
consult = output<void>();
|
||||||
|
|
||||||
|
// Local mutable state for items to handle quantity changes
|
||||||
|
items = signal<QuoteItem[]>([]);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
// Initialize local items when result inputs change
|
||||||
|
// We map to new objects to avoid mutating the input directly if it was a reference
|
||||||
|
this.items.set(this.result().items.map(i => ({...i})));
|
||||||
|
}, { allowSignalWrites: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateQuantity(index: number, newQty: number | string) {
|
||||||
|
const qty = typeof newQty === 'string' ? parseInt(newQty, 10) : newQty;
|
||||||
|
if (qty < 1 || isNaN(qty)) return;
|
||||||
|
|
||||||
|
this.items.update(current => {
|
||||||
|
const updated = [...current];
|
||||||
|
updated[index] = { ...updated[index], quantity: qty };
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
totals = computed(() => {
|
||||||
|
const currentItems = this.items();
|
||||||
|
const setup = this.result().setupCost;
|
||||||
|
|
||||||
|
let price = setup;
|
||||||
|
let time = 0;
|
||||||
|
let weight = 0;
|
||||||
|
|
||||||
|
currentItems.forEach(i => {
|
||||||
|
price += i.unitPrice * i.quantity;
|
||||||
|
time += i.unitTime * i.quantity;
|
||||||
|
weight += i.unitWeight * i.quantity;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hours = Math.floor(time / 3600);
|
||||||
|
const minutes = Math.ceil((time % 3600) / 60);
|
||||||
|
|
||||||
|
return {
|
||||||
|
price: Math.round(price * 100) / 100,
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
weight: Math.ceil(weight)
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import { map, catchError } from 'rxjs/operators';
|
|||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
|
|
||||||
export interface QuoteRequest {
|
export interface QuoteRequest {
|
||||||
files: File[];
|
items: { file: File, quantity: number }[];
|
||||||
material: string;
|
material: string;
|
||||||
quality: string;
|
quality: string;
|
||||||
quantity: number;
|
|
||||||
notes?: string;
|
notes?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
infillDensity?: number;
|
infillDensity?: number;
|
||||||
@@ -17,13 +16,24 @@ export interface QuoteRequest {
|
|||||||
mode: 'easy' | 'advanced';
|
mode: 'easy' | 'advanced';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface QuoteItem {
|
||||||
|
fileName: string;
|
||||||
|
unitPrice: number;
|
||||||
|
unitTime: number; // seconds
|
||||||
|
unitWeight: number; // grams
|
||||||
|
quantity: number;
|
||||||
|
// Computed values for UI convenience (optional, can be done in component)
|
||||||
|
}
|
||||||
|
|
||||||
export interface QuoteResult {
|
export interface QuoteResult {
|
||||||
price: number;
|
items: QuoteItem[];
|
||||||
currency: string;
|
|
||||||
printTimeHours: number;
|
|
||||||
printTimeMinutes: number;
|
|
||||||
materialUsageGrams: number;
|
|
||||||
setupCost: number;
|
setupCost: number;
|
||||||
|
currency: string;
|
||||||
|
// The following are aggregations that can be re-calculated
|
||||||
|
totalPrice: number;
|
||||||
|
totalTimeHours: number;
|
||||||
|
totalTimeMinutes: number;
|
||||||
|
totalWeight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BackendResponse {
|
interface BackendResponse {
|
||||||
@@ -45,80 +55,17 @@ export class QuoteEstimatorService {
|
|||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
calculate(request: QuoteRequest): Observable<number | QuoteResult> {
|
calculate(request: QuoteRequest): Observable<number | QuoteResult> {
|
||||||
const formData = new FormData();
|
if (request.items.length === 0) return of();
|
||||||
// Assuming single file primarily for now, or aggregating.
|
|
||||||
// The current UI seems to select one "active" file or handle multiple.
|
|
||||||
// The logic below was mapping multiple files to multiple requests.
|
|
||||||
// To support progress seamlessly for the "main" action, let's focus on the processing flow.
|
|
||||||
// If multiple files, we might need a more complex progress tracking or just track the first/total.
|
|
||||||
// Given the UI shows one big "Analyse" button, let's treat it as a batch or single.
|
|
||||||
|
|
||||||
// NOTE: The previous logic did `request.files.map(...)`.
|
|
||||||
// If we want a global progress, we can mistakenly complexity it.
|
|
||||||
// Let's assume we upload all files in one request if the API supported it, but the API seems to be 1 file per request from previous code?
|
|
||||||
// "formData.append('file', file)" inside the map implies multiple requests.
|
|
||||||
// To keep it simple and working with the progress bar which is global:
|
|
||||||
// We will emit progress for the *current* file being processed or average them.
|
|
||||||
// OR simpler: The user typically uploads one file for a quote?
|
|
||||||
// The UI `files: File[]` allows multiple.
|
|
||||||
// Let's stick to the previous logic but wrap it to emit progress.
|
|
||||||
// However, forkJoin waits for all. We can't easily get specialized progress for "overall upload" with forkJoin of distinct requests easily without merging.
|
|
||||||
|
|
||||||
// Refined approach:
|
|
||||||
// We will process files IN PARALLEL (forkJoin) but we can't easily track aggregated upload progress of multiple requests in a single simple number without extra code.
|
|
||||||
// BUT, the user wants "la barra di upload".
|
|
||||||
// If we assume standard use case is 1 file, it's easy.
|
|
||||||
// If multiple, we can emit progress as "average of all uploads" or just "uploading...".
|
|
||||||
// Let's modify the signature to return `Observable<{ type: 'progress' | 'result', value: any }>` or similar?
|
|
||||||
// The plan said `Observable<QuoteResult>` originally, now we need progress.
|
|
||||||
// Let's change return type to `Observable<any>` or a specific union.
|
|
||||||
|
|
||||||
// Let's handle just the first file for progress visualization simplicity if multiple are present,
|
|
||||||
// or better, create a wrapper that merges the progress.
|
|
||||||
|
|
||||||
// Actually, looking at the previous code: `const requests = request.files.map(...)`.
|
|
||||||
// If we have 3 files, we have 3 requests.
|
|
||||||
// We can emit progress events.
|
|
||||||
|
|
||||||
// START implementation for generalized progress:
|
|
||||||
|
|
||||||
const file = request.files[0]; // Primary target for now to ensure we have a progress to show.
|
|
||||||
// Ideally we should upload all.
|
|
||||||
|
|
||||||
// For this task, to satisfy "bar disappears after upload", we really need to know when upload finishes.
|
|
||||||
|
|
||||||
// Let's keep it robust:
|
|
||||||
// If multiple files, we likely want to just process them.
|
|
||||||
// Let's stick to the previous logic but capture progress events for at least one or all.
|
|
||||||
|
|
||||||
if (request.files.length === 0) return of();
|
|
||||||
|
|
||||||
// We will change the architecture slightly:
|
|
||||||
// We will execute requests and for EACH, we track progress.
|
|
||||||
// But we only have one boolean 'loading' and one 'progress' bar in UI.
|
|
||||||
// Let's average the progress?
|
|
||||||
|
|
||||||
// Simplification: The user probably uploads one file to check quote.
|
|
||||||
// Let's implement support for the first file's progress to drive the UI bar, handling the rest in background/parallel.
|
|
||||||
|
|
||||||
// Re-implementing the single file logic from the map, but enabled for progress.
|
|
||||||
|
|
||||||
return new Observable(observer => {
|
return new Observable(observer => {
|
||||||
let completed = 0;
|
const totalItems = request.items.length;
|
||||||
let total = request.files.length;
|
const allProgress: number[] = new Array(totalItems).fill(0);
|
||||||
const results: BackendResponse[] = [];
|
const finalResponses: any[] = [];
|
||||||
let grandTotal = 0; // For progress calculation if we wanted to average
|
let completedRequests = 0;
|
||||||
|
|
||||||
// We'll just track the "upload phase" of the bundle.
|
const uploads = request.items.map((item, index) => {
|
||||||
// Actually, let's just use `concat` or `merge`?
|
|
||||||
// Let's simplify: We will only track progress for the first file or "active" file.
|
|
||||||
// But the previous code sent ALL files.
|
|
||||||
|
|
||||||
// Let's change the return type to emit events.
|
|
||||||
|
|
||||||
const uploads = request.files.map(file => {
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', item.file);
|
||||||
formData.append('machine', 'bambu_a1');
|
formData.append('machine', 'bambu_a1');
|
||||||
formData.append('filament', this.mapMaterial(request.material));
|
formData.append('filament', this.mapMaterial(request.material));
|
||||||
formData.append('quality', this.mapQuality(request.quality));
|
formData.append('quality', this.mapQuality(request.quality));
|
||||||
@@ -138,27 +85,19 @@ export class QuoteEstimatorService {
|
|||||||
reportProgress: true,
|
reportProgress: true,
|
||||||
observe: 'events'
|
observe: 'events'
|
||||||
}).pipe(
|
}).pipe(
|
||||||
map(event => ({ file, event })),
|
map(event => ({ item, event, index })),
|
||||||
catchError(err => of({ file, error: err }))
|
catchError(err => of({ item, error: err, index }))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// We process all uploads.
|
|
||||||
// We want to emit:
|
|
||||||
// 1. Progress updates (average of all files?)
|
|
||||||
// 2. Final QuoteResult
|
|
||||||
|
|
||||||
const allProgress: number[] = new Array(request.files.length).fill(0);
|
|
||||||
let completedRequests = 0;
|
|
||||||
const finalResponses: any[] = [];
|
|
||||||
|
|
||||||
// Subscribe to all
|
// Subscribe to all
|
||||||
uploads.forEach((obs, index) => {
|
uploads.forEach((obs) => {
|
||||||
obs.subscribe({
|
obs.subscribe({
|
||||||
next: (wrapper: any) => {
|
next: (wrapper: any) => {
|
||||||
|
const idx = wrapper.index;
|
||||||
|
|
||||||
if (wrapper.error) {
|
if (wrapper.error) {
|
||||||
// handled in final calculation
|
finalResponses[idx] = { success: false, fileName: wrapper.item.file.name };
|
||||||
finalResponses[index] = { success: false, data: { cost: { total:0 }, print_time_seconds:0, material_grams:0 } };
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,64 +105,75 @@ export class QuoteEstimatorService {
|
|||||||
if (event.type === 1) { // HttpEventType.UploadProgress
|
if (event.type === 1) { // HttpEventType.UploadProgress
|
||||||
if (event.total) {
|
if (event.total) {
|
||||||
const percent = Math.round((100 * event.loaded) / event.total);
|
const percent = Math.round((100 * event.loaded) / event.total);
|
||||||
allProgress[index] = percent;
|
allProgress[idx] = percent;
|
||||||
// Emit average progress
|
// Emit average progress
|
||||||
const avg = Math.round(allProgress.reduce((a, b) => a + b, 0) / total);
|
const avg = Math.round(allProgress.reduce((a, b) => a + b, 0) / totalItems);
|
||||||
observer.next(avg); // Emit number for progress
|
observer.next(avg);
|
||||||
}
|
}
|
||||||
} else if (event.type === 4) { // HttpEventType.Response
|
} else if (event.type === 4) { // HttpEventType.Response
|
||||||
allProgress[index] = 100;
|
allProgress[idx] = 100;
|
||||||
finalResponses[index] = event.body;
|
finalResponses[idx] = { ...event.body, fileName: wrapper.item.file.name, originalQty: wrapper.item.quantity };
|
||||||
completedRequests++;
|
completedRequests++;
|
||||||
|
|
||||||
if (completedRequests === total) {
|
if (completedRequests === totalItems) {
|
||||||
// All done
|
// All done
|
||||||
observer.next(100); // Ensure complete
|
observer.next(100);
|
||||||
|
|
||||||
// Calculate Totals
|
// Calculate Results
|
||||||
const valid = finalResponses.filter(r => r && r.success);
|
const setupCost = 10;
|
||||||
if (valid.length === 0 && finalResponses.length > 0) {
|
|
||||||
|
const items: QuoteItem[] = [];
|
||||||
|
|
||||||
|
finalResponses.forEach(res => {
|
||||||
|
if (res && res.success) {
|
||||||
|
items.push({
|
||||||
|
fileName: res.fileName,
|
||||||
|
unitPrice: res.data.cost.total,
|
||||||
|
unitTime: res.data.print_time_seconds,
|
||||||
|
unitWeight: res.data.material_grams,
|
||||||
|
quantity: res.originalQty // Use the requested quantity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
observer.error('All calculations failed.');
|
observer.error('All calculations failed.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalPrice = 0;
|
// Initial Aggregation
|
||||||
|
let grandTotal = setupCost;
|
||||||
let totalTime = 0;
|
let totalTime = 0;
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
let setupCost = 10;
|
|
||||||
|
|
||||||
valid.forEach(res => {
|
items.forEach(item => {
|
||||||
totalPrice += res.data.cost.total;
|
grandTotal += item.unitPrice * item.quantity;
|
||||||
totalTime += res.data.print_time_seconds;
|
totalTime += item.unitTime * item.quantity;
|
||||||
totalWeight += res.data.material_grams;
|
totalWeight += item.unitWeight * item.quantity;
|
||||||
});
|
});
|
||||||
|
|
||||||
totalPrice = (totalPrice * request.quantity) + setupCost;
|
|
||||||
totalWeight = totalWeight * request.quantity;
|
|
||||||
totalTime = totalTime * request.quantity;
|
|
||||||
|
|
||||||
const totalHours = Math.floor(totalTime / 3600);
|
const totalHours = Math.floor(totalTime / 3600);
|
||||||
const totalMinutes = Math.ceil((totalTime % 3600) / 60);
|
const totalMinutes = Math.ceil((totalTime % 3600) / 60);
|
||||||
|
|
||||||
const result: QuoteResult = {
|
const result: QuoteResult = {
|
||||||
price: Math.round(totalPrice * 100) / 100,
|
items,
|
||||||
|
setupCost,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
printTimeHours: totalHours,
|
totalPrice: Math.round(grandTotal * 100) / 100,
|
||||||
printTimeMinutes: totalMinutes,
|
totalTimeHours: totalHours,
|
||||||
materialUsageGrams: Math.ceil(totalWeight),
|
totalTimeMinutes: totalMinutes,
|
||||||
setupCost
|
totalWeight: Math.ceil(totalWeight)
|
||||||
};
|
};
|
||||||
|
|
||||||
observer.next(result); // Emit final object
|
observer.next(result);
|
||||||
observer.complete();
|
observer.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('Error in request', err);
|
console.error('Error in request', err);
|
||||||
finalResponses[index] = { success: false };
|
|
||||||
completedRequests++;
|
completedRequests++;
|
||||||
if (completedRequests === total) {
|
if (completedRequests === totalItems) {
|
||||||
observer.error('Requests failed');
|
observer.error('Requests failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user