includio-cms 0.14.6 → 0.15.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 (127) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/DOCS.md +45 -1
  3. package/ROADMAP.md +23 -2
  4. package/dist/admin/auth-client.d.ts +42 -42
  5. package/dist/admin/client/entry/entry.svelte +1 -0
  6. package/dist/admin/client/index.d.ts +6 -0
  7. package/dist/admin/client/index.js +6 -0
  8. package/dist/admin/client/shop/shipping-method-edit-page.svelte +113 -0
  9. package/dist/admin/client/shop/shipping-method-edit-page.svelte.d.ts +3 -0
  10. package/dist/admin/client/shop/shipping-method-form.svelte +244 -0
  11. package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +37 -0
  12. package/dist/admin/client/shop/shipping-method-new-page.svelte +47 -0
  13. package/dist/admin/client/shop/shipping-method-new-page.svelte.d.ts +3 -0
  14. package/dist/admin/client/shop/shipping-methods-list-page.svelte +172 -0
  15. package/dist/admin/client/shop/shipping-methods-list-page.svelte.d.ts +3 -0
  16. package/dist/admin/client/shop/shop-order-detail-page.svelte +332 -0
  17. package/dist/admin/client/shop/shop-order-detail-page.svelte.d.ts +3 -0
  18. package/dist/admin/client/shop/shop-orders-list-page.svelte +150 -0
  19. package/dist/admin/client/shop/shop-orders-list-page.svelte.d.ts +3 -0
  20. package/dist/admin/client/shop/shop-products-list-page.svelte +157 -0
  21. package/dist/admin/client/shop/shop-products-list-page.svelte.d.ts +3 -0
  22. package/dist/admin/components/fields/field-renderer.svelte +4 -2
  23. package/dist/admin/components/fields/shop-field.svelte +298 -0
  24. package/dist/admin/components/fields/shop-field.svelte.d.ts +7 -0
  25. package/dist/admin/components/layout/app-sidebar.svelte +2 -0
  26. package/dist/admin/components/layout/lang.d.ts +6 -0
  27. package/dist/admin/components/layout/lang.js +12 -0
  28. package/dist/admin/components/layout/nav-shop.svelte +55 -0
  29. package/dist/admin/components/layout/nav-shop.svelte.d.ts +3 -0
  30. package/dist/admin/remote/index.d.ts +1 -0
  31. package/dist/admin/remote/index.js +1 -0
  32. package/dist/admin/remote/shop.remote.d.ts +244 -0
  33. package/dist/admin/remote/shop.remote.js +153 -0
  34. package/dist/cli/scaffold/admin.js +84 -0
  35. package/dist/core/cms.d.ts +2 -0
  36. package/dist/core/cms.js +2 -0
  37. package/dist/core/fields/fieldSchemaToTs.js +5 -0
  38. package/dist/core/server/entries/operations/get.js +3 -3
  39. package/dist/core/server/fields/populateEntry.d.ts +1 -1
  40. package/dist/core/server/fields/populateEntry.js +3 -1
  41. package/dist/core/server/generator/fields.js +14 -0
  42. package/dist/core/server/generator/generator.js +13 -0
  43. package/dist/db-postgres/schema/index.d.ts +1 -0
  44. package/dist/db-postgres/schema/index.js +1 -0
  45. package/dist/db-postgres/schema/shop/index.d.ts +8 -0
  46. package/dist/db-postgres/schema/shop/index.js +8 -0
  47. package/dist/db-postgres/schema/shop/order.d.ts +396 -0
  48. package/dist/db-postgres/schema/shop/order.js +28 -0
  49. package/dist/db-postgres/schema/shop/orderItem.d.ts +179 -0
  50. package/dist/db-postgres/schema/shop/orderItem.js +20 -0
  51. package/dist/db-postgres/schema/shop/orderStatusHistory.d.ts +112 -0
  52. package/dist/db-postgres/schema/shop/orderStatusHistory.js +12 -0
  53. package/dist/db-postgres/schema/shop/payment.d.ts +180 -0
  54. package/dist/db-postgres/schema/shop/payment.js +16 -0
  55. package/dist/db-postgres/schema/shop/product.d.ts +143 -0
  56. package/dist/db-postgres/schema/shop/product.js +15 -0
  57. package/dist/db-postgres/schema/shop/productVariant.d.ts +164 -0
  58. package/dist/db-postgres/schema/shop/productVariant.js +15 -0
  59. package/dist/db-postgres/schema/shop/shippingMethod.d.ts +190 -0
  60. package/dist/db-postgres/schema/shop/shippingMethod.js +13 -0
  61. package/dist/db-postgres/schema/shop/stockReservation.d.ts +109 -0
  62. package/dist/db-postgres/schema/shop/stockReservation.js +13 -0
  63. package/dist/db-postgres/schema-core.d.ts +9 -0
  64. package/dist/db-postgres/schema-core.js +9 -0
  65. package/dist/db-postgres/schema-shop.d.ts +1 -0
  66. package/dist/db-postgres/schema-shop.js +1 -0
  67. package/dist/email-nodemailer/index.d.ts +2 -9
  68. package/dist/shop/adapters/manual/index.d.ts +10 -0
  69. package/dist/shop/adapters/manual/index.js +16 -0
  70. package/dist/shop/cart/cookie.d.ts +8 -0
  71. package/dist/shop/cart/cookie.js +84 -0
  72. package/dist/shop/cart/types.d.ts +42 -0
  73. package/dist/shop/cart/types.js +1 -0
  74. package/dist/shop/client/index.d.ts +59 -0
  75. package/dist/shop/client/index.js +40 -0
  76. package/dist/shop/http/cart-handler.d.ts +7 -0
  77. package/dist/shop/http/cart-handler.js +88 -0
  78. package/dist/shop/http/checkout-handler.d.ts +4 -0
  79. package/dist/shop/http/checkout-handler.js +100 -0
  80. package/dist/shop/http/index.d.ts +3 -0
  81. package/dist/shop/http/index.js +3 -0
  82. package/dist/shop/http/shipping-handler.d.ts +4 -0
  83. package/dist/shop/http/shipping-handler.js +31 -0
  84. package/dist/shop/index.d.ts +4 -0
  85. package/dist/shop/index.js +17 -0
  86. package/dist/shop/pricing.d.ts +15 -0
  87. package/dist/shop/pricing.js +31 -0
  88. package/dist/shop/rate-limit.d.ts +9 -0
  89. package/dist/shop/rate-limit.js +28 -0
  90. package/dist/shop/server/cart-hydrate.d.ts +4 -0
  91. package/dist/shop/server/cart-hydrate.js +172 -0
  92. package/dist/shop/server/db.d.ts +4 -0
  93. package/dist/shop/server/db.js +16 -0
  94. package/dist/shop/server/email.d.ts +2 -0
  95. package/dist/shop/server/email.js +138 -0
  96. package/dist/shop/server/order-number.d.ts +5 -0
  97. package/dist/shop/server/order-number.js +15 -0
  98. package/dist/shop/server/orders.d.ts +45 -0
  99. package/dist/shop/server/orders.js +293 -0
  100. package/dist/shop/server/populate.d.ts +15 -0
  101. package/dist/shop/server/populate.js +39 -0
  102. package/dist/shop/server/shipping.d.ts +37 -0
  103. package/dist/shop/server/shipping.js +111 -0
  104. package/dist/shop/server/shop-data.d.ts +51 -0
  105. package/dist/shop/server/shop-data.js +186 -0
  106. package/dist/shop/services/cart.service.d.ts +38 -0
  107. package/dist/shop/services/cart.service.js +1 -0
  108. package/dist/shop/services/email.service.d.ts +6 -0
  109. package/dist/shop/services/email.service.js +1 -0
  110. package/dist/shop/services/index.d.ts +6 -0
  111. package/dist/shop/services/index.js +1 -0
  112. package/dist/shop/services/orders.service.d.ts +34 -0
  113. package/dist/shop/services/orders.service.js +1 -0
  114. package/dist/shop/services/payment.service.d.ts +7 -0
  115. package/dist/shop/services/payment.service.js +1 -0
  116. package/dist/shop/services/products.service.d.ts +31 -0
  117. package/dist/shop/services/products.service.js +1 -0
  118. package/dist/shop/services/shipping.service.d.ts +23 -0
  119. package/dist/shop/services/shipping.service.js +1 -0
  120. package/dist/shop/types.d.ts +72 -0
  121. package/dist/shop/types.js +1 -0
  122. package/dist/types/cms.d.ts +3 -0
  123. package/dist/types/fields.d.ts +18 -2
  124. package/dist/updates/0.15.0/index.d.ts +2 -0
  125. package/dist/updates/0.15.0/index.js +25 -0
  126. package/dist/updates/index.js +2 -1
  127. package/package.json +27 -1
@@ -0,0 +1,332 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import { getRemotes } from '../../../sveltekit/index.js';
4
+ import { Button } from '../../../components/ui/button/index.js';
5
+ import MailIcon from '@tabler/icons-svelte/icons/mail';
6
+
7
+ const remotes = getRemotes();
8
+
9
+ const orderId = $derived(page.params.id ?? '');
10
+ const query = $derived(remotes.getOrderForAdmin(orderId));
11
+
12
+ type OrderStatus =
13
+ | 'new'
14
+ | 'awaitingPayment'
15
+ | 'paid'
16
+ | 'preparing'
17
+ | 'sent'
18
+ | 'done'
19
+ | 'cancelled'
20
+ | 'paymentRejected';
21
+
22
+ const STATUSES: Array<{ value: OrderStatus; label: string }> = [
23
+ { value: 'new', label: 'Nowe' },
24
+ { value: 'awaitingPayment', label: 'Oczekuje na płatność' },
25
+ { value: 'paid', label: 'Opłacone' },
26
+ { value: 'preparing', label: 'W przygotowaniu' },
27
+ { value: 'sent', label: 'Wysłane' },
28
+ { value: 'done', label: 'Zrealizowane' },
29
+ { value: 'cancelled', label: 'Anulowane' },
30
+ { value: 'paymentRejected', label: 'Płatność odrzucona' }
31
+ ];
32
+
33
+ const STATUS_STYLES: Record<string, string> = {
34
+ new: 'bg-blue-100 text-blue-800',
35
+ awaitingPayment: 'bg-yellow-100 text-yellow-800',
36
+ paid: 'bg-green-100 text-green-800',
37
+ preparing: 'bg-purple-100 text-purple-800',
38
+ sent: 'bg-indigo-100 text-indigo-800',
39
+ done: 'bg-gray-100 text-gray-800',
40
+ cancelled: 'bg-red-100 text-red-800',
41
+ paymentRejected: 'bg-red-100 text-red-800'
42
+ };
43
+
44
+ let newStatus = $state<OrderStatus | ''>('');
45
+ let note = $state('');
46
+ let saving = $state(false);
47
+ let resending = $state(false);
48
+ let errorMessage = $state<string | null>(null);
49
+ let successMessage = $state<string | null>(null);
50
+
51
+ function statusLabel(s: string) {
52
+ return STATUSES.find((x) => x.value === s)?.label ?? s;
53
+ }
54
+
55
+ function formatPrice(cents: number, currency: string) {
56
+ return new Intl.NumberFormat('pl-PL', {
57
+ style: 'currency',
58
+ currency,
59
+ minimumFractionDigits: 2
60
+ }).format(cents / 100);
61
+ }
62
+
63
+ function formatDate(d: Date | string) {
64
+ return new Intl.DateTimeFormat('pl-PL', {
65
+ dateStyle: 'short',
66
+ timeStyle: 'short'
67
+ }).format(new Date(d));
68
+ }
69
+
70
+ async function changeStatus() {
71
+ if (!newStatus) return;
72
+ saving = true;
73
+ errorMessage = null;
74
+ successMessage = null;
75
+ try {
76
+ await remotes.updateOrderStatusCmd({
77
+ orderId,
78
+ status: newStatus,
79
+ note: note.trim() || undefined
80
+ });
81
+ await query.refresh();
82
+ note = '';
83
+ newStatus = '';
84
+ successMessage = 'Status zaktualizowany. Email wysłany do klienta.';
85
+ } catch (err) {
86
+ errorMessage = err instanceof Error ? err.message : 'Nie udało się zmienić statusu';
87
+ } finally {
88
+ saving = false;
89
+ }
90
+ }
91
+
92
+ async function resendEmail(status: OrderStatus) {
93
+ resending = true;
94
+ errorMessage = null;
95
+ successMessage = null;
96
+ try {
97
+ await remotes.resendOrderEmailCmd({ orderId, status });
98
+ successMessage = `Email (${statusLabel(status)}) został ponownie wysłany.`;
99
+ } catch (err) {
100
+ errorMessage = err instanceof Error ? err.message : 'Nie udało się wysłać emaila';
101
+ } finally {
102
+ resending = false;
103
+ }
104
+ }
105
+ </script>
106
+
107
+ {#if !query.ready}
108
+ <div class="text-muted-foreground p-6">Ładowanie…</div>
109
+ {:else if !query.current}
110
+ <div class="p-6">
111
+ <p class="mb-4">Zamówienie nie zostało znalezione.</p>
112
+ <Button href="/admin/shop/orders" variant="outline">← Wróć do listy</Button>
113
+ </div>
114
+ {:else}
115
+ {@const { order, items, history } = query.current}
116
+ {@const address = order.shippingAddress as Record<string, string> | null}
117
+ {@const consents = order.consents as Array<{ id: string; accepted: boolean; label: string }> | null}
118
+
119
+ <div class="space-y-6 p-6">
120
+ <div class="flex items-start justify-between gap-4">
121
+ <div>
122
+ <h1 class="text-2xl font-extrabold tracking-tight">
123
+ <span class="font-mono">{order.number}</span>
124
+ </h1>
125
+ <p class="text-muted-foreground text-sm">
126
+ {formatDate(order.createdAt)} · <a
127
+ href="/admin/shop/orders"
128
+ class="hover:underline">← Lista</a
129
+ >
130
+ </p>
131
+ </div>
132
+ <span
133
+ class="inline-flex rounded-full px-3 py-1 text-xs font-semibold {STATUS_STYLES[order.status] ?? 'bg-gray-100 text-gray-800'}"
134
+ >
135
+ {statusLabel(order.status)}
136
+ </span>
137
+ </div>
138
+
139
+ {#if errorMessage}
140
+ <div class="rounded-lg bg-red-50 p-3 text-sm text-red-800">{errorMessage}</div>
141
+ {/if}
142
+ {#if successMessage}
143
+ <div class="rounded-lg bg-green-50 p-3 text-sm text-green-800">{successMessage}</div>
144
+ {/if}
145
+
146
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-[2fr_1fr]">
147
+ <div class="space-y-6">
148
+ <section class="border-border bg-card space-y-3 rounded-xl border p-5">
149
+ <h2 class="text-base font-bold">Pozycje</h2>
150
+ <table class="w-full text-sm">
151
+ <thead class="text-muted-foreground text-left text-xs uppercase">
152
+ <tr>
153
+ <th class="pb-2 font-semibold">Nazwa</th>
154
+ <th class="pb-2 font-semibold">SKU</th>
155
+ <th class="pb-2 text-center font-semibold">Ilość</th>
156
+ <th class="pb-2 text-right font-semibold">Brutto / szt.</th>
157
+ <th class="pb-2 text-right font-semibold">VAT</th>
158
+ <th class="pb-2 text-right font-semibold">Suma</th>
159
+ </tr>
160
+ </thead>
161
+ <tbody>
162
+ {#each items as item (item.id)}
163
+ {@const name = (item.nameSnapshot as { product?: string; variant?: string }) ?? {}}
164
+ <tr class="border-border border-t">
165
+ <td class="py-2">
166
+ <div class="font-medium">{name.product ?? '—'}</div>
167
+ {#if name.variant}
168
+ <div class="text-muted-foreground text-xs">{name.variant}</div>
169
+ {/if}
170
+ </td>
171
+ <td class="text-muted-foreground py-2 font-mono text-xs">{item.skuSnapshot ?? '—'}</td>
172
+ <td class="py-2 text-center">{item.qty}</td>
173
+ <td class="py-2 text-right tabular-nums">{formatPrice(item.priceGrossSnapshot, order.currency)}</td>
174
+ <td class="text-muted-foreground py-2 text-right">{item.vatRate}%</td>
175
+ <td class="py-2 text-right font-semibold tabular-nums">
176
+ {formatPrice(item.priceGrossSnapshot * item.qty, order.currency)}
177
+ </td>
178
+ </tr>
179
+ {/each}
180
+ </tbody>
181
+ <tfoot class="border-border border-t-2">
182
+ <tr>
183
+ <td colspan="5" class="pt-3 text-right text-sm">Wysyłka (brutto):</td>
184
+ <td class="pt-3 text-right tabular-nums">{formatPrice(order.shippingGross, order.currency)}</td>
185
+ </tr>
186
+ <tr>
187
+ <td colspan="5" class="text-right text-sm">Razem netto:</td>
188
+ <td class="text-muted-foreground text-right tabular-nums">
189
+ {formatPrice(order.totalNet, order.currency)}
190
+ </td>
191
+ </tr>
192
+ <tr>
193
+ <td colspan="5" class="text-right text-sm">VAT:</td>
194
+ <td class="text-muted-foreground text-right tabular-nums">
195
+ {formatPrice(order.vatAmount, order.currency)}
196
+ </td>
197
+ </tr>
198
+ <tr>
199
+ <td colspan="5" class="pb-1 text-right text-base font-bold">Razem brutto:</td>
200
+ <td class="text-primary pb-1 text-right text-base font-extrabold tabular-nums">
201
+ {formatPrice(order.totalGross, order.currency)}
202
+ </td>
203
+ </tr>
204
+ </tfoot>
205
+ </table>
206
+ </section>
207
+
208
+ <section class="border-border bg-card space-y-3 rounded-xl border p-5">
209
+ <h2 class="text-base font-bold">Historia statusów</h2>
210
+ <ol class="space-y-2 text-sm">
211
+ {#each history as h (h.id)}
212
+ <li class="flex items-start gap-3">
213
+ <span
214
+ 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'}"
215
+ >
216
+ {statusLabel(h.status)}
217
+ </span>
218
+ <div class="flex-1">
219
+ <div class="text-muted-foreground text-xs">
220
+ {formatDate(h.changedAt)} · {h.changedBy ?? '—'}
221
+ </div>
222
+ {#if h.note}
223
+ <div class="text-sm">{h.note}</div>
224
+ {/if}
225
+ </div>
226
+ <Button
227
+ type="button"
228
+ variant="ghost"
229
+ size="sm"
230
+ disabled={resending}
231
+ onclick={() => resendEmail(h.status as OrderStatus)}
232
+ title="Wyślij ponownie email dla tego statusu"
233
+ >
234
+ <MailIcon class="size-4" />
235
+ </Button>
236
+ </li>
237
+ {/each}
238
+ </ol>
239
+ </section>
240
+ </div>
241
+
242
+ <div class="space-y-6">
243
+ <section class="border-border bg-card space-y-3 rounded-xl border p-5">
244
+ <h2 class="text-base font-bold">Zmień status</h2>
245
+ <label class="block">
246
+ <span class="text-muted-foreground mb-1 block text-xs font-semibold">Nowy status</span>
247
+ <select
248
+ bind:value={newStatus}
249
+ class="border-border w-full rounded-lg border px-3 py-2 text-sm"
250
+ >
251
+ <option value="">— wybierz —</option>
252
+ {#each STATUSES as s (s.value)}
253
+ <option value={s.value} disabled={s.value === order.status}>{s.label}</option>
254
+ {/each}
255
+ </select>
256
+ </label>
257
+ <label class="block">
258
+ <span class="text-muted-foreground mb-1 block text-xs font-semibold">Notatka (opcjonalna)</span>
259
+ <textarea
260
+ bind:value={note}
261
+ rows="2"
262
+ class="border-border w-full rounded-lg border px-3 py-2 text-sm"
263
+ placeholder="np. nr listu przewozowego"
264
+ ></textarea>
265
+ </label>
266
+ <Button onclick={changeStatus} disabled={saving || !newStatus} class="w-full">
267
+ {saving ? 'Zapisywanie…' : 'Zmień status + wyślij email'}
268
+ </Button>
269
+ </section>
270
+
271
+ <section class="border-border bg-card space-y-2 rounded-xl border p-5 text-sm">
272
+ <h2 class="text-base font-bold">Klient</h2>
273
+ <div>
274
+ <div class="font-medium">{order.customerName ?? '—'}</div>
275
+ <div class="text-muted-foreground text-xs">{order.customerEmail}</div>
276
+ {#if order.customerPhone}
277
+ <div class="text-muted-foreground text-xs">{order.customerPhone}</div>
278
+ {/if}
279
+ </div>
280
+ </section>
281
+
282
+ {#if address}
283
+ <section class="border-border bg-card space-y-1 rounded-xl border p-5 text-sm">
284
+ <h2 class="text-base font-bold">Adres</h2>
285
+ {#if address.street}<div>{address.street}</div>{/if}
286
+ {#if address.postcode || address.city}
287
+ <div>{address.postcode ?? ''} {address.city ?? ''}</div>
288
+ {/if}
289
+ {#if order.carrierRef}
290
+ <div class="text-muted-foreground text-xs">Paczkomat: {order.carrierRef}</div>
291
+ {/if}
292
+ {#if address.invoiceCompany}
293
+ <div class="border-border mt-2 border-t pt-2">
294
+ <div class="text-muted-foreground text-xs font-semibold">Faktura</div>
295
+ <div>{address.invoiceCompany}</div>
296
+ {#if address.invoiceNip}<div class="text-muted-foreground text-xs">NIP: {address.invoiceNip}</div>{/if}
297
+ {#if address.invoiceStreet}<div class="text-xs">{address.invoiceStreet}</div>{/if}
298
+ {#if address.invoicePostcode || address.invoiceCity}
299
+ <div class="text-xs">{address.invoicePostcode ?? ''} {address.invoiceCity ?? ''}</div>
300
+ {/if}
301
+ </div>
302
+ {/if}
303
+ </section>
304
+ {/if}
305
+
306
+ {#if consents && consents.length > 0}
307
+ <section class="border-border bg-card space-y-2 rounded-xl border p-5 text-sm">
308
+ <h2 class="text-base font-bold">Zgody</h2>
309
+ {#each consents as c (c.id)}
310
+ <div class="flex items-start gap-2">
311
+ <span class="text-xs {c.accepted ? 'text-green-700' : 'text-red-700'}">
312
+ {c.accepted ? '✓' : '✗'}
313
+ </span>
314
+ <div>
315
+ <div class="text-muted-foreground font-mono text-xs">{c.id}</div>
316
+ <div class="text-xs">{c.label}</div>
317
+ </div>
318
+ </div>
319
+ {/each}
320
+ </section>
321
+ {/if}
322
+
323
+ {#if order.notes}
324
+ <section class="border-border bg-card space-y-1 rounded-xl border p-5 text-sm">
325
+ <h2 class="text-base font-bold">Uwagi klienta</h2>
326
+ <p class="whitespace-pre-wrap text-xs">{order.notes}</p>
327
+ </section>
328
+ {/if}
329
+ </div>
330
+ </div>
331
+ </div>
332
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const ShopOrderDetailPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ShopOrderDetailPage = ReturnType<typeof ShopOrderDetailPage>;
3
+ export default ShopOrderDetailPage;
@@ -0,0 +1,150 @@
1
+ <script lang="ts">
2
+ import { getRemotes } from '../../../sveltekit/index.js';
3
+
4
+ const remotes = getRemotes();
5
+
6
+ const STATUSES = [
7
+ { value: 'all', label: 'Wszystkie' },
8
+ { value: 'new', label: 'Nowe' },
9
+ { value: 'awaitingPayment', label: 'Oczekują na płatność' },
10
+ { value: 'paid', label: 'Opłacone' },
11
+ { value: 'preparing', label: 'W przygotowaniu' },
12
+ { value: 'sent', label: 'Wysłane' },
13
+ { value: 'done', label: 'Zrealizowane' },
14
+ { value: 'cancelled', label: 'Anulowane' },
15
+ { value: 'paymentRejected', label: 'Płatność odrzucona' }
16
+ ] as const;
17
+
18
+ let statusFilter = $state<(typeof STATUSES)[number]['value']>('all');
19
+ let emailFilter = $state('');
20
+ let appliedEmail = $state('');
21
+
22
+ const query = $derived(
23
+ remotes.listOrdersAdmin({
24
+ status: statusFilter === 'all' ? undefined : statusFilter,
25
+ email: appliedEmail || undefined,
26
+ limit: 200
27
+ })
28
+ );
29
+
30
+ const STATUS_STYLES: Record<string, string> = {
31
+ new: 'bg-blue-100 text-blue-800',
32
+ awaitingPayment: 'bg-yellow-100 text-yellow-800',
33
+ paid: 'bg-green-100 text-green-800',
34
+ preparing: 'bg-purple-100 text-purple-800',
35
+ sent: 'bg-indigo-100 text-indigo-800',
36
+ done: 'bg-gray-100 text-gray-800',
37
+ cancelled: 'bg-red-100 text-red-800',
38
+ paymentRejected: 'bg-red-100 text-red-800'
39
+ };
40
+
41
+ function formatPrice(smallest: number, currency: string) {
42
+ return new Intl.NumberFormat('pl-PL', {
43
+ style: 'currency',
44
+ currency,
45
+ minimumFractionDigits: 2
46
+ }).format(smallest / 100);
47
+ }
48
+
49
+ function formatDate(d: Date | string) {
50
+ return new Intl.DateTimeFormat('pl-PL', {
51
+ dateStyle: 'short',
52
+ timeStyle: 'short'
53
+ }).format(new Date(d));
54
+ }
55
+
56
+ function applyEmailFilter() {
57
+ appliedEmail = emailFilter.trim();
58
+ }
59
+ </script>
60
+
61
+ <div class="flex flex-wrap items-end justify-between gap-4 p-6">
62
+ <div>
63
+ <h1 class="text-2xl font-extrabold tracking-tight">Zamówienia</h1>
64
+ <p class="text-muted-foreground text-sm">
65
+ {#if query.ready}
66
+ {query.current?.length ?? 0}
67
+ {(query.current?.length ?? 0) === 1 ? 'zamówienie' : 'zamówień'}
68
+ {:else}
69
+ Ładowanie…
70
+ {/if}
71
+ </p>
72
+ </div>
73
+ <div class="flex flex-wrap items-end gap-3">
74
+ <label class="block">
75
+ <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
+ >
80
+ {#each STATUSES as s (s.value)}
81
+ <option value={s.value}>{s.label}</option>
82
+ {/each}
83
+ </select>
84
+ </label>
85
+ <label class="block">
86
+ <span class="text-muted-foreground mb-1 block text-xs font-semibold">Email klienta</span>
87
+ <input
88
+ type="email"
89
+ bind:value={emailFilter}
90
+ onchange={applyEmailFilter}
91
+ onblur={applyEmailFilter}
92
+ placeholder="filtruj po email"
93
+ class="border-border h-9 rounded-lg border px-3 text-sm"
94
+ />
95
+ </label>
96
+ </div>
97
+ </div>
98
+
99
+ <div class="px-6 pb-12">
100
+ {#if !query.ready}
101
+ <div class="text-muted-foreground">Ładowanie…</div>
102
+ {:else if (query.current?.length ?? 0) === 0}
103
+ <div class="bg-lavender-lighter/40 border-border rounded-xl border p-8 text-center text-sm">
104
+ <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>
108
+ </div>
109
+ {:else}
110
+ <div class="border-border overflow-hidden rounded-xl border">
111
+ <table class="w-full text-sm">
112
+ <thead class="bg-muted/50 text-left text-xs uppercase tracking-wide">
113
+ <tr>
114
+ <th class="px-4 py-3 font-semibold">Numer</th>
115
+ <th class="px-4 py-3 font-semibold">Data</th>
116
+ <th class="px-4 py-3 font-semibold">Klient</th>
117
+ <th class="px-4 py-3 font-semibold">Suma (brutto)</th>
118
+ <th class="px-4 py-3 font-semibold">Płatność</th>
119
+ <th class="px-4 py-3 font-semibold">Status</th>
120
+ </tr>
121
+ </thead>
122
+ <tbody>
123
+ {#each query.current ?? [] as o (o.id)}
124
+ <tr class="border-border hover:bg-muted/30 border-t">
125
+ <td class="px-4 py-3 font-mono text-xs">
126
+ <a href={`/admin/shop/orders/${o.id}`} class="text-primary hover:underline">
127
+ {o.number}
128
+ </a>
129
+ </td>
130
+ <td class="text-muted-foreground px-4 py-3 text-xs">{formatDate(o.createdAt)}</td>
131
+ <td class="px-4 py-3">
132
+ <div class="font-medium">{o.customerName ?? '—'}</div>
133
+ <div class="text-muted-foreground text-xs">{o.customerEmail}</div>
134
+ </td>
135
+ <td class="px-4 py-3 tabular-nums">{formatPrice(o.totalGross, o.currency)}</td>
136
+ <td class="text-muted-foreground px-4 py-3 text-xs">{o.paymentMethod ?? '—'}</td>
137
+ <td class="px-4 py-3">
138
+ <span
139
+ class="inline-flex rounded-full px-2 py-0.5 text-xs {STATUS_STYLES[o.status] ?? 'bg-gray-100 text-gray-800'}"
140
+ >
141
+ {STATUSES.find((s) => s.value === o.status)?.label ?? o.status}
142
+ </span>
143
+ </td>
144
+ </tr>
145
+ {/each}
146
+ </tbody>
147
+ </table>
148
+ </div>
149
+ {/if}
150
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const ShopOrdersListPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ShopOrdersListPage = ReturnType<typeof ShopOrdersListPage>;
3
+ export default ShopOrdersListPage;
@@ -0,0 +1,157 @@
1
+ <script lang="ts">
2
+ import { getRemotes } from '../../../sveltekit/index.js';
3
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
4
+ import { getLocalizedLabel } from '../../utils/collectionLabel.js';
5
+ import { Button } from '../../../components/ui/button/index.js';
6
+ import * as DropdownMenu from '../../../components/ui/dropdown-menu/index.js';
7
+ import PlusIcon from '@tabler/icons-svelte/icons/plus';
8
+ import ChevronDownIcon from '@tabler/icons-svelte/icons/chevron-down';
9
+
10
+ const remotes = getRemotes();
11
+ const interfaceLanguage = useInterfaceLanguage();
12
+
13
+ const entriesQuery = $derived(remotes.listShopProductEntries());
14
+ const collectionsQuery = $derived(remotes.listShopableCollections());
15
+
16
+ function formatPrice(smallest: number) {
17
+ return new Intl.NumberFormat('pl-PL', {
18
+ style: 'currency',
19
+ currency: 'PLN',
20
+ minimumFractionDigits: 2
21
+ }).format(smallest / 100);
22
+ }
23
+
24
+ function resolveTitle(data: Record<string, unknown> | null, fallback: string): string {
25
+ if (!data) return fallback;
26
+ for (const key of ['name', 'title', 'adminTitle']) {
27
+ const v = data[key];
28
+ if (typeof v === 'string' && v.length > 0) return v;
29
+ }
30
+ return fallback;
31
+ }
32
+
33
+ async function createInCollection(slug: string) {
34
+ try {
35
+ const newEntry = await remotes.createEntry({ type: 'collection', slug });
36
+ window.location.href = `/admin/entries/${newEntry.id}`;
37
+ } catch (err) {
38
+ alert(err instanceof Error ? err.message : 'Nie udało się utworzyć wpisu');
39
+ }
40
+ }
41
+ </script>
42
+
43
+ <div class="flex items-center justify-between gap-4 p-6">
44
+ <div>
45
+ <h1 class="text-2xl font-extrabold tracking-tight">Produkty</h1>
46
+ <p class="text-muted-foreground text-sm">
47
+ {#if entriesQuery.ready}
48
+ {entriesQuery.current?.length ?? 0}
49
+ {(entriesQuery.current?.length ?? 0) === 1 ? 'produkt' : 'produktów'}
50
+ {:else}
51
+ Ładowanie…
52
+ {/if}
53
+ </p>
54
+ </div>
55
+ {#if collectionsQuery.ready && collectionsQuery.current && collectionsQuery.current.length > 0}
56
+ {#if collectionsQuery.current.length === 1}
57
+ {@const only = collectionsQuery.current[0]}
58
+ <Button onclick={() => createInCollection(only.slug)}>
59
+ <PlusIcon class="mr-1 size-4" />
60
+ Dodaj produkt
61
+ </Button>
62
+ {:else}
63
+ <DropdownMenu.Root>
64
+ <DropdownMenu.Trigger>
65
+ {#snippet child({ props })}
66
+ <Button {...props}>
67
+ <PlusIcon class="mr-1 size-4" />
68
+ Dodaj produkt
69
+ <ChevronDownIcon class="ml-1 size-4" />
70
+ </Button>
71
+ {/snippet}
72
+ </DropdownMenu.Trigger>
73
+ <DropdownMenu.Content>
74
+ {#each collectionsQuery.current as c (c.slug)}
75
+ <DropdownMenu.Item onclick={() => createInCollection(c.slug)}>
76
+ {getLocalizedLabel(c.labels?.singular, interfaceLanguage.current) ?? c.slug}
77
+ </DropdownMenu.Item>
78
+ {/each}
79
+ </DropdownMenu.Content>
80
+ </DropdownMenu.Root>
81
+ {/if}
82
+ {/if}
83
+ </div>
84
+
85
+ <div class="px-6 pb-12">
86
+ {#if !entriesQuery.ready}
87
+ <div class="text-muted-foreground">Ładowanie…</div>
88
+ {:else if (entriesQuery.current?.length ?? 0) === 0}
89
+ <div class="bg-lavender-lighter/40 border-border rounded-xl border p-8 text-center text-sm">
90
+ <p class="mb-2 font-semibold">Brak produktów</p>
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".
94
+ </p>
95
+ </div>
96
+ {:else}
97
+ <div class="border-border overflow-hidden rounded-xl border">
98
+ <table class="w-full text-sm">
99
+ <thead class="bg-muted/50 text-left text-xs uppercase tracking-wide">
100
+ <tr>
101
+ <th class="px-4 py-3 font-semibold">Nazwa</th>
102
+ <th class="px-4 py-3 font-semibold">Kolekcja</th>
103
+ <th class="px-4 py-3 font-semibold">Cena bazowa</th>
104
+ <th class="px-4 py-3 font-semibold">VAT</th>
105
+ <th class="px-4 py-3 font-semibold">Warianty</th>
106
+ <th class="px-4 py-3 font-semibold">Magazyn</th>
107
+ <th class="px-4 py-3 font-semibold">Status</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ {#each entriesQuery.current ?? [] as row (row.entryId)}
112
+ {@const title = resolveTitle(row.publishedData ?? row.draftData, row.collectionSlug)}
113
+ <tr class="border-border hover:bg-muted/30 border-t">
114
+ <td class="px-4 py-3">
115
+ <a
116
+ href={`/admin/entries/${row.entryId}`}
117
+ class="text-primary hover:underline"
118
+ >
119
+ {title}
120
+ </a>
121
+ </td>
122
+ <td class="text-muted-foreground px-4 py-3 font-mono text-xs">{row.collectionSlug}</td>
123
+ <td class="px-4 py-3">{formatPrice(row.basePrice)}</td>
124
+ <td class="px-4 py-3">{row.vatRate}%</td>
125
+ <td class="px-4 py-3">{row.variantCount}</td>
126
+ <td class="px-4 py-3">
127
+ {#if row.totalStock == null}
128
+ <span class="text-muted-foreground text-xs">nieograniczony</span>
129
+ {:else}
130
+ {row.totalStock}
131
+ {/if}
132
+ </td>
133
+ <td class="px-4 py-3">
134
+ <div class="flex gap-1.5">
135
+ {#if row.published}
136
+ <span class="inline-flex rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-800"
137
+ >Opublikowany</span
138
+ >
139
+ {:else}
140
+ <span class="inline-flex rounded-full bg-yellow-100 px-2 py-0.5 text-xs text-yellow-800"
141
+ >Szkic</span
142
+ >
143
+ {/if}
144
+ {#if !row.isActive}
145
+ <span class="inline-flex rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-800"
146
+ >Nieaktywny</span
147
+ >
148
+ {/if}
149
+ </div>
150
+ </td>
151
+ </tr>
152
+ {/each}
153
+ </tbody>
154
+ </table>
155
+ </div>
156
+ {/if}
157
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const ShopProductsListPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ShopProductsListPage = ReturnType<typeof ShopProductsListPage>;
3
+ export default ShopProductsListPage;
@@ -38,8 +38,8 @@
38
38
  // Cast to any: runtime types enforced by child components via $bindable
39
39
  const { value } = formFieldProxy(form, path) as { value: import('svelte/store').Writable<any> };
40
40
 
41
- const fieldsWithNoDescription: FieldType[] = ['boolean', 'object', 'blocks', 'seo'];
42
- const fieldsWithNoLabel: FieldType[] = ['boolean', 'object', 'blocks', 'seo'];
41
+ const fieldsWithNoDescription: FieldType[] = ['boolean', 'object', 'blocks', 'seo', 'shop'];
42
+ const fieldsWithNoLabel: FieldType[] = ['boolean', 'object', 'blocks', 'seo', 'shop'];
43
43
 
44
44
  const fieldsWithAlternativeDescription: FieldType[] = ['media', 'object', 'blocks'];
45
45
 
@@ -145,6 +145,8 @@
145
145
  <NumberField {field} bind:value={$value} />
146
146
  {:else if field.type === 'seo'}
147
147
  <LazyField loader={() => import('./seo-field.svelte')} props={{ field, form, path }} skeletonClass="h-16" />
148
+ {:else if field.type === 'shop'}
149
+ <LazyField loader={() => import('./shop-field.svelte')} props={{ field }} skeletonClass="h-40" />
148
150
  {:else if field.type === 'url'}
149
151
  <UrlFieldWrapper {field} {form} {path} />
150
152
  {:else if field.type === 'relation'}