includio-cms 0.23.0 → 0.24.1

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 (118) hide show
  1. package/API.md +29 -6
  2. package/CHANGELOG.md +116 -0
  3. package/DOCS.md +80 -5
  4. package/ROADMAP.md +2 -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/core/server/entries/operations/get.bench.d.ts +1 -0
  34. package/dist/core/server/entries/operations/get.bench.js +68 -0
  35. package/dist/core/server/entries/operations/get.js +17 -7
  36. package/dist/core/server/fields/utils/imageStyles.bench.d.ts +1 -0
  37. package/dist/core/server/fields/utils/imageStyles.bench.js +82 -0
  38. package/dist/core/server/fields/utils/imageStyles.js +49 -53
  39. package/dist/core/server/media/operations/backgroundMaintenance.d.ts +6 -0
  40. package/dist/core/server/media/operations/backgroundMaintenance.js +6 -1
  41. package/dist/core/server/media/styles/operations/getImageStyle.d.ts +7 -0
  42. package/dist/core/server/media/styles/operations/getImageStyle.js +24 -0
  43. package/dist/db-postgres/index.d.ts +1 -1
  44. package/dist/db-postgres/index.js +27 -0
  45. package/dist/db-postgres/schema/shop/couponRedemptions.d.ts +97 -0
  46. package/dist/db-postgres/schema/shop/couponRedemptions.js +21 -0
  47. package/dist/db-postgres/schema/shop/coupons.d.ts +197 -0
  48. package/dist/db-postgres/schema/shop/coupons.js +18 -0
  49. package/dist/db-postgres/schema/shop/index.d.ts +4 -0
  50. package/dist/db-postgres/schema/shop/index.js +4 -0
  51. package/dist/db-postgres/schema/shop/product.d.ts +17 -0
  52. package/dist/db-postgres/schema/shop/product.js +2 -0
  53. package/dist/db-postgres/schema/shop/refunds.d.ts +214 -0
  54. package/dist/db-postgres/schema/shop/refunds.js +21 -0
  55. package/dist/db-postgres/schema/shop/webhookEvents.d.ts +183 -0
  56. package/dist/db-postgres/schema/shop/webhookEvents.js +22 -0
  57. package/dist/paraglide/messages/_index.d.ts +36 -3
  58. package/dist/paraglide/messages/_index.js +71 -3
  59. package/dist/paraglide/messages/en.d.ts +5 -0
  60. package/dist/paraglide/messages/en.js +14 -0
  61. package/dist/paraglide/messages/pl.d.ts +5 -0
  62. package/dist/paraglide/messages/pl.js +14 -0
  63. package/dist/shop/adapters/payu/client.d.ts +9 -0
  64. package/dist/shop/adapters/payu/client.js +29 -0
  65. package/dist/shop/adapters/payu/index.js +17 -1
  66. package/dist/shop/adapters/stripe/index.d.ts +64 -0
  67. package/dist/shop/adapters/stripe/index.js +169 -0
  68. package/dist/shop/adapters/stripe/payload.d.ts +38 -0
  69. package/dist/shop/adapters/stripe/payload.js +90 -0
  70. package/dist/shop/adapters/stripe/status-map.d.ts +11 -0
  71. package/dist/shop/adapters/stripe/status-map.js +31 -0
  72. package/dist/shop/cart/coupon-cookie.d.ts +7 -0
  73. package/dist/shop/cart/coupon-cookie.js +32 -0
  74. package/dist/shop/cart/types.d.ts +12 -0
  75. package/dist/shop/client/index.d.ts +118 -0
  76. package/dist/shop/client/index.js +39 -1
  77. package/dist/shop/http/cart-handler.d.ts +8 -0
  78. package/dist/shop/http/cart-handler.js +60 -1
  79. package/dist/shop/http/checkout-handler.js +7 -3
  80. package/dist/shop/http/index.d.ts +1 -1
  81. package/dist/shop/http/index.js +1 -1
  82. package/dist/shop/http/retry-payment-handler.js +1 -1
  83. package/dist/shop/http/webhook-handler.js +19 -1
  84. package/dist/shop/http/webhook-idempotency.d.ts +16 -0
  85. package/dist/shop/http/webhook-idempotency.js +51 -0
  86. package/dist/shop/http/webhook-logic.js +2 -1
  87. package/dist/shop/index.d.ts +3 -1
  88. package/dist/shop/index.js +3 -1
  89. package/dist/shop/pricing.d.ts +15 -0
  90. package/dist/shop/pricing.js +22 -0
  91. package/dist/shop/server/cart-hydrate.d.ts +1 -0
  92. package/dist/shop/server/cart-hydrate.js +58 -10
  93. package/dist/shop/server/coupons.d.ts +53 -0
  94. package/dist/shop/server/coupons.js +117 -0
  95. package/dist/shop/server/email.d.ts +15 -0
  96. package/dist/shop/server/email.js +46 -3
  97. package/dist/shop/server/orders.d.ts +1 -0
  98. package/dist/shop/server/orders.js +120 -54
  99. package/dist/shop/server/refund.d.ts +32 -0
  100. package/dist/shop/server/refund.js +140 -0
  101. package/dist/shop/svelte/InpostPicker.svelte +4 -7
  102. package/dist/shop/svelte/OrderStatus.svelte +6 -10
  103. package/dist/shop/svelte/labels.js +4 -2
  104. package/dist/shop/types.d.ts +41 -1
  105. package/dist/types/adapters/db.d.ts +16 -0
  106. package/dist/types/adapters/db.js +8 -1
  107. package/dist/updates/0.24.0/index.d.ts +2 -0
  108. package/dist/updates/0.24.0/index.js +20 -0
  109. package/dist/updates/0.25.0/index.d.ts +2 -0
  110. package/dist/updates/0.25.0/index.js +89 -0
  111. package/dist/updates/index.js +65 -1
  112. package/package.json +7 -1
  113. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  114. package/dist/paraglide/messages/hello_world.js +0 -33
  115. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  116. package/dist/paraglide/messages/login_hello.js +0 -34
  117. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  118. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -0,0 +1,170 @@
1
+ <script lang="ts">
2
+ import { Input } from '../../../components/ui/input/index.js';
3
+ import { Button } from '../../../components/ui/button/index.js';
4
+ import Label from '../../../components/ui/label/label.svelte';
5
+
6
+ type CouponInput = {
7
+ code: string;
8
+ type: 'percent' | 'fixed';
9
+ value: number;
10
+ minOrderAmount: number | null;
11
+ maxUses: number | null;
12
+ expiresAt: string | null;
13
+ isActive: boolean;
14
+ };
15
+
16
+ type Props = {
17
+ initial?: Partial<CouponInput>;
18
+ submitLabel?: string;
19
+ onSubmit: (input: CouponInput) => Promise<void>;
20
+ onCancel?: () => void;
21
+ };
22
+
23
+ let { initial, submitLabel = 'Zapisz', onSubmit, onCancel }: Props = $props();
24
+
25
+ let code = $state(initial?.code ?? '');
26
+ let type = $state<'percent' | 'fixed'>(initial?.type ?? 'percent');
27
+ let value = $state(initial?.value != null ? String(initial.value) : '');
28
+ let minOrderAmountPln = $state(
29
+ initial?.minOrderAmount != null ? (initial.minOrderAmount / 100).toFixed(2) : ''
30
+ );
31
+ let maxUses = $state(initial?.maxUses != null ? String(initial.maxUses) : '');
32
+ let expiresAt = $state(initial?.expiresAt ? initial.expiresAt.slice(0, 10) : '');
33
+ let isActive = $state(initial?.isActive ?? true);
34
+
35
+ let submitting = $state(false);
36
+ let error = $state<string | null>(null);
37
+
38
+ async function handleSubmit(e: Event) {
39
+ e.preventDefault();
40
+ error = null;
41
+ const numValue = Number(value.replace(',', '.'));
42
+ if (!Number.isFinite(numValue) || numValue <= 0) {
43
+ error = 'Podaj wartość większą od zera.';
44
+ return;
45
+ }
46
+ if (type === 'percent' && numValue > 100) {
47
+ error = 'Procent nie może przekraczać 100.';
48
+ return;
49
+ }
50
+ const minOrderCents = minOrderAmountPln
51
+ ? Math.round(Number(minOrderAmountPln.replace(',', '.')) * 100)
52
+ : null;
53
+ if (minOrderCents != null && (!Number.isFinite(minOrderCents) || minOrderCents < 0)) {
54
+ error = 'Minimalna wartość zamówienia musi być nieujemna.';
55
+ return;
56
+ }
57
+ const maxUsesNum = maxUses ? Number(maxUses) : null;
58
+ if (maxUsesNum != null && (!Number.isInteger(maxUsesNum) || maxUsesNum <= 0)) {
59
+ error = 'Maksymalna liczba użyć musi być dodatnią liczbą całkowitą.';
60
+ return;
61
+ }
62
+
63
+ submitting = true;
64
+ try {
65
+ await onSubmit({
66
+ code: code.trim().toUpperCase(),
67
+ type,
68
+ value: numValue,
69
+ minOrderAmount: minOrderCents,
70
+ maxUses: maxUsesNum,
71
+ expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null,
72
+ isActive
73
+ });
74
+ } catch (err) {
75
+ error = err instanceof Error ? err.message : 'Nie udało się zapisać.';
76
+ submitting = false;
77
+ }
78
+ }
79
+ </script>
80
+
81
+ <form onsubmit={handleSubmit} class="space-y-5">
82
+ <div class="space-y-2">
83
+ <Label for="coupon-code">Kod</Label>
84
+ <Input
85
+ id="coupon-code"
86
+ bind:value={code}
87
+ placeholder="np. ARIA10"
88
+ required
89
+ pattern="[A-Za-z0-9_-]+"
90
+ maxlength={64}
91
+ aria-describedby="coupon-code-hint"
92
+ />
93
+ <p id="coupon-code-hint" class="text-muted-foreground text-xs">
94
+ Wielkość liter zostanie ujednolicona do dużych liter. Tylko litery, cyfry, „_”, „-”.
95
+ </p>
96
+ </div>
97
+
98
+ <fieldset class="space-y-2">
99
+ <legend class="mb-1 text-sm font-semibold">Typ rabatu</legend>
100
+ <label class="flex items-center gap-2 text-sm">
101
+ <input type="radio" name="coupon-type" value="percent" bind:group={type} />
102
+ <span>Procent (% od kwoty netto)</span>
103
+ </label>
104
+ <label class="flex items-center gap-2 text-sm">
105
+ <input type="radio" name="coupon-type" value="fixed" bind:group={type} />
106
+ <span>Kwota stała (PLN)</span>
107
+ </label>
108
+ </fieldset>
109
+
110
+ <div class="space-y-2">
111
+ <Label for="coupon-value">{type === 'percent' ? 'Procent (0–100)' : 'Kwota (PLN)'}</Label>
112
+ <Input
113
+ id="coupon-value"
114
+ type="text"
115
+ inputmode="decimal"
116
+ bind:value
117
+ placeholder={type === 'percent' ? 'np. 10' : 'np. 50,00'}
118
+ required
119
+ />
120
+ </div>
121
+
122
+ <div class="grid grid-cols-2 gap-4">
123
+ <div class="space-y-2">
124
+ <Label for="coupon-min-order">Min. wartość zamówienia (PLN)</Label>
125
+ <Input
126
+ id="coupon-min-order"
127
+ type="text"
128
+ inputmode="decimal"
129
+ bind:value={minOrderAmountPln}
130
+ placeholder="opcjonalnie"
131
+ />
132
+ </div>
133
+ <div class="space-y-2">
134
+ <Label for="coupon-max-uses">Maks. liczba użyć</Label>
135
+ <Input
136
+ id="coupon-max-uses"
137
+ type="number"
138
+ min="1"
139
+ step="1"
140
+ bind:value={maxUses}
141
+ placeholder="bez limitu"
142
+ />
143
+ </div>
144
+ </div>
145
+
146
+ <div class="space-y-2">
147
+ <Label for="coupon-expires-at">Data wygaśnięcia</Label>
148
+ <Input id="coupon-expires-at" type="date" bind:value={expiresAt} />
149
+ </div>
150
+
151
+ <label class="flex items-center gap-2 text-sm">
152
+ <input type="checkbox" bind:checked={isActive} />
153
+ <span>Aktywny (dostępny przy checkoucie)</span>
154
+ </label>
155
+
156
+ {#if error}
157
+ <p class="text-destructive text-sm" role="alert">{error}</p>
158
+ {/if}
159
+
160
+ <div class="flex gap-2">
161
+ <Button type="submit" disabled={submitting}>
162
+ {submitting ? 'Zapisywanie…' : submitLabel}
163
+ </Button>
164
+ {#if onCancel}
165
+ <Button type="button" variant="outline" onclick={onCancel} disabled={submitting}>
166
+ Anuluj
167
+ </Button>
168
+ {/if}
169
+ </div>
170
+ </form>
@@ -0,0 +1,18 @@
1
+ type CouponInput = {
2
+ code: string;
3
+ type: 'percent' | 'fixed';
4
+ value: number;
5
+ minOrderAmount: number | null;
6
+ maxUses: number | null;
7
+ expiresAt: string | null;
8
+ isActive: boolean;
9
+ };
10
+ type Props = {
11
+ initial?: Partial<CouponInput>;
12
+ submitLabel?: string;
13
+ onSubmit: (input: CouponInput) => Promise<void>;
14
+ onCancel?: () => void;
15
+ };
16
+ declare const CouponForm: import("svelte").Component<Props, {}, "">;
17
+ type CouponForm = ReturnType<typeof CouponForm>;
18
+ export default CouponForm;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import { goto } from '$app/navigation';
3
+ import { getRemotes } from '../../../sveltekit/index.js';
4
+ import CouponForm from './coupon-form.svelte';
5
+
6
+ const remotes = getRemotes();
7
+ </script>
8
+
9
+ <div class="mx-auto max-w-2xl space-y-6 p-6">
10
+ <header>
11
+ <h1 class="text-2xl font-extrabold tracking-tight">Nowy kod rabatowy</h1>
12
+ <p class="text-muted-foreground text-sm">
13
+ <a href="/admin/shop/coupons" class="hover:underline">← Wróć do listy</a>
14
+ </p>
15
+ </header>
16
+
17
+ <CouponForm
18
+ submitLabel="Utwórz kod"
19
+ onSubmit={async (input) => {
20
+ await remotes.createCouponCmd(input);
21
+ await goto('/admin/shop/coupons');
22
+ }}
23
+ onCancel={() => goto('/admin/shop/coupons')}
24
+ />
25
+ </div>
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const CouponNewPage: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type CouponNewPage = InstanceType<typeof CouponNewPage>;
18
+ export default CouponNewPage;
@@ -0,0 +1,135 @@
1
+ <script lang="ts">
2
+ import { getRemotes } from '../../../sveltekit/index.js';
3
+ import { Button } from '../../../components/ui/button/index.js';
4
+ import PlusIcon from '@tabler/icons-svelte/icons/plus';
5
+ import EditIcon from '@tabler/icons-svelte/icons/edit';
6
+ import TrashIcon from '@tabler/icons-svelte/icons/trash';
7
+
8
+ const remotes = getRemotes();
9
+ const query = $derived(remotes.listCouponsAdmin());
10
+
11
+ function formatValue(type: string, value: string): string {
12
+ const num = Number(value);
13
+ if (type === 'percent') return `${num}%`;
14
+ return new Intl.NumberFormat('pl-PL', {
15
+ style: 'currency',
16
+ currency: 'PLN',
17
+ minimumFractionDigits: 2
18
+ }).format(num);
19
+ }
20
+
21
+ function formatDate(d: Date | string | null): string {
22
+ if (!d) return '—';
23
+ return new Intl.DateTimeFormat('pl-PL', {
24
+ dateStyle: 'short'
25
+ }).format(new Date(d));
26
+ }
27
+
28
+ async function handleDelete(id: string, code: string) {
29
+ if (!confirm(`Usunąć kod „${code}”? Tej operacji nie można cofnąć.`)) return;
30
+ try {
31
+ await remotes.deleteCouponCmd(id);
32
+ await query.refresh();
33
+ } catch (err) {
34
+ alert(err instanceof Error ? err.message : 'Nie udało się usunąć kodu.');
35
+ }
36
+ }
37
+ </script>
38
+
39
+ <div class="flex items-center justify-between gap-4 p-6">
40
+ <div>
41
+ <h1 class="text-2xl font-extrabold tracking-tight">Kody rabatowe</h1>
42
+ <p class="text-muted-foreground text-sm">
43
+ {#if query.ready}
44
+ {query.current?.length ?? 0}
45
+ {(query.current?.length ?? 0) === 1 ? 'kod' : 'kodów'}
46
+ {:else}
47
+ Ładowanie…
48
+ {/if}
49
+ </p>
50
+ </div>
51
+ <Button href="/admin/shop/coupons/new">
52
+ <PlusIcon class="mr-1 size-4" />
53
+ Dodaj kod
54
+ </Button>
55
+ </div>
56
+
57
+ {#if query.ready && query.current && query.current.length > 0}
58
+ <div class="px-6 pb-6">
59
+ <div class="border-border bg-card overflow-hidden rounded-xl border">
60
+ <table class="w-full text-sm">
61
+ <thead class="text-muted-foreground bg-muted/50 text-left text-xs uppercase">
62
+ <tr>
63
+ <th class="px-4 py-3 font-semibold">Kod</th>
64
+ <th class="px-4 py-3 font-semibold">Wartość</th>
65
+ <th class="px-4 py-3 font-semibold">Min. zamów.</th>
66
+ <th class="px-4 py-3 font-semibold">Wykorzystano</th>
67
+ <th class="px-4 py-3 font-semibold">Wygasa</th>
68
+ <th class="px-4 py-3 font-semibold">Status</th>
69
+ <th class="px-4 py-3"></th>
70
+ </tr>
71
+ </thead>
72
+ <tbody>
73
+ {#each query.current as c (c.id)}
74
+ <tr class="border-border border-t">
75
+ <td class="px-4 py-3 font-mono font-semibold">{c.code}</td>
76
+ <td class="px-4 py-3">{formatValue(c.type, c.value)}</td>
77
+ <td class="text-muted-foreground px-4 py-3">
78
+ {c.minOrderAmount != null
79
+ ? new Intl.NumberFormat('pl-PL', {
80
+ style: 'currency',
81
+ currency: 'PLN',
82
+ minimumFractionDigits: 2
83
+ }).format(c.minOrderAmount / 100)
84
+ : '—'}
85
+ </td>
86
+ <td class="px-4 py-3">
87
+ {c.usedCount}{c.maxUses != null ? ` / ${c.maxUses}` : ''}
88
+ </td>
89
+ <td class="text-muted-foreground px-4 py-3 text-xs">
90
+ {formatDate(c.expiresAt)}
91
+ </td>
92
+ <td class="px-4 py-3">
93
+ {#if c.isActive}
94
+ <span class="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-800"
95
+ >aktywny</span
96
+ >
97
+ {:else}
98
+ <span class="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-800">
99
+ wstrzymany
100
+ </span>
101
+ {/if}
102
+ </td>
103
+ <td class="px-4 py-3 text-right">
104
+ <div class="flex justify-end gap-1">
105
+ <Button
106
+ href="/admin/shop/coupons/{c.id}"
107
+ variant="ghost"
108
+ size="sm"
109
+ title="Edytuj"
110
+ >
111
+ <EditIcon class="size-4" />
112
+ </Button>
113
+ <Button
114
+ onclick={() => handleDelete(c.id, c.code)}
115
+ variant="ghost"
116
+ size="sm"
117
+ title="Usuń"
118
+ >
119
+ <TrashIcon class="size-4" />
120
+ </Button>
121
+ </div>
122
+ </td>
123
+ </tr>
124
+ {/each}
125
+ </tbody>
126
+ </table>
127
+ </div>
128
+ </div>
129
+ {:else if query.ready}
130
+ <div class="text-muted-foreground p-6 text-sm">
131
+ Brak kodów rabatowych. <a href="/admin/shop/coupons/new" class="text-primary hover:underline"
132
+ >Dodaj pierwszy</a
133
+ >.
134
+ </div>
135
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const CouponsListPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type CouponsListPage = ReturnType<typeof CouponsListPage>;
3
+ export default CouponsListPage;
@@ -0,0 +1,161 @@
1
+ <script lang="ts">
2
+ import * as Dialog from '../../../components/ui/dialog/index.js';
3
+ import { Input } from '../../../components/ui/input/index.js';
4
+ import Label from '../../../components/ui/label/label.svelte';
5
+ import Button from '../../../components/ui/button/button.svelte';
6
+ import { getRemotes } from '../../../sveltekit/index.js';
7
+
8
+ type Props = {
9
+ open: boolean;
10
+ orderId: string;
11
+ currency: string;
12
+ remainingRefundable: number;
13
+ onOpenChange: (open: boolean) => void;
14
+ onRefunded: () => void;
15
+ };
16
+
17
+ let {
18
+ open = $bindable(),
19
+ orderId,
20
+ currency,
21
+ remainingRefundable,
22
+ onOpenChange,
23
+ onRefunded
24
+ }: Props = $props();
25
+
26
+ const remotes = getRemotes();
27
+
28
+ let mode = $state<'full' | 'partial'>('full');
29
+ let amountInput = $state('');
30
+ let reason = $state('');
31
+ let submitting = $state(false);
32
+ let error = $state<string | null>(null);
33
+
34
+ const remainingDisplay = $derived(
35
+ new Intl.NumberFormat('pl-PL', {
36
+ style: 'currency',
37
+ currency,
38
+ minimumFractionDigits: 2
39
+ }).format(remainingRefundable / 100)
40
+ );
41
+
42
+ function reset() {
43
+ mode = 'full';
44
+ amountInput = '';
45
+ reason = '';
46
+ error = null;
47
+ submitting = false;
48
+ }
49
+
50
+ async function handleSubmit(e: Event) {
51
+ e.preventDefault();
52
+ error = null;
53
+
54
+ let amount: number | undefined;
55
+ if (mode === 'partial') {
56
+ const pln = Number(amountInput.replace(',', '.'));
57
+ if (!Number.isFinite(pln) || pln <= 0) {
58
+ error = 'Kwota musi być większa od zera.';
59
+ return;
60
+ }
61
+ amount = Math.round(pln * 100);
62
+ if (amount > remainingRefundable) {
63
+ error = `Kwota nie może przekroczyć pozostałych ${remainingDisplay}.`;
64
+ return;
65
+ }
66
+ }
67
+
68
+ submitting = true;
69
+ try {
70
+ const result = await remotes.refundOrderCmd({
71
+ orderId,
72
+ amount,
73
+ reason: reason.trim() || undefined
74
+ });
75
+ if (!result.success) {
76
+ error = result.error;
77
+ submitting = false;
78
+ return;
79
+ }
80
+ reset();
81
+ onOpenChange(false);
82
+ onRefunded();
83
+ } catch (err) {
84
+ error = err instanceof Error ? err.message : 'Nie udało się wykonać zwrotu.';
85
+ submitting = false;
86
+ }
87
+ }
88
+ </script>
89
+
90
+ <Dialog.Root
91
+ {open}
92
+ onOpenChange={(v) => {
93
+ if (!v) reset();
94
+ onOpenChange(v);
95
+ }}
96
+ >
97
+ <Dialog.Content>
98
+ <Dialog.Header>
99
+ <Dialog.Title>Zwrot środków</Dialog.Title>
100
+ <Dialog.Description>
101
+ Pozostała kwota do zwrotu: <strong>{remainingDisplay}</strong>.
102
+ </Dialog.Description>
103
+ </Dialog.Header>
104
+ <form onsubmit={handleSubmit} class="space-y-4">
105
+ <fieldset class="space-y-2">
106
+ <legend class="mb-1 text-sm font-semibold">Typ zwrotu</legend>
107
+ <label class="flex items-center gap-2 text-sm">
108
+ <input type="radio" name="refund-mode" value="full" bind:group={mode} />
109
+ <span>Pełny zwrot ({remainingDisplay})</span>
110
+ </label>
111
+ <label class="flex items-center gap-2 text-sm">
112
+ <input type="radio" name="refund-mode" value="partial" bind:group={mode} />
113
+ <span>Częściowy zwrot</span>
114
+ </label>
115
+ </fieldset>
116
+
117
+ {#if mode === 'partial'}
118
+ <div class="space-y-2">
119
+ <Label for="refund-amount">Kwota (PLN)</Label>
120
+ <Input
121
+ id="refund-amount"
122
+ type="text"
123
+ inputmode="decimal"
124
+ bind:value={amountInput}
125
+ placeholder="np. 49,99"
126
+ required
127
+ />
128
+ </div>
129
+ {/if}
130
+
131
+ <div class="space-y-2">
132
+ <Label for="refund-reason">Powód (opcjonalnie)</Label>
133
+ <Input
134
+ id="refund-reason"
135
+ type="text"
136
+ bind:value={reason}
137
+ maxlength={500}
138
+ placeholder="np. anulowanie zamówienia przez klienta"
139
+ />
140
+ </div>
141
+
142
+ {#if error}
143
+ <p class="text-destructive text-sm" role="alert">{error}</p>
144
+ {/if}
145
+
146
+ <Dialog.Footer>
147
+ <Button
148
+ type="button"
149
+ variant="outline"
150
+ onclick={() => onOpenChange(false)}
151
+ disabled={submitting}
152
+ >
153
+ Anuluj
154
+ </Button>
155
+ <Button type="submit" disabled={submitting}>
156
+ {submitting ? 'Wykonuję…' : 'Wykonaj zwrot'}
157
+ </Button>
158
+ </Dialog.Footer>
159
+ </form>
160
+ </Dialog.Content>
161
+ </Dialog.Root>
@@ -0,0 +1,11 @@
1
+ type Props = {
2
+ open: boolean;
3
+ orderId: string;
4
+ currency: string;
5
+ remainingRefundable: number;
6
+ onOpenChange: (open: boolean) => void;
7
+ onRefunded: () => void;
8
+ };
9
+ declare const RefundDialog: import("svelte").Component<Props, {}, "open">;
10
+ type RefundDialog = ReturnType<typeof RefundDialog>;
11
+ export default RefundDialog;
@@ -4,9 +4,7 @@
4
4
  import { getRemotes } from '../../../sveltekit/index.js';
5
5
  import { Button } from '../../../components/ui/button/index.js';
6
6
  import TrashIcon from '@tabler/icons-svelte/icons/trash';
7
- import ShippingMethodForm, {
8
- type ShippingFormPayload
9
- } from './shipping-method-form.svelte';
7
+ import ShippingMethodForm, { type ShippingFormPayload } from './shipping-method-form.svelte';
10
8
 
11
9
  const remotes = getRemotes();
12
10
 
@@ -59,9 +57,8 @@
59
57
  <div class="flex items-start justify-between gap-4">
60
58
  <div>
61
59
  <h1 class="text-2xl font-extrabold tracking-tight">Edycja metody wysyłki</h1>
62
- <a
63
- href="/admin/shop/shipping-methods"
64
- class="text-muted-foreground text-sm hover:underline">← Wróć do listy</a
60
+ <a href="/admin/shop/shipping-methods" class="text-muted-foreground text-sm hover:underline"
61
+ >← Wróć do listy</a
65
62
  >
66
63
  </div>
67
64
  <div>
@@ -69,16 +69,10 @@
69
69
  }
70
70
 
71
71
  let names = $state<Record<string, string>>(
72
- languages.reduce(
73
- (acc, l) => ({ ...acc, [l]: asRecord(initial?.name)[l] ?? '' }),
74
- {}
75
- )
72
+ languages.reduce((acc, l) => ({ ...acc, [l]: asRecord(initial?.name)[l] ?? '' }), {})
76
73
  );
77
74
  let descriptions = $state<Record<string, string>>(
78
- languages.reduce(
79
- (acc, l) => ({ ...acc, [l]: asRecord(initial?.description)[l] ?? '' }),
80
- {}
81
- )
75
+ languages.reduce((acc, l) => ({ ...acc, [l]: asRecord(initial?.description)[l] ?? '' }), {})
82
76
  );
83
77
  type InputMode = 'net' | 'gross';
84
78
  // initial.price — teraz PLN (number, netto) z API. Hydratujemy jako gross, żeby user widział dokładnie to co wpisał.
@@ -86,9 +80,7 @@
86
80
  const initialVat = Number(initial?.vatRate ?? vatRates[0] ?? 23);
87
81
  let inputMode = $state<InputMode>(initialNetPln != null ? 'gross' : 'gross');
88
82
  let inputPrice = $state(
89
- initialNetPln != null
90
- ? (initialNetPln * (1 + initialVat / 100)).toFixed(2)
91
- : '0.00'
83
+ initialNetPln != null ? (initialNetPln * (1 + initialVat / 100)).toFixed(2) : '0.00'
92
84
  );
93
85
  let vatRate = $state<number | string>(initial?.vatRate ?? vatRates[0] ?? 23);
94
86
 
@@ -118,7 +110,8 @@
118
110
  let isActive = $state(initial?.isActive ?? true);
119
111
  // null/undefined = no restriction (all allowed); otherwise a whitelist.
120
112
  let restrictPayments = $state(
121
- Array.isArray(initial?.allowedPaymentMethods) && (initial?.allowedPaymentMethods?.length ?? 0) > 0
113
+ Array.isArray(initial?.allowedPaymentMethods) &&
114
+ (initial?.allowedPaymentMethods?.length ?? 0) > 0
122
115
  );
123
116
  let allowedPaymentIds = $state<string[]>(
124
117
  Array.isArray(initial?.allowedPaymentMethods) ? [...(initial?.allowedPaymentMethods ?? [])] : []
@@ -248,17 +241,19 @@
248
241
  </select>
249
242
  </label>
250
243
  </div>
251
- <div class="bg-muted/40 border-border grid grid-cols-3 gap-2 rounded-lg border p-2.5 text-center text-xs">
244
+ <div
245
+ class="bg-muted/40 border-border grid grid-cols-3 gap-2 rounded-lg border p-2.5 text-center text-xs"
246
+ >
252
247
  <div>
253
- <div class="text-muted-foreground font-semibold uppercase tracking-wide">Netto</div>
248
+ <div class="text-muted-foreground font-semibold tracking-wide uppercase">Netto</div>
254
249
  <div class="text-sm font-bold tabular-nums">{formatPln(netPln)} zł</div>
255
250
  </div>
256
251
  <div class="border-border border-x">
257
- <div class="text-muted-foreground font-semibold uppercase tracking-wide">VAT</div>
252
+ <div class="text-muted-foreground font-semibold tracking-wide uppercase">VAT</div>
258
253
  <div class="text-sm font-bold tabular-nums">{formatPln(vatPln)} zł</div>
259
254
  </div>
260
255
  <div>
261
- <div class="text-muted-foreground font-semibold uppercase tracking-wide">Brutto</div>
256
+ <div class="text-muted-foreground font-semibold tracking-wide uppercase">Brutto</div>
262
257
  <div class="text-primary text-sm font-bold tabular-nums">{formatPln(grossPln)} zł</div>
263
258
  </div>
264
259
  </div>
@@ -284,8 +279,8 @@
284
279
  <section class="border-border bg-card space-y-4 rounded-xl border p-6">
285
280
  <h2 class="text-lg font-bold">Dozwolone metody płatności</h2>
286
281
  <p class="text-muted-foreground text-sm">
287
- Ogranicz metody płatności dla tej dostawy (np. wyłącz płatność za pobraniem dla
288
- paczkomatu). Jeśli wyłączone — wszystkie skonfigurowane metody są dostępne.
282
+ Ogranicz metody płatności dla tej dostawy (np. wyłącz płatność za pobraniem dla paczkomatu).
283
+ Jeśli wyłączone — wszystkie skonfigurowane metody są dostępne.
289
284
  </p>
290
285
  <label class="flex items-center gap-2">
291
286
  <Switch bind:checked={restrictPayments} />
@@ -307,8 +302,7 @@
307
302
  {/each}
308
303
  {#if allowedPaymentIds.length === 0}
309
304
  <p class="text-xs text-amber-700">
310
- Wybierz co najmniej jedną metodę, inaczej checkout dla tej dostawy będzie
311
- zablokowany.
305
+ Wybierz co najmniej jedną metodę, inaczej checkout dla tej dostawy będzie zablokowany.
312
306
  </p>
313
307
  {/if}
314
308
  </div>
@@ -327,7 +321,7 @@
327
321
  </label>
328
322
 
329
323
  {#if carrierType === 'inpost'}
330
- <div class="space-y-4 border-l-2 border-primary/30 pl-4">
324
+ <div class="border-primary/30 space-y-4 border-l-2 pl-4">
331
325
  <p class="text-muted-foreground text-sm">
332
326
  Wymaga skonfigurowanego adaptera <code>inpostAdapter</code> w <code>cms.config.ts</code>.
333
327
  </p>
@@ -1,9 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { goto } from '$app/navigation';
3
3
  import { getRemotes } from '../../../sveltekit/index.js';
4
- import ShippingMethodForm, {
5
- type ShippingFormPayload
6
- } from './shipping-method-form.svelte';
4
+ import ShippingMethodForm, { type ShippingFormPayload } from './shipping-method-form.svelte';
7
5
 
8
6
  const remotes = getRemotes();
9
7
  const configQuery = $derived(remotes.getShopConfig());
@@ -31,9 +29,8 @@
31
29
  <div class="space-y-6 p-6">
32
30
  <div>
33
31
  <h1 class="text-2xl font-extrabold tracking-tight">Nowa metoda wysyłki</h1>
34
- <a
35
- href="/admin/shop/shipping-methods"
36
- class="text-muted-foreground text-sm hover:underline">← Wróć do listy</a
32
+ <a href="/admin/shop/shipping-methods" class="text-muted-foreground text-sm hover:underline"
33
+ >← Wróć do listy</a
37
34
  >
38
35
  </div>
39
36
  <ShippingMethodForm