includio-cms 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/API.md +29 -6
  2. package/CHANGELOG.md +95 -0
  3. package/DOCS.md +80 -5
  4. package/ROADMAP.md +1 -0
  5. package/dist/admin/client/index.d.ts +3 -0
  6. package/dist/admin/client/index.js +3 -0
  7. package/dist/admin/client/shop/coupon-edit-page.svelte +44 -0
  8. package/dist/admin/client/shop/coupon-edit-page.svelte.d.ts +3 -0
  9. package/dist/admin/client/shop/coupon-form.svelte +170 -0
  10. package/dist/admin/client/shop/coupon-form.svelte.d.ts +18 -0
  11. package/dist/admin/client/shop/coupon-new-page.svelte +25 -0
  12. package/dist/admin/client/shop/coupon-new-page.svelte.d.ts +18 -0
  13. package/dist/admin/client/shop/coupons-list-page.svelte +135 -0
  14. package/dist/admin/client/shop/coupons-list-page.svelte.d.ts +3 -0
  15. package/dist/admin/client/shop/refund-dialog.svelte +161 -0
  16. package/dist/admin/client/shop/refund-dialog.svelte.d.ts +11 -0
  17. package/dist/admin/client/shop/shipping-method-edit-page.svelte +3 -6
  18. package/dist/admin/client/shop/shipping-method-form.svelte +15 -21
  19. package/dist/admin/client/shop/shipping-method-new-page.svelte +3 -6
  20. package/dist/admin/client/shop/shipping-methods-list-page.svelte +6 -6
  21. package/dist/admin/client/shop/shop-order-detail-page.svelte +107 -27
  22. package/dist/admin/client/shop/shop-orders-list-page.svelte +49 -11
  23. package/dist/admin/client/shop/shop-products-list-page.svelte +12 -11
  24. package/dist/admin/components/layout/lang.d.ts +1 -0
  25. package/dist/admin/components/layout/lang.js +4 -2
  26. package/dist/admin/components/layout/layout-renderer.svelte +12 -11
  27. package/dist/admin/components/layout/nav-breadcrumbs.svelte +3 -5
  28. package/dist/admin/components/layout/nav-shop.svelte +3 -1
  29. package/dist/admin/components/layout/nav-user.svelte +6 -4
  30. package/dist/admin/components/layout/site-header.svelte +11 -5
  31. package/dist/admin/remote/shop.remote.d.ts +122 -3
  32. package/dist/admin/remote/shop.remote.js +161 -5
  33. package/dist/db-postgres/schema/shop/couponRedemptions.d.ts +97 -0
  34. package/dist/db-postgres/schema/shop/couponRedemptions.js +21 -0
  35. package/dist/db-postgres/schema/shop/coupons.d.ts +197 -0
  36. package/dist/db-postgres/schema/shop/coupons.js +18 -0
  37. package/dist/db-postgres/schema/shop/index.d.ts +4 -0
  38. package/dist/db-postgres/schema/shop/index.js +4 -0
  39. package/dist/db-postgres/schema/shop/product.d.ts +17 -0
  40. package/dist/db-postgres/schema/shop/product.js +2 -0
  41. package/dist/db-postgres/schema/shop/refunds.d.ts +214 -0
  42. package/dist/db-postgres/schema/shop/refunds.js +21 -0
  43. package/dist/db-postgres/schema/shop/webhookEvents.d.ts +183 -0
  44. package/dist/db-postgres/schema/shop/webhookEvents.js +22 -0
  45. package/dist/shop/adapters/payu/client.d.ts +9 -0
  46. package/dist/shop/adapters/payu/client.js +29 -0
  47. package/dist/shop/adapters/payu/index.js +17 -1
  48. package/dist/shop/adapters/stripe/index.d.ts +64 -0
  49. package/dist/shop/adapters/stripe/index.js +169 -0
  50. package/dist/shop/adapters/stripe/payload.d.ts +38 -0
  51. package/dist/shop/adapters/stripe/payload.js +90 -0
  52. package/dist/shop/adapters/stripe/status-map.d.ts +11 -0
  53. package/dist/shop/adapters/stripe/status-map.js +31 -0
  54. package/dist/shop/cart/coupon-cookie.d.ts +7 -0
  55. package/dist/shop/cart/coupon-cookie.js +32 -0
  56. package/dist/shop/cart/types.d.ts +12 -0
  57. package/dist/shop/client/index.d.ts +118 -0
  58. package/dist/shop/client/index.js +39 -1
  59. package/dist/shop/http/cart-handler.d.ts +8 -0
  60. package/dist/shop/http/cart-handler.js +60 -1
  61. package/dist/shop/http/checkout-handler.js +7 -3
  62. package/dist/shop/http/index.d.ts +1 -1
  63. package/dist/shop/http/index.js +1 -1
  64. package/dist/shop/http/retry-payment-handler.js +1 -1
  65. package/dist/shop/http/webhook-handler.js +19 -1
  66. package/dist/shop/http/webhook-idempotency.d.ts +16 -0
  67. package/dist/shop/http/webhook-idempotency.js +51 -0
  68. package/dist/shop/http/webhook-logic.js +2 -1
  69. package/dist/shop/index.d.ts +3 -1
  70. package/dist/shop/index.js +3 -1
  71. package/dist/shop/pricing.d.ts +15 -0
  72. package/dist/shop/pricing.js +22 -0
  73. package/dist/shop/server/cart-hydrate.d.ts +1 -0
  74. package/dist/shop/server/cart-hydrate.js +58 -10
  75. package/dist/shop/server/coupons.d.ts +53 -0
  76. package/dist/shop/server/coupons.js +117 -0
  77. package/dist/shop/server/email.d.ts +15 -0
  78. package/dist/shop/server/email.js +46 -3
  79. package/dist/shop/server/orders.d.ts +1 -0
  80. package/dist/shop/server/orders.js +120 -54
  81. package/dist/shop/server/refund.d.ts +32 -0
  82. package/dist/shop/server/refund.js +140 -0
  83. package/dist/shop/svelte/InpostPicker.svelte +4 -7
  84. package/dist/shop/svelte/OrderStatus.svelte +6 -10
  85. package/dist/shop/svelte/labels.js +4 -2
  86. package/dist/shop/types.d.ts +41 -1
  87. package/dist/updates/0.25.0/index.d.ts +2 -0
  88. package/dist/updates/0.25.0/index.js +89 -0
  89. package/dist/updates/index.js +64 -1
  90. package/package.json +6 -1
@@ -3,11 +3,14 @@
3
3
  import { getRemotes } from '../../../sveltekit/index.js';
4
4
  import { Button } from '../../../components/ui/button/index.js';
5
5
  import MailIcon from '@tabler/icons-svelte/icons/mail';
6
+ import RefundDialog from './refund-dialog.svelte';
6
7
 
7
8
  const remotes = getRemotes();
8
9
 
9
10
  const orderId = $derived(page.params.id ?? '');
10
11
  const query = $derived(remotes.getOrderForAdmin(orderId));
12
+ const refundsQuery = $derived(remotes.getOrderRefundsAdmin(orderId));
13
+ let refundDialogOpen = $state(false);
11
14
 
12
15
  type OrderStatus =
13
16
  | 'new'
@@ -17,7 +20,8 @@
17
20
  | 'sent'
18
21
  | 'done'
19
22
  | 'cancelled'
20
- | 'paymentRejected';
23
+ | 'paymentRejected'
24
+ | 'refunded';
21
25
 
22
26
  const STATUSES: Array<{ value: OrderStatus; label: string }> = [
23
27
  { value: 'new', label: 'Nowe' },
@@ -27,7 +31,8 @@
27
31
  { value: 'sent', label: 'Wysłane' },
28
32
  { value: 'done', label: 'Zrealizowane' },
29
33
  { value: 'cancelled', label: 'Anulowane' },
30
- { value: 'paymentRejected', label: 'Płatność odrzucona' }
34
+ { value: 'paymentRejected', label: 'Płatność odrzucona' },
35
+ { value: 'refunded', label: 'Zwrócone' }
31
36
  ];
32
37
 
33
38
  const STATUS_STYLES: Record<string, string> = {
@@ -38,7 +43,8 @@
38
43
  sent: 'bg-indigo-100 text-indigo-800',
39
44
  done: 'bg-gray-100 text-gray-800',
40
45
  cancelled: 'bg-red-100 text-red-800',
41
- paymentRejected: 'bg-red-100 text-red-800'
46
+ paymentRejected: 'bg-red-100 text-red-800',
47
+ refunded: 'bg-orange-100 text-orange-800'
42
48
  };
43
49
 
44
50
  let newStatus = $state<OrderStatus | ''>('');
@@ -119,8 +125,7 @@
119
125
  await query.refresh();
120
126
  }
121
127
  } catch (err) {
122
- errorMessage =
123
- err instanceof Error ? err.message : 'Błąd przy tworzeniu przesyłki';
128
+ errorMessage = err instanceof Error ? err.message : 'Błąd przy tworzeniu przesyłki';
124
129
  } finally {
125
130
  shipping = false;
126
131
  }
@@ -140,8 +145,7 @@
140
145
  await query.refresh();
141
146
  }
142
147
  } catch (err) {
143
- errorMessage =
144
- err instanceof Error ? err.message : 'Błąd przy anulowaniu przesyłki';
148
+ errorMessage = err instanceof Error ? err.message : 'Błąd przy anulowaniu przesyłki';
145
149
  } finally {
146
150
  shipping = false;
147
151
  }
@@ -158,7 +162,11 @@
158
162
  {:else}
159
163
  {@const { order, items, history } = query.current}
160
164
  {@const address = order.shippingAddress as Record<string, string> | null}
161
- {@const consents = order.consents as Array<{ id: string; accepted: boolean; label: string }> | null}
165
+ {@const consents = order.consents as Array<{
166
+ id: string;
167
+ accepted: boolean;
168
+ label: string;
169
+ }> | null}
162
170
 
163
171
  <div class="space-y-6 p-6">
164
172
  <div class="flex items-start justify-between gap-4">
@@ -167,14 +175,14 @@
167
175
  <span class="font-mono">{order.number}</span>
168
176
  </h1>
169
177
  <p class="text-muted-foreground text-sm">
170
- {formatDate(order.createdAt)} · <a
171
- href="/admin/shop/orders"
172
- class="hover:underline">← Lista</a
173
- >
178
+ {formatDate(order.createdAt)} ·
179
+ <a href="/admin/shop/orders" class="hover:underline">← Lista</a>
174
180
  </p>
175
181
  </div>
176
182
  <span
177
- class="inline-flex rounded-full px-3 py-1 text-xs font-semibold {STATUS_STYLES[order.status] ?? 'bg-gray-100 text-gray-800'}"
183
+ class="inline-flex rounded-full px-3 py-1 text-xs font-semibold {STATUS_STYLES[
184
+ order.status
185
+ ] ?? 'bg-gray-100 text-gray-800'}"
178
186
  >
179
187
  {statusLabel(order.status)}
180
188
  </span>
@@ -212,9 +220,13 @@
212
220
  <div class="text-muted-foreground text-xs">{name.variant}</div>
213
221
  {/if}
214
222
  </td>
215
- <td class="text-muted-foreground py-2 font-mono text-xs">{item.skuSnapshot ?? '—'}</td>
223
+ <td class="text-muted-foreground py-2 font-mono text-xs"
224
+ >{item.skuSnapshot ?? '—'}</td
225
+ >
216
226
  <td class="py-2 text-center">{item.qty}</td>
217
- <td class="py-2 text-right tabular-nums">{formatPrice(item.priceGrossSnapshot, order.currency)}</td>
227
+ <td class="py-2 text-right tabular-nums"
228
+ >{formatPrice(item.priceGrossSnapshot, order.currency)}</td
229
+ >
218
230
  <td class="text-muted-foreground py-2 text-right">{item.vatRate}%</td>
219
231
  <td class="py-2 text-right font-semibold tabular-nums">
220
232
  {formatPrice(item.priceGrossSnapshot * item.qty, order.currency)}
@@ -225,7 +237,9 @@
225
237
  <tfoot class="border-border border-t-2">
226
238
  <tr>
227
239
  <td colspan="5" class="pt-3 text-right text-sm">Wysyłka (brutto):</td>
228
- <td class="pt-3 text-right tabular-nums">{formatPrice(order.shippingGross, order.currency)}</td>
240
+ <td class="pt-3 text-right tabular-nums"
241
+ >{formatPrice(order.shippingGross, order.currency)}</td
242
+ >
229
243
  </tr>
230
244
  <tr>
231
245
  <td colspan="5" class="text-right text-sm">Razem netto:</td>
@@ -255,7 +269,9 @@
255
269
  {#each history as h (h.id)}
256
270
  <li class="flex items-start gap-3">
257
271
  <span
258
- class="mt-0.5 inline-flex shrink-0 rounded-full px-2 py-0.5 text-xs {STATUS_STYLES[h.status] ?? 'bg-gray-100 text-gray-800'}"
272
+ class="mt-0.5 inline-flex shrink-0 rounded-full px-2 py-0.5 text-xs {STATUS_STYLES[
273
+ h.status
274
+ ] ?? 'bg-gray-100 text-gray-800'}"
259
275
  >
260
276
  {statusLabel(h.status)}
261
277
  </span>
@@ -299,7 +315,9 @@
299
315
  </select>
300
316
  </label>
301
317
  <label class="block">
302
- <span class="text-muted-foreground mb-1 block text-xs font-semibold">Notatka (opcjonalna)</span>
318
+ <span class="text-muted-foreground mb-1 block text-xs font-semibold"
319
+ >Notatka (opcjonalna)</span
320
+ >
303
321
  <textarea
304
322
  bind:value={note}
305
323
  rows="2"
@@ -312,10 +330,57 @@
312
330
  </Button>
313
331
  </section>
314
332
 
333
+ {#if refundsQuery.ready && refundsQuery.current}
334
+ {@const r = refundsQuery.current}
335
+ <section class="border-border bg-card space-y-3 rounded-xl border p-5 text-sm">
336
+ <h2 class="text-base font-bold">Zwroty</h2>
337
+ {#if r.refunds.length > 0}
338
+ <ul class="space-y-2">
339
+ {#each r.refunds as refund (refund.id)}
340
+ <li class="border-border flex items-start justify-between gap-2 border-t pt-2">
341
+ <div>
342
+ <div class="font-medium tabular-nums">
343
+ {formatPrice(refund.amount, r.currency)}
344
+ </div>
345
+ <div class="text-muted-foreground text-xs">
346
+ {formatDate(refund.createdAt)} · {refund.status}
347
+ </div>
348
+ {#if refund.reason}
349
+ <div class="text-muted-foreground text-xs">{refund.reason}</div>
350
+ {/if}
351
+ </div>
352
+ </li>
353
+ {/each}
354
+ </ul>
355
+ <div class="border-border border-t pt-2 text-xs">
356
+ <div class="text-muted-foreground">
357
+ Zwrócono: <strong>{formatPrice(r.refundedAmount, r.currency)}</strong>
358
+ </div>
359
+ <div class="text-muted-foreground">
360
+ Pozostało: <strong>{formatPrice(r.remainingRefundable, r.currency)}</strong>
361
+ </div>
362
+ </div>
363
+ {:else}
364
+ <p class="text-muted-foreground text-xs">Brak zwrotów dla tego zamówienia.</p>
365
+ {/if}
366
+ {#if r.refundSupported && r.remainingRefundable > 0 && (order.status === 'paid' || order.status === 'preparing' || order.status === 'sent' || order.status === 'done')}
367
+ <Button onclick={() => (refundDialogOpen = true)} variant="outline" class="w-full">
368
+ Wykonaj zwrot
369
+ </Button>
370
+ {:else if !r.refundSupported && r.remainingRefundable > 0}
371
+ <p class="text-muted-foreground text-xs">
372
+ Adapter „{r.paymentMethod}” nie obsługuje zwrotów programowych.
373
+ </p>
374
+ {/if}
375
+ </section>
376
+ {/if}
377
+
315
378
  {#if order.carrierType && order.carrierType !== 'none'}
316
379
  <section class="border-border bg-card space-y-3 rounded-xl border p-5 text-sm">
317
380
  <h2 class="text-base font-bold">
318
- Przesyłka <span class="text-muted-foreground font-mono text-xs">{order.carrierType}</span>
381
+ Przesyłka <span class="text-muted-foreground font-mono text-xs"
382
+ >{order.carrierType}</span
383
+ >
319
384
  </h2>
320
385
  {#if order.shipmentId}
321
386
  <div class="space-y-1">
@@ -354,11 +419,7 @@
354
419
  <p class="text-muted-foreground text-xs">
355
420
  Wygeneruj etykietę i nadaj paczkę przez ShipX.
356
421
  </p>
357
- <Button
358
- onclick={handleCreateShipment}
359
- disabled={shipping}
360
- class="w-full"
361
- >
422
+ <Button onclick={handleCreateShipment} disabled={shipping} class="w-full">
362
423
  {shipping ? 'Tworzenie…' : 'Utwórz przesyłkę InPost'}
363
424
  </Button>
364
425
  {:else}
@@ -394,10 +455,15 @@
394
455
  <div class="border-border mt-2 border-t pt-2">
395
456
  <div class="text-muted-foreground text-xs font-semibold">Faktura</div>
396
457
  <div>{address.invoiceCompany}</div>
397
- {#if address.invoiceNip}<div class="text-muted-foreground text-xs">NIP: {address.invoiceNip}</div>{/if}
458
+ {#if address.invoiceNip}<div class="text-muted-foreground text-xs">
459
+ NIP: {address.invoiceNip}
460
+ </div>{/if}
398
461
  {#if address.invoiceStreet}<div class="text-xs">{address.invoiceStreet}</div>{/if}
399
462
  {#if address.invoicePostcode || address.invoiceCity}
400
- <div class="text-xs">{address.invoicePostcode ?? ''} {address.invoiceCity ?? ''}</div>
463
+ <div class="text-xs">
464
+ {address.invoicePostcode ?? ''}
465
+ {address.invoiceCity ?? ''}
466
+ </div>
401
467
  {/if}
402
468
  </div>
403
469
  {/if}
@@ -424,10 +490,24 @@
424
490
  {#if order.notes}
425
491
  <section class="border-border bg-card space-y-1 rounded-xl border p-5 text-sm">
426
492
  <h2 class="text-base font-bold">Uwagi klienta</h2>
427
- <p class="whitespace-pre-wrap text-xs">{order.notes}</p>
493
+ <p class="text-xs whitespace-pre-wrap">{order.notes}</p>
428
494
  </section>
429
495
  {/if}
430
496
  </div>
431
497
  </div>
432
498
  </div>
499
+
500
+ {#if refundsQuery.ready && refundsQuery.current && refundsQuery.current.refundSupported}
501
+ <RefundDialog
502
+ bind:open={refundDialogOpen}
503
+ {orderId}
504
+ currency={refundsQuery.current.currency}
505
+ remainingRefundable={refundsQuery.current.remainingRefundable}
506
+ onOpenChange={(v) => (refundDialogOpen = v)}
507
+ onRefunded={async () => {
508
+ successMessage = 'Zwrot wykonany.';
509
+ await Promise.all([query.refresh(), refundsQuery.refresh()]);
510
+ }}
511
+ />
512
+ {/if}
433
513
  {/if}
@@ -12,9 +12,41 @@
12
12
  { value: 'sent', label: 'Wysłane' },
13
13
  { value: 'done', label: 'Zrealizowane' },
14
14
  { value: 'cancelled', label: 'Anulowane' },
15
- { value: 'paymentRejected', label: 'Płatność odrzucona' }
15
+ { value: 'paymentRejected', label: 'Płatność odrzucona' },
16
+ { value: 'refunded', label: 'Zwrócone' }
16
17
  ] as const;
17
18
 
19
+ let exporting = $state(false);
20
+
21
+ async function handleExportCsv() {
22
+ exporting = true;
23
+ try {
24
+ const result = await remotes.exportOrdersCsv(
25
+ statusFilter === 'all'
26
+ ? appliedEmail
27
+ ? { email: appliedEmail }
28
+ : undefined
29
+ : appliedEmail
30
+ ? { status: statusFilter, email: appliedEmail }
31
+ : { status: statusFilter }
32
+ );
33
+ const blob = new Blob(['' + result.csv], { type: 'text/csv;charset=utf-8' });
34
+ const url = URL.createObjectURL(blob);
35
+ const a = document.createElement('a');
36
+ const ts = new Date().toISOString().slice(0, 10);
37
+ a.href = url;
38
+ a.download = `zamowienia-${ts}.csv`;
39
+ document.body.appendChild(a);
40
+ a.click();
41
+ a.remove();
42
+ URL.revokeObjectURL(url);
43
+ } catch (err) {
44
+ alert(err instanceof Error ? err.message : 'Eksport nie powiódł się.');
45
+ } finally {
46
+ exporting = false;
47
+ }
48
+ }
49
+
18
50
  let statusFilter = $state<(typeof STATUSES)[number]['value']>('all');
19
51
  let emailFilter = $state('');
20
52
  let appliedEmail = $state('');
@@ -35,7 +67,8 @@
35
67
  sent: 'bg-indigo-100 text-indigo-800',
36
68
  done: 'bg-gray-100 text-gray-800',
37
69
  cancelled: 'bg-red-100 text-red-800',
38
- paymentRejected: 'bg-red-100 text-red-800'
70
+ paymentRejected: 'bg-red-100 text-red-800',
71
+ refunded: 'bg-orange-100 text-orange-800'
39
72
  };
40
73
 
41
74
  function formatPrice(smallest: number, currency: string) {
@@ -73,10 +106,7 @@
73
106
  <div class="flex flex-wrap items-end gap-3">
74
107
  <label class="block">
75
108
  <span class="text-muted-foreground mb-1 block text-xs font-semibold">Status</span>
76
- <select
77
- bind:value={statusFilter}
78
- class="border-border h-9 rounded-lg border px-3 text-sm"
79
- >
109
+ <select bind:value={statusFilter} class="border-border h-9 rounded-lg border px-3 text-sm">
80
110
  {#each STATUSES as s (s.value)}
81
111
  <option value={s.value}>{s.label}</option>
82
112
  {/each}
@@ -93,6 +123,15 @@
93
123
  class="border-border h-9 rounded-lg border px-3 text-sm"
94
124
  />
95
125
  </label>
126
+ <button
127
+ type="button"
128
+ onclick={handleExportCsv}
129
+ disabled={exporting}
130
+ class="border-border hover:bg-muted h-9 rounded-lg border px-3 text-sm disabled:opacity-50"
131
+ title="Pobierz CSV z bieżącymi filtrami"
132
+ >
133
+ {exporting ? 'Eksportuję…' : 'Eksport CSV'}
134
+ </button>
96
135
  </div>
97
136
  </div>
98
137
 
@@ -102,14 +141,12 @@
102
141
  {:else if (query.current?.length ?? 0) === 0}
103
142
  <div class="bg-lavender-lighter/40 border-border rounded-xl border p-8 text-center text-sm">
104
143
  <p class="mb-2 font-semibold">Brak zamówień</p>
105
- <p class="text-muted-foreground">
106
- Gdy klient złoży zamówienie, pojawi się tutaj.
107
- </p>
144
+ <p class="text-muted-foreground">Gdy klient złoży zamówienie, pojawi się tutaj.</p>
108
145
  </div>
109
146
  {:else}
110
147
  <div class="border-border overflow-hidden rounded-xl border">
111
148
  <table class="w-full text-sm">
112
- <thead class="bg-muted/50 text-left text-xs uppercase tracking-wide">
149
+ <thead class="bg-muted/50 text-left text-xs tracking-wide uppercase">
113
150
  <tr>
114
151
  <th class="px-4 py-3 font-semibold">Numer</th>
115
152
  <th class="px-4 py-3 font-semibold">Data</th>
@@ -136,7 +173,8 @@
136
173
  <td class="text-muted-foreground px-4 py-3 text-xs">{o.paymentMethod ?? '—'}</td>
137
174
  <td class="px-4 py-3">
138
175
  <span
139
- class="inline-flex rounded-full px-2 py-0.5 text-xs {STATUS_STYLES[o.status] ?? 'bg-gray-100 text-gray-800'}"
176
+ class="inline-flex rounded-full px-2 py-0.5 text-xs {STATUS_STYLES[o.status] ??
177
+ 'bg-gray-100 text-gray-800'}"
140
178
  >
141
179
  {STATUSES.find((s) => s.value === o.status)?.label ?? o.status}
142
180
  </span>
@@ -89,14 +89,14 @@
89
89
  <div class="bg-lavender-lighter/40 border-border rounded-xl border p-8 text-center text-sm">
90
90
  <p class="mb-2 font-semibold">Brak produktów</p>
91
91
  <p class="text-muted-foreground">
92
- Dodaj pole <code class="text-primary">&lbrace; type: 'shop' &rbrace;</code> do kolekcji,
93
- a następnie utwórz wpis i wypełnij sekcję „Sklep".
92
+ Dodaj pole <code class="text-primary">&lbrace; type: 'shop' &rbrace;</code> do kolekcji, a następnie
93
+ utwórz wpis i wypełnij sekcję „Sklep".
94
94
  </p>
95
95
  </div>
96
96
  {:else}
97
97
  <div class="border-border overflow-hidden rounded-xl border">
98
98
  <table class="w-full text-sm">
99
- <thead class="bg-muted/50 text-left text-xs uppercase tracking-wide">
99
+ <thead class="bg-muted/50 text-left text-xs tracking-wide uppercase">
100
100
  <tr>
101
101
  <th class="px-4 py-3 font-semibold">Nazwa</th>
102
102
  <th class="px-4 py-3 font-semibold">Kolekcja</th>
@@ -112,14 +112,12 @@
112
112
  {@const title = resolveTitle(row.publishedData ?? row.draftData, row.collectionSlug)}
113
113
  <tr class="border-border hover:bg-muted/30 border-t">
114
114
  <td class="px-4 py-3">
115
- <a
116
- href={`/admin/entries/${row.entryId}`}
117
- class="text-primary hover:underline"
118
- >
115
+ <a href={`/admin/entries/${row.entryId}`} class="text-primary hover:underline">
119
116
  {title}
120
117
  </a>
121
118
  </td>
122
- <td class="text-muted-foreground px-4 py-3 font-mono text-xs">{row.collectionSlug}</td>
119
+ <td class="text-muted-foreground px-4 py-3 font-mono text-xs">{row.collectionSlug}</td
120
+ >
123
121
  <td class="px-4 py-3">{formatPrice(row.basePrice)}</td>
124
122
  <td class="px-4 py-3">{row.vatRate}%</td>
125
123
  <td class="px-4 py-3">{row.variantCount}</td>
@@ -133,16 +131,19 @@
133
131
  <td class="px-4 py-3">
134
132
  <div class="flex gap-1.5">
135
133
  {#if row.published}
136
- <span class="inline-flex rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-800"
134
+ <span
135
+ class="inline-flex rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-800"
137
136
  >Opublikowany</span
138
137
  >
139
138
  {:else}
140
- <span class="inline-flex rounded-full bg-yellow-100 px-2 py-0.5 text-xs text-yellow-800"
139
+ <span
140
+ class="inline-flex rounded-full bg-yellow-100 px-2 py-0.5 text-xs text-yellow-800"
141
141
  >Szkic</span
142
142
  >
143
143
  {/if}
144
144
  {#if !row.isActive}
145
- <span class="inline-flex rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-800"
145
+ <span
146
+ class="inline-flex rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-800"
146
147
  >Nieaktywny</span
147
148
  >
148
149
  {/if}
@@ -24,6 +24,7 @@ export declare const sidebarLang: Record<string, {
24
24
  products: string;
25
25
  orders: string;
26
26
  shipping: string;
27
+ coupons: string;
27
28
  };
28
29
  footer: {
29
30
  help: string;
@@ -24,7 +24,8 @@ export const sidebarLang = {
24
24
  title: 'Sklep',
25
25
  products: 'Produkty',
26
26
  orders: 'Zamówienia',
27
- shipping: 'Metody wysyłki'
27
+ shipping: 'Metody wysyłki',
28
+ coupons: 'Kody rabatowe'
28
29
  },
29
30
  footer: {
30
31
  help: 'Pomoc'
@@ -63,7 +64,8 @@ export const sidebarLang = {
63
64
  title: 'Shop',
64
65
  products: 'Products',
65
66
  orders: 'Orders',
66
- shipping: 'Shipping methods'
67
+ shipping: 'Shipping methods',
68
+ coupons: 'Coupon codes'
67
69
  },
68
70
  footer: {
69
71
  help: 'Help'
@@ -42,7 +42,9 @@
42
42
  const fieldMap = $derived(new Map(fields.map((f) => [f.slug, f])));
43
43
 
44
44
  // Compute distributed object slugs once at top level
45
- const distributedSlugs = $derived(parentDistributedSlugs ?? getDistributedObjectSlugs(nodes, fields));
45
+ const distributedSlugs = $derived(
46
+ parentDistributedSlugs ?? getDistributedObjectSlugs(nodes, fields)
47
+ );
46
48
 
47
49
  /**
48
50
  * Resolve a field reference (slug or dot-notation path) to its Field definition and form path.
@@ -90,7 +92,8 @@
90
92
  class={cn(
91
93
  'rounded-lg transition-all duration-500',
92
94
  autoGrid && !isCompactField(field) && 'auto-grid-full',
93
- isFlashing(formPath) && 'ring-2 ring-primary ring-offset-2 bg-primary/5 animate-in fade-in'
95
+ isFlashing(formPath) &&
96
+ 'ring-primary bg-primary/5 animate-in fade-in ring-2 ring-offset-2'
94
97
  )}
95
98
  >
96
99
  <FieldRenderer
@@ -135,12 +138,8 @@
135
138
  {@render recurse(node.children)}
136
139
  {/if}
137
140
  </section>
138
-
139
141
  {:else if node.type === 'columns'}
140
- <div
141
- class="layout-columns"
142
- style="grid-template-columns: {node.ratio};"
143
- >
142
+ <div class="layout-columns" style="grid-template-columns: {node.ratio};">
144
143
  {#if isLayoutBranch(node)}
145
144
  {#each node.children as child (child)}
146
145
  <div class="layout-column">
@@ -149,9 +148,13 @@
149
148
  {/each}
150
149
  {/if}
151
150
  </div>
152
-
153
151
  {:else if node.type === 'card'}
154
- <div role="group" aria-label={getLabel(node) || undefined} class="layout-card" class:no-header={!getLabel(node)}>
152
+ <div
153
+ role="group"
154
+ aria-label={getLabel(node) || undefined}
155
+ class="layout-card"
156
+ class:no-header={!getLabel(node)}
157
+ >
155
158
  {#if getLabel(node)}
156
159
  <div class="layout-card-header">
157
160
  {getLabel(node)}
@@ -176,7 +179,6 @@
176
179
  {/if}
177
180
  </div>
178
181
  </div>
179
-
180
182
  {:else if node.type === 'accordion'}
181
183
  <div class="layout-accordion-wrapper">
182
184
  <Accordion.Root type="single" value={node.defaultOpen ? 'item' : ''}>
@@ -199,7 +201,6 @@
199
201
  </Accordion.Item>
200
202
  </Accordion.Root>
201
203
  </div>
202
-
203
204
  {:else if node.type === 'stack'}
204
205
  <div class="layout-stack">
205
206
  {#if isLayoutLeaf(node)}
@@ -6,15 +6,13 @@
6
6
  </script>
7
7
 
8
8
  <Breadcrumb.Root>
9
- <Breadcrumb.List class="text-[13px] gap-2">
9
+ <Breadcrumb.List class="gap-2 text-[13px]">
10
10
  {#each breadcrumbs.state as crumb, i}
11
11
  <Breadcrumb.Item>
12
12
  {#if crumb.href && i < breadcrumbs.state.length - 1}
13
- <Breadcrumb.Link class="font-medium" href={crumb.href}
14
- >{crumb.label}</Breadcrumb.Link
15
- >
13
+ <Breadcrumb.Link class="font-medium" href={crumb.href}>{crumb.label}</Breadcrumb.Link>
16
14
  {:else}
17
- <Breadcrumb.Page class="font-semibold max-w-60 truncate">{crumb.label}</Breadcrumb.Page>
15
+ <Breadcrumb.Page class="max-w-60 truncate font-semibold">{crumb.label}</Breadcrumb.Page>
18
16
  {/if}
19
17
  </Breadcrumb.Item>
20
18
  {#if i < breadcrumbs.state.length - 1}
@@ -6,6 +6,7 @@
6
6
  import PackageIcon from '@tabler/icons-svelte/icons/package';
7
7
  import ShoppingCartIcon from '@tabler/icons-svelte/icons/shopping-cart';
8
8
  import TruckIcon from '@tabler/icons-svelte/icons/truck';
9
+ import TicketIcon from '@tabler/icons-svelte/icons/ticket';
9
10
  import BuildingStoreIcon from '@tabler/icons-svelte/icons/building-store';
10
11
  import { page } from '$app/state';
11
12
 
@@ -22,7 +23,8 @@
22
23
  const items = $derived([
23
24
  { title: lang.products, url: '/admin/shop/products', icon: PackageIcon },
24
25
  { title: lang.orders, url: '/admin/shop/orders', icon: ShoppingCartIcon },
25
- { title: lang.shipping, url: '/admin/shop/shipping-methods', icon: TruckIcon }
26
+ { title: lang.shipping, url: '/admin/shop/shipping-methods', icon: TruckIcon },
27
+ { title: lang.coupons, url: '/admin/shop/coupons', icon: TicketIcon }
26
28
  ]);
27
29
  </script>
28
30
 
@@ -29,18 +29,20 @@
29
29
  <Sidebar.MenuItem>
30
30
  {#if $session.data}
31
31
  <div class="flex items-center gap-2 px-2 py-1.5">
32
- <a href="/admin/account" class="flex items-center gap-2 flex-1 min-w-0 group">
33
- <Avatar.Root class="size-7 rounded-lg shrink-0">
32
+ <a href="/admin/account" class="group flex min-w-0 flex-1 items-center gap-2">
33
+ <Avatar.Root class="size-7 shrink-0 rounded-lg">
34
34
  <Avatar.Image src={$session.data.user.image} alt={$session.data.user.name} />
35
35
  <Avatar.Fallback class="rounded-lg text-xs">
36
36
  {$session.data.user.name[0].toUpperCase()}
37
37
  </Avatar.Fallback>
38
38
  </Avatar.Root>
39
- <div class="min-w-0 flex flex-col">
39
+ <div class="flex min-w-0 flex-col">
40
40
  <span class="truncate text-sm font-medium group-hover:underline">
41
41
  {$session.data.user.name}
42
42
  </span>
43
- <span class="text-muted-foreground text-[10px] leading-tight">v{updates[0].version}</span>
43
+ <span class="text-muted-foreground text-[10px] leading-tight"
44
+ >v{updates[0].version}</span
45
+ >
44
46
  </div>
45
47
  </a>
46
48
  <button
@@ -40,7 +40,7 @@
40
40
  </script>
41
41
 
42
42
  <header
43
- class="flex h-(--header-height) shrink-0 items-center gap-2 border-b border-sidebar-border bg-background/60 backdrop-blur-xl transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"
43
+ class="border-sidebar-border bg-background/60 flex h-(--header-height) shrink-0 items-center gap-2 border-b backdrop-blur-xl transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"
44
44
  >
45
45
  <div class="flex w-full items-center px-5">
46
46
  <div class="flex items-center gap-3">
@@ -53,7 +53,7 @@
53
53
  href="/"
54
54
  variant="outline"
55
55
  size="sm"
56
- class="text-[13px] font-medium text-primary hover:bg-[var(--lavender-lighter)] hover:border-[var(--lavender)]"
56
+ class="text-primary text-[13px] font-medium hover:border-[var(--lavender)] hover:bg-[var(--lavender-lighter)]"
57
57
  target="_blank"
58
58
  rel="noopener noreferrer"
59
59
  >
@@ -62,10 +62,14 @@
62
62
  </Button>
63
63
  {#if $session.data}
64
64
  <DropdownMenu.Root>
65
- <DropdownMenu.Trigger class="cursor-pointer rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
65
+ <DropdownMenu.Trigger
66
+ class="focus-visible:ring-ring cursor-pointer rounded-full outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
67
+ >
66
68
  <Avatar.Root class="size-[30px]">
67
69
  <Avatar.Image src={$session.data.user.image} alt={$session.data.user.name} />
68
- <Avatar.Fallback class="bg-gradient-to-br from-[var(--lavender)] to-primary text-[12px] font-bold text-white">
70
+ <Avatar.Fallback
71
+ class="to-primary bg-gradient-to-br from-[var(--lavender)] text-[12px] font-bold text-white"
72
+ >
69
73
  {$session.data.user.name
70
74
  .split(' ')
71
75
  .map((n) => n[0])
@@ -78,7 +82,9 @@
78
82
  <DropdownMenu.Content align="end" class="w-56">
79
83
  <DropdownMenu.Label class="flex flex-col gap-0.5">
80
84
  <span class="text-sm font-medium">{$session.data.user.name}</span>
81
- <span class="text-xs font-normal text-muted-foreground">{$session.data.user.email}</span>
85
+ <span class="text-muted-foreground text-xs font-normal"
86
+ >{$session.data.user.email}</span
87
+ >
82
88
  </DropdownMenu.Label>
83
89
  <DropdownMenu.Separator />
84
90
  <DropdownMenu.Item onSelect={() => goto('/admin/account')}>