389 lines
14 KiB
HTML
389 lines
14 KiB
HTML
<div class="checkout-page">
|
||
<div class="container ui-page-hero ui-page-hero--spacious checkout-hero">
|
||
<h1 class="ui-page-title">{{ "CHECKOUT.TITLE" | translate }}</h1>
|
||
<p class="ui-page-subtitle cad-subtitle" *ngIf="isCadSession()">
|
||
{{ "CHECKOUT.CAD_SERVICE" | translate }}
|
||
<ng-container *ngIf="cadRequestId()">
|
||
{{ "CHECKOUT.CAD_REQUEST_REF" | translate: { id: 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]="languageService.localizedPath('/terms')"
|
||
target="_blank"
|
||
rel="noopener"
|
||
>{{ "LEGAL.CONSENT.TERMS_LINK" | translate }}</a
|
||
>
|
||
{{ "LEGAL.CONSENT.AND" | translate }}
|
||
<a
|
||
[href]="languageService.localizedPath('/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 | translate }}
|
||
</span>
|
||
<span class="item-color" *ngIf="itemColorLabel(item) !== '-'">
|
||
<span
|
||
class="color-dot"
|
||
[style.background-color]="itemColorSwatch(item)"
|
||
></span>
|
||
<span class="color-name">{{
|
||
itemColorLabel(item) | translate
|
||
}}</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">{{
|
||
"CHECKOUT.CAD_SERVICE" | translate
|
||
}}</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>
|