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
@@ -12,3 +12,9 @@ export { default as AcceptInvitePage } from './users/accept-invite-page.svelte';
12
12
  export { default as ResetPasswordPage } from './login/reset-password-page.svelte';
13
13
  export { default as MaintenancePage } from './maintenance/maintenance-page.svelte';
14
14
  export { default as MediaSelector } from '../components/media/media-selector.svelte';
15
+ export { default as ShopProductsListPage } from './shop/shop-products-list-page.svelte';
16
+ export { default as ShippingMethodsListPage } from './shop/shipping-methods-list-page.svelte';
17
+ export { default as ShippingMethodNewPage } from './shop/shipping-method-new-page.svelte';
18
+ export { default as ShippingMethodEditPage } from './shop/shipping-method-edit-page.svelte';
19
+ export { default as ShopOrdersListPage } from './shop/shop-orders-list-page.svelte';
20
+ export { default as ShopOrderDetailPage } from './shop/shop-order-detail-page.svelte';
@@ -12,3 +12,9 @@ export { default as AcceptInvitePage } from './users/accept-invite-page.svelte';
12
12
  export { default as ResetPasswordPage } from './login/reset-password-page.svelte';
13
13
  export { default as MaintenancePage } from './maintenance/maintenance-page.svelte';
14
14
  export { default as MediaSelector } from '../components/media/media-selector.svelte';
15
+ export { default as ShopProductsListPage } from './shop/shop-products-list-page.svelte';
16
+ export { default as ShippingMethodsListPage } from './shop/shipping-methods-list-page.svelte';
17
+ export { default as ShippingMethodNewPage } from './shop/shipping-method-new-page.svelte';
18
+ export { default as ShippingMethodEditPage } from './shop/shipping-method-edit-page.svelte';
19
+ export { default as ShopOrdersListPage } from './shop/shop-orders-list-page.svelte';
20
+ export { default as ShopOrderDetailPage } from './shop/shop-order-detail-page.svelte';
@@ -0,0 +1,113 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import { goto } from '$app/navigation';
4
+ import { getRemotes } from '../../../sveltekit/index.js';
5
+ import { Button } from '../../../components/ui/button/index.js';
6
+ import TrashIcon from '@tabler/icons-svelte/icons/trash';
7
+ import ShippingMethodForm, {
8
+ type ShippingFormPayload
9
+ } from './shipping-method-form.svelte';
10
+
11
+ const remotes = getRemotes();
12
+
13
+ const methodId = $derived(page.params.id ?? '');
14
+ const methodQuery = $derived(remotes.getShippingMethodForAdmin(methodId));
15
+ const configQuery = $derived(remotes.getShopConfig());
16
+
17
+ let saving = $state(false);
18
+ let deleting = $state(false);
19
+ let errorMessage = $state<string | null>(null);
20
+ let successMessage = $state<string | null>(null);
21
+ let confirmingDelete = $state(false);
22
+
23
+ async function handleSubmit(payload: ShippingFormPayload) {
24
+ saving = true;
25
+ errorMessage = null;
26
+ successMessage = null;
27
+ try {
28
+ await remotes.updateShippingMethodCmd({ id: methodId, input: payload });
29
+ await methodQuery.refresh();
30
+ successMessage = 'Zapisano zmiany.';
31
+ } catch (err) {
32
+ errorMessage = err instanceof Error ? err.message : 'Nie udało się zapisać';
33
+ } finally {
34
+ saving = false;
35
+ }
36
+ }
37
+
38
+ async function handleDelete() {
39
+ deleting = true;
40
+ try {
41
+ await remotes.deleteShippingMethodCmd(methodId);
42
+ await goto('/admin/shop/shipping-methods');
43
+ } catch (err) {
44
+ errorMessage = err instanceof Error ? err.message : 'Nie udało się usunąć';
45
+ deleting = false;
46
+ }
47
+ }
48
+ </script>
49
+
50
+ {#if !methodQuery.ready || !configQuery.ready}
51
+ <div class="text-muted-foreground p-6">Ładowanie…</div>
52
+ {:else if !methodQuery.current}
53
+ <div class="p-6">
54
+ <p class="mb-4">Metoda nie została znaleziona.</p>
55
+ <Button href="/admin/shop/shipping-methods" variant="outline">← Wróć do listy</Button>
56
+ </div>
57
+ {:else if configQuery.current}
58
+ <div class="space-y-6 p-6">
59
+ <div class="flex items-start justify-between gap-4">
60
+ <div>
61
+ <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
65
+ >
66
+ </div>
67
+ <div>
68
+ {#if confirmingDelete}
69
+ <div class="flex items-center gap-2">
70
+ <span class="text-sm">Na pewno?</span>
71
+ <Button
72
+ type="button"
73
+ variant="destructive"
74
+ size="sm"
75
+ onclick={handleDelete}
76
+ disabled={deleting}
77
+ >
78
+ {deleting ? 'Usuwanie…' : 'Usuń'}
79
+ </Button>
80
+ <Button
81
+ type="button"
82
+ variant="outline"
83
+ size="sm"
84
+ onclick={() => (confirmingDelete = false)}>Anuluj</Button
85
+ >
86
+ </div>
87
+ {:else}
88
+ <Button
89
+ type="button"
90
+ variant="outline"
91
+ size="sm"
92
+ onclick={() => (confirmingDelete = true)}
93
+ >
94
+ <TrashIcon class="mr-1 size-4" /> Usuń
95
+ </Button>
96
+ {/if}
97
+ </div>
98
+ </div>
99
+
100
+ {#if successMessage}
101
+ <div class="rounded-lg bg-green-50 p-3 text-sm text-green-800">{successMessage}</div>
102
+ {/if}
103
+
104
+ <ShippingMethodForm
105
+ languages={configQuery.current.languages}
106
+ vatRates={configQuery.current.vatRates}
107
+ initial={methodQuery.current}
108
+ {saving}
109
+ {errorMessage}
110
+ onsubmit={handleSubmit}
111
+ />
112
+ </div>
113
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const ShippingMethodEditPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ShippingMethodEditPage = ReturnType<typeof ShippingMethodEditPage>;
3
+ export default ShippingMethodEditPage;
@@ -0,0 +1,244 @@
1
+ <script lang="ts">
2
+ import { Button } from '../../../components/ui/button/index.js';
3
+ import { Switch } from '../../../components/ui/switch/index.js';
4
+
5
+ type CarrierType = 'none' | 'inpost';
6
+ export interface ShippingFormPayload {
7
+ name: Record<string, string>;
8
+ description: Record<string, string> | null;
9
+ price: number;
10
+ vatRate: number;
11
+ carrierType: CarrierType;
12
+ conditions: { freeAbove?: number } | null;
13
+ isActive: boolean;
14
+ sortOrder: number | null;
15
+ }
16
+ export interface ShippingFormInitial {
17
+ name?: Record<string, string> | unknown;
18
+ description?: Record<string, string> | unknown | null;
19
+ price?: number;
20
+ vatRate?: number;
21
+ carrierType?: string;
22
+ conditions?: { freeAbove?: number } | null;
23
+ isActive?: boolean;
24
+ sortOrder?: number | null;
25
+ }
26
+
27
+ interface Props {
28
+ languages: string[];
29
+ vatRates: number[];
30
+ initial?: ShippingFormInitial | null;
31
+ saving?: boolean;
32
+ errorMessage?: string | null;
33
+ submitLabel?: string;
34
+ onsubmit: (payload: ShippingFormPayload) => void | Promise<void>;
35
+ }
36
+
37
+ const {
38
+ languages,
39
+ vatRates,
40
+ initial = null,
41
+ saving = false,
42
+ errorMessage = null,
43
+ submitLabel = 'Zapisz',
44
+ onsubmit
45
+ }: Props = $props();
46
+
47
+ function asRecord(v: unknown): Record<string, string> {
48
+ return v && typeof v === 'object' ? (v as Record<string, string>) : {};
49
+ }
50
+
51
+ let names = $state<Record<string, string>>(
52
+ languages.reduce(
53
+ (acc, l) => ({ ...acc, [l]: asRecord(initial?.name)[l] ?? '' }),
54
+ {}
55
+ )
56
+ );
57
+ let descriptions = $state<Record<string, string>>(
58
+ languages.reduce(
59
+ (acc, l) => ({ ...acc, [l]: asRecord(initial?.description)[l] ?? '' }),
60
+ {}
61
+ )
62
+ );
63
+ type InputMode = 'net' | 'gross';
64
+ let inputMode = $state<InputMode>(initial?.price != null ? 'net' : 'gross');
65
+ let inputPrice = $state(initial?.price != null ? (initial.price / 100).toFixed(2) : '0.00');
66
+ let vatRate = $state<number | string>(initial?.vatRate ?? vatRates[0] ?? 23);
67
+
68
+ const inputPriceCents = $derived(Math.round(parseFloat(inputPrice || '0') * 100));
69
+ const vat = $derived(Number(vatRate) || 0);
70
+ const netCents = $derived(
71
+ inputMode === 'net' ? inputPriceCents : Math.round(inputPriceCents / (1 + vat / 100))
72
+ );
73
+ const grossCents = $derived(
74
+ inputMode === 'gross' ? inputPriceCents : Math.round(inputPriceCents * (1 + vat / 100))
75
+ );
76
+ const vatCents = $derived(grossCents - netCents);
77
+
78
+ function formatCents(cents: number) {
79
+ return (cents / 100).toFixed(2);
80
+ }
81
+
82
+ function switchMode(newMode: InputMode) {
83
+ if (newMode === inputMode) return;
84
+ const preservedCents = newMode === 'net' ? netCents : grossCents;
85
+ inputMode = newMode;
86
+ inputPrice = formatCents(preservedCents);
87
+ }
88
+ let carrierType = $state<CarrierType>((initial?.carrierType as CarrierType) ?? 'none');
89
+ let isActive = $state(initial?.isActive ?? true);
90
+ let freeAboveEnabled = $state(initial?.conditions?.freeAbove != null);
91
+ let freeAbove = $state(
92
+ initial?.conditions?.freeAbove != null
93
+ ? (initial.conditions.freeAbove / 100).toFixed(2)
94
+ : '200.00'
95
+ );
96
+ async function handleSubmit(e: SubmitEvent) {
97
+ e.preventDefault();
98
+ await onsubmit({
99
+ name: names,
100
+ description: Object.values(descriptions).some((d) => d.length > 0) ? descriptions : null,
101
+ price: netCents,
102
+ vatRate: Number(vatRate),
103
+ carrierType,
104
+ conditions: freeAboveEnabled
105
+ ? { freeAbove: Math.round(parseFloat(freeAbove || '0') * 100) }
106
+ : null,
107
+ isActive,
108
+ sortOrder: initial?.sortOrder ?? null
109
+ });
110
+ }
111
+ </script>
112
+
113
+ <form onsubmit={handleSubmit} class="space-y-6">
114
+ {#if errorMessage}
115
+ <div class="rounded-lg bg-red-50 p-3 text-sm text-red-800">{errorMessage}</div>
116
+ {/if}
117
+
118
+ <section class="border-border bg-card space-y-4 rounded-xl border p-6">
119
+ <h2 class="text-lg font-bold">Podstawowe informacje</h2>
120
+
121
+ {#each languages as lang (lang)}
122
+ <label class="block">
123
+ <span class="mb-1 block text-sm font-semibold">Nazwa ({lang})</span>
124
+ <input
125
+ type="text"
126
+ bind:value={names[lang]}
127
+ required={lang === languages[0]}
128
+ class="border-border w-full rounded-lg border px-3 py-2"
129
+ />
130
+ </label>
131
+ {/each}
132
+
133
+ {#each languages as lang (lang)}
134
+ <label class="block">
135
+ <span class="mb-1 block text-sm font-semibold">Opis ({lang})</span>
136
+ <textarea
137
+ bind:value={descriptions[lang]}
138
+ rows="2"
139
+ class="border-border w-full rounded-lg border px-3 py-2"
140
+ ></textarea>
141
+ </label>
142
+ {/each}
143
+
144
+ <label class="flex items-center gap-2">
145
+ <Switch bind:checked={isActive} />
146
+ <span class="text-sm">Aktywna (widoczna w checkout)</span>
147
+ </label>
148
+ </section>
149
+
150
+ <section class="border-border bg-card space-y-4 rounded-xl border p-6">
151
+ <h2 class="text-lg font-bold">Cena i VAT</h2>
152
+ <div class="grid grid-cols-[1fr_auto] gap-3">
153
+ <label class="block">
154
+ <div class="mb-1 flex items-center justify-between">
155
+ <span class="text-muted-foreground text-xs font-semibold">
156
+ Cena ({inputMode === 'net' ? 'netto' : 'brutto'}, PLN)
157
+ </span>
158
+ <div class="bg-muted inline-flex rounded-md p-0.5 text-xs">
159
+ <button
160
+ type="button"
161
+ class="rounded px-2 py-0.5 {inputMode === 'net'
162
+ ? 'bg-background text-primary font-semibold shadow-sm'
163
+ : 'text-muted-foreground'}"
164
+ onclick={() => switchMode('net')}
165
+ >
166
+ Netto
167
+ </button>
168
+ <button
169
+ type="button"
170
+ class="rounded px-2 py-0.5 {inputMode === 'gross'
171
+ ? 'bg-background text-primary font-semibold shadow-sm'
172
+ : 'text-muted-foreground'}"
173
+ onclick={() => switchMode('gross')}
174
+ >
175
+ Brutto
176
+ </button>
177
+ </div>
178
+ </div>
179
+ <input
180
+ type="number"
181
+ step="0.01"
182
+ min="0"
183
+ bind:value={inputPrice}
184
+ required
185
+ class="border-border w-full rounded-lg border px-3 py-2"
186
+ />
187
+ </label>
188
+ <label class="block">
189
+ <span class="text-muted-foreground mb-1 block text-xs font-semibold">VAT</span>
190
+ <select bind:value={vatRate} class="border-border w-full rounded-lg border px-3 py-2">
191
+ {#each vatRates as r (r)}
192
+ <option value={r}>{r}%</option>
193
+ {/each}
194
+ </select>
195
+ </label>
196
+ </div>
197
+ <div class="bg-muted/40 border-border grid grid-cols-3 gap-2 rounded-lg border p-2.5 text-center text-xs">
198
+ <div>
199
+ <div class="text-muted-foreground font-semibold uppercase tracking-wide">Netto</div>
200
+ <div class="text-sm font-bold tabular-nums">{formatCents(netCents)} zł</div>
201
+ </div>
202
+ <div class="border-border border-x">
203
+ <div class="text-muted-foreground font-semibold uppercase tracking-wide">VAT</div>
204
+ <div class="text-sm font-bold tabular-nums">{formatCents(vatCents)} zł</div>
205
+ </div>
206
+ <div>
207
+ <div class="text-muted-foreground font-semibold uppercase tracking-wide">Brutto</div>
208
+ <div class="text-primary text-sm font-bold tabular-nums">{formatCents(grossCents)} zł</div>
209
+ </div>
210
+ </div>
211
+ <label class="flex items-center gap-2">
212
+ <Switch bind:checked={freeAboveEnabled} />
213
+ <span class="text-sm">Darmowa dostawa powyżej progu</span>
214
+ </label>
215
+ {#if freeAboveEnabled}
216
+ <label class="block">
217
+ <span class="mb-1 block text-sm font-semibold">Próg (brutto, PLN)</span>
218
+ <input
219
+ type="number"
220
+ step="0.01"
221
+ min="0"
222
+ bind:value={freeAbove}
223
+ class="border-border w-full rounded-lg border px-3 py-2"
224
+ />
225
+ </label>
226
+ {/if}
227
+ </section>
228
+
229
+ <section class="border-border bg-card space-y-4 rounded-xl border p-6">
230
+ <h2 class="text-lg font-bold">Przewoźnik</h2>
231
+ <label class="block">
232
+ <span class="mb-1 block text-sm font-semibold">Typ</span>
233
+ <select bind:value={carrierType} class="border-border w-full rounded-lg border px-3 py-2">
234
+ <option value="none">Bez integracji (adres)</option>
235
+ <option value="inpost" disabled>InPost paczkomat (wkrótce)</option>
236
+ </select>
237
+ </label>
238
+ </section>
239
+
240
+ <div class="flex gap-3">
241
+ <Button type="submit" disabled={saving}>{saving ? 'Zapisywanie…' : submitLabel}</Button>
242
+ <Button type="button" variant="outline" href="/admin/shop/shipping-methods">Anuluj</Button>
243
+ </div>
244
+ </form>
@@ -0,0 +1,37 @@
1
+ type CarrierType = 'none' | 'inpost';
2
+ export interface ShippingFormPayload {
3
+ name: Record<string, string>;
4
+ description: Record<string, string> | null;
5
+ price: number;
6
+ vatRate: number;
7
+ carrierType: CarrierType;
8
+ conditions: {
9
+ freeAbove?: number;
10
+ } | null;
11
+ isActive: boolean;
12
+ sortOrder: number | null;
13
+ }
14
+ export interface ShippingFormInitial {
15
+ name?: Record<string, string> | unknown;
16
+ description?: Record<string, string> | unknown | null;
17
+ price?: number;
18
+ vatRate?: number;
19
+ carrierType?: string;
20
+ conditions?: {
21
+ freeAbove?: number;
22
+ } | null;
23
+ isActive?: boolean;
24
+ sortOrder?: number | null;
25
+ }
26
+ interface Props {
27
+ languages: string[];
28
+ vatRates: number[];
29
+ initial?: ShippingFormInitial | null;
30
+ saving?: boolean;
31
+ errorMessage?: string | null;
32
+ submitLabel?: string;
33
+ onsubmit: (payload: ShippingFormPayload) => void | Promise<void>;
34
+ }
35
+ declare const ShippingMethodForm: import("svelte").Component<Props, {}, "">;
36
+ type ShippingMethodForm = ReturnType<typeof ShippingMethodForm>;
37
+ export default ShippingMethodForm;
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import { goto } from '$app/navigation';
3
+ import { getRemotes } from '../../../sveltekit/index.js';
4
+ import ShippingMethodForm, {
5
+ type ShippingFormPayload
6
+ } from './shipping-method-form.svelte';
7
+
8
+ const remotes = getRemotes();
9
+ const configQuery = $derived(remotes.getShopConfig());
10
+
11
+ let saving = $state(false);
12
+ let errorMessage = $state<string | null>(null);
13
+
14
+ async function handleSubmit(payload: ShippingFormPayload) {
15
+ saving = true;
16
+ errorMessage = null;
17
+ try {
18
+ const row = await remotes.createShippingMethodCmd(payload);
19
+ await goto(`/admin/shop/shipping-methods/${row.id}`);
20
+ } catch (err) {
21
+ errorMessage = err instanceof Error ? err.message : 'Nie udało się zapisać';
22
+ } finally {
23
+ saving = false;
24
+ }
25
+ }
26
+ </script>
27
+
28
+ {#if !configQuery.ready}
29
+ <div class="text-muted-foreground p-6">Ładowanie…</div>
30
+ {:else if configQuery.current}
31
+ <div class="space-y-6 p-6">
32
+ <div>
33
+ <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
37
+ >
38
+ </div>
39
+ <ShippingMethodForm
40
+ languages={configQuery.current.languages}
41
+ vatRates={configQuery.current.vatRates}
42
+ {saving}
43
+ {errorMessage}
44
+ onsubmit={handleSubmit}
45
+ />
46
+ </div>
47
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const ShippingMethodNewPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ShippingMethodNewPage = ReturnType<typeof ShippingMethodNewPage>;
3
+ export default ShippingMethodNewPage;
@@ -0,0 +1,172 @@
1
+ <script lang="ts">
2
+ import { flip } from 'svelte/animate';
3
+ import { droppable, draggable, dndState } from '@thisux/sveltednd';
4
+ import { getRemotes } from '../../../sveltekit/index.js';
5
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
6
+ import { resolveI18n } from '../../../shop/pricing.js';
7
+ import { Button } from '../../../components/ui/button/index.js';
8
+ import PlusIcon from '@tabler/icons-svelte/icons/plus';
9
+ import GripIcon from '@tabler/icons-svelte/icons/grip-vertical';
10
+
11
+ const remotes = getRemotes();
12
+ const interfaceLanguage = useInterfaceLanguage();
13
+ const query = $derived(remotes.listShippingMethodsAdmin());
14
+
15
+ type Row = NonNullable<ReturnType<typeof remotes.listShippingMethodsAdmin>['current']>[number];
16
+
17
+ let localRows = $state<Row[]>([]);
18
+ let dropProcessing = false;
19
+
20
+ let matchesReducedMotion = $state(false);
21
+ $effect(() => {
22
+ if (typeof window !== 'undefined') {
23
+ matchesReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
24
+ }
25
+ });
26
+
27
+ $effect(() => {
28
+ if (query.ready && query.current) {
29
+ if (
30
+ localRows.length !== query.current.length ||
31
+ localRows.some((r, i) => r.id !== query.current![i].id)
32
+ ) {
33
+ localRows = [...query.current];
34
+ }
35
+ }
36
+ });
37
+
38
+ function formatPrice(smallest: number) {
39
+ return new Intl.NumberFormat('pl-PL', {
40
+ style: 'currency',
41
+ currency: 'PLN',
42
+ minimumFractionDigits: 2
43
+ }).format(smallest / 100);
44
+ }
45
+
46
+ async function doReorder(fromIndex: number, toIndex: number) {
47
+ if (fromIndex === toIndex) return;
48
+ const next = [...localRows];
49
+ const [moved] = next.splice(fromIndex, 1);
50
+ next.splice(toIndex, 0, moved);
51
+ localRows = next;
52
+ try {
53
+ await remotes.reorderShippingMethodsCmd(next.map((r) => r.id));
54
+ await query.refresh();
55
+ } catch {
56
+ await query.refresh();
57
+ }
58
+ }
59
+ </script>
60
+
61
+ <div class="flex items-center justify-between gap-4 p-6">
62
+ <div>
63
+ <h1 class="text-2xl font-extrabold tracking-tight">Metody wysyłki</h1>
64
+ <p class="text-muted-foreground text-sm">
65
+ {#if query.ready}
66
+ {localRows.length}
67
+ {localRows.length === 1 ? 'metoda' : 'metod'} · przeciągnij
68
+ <GripIcon class="inline size-3.5 align-[-2px]" />
69
+ by zmienić kolejność
70
+ {:else}
71
+ Ładowanie…
72
+ {/if}
73
+ </p>
74
+ </div>
75
+ <Button href="/admin/shop/shipping-methods/new">
76
+ <PlusIcon class="mr-1 size-4" />
77
+ Dodaj metodę
78
+ </Button>
79
+ </div>
80
+
81
+ <div class="px-6 pb-12">
82
+ {#if !query.ready}
83
+ <div class="text-muted-foreground">Ładowanie…</div>
84
+ {:else if localRows.length === 0}
85
+ <div class="bg-lavender-lighter/40 border-border rounded-xl border p-8 text-center text-sm">
86
+ <p class="mb-2 font-semibold">Brak metod wysyłki</p>
87
+ <p class="text-muted-foreground">
88
+ Dodaj pierwszą metodę — np. kurier, paczkomat, odbiór osobisty.
89
+ </p>
90
+ </div>
91
+ {:else}
92
+ <div class="border-border overflow-hidden rounded-xl border">
93
+ <div class="bg-muted/50 grid grid-cols-[32px_minmax(0,2fr)_minmax(0,1fr)_minmax(0,0.6fr)_minmax(0,1fr)_minmax(0,0.8fr)_minmax(0,1fr)] gap-3 px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide">
94
+ <div></div>
95
+ <div>Nazwa</div>
96
+ <div>Cena (netto)</div>
97
+ <div>VAT</div>
98
+ <div>Darmowa od</div>
99
+ <div>Typ</div>
100
+ <div>Status</div>
101
+ </div>
102
+ <div>
103
+ {#each localRows as m, i (m.id)}
104
+ {@const cond = m.conditions as { freeAbove?: number } | null}
105
+ <div
106
+ use:droppable={{
107
+ container: i.toString(),
108
+ callbacks: {
109
+ onDrop: (state) => {
110
+ if (dropProcessing) return;
111
+ dropProcessing = true;
112
+ const fromIndex = parseInt(state.sourceContainer ?? '');
113
+ const toIndex = parseInt(state.targetContainer ?? '');
114
+ if (!isNaN(fromIndex) && !isNaN(toIndex)) doReorder(fromIndex, toIndex);
115
+ dndState.isDragging = false;
116
+ dndState.draggedItem = null;
117
+ dndState.sourceContainer = '';
118
+ dndState.targetContainer = null;
119
+ dndState.targetElement = null;
120
+ setTimeout(() => {
121
+ dropProcessing = false;
122
+ }, 50);
123
+ }
124
+ }
125
+ }}
126
+ animate:flip={{ duration: matchesReducedMotion ? 0 : 200 }}
127
+ class="border-border hover:bg-muted/30 grid grid-cols-[32px_minmax(0,2fr)_minmax(0,1fr)_minmax(0,0.6fr)_minmax(0,1fr)_minmax(0,0.8fr)_minmax(0,1fr)] items-center gap-3 border-t px-4 py-3 text-sm transition-colors"
128
+ >
129
+ <div
130
+ use:draggable={{ container: i.toString(), dragData: { index: i } }}
131
+ class="text-muted-foreground hover:text-foreground flex cursor-grab items-center justify-center"
132
+ role="button"
133
+ tabindex="0"
134
+ aria-label={`Drag to reorder, row ${i + 1} of ${localRows.length}`}
135
+ >
136
+ <GripIcon class="size-4" />
137
+ </div>
138
+ <div>
139
+ <a
140
+ href={`/admin/shop/shipping-methods/${m.id}`}
141
+ class="text-primary hover:underline"
142
+ >
143
+ {resolveI18n(m.name as Record<string, string>, interfaceLanguage.current, '')}
144
+ </a>
145
+ </div>
146
+ <div>{formatPrice(m.price)}</div>
147
+ <div>{m.vatRate}%</div>
148
+ <div>
149
+ {#if cond?.freeAbove != null}
150
+ {formatPrice(cond.freeAbove)}
151
+ {:else}
152
+ <span class="text-muted-foreground text-xs">—</span>
153
+ {/if}
154
+ </div>
155
+ <div class="text-muted-foreground font-mono text-xs">{m.carrierType}</div>
156
+ <div>
157
+ {#if m.isActive}
158
+ <span class="inline-flex rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-800"
159
+ >Aktywna</span
160
+ >
161
+ {:else}
162
+ <span class="inline-flex rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-800"
163
+ >Nieaktywna</span
164
+ >
165
+ {/if}
166
+ </div>
167
+ </div>
168
+ {/each}
169
+ </div>
170
+ </div>
171
+ {/if}
172
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const ShippingMethodsListPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ShippingMethodsListPage = ReturnType<typeof ShippingMethodsListPage>;
3
+ export default ShippingMethodsListPage;