import { Component, OnInit, PLATFORM_ID, inject, signal } from '@angular/core'; import { CommonModule, isPlatformBrowser } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { environment } from '../../../environments/environment'; import { findColorHex, resolveLocalizedColorLabel, } from '../../core/constants/colors.const'; import { PriceBreakdownComponent, PriceBreakdownRow, } from '../../shared/components/price-breakdown/price-breakdown.component'; interface PublicOrderItem { id: string; itemType?: string; originalFilename?: string; displayName?: string; materialCode?: string; colorCode?: string; filamentVariantId?: number; shopProductId?: string; shopProductVariantId?: string; shopProductSlug?: string; shopProductName?: string; shopVariantLabel?: string; shopVariantColorName?: string; shopVariantColorLabelIt?: string; shopVariantColorLabelEn?: string; shopVariantColorLabelDe?: string; shopVariantColorLabelFr?: string; shopVariantColorHex?: string; filamentVariantDisplayName?: string; filamentColorName?: string; filamentColorLabelIt?: string; filamentColorLabelEn?: string; filamentColorLabelDe?: string; filamentColorLabelFr?: string; filamentColorHex?: string; quality?: string; nozzleDiameterMm?: number; layerHeightMm?: number; infillPercent?: number; infillPattern?: string; supportsEnabled?: boolean; quantity: number; printTimeSeconds?: number; materialGrams?: number; unitPriceChf?: number; lineTotalChf?: number; } interface PublicOrder { id: string; orderNumber?: string; status?: string; paymentStatus?: string; paymentMethod?: string; subtotalChf?: number; shippingCostChf?: number; setupCostChf?: number; totalChf?: number; cadHours?: number; cadTotalChf?: number; items?: PublicOrderItem[]; } @Component({ selector: 'app-order', standalone: true, imports: [ CommonModule, AppButtonComponent, AppCardComponent, TranslateModule, PriceBreakdownComponent, ], templateUrl: './order.component.html', styleUrl: './order.component.scss', }) export class OrderComponent implements OnInit { private route = inject(ActivatedRoute); private router = inject(Router); private quoteService = inject(QuoteEstimatorService); private translate = inject(TranslateService); private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); orderId: string | null = null; selectedPaymentMethod: 'twint' | 'bill' | null = 'twint'; order = signal(null); loading = signal(true); error = signal(null); twintOpenUrl = signal(null); twintQrUrl = signal(null); ngOnInit(): void { this.orderId = this.route.snapshot.paramMap.get('orderId'); if (this.orderId) { this.loadOrder(); this.loadTwintPayment(); } else { this.error.set('ORDER.ERR_ID_NOT_FOUND'); this.loading.set(false); } } loadOrder() { if (!this.orderId) return; this.quoteService.getOrder(this.orderId).subscribe({ next: (order) => { this.order.set(order); this.loading.set(false); }, error: (err) => { console.error('Failed to load order', err); this.error.set('ORDER.ERR_LOAD_ORDER'); this.loading.set(false); }, }); } selectPayment(method: 'twint' | 'bill'): void { this.selectedPaymentMethod = method; } downloadQrInvoice() { if (!this.isBrowser) { return; } const orderId = this.orderId; if (!orderId) return; this.quoteService.getOrderConfirmation(orderId).subscribe({ next: (blob) => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const fallbackOrderNumber = this.extractOrderNumber(orderId); const orderNumber = this.order()?.orderNumber ?? fallbackOrderNumber; a.download = `qr-invoice-${orderNumber}.pdf`; a.click(); window.URL.revokeObjectURL(url); }, error: (err) => console.error('Failed to download QR invoice', err), }); } loadTwintPayment() { if (!this.orderId) return; this.quoteService.getTwintPayment(this.orderId).subscribe({ next: (res) => { const qrPath = typeof res.qrImageUrl === 'string' ? `${res.qrImageUrl}?size=360` : null; const qrDataUri = typeof res.qrImageDataUri === 'string' ? res.qrImageDataUri : null; this.twintOpenUrl.set(this.resolveApiUrl(res.openUrl)); this.twintQrUrl.set(qrDataUri ?? this.resolveApiUrl(qrPath)); }, error: (err) => { console.error('Failed to load TWINT payment details', err); }, }); } openTwintPayment(): void { const openUrl = this.twintOpenUrl(); if (this.isBrowser && openUrl) { window.open(openUrl, '_blank'); } } getTwintQrUrl(): string { return this.twintQrUrl() ?? ''; } getTwintButtonImageUrl(): string { const lang = this.translate.currentLang; if (lang === 'de') { return 'https://go.twint.ch/static/img/button_dark_de.svg'; } if (lang === 'it') { return 'https://go.twint.ch/static/img/button_dark_it.svg'; } if (lang === 'fr') { return 'https://go.twint.ch/static/img/button_dark_fr.svg'; } // Default to EN for everything else (it, fr, en) as instructed or if not DE return 'https://go.twint.ch/static/img/button_dark_en.svg'; } onTwintQrError(): void { this.twintQrUrl.set(null); } private resolveApiUrl(urlOrPath: string | null | undefined): string | null { if (!urlOrPath) return null; if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://')) { return urlOrPath; } const base = (environment.apiUrl || '').replace(/\/$/, ''); const path = urlOrPath.startsWith('/') ? urlOrPath : `/${urlOrPath}`; return `${base}${path}`; } completeOrder(): void { if (!this.orderId || !this.selectedPaymentMethod) { return; } this.quoteService .reportPayment(this.orderId, this.selectedPaymentMethod) .subscribe({ next: (order) => { this.order.set(order); // The UI will re-render and show the 'REPORTED' state. // We stay on this page to let the user see the "In verifica" // status along with payment instructions. }, error: (err) => { console.error('Failed to report payment', err); this.error.set('ORDER.ERR_REPORT_PAYMENT'); }, }); } getDisplayOrderNumber(order: any): string { if (order?.orderNumber) { return order.orderNumber; } if (order?.id) { return this.extractOrderNumber(order.id); } return this.translate.instant('ORDER.NOT_AVAILABLE'); } orderPriceBreakdownRows(order: any): PriceBreakdownRow[] { return [ { labelKey: 'PAYMENT.SUBTOTAL', amount: order?.subtotalChf ?? 0, }, { label: this.translate.instant('ORDER.CAD_SERVICE', { hours: order?.cadHours || 0, }), amount: order?.cadTotalChf ?? 0, visible: (order?.cadTotalChf ?? 0) > 0, }, { labelKey: 'PAYMENT.SHIPPING', amount: order?.shippingCostChf ?? 0, }, { labelKey: 'PAYMENT.SETUP_FEE', amount: order?.setupCostChf ?? 0, }, ]; } private extractOrderNumber(orderId: string): string { return orderId.split('-')[0]; } isShopItem(item: PublicOrderItem): boolean { return String(item?.itemType ?? '').toUpperCase() === 'SHOP_PRODUCT'; } itemDisplayName(item: PublicOrderItem): string { const displayName = String(item?.displayName ?? '').trim(); if (displayName) { return displayName; } const shopName = String(item?.shopProductName ?? '').trim(); if (shopName) { return shopName; } return String( item?.originalFilename ?? this.translate.instant('ORDER.NOT_AVAILABLE'), ); } itemVariantLabel(item: PublicOrderItem): string | null { const variantLabel = String(item?.shopVariantLabel ?? '').trim(); if (variantLabel) { return variantLabel; } return this.localizedColorLabel(item, 'shop'); } itemColorLabel(item: PublicOrderItem): string { return ( this.localizedColorLabel(item, 'shop') || this.localizedColorLabel(item, 'filament') || String(item?.colorCode ?? '').trim() || this.translate.instant('ORDER.NOT_AVAILABLE') ); } itemColorHex(item: PublicOrderItem): string | null { const shopHex = String(item?.shopVariantColorHex ?? '').trim(); if (this.isHexColor(shopHex)) { return shopHex; } const filamentHex = String(item?.filamentColorHex ?? '').trim(); if (this.isHexColor(filamentHex)) { return filamentHex; } const rawColor = String(item?.colorCode ?? '').trim(); if (this.isHexColor(rawColor)) { return rawColor; } return findColorHex(rawColor); } showItemMaterial(item: PublicOrderItem): boolean { return !this.isShopItem(item); } showItemPrintMetrics(item: PublicOrderItem): boolean { return !this.isShopItem(item); } private localizedColorLabel( item: PublicOrderItem, source: 'shop' | 'filament', ): string | null { if (source === 'shop') { return resolveLocalizedColorLabel(this.translate.currentLang, { fallback: item.shopVariantColorName, it: item.shopVariantColorLabelIt, en: item.shopVariantColorLabelEn, de: item.shopVariantColorLabelDe, fr: item.shopVariantColorLabelFr, }); } return resolveLocalizedColorLabel(this.translate.currentLang, { fallback: item.filamentColorName ?? item.colorCode, it: item.filamentColorLabelIt, en: item.filamentColorLabelEn, de: item.filamentColorLabelDe, fr: item.filamentColorLabelFr, }); } orderKind(order: PublicOrder | null): 'SHOP' | 'CALCULATOR' | 'MIXED' { const items = order?.items ?? []; const hasShop = items.some((item) => this.isShopItem(item)); const hasPrint = items.some((item) => !this.isShopItem(item)); if (hasShop && hasPrint) { return 'MIXED'; } if (hasShop) { return 'SHOP'; } return 'CALCULATOR'; } orderKindLabel(order: PublicOrder | null): string { switch (this.orderKind(order)) { case 'SHOP': return this.translate.instant('ORDER.TYPE_SHOP'); case 'MIXED': return this.translate.instant('ORDER.TYPE_MIXED'); default: return this.translate.instant('ORDER.TYPE_CALCULATOR'); } } private isHexColor(value?: string): boolean { return ( typeof value === 'string' && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value) ); } }