Merge pull request 'feat/shop' (#33) from feat/shop into dev
Some checks failed
Build and Deploy / test-backend (push) Successful in 25s
Build and Deploy / test-frontend (push) Successful in 1m1s
Build and Deploy / build-and-push (push) Failing after 25s
Build and Deploy / deploy (push) Has been skipped

Reviewed-on: #33
This commit was merged in pull request #33.
This commit is contained in:
2026-03-09 19:21:27 +01:00
25 changed files with 843 additions and 1040 deletions

View File

@@ -1,11 +1,18 @@
<section class="section-card">
<header class="section-header">
<div class="header-copy">
<h2>Richieste di contatto</h2>
<p>Richieste preventivo personalizzato ricevute dal sito.</p>
<span class="total-pill">{{ requests.length }} richieste</span>
<section class="section-card ui-section-card">
<header class="section-header ui-section-header">
<div class="header-copy ui-section-header__copy">
<h2 class="ui-section-header__title">Richieste di contatto</h2>
<p class="ui-section-header__description">
Richieste preventivo personalizzato ricevute dal sito.
</p>
<span class="total-pill ui-pill">{{ requests.length }} richieste</span>
</div>
<button type="button" (click)="loadRequests()" [disabled]="loading">
<button
type="button"
class="ui-button"
(click)="loadRequests()"
[disabled]="loading"
>
Aggiorna
</button>
</header>
@@ -13,11 +20,11 @@
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
<p class="success" *ngIf="successMessage">{{ successMessage }}</p>
<div class="workspace" *ngIf="!loading; else loadingTpl">
<div class="workspace ui-split-workspace" *ngIf="!loading; else loadingTpl">
<section class="list-panel">
<h3>Lista richieste</h3>
<div class="table-wrap">
<table class="requests-table">
<div class="table-wrap ui-table-wrap">
<table class="requests-table ui-data-table">
<thead>
<tr>
<th>Data</th>
@@ -50,14 +57,18 @@
</td>
<td class="email-cell">{{ request.email }}</td>
<td>
<span class="chip chip-neutral">{{ request.requestType }}</span>
<span class="ui-status-chip ui-status-chip--neutral">{{
request.requestType
}}</span>
</td>
<td>
<span class="chip chip-light">{{ request.customerType }}</span>
<span class="ui-status-chip ui-status-chip--neutral">{{
request.customerType
}}</span>
</td>
<td>
<span
class="chip"
class="ui-status-chip"
[ngClass]="getStatusChipClass(request.status)"
>{{ request.status }}</span
>
@@ -71,13 +82,14 @@
</div>
</section>
<section class="detail-panel" *ngIf="selectedRequest">
<section class="detail-panel ui-detail-panel" *ngIf="selectedRequest">
<header class="detail-header">
<div>
<h3>Dettaglio richiesta</h3>
<p class="request-id">
<span>ID</span>
<code
class="ui-code-pill"
[title]="selectedRequest.id"
[appCopyOnClick]="selectedRequest.id"
>{{ selectedRequest.id }}</code
@@ -86,14 +98,14 @@
</div>
<div class="detail-chips">
<span
class="chip"
class="ui-status-chip"
[ngClass]="getStatusChipClass(selectedRequest.status)"
>{{ selectedRequest.status }}</span
>
<span class="chip chip-neutral">{{
<span class="ui-status-chip ui-status-chip--neutral">{{
selectedRequest.requestType
}}</span>
<span class="chip chip-light">{{
<span class="ui-status-chip ui-status-chip--neutral">{{
selectedRequest.customerType
}}</span>
</div>
@@ -103,32 +115,32 @@
Caricamento dettaglio...
</p>
<dl class="meta-grid">
<div class="meta-item">
<dl class="meta-grid ui-meta-grid">
<div class="meta-item ui-meta-item">
<dt>Creata</dt>
<dd>{{ selectedRequest.createdAt | date: "medium" }}</dd>
</div>
<div class="meta-item">
<div class="meta-item ui-meta-item">
<dt>Aggiornata</dt>
<dd>{{ selectedRequest.updatedAt | date: "medium" }}</dd>
</div>
<div class="meta-item">
<div class="meta-item ui-meta-item">
<dt>Email</dt>
<dd>{{ selectedRequest.email }}</dd>
</div>
<div class="meta-item">
<div class="meta-item ui-meta-item">
<dt>Telefono</dt>
<dd>{{ selectedRequest.phone || "-" }}</dd>
</div>
<div class="meta-item">
<div class="meta-item ui-meta-item">
<dt>Nome</dt>
<dd>{{ selectedRequest.name || "-" }}</dd>
</div>
<div class="meta-item">
<div class="meta-item ui-meta-item">
<dt>Azienda</dt>
<dd>{{ selectedRequest.companyName || "-" }}</dd>
</div>
<div class="meta-item">
<div class="meta-item ui-meta-item">
<dt>Referente</dt>
<dd>{{ selectedRequest.contactPerson || "-" }}</dd>
</div>
@@ -138,6 +150,7 @@
<div class="status-editor-field">
<label for="contact-request-status">Stato richiesta</label>
<select
class="ui-form-control"
id="contact-request-status"
[ngModel]="selectedStatus"
(ngModelChange)="selectedStatus = $event"
@@ -148,6 +161,7 @@
</select>
</div>
<button
class="ui-button"
type="button"
(click)="updateRequestStatus()"
[disabled]="
@@ -190,7 +204,7 @@
</div>
<button
type="button"
class="ghost"
class="ui-button ui-button--ghost"
(click)="downloadAttachment(attachment)"
>
Scarica file
@@ -200,7 +214,10 @@
</div>
</section>
<section class="detail-panel empty" *ngIf="!selectedRequest">
<section
class="detail-panel ui-detail-panel ui-detail-panel--empty"
*ngIf="!selectedRequest"
>
<h3>Nessuna richiesta selezionata</h3>
<p>Seleziona una riga dalla lista per vedere il dettaglio.</p>
</section>

View File

@@ -4,23 +4,6 @@
gap: var(--space-5);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-4);
}
.section-header h2 {
margin: 0;
font-size: 1.4rem;
}
.section-header p {
margin: var(--space-2) 0 0;
color: var(--color-text-muted);
}
.header-copy {
display: grid;
gap: var(--space-1);
@@ -29,21 +12,6 @@
.total-pill {
width: fit-content;
margin-top: var(--space-1);
border-radius: 999px;
border: 1px solid var(--color-border);
background: var(--color-neutral-100);
color: var(--color-text-muted);
font-size: 0.78rem;
font-weight: 700;
line-height: 1;
padding: 6px 10px;
}
.workspace {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-4);
align-items: start;
}
.list-panel,
@@ -51,72 +19,11 @@
min-width: 0;
}
button {
border: 0;
border-radius: var(--radius-md);
background: var(--color-brand);
color: var(--color-neutral-900);
padding: var(--space-2) var(--space-4);
font-weight: 600;
cursor: pointer;
transition:
background-color 0.2s ease,
opacity 0.2s ease;
line-height: 1.2;
}
button:hover:not(:disabled) {
background: var(--color-brand-hover);
}
button.ghost {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
color: var(--color-text);
}
.list-panel h3 {
margin: 0 0 var(--space-2);
font-size: 1.02rem;
}
.table-wrap {
overflow: auto;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
max-height: 72vh;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
}
thead {
position: sticky;
top: 0;
z-index: 1;
background: var(--color-neutral-100);
}
th,
td {
text-align: left;
padding: var(--space-3);
border-bottom: 1px solid var(--color-border);
font-size: 0.92rem;
vertical-align: top;
}
th {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--color-text-muted);
}
.name-cell .primary {
margin: 0;
font-weight: 600;
@@ -135,11 +42,6 @@ th {
tbody tr {
cursor: pointer;
transition: background-color 0.15s ease;
}
tbody tr:hover {
background: #fff9d9;
}
tbody tr.selected {
@@ -154,27 +56,6 @@ tbody tr.selected {
background: transparent;
}
.detail-panel {
display: grid;
gap: var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
padding: var(--space-4);
min-height: 500px;
}
.detail-panel.empty {
display: grid;
align-content: center;
justify-items: center;
text-align: center;
}
.detail-panel.empty h3 {
margin: 0 0 var(--space-2);
}
.detail-header {
display: flex;
justify-content: space-between;
@@ -210,11 +91,6 @@ tbody tr.selected {
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
color: var(--color-text);
background: var(--color-neutral-100);
border: 1px solid var(--color-border);
border-radius: 7px;
padding: 3px 8px;
}
.loading-detail {
@@ -223,28 +99,12 @@ tbody tr.selected {
font-size: 0.85rem;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: var(--space-2);
margin: 0;
}
.meta-item {
margin: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-3);
background: var(--color-neutral-100);
display: grid;
gap: 4px;
}
.meta-item dt {
margin: 0;
font-size: 0.78rem;
font-weight: 700;
color: var(--color-text-muted);
}
.meta-item dd {
@@ -323,7 +183,7 @@ tbody tr.selected {
.status-editor {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-neutral-100);
background: var(--color-surface-muted);
padding: var(--space-3);
display: flex;
flex-wrap: wrap;
@@ -343,79 +203,7 @@ tbody tr.selected {
color: var(--color-text-muted);
}
.status-editor-field select {
width: 100%;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-card);
font: inherit;
}
.chip {
display: inline-flex;
align-items: center;
border-radius: 999px;
border: 1px solid transparent;
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.02em;
line-height: 1;
padding: 5px 9px;
text-transform: uppercase;
}
.chip-neutral {
background: #e9f4ff;
border-color: #c8def4;
color: #1e4d78;
}
.chip-light {
background: #f4f5f8;
border-color: #dde1e8;
color: #4a5567;
}
.chip-warning {
background: #fff4cd;
border-color: #f7dd85;
color: #684b00;
}
.chip-success {
background: #dff6ea;
border-color: #b6e2cb;
color: #14543a;
}
.chip-danger {
background: #fde4e2;
border-color: #f3c0ba;
color: #812924;
}
button:disabled {
opacity: 0.68;
cursor: default;
}
@media (min-width: 1460px) {
.workspace {
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.9fr);
}
}
@media (max-width: 760px) {
.section-card {
gap: var(--space-4);
}
.section-header {
flex-direction: column;
align-items: stretch;
}
.detail-header {
flex-direction: column;
}
@@ -444,8 +232,8 @@ button:disabled {
}
@media (max-width: 520px) {
th,
td {
.ui-data-table th,
.ui-data-table td {
padding: var(--space-2);
font-size: 0.86rem;
}

View File

@@ -118,15 +118,15 @@ export class AdminContactRequestsComponent implements OnInit {
getStatusChipClass(status?: string): string {
const normalized = (status || '').trim().toUpperCase();
if (['PENDING', 'NEW', 'OPEN', 'IN_PROGRESS'].includes(normalized)) {
return 'chip-warning';
return 'ui-status-chip--warning';
}
if (['DONE', 'COMPLETED', 'RESOLVED', 'CLOSED'].includes(normalized)) {
return 'chip-success';
return 'ui-status-chip--success';
}
if (['REJECTED', 'FAILED', 'ERROR', 'SPAM'].includes(normalized)) {
return 'chip-danger';
return 'ui-status-chip--danger';
}
return 'chip-light';
return 'ui-status-chip--neutral';
}
updateRequestStatus(): void {

View File

@@ -1,11 +1,18 @@
<section class="admin-dashboard">
<header class="dashboard-header">
<div>
<h1>Ordini</h1>
<p>Seleziona un ordine a sinistra e gestiscilo nel dettaglio a destra.</p>
<section class="admin-dashboard ui-section-card">
<header class="dashboard-header ui-section-header">
<div class="ui-section-header__copy">
<h1 class="ui-section-header__title">Ordini</h1>
<p class="ui-section-header__description">
Seleziona un ordine a sinistra e gestiscilo nel dettaglio a destra.
</p>
</div>
<div class="header-actions">
<button type="button" (click)="loadOrders()" [disabled]="loading">
<button
type="button"
class="ui-button"
(click)="loadOrders()"
[disabled]="loading"
>
Aggiorna
</button>
</div>
@@ -13,13 +20,14 @@
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
<div class="workspace" *ngIf="!loading; else loadingTpl">
<div class="workspace ui-split-workspace" *ngIf="!loading; else loadingTpl">
<section class="list-panel">
<h2>Lista ordini</h2>
<div class="list-toolbar">
<label class="toolbar-field" for="order-search">
<span>Cerca UUID</span>
<input
class="ui-form-control"
id="order-search"
type="search"
[ngModel]="orderSearchTerm"
@@ -30,6 +38,7 @@
<label class="toolbar-field" for="payment-status-filter">
<span>Stato pagamento</span>
<select
class="ui-form-control"
id="payment-status-filter"
[ngModel]="paymentStatusFilter"
(ngModelChange)="onPaymentStatusFilterChange($event)"
@@ -45,6 +54,7 @@
<label class="toolbar-field" for="order-status-filter">
<span>Stato ordine</span>
<select
class="ui-form-control"
id="order-status-filter"
[ngModel]="orderStatusFilter"
(ngModelChange)="onOrderStatusFilterChange($event)"
@@ -58,8 +68,8 @@
</select>
</label>
</div>
<div class="table-wrap">
<table>
<div class="table-wrap ui-table-wrap">
<table class="ui-data-table">
<thead>
<tr>
<th>Ordine</th>
@@ -93,12 +103,13 @@
</div>
</section>
<section class="detail-panel" *ngIf="selectedOrder">
<section class="detail-panel ui-detail-panel" *ngIf="selectedOrder">
<div class="detail-header">
<h2>Dettaglio ordine {{ selectedOrder.orderNumber }}</h2>
<p class="order-uuid">
UUID:
<code
class="ui-code-pill"
[title]="selectedOrder.id"
[appCopyOnClick]="selectedOrder.id"
>{{ selectedOrder.id }}</code
@@ -107,18 +118,18 @@
<p *ngIf="detailLoading">Caricamento dettaglio...</p>
</div>
<div class="meta-grid">
<div>
<div class="meta-grid ui-meta-grid">
<div class="ui-meta-item">
<strong>Cliente</strong><span>{{ selectedOrder.customerEmail }}</span>
</div>
<div>
<div class="ui-meta-item">
<strong>Stato pagamento</strong
><span>{{ selectedOrder.paymentStatus || "PENDING" }}</span>
</div>
<div>
<div class="ui-meta-item">
<strong>Stato ordine</strong><span>{{ selectedOrder.status }}</span>
</div>
<div>
<div class="ui-meta-item">
<strong>Totale</strong
><span>{{
selectedOrder.totalChf | currency: "CHF" : "symbol" : "1.2-2"
@@ -130,6 +141,7 @@
<div class="status-editor">
<label for="order-status">Stato ordine</label>
<select
class="ui-form-control"
id="order-status"
[value]="selectedStatus"
(change)="onStatusChange($event)"
@@ -139,6 +151,7 @@
</option>
</select>
<button
class="ui-button"
type="button"
(click)="updateStatus()"
[disabled]="updatingStatus"
@@ -150,6 +163,7 @@
<div class="status-editor">
<label for="payment-method">Metodo pagamento</label>
<select
class="ui-form-control"
id="payment-method"
[value]="selectedPaymentMethod"
(change)="onPaymentMethodChange($event)"
@@ -162,6 +176,7 @@
</option>
</select>
<button
class="ui-button"
type="button"
(click)="updatePaymentMethod()"
[disabled]="confirmingPayment"
@@ -174,13 +189,25 @@
</div>
<div class="doc-actions">
<button type="button" class="ghost" (click)="downloadConfirmation()">
<button
type="button"
class="ui-button ui-button--ghost"
(click)="downloadConfirmation()"
>
Scarica conferma + QR bill
</button>
<button type="button" class="ghost" (click)="downloadInvoice()">
<button
type="button"
class="ui-button ui-button--ghost"
(click)="downloadInvoice()"
>
Scarica fattura
</button>
<button type="button" class="ghost" (click)="openPrintDetails()">
<button
type="button"
class="ui-button ui-button--ghost"
(click)="openPrintDetails()"
>
Dettagli stampa
</button>
</div>
@@ -215,7 +242,7 @@
</div>
<button
type="button"
class="ghost"
class="ui-button ui-button--ghost"
(click)="downloadItemFile(item.id, item.originalFilename)"
>
Scarica file
@@ -224,7 +251,10 @@
</div>
</section>
<section class="detail-panel empty" *ngIf="!selectedOrder">
<section
class="detail-panel ui-detail-panel ui-detail-panel--empty"
*ngIf="!selectedOrder"
>
<h2>Nessun ordine selezionato</h2>
<p>Seleziona un ordine dalla lista per vedere i dettagli.</p>
</section>
@@ -245,39 +275,39 @@
<h3>Dettagli stampa ordine {{ selectedOrder.orderNumber }}</h3>
<button
type="button"
class="ghost close-btn"
class="ui-button ui-button--ghost close-btn"
(click)="closePrintDetails()"
>
Chiudi
</button>
</header>
<div class="modal-grid">
<div>
<div class="modal-grid ui-meta-grid">
<div class="ui-meta-item">
<strong>Qualità</strong
><span>{{ getQualityLabel(selectedOrder.printLayerHeightMm) }}</span>
</div>
<div>
<div class="ui-meta-item">
<strong>Materiale</strong
><span>{{ selectedOrder.printMaterialCode || "-" }}</span>
</div>
<div>
<div class="ui-meta-item">
<strong>Layer height</strong
><span>{{ selectedOrder.printLayerHeightMm || "-" }} mm</span>
</div>
<div>
<div class="ui-meta-item">
<strong>Nozzle</strong
><span>{{ selectedOrder.printNozzleDiameterMm || "-" }} mm</span>
</div>
<div>
<div class="ui-meta-item">
<strong>Infill pattern</strong
><span>{{ selectedOrder.printInfillPattern || "-" }}</span>
</div>
<div>
<div class="ui-meta-item">
<strong>Infill %</strong
><span>{{ selectedOrder.printInfillPercent ?? "-" }}</span>
</div>
<div>
<div class="ui-meta-item">
<strong>Supporti</strong
><span>{{ selectedOrder.printSupportsEnabled ? "Sì" : "No" }}</span>
</div>

View File

@@ -4,67 +4,16 @@
gap: var(--space-5);
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-4);
}
.dashboard-header h1 {
margin: 0;
font-size: 1.45rem;
}
.dashboard-header p {
margin: var(--space-2) 0 0;
color: var(--color-text-muted);
}
.header-actions {
display: flex;
gap: var(--space-2);
}
.workspace {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.9fr);
gap: var(--space-4);
align-items: start;
}
.list-panel,
.detail-panel {
min-width: 0;
}
button {
border: 0;
border-radius: var(--radius-md);
background: var(--color-brand);
color: var(--color-neutral-900);
padding: var(--space-2) var(--space-4);
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
line-height: 1.2;
}
button:hover:not(:disabled) {
background: var(--color-brand-hover);
}
button.ghost {
background: var(--color-bg-card);
color: var(--color-text);
border: 1px solid var(--color-border);
}
button:disabled {
opacity: 0.65;
cursor: default;
}
.list-panel h2 {
font-size: 1.05rem;
margin-bottom: var(--space-2);
@@ -91,50 +40,8 @@ button:disabled {
color: var(--color-text-muted);
}
.toolbar-field input,
.toolbar-field select {
width: 100%;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-card);
font: inherit;
}
.table-wrap {
overflow: auto;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
max-height: 72vh;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
}
thead {
background: var(--color-neutral-100);
}
th,
td {
text-align: left;
padding: var(--space-3);
border-bottom: 1px solid var(--color-border);
vertical-align: top;
font-size: 0.93rem;
}
tbody tr {
cursor: pointer;
transition: background-color 0.15s ease;
}
tbody tr:hover {
background: #fff9d9;
}
tbody tr.selected {
@@ -149,22 +56,6 @@ tbody tr.no-results:hover {
background: transparent;
}
.detail-panel {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4);
min-height: 520px;
}
.detail-panel.empty {
display: grid;
align-content: center;
justify-items: center;
text-align: center;
color: var(--color-text-muted);
}
.order-uuid {
font-size: 0.84rem;
color: var(--color-text-muted);
@@ -172,38 +63,12 @@ tbody tr.no-results:hover {
.order-uuid code {
font-size: 0.82rem;
color: var(--color-text);
background: var(--color-neutral-100);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 2px 6px;
}
.detail-header h2 {
margin: 0 0 var(--space-2);
}
.meta-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.meta-grid > div {
background: var(--color-neutral-100);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-3);
display: grid;
gap: 2px;
}
.meta-grid strong {
font-size: 0.78rem;
color: var(--color-text-muted);
}
.actions-block {
display: flex;
flex-wrap: wrap;
@@ -223,15 +88,6 @@ tbody tr.no-results:hover {
color: var(--color-text-muted);
}
.status-editor select {
min-width: 210px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-card);
font: inherit;
}
.doc-actions {
display: flex;
flex-wrap: wrap;
@@ -333,26 +189,9 @@ tbody tr.no-results:hover {
}
.modal-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.modal-grid > div {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-neutral-100);
padding: var(--space-3);
display: grid;
gap: 2px;
}
.modal-grid strong {
font-size: 0.78rem;
color: var(--color-text-muted);
}
h4 {
margin: 0 0 var(--space-2);
}
@@ -390,10 +229,6 @@ h4 {
}
@media (max-width: 1280px) {
.workspace {
grid-template-columns: 1fr;
}
.detail-panel {
min-height: unset;
}
@@ -408,15 +243,6 @@ h4 {
grid-template-columns: 1fr;
}
.dashboard-header {
flex-direction: column;
}
.meta-grid,
.modal-grid {
grid-template-columns: 1fr;
}
.item {
align-items: flex-start;
}
@@ -446,8 +272,8 @@ h4 {
}
@media (max-width: 520px) {
th,
td {
.ui-data-table th,
.ui-data-table td {
padding: var(--space-2);
font-size: 0.88rem;
}

View File

@@ -1,12 +1,14 @@
<section class="section-card">
<header class="section-header">
<div>
<h2>Sessioni quote</h2>
<p>Sessioni create dal configuratore con stato e conversione ordine.</p>
<section class="section-card ui-section-card">
<header class="section-header ui-section-header">
<div class="ui-section-header__copy">
<h2 class="ui-section-header__title">Sessioni quote</h2>
<p class="ui-section-header__description">
Sessioni create dal configuratore con stato e conversione ordine.
</p>
</div>
<button
type="button"
class="btn-primary"
class="ui-button"
(click)="loadSessions()"
[disabled]="loading"
>
@@ -17,8 +19,8 @@
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>
<p class="success" *ngIf="successMessage">{{ successMessage }}</p>
<div class="table-wrap" *ngIf="!loading; else loadingTpl">
<table>
<div class="table-wrap ui-table-wrap" *ngIf="!loading; else loadingTpl">
<table class="ui-data-table">
<thead>
<tr>
<th>Sessione</th>
@@ -53,14 +55,14 @@
<td class="actions">
<button
type="button"
class="btn-secondary"
class="ui-button ui-button--ghost"
(click)="toggleSessionDetail(session)"
>
{{ isDetailOpen(session.id) ? "Nascondi" : "Vedi" }}
</button>
<button
type="button"
class="btn-danger"
class="ui-button ui-button--danger"
(click)="deleteSession(session)"
[disabled]="
isDeletingSession(session.id) || !!session.convertedOrderId
@@ -92,6 +94,7 @@
<div class="detail-session-id">
<strong>UUID sessione:</strong>
<code
class="ui-code-pill"
[title]="detail.session.id"
[appCopyOnClick]="detail.session.id"
>{{ detail.session.id }}</code

View File

@@ -4,76 +4,6 @@
gap: var(--space-5);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-4);
}
h2 {
margin: 0;
}
p {
margin: var(--space-2) 0 0;
color: var(--color-text-muted);
}
button {
border: 0;
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-4);
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-primary {
background: var(--color-brand);
color: var(--color-neutral-900);
}
.btn-primary:hover:not(:disabled) {
background: var(--color-brand-hover);
}
.btn-danger {
background: var(--color-danger-500);
color: #fff;
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
}
.btn-secondary {
background: transparent;
color: var(--color-text);
border: 1px solid var(--color-border);
}
.btn-secondary:hover:not(:disabled) {
background: var(--color-neutral-100);
}
.table-wrap {
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 920px;
}
th,
td {
text-align: left;
padding: var(--space-3);
border-bottom: 1px solid var(--color-border);
}
.error {
color: var(--color-danger-500);
margin: 0;
@@ -111,10 +41,6 @@ td {
.detail-session-id code {
display: block;
max-width: 100%;
padding: var(--space-2);
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
background: var(--color-neutral-100);
overflow-wrap: anywhere;
}
@@ -143,11 +69,6 @@ td {
}
@media (max-width: 900px) {
.section-header {
flex-direction: column;
align-items: stretch;
}
.actions {
flex-wrap: wrap;
}
@@ -171,8 +92,8 @@ td {
gap: var(--space-4);
}
th,
td {
.ui-data-table th,
.ui-data-table td {
padding: var(--space-2);
font-size: 0.86rem;
}

View File

@@ -1,7 +1,7 @@
<div class="checkout-page">
<div class="container hero">
<h1 class="section-title">{{ "CHECKOUT.TITLE" | translate }}</h1>
<p class="cad-subtitle" *ngIf="isCadSession()">
<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() }}
@@ -10,7 +10,7 @@
</div>
<div class="container">
<div class="checkout-layout">
<div class="checkout-layout ui-two-column-layout">
<!-- LEFT COLUMN: Form -->
<div class="checkout-form-section">
<!-- Error Message -->
@@ -21,10 +21,12 @@
<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 class="ui-card-header">
<h3 class="ui-card-title">
{{ "CHECKOUT.CONTACT_INFO" | translate }}
</h3>
</div>
<div class="form-row">
<div class="ui-form-row">
<app-input
formControlName="email"
type="email"
@@ -47,8 +49,10 @@
<!-- Billing Address Card -->
<app-card class="mb-6">
<div class="card-header-simple">
<h3>{{ "CHECKOUT.BILLING_ADDR" | translate }}</h3>
<div class="ui-card-header">
<h3 class="ui-card-title">
{{ "CHECKOUT.BILLING_ADDR" | translate }}
</h3>
</div>
<div formGroupName="billingAddress">
<!-- User Type Selector -->
@@ -61,7 +65,7 @@
</app-toggle-selector>
<!-- Private Person Fields -->
<div *ngIf="!isCompany" class="form-row">
<div *ngIf="!isCompany" class="ui-form-row">
<app-input
formControlName="firstName"
[label]="'CHECKOUT.FIRST_NAME' | translate"
@@ -75,7 +79,10 @@
</div>
<!-- Company Fields -->
<div *ngIf="isCompany" class="company-fields mb-4">
<div
*ngIf="isCompany"
class="ui-field-stack ui-field-stack--indented mb-4"
>
<app-input
formControlName="companyName"
[label]="'CHECKOUT.COMPANY_NAME' | translate"
@@ -100,7 +107,7 @@
[label]="'CHECKOUT.ADDRESS_2' | translate"
></app-input>
<div class="form-row three-cols">
<div class="ui-form-row ui-form-row--three">
<app-input
formControlName="zip"
[label]="'CHECKOUT.ZIP' | translate"
@@ -123,10 +130,10 @@
</app-card>
<!-- Shipping Option -->
<div class="shipping-option">
<label class="checkbox-container">
<div class="shipping-option ui-soft-panel">
<label class="ui-checkbox">
<input type="checkbox" formControlName="shippingSameAsBilling" />
<span class="checkmark"></span>
<span class="ui-checkbox__mark"></span>
{{ "CHECKOUT.SHIPPING_SAME" | translate }}
</label>
</div>
@@ -136,11 +143,13 @@
*ngIf="!checkoutForm.get('shippingSameAsBilling')?.value"
class="mb-6"
>
<div class="card-header-simple">
<h3>{{ "CHECKOUT.SHIPPING_ADDR" | translate }}</h3>
<div class="ui-card-header">
<h3 class="ui-card-title">
{{ "CHECKOUT.SHIPPING_ADDR" | translate }}
</h3>
</div>
<div formGroupName="shippingAddress">
<div class="form-row">
<div class="ui-form-row">
<app-input
formControlName="firstName"
[label]="'CHECKOUT.FIRST_NAME' | translate"
@@ -151,7 +160,10 @@
></app-input>
</div>
<div *ngIf="isCompany" class="company-fields">
<div
*ngIf="isCompany"
class="ui-field-stack ui-field-stack--indented"
>
<app-input
formControlName="companyName"
[label]="'CHECKOUT.COMPANY_OPTIONAL' | translate"
@@ -167,7 +179,7 @@
[label]="'CHECKOUT.ADDRESS_1' | translate"
></app-input>
<div class="form-row three-cols">
<div class="ui-form-row ui-form-row--three">
<app-input
formControlName="zip"
[label]="'CHECKOUT.ZIP' | translate"
@@ -187,9 +199,9 @@
</app-card>
<div class="legal-consent">
<label class="checkbox-container">
<label class="ui-checkbox">
<input type="checkbox" formControlName="acceptLegal" />
<span class="checkmark"></span>
<span class="ui-checkbox__mark"></span>
<span>
{{ "LEGAL.CONSENT.LABEL_PREFIX" | translate }}
<a href="/terms" target="_blank" rel="noopener">{{
@@ -213,7 +225,7 @@
</div>
</div>
<div class="actions">
<div class="ui-actions">
<app-button
type="submit"
[disabled]="checkoutForm.invalid || isSubmitting()"
@@ -233,8 +245,10 @@
<!-- 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 class="ui-card-header">
<h3 class="ui-card-title">
{{ "CHECKOUT.SUMMARY_TITLE" | translate }}
</h3>
</div>
<div class="summary-items" *ngIf="quoteSession() as session">

View File

@@ -1,74 +1,5 @@
.hero {
padding: var(--space-8) 0;
text-align: center;
.section-title {
font-size: 2.5rem;
margin-bottom: var(--space-2);
}
}
.cad-subtitle {
margin: 0;
color: var(--color-text-muted);
}
.checkout-layout {
display: grid;
grid-template-columns: 1fr 420px;
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;
}
}
.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: 1.5fr 2fr 1fr;
gap: var(--space-4);
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
app-input {
width: 100%;
}
}
/* User Type Selector CSS has been extracted to app-toggle-selector component */
@@ -78,97 +9,18 @@ app-toggle-selector.user-type-selector-compact {
max-width: 400px;
}
.company-fields {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding-left: var(--space-4);
border-left: 2px solid var(--color-border);
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);
}
.legal-consent {
margin: var(--space-4) 0 var(--space-4);
.checkbox-container {
font-size: 0.95rem;
line-height: 1.4;
align-items: flex-start;
min-height: 24px;
}
}
/* Custom Checkbox */
.checkbox-container {
display: flex;
align-items: center;
position: relative;
padding-left: 36px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
user-select: none;
color: var(--color-text);
input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
&:checked ~ .checkmark {
background-color: var(--color-brand);
border-color: var(--color-brand);
&:after {
display: block;
}
}
}
.checkmark {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
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;
&:after {
content: "";
position: absolute;
display: none;
left: 7px;
top: 3px;
width: 6px;
height: 12px;
border: solid #000;
border-width: 0 2.5px 2.5px 0;
transform: rotate(45deg);
}
}
&:hover input ~ .checkmark {
border-color: var(--color-brand);
}
}
.consent-error {
margin-top: var(--space-2);
margin-left: 36px;
color: var(--color-danger-500, #ef4444);
margin-left: 2.1rem;
color: var(--color-danger-500);
font-size: 0.9rem;
}
@@ -355,9 +207,7 @@ app-toggle-selector.user-type-selector-compact {
padding-right: var(--space-3);
}
.actions {
margin-top: var(--space-8);
.ui-actions {
app-button {
width: 100%;
}
@@ -365,11 +215,11 @@ app-toggle-selector.user-type-selector-compact {
.error-message {
color: var(--color-error);
background: #fef2f2;
background: var(--color-surface-danger);
padding: var(--space-4);
border-radius: var(--radius-md);
margin-bottom: var(--space-6);
border: 1px solid #fee2e2;
border: 1px solid #fecaca;
font-weight: 500;
}

View File

@@ -6,16 +6,18 @@
} @else {
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<!-- Request Type -->
<div class="form-group">
<label>{{ "CONTACT.REQ_TYPE_LABEL" | translate }} *</label>
<select formControlName="requestType" class="form-control">
<div class="ui-form-group">
<label class="ui-form-label"
>{{ "CONTACT.REQ_TYPE_LABEL" | translate }} *</label
>
<select formControlName="requestType" class="ui-form-control">
<option *ngFor="let type of requestTypes" [value]="type.value">
{{ type.label | translate }}
</option>
</select>
</div>
<div class="row">
<div class="ui-form-row">
<!-- Phone -->
<app-input
formControlName="email"
@@ -35,22 +37,12 @@
</div>
<!-- User Type Selector (Segmented Control) -->
<div class="user-type-selector">
<div
class="type-option"
[class.selected]="!isCompany"
(click)="setCompanyMode(false)"
>
{{ "CONTACT.TYPE_PRIVATE" | translate }}
</div>
<div
class="type-option"
[class.selected]="isCompany"
(click)="setCompanyMode(true)"
>
{{ "CONTACT.TYPE_COMPANY" | translate }}
</div>
</div>
<app-toggle-selector
class="mb-4"
[options]="customerTypeOptions"
[selectedValue]="isCompany"
(selectionChange)="setCompanyMode($event)"
></app-toggle-selector>
<!-- Personal Name (Only if NOT Company) -->
<app-input
@@ -61,7 +53,7 @@
></app-input>
<!-- Company Fields (Only if Company) -->
<div *ngIf="isCompany" class="company-fields">
<div *ngIf="isCompany" class="ui-field-stack ui-field-stack--indented">
<app-input
formControlName="companyName"
[label]="('CONTACT.COMPANY_NAME' | translate) + ' *'"
@@ -74,20 +66,24 @@
></app-input>
</div>
<div class="form-group">
<label>{{ "CONTACT.LABEL_MESSAGE" | translate }}</label>
<div class="ui-form-group">
<label class="ui-form-label">{{
"CONTACT.LABEL_MESSAGE" | translate
}}</label>
<textarea
formControlName="message"
class="form-control"
class="ui-form-control"
rows="4"
></textarea>
</div>
<!-- File Upload Section -->
<div class="form-group">
<label>{{ "CONTACT.UPLOAD_LABEL" | translate }}</label>
<p class="hint">{{ "CONTACT.UPLOAD_HINT" | translate }}</p>
<p class="hint upload-privacy-note">
<div class="ui-form-group">
<label class="ui-form-label">{{
"CONTACT.UPLOAD_LABEL" | translate
}}</label>
<p class="ui-form-hint">{{ "CONTACT.UPLOAD_HINT" | translate }}</p>
<p class="ui-form-hint upload-privacy-note">
{{ "LEGAL.CONSENT.UPLOAD_NOTICE_PREFIX" | translate }}
<a href="/privacy" target="_blank" rel="noopener">{{
"LEGAL.CONSENT.UPLOAD_NOTICE_LINK" | translate
@@ -160,9 +156,9 @@
</div>
<div class="legal-consent">
<label class="checkbox-container">
<label class="ui-checkbox">
<input type="checkbox" formControlName="acceptLegal" />
<span class="checkmark"></span>
<span class="ui-checkbox__mark"></span>
<span>
{{ "LEGAL.CONSENT.LABEL_PREFIX" | translate }}
<a href="/terms" target="_blank" rel="noopener">{{
@@ -185,7 +181,7 @@
</div>
</div>
<div class="actions">
<div class="ui-actions ui-actions--end">
<app-button type="submit" [disabled]="form.invalid || sent()">
{{
sent()

View File

@@ -1,109 +1,12 @@
.form-group {
display: flex;
flex-direction: column;
margin-bottom: var(--space-4);
}
label {
font-size: 0.875rem;
font-weight: 500;
margin-bottom: var(--space-2);
color: var(--color-text);
}
.hint {
font-size: 0.75rem;
color: var(--color-text-muted);
margin-bottom: var(--space-2);
}
.upload-privacy-note {
margin-top: calc(var(--space-2) * -1);
font-size: 0.78rem;
}
.form-control {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
width: 100%;
background: var(--color-bg-card);
color: var(--color-text);
font-family: inherit;
&:focus {
outline: none;
border-color: var(--color-brand);
}
}
select.form-control {
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 1rem center;
background-size: 1em;
}
.row {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin-bottom: var(--space-4);
@media (min-width: 768px) {
flex-direction: row;
.col {
flex: 1;
margin-bottom: 0;
}
}
}
app-input.col {
width: 100%;
}
/* User Type Selector Styles */
.user-type-selector {
display: flex;
background-color: var(--color-neutral-100);
border-radius: var(--radius-md);
padding: 4px;
margin-bottom: var(--space-4);
gap: 4px;
width: 100%; /* Full width */
max-width: 400px; /* Limit on desktop */
}
.type-option {
flex: 1; /* Equal width */
text-align: center;
padding: 8px 16px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-muted);
transition: all 0.2s ease;
user-select: none;
&:hover {
color: var(--color-text);
}
&.selected {
background-color: var(--color-brand);
color: #000;
font-weight: 600;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
}
.company-fields {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding-left: var(--space-4);
border-left: 2px solid var(--color-border);
margin-bottom: var(--space-4);
}
/* File Upload Styles */
.drop-zone {
border: 2px dashed var(--color-border);
@@ -204,66 +107,6 @@ app-input.col {
.legal-consent {
margin: var(--space-4) 0 var(--space-4);
}
.checkbox-container {
display: flex;
align-items: center;
position: relative;
padding-left: 36px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
user-select: none;
color: var(--color-text);
line-height: 1.4;
input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
&:checked ~ .checkmark {
background-color: var(--color-brand);
border-color: var(--color-brand);
&:after {
display: block;
}
}
}
.checkmark {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
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;
&:after {
content: "";
position: absolute;
display: none;
left: 7px;
top: 3px;
width: 6px;
height: 12px;
border: solid #000;
border-width: 0 2.5px 2.5px 0;
transform: rotate(45deg);
}
}
&:hover input ~ .checkmark {
border-color: var(--color-brand);
}
a {
color: var(--color-brand);
@@ -273,8 +116,8 @@ app-input.col {
.consent-error {
margin-top: var(--space-2);
margin-left: 36px;
color: var(--color-danger-500, #ef4444);
margin-left: 2.1rem;
color: var(--color-danger-500);
font-size: 0.9rem;
}

View File

@@ -9,6 +9,10 @@ import {
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { AppInputComponent } from '../../../../shared/components/app-input/app-input.component';
import { AppButtonComponent } from '../../../../shared/components/app-button/app-button.component';
import {
AppToggleSelectorComponent,
ToggleOption,
} from '../../../../shared/components/app-toggle-selector/app-toggle-selector.component';
import { QuoteEstimatorService } from '../../../calculator/services/quote-estimator.service';
import { QuoteRequestService } from '../../../../core/services/quote-request.service';
import { LanguageService } from '../../../../core/services/language.service';
@@ -30,6 +34,7 @@ import { SuccessStateComponent } from '../../../../shared/components/success-sta
TranslateModule,
AppInputComponent,
AppButtonComponent,
AppToggleSelectorComponent,
SuccessStateComponent,
],
templateUrl: './contact-form.component.html',
@@ -52,6 +57,10 @@ export class ContactFormComponent implements OnDestroy {
{ value: 'consult', label: 'CONTACT.REQ_TYPE_CONSULT' },
{ value: 'question', label: 'CONTACT.REQ_TYPE_QUESTION' },
];
customerTypeOptions: ToggleOption[] = [
{ label: 'CONTACT.TYPE_PRIVATE', value: false },
{ label: 'CONTACT.TYPE_COMPANY', value: true },
];
private quoteRequestService = inject(QuoteRequestService);
private languageService = inject(LanguageService);

View File

@@ -1,5 +1,5 @@
<div class="container hero">
<h1>
<div class="container ui-page-hero ui-page-hero--spacious">
<h1 class="ui-page-title">
{{ "TRACKING.TITLE" | translate }}
<ng-container *ngIf="order()">
<br /><span class="order-id-title"
@@ -7,7 +7,7 @@
>
</ng-container>
</h1>
<p class="subtitle">{{ "TRACKING.SUBTITLE" | translate }}</p>
<p class="ui-page-subtitle subtitle">{{ "TRACKING.SUBTITLE" | translate }}</p>
</div>
<div class="container">
@@ -69,18 +69,18 @@
</div>
</app-card>
<div class="payment-layout">
<div class="payment-layout ui-two-column-layout">
<div class="payment-main">
<app-card class="mb-6">
<div class="card-header-simple">
<h3>{{ "PAYMENT.METHOD" | translate }}</h3>
<div class="ui-card-header">
<h3 class="ui-card-title">{{ "PAYMENT.METHOD" | translate }}</h3>
</div>
<div class="payment-selection">
<div class="methods-grid">
<div class="ui-choice-grid">
<div
class="type-option"
[class.selected]="selectedPaymentMethod === 'twint'"
class="ui-choice-card"
[class.is-selected]="selectedPaymentMethod === 'twint'"
(click)="selectPayment('twint')"
>
<span class="method-name">{{
@@ -88,8 +88,8 @@
}}</span>
</div>
<div
class="type-option"
[class.selected]="selectedPaymentMethod === 'bill'"
class="ui-choice-card"
[class.is-selected]="selectedPaymentMethod === 'bill'"
(click)="selectPayment('bill')"
>
<span class="method-name">{{
@@ -100,7 +100,7 @@
</div>
<div
class="payment-details fade-in text-center"
class="payment-details ui-soft-panel fade-in text-center"
*ngIf="selectedPaymentMethod === 'twint'"
>
<div class="details-header">
@@ -120,21 +120,12 @@
</p>
<div class="twint-mobile-action twint-button-container">
<button
style="
width: auto;
height: 58px;
border-radius: 6px;
display: flex;
justify-content: center;
cursor: pointer;
background-color: transparent;
border: none;
align-items: center;
"
type="button"
class="twint-launch-button"
(click)="openTwintPayment()"
>
<img
style="width: auto; height: 58px"
class="twint-launch-button__image"
[attr.alt]="'PAYMENT.TWINT_BUTTON_ALT' | translate"
[src]="getTwintButtonImageUrl()"
/>
@@ -148,7 +139,7 @@
</div>
<div
class="payment-details fade-in text-center"
class="payment-details ui-soft-panel fade-in text-center"
*ngIf="selectedPaymentMethod === 'bill'"
>
<div class="details-header">
@@ -167,7 +158,7 @@
</div>
</div>
<div class="actions">
<div class="ui-actions">
<app-button
variant="outline"
(click)="completeOrder()"
@@ -188,9 +179,13 @@
<div class="payment-summary">
<app-card class="sticky-card">
<div class="card-header-simple">
<h3>{{ "PAYMENT.SUMMARY_TITLE" | translate }}</h3>
<p class="order-id">#{{ getDisplayOrderNumber(o) }}</p>
<div class="ui-card-header">
<h3 class="ui-card-title">
{{ "PAYMENT.SUMMARY_TITLE" | translate }}
</h3>
<p class="ui-card-subtitle order-id">
#{{ getDisplayOrderNumber(o) }}
</p>
</div>
<app-price-breakdown

View File

@@ -1,99 +1,13 @@
.hero {
padding: var(--space-12) 0 var(--space-8);
text-align: center;
h1 {
font-size: 2.5rem;
margin-bottom: var(--space-2);
}
}
.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);
&.text-center {
text-align: center;
@@ -127,6 +41,23 @@
}
}
.twint-launch-button {
width: auto;
height: 58px;
border: none;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
cursor: pointer;
}
.twint-launch-button__image {
width: auto;
height: 58px;
}
.qr-placeholder {
display: flex;
flex-direction: column;
@@ -184,10 +115,6 @@
top: var(--space-6);
}
.actions {
margin-top: var(--space-8);
}
.fade-in {
animation: fadeIn 0.4s ease-out;
}

View File

@@ -60,3 +60,20 @@
color: var(--color-text);
}
}
.btn-ghost {
background-color: var(--color-bg-card);
border-color: var(--color-border);
color: var(--color-text);
&:hover:not(:disabled) {
background-color: var(--color-surface-muted);
}
}
.btn-danger {
background-color: var(--color-danger-500);
color: #fff;
&:hover:not(:disabled) {
background-color: #dc2626;
}
}

View File

@@ -9,7 +9,9 @@ import { CommonModule } from '@angular/common';
styleUrl: './app-button.component.scss',
})
export class AppButtonComponent {
variant = input<'primary' | 'secondary' | 'outline' | 'text'>('primary');
variant = input<
'primary' | 'secondary' | 'outline' | 'text' | 'ghost' | 'danger'
>('primary');
type = input<'button' | 'submit' | 'reset'>('button');
disabled = input<boolean>(false);
fullWidth = input<boolean>(false);

View File

@@ -1,3 +1,15 @@
<div class="card">
@if (title() || subtitle()) {
<header class="card-header">
@if (title()) {
<h3 class="card-title">{{ title() }}</h3>
}
@if (subtitle()) {
<p class="card-subtitle">{{ subtitle() }}</p>
}
</header>
}
<div class="card-body">
<ng-content></ng-content>
</div>
</div>

View File

@@ -8,7 +8,6 @@
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-sm);
padding: var(--space-6);
transition:
box-shadow 0.2s ease,
transform 0.2s ease,
@@ -22,3 +21,23 @@
border-color: var(--color-neutral-300);
}
}
.card-header {
padding: var(--space-6) var(--space-6) var(--space-4);
border-bottom: 1px solid var(--color-border);
}
.card-title {
margin: 0;
font-size: 1.2rem;
}
.card-subtitle {
margin: var(--space-2) 0 0;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.card-body {
padding: var(--space-6);
}

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, input } from '@angular/core';
@Component({
selector: 'app-card',
@@ -6,4 +6,7 @@ import { Component } from '@angular/core';
templateUrl: './app-card.component.html',
styleUrl: './app-card.component.scss',
})
export class AppCardComponent {}
export class AppCardComponent {
title = input<string>('');
subtitle = input<string>('');
}

View File

@@ -14,20 +14,24 @@ label {
margin-left: 2px;
}
.form-control {
padding: 0.5rem 0.75rem;
padding: 0.625rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: 1rem;
width: 100%;
background: var(--color-bg-card);
color: var(--color-text);
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
background-color 0.2s ease;
&:focus {
outline: none;
border-color: var(--color-brand);
box-shadow: 0 0 0 2px rgba(250, 207, 10, 0.25);
box-shadow: var(--focus-ring);
}
&:disabled {
background: var(--color-neutral-100);
background: var(--color-surface-muted);
cursor: not-allowed;
}
}

View File

@@ -10,16 +10,20 @@ label {
color: var(--color-text);
}
.form-control {
padding: 0.5rem 0.75rem;
padding: 0.625rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: 1rem;
width: 100%;
background: var(--color-bg-card);
color: var(--color-text);
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
&:focus {
outline: none;
border-color: var(--color-brand);
box-shadow: var(--focus-ring);
}
}
.error-text {

View File

@@ -1,6 +1,7 @@
/* src/styles.scss */
@use "./styles/theme";
@use "./styles/patterns";
@use "./styles/ui";
/* Reset / Base */
*,

View File

@@ -0,0 +1,509 @@
.ui-page-hero {
padding: var(--space-8) 0;
text-align: center;
}
.ui-page-hero--spacious {
padding: var(--space-12) 0 var(--space-8);
}
.ui-page-title {
margin: 0 0 var(--space-2);
font-size: clamp(2rem, 4vw, 2.75rem);
}
.ui-page-subtitle {
max-width: 680px;
margin: 0 auto;
color: var(--color-text-muted);
font-size: 1.05rem;
}
.ui-two-column-layout {
--ui-sidebar-width: 400px;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(300px, var(--ui-sidebar-width));
gap: var(--space-8);
align-items: start;
margin-bottom: var(--space-12);
}
.ui-section-card {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: clamp(12px, 2vw, 24px);
box-shadow: var(--shadow-sm);
}
.ui-section-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-4);
margin-bottom: var(--space-5);
}
.ui-section-header__copy {
display: grid;
gap: var(--space-1);
}
.ui-section-header__title {
margin: 0;
font-size: 1.35rem;
}
.ui-section-header__description {
margin: 0;
color: var(--color-text-muted);
}
.ui-section-header__meta {
width: fit-content;
}
.ui-card-header {
margin-bottom: var(--space-6);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--color-border);
}
.ui-card-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text);
}
.ui-card-subtitle {
margin: var(--space-2) 0 0;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.ui-form-group {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.ui-form-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text);
}
.ui-form-hint {
margin: 0;
font-size: 0.78rem;
color: var(--color-text-muted);
}
.ui-form-control {
width: 100%;
padding: 0.625rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
color: var(--color-text);
font: inherit;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
background-color 0.2s ease;
}
.ui-form-control:focus {
outline: none;
border-color: var(--color-brand);
box-shadow: var(--focus-ring);
}
.ui-form-control:disabled {
background: var(--color-surface-muted);
cursor: not-allowed;
}
select.ui-form-control {
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23101820' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 0.9rem center;
background-size: 1rem;
padding-right: 2.5rem;
}
.ui-form-row {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin-bottom: var(--space-4);
}
.ui-form-row > * {
min-width: 0;
}
.ui-form-row--three {
display: grid;
grid-template-columns: 1.5fr 2fr 1fr;
}
.ui-field-stack {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin-bottom: var(--space-4);
}
.ui-field-stack--indented {
padding-left: var(--space-4);
border-left: 2px solid var(--color-border);
}
.ui-checkbox {
position: relative;
display: flex;
align-items: flex-start;
gap: 0.85rem;
cursor: pointer;
color: var(--color-text);
line-height: 1.45;
}
.ui-checkbox input {
position: absolute;
inset: 0 auto auto 0;
width: 1.5rem;
height: 1.5rem;
opacity: 0;
margin: 0;
}
.ui-checkbox__mark {
width: 1.25rem;
height: 1.25rem;
margin-top: 0.1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-bg-card);
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
transition:
background-color 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.ui-checkbox__mark::after {
content: "";
width: 0.4rem;
height: 0.72rem;
border: solid var(--color-neutral-900);
border-width: 0 2px 2px 0;
transform: rotate(45deg) scale(0);
transition: transform 0.16s ease;
}
.ui-checkbox input:focus-visible + .ui-checkbox__mark {
box-shadow: var(--focus-ring);
}
.ui-checkbox input:checked + .ui-checkbox__mark {
background: var(--color-brand);
border-color: var(--color-brand);
}
.ui-checkbox input:checked + .ui-checkbox__mark::after {
transform: rotate(45deg) scale(1);
}
.ui-soft-panel {
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface-muted);
}
.ui-choice-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-4);
}
.ui-choice-card {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-5);
display: flex;
align-items: center;
justify-content: center;
background: var(--color-bg-card);
color: var(--color-text-muted);
text-align: center;
font-weight: 600;
cursor: pointer;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
background-color 0.2s ease,
color 0.2s ease,
transform 0.2s ease;
}
.ui-choice-card:hover {
border-color: var(--color-brand);
color: var(--color-text);
transform: translateY(-1px);
}
.ui-choice-card.is-selected,
.ui-choice-card.selected {
border-color: var(--color-brand);
background: var(--color-surface-muted);
color: var(--color-neutral-900);
box-shadow: var(--shadow-sm);
}
.ui-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
margin-top: var(--space-6);
}
.ui-actions--end {
justify-content: flex-end;
}
.ui-actions--stack > * {
flex: 1 1 220px;
}
.ui-button {
border: 0;
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-4);
font: inherit;
font-weight: 600;
line-height: 1.2;
cursor: pointer;
transition:
background-color 0.2s ease,
border-color 0.2s ease,
color 0.2s ease,
opacity 0.2s ease;
background: var(--color-brand);
color: var(--color-neutral-900);
}
.ui-button:hover:not(:disabled) {
background: var(--color-brand-hover);
}
.ui-button:disabled {
opacity: 0.65;
cursor: default;
}
.ui-button--ghost {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
color: var(--color-text);
}
.ui-button--ghost:hover:not(:disabled) {
background: var(--color-surface-muted);
}
.ui-button--danger {
background: var(--color-danger-500);
color: #fff;
}
.ui-button--danger:hover:not(:disabled) {
background: #dc2626;
}
.ui-pill,
.ui-status-chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.45rem 0.7rem;
border-radius: 999px;
border: 1px solid var(--color-border);
background: var(--color-surface-muted);
color: var(--color-text-muted);
font-size: 0.78rem;
font-weight: 700;
line-height: 1;
}
.ui-status-chip--neutral {
background: var(--color-surface-muted);
}
.ui-status-chip--warning {
background: var(--color-surface-warning);
color: #854d0e;
border-color: #f5d66d;
}
.ui-status-chip--success {
background: var(--color-surface-success);
color: #166534;
border-color: #bbf7d0;
}
.ui-status-chip--danger {
background: var(--color-surface-danger);
color: #991b1b;
border-color: #fecaca;
}
.ui-split-workspace {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.9fr);
gap: var(--space-4);
align-items: start;
}
.ui-table-wrap {
overflow: auto;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-card);
max-height: 72vh;
}
.ui-data-table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
}
.ui-data-table thead {
background: var(--color-surface-muted);
}
.ui-data-table th,
.ui-data-table td {
text-align: left;
padding: var(--space-3);
border-bottom: 1px solid var(--color-border);
vertical-align: top;
font-size: 0.93rem;
}
.ui-data-table th {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--color-text-muted);
}
.ui-data-table tbody tr {
transition: background-color 0.15s ease;
}
.ui-data-table tbody tr:hover {
background: #fff9d9;
}
.ui-detail-panel {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4);
min-height: 500px;
}
.ui-detail-panel--empty {
display: grid;
align-content: center;
justify-items: center;
text-align: center;
color: var(--color-text-muted);
}
.ui-meta-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--space-2);
}
.ui-meta-item {
margin: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-3);
background: var(--color-surface-muted);
display: grid;
gap: 4px;
}
.ui-meta-item strong,
.ui-meta-item dt {
margin: 0;
font-size: 0.78rem;
font-weight: 700;
color: var(--color-text-muted);
}
.ui-meta-item span,
.ui-meta-item dd {
margin: 0;
overflow-wrap: anywhere;
}
.ui-code-pill {
display: inline-flex;
align-items: center;
padding: 3px 8px;
border: 1px solid var(--color-border);
border-radius: 7px;
background: var(--color-surface-muted);
color: var(--color-text);
font-size: 0.82rem;
overflow-wrap: anywhere;
}
@media (min-width: 768px) {
.ui-form-row {
flex-direction: row;
}
.ui-form-row > * {
flex: 1;
}
}
@media (max-width: 1024px) {
.ui-two-column-layout,
.ui-split-workspace {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.ui-form-row--three,
.ui-choice-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.ui-section-header {
flex-direction: column;
align-items: stretch;
}
.ui-page-hero {
padding: var(--space-6) 0;
}
}

View File

@@ -5,10 +5,17 @@
/* Semantic Colors - Theming Layer */
--color-bg: #faf9f6;
--color-bg-card: #ffffff;
--color-surface-card: var(--color-bg-card);
--color-surface-muted: var(--color-neutral-100);
--color-surface-success: var(--color-success-100);
--color-surface-warning: var(--color-warning-100);
--color-surface-danger: var(--color-danger-100);
--color-text: var(--color-neutral-900);
--color-text-main: var(--color-text);
--color-text-muted: var(--color-secondary-500);
--color-brand: var(--color-primary-500);
--color-brand-600: var(--color-primary-600);
--color-brand-hover: var(--color-primary-600);
--color-border: var(--color-neutral-200);
@@ -16,6 +23,7 @@
--color-success: var(--color-success-500);
--color-warning: var(--color-warning-500);
--color-error: var(--color-danger-500);
--focus-ring: 0 0 0 2px rgb(250 207 10 / 0.28);
/* Font */
--font-family-sans: "IBM Plex Sans", "Space Grotesk", sans-serif;

View File

@@ -9,13 +9,18 @@
--color-secondary-600: #514d43;
--color-success-500: #22c55e;
--color-success-100: #dcfce7;
--color-warning-500: #eab308;
--color-warning-100: #fef3c7;
--color-danger-500: #ef4444;
--color-danger-600: #dc2626;
--color-danger-100: #fee2e2;
--color-neutral-50: #ffffff;
--color-neutral-100: #efede7;
--color-neutral-200: #dedad1;
--color-neutral-300: #c6c1b6;
--color-neutral-400: #a8a396;
--color-neutral-800: #1f2933;
--color-neutral-900: #101820;