Files
print-calculator/frontend/src/app/features/checkout/checkout.component.html
printcalc-ci 6f700c923a
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 9s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-backend (pull_request) Successful in 27s
PR Checks / test-frontend (pull_request) Successful in 58s
style: apply prettier formatting
2026-03-14 14:15:10 +00:00

389 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 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>