Files
print-calculator/frontend/src/app/features/checkout/checkout.component.html
Joe Küng a212a1d8cc
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 19s
PR Checks / security-sast (pull_request) Successful in 34s
PR Checks / test-backend (pull_request) Successful in 29s
PR Checks / test-frontend (pull_request) Successful in 59s
feat(back-end): shop ui implementation
2026-03-10 08:31:29 +01:00

380 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="checkout-page">
<div class="container ui-page-hero">
<h1 class="ui-page-title">{{ "CHECKOUT.TITLE" | translate }}</h1>
<p class="ui-page-subtitle cad-subtitle" *ngIf="isCadSession()">
Servizio CAD
<ng-container *ngIf="cadRequestId()">
riferito alla richiesta contatto #{{ cadRequestId() }}
</ng-container>
</p>
</div>
<div class="container">
<div class="checkout-layout ui-two-column-layout">
<!-- LEFT COLUMN: Form -->
<div class="checkout-form-section">
<!-- Error Message -->
<div *ngIf="error" class="error-message">
{{ error | translate }}
</div>
<form [formGroup]="checkoutForm" (ngSubmit)="onSubmit()" *ngIf="!error">
<!-- Contact Info Card -->
<app-card class="mb-6">
<div class="ui-card-header">
<h3 class="ui-card-title">
{{ "CHECKOUT.CONTACT_INFO" | translate }}
</h3>
</div>
<div class="ui-form-row">
<app-input
formControlName="email"
type="email"
[label]="'CHECKOUT.EMAIL' | translate"
[required]="true"
[error]="
checkoutForm.get('email')?.hasError('email')
? ('CHECKOUT.INVALID_EMAIL' | translate)
: null
"
></app-input>
<app-input
formControlName="phone"
type="tel"
[label]="'CHECKOUT.PHONE' | translate"
[required]="true"
></app-input>
</div>
</app-card>
<!-- Billing Address Card -->
<app-card class="mb-6">
<div class="ui-card-header">
<h3 class="ui-card-title">
{{ "CHECKOUT.BILLING_ADDR" | translate }}
</h3>
</div>
<div formGroupName="billingAddress">
<!-- User Type Selector -->
<app-toggle-selector
class="mb-4 user-type-selector-compact"
[options]="userTypeOptions"
[selectedValue]="checkoutForm.get('customerType')?.value"
(selectionChange)="setCustomerType($event)"
>
</app-toggle-selector>
<!-- Private Person Fields -->
<div *ngIf="!isCompany" class="ui-form-row">
<app-input
formControlName="firstName"
[label]="'CHECKOUT.FIRST_NAME' | translate"
[required]="true"
></app-input>
<app-input
formControlName="lastName"
[label]="'CHECKOUT.LAST_NAME' | translate"
[required]="true"
></app-input>
</div>
<!-- Company Fields -->
<div
*ngIf="isCompany"
class="ui-field-stack ui-field-stack--indented mb-4"
>
<app-input
formControlName="companyName"
[label]="'CHECKOUT.COMPANY_NAME' | translate"
[required]="true"
[placeholder]="'CONTACT.PLACEHOLDER_COMPANY' | translate"
></app-input>
<app-input
formControlName="referencePerson"
[label]="'CONTACT.REF_PERSON' | translate"
[required]="true"
[placeholder]="'CONTACT.PLACEHOLDER_REF_PERSON' | translate"
></app-input>
</div>
<app-input
formControlName="addressLine1"
[label]="'CHECKOUT.ADDRESS_1' | translate"
[required]="true"
></app-input>
<app-input
formControlName="addressLine2"
[label]="'CHECKOUT.ADDRESS_2' | translate"
></app-input>
<div class="ui-form-row ui-form-row--three">
<app-input
formControlName="zip"
[label]="'CHECKOUT.ZIP' | translate"
[required]="true"
></app-input>
<app-input
formControlName="city"
[label]="'CHECKOUT.CITY' | translate"
class="city-field"
[required]="true"
></app-input>
<app-input
formControlName="countryCode"
[label]="'CHECKOUT.COUNTRY' | translate"
[disabled]="true"
[required]="true"
></app-input>
</div>
</div>
</app-card>
<!-- Shipping Option -->
<div class="shipping-option ui-soft-panel">
<label class="ui-checkbox">
<input type="checkbox" formControlName="shippingSameAsBilling" />
<span class="ui-checkbox__mark"></span>
{{ "CHECKOUT.SHIPPING_SAME" | translate }}
</label>
</div>
<!-- Shipping Address Card (Conditional) -->
<app-card
*ngIf="!checkoutForm.get('shippingSameAsBilling')?.value"
class="mb-6"
>
<div class="ui-card-header">
<h3 class="ui-card-title">
{{ "CHECKOUT.SHIPPING_ADDR" | translate }}
</h3>
</div>
<div formGroupName="shippingAddress">
<div class="ui-form-row">
<app-input
formControlName="firstName"
[label]="'CHECKOUT.FIRST_NAME' | translate"
></app-input>
<app-input
formControlName="lastName"
[label]="'CHECKOUT.LAST_NAME' | translate"
></app-input>
</div>
<div
*ngIf="isCompany"
class="ui-field-stack ui-field-stack--indented"
>
<app-input
formControlName="companyName"
[label]="'CHECKOUT.COMPANY_OPTIONAL' | translate"
></app-input>
<app-input
formControlName="referencePerson"
[label]="'CHECKOUT.REF_PERSON_OPTIONAL' | translate"
></app-input>
</div>
<app-input
formControlName="addressLine1"
[label]="'CHECKOUT.ADDRESS_1' | translate"
></app-input>
<div class="ui-form-row ui-form-row--three">
<app-input
formControlName="zip"
[label]="'CHECKOUT.ZIP' | translate"
></app-input>
<app-input
formControlName="city"
[label]="'CHECKOUT.CITY' | translate"
class="city-field"
></app-input>
<app-input
formControlName="countryCode"
[label]="'CHECKOUT.COUNTRY' | translate"
[disabled]="true"
></app-input>
</div>
</div>
</app-card>
<div class="legal-consent">
<label class="ui-checkbox">
<input type="checkbox" formControlName="acceptLegal" />
<span class="ui-checkbox__mark"></span>
<span>
{{ "LEGAL.CONSENT.LABEL_PREFIX" | translate }}
<a href="/terms" target="_blank" rel="noopener">{{
"LEGAL.CONSENT.TERMS_LINK" | translate
}}</a>
{{ "LEGAL.CONSENT.AND" | translate }}
<a href="/privacy" target="_blank" rel="noopener">{{
"LEGAL.CONSENT.PRIVACY_LINK" | translate
}}</a
>.
</span>
</label>
<div
class="consent-error"
*ngIf="
checkoutForm.get('acceptLegal')?.invalid &&
checkoutForm.get('acceptLegal')?.touched
"
>
{{ "LEGAL.CONSENT.REQUIRED_ERROR" | translate }}
</div>
</div>
<div class="ui-actions">
<app-button
type="submit"
[disabled]="checkoutForm.invalid || isSubmitting()"
[fullWidth]="true"
>
{{
(isSubmitting()
? "CHECKOUT.PROCESSING"
: "CHECKOUT.PLACE_ORDER"
) | translate
}}
</app-button>
</div>
</form>
</div>
<!-- RIGHT COLUMN: Order Summary -->
<div class="checkout-summary-section">
<app-card class="sticky-card">
<div class="ui-card-header">
<h3 class="ui-card-title">
{{ "CHECKOUT.SUMMARY_TITLE" | translate }}
</h3>
</div>
<div class="summary-items" *ngIf="quoteSession() as session">
<div class="summary-item" *ngFor="let item of session.items">
<div class="item-details">
<span class="item-name">{{ itemDisplayName(item) }}</span>
<div class="item-specs">
<span
>{{ "CHECKOUT.QTY" | translate }}: {{ item.quantity }}</span
>
<span *ngIf="showItemMaterial(item)">
{{ "CHECKOUT.MATERIAL" | translate }}:
{{ itemMaterial(item) }}
</span>
<span *ngIf="itemVariantLabel(item) as variantLabel">
{{ "SHOP.VARIANT" | translate }}:
{{ variantLabel }}
</span>
<span class="item-color" *ngIf="itemColorLabel(item) !== '-'">
<span
class="color-dot"
[style.background-color]="itemColorSwatch(item)"
></span>
<span class="color-name">{{ itemColorLabel(item) }}</span>
</span>
</div>
<div class="item-specs-sub" *ngIf="showItemPrintMetrics(item)">
{{ item.printTimeSeconds / 3600 | number: "1.1-1" }}h |
{{ item.materialGrams | number: "1.0-0" }}g
</div>
<div class="item-preview" *ngIf="isStlItem(item)">
<ng-container
*ngIf="previewFile(item) as itemPreview; else previewState"
>
<button
type="button"
class="preview-trigger"
(click)="openPreview(item)"
[attr.aria-label]="'CHECKOUT.PREVIEW_OPEN' | translate"
>
<div class="preview-surface">
<app-stl-viewer
[file]="itemPreview"
[height]="116"
[color]="previewColor(item)"
[borderRadius]="'var(--radius-lg)'"
></app-stl-viewer>
<span class="preview-pill">{{
"CHECKOUT.PREVIEW_OPEN" | translate
}}</span>
</div>
</button>
</ng-container>
<ng-template #previewState>
<div class="preview-state" *ngIf="isPreviewLoading(item)">
{{ "CHECKOUT.PREVIEW_LOADING" | translate }}
</div>
<div
class="preview-state preview-state-error"
*ngIf="!isPreviewLoading(item) && hasPreviewError(item)"
>
{{ "CHECKOUT.PREVIEW_UNAVAILABLE" | translate }}
</div>
</ng-template>
</div>
</div>
<div class="item-price">
<span class="item-total-price">
{{ item.unitPriceChf * item.quantity | currency: "CHF" }}
</span>
<small class="item-unit-price" *ngIf="item.quantity > 1">
{{ item.unitPriceChf | currency: "CHF" }}
{{ "CHECKOUT.PER_PIECE" | translate }}
</small>
</div>
</div>
<div class="summary-item cad-summary-item" *ngIf="cadTotal() > 0">
<div class="item-details">
<span class="item-name">Servizio CAD</span>
<div class="item-specs-sub">{{ cadHours() }}h</div>
</div>
<div class="item-price">
<span class="item-total-price">
{{ cadTotal() | currency: "CHF" }}
</span>
</div>
</div>
</div>
<app-price-breakdown
*ngIf="quoteSession() as session"
[rows]="checkoutPriceBreakdownRows(session)"
[total]="session.grandTotalChf || 0"
[currency]="'CHF'"
[totalLabelKey]="'CHECKOUT.TOTAL'"
></app-price-breakdown>
</app-card>
</div>
</div>
</div>
</div>
<div
class="preview-modal-backdrop"
*ngIf="previewModalOpen()"
(click)="closePreview()"
>
<div class="preview-modal" (click)="$event.stopPropagation()">
<div class="preview-modal-header">
<h4>{{ selectedPreviewName() }}</h4>
<button
type="button"
class="preview-modal-close"
(click)="closePreview()"
[attr.aria-label]="'CHECKOUT.PREVIEW_CLOSE' | translate"
>
×
</button>
</div>
<app-stl-viewer
*ngIf="selectedPreviewFile() as preview"
[file]="preview"
[height]="460"
[color]="selectedPreviewColor()"
[borderRadius]="'var(--radius-lg)'"
></app-stl-viewer>
</div>
</div>