includio-cms 0.15.2 → 0.15.4

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 (77) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/DOCS.md +142 -3
  3. package/ROADMAP.md +13 -2
  4. package/dist/admin/client/shop/shipping-method-form.svelte +66 -1
  5. package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +8 -0
  6. package/dist/admin/client/shop/shop-order-detail-page.svelte +101 -0
  7. package/dist/admin/remote/shop.remote.d.ts +44 -0
  8. package/dist/admin/remote/shop.remote.js +35 -0
  9. package/dist/cli/index.js +49 -4
  10. package/dist/cli/scaffold/admin.d.ts +9 -2
  11. package/dist/cli/scaffold/admin.js +91 -3
  12. package/dist/core/server/forms/submissions/operations/create.js +11 -5
  13. package/dist/db-postgres/schema/shop/order.d.ts +68 -0
  14. package/dist/db-postgres/schema/shop/order.js +4 -0
  15. package/dist/db-postgres/schema/shop/shippingMethod.d.ts +25 -0
  16. package/dist/db-postgres/schema/shop/shippingMethod.js +1 -0
  17. package/dist/paraglide/messages/_index.d.ts +3 -36
  18. package/dist/paraglide/messages/_index.js +3 -71
  19. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  20. package/dist/paraglide/messages/hello_world.js +33 -0
  21. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  22. package/dist/paraglide/messages/login_hello.js +34 -0
  23. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  24. package/dist/paraglide/messages/login_please_login.js +34 -0
  25. package/dist/shop/adapters/inpost/geowidget.d.ts +27 -0
  26. package/dist/shop/adapters/inpost/geowidget.js +31 -0
  27. package/dist/shop/adapters/inpost/index.d.ts +89 -0
  28. package/dist/shop/adapters/inpost/index.js +156 -0
  29. package/dist/shop/adapters/inpost/payload.d.ts +18 -0
  30. package/dist/shop/adapters/inpost/payload.js +85 -0
  31. package/dist/shop/adapters/inpost/points-api.d.ts +17 -0
  32. package/dist/shop/adapters/inpost/points-api.js +55 -0
  33. package/dist/shop/adapters/inpost/shipx-client.d.ts +56 -0
  34. package/dist/shop/adapters/inpost/shipx-client.js +95 -0
  35. package/dist/shop/adapters/inpost/status-map.d.ts +9 -0
  36. package/dist/shop/adapters/inpost/status-map.js +46 -0
  37. package/dist/shop/adapters/inpost/webhook.d.ts +16 -0
  38. package/dist/shop/adapters/inpost/webhook.js +55 -0
  39. package/dist/shop/client/index.d.ts +5 -0
  40. package/dist/shop/http/carrier-handler.d.ts +12 -0
  41. package/dist/shop/http/carrier-handler.js +45 -0
  42. package/dist/shop/http/carrier-webhook-handler.d.ts +13 -0
  43. package/dist/shop/http/carrier-webhook-handler.js +66 -0
  44. package/dist/shop/http/checkout-handler.js +23 -1
  45. package/dist/shop/http/index.d.ts +3 -0
  46. package/dist/shop/http/index.js +3 -0
  47. package/dist/shop/http/order-handler.js +14 -0
  48. package/dist/shop/http/shipment-label-handler.d.ts +10 -0
  49. package/dist/shop/http/shipment-label-handler.js +53 -0
  50. package/dist/shop/http/shipping-handler.js +3 -0
  51. package/dist/shop/index.d.ts +3 -1
  52. package/dist/shop/index.js +1 -0
  53. package/dist/shop/server/email.js +37 -0
  54. package/dist/shop/server/orders.d.ts +9 -0
  55. package/dist/shop/server/orders.js +48 -0
  56. package/dist/shop/server/shipments.d.ts +33 -0
  57. package/dist/shop/server/shipments.js +145 -0
  58. package/dist/shop/server/shipping.d.ts +2 -1
  59. package/dist/shop/server/shipping.js +9 -0
  60. package/dist/shop/svelte/InpostPicker.svelte +270 -0
  61. package/dist/shop/svelte/InpostPicker.svelte.d.ts +51 -0
  62. package/dist/shop/svelte/OrderStatus.svelte +53 -1
  63. package/dist/shop/svelte/index.d.ts +1 -0
  64. package/dist/shop/svelte/index.js +1 -0
  65. package/dist/shop/svelte/labels.d.ts +5 -0
  66. package/dist/shop/svelte/labels.js +6 -1
  67. package/dist/shop/types.d.ts +49 -1
  68. package/dist/updates/0.15.3/index.d.ts +2 -0
  69. package/dist/updates/0.15.3/index.js +19 -0
  70. package/dist/updates/0.15.4/index.d.ts +2 -0
  71. package/dist/updates/0.15.4/index.js +14 -0
  72. package/dist/updates/index.js +3 -1
  73. package/package.json +1 -1
  74. package/dist/paraglide/messages/en.d.ts +0 -5
  75. package/dist/paraglide/messages/en.js +0 -14
  76. package/dist/paraglide/messages/pl.d.ts +0 -5
  77. package/dist/paraglide/messages/pl.js +0 -14
@@ -0,0 +1,145 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { shopShippingMethodsTable } from '../../db-postgres/schema/shop/index.js';
3
+ import { clearShipmentInfo, getOrderById, getOrderByShipmentId, setShipmentInfo, updateOrderStatus, updateTrackingNumber } from './orders.js';
4
+ import { getShopDb, requireShopConfig } from './db.js';
5
+ function findAdapterFor(order) {
6
+ if (!order.carrierType || order.carrierType === 'none') {
7
+ throw new Error('Order has no carrier configured.');
8
+ }
9
+ const shop = requireShopConfig();
10
+ const adapter = shop.carriers.find((c) => c.id === order.carrierType);
11
+ if (!adapter) {
12
+ throw new Error(`No carrier adapter registered for "${order.carrierType}". Add it to defineShop({ carriers: [...] }).`);
13
+ }
14
+ return adapter;
15
+ }
16
+ async function loadServiceConfig(shippingMethodId) {
17
+ if (!shippingMethodId) {
18
+ throw new Error('Order has no shipping method — cannot create shipment.');
19
+ }
20
+ const db = getShopDb();
21
+ const [row] = await db
22
+ .select()
23
+ .from(shopShippingMethodsTable)
24
+ .where(eq(shopShippingMethodsTable.id, shippingMethodId));
25
+ if (!row) {
26
+ throw new Error('Shipping method not found.');
27
+ }
28
+ const cfg = row.carrierConfig ?? {};
29
+ if (!cfg.serviceType) {
30
+ throw new Error('Shipping method has no carrierConfig.serviceType. Set it in admin → shipping methods.');
31
+ }
32
+ return { serviceType: cfg.serviceType, defaultSize: cfg.defaultSize };
33
+ }
34
+ /**
35
+ * Create a shipment for the order with the carrier configured on the order.
36
+ * Saves shipmentId/trackingNumber on the order and bumps status to `preparing`.
37
+ * Idempotent guard: throws if a shipment already exists (use `cancelShipmentForOrder` first).
38
+ */
39
+ export async function createShipmentForOrder(orderId) {
40
+ const order = await getOrderById(orderId);
41
+ if (!order)
42
+ throw new Error('Order not found.');
43
+ if (order.shipmentId) {
44
+ throw new Error(`Order already has a shipment (${order.shipmentId}). Cancel it first if you need to recreate.`);
45
+ }
46
+ const adapter = findAdapterFor(order);
47
+ if (!adapter.createShipment) {
48
+ throw new Error(`Carrier "${adapter.id}" does not support shipment creation.`);
49
+ }
50
+ const { serviceType, defaultSize } = await loadServiceConfig(order.shippingMethodId);
51
+ const result = await adapter.createShipment({
52
+ order: {
53
+ id: order.id,
54
+ number: order.number,
55
+ customerEmail: order.customerEmail,
56
+ customerName: order.customerName,
57
+ customerPhone: order.customerPhone
58
+ },
59
+ shippingAddress: order.shippingAddress,
60
+ carrierRef: order.carrierRef,
61
+ serviceType,
62
+ parcelSize: defaultSize,
63
+ cartTotalGross: order.totalGross,
64
+ language: order.language
65
+ });
66
+ await setShipmentInfo(order.id, {
67
+ shipmentId: result.shipmentId,
68
+ trackingNumber: result.trackingNumber || null,
69
+ labelUrl: result.labelUrl ?? null
70
+ });
71
+ if (order.status !== 'preparing' && order.status !== 'sent' && order.status !== 'done') {
72
+ await updateOrderStatus(order.id, 'preparing', {
73
+ note: `InPost shipment created (${result.shipmentId})`,
74
+ changedBy: 'admin'
75
+ });
76
+ }
77
+ const updated = await getOrderById(order.id);
78
+ if (!updated)
79
+ throw new Error('Order vanished after shipment update.');
80
+ return updated;
81
+ }
82
+ /**
83
+ * Cancel an existing shipment with the carrier and clear shipment data on the order.
84
+ * Order status is left untouched (admin decides whether to drop back to `paid`).
85
+ */
86
+ export async function cancelShipmentForOrder(orderId) {
87
+ const order = await getOrderById(orderId);
88
+ if (!order)
89
+ throw new Error('Order not found.');
90
+ if (!order.shipmentId) {
91
+ throw new Error('Order has no shipment to cancel.');
92
+ }
93
+ const adapter = findAdapterFor(order);
94
+ if (!adapter.cancelShipment) {
95
+ throw new Error(`Carrier "${adapter.id}" does not support shipment cancellation.`);
96
+ }
97
+ await adapter.cancelShipment(order.shipmentId);
98
+ await clearShipmentInfo(order.id);
99
+ const updated = await getOrderById(order.id);
100
+ if (!updated)
101
+ throw new Error('Order vanished after shipment cancel.');
102
+ return updated;
103
+ }
104
+ /**
105
+ * Fetch the shipping label PDF (or other format) from the carrier. Used by the
106
+ * admin label proxy endpoint.
107
+ */
108
+ export async function getShipmentLabelForOrder(orderId, opts = {}) {
109
+ const order = await getOrderById(orderId);
110
+ if (!order)
111
+ throw new Error('Order not found.');
112
+ if (!order.shipmentId)
113
+ throw new Error('Order has no shipment.');
114
+ const adapter = findAdapterFor(order);
115
+ if (!adapter.getShipmentLabel) {
116
+ throw new Error(`Carrier "${adapter.id}" does not support label fetching.`);
117
+ }
118
+ return adapter.getShipmentLabel(order.shipmentId, { size: opts.size });
119
+ }
120
+ /**
121
+ * Apply an incoming carrier event (e.g. from a webhook) to the matching order:
122
+ * update the tracking number when supplied and bump the order status when the
123
+ * mapped status differs from the current one. Unknown carrier statuses are
124
+ * skipped — caller still gets `matched=true` so the webhook can return 200.
125
+ */
126
+ export async function applyCarrierEvent(event) {
127
+ const order = await getOrderByShipmentId(event.shipmentId);
128
+ if (!order) {
129
+ return { matched: false };
130
+ }
131
+ let trackingChanged = false;
132
+ if (event.trackingNumber && event.trackingNumber !== order.trackingNumber) {
133
+ await updateTrackingNumber(order.id, event.trackingNumber);
134
+ trackingChanged = true;
135
+ }
136
+ let statusChanged = false;
137
+ if (event.status !== 'unknown' && event.status !== order.status) {
138
+ await updateOrderStatus(order.id, event.status, {
139
+ note: `Carrier event (${event.shipmentId})`,
140
+ changedBy: 'carrier-webhook'
141
+ });
142
+ statusChanged = true;
143
+ }
144
+ return { matched: true, orderId: order.id, statusChanged, trackingChanged };
145
+ }
@@ -1,5 +1,5 @@
1
1
  import { shopShippingMethodsTable } from '../../db-postgres/schema/shop/index.js';
2
- import type { ShopCarrierType } from '../../db-postgres/schema/shop/shippingMethod.js';
2
+ import type { ShippingCarrierConfig, ShopCarrierType } from '../../db-postgres/schema/shop/shippingMethod.js';
3
3
  type RawShippingMethodRow = typeof shopShippingMethodsTable.$inferSelect;
4
4
  export type ShippingMethodRow = Omit<RawShippingMethodRow, 'price'> & {
5
5
  price: number;
@@ -13,6 +13,7 @@ export interface ShippingMethodInput {
13
13
  price: number;
14
14
  vatRate: number;
15
15
  carrierType?: ShopCarrierType;
16
+ carrierConfig?: ShippingCarrierConfig | null;
16
17
  conditions?: ShippingConditions | null;
17
18
  allowedPaymentMethods?: string[] | null;
18
19
  isActive?: boolean;
@@ -21,6 +21,12 @@ function validate(input) {
21
21
  throw new Error('conditions.freeAbove must be non-negative integer (grosze).');
22
22
  }
23
23
  }
24
+ if (input.carrierType === 'inpost') {
25
+ const cfg = input.carrierConfig;
26
+ if (!cfg?.serviceType) {
27
+ throw new Error('InPost shipping method requires carrierConfig.serviceType.');
28
+ }
29
+ }
24
30
  }
25
31
  export async function listShippingMethods(opts = {}) {
26
32
  const db = getShopDb();
@@ -50,6 +56,7 @@ export async function createShippingMethod(input) {
50
56
  price: String(input.price),
51
57
  vatRate: input.vatRate,
52
58
  carrierType: input.carrierType ?? 'none',
59
+ carrierConfig: input.carrierConfig ?? null,
53
60
  conditions: input.conditions ?? null,
54
61
  allowedPaymentMethods: input.allowedPaymentMethods ?? null,
55
62
  isActive: input.isActive ?? true,
@@ -71,6 +78,8 @@ export async function updateShippingMethod(id, input) {
71
78
  patch.vatRate = input.vatRate;
72
79
  if (input.carrierType !== undefined)
73
80
  patch.carrierType = input.carrierType;
81
+ if (input.carrierConfig !== undefined)
82
+ patch.carrierConfig = input.carrierConfig;
74
83
  if (input.conditions !== undefined)
75
84
  patch.conditions = input.conditions;
76
85
  if (input.allowedPaymentMethods !== undefined)
@@ -0,0 +1,270 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+
4
+ interface CarrierWidgetConfig {
5
+ token: string;
6
+ language?: string;
7
+ config?: string;
8
+ }
9
+
10
+ interface CarrierDescriptor {
11
+ id: string;
12
+ widget: {
13
+ scriptUrl: string | null;
14
+ stylesheetUrl: string | null;
15
+ config: CarrierWidgetConfig;
16
+ } | null;
17
+ }
18
+
19
+ interface InpostPoint {
20
+ name: string;
21
+ address?: { line1?: string; line2?: string };
22
+ address_details?: Record<string, string>;
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ interface Props {
27
+ /** Currently selected paczkomat code (e.g. `KRA01M`). Two-way bindable. */
28
+ value?: string;
29
+ /** Optional pre-fetched config (skips network call). */
30
+ descriptor?: CarrierDescriptor | null;
31
+ /** Carrier id to fetch when no `descriptor` provided. Default: `inpost`. */
32
+ carrierId?: string;
33
+ /** Base URL prefix (when shop API lives on a different origin). */
34
+ apiBase?: string;
35
+ /** Override geowidget language. */
36
+ language?: string;
37
+ /** Override geowidget config preset (e.g. `parcelCollect247`). */
38
+ config?: string;
39
+ /** Custom fetch (e.g. SvelteKit `fetch` from a load function). */
40
+ fetchFn?: typeof fetch;
41
+ /**
42
+ * Hide picker when `serviceType` is a courier service (no parcel locker
43
+ * needed). Pass `shippingMethod.carrierConfig?.serviceType` from the
44
+ * public shipping methods API.
45
+ */
46
+ serviceType?: string | null;
47
+ /** Called when user picks a paczkomat with the full point payload. */
48
+ onSelect?: (point: InpostPoint) => void;
49
+ class?: string;
50
+ style?: string;
51
+ }
52
+
53
+ let {
54
+ value = $bindable(''),
55
+ descriptor = null,
56
+ carrierId = 'inpost',
57
+ apiBase = '',
58
+ language,
59
+ config,
60
+ fetchFn,
61
+ serviceType = null,
62
+ onSelect,
63
+ class: className = '',
64
+ style = ''
65
+ }: Props = $props();
66
+
67
+ let mountEl: HTMLDivElement | undefined = $state();
68
+ let loading = $state(false);
69
+ let error = $state<string | null>(null);
70
+ let resolved = $state<CarrierDescriptor | null>(descriptor);
71
+ let pointHandler: ((e: Event) => void) | null = null;
72
+ let pointEventName: string | null = null;
73
+ let geowidgetEl: HTMLElement | null = null;
74
+
75
+ const isCourier = $derived(
76
+ typeof serviceType === 'string' && serviceType.startsWith('inpost_courier_')
77
+ );
78
+
79
+ async function loadDescriptor(): Promise<CarrierDescriptor | null> {
80
+ if (resolved) return resolved;
81
+ const f = fetchFn ?? globalThis.fetch.bind(globalThis);
82
+ const res = await f(`${apiBase}/api/shop/carriers/${encodeURIComponent(carrierId)}`);
83
+ if (!res.ok) throw new Error(`Carrier config fetch failed: ${res.status}`);
84
+ return (await res.json()) as CarrierDescriptor;
85
+ }
86
+
87
+ function loadStylesheet(href: string): void {
88
+ const exists = document.querySelector(`link[data-inpost-css="${href}"]`);
89
+ if (exists) return;
90
+ const link = document.createElement('link');
91
+ link.rel = 'stylesheet';
92
+ link.href = href;
93
+ link.dataset.inpostCss = href;
94
+ document.head.appendChild(link);
95
+ }
96
+
97
+ function loadScript(src: string): Promise<void> {
98
+ return new Promise((resolve, reject) => {
99
+ const existing = document.querySelector<HTMLScriptElement>(
100
+ `script[data-inpost-script="${src}"]`
101
+ );
102
+ if (existing) {
103
+ if (existing.dataset.loaded === '1') return resolve();
104
+ existing.addEventListener('load', () => resolve(), { once: true });
105
+ existing.addEventListener(
106
+ 'error',
107
+ () => reject(new Error(`Geowidget script failed: ${src}`)),
108
+ { once: true }
109
+ );
110
+ return;
111
+ }
112
+ const s = document.createElement('script');
113
+ s.src = src;
114
+ s.async = true;
115
+ s.defer = true;
116
+ s.dataset.inpostScript = src;
117
+ s.addEventListener(
118
+ 'load',
119
+ () => {
120
+ s.dataset.loaded = '1';
121
+ resolve();
122
+ },
123
+ { once: true }
124
+ );
125
+ s.addEventListener(
126
+ 'error',
127
+ () => reject(new Error(`Geowidget script failed: ${src}`)),
128
+ { once: true }
129
+ );
130
+ document.head.appendChild(s);
131
+ });
132
+ }
133
+
134
+ function mountWidget(d: CarrierDescriptor): void {
135
+ if (!mountEl || !d.widget) return;
136
+ mountEl.innerHTML = '';
137
+
138
+ // `onpoint` attribute is the *name of a custom event* that Geowidget
139
+ // dispatches on `document` after a user picks a point. Use a unique name
140
+ // per instance so multiple pickers don't fight over the same listener.
141
+ pointEventName =
142
+ 'aria-inpost-pick-' + Math.random().toString(36).slice(2, 10);
143
+
144
+ const el = document.createElement('inpost-geowidget');
145
+ el.setAttribute('token', d.widget.config.token);
146
+ el.setAttribute('language', language ?? d.widget.config.language ?? 'pl');
147
+ el.setAttribute('config', config ?? d.widget.config.config ?? 'parcelcollect');
148
+ el.setAttribute('onpoint', pointEventName);
149
+ if (value) el.setAttribute('selected_point', value);
150
+
151
+ pointHandler = (e: Event) => {
152
+ const ce = e as CustomEvent<InpostPoint> & { details?: InpostPoint };
153
+ const detail = ce.detail ?? ce.details;
154
+ if (!detail || typeof detail.name !== 'string') return;
155
+ value = detail.name;
156
+ onSelect?.(detail);
157
+ };
158
+ document.addEventListener(pointEventName, pointHandler);
159
+ mountEl.appendChild(el);
160
+ geowidgetEl = el;
161
+ }
162
+
163
+ onMount(async () => {
164
+ if (isCourier) return;
165
+ loading = true;
166
+ error = null;
167
+ try {
168
+ const d = await loadDescriptor();
169
+ resolved = d;
170
+ if (!d?.widget?.scriptUrl) {
171
+ throw new Error('Geowidget script URL missing in carrier config.');
172
+ }
173
+ if (d.widget.stylesheetUrl) loadStylesheet(d.widget.stylesheetUrl);
174
+ await loadScript(d.widget.scriptUrl);
175
+ mountWidget(d);
176
+ } catch (err) {
177
+ error = err instanceof Error ? err.message : 'Failed to load InPost widget.';
178
+ } finally {
179
+ loading = false;
180
+ }
181
+ });
182
+
183
+ onDestroy(() => {
184
+ if (pointEventName && pointHandler) {
185
+ document.removeEventListener(pointEventName, pointHandler);
186
+ }
187
+ geowidgetEl = null;
188
+ pointHandler = null;
189
+ pointEventName = null;
190
+ });
191
+ </script>
192
+
193
+ {#if isCourier}
194
+ <div class="aria-inpost-courier-note {className}" {style} part="courier-note">
195
+ Kurier dostarczy paczkę pod podany adres — wybór paczkomatu nie jest potrzebny.
196
+ </div>
197
+ {:else}
198
+ <div class="aria-inpost-picker {className}" {style} part="root">
199
+ {#if loading && !resolved}
200
+ <div class="state-loading" part="loading" role="status" aria-live="polite">
201
+ Ładowanie wyboru paczkomatu…
202
+ </div>
203
+ {/if}
204
+ {#if error}
205
+ <div class="state-error" part="error" role="alert">{error}</div>
206
+ {/if}
207
+
208
+ <div class="mount" bind:this={mountEl} part="mount"></div>
209
+
210
+ {#if value}
211
+ <div class="selected" part="selected" aria-live="polite">
212
+ Wybrany paczkomat: <strong>{value}</strong>
213
+ </div>
214
+ {/if}
215
+ </div>
216
+ {/if}
217
+
218
+ <style>
219
+ .aria-inpost-picker {
220
+ --inpost-bg: #fafafe;
221
+ --inpost-fg: #1a1a2e;
222
+ --inpost-muted: #555566;
223
+ --inpost-accent: #5b4a9e;
224
+ --inpost-border: #e2dff0;
225
+ --inpost-radius: 12px;
226
+ --inpost-min-height: 520px;
227
+
228
+ display: block;
229
+ font-family: inherit;
230
+ color: var(--inpost-fg);
231
+ }
232
+
233
+ .aria-inpost-courier-note {
234
+ color: var(--inpost-muted, #555566);
235
+ font-size: 0.9rem;
236
+ padding: 0.75rem 1rem;
237
+ border: 1px dashed var(--inpost-border, #e2dff0);
238
+ border-radius: var(--inpost-radius, 12px);
239
+ }
240
+
241
+ .mount {
242
+ min-height: var(--inpost-min-height);
243
+ border: 1px solid var(--inpost-border);
244
+ border-radius: var(--inpost-radius);
245
+ overflow: hidden;
246
+ background: var(--inpost-bg);
247
+ }
248
+
249
+ .mount :global(inpost-geowidget) {
250
+ display: block;
251
+ width: 100%;
252
+ height: var(--inpost-min-height);
253
+ }
254
+
255
+ .state-loading,
256
+ .state-error {
257
+ padding: 0.75rem 1rem;
258
+ font-size: 0.9rem;
259
+ color: var(--inpost-muted);
260
+ }
261
+
262
+ .state-error {
263
+ color: #c44b4b;
264
+ }
265
+
266
+ .selected {
267
+ margin-top: 0.5rem;
268
+ font-size: 0.95rem;
269
+ }
270
+ </style>
@@ -0,0 +1,51 @@
1
+ interface CarrierWidgetConfig {
2
+ token: string;
3
+ language?: string;
4
+ config?: string;
5
+ }
6
+ interface CarrierDescriptor {
7
+ id: string;
8
+ widget: {
9
+ scriptUrl: string | null;
10
+ stylesheetUrl: string | null;
11
+ config: CarrierWidgetConfig;
12
+ } | null;
13
+ }
14
+ interface InpostPoint {
15
+ name: string;
16
+ address?: {
17
+ line1?: string;
18
+ line2?: string;
19
+ };
20
+ address_details?: Record<string, string>;
21
+ [key: string]: unknown;
22
+ }
23
+ interface Props {
24
+ /** Currently selected paczkomat code (e.g. `KRA01M`). Two-way bindable. */
25
+ value?: string;
26
+ /** Optional pre-fetched config (skips network call). */
27
+ descriptor?: CarrierDescriptor | null;
28
+ /** Carrier id to fetch when no `descriptor` provided. Default: `inpost`. */
29
+ carrierId?: string;
30
+ /** Base URL prefix (when shop API lives on a different origin). */
31
+ apiBase?: string;
32
+ /** Override geowidget language. */
33
+ language?: string;
34
+ /** Override geowidget config preset (e.g. `parcelCollect247`). */
35
+ config?: string;
36
+ /** Custom fetch (e.g. SvelteKit `fetch` from a load function). */
37
+ fetchFn?: typeof fetch;
38
+ /**
39
+ * Hide picker when `serviceType` is a courier service (no parcel locker
40
+ * needed). Pass `shippingMethod.carrierConfig?.serviceType` from the
41
+ * public shipping methods API.
42
+ */
43
+ serviceType?: string | null;
44
+ /** Called when user picks a paczkomat with the full point payload. */
45
+ onSelect?: (point: InpostPoint) => void;
46
+ class?: string;
47
+ style?: string;
48
+ }
49
+ declare const InpostPicker: import("svelte").Component<Props, {}, "value">;
50
+ type InpostPicker = ReturnType<typeof InpostPicker>;
51
+ export default InpostPicker;
@@ -34,7 +34,8 @@
34
34
  intro: { ...DEFAULT_LABELS_PL.intro, ...labelOverrides?.intro },
35
35
  totals: { ...DEFAULT_LABELS_PL.totals, ...labelOverrides?.totals },
36
36
  actions: { ...DEFAULT_LABELS_PL.actions, ...labelOverrides?.actions },
37
- errors: { ...DEFAULT_LABELS_PL.errors, ...labelOverrides?.errors }
37
+ errors: { ...DEFAULT_LABELS_PL.errors, ...labelOverrides?.errors },
38
+ tracking: { ...DEFAULT_LABELS_PL.tracking, ...labelOverrides?.tracking }
38
39
  });
39
40
 
40
41
  const order: OrderState = createOrderState({ number, token, initialData });
@@ -187,6 +188,27 @@
187
188
  </section>
188
189
  {/if}
189
190
 
191
+ {#if o.trackingNumber}
192
+ <section class="tracking" part="tracking">
193
+ <h2 class="section-title">{labels.tracking.title}</h2>
194
+ <div class="tracking-row">
195
+ <span class="tracking-label">{labels.tracking.number}</span>
196
+ <span class="tracking-value">{o.trackingNumber}</span>
197
+ </div>
198
+ {#if o.trackingUrl}
199
+ <a
200
+ class="tracking-link"
201
+ part="tracking-link"
202
+ href={o.trackingUrl}
203
+ target="_blank"
204
+ rel="noopener noreferrer"
205
+ >
206
+ {labels.tracking.link}
207
+ </a>
208
+ {/if}
209
+ </section>
210
+ {/if}
211
+
190
212
  {#if order.data.statusHistory.length > 0}
191
213
  <section class="history" part="history">
192
214
  <h2 class="section-title">{labels.statusHistory}</h2>
@@ -274,12 +296,42 @@
274
296
  .summary,
275
297
  .items,
276
298
  .actions,
299
+ .tracking,
277
300
  .history {
278
301
  margin-top: 1.5rem;
279
302
  padding-top: 1.5rem;
280
303
  border-top: 1px solid var(--shop-border);
281
304
  }
282
305
 
306
+ .tracking-row {
307
+ display: flex;
308
+ justify-content: space-between;
309
+ gap: 1rem;
310
+ padding: 0.4rem 0;
311
+ font-size: 0.95rem;
312
+ }
313
+
314
+ .tracking-label {
315
+ color: var(--shop-muted);
316
+ }
317
+
318
+ .tracking-value {
319
+ font-family: ui-monospace, monospace;
320
+ font-weight: 600;
321
+ }
322
+
323
+ .tracking-link {
324
+ display: inline-block;
325
+ margin-top: 0.5rem;
326
+ color: var(--shop-accent);
327
+ font-weight: 600;
328
+ text-decoration: none;
329
+ }
330
+
331
+ .tracking-link:hover {
332
+ text-decoration: underline;
333
+ }
334
+
283
335
  .section-title {
284
336
  font-size: 0.875rem;
285
337
  font-weight: 600;
@@ -1,3 +1,4 @@
1
1
  export { default as OrderStatus } from './OrderStatus.svelte';
2
+ export { default as InpostPicker } from './InpostPicker.svelte';
2
3
  export { DEFAULT_LABELS_PL } from './labels.js';
3
4
  export type { OrderStatusLabels } from './labels.js';
@@ -1,2 +1,3 @@
1
1
  export { default as OrderStatus } from './OrderStatus.svelte';
2
+ export { default as InpostPicker } from './InpostPicker.svelte';
2
3
  export { DEFAULT_LABELS_PL } from './labels.js';
@@ -21,5 +21,10 @@ export interface OrderStatusLabels {
21
21
  forbidden: string;
22
22
  };
23
23
  statusHistory: string;
24
+ tracking: {
25
+ title: string;
26
+ number: string;
27
+ link: string;
28
+ };
24
29
  }
25
30
  export declare const DEFAULT_LABELS_PL: OrderStatusLabels;
@@ -37,5 +37,10 @@ export const DEFAULT_LABELS_PL = {
37
37
  loadFailed: 'Nie udało się pobrać zamówienia.',
38
38
  forbidden: 'Link wygasł lub jest nieprawidłowy.'
39
39
  },
40
- statusHistory: 'Historia statusów'
40
+ statusHistory: 'Historia statusów',
41
+ tracking: {
42
+ title: 'Śledzenie przesyłki',
43
+ number: 'Numer',
44
+ link: 'Sprawdź status przesyłki ↗'
45
+ }
41
46
  };
@@ -40,11 +40,59 @@ export interface PaymentAdapter {
40
40
  }
41
41
  export interface CarrierAdapter {
42
42
  id: string;
43
+ label?: I18nText;
43
44
  widget?: {
44
45
  scriptUrl?: string;
46
+ stylesheetUrl?: string;
45
47
  config: Record<string, unknown>;
46
48
  };
47
- validateSelection?(ref: string): Promise<boolean> | boolean;
49
+ validateSelection?(ref: string, ctx?: {
50
+ serviceType?: string;
51
+ }): Promise<boolean> | boolean;
52
+ createShipment?(input: ShipmentCreateInput): Promise<ShipmentCreateResult>;
53
+ getShipmentLabel?(shipmentId: string, opts?: {
54
+ format?: 'pdf';
55
+ size?: 'A4' | 'A6';
56
+ }): Promise<ShipmentLabel>;
57
+ cancelShipment?(shipmentId: string): Promise<void>;
58
+ handleWebhook?(req: Request): Promise<CarrierEvent>;
59
+ trackingUrl?(trackingNumber: string): string;
60
+ }
61
+ export interface ShipmentReceiver {
62
+ id: string;
63
+ number: string;
64
+ customerEmail: string;
65
+ customerName?: string | null;
66
+ customerPhone?: string | null;
67
+ }
68
+ export interface ShipmentCreateInput {
69
+ order: ShipmentReceiver;
70
+ shippingAddress: Record<string, string> | null;
71
+ carrierRef?: string | null;
72
+ serviceType: string;
73
+ parcelSize?: string;
74
+ cartTotalGross: number;
75
+ insuranceAmount?: number;
76
+ cod?: number;
77
+ language?: string | null;
78
+ }
79
+ export interface ShipmentCreateResult {
80
+ shipmentId: string;
81
+ trackingNumber: string;
82
+ labelUrl?: string;
83
+ raw?: unknown;
84
+ }
85
+ export interface ShipmentLabel {
86
+ contentType: string;
87
+ body: Uint8Array;
88
+ filename?: string;
89
+ }
90
+ export interface CarrierEvent {
91
+ shipmentId: string;
92
+ trackingNumber?: string;
93
+ orderNumber?: string;
94
+ status: 'preparing' | 'sent' | 'done' | 'cancelled' | 'unknown';
95
+ raw: unknown;
48
96
  }
49
97
  export interface OrderRef {
50
98
  id: string;
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;