feat(back-end): bill and qr
This commit is contained in:
@@ -144,6 +144,23 @@ export class QuoteEstimatorService {
|
||||
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth);
|
||||
return this.http.post(`${environment.apiUrl}/api/orders/from-quote/${sessionId}`, orderDetails, { headers });
|
||||
}
|
||||
|
||||
getOrder(orderId: string): Observable<any> {
|
||||
const headers: any = {};
|
||||
// @ts-ignore
|
||||
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth);
|
||||
return this.http.get(`${environment.apiUrl}/api/orders/${orderId}`, { headers });
|
||||
}
|
||||
|
||||
getOrderInvoice(orderId: string): Observable<Blob> {
|
||||
const headers: any = {};
|
||||
// @ts-ignore
|
||||
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth);
|
||||
return this.http.get(`${environment.apiUrl}/api/orders/${orderId}/invoice`, {
|
||||
headers,
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
calculate(request: QuoteRequest): Observable<number | QuoteResult> {
|
||||
console.log('QuoteEstimatorService: Calculating quote...', request);
|
||||
|
||||
@@ -1,120 +1,122 @@
|
||||
<div class="checkout-page">
|
||||
<h2 class="section-title">{{ 'CHECKOUT.TITLE' | translate }}</h2>
|
||||
<div class="container hero">
|
||||
<h1 class="section-title">{{ 'CHECKOUT.TITLE' | translate }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="checkout-layout">
|
||||
|
||||
<!-- LEFT COLUMN: Form -->
|
||||
<div class="checkout-form-section">
|
||||
<!-- Error Message -->
|
||||
<div *ngIf="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="checkout-layout">
|
||||
|
||||
<!-- LEFT COLUMN: Form -->
|
||||
<div class="checkout-form-section">
|
||||
<!-- Error Message -->
|
||||
<div *ngIf="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<form [formGroup]="checkoutForm" (ngSubmit)="onSubmit()" *ngIf="!error">
|
||||
|
||||
<!-- Contact Info Card -->
|
||||
<div class="form-card">
|
||||
<div class="card-header">
|
||||
<h3>{{ 'CHECKOUT.CONTACT_INFO' | translate }}</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<form [formGroup]="checkoutForm" (ngSubmit)="onSubmit()" *ngIf="!error">
|
||||
|
||||
<!-- Contact Info Card -->
|
||||
<app-card class="mb-6">
|
||||
<div class="card-header-simple">
|
||||
<h3>{{ 'CHECKOUT.CONTACT_INFO' | translate }}</h3>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<app-input formControlName="email" type="email" [label]="'CHECKOUT.EMAIL' | translate" [required]="true" [error]="checkoutForm.get('email')?.hasError('email') ? 'Invalid email' : null"></app-input>
|
||||
<app-input formControlName="phone" type="tel" [label]="'CHECKOUT.PHONE' | translate" [required]="true"></app-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-card>
|
||||
|
||||
<!-- Billing Address Card -->
|
||||
<div class="form-card">
|
||||
<div class="card-header">
|
||||
<h3>{{ 'CHECKOUT.BILLING_ADDR' | translate }}</h3>
|
||||
</div>
|
||||
<div class="card-content" formGroupName="billingAddress">
|
||||
<div class="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>
|
||||
<!-- Billing Address Card -->
|
||||
<app-card class="mb-6">
|
||||
<div class="card-header-simple">
|
||||
<h3>{{ 'CHECKOUT.BILLING_ADDR' | translate }}</h3>
|
||||
</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="form-row three-cols">
|
||||
<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>
|
||||
|
||||
<!-- User Type Selector -->
|
||||
<div class="user-type-selector">
|
||||
<div class="type-option" [class.selected]="!isCompany" (click)="setCustomerType(false)">
|
||||
{{ 'CONTACT.TYPE_PRIVATE' | translate }}
|
||||
<div formGroupName="billingAddress">
|
||||
|
||||
<!-- Private Person Fields -->
|
||||
<div *ngIf="!isCompany" class="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>
|
||||
<div class="type-option" [class.selected]="isCompany" (click)="setCustomerType(true)">
|
||||
{{ 'CONTACT.TYPE_COMPANY' | translate }}
|
||||
|
||||
<!-- Company Fields -->
|
||||
<div *ngIf="isCompany" class="company-fields mb-4">
|
||||
<app-input formControlName="companyName" [label]="('CONTACT.COMPANY_NAME' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_COMPANY' | translate"></app-input>
|
||||
<app-input formControlName="referencePerson" [label]="('CONTACT.REF_PERSON' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_REF_PERSON' | translate"></app-input>
|
||||
</div>
|
||||
|
||||
<!-- User Type Selector -->
|
||||
<div class="user-type-selector">
|
||||
<div class="type-option" [class.selected]="!isCompany" (click)="setCustomerType(false)">
|
||||
{{ 'CONTACT.TYPE_PRIVATE' | translate }}
|
||||
</div>
|
||||
<div class="type-option" [class.selected]="isCompany" (click)="setCustomerType(true)">
|
||||
{{ 'CONTACT.TYPE_COMPANY' | translate }}
|
||||
</div>
|
||||
</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="form-row three-cols">
|
||||
<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>
|
||||
|
||||
<!-- Company Fields (Indented with left border) -->
|
||||
<div *ngIf="isCompany" class="company-fields">
|
||||
<app-input formControlName="companyName" [label]="('CONTACT.COMPANY_NAME' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_COMPANY' | translate"></app-input>
|
||||
<app-input formControlName="referencePerson" [label]="('CONTACT.REF_PERSON' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_REF_PERSON' | translate"></app-input>
|
||||
</div>
|
||||
<!-- Shipping Option -->
|
||||
<div class="shipping-option">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" formControlName="shippingSameAsBilling">
|
||||
<span class="checkmark"></span>
|
||||
{{ 'CHECKOUT.SHIPPING_SAME' | translate }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Option -->
|
||||
<div class="shipping-option">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" formControlName="shippingSameAsBilling">
|
||||
<span class="checkmark"></span>
|
||||
{{ 'CHECKOUT.SHIPPING_SAME' | translate }}
|
||||
</label>
|
||||
</div>
|
||||
<!-- Shipping Address Card (Conditional) -->
|
||||
<app-card *ngIf="!checkoutForm.get('shippingSameAsBilling')?.value" class="mb-6">
|
||||
<div class="card-header-simple">
|
||||
<h3>{{ 'CHECKOUT.SHIPPING_ADDR' | translate }}</h3>
|
||||
</div>
|
||||
<div formGroupName="shippingAddress">
|
||||
<div class="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="company-fields">
|
||||
<app-input formControlName="companyName" [label]="('CHECKOUT.COMPANY_NAME' | translate) + ' (Optional)'"></app-input>
|
||||
<app-input formControlName="referencePerson" [label]="('CONTACT.REF_PERSON' | translate) + ' (Optional)'"></app-input>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Address Card (Conditional) -->
|
||||
<div class="form-card" *ngIf="!checkoutForm.get('shippingSameAsBilling')?.value">
|
||||
<div class="card-header">
|
||||
<h3>{{ 'CHECKOUT.SHIPPING_ADDR' | translate }}</h3>
|
||||
<app-input formControlName="addressLine1" [label]="'CHECKOUT.ADDRESS_1' | translate"></app-input>
|
||||
|
||||
<div class="form-row three-cols">
|
||||
<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="actions">
|
||||
<app-button type="submit" [disabled]="checkoutForm.invalid || isSubmitting()" [fullWidth]="true">
|
||||
{{ (isSubmitting() ? 'CHECKOUT.PROCESSING' : 'CHECKOUT.PLACE_ORDER') | translate }}
|
||||
</app-button>
|
||||
</div>
|
||||
<div class="card-content" formGroupName="shippingAddress">
|
||||
<div class="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="company-fields">
|
||||
<app-input formControlName="companyName" [label]="('CHECKOUT.COMPANY_NAME' | translate) + ' (Optional)'"></app-input>
|
||||
<app-input formControlName="referencePerson" [label]="('CONTACT.REF_PERSON' | translate) + ' (Optional)'"></app-input>
|
||||
</div>
|
||||
|
||||
<app-input formControlName="addressLine1" [label]="'CHECKOUT.ADDRESS_1' | translate"></app-input>
|
||||
|
||||
<div class="form-row three-cols">
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN: Order Summary -->
|
||||
<div class="checkout-summary-section">
|
||||
<app-card class="sticky-card">
|
||||
<div class="card-header-simple">
|
||||
<h3>{{ 'CHECKOUT.SUMMARY_TITLE' | translate }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<app-button type="submit" [disabled]="checkoutForm.invalid || isSubmitting()" [fullWidth]="true">
|
||||
{{ isSubmitting() ? ('CHECKOUT.PROCESSING' | translate) : ('CHECKOUT.PLACE_ORDER' | translate) }}
|
||||
</app-button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN: Order Summary -->
|
||||
<div class="checkout-summary-section">
|
||||
<div class="form-card sticky-card">
|
||||
<div class="card-header">
|
||||
<h3>{{ 'CHECKOUT.SUMMARY_TITLE' | translate }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
|
||||
<div class="summary-items" *ngIf="quoteSession() as session">
|
||||
<div class="summary-item" *ngFor="let item of session.items">
|
||||
<div class="item-details">
|
||||
@@ -142,15 +144,18 @@
|
||||
<span>{{ 'CHECKOUT.SETUP_FEE' | translate }}</span>
|
||||
<span>{{ session.session.setupCostChf | currency:'CHF' }}</span>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="total-row grand-total">
|
||||
<div class="total-row">
|
||||
<span>{{ 'CHECKOUT.SHIPPING' | translate }}</span>
|
||||
<span>{{ 9.00 | currency:'CHF' }}</span>
|
||||
</div>
|
||||
<div class="grand-total">
|
||||
<span>{{ 'CHECKOUT.TOTAL' | translate }}</span>
|
||||
<span>{{ session.grandTotalChf | currency:'CHF' }}</span>
|
||||
<span>{{ (session.grandTotalChf + 9.00) | currency:'CHF' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,84 +1,76 @@
|
||||
.checkout-page {
|
||||
padding: 3rem 1rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
.hero {
|
||||
padding: var(--space-8) 0;
|
||||
text-align: center;
|
||||
|
||||
.section-title {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
}
|
||||
|
||||
.checkout-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 380px;
|
||||
grid-template-columns: 1fr 420px;
|
||||
gap: var(--space-8);
|
||||
align-items: start;
|
||||
margin-bottom: var(--space-12);
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 1024px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-6);
|
||||
gap: var(--space-8);
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
.card-header-simple {
|
||||
margin-bottom: var(--space-6);
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
.form-card {
|
||||
margin-bottom: var(--space-6);
|
||||
background: var(--color-bg-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
|
||||
.card-header {
|
||||
padding: var(--space-4) var(--space-6);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-bg-subtle);
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-heading);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: var(--space-6);
|
||||
padding-bottom: var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
margin-bottom: var(--space-4);
|
||||
|
||||
@media(min-width: 768px) {
|
||||
flex-direction: row;
|
||||
& > * { flex: 1; }
|
||||
}
|
||||
|
||||
&.no-margin {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.three-cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
grid-template-columns: 1.5fr 2fr 1fr;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
app-input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
&.three-cols {
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
app-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* User Type Selector Styles - Matched with Contact Form */
|
||||
/* User Type Selector - Matching Contact Form Style */
|
||||
.user-type-selector {
|
||||
display: flex;
|
||||
background-color: var(--color-neutral-100);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 4px;
|
||||
margin-bottom: var(--space-4);
|
||||
margin: var(--space-6) 0;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
@@ -112,12 +104,14 @@
|
||||
gap: var(--space-4);
|
||||
padding-left: var(--space-4);
|
||||
border-left: 2px solid var(--color-border);
|
||||
margin-top: var(--space-4);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.shipping-option {
|
||||
margin: var(--space-6) 0;
|
||||
padding: var(--space-4);
|
||||
background: var(--color-neutral-100);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* Custom Checkbox */
|
||||
@@ -125,9 +119,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding-left: 30px;
|
||||
padding-left: 36px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
color: var(--color-text);
|
||||
|
||||
@@ -153,10 +148,10 @@
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background-color: var(--color-bg-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background-color: var(--color-bg-card);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.2s;
|
||||
|
||||
@@ -164,12 +159,12 @@
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 6px;
|
||||
top: 2px;
|
||||
left: 7px;
|
||||
top: 3px;
|
||||
width: 6px;
|
||||
height: 12px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
border: solid #000;
|
||||
border-width: 0 2.5px 2.5px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
@@ -179,40 +174,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.checkout-summary-section {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sticky-card {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
/* Inherits styles from .form-card */
|
||||
top: var(--space-6);
|
||||
}
|
||||
|
||||
.summary-items {
|
||||
margin-bottom: var(--space-6);
|
||||
max-height: 400px;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
padding-right: var(--space-2);
|
||||
padding-top: var(--space-2);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: var(--space-3) 0;
|
||||
padding: var(--space-4) 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
&:first-child { padding-top: 0; }
|
||||
&:last-child { border-bottom: none; }
|
||||
|
||||
.item-details {
|
||||
flex: 1;
|
||||
|
||||
.item-name {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: var(--space-1);
|
||||
word-break: break-all;
|
||||
color: var(--color-text);
|
||||
@@ -226,8 +229,8 @@
|
||||
color: var(--color-text-muted);
|
||||
|
||||
.color-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -245,55 +248,52 @@
|
||||
font-weight: 600;
|
||||
margin-left: var(--space-3);
|
||||
white-space: nowrap;
|
||||
color: var(--color-heading);
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.summary-totals {
|
||||
padding-top: var(--space-4);
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin-top: var(--space-4);
|
||||
background: var(--color-neutral-100);
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--space-6);
|
||||
|
||||
.total-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-text);
|
||||
font-size: 0.95rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&.grand-total {
|
||||
color: var(--color-heading);
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
margin-top: var(--space-4);
|
||||
padding-top: var(--space-4);
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.grand-total {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--color-text);
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
margin-top: var(--space-4);
|
||||
padding-top: var(--space-4);
|
||||
border-top: 2px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: var(--space-6);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--space-8);
|
||||
|
||||
app-button {
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 900px) {
|
||||
width: auto;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--color-danger);
|
||||
background: var(--color-danger-subtle);
|
||||
color: var(--color-error);
|
||||
background: #fef2f2;
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--space-6);
|
||||
border: 1px solid var(--color-danger);
|
||||
border: 1px solid #fee2e2;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mb-6 { margin-bottom: var(--space-6); }
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service';
|
||||
import { AppInputComponent } from '../../shared/components/app-input/app-input.component';
|
||||
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
|
||||
import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-checkout',
|
||||
@@ -15,7 +16,8 @@ import { AppButtonComponent } from '../../shared/components/app-button/app-butto
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
AppInputComponent,
|
||||
AppButtonComponent
|
||||
AppButtonComponent,
|
||||
AppCardComponent
|
||||
],
|
||||
templateUrl: './checkout.component.html',
|
||||
styleUrls: ['./checkout.component.scss']
|
||||
@@ -75,20 +77,27 @@ export class CheckoutComponent implements OnInit {
|
||||
const type = isCompany ? 'BUSINESS' : 'PRIVATE';
|
||||
this.checkoutForm.patchValue({ customerType: type });
|
||||
|
||||
// Update validators based on type
|
||||
const billingGroup = this.checkoutForm.get('billingAddress') as FormGroup;
|
||||
const companyControl = billingGroup.get('companyName');
|
||||
const referenceControl = billingGroup.get('referencePerson');
|
||||
const firstNameControl = billingGroup.get('firstName');
|
||||
const lastNameControl = billingGroup.get('lastName');
|
||||
|
||||
if (isCompany) {
|
||||
companyControl?.setValidators([Validators.required]);
|
||||
referenceControl?.setValidators([Validators.required]);
|
||||
firstNameControl?.clearValidators();
|
||||
lastNameControl?.clearValidators();
|
||||
} else {
|
||||
companyControl?.clearValidators();
|
||||
referenceControl?.clearValidators();
|
||||
firstNameControl?.setValidators([Validators.required]);
|
||||
lastNameControl?.setValidators([Validators.required]);
|
||||
}
|
||||
companyControl?.updateValueAndValidity();
|
||||
referenceControl?.updateValueAndValidity();
|
||||
firstNameControl?.updateValueAndValidity();
|
||||
lastNameControl?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<app-input formControlName="companyName" [label]="('CONTACT.COMPANY_NAME' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_COMPANY' | translate"></app-input>
|
||||
<app-input formControlName="referencePerson" [label]="('CONTACT.REF_PERSON' | translate) + ' *'" [placeholder]="'CONTACT.PLACEHOLDER_REF_PERSON' | translate"></app-input>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{ 'CONTACT.LABEL_MESSAGE' | translate }}</label>
|
||||
<textarea formControlName="message" class="form-control" rows="4"></textarea>
|
||||
@@ -47,10 +47,10 @@
|
||||
<div class="form-group">
|
||||
<label>{{ 'CONTACT.UPLOAD_LABEL' | translate }}</label>
|
||||
<p class="hint">{{ 'CONTACT.UPLOAD_HINT' | translate }}</p>
|
||||
|
||||
<div class="drop-zone" (click)="fileInput.click()"
|
||||
|
||||
<div class="drop-zone" (click)="fileInput.click()"
|
||||
(dragover)="onDragOver($event)" (drop)="onDrop($event)">
|
||||
<input #fileInput type="file" multiple (change)="onFileSelected($event)" hidden
|
||||
<input #fileInput type="file" multiple (change)="onFileSelected($event)" hidden
|
||||
accept=".jpg,.jpeg,.png,.pdf,.stl,.step,.stp,.3mf,.obj">
|
||||
<p>{{ 'CONTACT.DROP_FILES' | translate }}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,109 @@
|
||||
<div class="payment-container">
|
||||
<mat-card class="payment-card">
|
||||
<mat-card-header>
|
||||
<mat-icon mat-card-avatar>payment</mat-icon>
|
||||
<mat-card-title>Payment Integration</mat-card-title>
|
||||
<mat-card-subtitle>Order #{{ orderId }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="coming-soon">
|
||||
<h3>Coming Soon</h3>
|
||||
<p>The online payment system is currently under development.</p>
|
||||
<p>Your order has been saved. Please contact us to arrange payment.</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<mat-card-actions align="end">
|
||||
<button mat-raised-button color="primary" (click)="completeOrder()">
|
||||
Simulate Payment Completion
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
<div class="container hero">
|
||||
<h1>{{ 'PAYMENT.TITLE' | translate }}</h1>
|
||||
<p class="subtitle">{{ 'CHECKOUT.SUBTITLE' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="payment-layout" *ngIf="order() as o">
|
||||
<div class="payment-main">
|
||||
<app-card class="mb-6">
|
||||
<div class="card-header-simple">
|
||||
<h3>{{ 'PAYMENT.METHOD' | translate }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="payment-selection">
|
||||
<div class="methods-grid">
|
||||
<div
|
||||
class="type-option"
|
||||
[class.selected]="selectedPaymentMethod === 'twint'"
|
||||
(click)="selectPayment('twint')">
|
||||
<span class="method-name">TWINT</span>
|
||||
</div>
|
||||
<div
|
||||
class="type-option"
|
||||
[class.selected]="selectedPaymentMethod === 'bill'"
|
||||
(click)="selectPayment('bill')">
|
||||
<span class="method-name">QR Bill / Bank Transfer</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-details fade-in" *ngIf="selectedPaymentMethod === 'twint'">
|
||||
<div class="details-header">
|
||||
<h4>{{ 'PAYMENT.TWINT_TITLE' | translate }}</h4>
|
||||
</div>
|
||||
<div class="qr-placeholder">
|
||||
<div class="qr-box">
|
||||
<span>QR CODE</span>
|
||||
</div>
|
||||
<p>{{ 'PAYMENT.TWINT_DESC' | translate }}</p>
|
||||
<p class="amount">{{ 'PAYMENT.TOTAL' | translate }}: {{ o.totalChf | currency:'CHF' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-details fade-in" *ngIf="selectedPaymentMethod === 'bill'">
|
||||
<div class="details-header">
|
||||
<h4>{{ 'PAYMENT.BANK_TITLE' | translate }}</h4>
|
||||
</div>
|
||||
<div class="bank-details">
|
||||
<p><strong>{{ 'PAYMENT.BANK_OWNER' | translate }}:</strong> 3D Fab Switzerland</p>
|
||||
<p><strong>{{ 'PAYMENT.BANK_IBAN' | translate }}:</strong> CH98 0000 0000 0000 0000 0</p>
|
||||
<p><strong>{{ 'PAYMENT.BANK_REF' | translate }}:</strong> {{ o.id }}</p>
|
||||
|
||||
<div class="qr-bill-actions">
|
||||
<app-button variant="outline" (click)="downloadInvoice()">
|
||||
{{ 'PAYMENT.DOWNLOAD_QR' | translate }}
|
||||
</app-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<app-button (click)="completeOrder()" [disabled]="!selectedPaymentMethod" [fullWidth]="true">
|
||||
{{ 'PAYMENT.CONFIRM' | translate }}
|
||||
</app-button>
|
||||
</div>
|
||||
</app-card>
|
||||
</div>
|
||||
|
||||
<div class="payment-summary">
|
||||
<app-card class="sticky-card">
|
||||
<div class="card-header-simple">
|
||||
<h3>{{ 'PAYMENT.SUMMARY_TITLE' | translate }}</h3>
|
||||
<p class="order-id">#{{ o.id.substring(0, 8) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="summary-totals">
|
||||
<div class="total-row">
|
||||
<span>{{ 'PAYMENT.SUBTOTAL' | translate }}</span>
|
||||
<span>{{ o.subtotalChf | currency:'CHF' }}</span>
|
||||
</div>
|
||||
<div class="total-row">
|
||||
<span>{{ 'PAYMENT.SHIPPING' | translate }}</span>
|
||||
<span>{{ o.shippingCostChf | currency:'CHF' }}</span>
|
||||
</div>
|
||||
<div class="total-row">
|
||||
<span>{{ 'PAYMENT.SETUP_FEE' | translate }}</span>
|
||||
<span>{{ o.setupCostChf | currency:'CHF' }}</span>
|
||||
</div>
|
||||
<div class="grand-total-row">
|
||||
<span>{{ 'PAYMENT.TOTAL' | translate }}</span>
|
||||
<span>{{ o.totalChf | currency:'CHF' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</app-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="loading()" class="loading-state">
|
||||
<app-card>
|
||||
<p>{{ 'PAYMENT.LOADING' | translate }}</p>
|
||||
</app-card>
|
||||
</div>
|
||||
|
||||
<div *ngIf="error()" class="error-message">
|
||||
<app-card>
|
||||
<p>{{ error() }}</p>
|
||||
</app-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,202 @@
|
||||
.payment-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 80vh;
|
||||
padding: 2rem;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.payment-card {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.coming-soon {
|
||||
.hero {
|
||||
padding: var(--space-12) 0 var(--space-8);
|
||||
text-align: center;
|
||||
padding: 2rem 0;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #777;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #3f51b5;
|
||||
.subtitle {
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-text-muted);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.payment-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
gap: var(--space-8);
|
||||
align-items: start;
|
||||
margin-bottom: var(--space-12);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-simple {
|
||||
margin-bottom: var(--space-6);
|
||||
padding-bottom: var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.order-id {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-selection {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.methods-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-4);
|
||||
|
||||
@media (max-width: 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.type-option {
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--color-bg-card);
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-brand);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: var(--color-brand);
|
||||
background-color: var(--color-neutral-100);
|
||||
color: #000;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.payment-details {
|
||||
background: var(--color-neutral-100);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-6);
|
||||
margin-bottom: var(--space-6);
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
.details-header {
|
||||
margin-bottom: var(--space-4);
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qr-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.qr-box {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background-color: white;
|
||||
border: 2px solid var(--color-neutral-900);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-top: var(--space-2);
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.bank-details {
|
||||
p {
|
||||
margin-bottom: var(--space-2);
|
||||
font-size: 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.qr-bill-actions {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.sticky-card {
|
||||
position: sticky;
|
||||
top: var(--space-6);
|
||||
}
|
||||
|
||||
.summary-totals {
|
||||
background: var(--color-neutral-100);
|
||||
padding: var(--space-6);
|
||||
border-radius: var(--radius-md);
|
||||
|
||||
.total-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--space-2);
|
||||
font-size: 0.95rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.grand-total-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--color-text);
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
margin-top: var(--space-4);
|
||||
padding-top: var(--space-4);
|
||||
border-top: 2px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: var(--space-8);
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.mb-6 { margin-bottom: var(--space-6); }
|
||||
|
||||
.error-message,
|
||||
.loading-state {
|
||||
margin-top: var(--space-12);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,75 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
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 } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-payment',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatButtonModule, MatCardModule, MatIconModule],
|
||||
imports: [CommonModule, AppButtonComponent, AppCardComponent, TranslateModule],
|
||||
templateUrl: './payment.component.html',
|
||||
styleUrl: './payment.component.scss'
|
||||
})
|
||||
export class PaymentComponent implements OnInit {
|
||||
orderId: string | null = null;
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private quoteService = inject(QuoteEstimatorService);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {}
|
||||
orderId: string | null = null;
|
||||
selectedPaymentMethod: 'twint' | 'bill' | null = null;
|
||||
order = signal<any>(null);
|
||||
loading = signal(true);
|
||||
error = signal<string | null>(null);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.orderId = this.route.snapshot.paramMap.get('orderId');
|
||||
if (this.orderId) {
|
||||
this.loadOrder();
|
||||
} else {
|
||||
this.error.set('Order 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('Failed to load order details.');
|
||||
this.loading.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectPayment(method: 'twint' | 'bill'): void {
|
||||
this.selectedPaymentMethod = method;
|
||||
}
|
||||
|
||||
downloadInvoice() {
|
||||
if (!this.orderId) return;
|
||||
this.quoteService.getOrderInvoice(this.orderId).subscribe({
|
||||
next: (blob) => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `invoice-${this.orderId}.pdf`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
error: (err) => console.error('Failed to download invoice', err)
|
||||
});
|
||||
}
|
||||
|
||||
completeOrder(): void {
|
||||
// Simulate payment completion
|
||||
alert('Payment Simulated! Order marked as PAID.');
|
||||
// Here you would call the backend to mark as paid if we had that endpoint ready
|
||||
// For now, redirect home or show success
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
},
|
||||
"CHECKOUT": {
|
||||
"TITLE": "Checkout",
|
||||
"SUBTITLE": "Complete your order by entering the shipping and payment details.",
|
||||
"CONTACT_INFO": "Contact Information",
|
||||
"BILLING_ADDR": "Billing Address",
|
||||
"SHIPPING_ADDR": "Shipping Address",
|
||||
@@ -171,6 +172,25 @@
|
||||
"SUBTOTAL": "Subtotal",
|
||||
"SETUP_FEE": "Setup Fee",
|
||||
"TOTAL": "Total",
|
||||
"QTY": "Qty"
|
||||
"QTY": "Qty",
|
||||
"SHIPPING": "Shipping"
|
||||
},
|
||||
"PAYMENT": {
|
||||
"TITLE": "Payment",
|
||||
"METHOD": "Payment Method",
|
||||
"TWINT_TITLE": "Pay with TWINT",
|
||||
"TWINT_DESC": "Scan the code with your TWINT app",
|
||||
"BANK_TITLE": "Bank Transfer",
|
||||
"BANK_OWNER": "Owner",
|
||||
"BANK_IBAN": "IBAN",
|
||||
"BANK_REF": "Reference",
|
||||
"DOWNLOAD_QR": "Download QR-Invoice (PDF)",
|
||||
"CONFIRM": "Confirm Order",
|
||||
"SUMMARY_TITLE": "Order Summary",
|
||||
"SUBTOTAL": "Subtotal",
|
||||
"SHIPPING": "Shipping",
|
||||
"SETUP_FEE": "Setup Fee",
|
||||
"TOTAL": "Total",
|
||||
"LOADING": "Loading order details..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
},
|
||||
"CHECKOUT": {
|
||||
"TITLE": "Checkout",
|
||||
"SUBTITLE": "Completa il tuo ordine inserendo i dettagli per la spedizione e il pagamento.",
|
||||
"CONTACT_INFO": "Informazioni di Contatto",
|
||||
"BILLING_ADDR": "Indirizzo di Fatturazione",
|
||||
"SHIPPING_ADDR": "Indirizzo di Spedizione",
|
||||
@@ -150,6 +151,25 @@
|
||||
"SUBTOTAL": "Subtotale",
|
||||
"SETUP_FEE": "Costo di Avvio",
|
||||
"TOTAL": "Totale",
|
||||
"QTY": "Qtà"
|
||||
"QTY": "Qtà",
|
||||
"SHIPPING": "Spedizione"
|
||||
},
|
||||
"PAYMENT": {
|
||||
"TITLE": "Pagamento",
|
||||
"METHOD": "Metodo di Pagamento",
|
||||
"TWINT_TITLE": "Paga con TWINT",
|
||||
"TWINT_DESC": "Inquadra il codice con l'app TWINT",
|
||||
"BANK_TITLE": "Bonifico Bancario",
|
||||
"BANK_OWNER": "Titolare",
|
||||
"BANK_IBAN": "IBAN",
|
||||
"BANK_REF": "Riferimento",
|
||||
"DOWNLOAD_QR": "Scarica QR-Fattura (PDF)",
|
||||
"CONFIRM": "Conferma Ordine",
|
||||
"SUMMARY_TITLE": "Riepilogo Ordine",
|
||||
"SUBTOTAL": "Subtotale",
|
||||
"SHIPPING": "Spedizione",
|
||||
"SETUP_FEE": "Costo Setup",
|
||||
"TOTAL": "Totale",
|
||||
"LOADING": "Caricamento dettagli ordine..."
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user