includio-cms 0.15.1 → 0.15.3
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.
- package/CHANGELOG.md +52 -0
- package/DOCS.md +137 -2
- package/ROADMAP.md +12 -2
- package/dist/admin/client/shop/shipping-method-form.svelte +90 -22
- package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +9 -1
- package/dist/admin/client/shop/shipping-methods-list-page.svelte +7 -4
- package/dist/admin/client/shop/shop-order-detail-page.svelte +101 -0
- package/dist/admin/client/shop/shop-products-list-page.svelte +2 -2
- package/dist/admin/components/fields/shop-field.svelte +63 -22
- package/dist/admin/remote/shop.remote.d.ts +48 -60
- package/dist/admin/remote/shop.remote.js +38 -3
- package/dist/cli/index.js +49 -4
- package/dist/cli/scaffold/admin.d.ts +9 -2
- package/dist/cli/scaffold/admin.js +32 -3
- package/dist/db-postgres/schema/shop/order.d.ts +68 -0
- package/dist/db-postgres/schema/shop/order.js +4 -0
- package/dist/db-postgres/schema/shop/product.d.ts +4 -4
- package/dist/db-postgres/schema/shop/product.js +3 -2
- package/dist/db-postgres/schema/shop/productVariant.d.ts +4 -4
- package/dist/db-postgres/schema/shop/productVariant.js +3 -2
- package/dist/db-postgres/schema/shop/shippingMethod.d.ts +29 -4
- package/dist/db-postgres/schema/shop/shippingMethod.js +4 -2
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/shop/adapters/inpost/geowidget.d.ts +27 -0
- package/dist/shop/adapters/inpost/geowidget.js +31 -0
- package/dist/shop/adapters/inpost/index.d.ts +89 -0
- package/dist/shop/adapters/inpost/index.js +156 -0
- package/dist/shop/adapters/inpost/payload.d.ts +18 -0
- package/dist/shop/adapters/inpost/payload.js +85 -0
- package/dist/shop/adapters/inpost/points-api.d.ts +17 -0
- package/dist/shop/adapters/inpost/points-api.js +55 -0
- package/dist/shop/adapters/inpost/shipx-client.d.ts +56 -0
- package/dist/shop/adapters/inpost/shipx-client.js +95 -0
- package/dist/shop/adapters/inpost/status-map.d.ts +9 -0
- package/dist/shop/adapters/inpost/status-map.js +46 -0
- package/dist/shop/adapters/inpost/webhook.d.ts +16 -0
- package/dist/shop/adapters/inpost/webhook.js +55 -0
- package/dist/shop/client/index.d.ts +6 -0
- package/dist/shop/http/carrier-handler.d.ts +12 -0
- package/dist/shop/http/carrier-handler.js +45 -0
- package/dist/shop/http/carrier-webhook-handler.d.ts +13 -0
- package/dist/shop/http/carrier-webhook-handler.js +66 -0
- package/dist/shop/http/checkout-handler.js +23 -1
- package/dist/shop/http/index.d.ts +3 -0
- package/dist/shop/http/index.js +3 -0
- package/dist/shop/http/order-handler.js +14 -0
- package/dist/shop/http/shipment-label-handler.d.ts +10 -0
- package/dist/shop/http/shipment-label-handler.js +53 -0
- package/dist/shop/http/shipping-handler.js +3 -0
- package/dist/shop/index.d.ts +3 -1
- package/dist/shop/index.js +1 -0
- package/dist/shop/pricing.d.ts +4 -0
- package/dist/shop/pricing.js +18 -0
- package/dist/shop/server/cart-hydrate.js +6 -3
- package/dist/shop/server/email.js +37 -0
- package/dist/shop/server/orders.d.ts +9 -0
- package/dist/shop/server/orders.js +48 -0
- package/dist/shop/server/populate.d.ts +2 -0
- package/dist/shop/server/shipments.d.ts +33 -0
- package/dist/shop/server/shipments.js +145 -0
- package/dist/shop/server/shipping.d.ts +13 -5
- package/dist/shop/server/shipping.js +30 -14
- package/dist/shop/server/shop-data.d.ts +8 -2
- package/dist/shop/server/shop-data.js +18 -10
- package/dist/shop/svelte/InpostPicker.svelte +270 -0
- package/dist/shop/svelte/InpostPicker.svelte.d.ts +51 -0
- package/dist/shop/svelte/OrderStatus.svelte +53 -1
- package/dist/shop/svelte/index.d.ts +1 -0
- package/dist/shop/svelte/index.js +1 -0
- package/dist/shop/svelte/labels.d.ts +5 -0
- package/dist/shop/svelte/labels.js +6 -1
- package/dist/shop/types.d.ts +49 -1
- package/dist/updates/0.15.2/index.d.ts +2 -0
- package/dist/updates/0.15.2/index.js +18 -0
- package/dist/updates/0.15.3/index.d.ts +2 -0
- package/dist/updates/0.15.3/index.js +19 -0
- package/dist/updates/index.js +3 -1
- package/package.json +1 -1
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- package/dist/paraglide/messages/login_please_login.js +0 -34
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,58 @@
|
|
|
3
3
|
All notable changes to includio-cms are documented here.
|
|
4
4
|
Generated from `src/lib/updates/` — do not edit manually.
|
|
5
5
|
|
|
6
|
+
## 0.15.3 — 2026-04-16
|
|
7
|
+
|
|
8
|
+
Shop: InPost carrier adapter — Geowidget v5 picker + ShipX shipment + webhook auto-status.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `inpostAdapter()` ships in `includio-cms/shop` — wires Geowidget v5 (paczkomat picker on the frontend), ShipX (admin "Utwórz przesyłkę InPost" button creates the shipment, auto-buys the offer, fetches the label PDF) and a webhook receiver (`POST /api/shop/carriers/inpost/webhook?secret=...`) that updates order status + tracking number when ShipX events arrive.
|
|
12
|
+
- `<InpostPicker>` Svelte component (`includio-cms/shop/svelte`) — drop-in widget with `bind:value={carrierRef}`, lazy-loads the geowidget script + CSS, hides itself for `inpost_courier_*` services. Raw API alternative: `GET /api/shop/carriers/inpost`.
|
|
13
|
+
- Per-shipping-method service config — `shop_shipping_methods.carrier_config` jsonb with `serviceType` (`inpost_locker_standard` / `inpost_locker_express` / `inpost_courier_standard` / `inpost_courier_express`) and `defaultSize` (A/B/C → small/medium/large). Admin shipping form ungates the InPost option and shows the new fields when selected.
|
|
14
|
+
- Customer order endpoint exposes `trackingNumber` + `trackingUrl` (resolved via `adapter.trackingUrl()`); `<OrderStatus>` renders a "Śledzenie przesyłki" section automatically. Status emails for `preparing` / `sent` / `done` include a tracking block with a clickable carrier link.
|
|
15
|
+
- Admin order detail page gains a "Przesyłka {carrierType}" panel: Utwórz przesyłkę / Pobierz etykietę PDF / Anuluj przesyłkę. PDF is streamed through an admin-auth proxy at `/api/shop/admin/orders/[id]/label`.
|
|
16
|
+
- Checkout validates `carrierRef` against the carrier (Operating-only paczkomats via InPost Points API, in-memory cached 5 min). Invalid selections reject with HTTP 400 before the order is created.
|
|
17
|
+
- CLI scaffold (`pnpm includio scaffold admin`) emits the new routes: `api/shop/carriers/[id]/+server.ts`, `api/shop/carriers/[id]/webhook/+server.ts`, `api/shop/admin/orders/[id]/label/+server.ts`.
|
|
18
|
+
- CLI scaffolder auto-detects shop usage by scanning `src/lib/cms/cms.config.ts` for an active `shop:` property — projects without the shop module get a clean route tree by default, no flag required. Override with `--shop` / `--no-shop` (or pass `scaffoldAdmin({ shop: false })` programmatically). Combined with the existing config-driven schema generator + admin sidebar guard, shop is now fully opt-in with zero footprint when unused.
|
|
19
|
+
|
|
20
|
+
### Migration
|
|
21
|
+
|
|
22
|
+
```sql
|
|
23
|
+
ALTER TABLE shop_shipping_methods ADD COLUMN carrier_config jsonb;
|
|
24
|
+
ALTER TABLE shop_orders ADD COLUMN shipment_id text, ADD COLUMN tracking_number text, ADD COLUMN label_url text, ADD COLUMN shipment_created_at timestamptz;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Notes
|
|
28
|
+
|
|
29
|
+
Run the SQL once, then `pnpm db:push` to sync the rest of the schema. InPost requires two tokens: a public Geowidget v5 token (browser-side, from `manager.paczkomaty.pl`) and a private ShipX organization token (server-side). On sandbox, ShipX `POST /shipments/:id/buy` is asynchronous and often stalls in `offer_selected` because test accounts have no wallet to pay the offer — the webhook reconciles whatever ends up. See /docs/shop/inpost for full setup.
|
|
30
|
+
|
|
31
|
+
## 0.15.2 — 2026-04-15
|
|
32
|
+
|
|
33
|
+
Shop: cena przechowywana jako numeric(20,6) — eliminacja driftu brutto/netto po reload; toggle netto/brutto per wariant.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- Warianty produktu mają teraz toggle netto/brutto obok pola "Zmiana ceny" — spójne z toggle'em ceny bazowej. Delta zapisywana kanonicznie jako netto.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- Shop: po wpisaniu ceny brutto (np. 65,00 zł przy VAT 23%) i refreshu, cena nie "ucieka" o ±1 grosz. Wartości przechowywane są jako PLN z 6 miejscami po przecinku (wzorzec PrestaShop), zamiast jednostek groszy — brutto zawsze odtwarzane z netto bez utraty informacji.
|
|
40
|
+
- Shipping: ta sama poprawka dla ceny metody wysyłki (stored jako netto PLN z 6dp).
|
|
41
|
+
|
|
42
|
+
### Breaking
|
|
43
|
+
- Kolumny `shop_products.base_price`, `shop_product_variants.price_delta`, `shop_shipping_methods.price` zmieniły typ z `integer` (grosze) na `numeric(20,6)` (PLN). Snapshot zamówienia (`shop_orders.*`, `shop_order_items.price_*_snapshot`) pozostaje w groszach (`integer`) — KSeF-compatible.
|
|
44
|
+
- Publiczne typy `ShopDataWithVariants.basePrice`, `VariantRow.priceDelta`, `ShippingMethodRow.price` — wartość dalej `number`, ale wyrażona w PLN (nie groszach). Dla konsumentów SDK/populate: mnożenie × 100 jeśli potrzebujesz groszy.
|
|
45
|
+
|
|
46
|
+
### Migration
|
|
47
|
+
|
|
48
|
+
```sql
|
|
49
|
+
ALTER TABLE shop_products ALTER COLUMN base_price TYPE numeric(20,6) USING (base_price::numeric / 100);
|
|
50
|
+
ALTER TABLE shop_product_variants ALTER COLUMN price_delta TYPE numeric(20,6) USING (price_delta::numeric / 100);
|
|
51
|
+
ALTER TABLE shop_shipping_methods ALTER COLUMN price TYPE numeric(20,6) USING (price::numeric / 100);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Notes
|
|
55
|
+
|
|
56
|
+
Migrację SQL trzeba uruchomić RAZ, PRZED `db:push` — dzieli istniejące wartości ÷100, bo stare dane były w groszach, a nowy typ przechowuje PLN. Po migracji `db:push` tylko zsynchronizuje schemat (bez zmiany danych).
|
|
57
|
+
|
|
6
58
|
## 0.15.1 — 2026-04-15
|
|
7
59
|
|
|
8
60
|
Shop: PayU payment adapter + secure order access (token-gated view API, email link).
|
package/DOCS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Includio CMS Documentation (v0.15.
|
|
1
|
+
# Includio CMS Documentation (v0.15.3)
|
|
2
2
|
|
|
3
3
|
> This file is auto-generated from the docs site. For the latest version, update the package.
|
|
4
4
|
|
|
@@ -4311,6 +4311,7 @@ Headless e-commerce module. Optional — activate by adding `shop: defineShop({.
|
|
|
4311
4311
|
- **Cart** — signed cookie, headless SDK (`createShopClient()`), `POST/PATCH/DELETE /api/shop/cart`.
|
|
4312
4312
|
- **Checkout + orders** — `POST /api/shop/checkout` creates an order with consent validation, stock reservation (30-min TTL), and status emails.
|
|
4313
4313
|
- **Payment adapters** — `manualAdapter()` (bank transfer / COD) and `payuAdapter()` ship built-in; plug your own by implementing `PaymentAdapter`.
|
|
4314
|
+
- **Carrier adapters** — `inpostAdapter()` ships built-in (Geowidget v5 picker + ShipX shipments + webhook). Plug other carriers by implementing `CarrierAdapter`.
|
|
4314
4315
|
- **Webhook infrastructure** — `createPaymentWebhookHandler()` dispatches provider callbacks, verifies signatures, is idempotent.
|
|
4315
4316
|
- **Secure order view** — per-order `accessToken` + cookie fallback + `GET /api/shop/orders/[number]` token-gated API.
|
|
4316
4317
|
- **Shipping ↔ payment compatibility** — restrict payment methods per shipping (e.g. no COD for paczkomat).
|
|
@@ -4344,7 +4345,18 @@ export const cmsConfig = defineCMS({
|
|
|
4344
4345
|
|
|
4345
4346
|
## Routes
|
|
4346
4347
|
|
|
4347
|
-
|
|
4348
|
+
Shop is **opt-in**: projects without `shop: defineShop(...)` in their CMS config don't get shop tables in the DB schema (generator gates on `config.shop`), the admin sidebar hides the Shop section, and shop API handlers always return 404.
|
|
4349
|
+
|
|
4350
|
+
The CLI scaffolder **auto-detects** shop usage by scanning your `src/lib/cms/cms.config.ts` for `shop: …`. Override with `--shop` or `--no-shop` if needed:
|
|
4351
|
+
|
|
4352
|
+
```sh
|
|
4353
|
+
pnpm includio scaffold admin # auto: detects shop in cms config
|
|
4354
|
+
pnpm includio scaffold admin --shop # force include shop routes
|
|
4355
|
+
pnpm includio scaffold admin --no-shop # force exclude shop routes
|
|
4356
|
+
pnpm includio scaffold admin --cms-config path/to/config.ts # custom location
|
|
4357
|
+
```
|
|
4358
|
+
|
|
4359
|
+
Scaffold provisions these endpoints (when shop is enabled):
|
|
4348
4360
|
|
|
4349
4361
|
| Path | Handler | Purpose |
|
|
4350
4362
|
|------|---------|---------|
|
|
@@ -4355,11 +4367,15 @@ Scaffold provisions these endpoints in your app:
|
|
|
4355
4367
|
| `POST /api/shop/orders/[number]/refresh-payment` | `createRefreshPaymentHandler()` | Pull status from provider |
|
|
4356
4368
|
| `POST /api/shop/orders/[number]/retry-payment` | `createRetryPaymentHandler()` | New payment attempt |
|
|
4357
4369
|
| `POST /api/shop/webhooks/[provider]` | `createPaymentWebhookHandler()` | Provider callbacks |
|
|
4370
|
+
| `GET /api/shop/carriers/[id]` | `createCarrierConfigHandler()` | Public carrier widget descriptor |
|
|
4371
|
+
| `POST /api/shop/carriers/[id]/webhook` | `createCarrierWebhookHandler()` | Carrier event receiver |
|
|
4372
|
+
| `GET /api/shop/admin/orders/[id]/label` | `createShipmentLabelHandler()` | Admin shipping-label PDF proxy |
|
|
4358
4373
|
|
|
4359
4374
|
## See also
|
|
4360
4375
|
|
|
4361
4376
|
- [Order view](/docs/shop/order-view) — generic `<OrderStatus>` component + headless helper
|
|
4362
4377
|
- [Retry payment](/docs/shop/retry-payment) — lifecycle + endpoint
|
|
4378
|
+
- [InPost carrier](/docs/shop/inpost) — Geowidget v5 + ShipX shipment + webhook
|
|
4363
4379
|
|
|
4364
4380
|
|
|
4365
4381
|
---
|
|
@@ -4527,6 +4543,125 @@ if (url) window.location.href = url;
|
|
|
4527
4543
|
If the customer is completely stuck, the admin can manually move the order to `paid` / `cancelled` from the orders detail page — same as before, status-change emails trigger automatically.
|
|
4528
4544
|
|
|
4529
4545
|
|
|
4546
|
+
---
|
|
4547
|
+
|
|
4548
|
+
# InPost carrier
|
|
4549
|
+
|
|
4550
|
+
`inpostAdapter()` ships in 0.15.3. Wires three things together:
|
|
4551
|
+
|
|
4552
|
+
1. **Geowidget v5** — customer picks a paczkomat on the checkout page (`<InpostPicker>` Svelte component or raw config endpoint).
|
|
4553
|
+
2. **ShipX shipment** — admin clicks "Utwórz przesyłkę InPost" on the order detail page → backend POSTs the shipment to ShipX, auto-confirms the offer, stores the carrier-side shipment id + tracking number.
|
|
4554
|
+
3. **Webhook** — ShipX events (`confirmed`/`taken_by_courier`/`delivered`) update the order status and tracking number automatically.
|
|
4555
|
+
|
|
4556
|
+
> InPost requires two separate tokens: a **public Geowidget v5 token** (delivered to the browser) and a **private ShipX organization token** (server-side only). Both come from `manager.paczkomaty.pl` (production) or `sandbox-manager.paczkomaty.pl` (sandbox).
|
|
4557
|
+
|
|
4558
|
+
## Config
|
|
4559
|
+
|
|
4560
|
+
```ts
|
|
4561
|
+
import { defineShop, inpostAdapter } from 'includio-cms/shop';
|
|
4562
|
+
|
|
4563
|
+
defineShop({
|
|
4564
|
+
// …currency, payment, etc.
|
|
4565
|
+
carriers: [
|
|
4566
|
+
inpostAdapter({
|
|
4567
|
+
geowidgetToken: process.env.INPOST_GEOWIDGET_TOKEN!,
|
|
4568
|
+
shipxToken: process.env.INPOST_SHIPX_TOKEN!,
|
|
4569
|
+
organizationId: process.env.INPOST_ORG_ID!,
|
|
4570
|
+
environment: 'production', // or 'sandbox'
|
|
4571
|
+
webhookSecret: process.env.INPOST_WEBHOOK_SECRET!,
|
|
4572
|
+
senderAddress: {
|
|
4573
|
+
name: 'Twoja firma',
|
|
4574
|
+
company: 'AriaCMS',
|
|
4575
|
+
street: 'Mokotowska',
|
|
4576
|
+
buildingNumber: '1',
|
|
4577
|
+
city: 'Warszawa',
|
|
4578
|
+
postCode: '00-001',
|
|
4579
|
+
email: 'shop@example.com',
|
|
4580
|
+
phone: '+48500600700'
|
|
4581
|
+
}
|
|
4582
|
+
})
|
|
4583
|
+
]
|
|
4584
|
+
});
|
|
4585
|
+
```
|
|
4586
|
+
|
|
4587
|
+
**Optional opts:**
|
|
4588
|
+
|
|
4589
|
+
- `additionalServices: ['email', 'sms']` — InPost SMS/email notifications. Requires the service to be enabled on your account; sandbox usually doesn't have it.
|
|
4590
|
+
- `autoConfirm: false` — disable the post-create `POST /shipments/:id/buy` step. Default `true`.
|
|
4591
|
+
- `autoConfirmDelayMs` (default `1500`), `autoConfirmPollTimeoutMs` (default `8000`) — tune the offer-prep wait and post-buy polling.
|
|
4592
|
+
- `trackingUrlTemplate` — override `https://inpost.pl/sledzenie-przesylek?number={trackingNumber}`.
|
|
4593
|
+
- `labelFormat: 'Pdf' | 'ZebraLP'`, `labelSize: 'A4' | 'A6'` — default label format.
|
|
4594
|
+
- `debug: true` — verbose logging of the create→buy→poll flow (payload dump, every poll tick). Off by default; warnings + errors always log.
|
|
4595
|
+
|
|
4596
|
+
## Routes scaffolded
|
|
4597
|
+
|
|
4598
|
+
`pnpm includio scaffold admin` provisions:
|
|
4599
|
+
|
|
4600
|
+
| Path | Handler | Purpose |
|
|
4601
|
+
|------|---------|---------|
|
|
4602
|
+
| `GET /api/shop/carriers/[id]` | `createCarrierConfigHandler()` | Public widget descriptor (script URL + token + preset) |
|
|
4603
|
+
| `POST /api/shop/carriers/[id]/webhook` | `createCarrierWebhookHandler()` | ShipX event receiver — secret-gated via `?secret=…` |
|
|
4604
|
+
| `GET /api/shop/admin/orders/[id]/label` | `createShipmentLabelHandler()` | Admin-only PDF proxy for the shipment label |
|
|
4605
|
+
|
|
4606
|
+
In `manager.paczkomaty.pl` configure the webhook URL as
|
|
4607
|
+
`https://your-domain.pl/api/shop/carriers/inpost/webhook?secret=<INPOST_WEBHOOK_SECRET>`.
|
|
4608
|
+
|
|
4609
|
+
## Per-shipping-method service
|
|
4610
|
+
|
|
4611
|
+
Each shipping method with `carrierType=inpost` carries a `carrierConfig`:
|
|
4612
|
+
|
|
4613
|
+
| Field | Values |
|
|
4614
|
+
|------|--------|
|
|
4615
|
+
| `serviceType` | `inpost_locker_standard` (paczkomat), `inpost_locker_express`, `inpost_courier_standard`, `inpost_courier_express` |
|
|
4616
|
+
| `defaultSize` | `A` (small, 8×38×64 cm) / `B` (medium, 19×38×64 cm) / `C` (large, 41×38×64 cm) |
|
|
4617
|
+
|
|
4618
|
+
Set both in admin → Shop → Shipping methods. For `inpost_courier_*` services the picker hides itself and the receiver address from the order is used instead.
|
|
4619
|
+
|
|
4620
|
+
## Frontend picker
|
|
4621
|
+
|
|
4622
|
+
```svelte
|
|
4623
|
+
<InpostPicker
|
|
4624
|
+
bind:value={carrierRef}
|
|
4625
|
+
serviceType="inpost_locker_standard"
|
|
4626
|
+
onSelect={(point) => console.log('picked', point.name, point.address)}
|
|
4627
|
+
/>
|
|
4628
|
+
```
|
|
4629
|
+
|
|
4630
|
+
The component lazy-loads the Geowidget v5 script + CSS, mounts the `<inpost-geowidget>` custom element, and writes the chosen paczkomat code into `value`. For `inpost_courier_*` services it renders a short notice instead.
|
|
4631
|
+
|
|
4632
|
+
**Custom UI** — skip the component and call `GET /api/shop/carriers/inpost` yourself; the response is `{ id, label, widget: { scriptUrl, stylesheetUrl, config: { token, language, config } } }`.
|
|
4633
|
+
|
|
4634
|
+
## Admin actions
|
|
4635
|
+
|
|
4636
|
+
On any order with an InPost shipping method, the order detail page shows:
|
|
4637
|
+
|
|
4638
|
+
- **"Utwórz przesyłkę InPost"** when status is `paid`/`preparing` and no shipment exists yet — POSTs to ShipX, auto-buys the offer, polls until status leaves `offer_selected`, writes `shipmentId` + `trackingNumber` on the order, bumps status to `preparing`.
|
|
4639
|
+
- **"Pobierz etykietę PDF"** — opens the admin label proxy in a new tab.
|
|
4640
|
+
- **"Anuluj przesyłkę"** — `DELETE /v1/shipments/:id` on ShipX, clears local data. Only works after the shipment reaches `confirmed`.
|
|
4641
|
+
|
|
4642
|
+
## Webhook → status mapping
|
|
4643
|
+
|
|
4644
|
+
| ShipX event status | Order status |
|
|
4645
|
+
|---|---|
|
|
4646
|
+
| `created`, `offers_prepared`, `offer_selected`, `confirmed`, `dispatched_by_sender` | `preparing` |
|
|
4647
|
+
| `taken_by_courier`, `adopted_at_*`, `out_for_delivery`, `ready_to_pickup` | `sent` |
|
|
4648
|
+
| `delivered` | `done` |
|
|
4649
|
+
| `canceled`, `returned_to_sender`, `rejected` | `cancelled` |
|
|
4650
|
+
| anything else | (ignored) |
|
|
4651
|
+
|
|
4652
|
+
Tracking number is updated whenever the webhook payload contains a non-null `tracking_number`. Status emails (built-in PL/EN templates) include a tracking block on `preparing`/`sent`/`done`.
|
|
4653
|
+
|
|
4654
|
+
## Sandbox quirks
|
|
4655
|
+
|
|
4656
|
+
- ShipX `POST /shipments/:id/buy` is **asynchronous**. Production confirms in ~1–3 s; sandbox often **stalls in `offer_selected`** because test accounts have no wallet to pay the offer. The adapter polls for ~8 s then gives up and lets the webhook reconcile.
|
|
4657
|
+
- Sandbox geowidget paczkomaty are **separate** from production codes. Use `KRA012` (Kraków, Os. Kombatantów 20) or any code from `GET https://sandbox-api-shipx-pl.easypack24.net/v1/points?per_page=10`.
|
|
4658
|
+
- Sandbox `additional_services: ['email', 'sms']` returns `unavailable` — drop the option for sandbox testing.
|
|
4659
|
+
|
|
4660
|
+
## Customer tracking display
|
|
4661
|
+
|
|
4662
|
+
`OrderStatus.svelte` (the built-in component) renders a "Śledzenie przesyłki" section automatically when the order has a `trackingNumber`. If you wrote a custom order view, read `order.trackingNumber` and `order.trackingUrl` from the order endpoint response and render them yourself.
|
|
4663
|
+
|
|
4664
|
+
|
|
4530
4665
|
---
|
|
4531
4666
|
|
|
4532
4667
|
# Migration Guide
|
package/ROADMAP.md
CHANGED
|
@@ -304,10 +304,20 @@
|
|
|
304
304
|
- [x] `[feature]` `[P0]` Random Crockford base32 order numbers (`XXXXX-XXXXX`), unguessable
|
|
305
305
|
- [x] `[feature]` `[P0]` CLI scaffold provisions all shop routes (admin + public API) in consumer app
|
|
306
306
|
- [x] `[feature]` `[P1]` PayU payment adapter + webhook — shipped in 0.15.1 (access-token-gated order view, idempotent webhooks, MD5 signature, shipping↔payment compat, poll fallback)
|
|
307
|
-
- [ ] `[feature]` `[P1]` Stripe payment adapter + webhook — deferred to 0.15.
|
|
308
|
-
- [
|
|
307
|
+
- [ ] `[feature]` `[P1]` Stripe payment adapter + webhook — deferred to 0.15.4
|
|
308
|
+
- [x] `[feature]` `[P2]` InPost carrier adapter — shipped in 0.15.3 (Geowidget v5 picker + ShipX shipments + auto-confirm + webhook status updates)
|
|
309
|
+
|
|
310
|
+
## 0.15.2 — Price precision fix
|
|
311
|
+
|
|
312
|
+
- [x] `[fix]` `[P1]` Price drift on reload — storage jako `numeric(20,6)` (PLN z 6dp), brutto round-tripuje bez utraty ±1gr. Snapshot zamówienia dalej w groszach (KSeF). <!-- files: src/lib/db-postgres/schema/shop/product.ts, productVariant.ts, shippingMethod.ts, src/lib/shop/pricing.ts, src/lib/shop/server/cart-hydrate.ts, shipping.ts, shop-data.ts, src/lib/admin/components/fields/shop-field.svelte, src/lib/admin/client/shop/shipping-method-form.svelte -->
|
|
313
|
+
- [x] `[feature]` `[P2]` Variant price toggle netto/brutto (spójne z toggle\'em ceny bazowej)
|
|
309
314
|
- [x] `[feature]` `[P2]` `nodemailerAdapter` — typowanie `transportOptions` jako `SMTPTransport.Options` (OAuth2, pool, itd.)
|
|
310
315
|
|
|
316
|
+
## 0.15.3 — InPost carrier
|
|
317
|
+
|
|
318
|
+
- [x] `[feature]` `[P2]` `inpostAdapter()` — Geowidget v5 (`<InpostPicker>` Svelte + raw config endpoint), ShipX shipment create + auto-buy + label PDF + cancel, webhook → status + tracking, per-shipping-method service config, customer tracking display in `<OrderStatus>` + email templates
|
|
319
|
+
- [x] `[chore]` `[P2]` Verbose logging on adapter auto-confirm flow (offer prep wait, buy POST, polling) for sandbox debugging
|
|
320
|
+
|
|
311
321
|
## 0.16.0 — SEO module
|
|
312
322
|
|
|
313
323
|
- [ ] `[feature]` `[P1]` SERP preview + character limits for title/description <!-- files: src/lib/admin/components/fields/seo-field.svelte -->
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
import { Switch } from '../../../components/ui/switch/index.js';
|
|
4
4
|
|
|
5
5
|
type CarrierType = 'none' | 'inpost';
|
|
6
|
+
type InpostServiceType =
|
|
7
|
+
| 'inpost_locker_standard'
|
|
8
|
+
| 'inpost_locker_express'
|
|
9
|
+
| 'inpost_courier_standard'
|
|
10
|
+
| 'inpost_courier_express';
|
|
11
|
+
type InpostParcelSize = 'A' | 'B' | 'C';
|
|
12
|
+
interface CarrierConfig {
|
|
13
|
+
serviceType?: InpostServiceType | string;
|
|
14
|
+
defaultSize?: InpostParcelSize | string;
|
|
15
|
+
}
|
|
6
16
|
export interface PaymentMethodOption {
|
|
7
17
|
id: string;
|
|
8
18
|
label: Record<string, string>;
|
|
@@ -10,10 +20,11 @@
|
|
|
10
20
|
export interface ShippingFormPayload {
|
|
11
21
|
name: Record<string, string>;
|
|
12
22
|
description: Record<string, string> | null;
|
|
13
|
-
price: number;
|
|
23
|
+
price: number; // PLN (number, netto)
|
|
14
24
|
vatRate: number;
|
|
15
25
|
carrierType: CarrierType;
|
|
16
|
-
|
|
26
|
+
carrierConfig: CarrierConfig | null;
|
|
27
|
+
conditions: { freeAbove?: number } | null; // freeAbove: grosze
|
|
17
28
|
allowedPaymentMethods: string[] | null;
|
|
18
29
|
isActive: boolean;
|
|
19
30
|
sortOrder: number | null;
|
|
@@ -21,9 +32,10 @@
|
|
|
21
32
|
export interface ShippingFormInitial {
|
|
22
33
|
name?: Record<string, string> | unknown;
|
|
23
34
|
description?: Record<string, string> | unknown | null;
|
|
24
|
-
price?: number;
|
|
35
|
+
price?: number | string;
|
|
25
36
|
vatRate?: number;
|
|
26
37
|
carrierType?: string;
|
|
38
|
+
carrierConfig?: CarrierConfig | null;
|
|
27
39
|
conditions?: { freeAbove?: number } | null;
|
|
28
40
|
allowedPaymentMethods?: string[] | null;
|
|
29
41
|
isActive?: boolean;
|
|
@@ -69,31 +81,40 @@
|
|
|
69
81
|
)
|
|
70
82
|
);
|
|
71
83
|
type InputMode = 'net' | 'gross';
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
// initial.price — teraz PLN (number, netto) z API. Hydratujemy jako gross, żeby user widział dokładnie to co wpisał.
|
|
85
|
+
const initialNetPln = initial?.price != null ? Number(initial.price) : null;
|
|
86
|
+
const initialVat = Number(initial?.vatRate ?? vatRates[0] ?? 23);
|
|
87
|
+
let inputMode = $state<InputMode>(initialNetPln != null ? 'gross' : 'gross');
|
|
88
|
+
let inputPrice = $state(
|
|
89
|
+
initialNetPln != null
|
|
90
|
+
? (initialNetPln * (1 + initialVat / 100)).toFixed(2)
|
|
91
|
+
: '0.00'
|
|
92
|
+
);
|
|
74
93
|
let vatRate = $state<number | string>(initial?.vatRate ?? vatRates[0] ?? 23);
|
|
75
94
|
|
|
76
|
-
const
|
|
95
|
+
const inputPln = $derived(parseFloat(inputPrice || '0') || 0);
|
|
77
96
|
const vat = $derived(Number(vatRate) || 0);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
);
|
|
81
|
-
const grossCents = $derived(
|
|
82
|
-
inputMode === 'gross' ? inputPriceCents : Math.round(inputPriceCents * (1 + vat / 100))
|
|
83
|
-
);
|
|
84
|
-
const vatCents = $derived(grossCents - netCents);
|
|
97
|
+
const netPln = $derived(inputMode === 'net' ? inputPln : inputPln / (1 + vat / 100));
|
|
98
|
+
const grossPln = $derived(inputMode === 'gross' ? inputPln : inputPln * (1 + vat / 100));
|
|
99
|
+
const vatPln = $derived(grossPln - netPln);
|
|
85
100
|
|
|
86
|
-
function
|
|
87
|
-
return
|
|
101
|
+
function formatPln(pln: number) {
|
|
102
|
+
return pln.toFixed(2);
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
function switchMode(newMode: InputMode) {
|
|
91
106
|
if (newMode === inputMode) return;
|
|
92
|
-
const
|
|
107
|
+
const preserved = newMode === 'net' ? netPln : grossPln;
|
|
93
108
|
inputMode = newMode;
|
|
94
|
-
inputPrice =
|
|
109
|
+
inputPrice = formatPln(preserved);
|
|
95
110
|
}
|
|
96
111
|
let carrierType = $state<CarrierType>((initial?.carrierType as CarrierType) ?? 'none');
|
|
112
|
+
let inpostServiceType = $state<InpostServiceType>(
|
|
113
|
+
(initial?.carrierConfig?.serviceType as InpostServiceType) ?? 'inpost_locker_standard'
|
|
114
|
+
);
|
|
115
|
+
let inpostDefaultSize = $state<InpostParcelSize>(
|
|
116
|
+
(initial?.carrierConfig?.defaultSize as InpostParcelSize) ?? 'A'
|
|
117
|
+
);
|
|
97
118
|
let isActive = $state(initial?.isActive ?? true);
|
|
98
119
|
// null/undefined = no restriction (all allowed); otherwise a whitelist.
|
|
99
120
|
let restrictPayments = $state(
|
|
@@ -126,9 +147,13 @@
|
|
|
126
147
|
await onsubmit({
|
|
127
148
|
name: names,
|
|
128
149
|
description: Object.values(descriptions).some((d) => d.length > 0) ? descriptions : null,
|
|
129
|
-
price:
|
|
150
|
+
price: netPln,
|
|
130
151
|
vatRate: Number(vatRate),
|
|
131
152
|
carrierType,
|
|
153
|
+
carrierConfig:
|
|
154
|
+
carrierType === 'inpost'
|
|
155
|
+
? { serviceType: inpostServiceType, defaultSize: inpostDefaultSize }
|
|
156
|
+
: null,
|
|
132
157
|
conditions: freeAboveEnabled
|
|
133
158
|
? { freeAbove: Math.round(parseFloat(freeAbove || '0') * 100) }
|
|
134
159
|
: null,
|
|
@@ -226,15 +251,15 @@
|
|
|
226
251
|
<div class="bg-muted/40 border-border grid grid-cols-3 gap-2 rounded-lg border p-2.5 text-center text-xs">
|
|
227
252
|
<div>
|
|
228
253
|
<div class="text-muted-foreground font-semibold uppercase tracking-wide">Netto</div>
|
|
229
|
-
<div class="text-sm font-bold tabular-nums">{
|
|
254
|
+
<div class="text-sm font-bold tabular-nums">{formatPln(netPln)} zł</div>
|
|
230
255
|
</div>
|
|
231
256
|
<div class="border-border border-x">
|
|
232
257
|
<div class="text-muted-foreground font-semibold uppercase tracking-wide">VAT</div>
|
|
233
|
-
<div class="text-sm font-bold tabular-nums">{
|
|
258
|
+
<div class="text-sm font-bold tabular-nums">{formatPln(vatPln)} zł</div>
|
|
234
259
|
</div>
|
|
235
260
|
<div>
|
|
236
261
|
<div class="text-muted-foreground font-semibold uppercase tracking-wide">Brutto</div>
|
|
237
|
-
<div class="text-primary text-sm font-bold tabular-nums">{
|
|
262
|
+
<div class="text-primary text-sm font-bold tabular-nums">{formatPln(grossPln)} zł</div>
|
|
238
263
|
</div>
|
|
239
264
|
</div>
|
|
240
265
|
<label class="flex items-center gap-2">
|
|
@@ -297,9 +322,52 @@
|
|
|
297
322
|
<span class="mb-1 block text-sm font-semibold">Typ</span>
|
|
298
323
|
<select bind:value={carrierType} class="border-border w-full rounded-lg border px-3 py-2">
|
|
299
324
|
<option value="none">Bez integracji (adres)</option>
|
|
300
|
-
<option value="inpost"
|
|
325
|
+
<option value="inpost">InPost</option>
|
|
301
326
|
</select>
|
|
302
327
|
</label>
|
|
328
|
+
|
|
329
|
+
{#if carrierType === 'inpost'}
|
|
330
|
+
<div class="space-y-4 border-l-2 border-primary/30 pl-4">
|
|
331
|
+
<p class="text-muted-foreground text-sm">
|
|
332
|
+
Wymaga skonfigurowanego adaptera <code>inpostAdapter</code> w <code>cms.config.ts</code>.
|
|
333
|
+
</p>
|
|
334
|
+
|
|
335
|
+
<label class="block">
|
|
336
|
+
<span class="mb-1 block text-sm font-semibold">Usługa ShipX</span>
|
|
337
|
+
<select
|
|
338
|
+
bind:value={inpostServiceType}
|
|
339
|
+
class="border-border w-full rounded-lg border px-3 py-2"
|
|
340
|
+
>
|
|
341
|
+
<option value="inpost_locker_standard">Paczkomat — standard</option>
|
|
342
|
+
<option value="inpost_locker_express">Paczkomat — Express</option>
|
|
343
|
+
<option value="inpost_courier_standard">Kurier — standard</option>
|
|
344
|
+
<option value="inpost_courier_express">Kurier — Express</option>
|
|
345
|
+
</select>
|
|
346
|
+
</label>
|
|
347
|
+
|
|
348
|
+
<label class="block">
|
|
349
|
+
<span class="mb-1 block text-sm font-semibold">Domyślny rozmiar paczki</span>
|
|
350
|
+
<select
|
|
351
|
+
bind:value={inpostDefaultSize}
|
|
352
|
+
class="border-border w-full rounded-lg border px-3 py-2"
|
|
353
|
+
>
|
|
354
|
+
<option value="A">A — mała (8 × 38 × 64 cm)</option>
|
|
355
|
+
<option value="B">B — średnia (19 × 38 × 64 cm)</option>
|
|
356
|
+
<option value="C">C — duża (41 × 38 × 64 cm)</option>
|
|
357
|
+
</select>
|
|
358
|
+
</label>
|
|
359
|
+
|
|
360
|
+
{#if inpostServiceType.startsWith('inpost_courier_')}
|
|
361
|
+
<p class="text-muted-foreground text-xs">
|
|
362
|
+
Kurier dostarcza pod adres — klient nie wybiera paczkomatu.
|
|
363
|
+
</p>
|
|
364
|
+
{:else}
|
|
365
|
+
<p class="text-muted-foreground text-xs">
|
|
366
|
+
Klient wybiera paczkomat odbiorczy w geowidgetcie InPost.
|
|
367
|
+
</p>
|
|
368
|
+
{/if}
|
|
369
|
+
</div>
|
|
370
|
+
{/if}
|
|
303
371
|
</section>
|
|
304
372
|
|
|
305
373
|
<div class="flex gap-3">
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
type CarrierType = 'none' | 'inpost';
|
|
2
|
+
type InpostServiceType = 'inpost_locker_standard' | 'inpost_locker_express' | 'inpost_courier_standard' | 'inpost_courier_express';
|
|
3
|
+
type InpostParcelSize = 'A' | 'B' | 'C';
|
|
4
|
+
interface CarrierConfig {
|
|
5
|
+
serviceType?: InpostServiceType | string;
|
|
6
|
+
defaultSize?: InpostParcelSize | string;
|
|
7
|
+
}
|
|
2
8
|
export interface PaymentMethodOption {
|
|
3
9
|
id: string;
|
|
4
10
|
label: Record<string, string>;
|
|
@@ -9,6 +15,7 @@ export interface ShippingFormPayload {
|
|
|
9
15
|
price: number;
|
|
10
16
|
vatRate: number;
|
|
11
17
|
carrierType: CarrierType;
|
|
18
|
+
carrierConfig: CarrierConfig | null;
|
|
12
19
|
conditions: {
|
|
13
20
|
freeAbove?: number;
|
|
14
21
|
} | null;
|
|
@@ -19,9 +26,10 @@ export interface ShippingFormPayload {
|
|
|
19
26
|
export interface ShippingFormInitial {
|
|
20
27
|
name?: Record<string, string> | unknown;
|
|
21
28
|
description?: Record<string, string> | unknown | null;
|
|
22
|
-
price?: number;
|
|
29
|
+
price?: number | string;
|
|
23
30
|
vatRate?: number;
|
|
24
31
|
carrierType?: string;
|
|
32
|
+
carrierConfig?: CarrierConfig | null;
|
|
25
33
|
conditions?: {
|
|
26
34
|
freeAbove?: number;
|
|
27
35
|
} | null;
|
|
@@ -35,12 +35,15 @@
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
function
|
|
38
|
+
function formatPricePln(pln: number) {
|
|
39
39
|
return new Intl.NumberFormat('pl-PL', {
|
|
40
40
|
style: 'currency',
|
|
41
41
|
currency: 'PLN',
|
|
42
42
|
minimumFractionDigits: 2
|
|
43
|
-
}).format(
|
|
43
|
+
}).format(pln);
|
|
44
|
+
}
|
|
45
|
+
function formatPriceCents(cents: number) {
|
|
46
|
+
return formatPricePln(cents / 100);
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
async function doReorder(fromIndex: number, toIndex: number) {
|
|
@@ -143,11 +146,11 @@
|
|
|
143
146
|
{resolveI18n(m.name as Record<string, string>, interfaceLanguage.current, '')}
|
|
144
147
|
</a>
|
|
145
148
|
</div>
|
|
146
|
-
<div>{
|
|
149
|
+
<div>{formatPricePln(Number(m.price))}</div>
|
|
147
150
|
<div>{m.vatRate}%</div>
|
|
148
151
|
<div>
|
|
149
152
|
{#if cond?.freeAbove != null}
|
|
150
|
-
{
|
|
153
|
+
{formatPriceCents(cond.freeAbove)}
|
|
151
154
|
{:else}
|
|
152
155
|
<span class="text-muted-foreground text-xs">—</span>
|
|
153
156
|
{/if}
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
let note = $state('');
|
|
46
46
|
let saving = $state(false);
|
|
47
47
|
let resending = $state(false);
|
|
48
|
+
let shipping = $state(false);
|
|
48
49
|
let errorMessage = $state<string | null>(null);
|
|
49
50
|
let successMessage = $state<string | null>(null);
|
|
50
51
|
|
|
@@ -102,6 +103,49 @@
|
|
|
102
103
|
resending = false;
|
|
103
104
|
}
|
|
104
105
|
}
|
|
106
|
+
|
|
107
|
+
async function handleCreateShipment() {
|
|
108
|
+
shipping = true;
|
|
109
|
+
errorMessage = null;
|
|
110
|
+
successMessage = null;
|
|
111
|
+
try {
|
|
112
|
+
const result = await remotes.createShipmentForOrderCmd({ orderId });
|
|
113
|
+
if (!result.success) {
|
|
114
|
+
errorMessage = result.error;
|
|
115
|
+
} else {
|
|
116
|
+
successMessage = result.trackingNumber
|
|
117
|
+
? `Przesyłka utworzona — ${result.trackingNumber}`
|
|
118
|
+
: 'Przesyłka utworzona. Numer śledzenia pojawi się po potwierdzeniu w ShipX.';
|
|
119
|
+
await query.refresh();
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
errorMessage =
|
|
123
|
+
err instanceof Error ? err.message : 'Błąd przy tworzeniu przesyłki';
|
|
124
|
+
} finally {
|
|
125
|
+
shipping = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function handleCancelShipment() {
|
|
130
|
+
if (!confirm('Anulować przesyłkę? Etykieta zostanie unieważniona w ShipX.')) return;
|
|
131
|
+
shipping = true;
|
|
132
|
+
errorMessage = null;
|
|
133
|
+
successMessage = null;
|
|
134
|
+
try {
|
|
135
|
+
const result = await remotes.cancelShipmentForOrderCmd({ orderId });
|
|
136
|
+
if (!result.success) {
|
|
137
|
+
errorMessage = result.error;
|
|
138
|
+
} else {
|
|
139
|
+
successMessage = 'Przesyłka anulowana.';
|
|
140
|
+
await query.refresh();
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
errorMessage =
|
|
144
|
+
err instanceof Error ? err.message : 'Błąd przy anulowaniu przesyłki';
|
|
145
|
+
} finally {
|
|
146
|
+
shipping = false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
105
149
|
</script>
|
|
106
150
|
|
|
107
151
|
{#if !query.ready}
|
|
@@ -268,6 +312,63 @@
|
|
|
268
312
|
</Button>
|
|
269
313
|
</section>
|
|
270
314
|
|
|
315
|
+
{#if order.carrierType && order.carrierType !== 'none'}
|
|
316
|
+
<section class="border-border bg-card space-y-3 rounded-xl border p-5 text-sm">
|
|
317
|
+
<h2 class="text-base font-bold">
|
|
318
|
+
Przesyłka <span class="text-muted-foreground font-mono text-xs">{order.carrierType}</span>
|
|
319
|
+
</h2>
|
|
320
|
+
{#if order.shipmentId}
|
|
321
|
+
<div class="space-y-1">
|
|
322
|
+
<div class="text-muted-foreground text-xs font-semibold">Shipment ID</div>
|
|
323
|
+
<div class="font-mono text-xs break-all">{order.shipmentId}</div>
|
|
324
|
+
</div>
|
|
325
|
+
{#if order.trackingNumber}
|
|
326
|
+
<div class="space-y-1">
|
|
327
|
+
<div class="text-muted-foreground text-xs font-semibold">Numer śledzenia</div>
|
|
328
|
+
<div class="font-mono text-xs break-all">{order.trackingNumber}</div>
|
|
329
|
+
</div>
|
|
330
|
+
{:else}
|
|
331
|
+
<p class="text-xs text-amber-700">
|
|
332
|
+
Numer śledzenia pojawi się gdy ShipX potwierdzi przesyłkę (zwykle w kilka minut).
|
|
333
|
+
</p>
|
|
334
|
+
{/if}
|
|
335
|
+
<div class="flex flex-wrap gap-2 pt-2">
|
|
336
|
+
<Button
|
|
337
|
+
href="/api/shop/admin/orders/{orderId}/label"
|
|
338
|
+
variant="outline"
|
|
339
|
+
size="sm"
|
|
340
|
+
target="_blank"
|
|
341
|
+
>
|
|
342
|
+
Pobierz etykietę PDF
|
|
343
|
+
</Button>
|
|
344
|
+
<Button
|
|
345
|
+
onclick={handleCancelShipment}
|
|
346
|
+
disabled={shipping}
|
|
347
|
+
variant="outline"
|
|
348
|
+
size="sm"
|
|
349
|
+
>
|
|
350
|
+
Anuluj przesyłkę
|
|
351
|
+
</Button>
|
|
352
|
+
</div>
|
|
353
|
+
{:else if order.status === 'paid' || order.status === 'preparing'}
|
|
354
|
+
<p class="text-muted-foreground text-xs">
|
|
355
|
+
Wygeneruj etykietę i nadaj paczkę przez ShipX.
|
|
356
|
+
</p>
|
|
357
|
+
<Button
|
|
358
|
+
onclick={handleCreateShipment}
|
|
359
|
+
disabled={shipping}
|
|
360
|
+
class="w-full"
|
|
361
|
+
>
|
|
362
|
+
{shipping ? 'Tworzenie…' : 'Utwórz przesyłkę InPost'}
|
|
363
|
+
</Button>
|
|
364
|
+
{:else}
|
|
365
|
+
<p class="text-muted-foreground text-xs">
|
|
366
|
+
Przesyłkę można utworzyć po opłaceniu zamówienia.
|
|
367
|
+
</p>
|
|
368
|
+
{/if}
|
|
369
|
+
</section>
|
|
370
|
+
{/if}
|
|
371
|
+
|
|
271
372
|
<section class="border-border bg-card space-y-2 rounded-xl border p-5 text-sm">
|
|
272
373
|
<h2 class="text-base font-bold">Klient</h2>
|
|
273
374
|
<div>
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
const entriesQuery = $derived(remotes.listShopProductEntries());
|
|
14
14
|
const collectionsQuery = $derived(remotes.listShopableCollections());
|
|
15
15
|
|
|
16
|
-
function formatPrice(
|
|
16
|
+
function formatPrice(pln: number) {
|
|
17
17
|
return new Intl.NumberFormat('pl-PL', {
|
|
18
18
|
style: 'currency',
|
|
19
19
|
currency: 'PLN',
|
|
20
20
|
minimumFractionDigits: 2
|
|
21
|
-
}).format(
|
|
21
|
+
}).format(pln);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function resolveTitle(data: Record<string, unknown> | null, fallback: string): string {
|