Add Pay again for unpaid orders and redesign admin modals.
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
Unpaid PayU confirmations can restart payment; initiate reuses the payment row. Admin modals now use the shared K-design shell. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
182557493a
commit
2786aba3ef
18 files changed
+750
-253
No files matched your search
@@ -223,6 +223,10 @@ export const CS_TRANSLATIONS: TranslationMap = {
|
|||||||
'orders.prev': 'Předchozí',
|
'orders.prev': 'Předchozí',
|
||||||
'orders.next': 'Další',
|
'orders.next': 'Další',
|
||||||
'orders.detail': 'Detail objednávky',
|
'orders.detail': 'Detail objednávky',
|
||||||
|
'orders.lines': 'Položky',
|
||||||
|
'orders.customerShipping': 'Zákazník a doručení',
|
||||||
|
'orders.guestCustomer': 'Host',
|
||||||
|
'orders.updateStatus': 'Uložit stav',
|
||||||
'orders.shipping': 'Doručení',
|
'orders.shipping': 'Doručení',
|
||||||
'orders.shipment': 'Zásilka',
|
'orders.shipment': 'Zásilka',
|
||||||
'orders.tracking': 'Sledovací číslo',
|
'orders.tracking': 'Sledovací číslo',
|
||||||
|
|||||||
@@ -223,6 +223,10 @@ export const DE_TRANSLATIONS: TranslationMap = {
|
|||||||
'orders.prev': 'Zurück',
|
'orders.prev': 'Zurück',
|
||||||
'orders.next': 'Weiter',
|
'orders.next': 'Weiter',
|
||||||
'orders.detail': 'Bestelldetails',
|
'orders.detail': 'Bestelldetails',
|
||||||
|
'orders.lines': 'Positionen',
|
||||||
|
'orders.customerShipping': 'Kunde & Versand',
|
||||||
|
'orders.guestCustomer': 'Gastkunde',
|
||||||
|
'orders.updateStatus': 'Status aktualisieren',
|
||||||
'orders.shipping': 'Versand',
|
'orders.shipping': 'Versand',
|
||||||
'orders.shipment': 'Sendung',
|
'orders.shipment': 'Sendung',
|
||||||
'orders.tracking': 'Sendungsnummer',
|
'orders.tracking': 'Sendungsnummer',
|
||||||
|
|||||||
@@ -221,6 +221,10 @@ export const EN_TRANSLATIONS: TranslationMap = {
|
|||||||
'orders.prev': 'Previous',
|
'orders.prev': 'Previous',
|
||||||
'orders.next': 'Next',
|
'orders.next': 'Next',
|
||||||
'orders.detail': 'Order details',
|
'orders.detail': 'Order details',
|
||||||
|
'orders.lines': 'Line items',
|
||||||
|
'orders.customerShipping': 'Customer & shipping',
|
||||||
|
'orders.guestCustomer': 'Guest customer',
|
||||||
|
'orders.updateStatus': 'Update status',
|
||||||
'orders.shipping': 'Shipping',
|
'orders.shipping': 'Shipping',
|
||||||
'orders.shipment': 'Shipment',
|
'orders.shipment': 'Shipment',
|
||||||
'orders.tracking': 'Tracking number',
|
'orders.tracking': 'Tracking number',
|
||||||
|
|||||||
@@ -221,6 +221,10 @@ export const PL_TRANSLATIONS: TranslationMap = {
|
|||||||
'orders.prev': 'Poprzednia',
|
'orders.prev': 'Poprzednia',
|
||||||
'orders.next': 'Następna',
|
'orders.next': 'Następna',
|
||||||
'orders.detail': 'Szczegóły zamówienia',
|
'orders.detail': 'Szczegóły zamówienia',
|
||||||
|
'orders.lines': 'Pozycje',
|
||||||
|
'orders.customerShipping': 'Klient i dostawa',
|
||||||
|
'orders.guestCustomer': 'Gość',
|
||||||
|
'orders.updateStatus': 'Zapisz status',
|
||||||
'orders.shipping': 'Dostawa',
|
'orders.shipping': 'Dostawa',
|
||||||
'orders.shipment': 'Przesyłka',
|
'orders.shipment': 'Przesyłka',
|
||||||
'orders.tracking': 'Numer śledzenia',
|
'orders.tracking': 'Numer śledzenia',
|
||||||
|
|||||||
@@ -1,115 +1,72 @@
|
|||||||
@if (open()) {
|
@if (open()) {
|
||||||
<div
|
<div
|
||||||
class="admin-modal fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 p-4"
|
class="admin-modal"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
(click)="handleBackdrop($event)"
|
(click)="handleBackdrop($event)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="admin-modal__panel my-4 w-full max-w-2xl bg-white shadow-lg"
|
class="admin-modal__panel admin-modal__panel--xl"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between border-b border-iled-grey-light px-6 py-4">
|
<div class="admin-modal__head">
|
||||||
<div>
|
<div style="min-width: 0">
|
||||||
<h2 class="text-lg font-bold">
|
<h1>
|
||||||
|
@if (detail(); as d) {
|
||||||
|
#{{ d.orderNumber }}
|
||||||
|
} @else {
|
||||||
{{ 'orders.detail' | translate }}
|
{{ 'orders.detail' | translate }}
|
||||||
@if (detail(); as d) {
|
|
||||||
<span class="font-normal text-iled-grey-dark">#{{ d.orderNumber }}</span>
|
|
||||||
}
|
}
|
||||||
</h2>
|
</h1>
|
||||||
@if (detail(); as d) {
|
@if (detail(); as d) {
|
||||||
<div style="margin-top: 6px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px">
|
<p class="page-purpose" style="margin: 0">
|
||||||
|
{{ d.createdAt | date: 'yyyy-MM-dd HH:mm' }}
|
||||||
|
@if (d.paymentProvider) {
|
||||||
|
· {{ d.paymentProvider }}
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<div class="admin-modal__meta">
|
||||||
|
<span class="badge" [class]="statusBadgeClass(d.status)">
|
||||||
|
{{ 'orders.status.' + d.status | translate }}
|
||||||
|
</span>
|
||||||
|
@if (d.paymentStatus) {
|
||||||
|
<span class="badge" [class]="paymentBadgeClass(d.paymentStatus)">
|
||||||
|
{{ 'orders.paymentStatus.' + d.paymentStatus | translate }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
<app-order-channel-icons
|
<app-order-channel-icons
|
||||||
[source]="d.source"
|
[source]="d.source"
|
||||||
[shippingCarrier]="d.shippingCarrier"
|
[shippingCarrier]="d.shippingCarrier"
|
||||||
[shippingServiceType]="d.shippingServiceType"
|
[shippingServiceType]="d.shippingServiceType"
|
||||||
[showLabels]="true"
|
[showLabels]="true"
|
||||||
[iconSize]="15"
|
[iconSize]="14"
|
||||||
/>
|
/>
|
||||||
@if (d.externalOrderId) {
|
@if (d.externalOrderId) {
|
||||||
<span class="cell-muted text-xs font-mono">
|
<span class="cell-muted" style="font-family: ui-monospace, monospace; font-size: 12px">
|
||||||
{{ d.externalOrderId }}
|
{{ d.externalOrderId }}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="page-actions">
|
||||||
|
@if (detail(); as d) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-iled-grey-dark hover:text-iled-black"
|
class="btn btn-secondary"
|
||||||
(click)="handleClose()"
|
(click)="handleInvoice()"
|
||||||
>
|
>
|
||||||
✕
|
{{ 'orders.invoice' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
@if (d.shipment?.labelRelativePath) {
|
||||||
|
|
||||||
@if (loading()) {
|
|
||||||
<div class="px-6 py-6">
|
|
||||||
<app-admin-skeleton variant="detail" [rows]="4" />
|
|
||||||
</div>
|
|
||||||
} @else if (detail(); as d) {
|
|
||||||
<div class="max-h-[70vh] space-y-5 overflow-y-auto px-6 py-4 text-sm">
|
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<h3 class="mb-2 font-semibold">{{ 'orders.customer' | translate }}</h3>
|
|
||||||
<p>{{ d.customerFirstName }} {{ d.customerLastName }}</p>
|
|
||||||
<p class="text-iled-grey-dark">{{ d.customerEmail }}</p>
|
|
||||||
<p class="text-iled-grey-dark">{{ d.customerPhone }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="mb-2 font-semibold">{{ 'orders.shipping' | translate }}</h3>
|
|
||||||
<p class="font-medium">
|
|
||||||
{{ 'orders.carrier.' + d.shippingCarrier | translate }}
|
|
||||||
@if (d.shippingServiceType === 'parcelLocker') {
|
|
||||||
({{ 'orders.service.parcelLocker' | translate }})
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
@if (d.parcelPointName) {
|
|
||||||
<p>{{ d.parcelPointName }}</p>
|
|
||||||
<p class="text-iled-grey-dark">{{ d.parcelPointAddress }}</p>
|
|
||||||
} @else {
|
|
||||||
<p>{{ d.shippingStreet }}</p>
|
|
||||||
<p>{{ d.shippingPostalCode }} {{ d.shippingCity }}</p>
|
|
||||||
<p>{{ d.shippingCountry }}</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded border border-iled-grey-light bg-iled-bg p-4">
|
|
||||||
<h3 class="mb-3 font-semibold">{{ 'orders.shipment' | translate }}</h3>
|
|
||||||
@if (d.shipment; as s) {
|
|
||||||
<div class="space-y-1 text-sm">
|
|
||||||
<p>
|
|
||||||
<span class="text-iled-grey-dark">{{ 'orders.tracking' | translate }}:</span>
|
|
||||||
<span class="ml-1 font-mono font-medium">{{ s.trackingNumber || '—' }}</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span class="text-iled-grey-dark">{{ 'orders.status' | translate }}:</span>
|
|
||||||
<span class="ml-1">{{ 'orders.shipmentStatus.' + s.status | translate }}</span>
|
|
||||||
</p>
|
|
||||||
@if (s.errorMessage) {
|
|
||||||
<p class="text-sm" style="color: var(--danger)">{{ s.errorMessage }}</p>
|
|
||||||
}
|
|
||||||
@if (s.labelRelativePath) {
|
|
||||||
<a
|
<a
|
||||||
[href]="labelUrl(s)"
|
[href]="labelUrl(d.shipment!)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="mt-2 inline-flex btn btn-secondary btn-sm"
|
class="btn btn-secondary"
|
||||||
>
|
>
|
||||||
{{ 'orders.downloadLabel' | translate }}
|
{{ 'orders.downloadLabel' | translate }}
|
||||||
</a>
|
</a>
|
||||||
} @else if (canGenerateLabel(d)) {
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm mt-16"
|
|
||||||
[disabled]="generatingLabel()"
|
|
||||||
(click)="handleGenerateLabel()"
|
|
||||||
>
|
|
||||||
{{ 'orders.generateLabel' | translate }}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
} @else if (canGenerateLabel(d)) {
|
} @else if (canGenerateLabel(d)) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -119,119 +76,106 @@
|
|||||||
>
|
>
|
||||||
{{ 'orders.generateLabel' | translate }}
|
{{ 'orders.generateLabel' | translate }}
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
|
||||||
<p class="text-sm text-iled-grey-dark">{{ 'orders.labelNotAvailable' | translate }}</p>
|
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4">
|
|
||||||
<div>
|
|
||||||
<span class="text-iled-grey-dark">{{ 'orders.status' | translate }}:</span>
|
|
||||||
<span class="ml-1 font-medium">{{ 'orders.status.' + d.status | translate }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-iled-grey-dark">{{ 'orders.payment' | translate }}:</span>
|
|
||||||
@if (d.paymentStatus) {
|
|
||||||
<span class="ml-1 font-medium">
|
|
||||||
{{ 'orders.paymentStatus.' + d.paymentStatus | translate }}
|
|
||||||
</span>
|
|
||||||
} @else {
|
|
||||||
<span class="ml-1">—</span>
|
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
@if (d.paymentProvider) {
|
|
||||||
<div>
|
|
||||||
<span class="text-iled-grey-dark">{{ 'orders.provider' | translate }}:</span>
|
|
||||||
<span class="ml-1">{{ d.paymentProvider }}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-end gap-3">
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-xs font-medium text-iled-grey-dark">
|
|
||||||
{{ 'orders.changeStatus' | translate }}
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
[(ngModel)]="selectedStatus"
|
|
||||||
name="orderStatus"
|
|
||||||
class="border border-iled-grey-light px-3 py-2 text-sm"
|
|
||||||
>
|
|
||||||
@for (s of statuses; track s) {
|
|
||||||
<option [ngValue]="s">{{ 'orders.status.' + s | translate }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bg-iled-black px-4 py-2 text-sm font-semibold text-white"
|
class="admin-modal__close"
|
||||||
(click)="handleSaveStatus()"
|
(click)="handleClose()"
|
||||||
|
[attr.aria-label]="'common.close' | translate"
|
||||||
>
|
>
|
||||||
{{ 'products.save' | translate }}
|
✕
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="border border-iled-grey-light px-4 py-2 text-sm"
|
|
||||||
(click)="handleInvoice()"
|
|
||||||
>
|
|
||||||
{{ 'orders.invoice' | translate }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="w-full text-left text-sm">
|
@if (loading()) {
|
||||||
<thead class="border-b border-iled-grey-light bg-iled-bg">
|
<div class="admin-modal__body">
|
||||||
|
<app-admin-skeleton variant="detail" [rows]="5" />
|
||||||
|
</div>
|
||||||
|
} @else if (detail(); as d) {
|
||||||
|
<div class="admin-modal__body">
|
||||||
|
<div class="split">
|
||||||
|
<div class="stack" style="gap: 16px">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h2>{{ 'orders.lines' | translate }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="data">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-3 py-2">{{ 'products.name' | translate }}</th>
|
<th>{{ 'products.name' | translate }}</th>
|
||||||
<th class="px-3 py-2">SKU</th>
|
<th>{{ 'orders.qty' | translate }}</th>
|
||||||
<th class="px-3 py-2">{{ 'orders.qty' | translate }}</th>
|
<th>{{ 'products.price' | translate }}</th>
|
||||||
<th class="px-3 py-2">{{ 'products.price' | translate }}</th>
|
|
||||||
@if (canPick()) {
|
@if (canPick()) {
|
||||||
<th class="px-3 py-2">{{ 'orders.pickFrom' | translate }}</th>
|
<th>{{ 'orders.pickFrom' | translate }}</th>
|
||||||
<th class="px-3 py-2">{{ 'orders.status' | translate }}</th>
|
<th></th>
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (line of d.lines; track line.id) {
|
@for (line of d.lines; track line.id) {
|
||||||
<tr class="border-b border-iled-grey-light" [class.bg-green-50]="line.isCollected">
|
<tr [class.is-collected]="line.isCollected">
|
||||||
<td class="px-3 py-2">
|
<td>
|
||||||
<div>{{ line.productName }}</div>
|
<div class="row-product">
|
||||||
<div class="text-xs text-iled-grey-dark">{{ line.variantColor }}</div>
|
<div>
|
||||||
|
<div class="cell-strong">{{ line.productName }}</div>
|
||||||
|
<div class="cell-muted">
|
||||||
|
{{ line.sku }}
|
||||||
|
@if (line.variantColor) {
|
||||||
|
· {{ line.variantColor }}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ line.quantity }}</td>
|
||||||
|
<td class="cell-strong">
|
||||||
|
{{ line.lineTotal | i18nCurrency }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 font-mono text-xs">{{ line.sku }}</td>
|
|
||||||
<td class="px-3 py-2">{{ line.quantity }}</td>
|
|
||||||
<td class="px-3 py-2">{{ line.lineTotal | i18nCurrency }}</td>
|
|
||||||
@if (canPick()) {
|
@if (canPick()) {
|
||||||
<td class="px-3 py-2">
|
<td>
|
||||||
@if (!line.isCollected) {
|
@if (!line.isCollected) {
|
||||||
<select
|
<select
|
||||||
class="border border-iled-grey-light px-2 py-1 text-xs"
|
class="select"
|
||||||
|
style="height: 32px; min-width: 140px"
|
||||||
[value]="getWarehouse(line.id)"
|
[value]="getWarehouse(line.id)"
|
||||||
(change)="setWarehouse(line.id, $any($event.target).value)"
|
(change)="
|
||||||
|
setWarehouse(
|
||||||
|
line.id,
|
||||||
|
$any($event.target).value
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<option value="">{{ 'orders.selectWarehouse' | translate }}</option>
|
<option value="">
|
||||||
@for (ws of line.warehouseStock; track ws.warehouseId) {
|
{{ 'orders.selectWarehouse' | translate }}
|
||||||
|
</option>
|
||||||
|
@for (
|
||||||
|
ws of line.warehouseStock;
|
||||||
|
track ws.warehouseId
|
||||||
|
) {
|
||||||
<option [value]="ws.warehouseId">
|
<option [value]="ws.warehouseId">
|
||||||
{{ ws.warehouseName }} ({{ ws.quantity }})
|
{{ ws.warehouseName }} ({{ ws.quantity }})
|
||||||
</option>
|
</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
} @else if (line.pickedFromWarehouseName) {
|
} @else if (line.pickedFromWarehouseName) {
|
||||||
<span class="text-xs">
|
<span class="cell-muted">
|
||||||
{{ line.pickedFromWarehouseName }}
|
{{ line.pickedFromWarehouseName }}
|
||||||
({{ line.pickedFromWarehouseCode }})
|
({{ line.pickedFromWarehouseCode }})
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2">
|
<td>
|
||||||
@if (line.isCollected) {
|
@if (line.isCollected) {
|
||||||
<span class="rounded bg-green-100 px-2 py-0.5 text-xs text-green-800">
|
<span class="badge badge-success">
|
||||||
{{ 'orders.collected' | translate }}
|
{{ 'orders.collected' | translate }}
|
||||||
</span>
|
</span>
|
||||||
} @else {
|
} @else {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bg-iled-black px-2 py-1 text-xs text-white disabled:opacity-40"
|
class="btn btn-secondary btn-sm"
|
||||||
[disabled]="!canCollectLine(line) || collecting()"
|
[disabled]="!canCollectLine(line) || collecting()"
|
||||||
(click)="handleCollectLine(line)"
|
(click)="handleCollectLine(line)"
|
||||||
>
|
>
|
||||||
@@ -244,20 +188,54 @@
|
|||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="totals">
|
||||||
|
<div class="totals__row">
|
||||||
|
<span>{{ 'orders.subtotal' | translate }}</span>
|
||||||
|
<span>{{ d.subtotal | i18nCurrency }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="totals__row">
|
||||||
|
<span>{{ 'orders.delivery' | translate }}</span>
|
||||||
|
<span>{{ d.deliveryCost | i18nCurrency }}</span>
|
||||||
|
</div>
|
||||||
|
@if (d.discountAmount > 0) {
|
||||||
|
<div class="totals__row">
|
||||||
|
<span>{{ 'orders.discount' | translate }}</span>
|
||||||
|
<span>-{{ d.discountAmount | i18nCurrency }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="totals__row">
|
||||||
|
<span>{{ 'orders.vat' | translate }}</span>
|
||||||
|
<span>{{ d.vatAmount | i18nCurrency }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="totals__row totals__row--total">
|
||||||
|
<span>{{ 'orders.total' | translate }}</span>
|
||||||
|
<span>{{ d.total | i18nCurrency }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (canPick() && pendingCount(d) > 0) {
|
@if (canPick() && pendingCount(d) > 0) {
|
||||||
<div class="flex flex-wrap items-end gap-3 rounded border border-iled-grey-light bg-iled-bg p-4">
|
<div class="card">
|
||||||
<div>
|
<div class="card-head">
|
||||||
<label class="mb-1 block text-xs font-medium text-iled-grey-dark">
|
<h2>{{ 'orders.orderWarehouse' | translate }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-pad">
|
||||||
|
<div class="flex items-end gap-8" style="flex-wrap: wrap">
|
||||||
|
<div class="field" style="flex: 1; min-width: 180px">
|
||||||
|
<label for="order-wh">
|
||||||
{{ 'orders.orderWarehouse' | translate }}
|
{{ 'orders.orderWarehouse' | translate }}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
class="border border-iled-grey-light px-3 py-2 text-sm"
|
id="order-wh"
|
||||||
|
class="select"
|
||||||
[ngModel]="orderWarehouse"
|
[ngModel]="orderWarehouse"
|
||||||
name="orderWarehouse"
|
name="orderWarehouse"
|
||||||
(ngModelChange)="setOrderWarehouse($event)"
|
(ngModelChange)="setOrderWarehouse($event)"
|
||||||
>
|
>
|
||||||
<option value="">{{ 'orders.selectWarehouse' | translate }}</option>
|
<option value="">
|
||||||
|
{{ 'orders.selectWarehouse' | translate }}
|
||||||
|
</option>
|
||||||
@for (ws of warehouseOptions(d); track ws.warehouseId) {
|
@for (ws of warehouseOptions(d); track ws.warehouseId) {
|
||||||
<option [ngValue]="ws.warehouseId">
|
<option [ngValue]="ws.warehouseId">
|
||||||
{{ ws.warehouseName }} ({{ ws.warehouseCode }})
|
{{ ws.warehouseName }} ({{ ws.warehouseCode }})
|
||||||
@@ -267,24 +245,155 @@
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bg-iled-primary px-4 py-2 text-sm font-semibold text-iled-black disabled:opacity-40"
|
class="btn btn-primary"
|
||||||
[disabled]="!canCollectAll(d) || collecting()"
|
[disabled]="!canCollectAll(d) || collecting()"
|
||||||
(click)="handleCollectOrder()"
|
(click)="handleCollectOrder()"
|
||||||
>
|
>
|
||||||
{{ 'orders.collectOrder' | translate }}
|
{{ 'orders.collectOrder' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1 text-right">
|
<div class="stack" style="gap: 16px">
|
||||||
<div>{{ 'orders.subtotal' | translate }}: {{ d.subtotal | i18nCurrency }}</div>
|
<div class="card">
|
||||||
<div>{{ 'orders.delivery' | translate }}: {{ d.deliveryCost | i18nCurrency }}</div>
|
<div class="card-head">
|
||||||
@if (d.discountAmount > 0) {
|
<h2>{{ 'orders.status' | translate }}</h2>
|
||||||
<div>{{ 'orders.discount' | translate }}: -{{ d.discountAmount | i18nCurrency }}</div>
|
</div>
|
||||||
|
<div class="card-pad stack" style="gap: 12px">
|
||||||
|
<div class="field">
|
||||||
|
<label for="order-status">
|
||||||
|
{{ 'orders.changeStatus' | translate }}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="order-status"
|
||||||
|
class="select"
|
||||||
|
[(ngModel)]="selectedStatus"
|
||||||
|
name="orderStatus"
|
||||||
|
>
|
||||||
|
@for (s of statuses; track s) {
|
||||||
|
<option [ngValue]="s">
|
||||||
|
{{ 'orders.status.' + s | translate }}
|
||||||
|
</option>
|
||||||
}
|
}
|
||||||
<div>{{ 'orders.vat' | translate }}: {{ d.vatAmount | i18nCurrency }}</div>
|
</select>
|
||||||
<div class="text-lg font-bold">
|
</div>
|
||||||
{{ 'orders.total' | translate }}: {{ d.total | i18nCurrency }}
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
(click)="handleSaveStatus()"
|
||||||
|
>
|
||||||
|
{{ 'orders.updateStatus' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h2>{{ 'orders.customerShipping' | translate }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="section-block">
|
||||||
|
<div class="cell-strong">
|
||||||
|
@if (d.customerFirstName || d.customerLastName) {
|
||||||
|
{{ d.customerFirstName }} {{ d.customerLastName }}
|
||||||
|
} @else {
|
||||||
|
{{ 'orders.guestCustomer' | translate }}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (d.customerEmail) {
|
||||||
|
<div class="cell-muted">{{ d.customerEmail }}</div>
|
||||||
|
}
|
||||||
|
@if (d.customerPhone) {
|
||||||
|
<div class="cell-muted">{{ d.customerPhone }}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="section-block">
|
||||||
|
<h3>{{ 'orders.shipping' | translate }}</h3>
|
||||||
|
<div class="flex items-center gap-8" style="margin-bottom: 8px">
|
||||||
|
<app-order-channel-icons
|
||||||
|
[source]="d.source"
|
||||||
|
[shippingCarrier]="d.shippingCarrier"
|
||||||
|
[shippingServiceType]="d.shippingServiceType"
|
||||||
|
[showDelivery]="true"
|
||||||
|
[showLabels]="false"
|
||||||
|
[iconSize]="14"
|
||||||
|
/>
|
||||||
|
<span class="cell-strong">
|
||||||
|
{{ 'orders.carrier.' + d.shippingCarrier | translate }}
|
||||||
|
·
|
||||||
|
{{ 'orders.service.' + d.shippingServiceType | translate }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if (d.parcelPointName) {
|
||||||
|
<div>{{ d.parcelPointName }}</div>
|
||||||
|
<div class="cell-muted">{{ d.parcelPointAddress }}</div>
|
||||||
|
} @else {
|
||||||
|
<div>{{ d.shippingStreet }}</div>
|
||||||
|
<div class="cell-muted">
|
||||||
|
{{ d.shippingPostalCode }} {{ d.shippingCity }}
|
||||||
|
</div>
|
||||||
|
<div class="cell-muted">{{ d.shippingCountry }}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="section-block">
|
||||||
|
<h3>{{ 'orders.shipment' | translate }}</h3>
|
||||||
|
@if (d.shipment; as s) {
|
||||||
|
<div class="stack" style="gap: 6px">
|
||||||
|
<div>
|
||||||
|
<span class="cell-muted">{{ 'orders.tracking' | translate }}:</span>
|
||||||
|
<span class="cell-strong" style="margin-left: 6px; font-family: ui-monospace, monospace">
|
||||||
|
{{ s.trackingNumber || '—' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="cell-muted">{{ 'orders.status' | translate }}:</span>
|
||||||
|
<span style="margin-left: 6px">
|
||||||
|
{{ 'orders.shipmentStatus.' + s.status | translate }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if (s.errorMessage) {
|
||||||
|
<p class="form-error" style="margin: 0">{{ s.errorMessage }}</p>
|
||||||
|
}
|
||||||
|
<div class="flex gap-8" style="margin-top: 8px; flex-wrap: wrap">
|
||||||
|
@if (s.labelRelativePath) {
|
||||||
|
<a
|
||||||
|
[href]="labelUrl(s)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
>
|
||||||
|
{{ 'orders.downloadLabel' | translate }}
|
||||||
|
</a>
|
||||||
|
} @else if (canGenerateLabel(d)) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
[disabled]="generatingLabel()"
|
||||||
|
(click)="handleGenerateLabel()"
|
||||||
|
>
|
||||||
|
{{ 'orders.generateLabel' | translate }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else if (canGenerateLabel(d)) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
[disabled]="generatingLabel()"
|
||||||
|
(click)="handleGenerateLabel()"
|
||||||
|
>
|
||||||
|
{{ 'orders.generateLabel' | translate }}
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<div class="cell-muted">
|
||||||
|
{{ 'orders.labelNotAvailable' | translate }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DatePipe } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
AdminOrderDetailLine,
|
AdminOrderDetailLine,
|
||||||
ORDER_STATUSES,
|
ORDER_STATUSES,
|
||||||
OrderStatus,
|
OrderStatus,
|
||||||
|
PaymentStatus,
|
||||||
ShipmentInfo,
|
ShipmentInfo,
|
||||||
} from './order.models';
|
} from './order.models';
|
||||||
import {
|
import {
|
||||||
@@ -37,6 +39,7 @@ import {
|
|||||||
selector: 'app-order-detail-modal',
|
selector: 'app-order-detail-modal',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
|
DatePipe,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
TranslatePipe,
|
TranslatePipe,
|
||||||
I18nCurrencyPipe,
|
I18nCurrencyPipe,
|
||||||
@@ -299,6 +302,38 @@ export class OrderDetailModalComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusBadgeClass(status: OrderStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'paid':
|
||||||
|
case 'delivered':
|
||||||
|
return 'badge-success';
|
||||||
|
case 'processing':
|
||||||
|
case 'shipped':
|
||||||
|
return 'badge-info';
|
||||||
|
case 'pendingPayment':
|
||||||
|
return 'badge-warning';
|
||||||
|
case 'cancelled':
|
||||||
|
case 'refunded':
|
||||||
|
return 'badge-danger';
|
||||||
|
default:
|
||||||
|
return 'badge-neutral';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentBadgeClass(status: PaymentStatus | string): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed':
|
||||||
|
return 'badge-success';
|
||||||
|
case 'pending':
|
||||||
|
return 'badge-warning';
|
||||||
|
case 'failed':
|
||||||
|
case 'refunded':
|
||||||
|
return 'badge-danger';
|
||||||
|
default:
|
||||||
|
return 'badge-neutral';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#toPickLine(detail: AdminOrderDetail, line: AdminOrderDetailLine) {
|
#toPickLine(detail: AdminOrderDetail, line: AdminOrderDetailLine) {
|
||||||
return {
|
return {
|
||||||
orderLineId: line.id,
|
orderLineId: line.id,
|
||||||
|
|||||||
@@ -1,97 +1,129 @@
|
|||||||
@if (open()) {
|
@if (open()) {
|
||||||
<div
|
<div
|
||||||
class="admin-modal invoice-backdrop fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 p-4 print:relative print:inset-auto print:block print:bg-white print:p-0"
|
class="admin-modal invoice-backdrop"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
(click)="handleBackdrop($event)"
|
(click)="handleBackdrop($event)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="invoice-print-area"
|
id="invoice-print-area"
|
||||||
class="admin-modal__panel invoice-panel my-4 w-full max-w-2xl bg-white p-8 shadow-lg print:my-0 print:max-w-none print:shadow-none"
|
class="admin-modal__panel admin-modal__panel--lg invoice-panel"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<div class="mb-6 flex items-start justify-between print:hidden">
|
<div class="admin-modal__head print:hidden">
|
||||||
<h2 class="text-lg font-bold">{{ 'orders.invoice' | translate }}</h2>
|
<div>
|
||||||
<div class="flex gap-2">
|
<h2>{{ 'orders.invoice' | translate }}</h2>
|
||||||
|
@if (invoice(); as inv) {
|
||||||
|
<p class="page-purpose" style="margin: 0">
|
||||||
|
{{ inv.invoiceNumber }}
|
||||||
|
· {{ inv.issueDate | date: 'yyyy-MM-dd' }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="page-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bg-iled-primary px-4 py-2 text-sm font-semibold"
|
class="btn btn-primary"
|
||||||
(click)="handlePrint()"
|
(click)="handlePrint()"
|
||||||
>
|
>
|
||||||
{{ 'orders.print' | translate }}
|
{{ 'orders.print' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="px-3 text-iled-grey-dark" (click)="handleClose()">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="admin-modal__close"
|
||||||
|
(click)="handleClose()"
|
||||||
|
[attr.aria-label]="'common.close' | translate"
|
||||||
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-modal__body">
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
<app-admin-skeleton variant="detail" [rows]="5" />
|
<app-admin-skeleton variant="detail" [rows]="5" />
|
||||||
} @else if (invoice(); as inv) {
|
} @else if (invoice(); as inv) {
|
||||||
<header class="mb-8 border-b border-iled-grey-light pb-6">
|
<header class="section-block" style="padding-top: 0">
|
||||||
<h1 class="text-2xl font-bold">{{ 'orders.invoice' | translate }}</h1>
|
<h1 style="margin: 0 0 6px; font-size: 22px">
|
||||||
<p class="mt-1 text-sm text-iled-grey-dark">{{ inv.invoiceNumber }}</p>
|
{{ 'orders.invoice' | translate }}
|
||||||
<p class="text-sm text-iled-grey-dark">
|
</h1>
|
||||||
|
<div class="cell-muted">{{ inv.invoiceNumber }}</div>
|
||||||
|
<div class="cell-muted">
|
||||||
{{ 'orders.issueDate' | translate }}:
|
{{ 'orders.issueDate' | translate }}:
|
||||||
{{ inv.issueDate | date: 'yyyy-MM-dd' }}
|
{{ inv.issueDate | date: 'yyyy-MM-dd' }}
|
||||||
</p>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="mb-8 grid gap-6 sm:grid-cols-2 text-sm">
|
<div class="grid grid-2" style="margin: 8px 0 20px">
|
||||||
<div>
|
<div class="section-block" style="border: 0; padding: 0">
|
||||||
<h3 class="mb-2 font-semibold uppercase text-iled-grey-dark">
|
<h3>{{ 'orders.seller' | translate }}</h3>
|
||||||
{{ 'orders.seller' | translate }}
|
<div class="cell-strong">{{ inv.sellerName }}</div>
|
||||||
</h3>
|
<div class="cell-muted">{{ inv.sellerAddress }}</div>
|
||||||
<p class="font-medium">{{ inv.sellerName }}</p>
|
<div class="cell-muted">NIP: {{ inv.sellerTaxId }}</div>
|
||||||
<p>{{ inv.sellerAddress }}</p>
|
|
||||||
<p>NIP: {{ inv.sellerTaxId }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="section-block" style="border: 0; padding: 0">
|
||||||
<h3 class="mb-2 font-semibold uppercase text-iled-grey-dark">
|
<h3>{{ 'orders.buyer' | translate }}</h3>
|
||||||
{{ 'orders.buyer' | translate }}
|
<div class="cell-strong">{{ inv.buyerName }}</div>
|
||||||
</h3>
|
<div class="cell-muted">{{ inv.buyerEmail }}</div>
|
||||||
<p class="font-medium">{{ inv.buyerName }}</p>
|
<div class="cell-muted">{{ inv.buyerAddress }}</div>
|
||||||
<p>{{ inv.buyerEmail }}</p>
|
|
||||||
<p>{{ inv.buyerAddress }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="mb-6 w-full text-left text-sm">
|
<div class="card" style="box-shadow: none">
|
||||||
<thead class="border-b-2 border-iled-black">
|
<div class="table-wrap">
|
||||||
|
<table class="data">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="py-2">{{ 'products.name' | translate }}</th>
|
<th>{{ 'products.name' | translate }}</th>
|
||||||
<th class="py-2">{{ 'orders.qty' | translate }}</th>
|
<th>{{ 'orders.qty' | translate }}</th>
|
||||||
<th class="py-2 text-right">{{ 'products.price' | translate }}</th>
|
<th style="text-align: right">
|
||||||
|
{{ 'products.price' | translate }}
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (line of inv.lines; track line.sku) {
|
@for (line of inv.lines; track line.sku) {
|
||||||
<tr class="border-b border-iled-grey-light">
|
<tr>
|
||||||
<td class="py-2">
|
<td>
|
||||||
{{ line.productName }}
|
<div class="cell-strong">{{ line.productName }}</div>
|
||||||
<span class="text-iled-grey-dark">({{ line.variantColor }})</span>
|
<div class="cell-muted">{{ line.variantColor }}</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ line.quantity }}</td>
|
||||||
|
<td style="text-align: right" class="cell-strong">
|
||||||
|
{{ line.lineTotal | i18nCurrency }}
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2">{{ line.quantity }}</td>
|
|
||||||
<td class="py-2 text-right">{{ line.lineTotal | i18nCurrency }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
<div class="space-y-1 text-right text-sm">
|
<div class="totals">
|
||||||
<div>{{ 'orders.subtotal' | translate }}: {{ inv.subtotal | i18nCurrency }}</div>
|
<div class="totals__row">
|
||||||
<div>{{ 'orders.delivery' | translate }}: {{ inv.deliveryCost | i18nCurrency }}</div>
|
<span>{{ 'orders.subtotal' | translate }}</span>
|
||||||
|
<span>{{ inv.subtotal | i18nCurrency }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="totals__row">
|
||||||
|
<span>{{ 'orders.delivery' | translate }}</span>
|
||||||
|
<span>{{ inv.deliveryCost | i18nCurrency }}</span>
|
||||||
|
</div>
|
||||||
@if (inv.discountAmount > 0) {
|
@if (inv.discountAmount > 0) {
|
||||||
<div>{{ 'orders.discount' | translate }}: -{{ inv.discountAmount | i18nCurrency }}</div>
|
<div class="totals__row">
|
||||||
|
<span>{{ 'orders.discount' | translate }}</span>
|
||||||
|
<span>-{{ inv.discountAmount | i18nCurrency }}</span>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
<div>{{ 'orders.vat' | translate }}: {{ inv.vatAmount | i18nCurrency }}</div>
|
<div class="totals__row">
|
||||||
<div class="text-lg font-bold">
|
<span>{{ 'orders.vat' | translate }}</span>
|
||||||
{{ 'orders.total' | translate }}: {{ inv.total | i18nCurrency }}
|
<span>{{ inv.vatAmount | i18nCurrency }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="totals__row totals__row--total">
|
||||||
|
<span>{{ 'orders.total' | translate }}</span>
|
||||||
|
<span>{{ inv.total | i18nCurrency }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-6 text-xs text-iled-grey-dark">
|
<p class="cell-muted" style="margin: 16px 0 0; font-size: 12px">
|
||||||
{{ 'orders.payment' | translate }}:
|
{{ 'orders.payment' | translate }}:
|
||||||
@if (inv.paymentStatus) {
|
@if (inv.paymentStatus) {
|
||||||
{{ 'orders.paymentStatus.' + inv.paymentStatus | translate }}
|
{{ 'orders.paymentStatus.' + inv.paymentStatus | translate }}
|
||||||
@@ -103,4 +135,5 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,86 +1,91 @@
|
|||||||
@if (open()) {
|
@if (open()) {
|
||||||
<div
|
<div
|
||||||
class="admin-modal fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 p-4"
|
class="admin-modal"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
(click)="handleBackdrop($event)"
|
(click)="handleBackdrop($event)"
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
class="admin-modal__panel my-4 w-full max-w-lg bg-white shadow-lg"
|
class="admin-modal__panel admin-modal__panel--sm"
|
||||||
(ngSubmit)="handleSubmit($event)"
|
(ngSubmit)="handleSubmit($event)"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between border-b border-iled-grey-light px-6 py-4">
|
<div class="admin-modal__head">
|
||||||
<h2 class="text-lg font-bold">
|
<div>
|
||||||
|
<h2>
|
||||||
@if (mode() === 'create') {
|
@if (mode() === 'create') {
|
||||||
{{ 'groups.add' | translate }}
|
{{ 'groups.add' | translate }}
|
||||||
} @else {
|
} @else {
|
||||||
{{ 'groups.edit' | translate }}
|
{{ 'groups.edit' | translate }}
|
||||||
}
|
}
|
||||||
</h2>
|
</h2>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-iled-grey-dark hover:text-iled-black"
|
class="admin-modal__close"
|
||||||
(click)="handleClose()"
|
(click)="handleClose()"
|
||||||
aria-label="{{ 'products.close' | translate }}"
|
[attr.aria-label]="'common.close' | translate"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4 px-6 py-4">
|
<div class="admin-modal__body stack" style="gap: 14px">
|
||||||
@if (error()) {
|
@if (error()) {
|
||||||
<p class="text-sm text-red-600">{{ error() | translate }}</p>
|
<p class="form-error" style="margin: 0">{{ error() | translate }}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="grid gap-3 sm:grid-cols-2">
|
<div class="grid grid-2">
|
||||||
@for (locale of locales; track locale.key) {
|
@for (locale of locales; track locale.key) {
|
||||||
<div>
|
<div class="field">
|
||||||
<label class="mb-1 block text-xs font-medium text-iled-grey-dark">
|
<label [for]="'group-name-' + locale.key">
|
||||||
{{ 'groups.name' | translate }} ({{ locale.label }})
|
{{ 'groups.name' | translate }} ({{ locale.label }})
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
[id]="'group-name-' + locale.key"
|
||||||
|
class="input"
|
||||||
[(ngModel)]="names[locale.key]"
|
[(ngModel)]="names[locale.key]"
|
||||||
[name]="'group-name-' + locale.key"
|
[name]="'group-name-' + locale.key"
|
||||||
class="w-full border border-iled-grey-light px-3 py-2 text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-3 sm:grid-cols-2">
|
<div class="grid grid-2">
|
||||||
<div>
|
<div class="field">
|
||||||
<label class="mb-1 block text-xs font-medium text-iled-grey-dark">
|
<label for="group-code">{{ 'groups.code' | translate }}</label>
|
||||||
{{ 'groups.code' | translate }}
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
|
id="group-code"
|
||||||
|
class="input"
|
||||||
[(ngModel)]="code"
|
[(ngModel)]="code"
|
||||||
name="groupCode"
|
name="groupCode"
|
||||||
[readonly]="mode() === 'edit'"
|
[readonly]="mode() === 'edit'"
|
||||||
[class.bg-iled-bg]="mode() === 'edit'"
|
style="text-transform: uppercase"
|
||||||
class="w-full border border-iled-grey-light px-3 py-2 text-sm uppercase"
|
[style.background]="
|
||||||
|
mode() === 'edit' ? 'var(--surface-muted)' : null
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="field">
|
||||||
<label class="mb-1 block text-xs font-medium text-iled-grey-dark">
|
<label for="group-sort">{{ 'groups.sortOrder' | translate }}</label>
|
||||||
{{ 'groups.sortOrder' | translate }}
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
|
id="group-sort"
|
||||||
|
class="input"
|
||||||
[(ngModel)]="sortOrder"
|
[(ngModel)]="sortOrder"
|
||||||
name="groupSortOrder"
|
name="groupSortOrder"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
class="w-full border border-iled-grey-light px-3 py-2 text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between border-t border-iled-grey-light px-6 py-4">
|
<div class="admin-modal__foot">
|
||||||
@if (mode() === 'edit') {
|
@if (mode() === 'edit') {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-sm text-red-600 hover:underline"
|
class="btn btn-ghost"
|
||||||
|
style="color: var(--danger)"
|
||||||
(click)="handleDelete()"
|
(click)="handleDelete()"
|
||||||
>
|
>
|
||||||
{{ 'groups.delete' | translate }}
|
{{ 'groups.delete' | translate }}
|
||||||
@@ -88,15 +93,15 @@
|
|||||||
} @else {
|
} @else {
|
||||||
<span></span>
|
<span></span>
|
||||||
}
|
}
|
||||||
<div class="flex gap-2">
|
<div class="page-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="border border-iled-grey-light px-5 py-2 text-sm"
|
class="btn btn-secondary"
|
||||||
(click)="handleClose()"
|
(click)="handleClose()"
|
||||||
>
|
>
|
||||||
{{ 'products.cancel' | translate }}
|
{{ 'products.cancel' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" class="bg-iled-primary px-5 py-2 text-sm font-semibold">
|
<button type="submit" class="btn btn-primary">
|
||||||
{{ 'products.save' | translate }}
|
{{ 'products.save' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
@if (open()) {
|
@if (open()) {
|
||||||
<div
|
<div
|
||||||
class="admin-modal fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 p-4"
|
class="admin-modal"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
(click)="handleBackdrop($event)"
|
(click)="handleBackdrop($event)"
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
class="admin-modal__panel my-4 w-full max-w-4xl overflow-hidden rounded-[var(--radius-lg)] bg-[var(--surface)] shadow-[var(--shadow-md)]"
|
class="admin-modal__panel admin-modal__panel--xl"
|
||||||
(ngSubmit)="handleSubmit($event)"
|
(ngSubmit)="handleSubmit($event)"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<div class="page-header" style="padding: 20px 24px 12px; margin: 0; border-bottom: 1px solid var(--border)">
|
<div class="admin-modal__head">
|
||||||
<div>
|
<div>
|
||||||
<h1 style="font-size: 1.25rem; margin: 0">
|
<h1>
|
||||||
@if (mode() === 'create') {
|
@if (mode() === 'create') {
|
||||||
{{ 'products.add' | translate }}
|
{{ 'products.add' | translate }}
|
||||||
} @else {
|
} @else {
|
||||||
{{ 'products.editTitle' | translate }}
|
{{ 'products.editTitle' | translate }}
|
||||||
}
|
}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="page-purpose" style="margin: 4px 0 0">
|
<p class="page-purpose" style="margin: 0">
|
||||||
@if (mode() === 'edit' && names.pl) {
|
@if (mode() === 'edit' && names.pl) {
|
||||||
{{ names.pl }}
|
{{ names.pl }}
|
||||||
@if (productCode) {
|
@if (productCode) {
|
||||||
@@ -41,20 +41,28 @@
|
|||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
{{ 'products.saveProduct' | translate }}
|
{{ 'products.saveProduct' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="admin-modal__close"
|
||||||
|
(click)="handleClose()"
|
||||||
|
[attr.aria-label]="'common.close' | translate"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
<div class="card-pad">
|
<div class="admin-modal__body">
|
||||||
<app-admin-skeleton variant="form" [rows]="6" />
|
<app-admin-skeleton variant="form" [rows]="6" />
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div style="padding: 0 24px">
|
<div class="admin-modal__body" style="padding-top: 8px">
|
||||||
@if (error()) {
|
@if (error()) {
|
||||||
<p class="form-error" style="margin: 12px 0 0">{{ error() | translate }}</p>
|
<p class="form-error" style="margin: 0 0 12px">{{ error() | translate }}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="tabs" role="tablist" style="margin-top: 8px">
|
<div class="tabs" role="tablist">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="tab"
|
class="tab"
|
||||||
|
|||||||
@@ -55,4 +55,9 @@
|
|||||||
.invoice-backdrop {
|
.invoice-backdrop {
|
||||||
background: white !important;
|
background: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-modal__head.print\:hidden,
|
||||||
|
.print\:hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -824,6 +824,10 @@ table.data tbody tr:hover {
|
|||||||
background: var(--surface-muted);
|
background: var(--surface-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.data tbody tr.is-collected {
|
||||||
|
background: color-mix(in srgb, var(--success-soft) 55%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
table.data tbody tr.is-selected {
|
table.data tbody tr.is-selected {
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
}
|
}
|
||||||
@@ -1535,10 +1539,145 @@ table.data tbody tr.is-selected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ——— Modal shells ——— */
|
/* ——— Modal shells ——— */
|
||||||
|
.admin-modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 24px 16px;
|
||||||
|
background: rgba(17, 24, 39, 0.48);
|
||||||
|
}
|
||||||
|
|
||||||
.admin-modal__panel {
|
.admin-modal__panel {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 12px 0 32px;
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal__panel--lg {
|
||||||
|
max-width: 920px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal__panel--xl {
|
||||||
|
max-width: 1100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal__panel--sm {
|
||||||
|
max-width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px 24px 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
color-mix(in srgb, var(--surface-muted) 65%, transparent) 0%,
|
||||||
|
var(--surface) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal__head h1,
|
||||||
|
.admin-modal__head h2 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal__body {
|
||||||
|
padding: 20px 24px 24px;
|
||||||
|
max-height: min(72vh, 820px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal__foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 14px 24px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: var(--surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal__close {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal__close:hover {
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
background: var(--surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal .split {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal .card {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal .totals {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal .totals__row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal .totals__row--total {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal .row-product {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ——— Premium motion ——— */
|
/* ——— Premium motion ——— */
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ public sealed class InitiatePaymentCommandHandler(
|
|||||||
var order = await orderService.GetOrderAsync(
|
var order = await orderService.GetOrderAsync(
|
||||||
request.OrderId, cancellationToken);
|
request.OrderId, cancellationToken);
|
||||||
|
|
||||||
|
if (order.Status != Domain.Enums.OrderStatus.PendingPayment)
|
||||||
|
{
|
||||||
|
return new PaymentResultDto(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
"Payment can only be started for unpaid orders.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order.PaymentMethod != Domain.Enums.PaymentMethod.PayU)
|
||||||
|
{
|
||||||
|
return new PaymentResultDto(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
"Only PayU orders can be paid online.");
|
||||||
|
}
|
||||||
|
|
||||||
var settings = await integrationSettings.GetAsync(cancellationToken);
|
var settings = await integrationSettings.GetAsync(cancellationToken);
|
||||||
var shopBase = configuration["Seo:ShopBaseUrl"] ?? "http://localhost:4200";
|
var shopBase = configuration["Seo:ShopBaseUrl"] ?? "http://localhost:4200";
|
||||||
var notifyUrl = settings.PayUNotifyUrl
|
var notifyUrl = settings.PayUNotifyUrl
|
||||||
@@ -52,14 +68,32 @@ public sealed class InitiatePaymentCommandHandler(
|
|||||||
return new PaymentResultDto(false, null, result.ErrorMessage);
|
return new PaymentResultDto(false, null, result.ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
var payment = new Domain.Payments.Payment
|
var payment = await db.Payments
|
||||||
|
.FirstOrDefaultAsync(
|
||||||
|
p => p.OrderId == order.Id,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (payment is null)
|
||||||
|
{
|
||||||
|
payment = new Domain.Payments.Payment
|
||||||
{
|
{
|
||||||
OrderId = order.Id,
|
OrderId = order.Id,
|
||||||
ExternalId = result.ExternalId ?? string.Empty,
|
ExternalId = result.ExternalId ?? string.Empty,
|
||||||
Amount = order.Total,
|
Amount = order.Total,
|
||||||
RedirectUrl = result.RedirectUrl,
|
RedirectUrl = result.RedirectUrl,
|
||||||
|
Status = Domain.Enums.PaymentStatus.Pending,
|
||||||
};
|
};
|
||||||
db.Add(payment);
|
db.Add(payment);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
payment.ExternalId = result.ExternalId ?? payment.ExternalId;
|
||||||
|
payment.Amount = order.Total;
|
||||||
|
payment.RedirectUrl = result.RedirectUrl;
|
||||||
|
payment.Status = Domain.Enums.PaymentStatus.Pending;
|
||||||
|
payment.UpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync(cancellationToken);
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
return new PaymentResultDto(true, result.RedirectUrl, null);
|
return new PaymentResultDto(true, result.RedirectUrl, null);
|
||||||
|
|||||||
@@ -271,6 +271,12 @@ export const CS_TRANSLATIONS: TranslationMap = {
|
|||||||
'checkout.payWithPayU': 'Zaplatit přes PayU',
|
'checkout.payWithPayU': 'Zaplatit přes PayU',
|
||||||
'checkout.orderError': 'Objednávku se nepodařilo vytvořit.',
|
'checkout.orderError': 'Objednávku se nepodařilo vytvořit.',
|
||||||
'checkout.thankYou': 'Děkujeme za objednávku!',
|
'checkout.thankYou': 'Děkujeme za objednávku!',
|
||||||
|
'checkout.paymentPendingTitle': 'Platba nebyla dokončena',
|
||||||
|
'checkout.paymentPendingBody':
|
||||||
|
'Objednávka je rezervovaná, ale platba neproběhla. Můžete to zkusit znovu.',
|
||||||
|
'checkout.payAgain': 'Zaplatit znovu',
|
||||||
|
'checkout.payingAgain': 'Přesměrování…',
|
||||||
|
'checkout.payAgainError': 'Platbu se nepodařilo spustit. Zkuste to znovu.',
|
||||||
'checkout.orderNumber': 'Číslo objednávky:',
|
'checkout.orderNumber': 'Číslo objednávky:',
|
||||||
'checkout.status': 'Stav:',
|
'checkout.status': 'Stav:',
|
||||||
'checkout.status.PendingPayment': 'Čeká na platbu',
|
'checkout.status.PendingPayment': 'Čeká na platbu',
|
||||||
|
|||||||
@@ -271,6 +271,13 @@ export const DE_TRANSLATIONS: TranslationMap = {
|
|||||||
'checkout.payWithPayU': 'Mit PayU bezahlen',
|
'checkout.payWithPayU': 'Mit PayU bezahlen',
|
||||||
'checkout.orderError': 'Bestellung fehlgeschlagen.',
|
'checkout.orderError': 'Bestellung fehlgeschlagen.',
|
||||||
'checkout.thankYou': 'Vielen Dank für Ihre Bestellung!',
|
'checkout.thankYou': 'Vielen Dank für Ihre Bestellung!',
|
||||||
|
'checkout.paymentPendingTitle': 'Zahlung nicht abgeschlossen',
|
||||||
|
'checkout.paymentPendingBody':
|
||||||
|
'Ihre Bestellung ist reserviert, aber die Zahlung ist fehlgeschlagen. Sie können es erneut versuchen.',
|
||||||
|
'checkout.payAgain': 'Erneut zahlen',
|
||||||
|
'checkout.payingAgain': 'Weiterleitung…',
|
||||||
|
'checkout.payAgainError':
|
||||||
|
'Zahlung konnte nicht gestartet werden. Bitte erneut versuchen.',
|
||||||
'checkout.orderNumber': 'Bestellnummer:',
|
'checkout.orderNumber': 'Bestellnummer:',
|
||||||
'checkout.status': 'Status:',
|
'checkout.status': 'Status:',
|
||||||
'checkout.status.PendingPayment': 'Zahlung ausstehend',
|
'checkout.status.PendingPayment': 'Zahlung ausstehend',
|
||||||
|
|||||||
@@ -271,6 +271,12 @@ export const EN_TRANSLATIONS: TranslationMap = {
|
|||||||
'checkout.payWithPayU': 'Pay with PayU',
|
'checkout.payWithPayU': 'Pay with PayU',
|
||||||
'checkout.orderError': 'Order placement failed.',
|
'checkout.orderError': 'Order placement failed.',
|
||||||
'checkout.thankYou': 'Thank you for your order!',
|
'checkout.thankYou': 'Thank you for your order!',
|
||||||
|
'checkout.paymentPendingTitle': 'Payment not completed',
|
||||||
|
'checkout.paymentPendingBody':
|
||||||
|
'Your order is reserved, but payment did not go through. You can try again.',
|
||||||
|
'checkout.payAgain': 'Pay again',
|
||||||
|
'checkout.payingAgain': 'Redirecting…',
|
||||||
|
'checkout.payAgainError': 'Could not start payment. Please try again.',
|
||||||
'checkout.orderNumber': 'Order number:',
|
'checkout.orderNumber': 'Order number:',
|
||||||
'checkout.status': 'Status:',
|
'checkout.status': 'Status:',
|
||||||
'checkout.status.PendingPayment': 'Pending payment',
|
'checkout.status.PendingPayment': 'Pending payment',
|
||||||
|
|||||||
@@ -271,6 +271,12 @@ export const PL_TRANSLATIONS: TranslationMap = {
|
|||||||
'checkout.payWithPayU': 'Zapłać przez PayU',
|
'checkout.payWithPayU': 'Zapłać przez PayU',
|
||||||
'checkout.orderError': 'Błąd składania zamówienia.',
|
'checkout.orderError': 'Błąd składania zamówienia.',
|
||||||
'checkout.thankYou': 'Dziękujemy za zamówienie!',
|
'checkout.thankYou': 'Dziękujemy za zamówienie!',
|
||||||
|
'checkout.paymentPendingTitle': 'Płatność nie została dokończona',
|
||||||
|
'checkout.paymentPendingBody':
|
||||||
|
'Zamówienie jest zarezerwowane, ale płatność nie przeszła. Możesz spróbować ponownie.',
|
||||||
|
'checkout.payAgain': 'Zapłać ponownie',
|
||||||
|
'checkout.payingAgain': 'Przekierowanie…',
|
||||||
|
'checkout.payAgainError': 'Nie udało się uruchomić płatności. Spróbuj ponownie.',
|
||||||
'checkout.orderNumber': 'Numer zamówienia:',
|
'checkout.orderNumber': 'Numer zamówienia:',
|
||||||
'checkout.status': 'Status:',
|
'checkout.status': 'Status:',
|
||||||
'checkout.status.PendingPayment': 'Oczekuje na płatność',
|
'checkout.status.PendingPayment': 'Oczekuje na płatność',
|
||||||
|
|||||||
@@ -351,10 +351,13 @@ export class CheckoutComponent implements OnInit {
|
|||||||
export class OrderConfirmationComponent implements OnInit {
|
export class OrderConfirmationComponent implements OnInit {
|
||||||
readonly #orders = inject(OrderService);
|
readonly #orders = inject(OrderService);
|
||||||
readonly #route = inject(ActivatedRoute);
|
readonly #route = inject(ActivatedRoute);
|
||||||
|
readonly #router = inject(Router);
|
||||||
readonly #i18n = inject(I18nService);
|
readonly #i18n = inject(I18nService);
|
||||||
|
|
||||||
readonly order = signal<OrderDto | null>(null);
|
readonly order = signal<OrderDto | null>(null);
|
||||||
readonly syncing = signal(false);
|
readonly syncing = signal(false);
|
||||||
|
readonly paying = signal(false);
|
||||||
|
readonly payError = signal('');
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.#route.paramMap.subscribe((params) => {
|
this.#route.paramMap.subscribe((params) => {
|
||||||
@@ -365,8 +368,43 @@ export class OrderConfirmationComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
needsPayment(order: OrderDto): boolean {
|
||||||
|
return (
|
||||||
|
order.paymentMethod === 'PayU' &&
|
||||||
|
order.status === 'PendingPayment'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePayAgain(): void {
|
||||||
|
const order = this.order();
|
||||||
|
if (!order || this.paying()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.paying.set(true);
|
||||||
|
this.payError.set('');
|
||||||
|
this.#orders.initiatePayment(order.id).subscribe({
|
||||||
|
next: (pay) => {
|
||||||
|
this.paying.set(false);
|
||||||
|
if (pay.success && pay.redirectUrl) {
|
||||||
|
this.#goToPayment(pay.redirectUrl, order.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.payError.set(
|
||||||
|
pay.errorMessage || this.#i18n.t('checkout.payAgainError'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.paying.set(false);
|
||||||
|
this.payError.set(this.#i18n.t('checkout.payAgainError'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async #loadAndSync(orderId: string): Promise<void> {
|
async #loadAndSync(orderId: string): Promise<void> {
|
||||||
this.syncing.set(true);
|
this.syncing.set(true);
|
||||||
|
this.payError.set('');
|
||||||
try {
|
try {
|
||||||
// PayU continueUrl lands here before/without webhook — pull status.
|
// PayU continueUrl lands here before/without webhook — pull status.
|
||||||
for (let attempt = 0; attempt < 4; attempt++) {
|
for (let attempt = 0; attempt < 4; attempt++) {
|
||||||
@@ -392,6 +430,25 @@ export class OrderConfirmationComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Internal sandbox paths stay same-origin; live PayU uses absolute URL. */
|
||||||
|
#goToPayment(redirectUrl: string, orderId: string): void {
|
||||||
|
if (
|
||||||
|
redirectUrl.startsWith('/') ||
|
||||||
|
redirectUrl.includes('/checkout/payu-sandbox/')
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const url = new URL(redirectUrl, window.location.origin);
|
||||||
|
void this.#router.navigateByUrl(url.pathname + url.search);
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
void this.#router.navigate(['/order', orderId]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
paymentHint(method: string): string {
|
paymentHint(method: string): string {
|
||||||
const key = `checkout.paymentHint.${enumKey(method)}`;
|
const key = `checkout.paymentHint.${enumKey(method)}`;
|
||||||
const translated = this.#i18n.t(key);
|
const translated = this.#i18n.t(key);
|
||||||
|
|||||||
@@ -4,9 +4,19 @@
|
|||||||
<p class="text-iled-grey-dark">{{ 'common.loading' | translate }}</p>
|
<p class="text-iled-grey-dark">{{ 'common.loading' | translate }}</p>
|
||||||
}
|
}
|
||||||
@if (order(); as o) {
|
@if (order(); as o) {
|
||||||
|
@if (needsPayment(o)) {
|
||||||
|
<h1 class="mb-4 text-3xl font-bold">
|
||||||
|
{{ 'checkout.paymentPendingTitle' | translate }}
|
||||||
|
</h1>
|
||||||
|
<p class="mb-6 text-iled-grey-dark">
|
||||||
|
{{ 'checkout.paymentPendingBody' | translate }}
|
||||||
|
</p>
|
||||||
|
} @else {
|
||||||
<h1 class="mb-4 text-3xl font-bold">
|
<h1 class="mb-4 text-3xl font-bold">
|
||||||
{{ 'checkout.thankYou' | translate }}
|
{{ 'checkout.thankYou' | translate }}
|
||||||
</h1>
|
</h1>
|
||||||
|
}
|
||||||
|
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
{{ 'checkout.orderNumber' | translate }}
|
{{ 'checkout.orderNumber' | translate }}
|
||||||
<strong>{{ o.orderNumber }}</strong>
|
<strong>{{ o.orderNumber }}</strong>
|
||||||
@@ -35,12 +45,33 @@
|
|||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
<p class="mb-8 text-xl font-bold">{{ o.total | i18nCurrency }}</p>
|
<p class="mb-8 text-xl font-bold">{{ o.total | i18nCurrency }}</p>
|
||||||
|
|
||||||
|
@if (payError()) {
|
||||||
|
<p class="mb-4 text-sm text-red-700">{{ payError() }}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-center gap-3">
|
||||||
|
@if (needsPayment(o)) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex bg-iled-primary px-8 py-3 font-semibold text-iled-black disabled:opacity-50"
|
||||||
|
[disabled]="paying()"
|
||||||
|
(click)="handlePayAgain()"
|
||||||
|
>
|
||||||
|
@if (paying()) {
|
||||||
|
{{ 'checkout.payingAgain' | translate }}
|
||||||
|
} @else {
|
||||||
|
{{ 'checkout.payAgain' | translate }}
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
<a
|
<a
|
||||||
routerLink="/"
|
routerLink="/"
|
||||||
class="inline-flex bg-iled-black px-8 py-3 font-medium text-white"
|
class="inline-flex border border-iled-grey-light bg-white px-8 py-3 font-medium text-iled-black"
|
||||||
>
|
>
|
||||||
{{ 'checkout.backToShop' | translate }}
|
{{ 'checkout.backToShop' | translate }}
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</app-shell>
|
</app-shell>
|
||||||
Reference in new issue
Block a user