includio-cms 0.28.0 → 0.34.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 (117) hide show
  1. package/API.md +39 -13
  2. package/CHANGELOG.md +19 -0
  3. package/DOCS.md +1 -1
  4. package/ROADMAP.md +1 -0
  5. package/dist/admin/api/handler.js +4 -0
  6. package/dist/admin/api/integrations.d.ts +13 -0
  7. package/dist/admin/api/integrations.js +61 -0
  8. package/dist/admin/api/test-email.d.ts +9 -0
  9. package/dist/admin/api/test-email.js +39 -0
  10. package/dist/admin/auth-client.d.ts +2209 -2209
  11. package/dist/admin/client/index.d.ts +10 -0
  12. package/dist/admin/client/index.js +12 -0
  13. package/dist/admin/client/maintenance/maintenance-page.svelte +210 -0
  14. package/dist/admin/client/shop/coupon-schema.d.ts +1 -1
  15. package/dist/admin/client/shop/restore-order-cell.svelte +29 -0
  16. package/dist/admin/client/shop/restore-order-cell.svelte.d.ts +8 -0
  17. package/dist/admin/client/shop/shop-order-detail-page.svelte +71 -1
  18. package/dist/admin/client/shop/shop-orders-list-page.svelte +113 -53
  19. package/dist/admin/components/layout/app-sidebar.svelte +2 -0
  20. package/dist/admin/components/layout/nav-custom.svelte +26 -0
  21. package/dist/admin/components/layout/nav-custom.svelte.d.ts +3 -0
  22. package/dist/admin/components/layout/page-header.svelte +13 -3
  23. package/dist/admin/components/layout/page-header.svelte.d.ts +13 -3
  24. package/dist/admin/remote/admin.remote.d.ts +7 -0
  25. package/dist/admin/remote/admin.remote.js +10 -0
  26. package/dist/admin/remote/entry.remote.d.ts +4 -4
  27. package/dist/admin/remote/index.d.ts +1 -0
  28. package/dist/admin/remote/index.js +1 -0
  29. package/dist/admin/remote/invite.d.ts +2 -2
  30. package/dist/admin/remote/shop.remote.d.ts +75 -48
  31. package/dist/admin/remote/shop.remote.js +41 -10
  32. package/dist/admin/types.d.ts +15 -0
  33. package/dist/admin/utils/csv-export.d.ts +45 -0
  34. package/dist/admin/utils/csv-export.js +61 -0
  35. package/dist/cli/scaffold/admin.js +1 -1
  36. package/dist/components/ui/button-group/button-group-separator.svelte.d.ts +1 -1
  37. package/dist/components/ui/command/command.svelte.d.ts +1 -1
  38. package/dist/components/ui/field/field-label.svelte.d.ts +1 -1
  39. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  40. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  41. package/dist/components/ui/item/item-separator.svelte.d.ts +1 -1
  42. package/dist/components/ui/select/select-group-heading.svelte.d.ts +1 -1
  43. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  44. package/dist/components/ui/sidebar/sidebar-separator.svelte.d.ts +1 -1
  45. package/dist/core/cms.d.ts +44 -2
  46. package/dist/core/cms.js +64 -0
  47. package/dist/core/index.d.ts +1 -4
  48. package/dist/core/index.js +4 -4
  49. package/dist/core/server/index.d.ts +4 -1
  50. package/dist/core/server/index.js +4 -1
  51. package/dist/db-postgres/schema/shop/order.d.ts +34 -0
  52. package/dist/db-postgres/schema/shop/order.js +4 -0
  53. package/dist/paraglide/messages/_index.d.ts +3 -36
  54. package/dist/paraglide/messages/_index.js +3 -71
  55. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  56. package/dist/paraglide/messages/hello_world.js +33 -0
  57. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  58. package/dist/paraglide/messages/login_hello.js +34 -0
  59. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  60. package/dist/paraglide/messages/login_please_login.js +34 -0
  61. package/dist/shop/adapters/fakturownia/client.d.ts +5 -0
  62. package/dist/shop/adapters/fakturownia/client.js +20 -0
  63. package/dist/shop/adapters/fakturownia/index.js +11 -0
  64. package/dist/shop/adapters/payu/index.js +11 -0
  65. package/dist/shop/index.d.ts +1 -1
  66. package/dist/shop/server/coupons.d.ts +10 -0
  67. package/dist/shop/server/coupons.js +19 -0
  68. package/dist/shop/server/email.d.ts +7 -3
  69. package/dist/shop/server/email.js +86 -112
  70. package/dist/shop/server/emailTemplateRegistry.d.ts +47 -0
  71. package/dist/shop/server/emailTemplateRegistry.js +288 -0
  72. package/dist/shop/server/orders.d.ts +60 -1
  73. package/dist/shop/server/orders.js +145 -16
  74. package/dist/shop/templates/_partials/footer.en.html +4 -0
  75. package/dist/shop/templates/_partials/footer.pl.html +4 -0
  76. package/dist/shop/templates/_partials/header.en.html +4 -0
  77. package/dist/shop/templates/_partials/header.pl.html +4 -0
  78. package/dist/shop/templates/_partials/items.en.html +14 -0
  79. package/dist/shop/templates/_partials/items.pl.html +14 -0
  80. package/dist/shop/templates/_partials/tracking.en.html +7 -0
  81. package/dist/shop/templates/_partials/tracking.pl.html +7 -0
  82. package/dist/shop/templates/awaiting-payment.en.html +6 -0
  83. package/dist/shop/templates/awaiting-payment.pl.html +6 -0
  84. package/dist/shop/templates/cancelled.en.html +6 -0
  85. package/dist/shop/templates/cancelled.pl.html +6 -0
  86. package/dist/shop/templates/low-stock.en.html +14 -0
  87. package/dist/shop/templates/low-stock.pl.html +14 -0
  88. package/dist/shop/templates/order-completed.en.html +6 -0
  89. package/dist/shop/templates/order-completed.pl.html +6 -0
  90. package/dist/shop/templates/order-received.en.html +7 -0
  91. package/dist/shop/templates/order-received.pl.html +7 -0
  92. package/dist/shop/templates/payment-received.en.html +7 -0
  93. package/dist/shop/templates/payment-received.pl.html +7 -0
  94. package/dist/shop/templates/payment-rejected.en.html +6 -0
  95. package/dist/shop/templates/payment-rejected.pl.html +6 -0
  96. package/dist/shop/templates/preparing.en.html +7 -0
  97. package/dist/shop/templates/preparing.pl.html +7 -0
  98. package/dist/shop/templates/refunded.en.html +6 -0
  99. package/dist/shop/templates/refunded.pl.html +6 -0
  100. package/dist/shop/templates/shipped.en.html +7 -0
  101. package/dist/shop/templates/shipped.pl.html +7 -0
  102. package/dist/shop/types.d.ts +63 -0
  103. package/dist/sveltekit/index.d.ts +0 -1
  104. package/dist/sveltekit/index.js +0 -1
  105. package/dist/sveltekit/server/index.d.ts +2 -0
  106. package/dist/sveltekit/server/index.js +4 -0
  107. package/dist/types/adapters/email.d.ts +13 -0
  108. package/dist/types/cms.d.ts +30 -0
  109. package/dist/types/index.d.ts +1 -1
  110. package/dist/updates/0.34.0/index.d.ts +2 -0
  111. package/dist/updates/0.34.0/index.js +17 -0
  112. package/dist/updates/index.js +3 -1
  113. package/package.json +7 -2
  114. package/dist/paraglide/messages/en.d.ts +0 -5
  115. package/dist/paraglide/messages/en.js +0 -14
  116. package/dist/paraglide/messages/pl.d.ts +0 -5
  117. package/dist/paraglide/messages/pl.js +0 -14
@@ -23,3 +23,13 @@ export { default as ShopCouponNewPage } from './shop/coupon-new-page.svelte';
23
23
  export { default as ShopCouponEditPage } from './shop/coupon-edit-page.svelte';
24
24
  export * from '../helpers/index.js';
25
25
  export * from '../ui/index.js';
26
+ export { default as DataTable } from './collection/data-table.svelte';
27
+ export { default as TableToolbar } from './collection/table-toolbar.svelte';
28
+ export { default as TablePagination } from './collection/table-pagination.svelte';
29
+ export { default as StateDisplay } from './collection/state-display.svelte';
30
+ export { default as PageHeader } from '../components/layout/page-header.svelte';
31
+ export { buildCsv, downloadCsv } from '../utils/csv-export.js';
32
+ export type { ColumnDef, PaginationState, SortingState } from '@tanstack/table-core';
33
+ export { getBreadcrumbs, setBreadcrumbs, Breadcrumbs } from '../state/breadcrumbs.svelte.js';
34
+ export type { Breadcrumb, AdminNavItem } from '../types.js';
35
+ export type { AdminConfig } from '../../types/cms.js';
@@ -25,3 +25,15 @@ export { default as ShopCouponEditPage } from './shop/coupon-edit-page.svelte';
25
25
  export * from '../helpers/index.js';
26
26
  // Folded from `./admin/ui` (dropped as separate export in 0.20.0)
27
27
  export * from '../ui/index.js';
28
+ // Public list-page primitives (since 0.32.0). Use these when building custom
29
+ // admin list views (e.g. `/admin/newsletter`) so they share visual style and
30
+ // behavior with the built-in `CollectionPage` / `ShopOrdersListPage`.
31
+ export { default as DataTable } from './collection/data-table.svelte';
32
+ export { default as TableToolbar } from './collection/table-toolbar.svelte';
33
+ export { default as TablePagination } from './collection/table-pagination.svelte';
34
+ export { default as StateDisplay } from './collection/state-display.svelte';
35
+ export { default as PageHeader } from '../components/layout/page-header.svelte';
36
+ export { buildCsv, downloadCsv } from '../utils/csv-export.js';
37
+ // Breadcrumbs API (since 0.33.0). Set the breadcrumb trail from a custom
38
+ // admin page with `getBreadcrumbs().state = [{ label, href? }, …]`.
39
+ export { getBreadcrumbs, setBreadcrumbs, Breadcrumbs } from '../state/breadcrumbs.svelte.js';
@@ -14,6 +14,11 @@
14
14
  import Server from '@tabler/icons-svelte/icons/server';
15
15
  import Clock from '@tabler/icons-svelte/icons/clock';
16
16
  import Settings from '@tabler/icons-svelte/icons/automation';
17
+ import Plug from '@tabler/icons-svelte/icons/plug-connected';
18
+ import Mail from '@tabler/icons-svelte/icons/mail';
19
+ import CreditCard from '@tabler/icons-svelte/icons/credit-card';
20
+ import FileInvoice from '@tabler/icons-svelte/icons/file-invoice';
21
+ import Send from '@tabler/icons-svelte/icons/send';
17
22
  import Button from '../../../components/ui/button/button.svelte';
18
23
  import * as Card from '../../../components/ui/card/index.js';
19
24
  import { toast } from 'svelte-sonner';
@@ -363,8 +368,80 @@
363
368
  }
364
369
  }
365
370
 
371
+ // --- Integrations health-check ---
372
+ interface IntegrationsInfo {
373
+ email: { configured: boolean; adminEmail: string | null };
374
+ payment: { id: string; label: Record<string, string>; canPing: boolean }[];
375
+ invoicing: { id: string; canPing: boolean } | null;
376
+ }
377
+ type PingState = { state: 'idle' | 'checking' | 'ok' | 'error'; message?: string };
378
+
379
+ let integrations = $state<IntegrationsInfo | null>(null);
380
+ let pingStates = $state<Record<string, PingState>>({});
381
+ let testEmailTo = $state('');
382
+ let sendingTestEmail = $state(false);
383
+
384
+ function i18n(label: Record<string, string>): string {
385
+ return label[interfaceLanguage.current] ?? label.pl ?? label.en ?? Object.values(label)[0] ?? '';
386
+ }
387
+
388
+ async function loadIntegrations() {
389
+ try {
390
+ const res = await fetch('/admin/api/integrations');
391
+ if (res.ok) integrations = await res.json();
392
+ } catch {
393
+ // non-fatal — integrations section just stays hidden
394
+ }
395
+ }
396
+
397
+ async function pingIntegration(kind: 'payment' | 'invoicing', id: string) {
398
+ const key = kind === 'payment' ? `payment:${id}` : 'invoicing';
399
+ pingStates[key] = { state: 'checking' };
400
+ try {
401
+ const res = await fetch('/admin/api/integrations', {
402
+ method: 'POST',
403
+ headers: { 'Content-Type': 'application/json' },
404
+ body: JSON.stringify({ kind, id })
405
+ });
406
+ const data = await res.json();
407
+ if (res.ok && data.ok) {
408
+ pingStates[key] = { state: 'ok' };
409
+ toast.success('Połączenie działa');
410
+ } else {
411
+ const message = data.message ?? data.error ?? 'Błąd połączenia';
412
+ pingStates[key] = { state: 'error', message };
413
+ toast.error(message);
414
+ }
415
+ } catch {
416
+ pingStates[key] = { state: 'error', message: 'Błąd sieci' };
417
+ toast.error('Błąd sieci');
418
+ }
419
+ }
420
+
421
+ async function sendTestEmail() {
422
+ sendingTestEmail = true;
423
+ try {
424
+ const res = await fetch('/admin/api/test-email', {
425
+ method: 'POST',
426
+ headers: { 'Content-Type': 'application/json' },
427
+ body: JSON.stringify({ to: testEmailTo.trim() || undefined })
428
+ });
429
+ const data = await res.json();
430
+ if (res.ok && data.ok) {
431
+ toast.success('Wysłano testową wiadomość');
432
+ } else {
433
+ toast.error(data.message ?? 'Nie udało się wysłać wiadomości');
434
+ }
435
+ } catch {
436
+ toast.error('Błąd sieci');
437
+ } finally {
438
+ sendingTestEmail = false;
439
+ }
440
+ }
441
+
366
442
  $effect(() => {
367
443
  loadReport();
444
+ loadIntegrations();
368
445
  });
369
446
  </script>
370
447
 
@@ -814,4 +891,137 @@
814
891
  </div>
815
892
  {/if}
816
893
  {/if}
894
+
895
+ <!-- Section: Integrations health-check -->
896
+ {#snippet statusBadge(ps: PingState | undefined)}
897
+ {#if ps?.state === 'ok'}
898
+ <span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--success, #3A8A5C) 15%, transparent); color: var(--success, #3A8A5C);">Połączenie OK</span>
899
+ {:else if ps?.state === 'error'}
900
+ <span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--error, #C44B4B) 15%, transparent); color: var(--error, #C44B4B);">Błąd</span>
901
+ {:else if ps?.state === 'checking'}
902
+ <span class="flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium" style="background: var(--muted, #e5e7eb); color: var(--muted-foreground);"><Loader2 class="size-3 animate-spin" /> Sprawdzanie…</span>
903
+ {:else}
904
+ <span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: var(--muted, #e5e7eb); color: var(--muted-foreground);">Niesprawdzone</span>
905
+ {/if}
906
+ {/snippet}
907
+
908
+ {#if integrations}
909
+ <div class="mt-8">
910
+ <h2 class="mb-3 flex items-center gap-2 text-sm font-semibold uppercase tracking-wider" style="color: var(--muted-foreground);">
911
+ <Plug class="size-4" /> Integracje
912
+ </h2>
913
+ <div class="grid gap-4 md:grid-cols-2">
914
+ <!-- Email -->
915
+ <Card.Root>
916
+ <Card.Header class="pb-3">
917
+ <div class="flex items-center justify-between">
918
+ <div class="flex items-center gap-2">
919
+ <Mail class="size-5" style="color: var(--primary);" />
920
+ <Card.Title class="text-base">E-mail</Card.Title>
921
+ </div>
922
+ {#if integrations.email.configured}
923
+ <span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--success, #3A8A5C) 15%, transparent); color: var(--success, #3A8A5C);">Skonfigurowane</span>
924
+ {:else}
925
+ <span class="rounded-full px-2 py-0.5 text-xs font-medium" style="background: var(--muted, #e5e7eb); color: var(--muted-foreground);">Nieskonfigurowane</span>
926
+ {/if}
927
+ </div>
928
+ </Card.Header>
929
+ <Card.Content>
930
+ <p class="text-sm" style="color: var(--muted-foreground);">
931
+ Wyślij testową wiadomość, aby sprawdzić dostarczanie e-maili.
932
+ </p>
933
+ {#if integrations.email.configured}
934
+ <div class="mt-3 flex flex-col gap-2 sm:flex-row">
935
+ <input
936
+ type="email"
937
+ bind:value={testEmailTo}
938
+ placeholder={integrations.email.adminEmail ?? 'adres@przyklad.pl'}
939
+ class="flex-1 rounded-md border bg-transparent px-3 py-1.5 text-sm outline-none focus:ring-2"
940
+ style="border-color: var(--border);"
941
+ />
942
+ <Button variant="default" size="sm" onclick={sendTestEmail} disabled={sendingTestEmail}>
943
+ {#if sendingTestEmail}<Loader2 class="size-3.5 animate-spin" />{:else}<Send class="size-3.5" />{/if}
944
+ Wyślij testowy e-mail
945
+ </Button>
946
+ </div>
947
+ {#if !testEmailTo.trim() && integrations.email.adminEmail}
948
+ <p class="mt-1.5 text-xs" style="color: var(--muted-foreground);">
949
+ Puste pole → wyślemy na {integrations.email.adminEmail}
950
+ </p>
951
+ {:else if !testEmailTo.trim() && !integrations.email.adminEmail}
952
+ <p class="mt-1.5 text-xs" style="color: var(--warning, #C4893A);">
953
+ Podaj adres odbiorcy (brak ustawionego ADMIN_EMAIL).
954
+ </p>
955
+ {/if}
956
+ {/if}
957
+ </Card.Content>
958
+ </Card.Root>
959
+
960
+ <!-- Payment adapters -->
961
+ {#each integrations.payment as p (p.id)}
962
+ {@const ps = pingStates[`payment:${p.id}`]}
963
+ <Card.Root>
964
+ <Card.Header class="pb-3">
965
+ <div class="flex items-center justify-between">
966
+ <div class="flex items-center gap-2">
967
+ <CreditCard class="size-5" style="color: var(--primary);" />
968
+ <Card.Title class="text-base">{i18n(p.label)}</Card.Title>
969
+ </div>
970
+ {@render statusBadge(ps)}
971
+ </div>
972
+ </Card.Header>
973
+ <Card.Content>
974
+ <p class="text-sm" style="color: var(--muted-foreground);">Płatności · {p.id}</p>
975
+ {#if ps?.state === 'error' && ps.message}
976
+ <p class="mt-2 break-words text-xs" style="color: var(--error, #C44B4B);">{ps.message}</p>
977
+ {/if}
978
+ <div class="mt-3">
979
+ {#if p.canPing}
980
+ <Button variant="outline" size="sm" onclick={() => pingIntegration('payment', p.id)} disabled={ps?.state === 'checking'}>
981
+ {#if ps?.state === 'checking'}<Loader2 class="size-3.5 animate-spin" />{:else}<Plug class="size-3.5" />{/if}
982
+ Sprawdź połączenie
983
+ </Button>
984
+ {:else}
985
+ <p class="text-xs" style="color: var(--muted-foreground);">Sprawdzanie połączenia niedostępne dla tego adaptera.</p>
986
+ {/if}
987
+ </div>
988
+ </Card.Content>
989
+ </Card.Root>
990
+ {/each}
991
+
992
+ <!-- Invoicing -->
993
+ {#if integrations.invoicing}
994
+ {@const inv = integrations.invoicing}
995
+ {@const ps = pingStates['invoicing']}
996
+ <Card.Root>
997
+ <Card.Header class="pb-3">
998
+ <div class="flex items-center justify-between">
999
+ <div class="flex items-center gap-2">
1000
+ <FileInvoice class="size-5" style="color: var(--primary);" />
1001
+ <Card.Title class="text-base">Faktury</Card.Title>
1002
+ </div>
1003
+ {@render statusBadge(ps)}
1004
+ </div>
1005
+ </Card.Header>
1006
+ <Card.Content>
1007
+ <p class="text-sm" style="color: var(--muted-foreground);">Wystawianie faktur · {inv.id}</p>
1008
+ {#if ps?.state === 'error' && ps.message}
1009
+ <p class="mt-2 break-words text-xs" style="color: var(--error, #C44B4B);">{ps.message}</p>
1010
+ {/if}
1011
+ <div class="mt-3">
1012
+ {#if inv.canPing}
1013
+ <Button variant="outline" size="sm" onclick={() => pingIntegration('invoicing', inv.id)} disabled={ps?.state === 'checking'}>
1014
+ {#if ps?.state === 'checking'}<Loader2 class="size-3.5 animate-spin" />{:else}<Plug class="size-3.5" />{/if}
1015
+ Sprawdź połączenie
1016
+ </Button>
1017
+ {:else}
1018
+ <p class="text-xs" style="color: var(--muted-foreground);">Sprawdzanie połączenia niedostępne dla tego adaptera.</p>
1019
+ {/if}
1020
+ </div>
1021
+ </Card.Content>
1022
+ </Card.Root>
1023
+ {/if}
1024
+ </div>
1025
+ </div>
1026
+ {/if}
817
1027
  </div>
@@ -16,8 +16,8 @@ export type CouponInput = {
16
16
  export declare function createCouponSchema(lang?: InterfaceLanguage): z.ZodObject<{
17
17
  code: z.ZodString;
18
18
  type: z.ZodEnum<{
19
- percent: "percent";
20
19
  fixed: "fixed";
20
+ percent: "percent";
21
21
  }>;
22
22
  value: z.ZodNumber;
23
23
  minOrderAmount: z.ZodNullable<z.ZodNumber>;
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import { getRemotes } from '../../../sveltekit/index.js';
3
+ import { Button } from '../../../components/ui/button/index.js';
4
+ import RestoreIcon from '@tabler/icons-svelte/icons/restore';
5
+
6
+ let {
7
+ orderId,
8
+ label,
9
+ onRestored
10
+ }: { orderId: string; label: string; onRestored: () => void } = $props();
11
+
12
+ const remotes = getRemotes();
13
+ let busy = $state(false);
14
+
15
+ async function restore() {
16
+ busy = true;
17
+ try {
18
+ await remotes.restoreOrderCmd({ orderId });
19
+ onRestored();
20
+ } finally {
21
+ busy = false;
22
+ }
23
+ }
24
+ </script>
25
+
26
+ <Button variant="outline" size="sm" disabled={busy} onclick={restore}>
27
+ <RestoreIcon class="size-4" />
28
+ {label}
29
+ </Button>
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ orderId: string;
3
+ label: string;
4
+ onRestored: () => void;
5
+ };
6
+ declare const RestoreOrderCell: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type RestoreOrderCell = ReturnType<typeof RestoreOrderCell>;
8
+ export default RestoreOrderCell;
@@ -1,8 +1,11 @@
1
1
  <script lang="ts">
2
2
  import { page } from '$app/state';
3
+ import { goto } from '$app/navigation';
3
4
  import { getRemotes } from '../../../sveltekit/index.js';
5
+ import { authClient } from '../../auth-client.js';
4
6
  import { Button } from '../../../components/ui/button/index.js';
5
7
  import MailIcon from '@tabler/icons-svelte/icons/mail';
8
+ import TrashIcon from '@tabler/icons-svelte/icons/trash';
6
9
  import RefundDialog from './refund-dialog.svelte';
7
10
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
8
11
  import { getBreadcrumbs } from '../../state/breadcrumbs.svelte.js';
@@ -21,6 +24,16 @@
21
24
  const interfaceLanguage = useInterfaceLanguage();
22
25
  const breadcrumbs = getBreadcrumbs();
23
26
 
27
+ const session = authClient.useSession();
28
+ const isAdmin = $derived($session.data?.user?.role === 'admin');
29
+ // Mirror of DELETABLE_ORDER_STATUSES (server) — statuses an admin may hide.
30
+ const DELETABLE_STATUSES = new Set<OrderStatusT>([
31
+ 'new',
32
+ 'awaitingPayment',
33
+ 'cancelled',
34
+ 'paymentRejected'
35
+ ]);
36
+
24
37
  const orderId = $derived(page.params.id ?? '');
25
38
  const query = $derived(remotes.getOrderForAdmin(orderId));
26
39
 
@@ -229,6 +242,29 @@
229
242
  balanceLinkBusy = false;
230
243
  }
231
244
  }
245
+
246
+ let deleteDialogOpen = $state(false);
247
+ let deleting = $state(false);
248
+
249
+ async function performDelete() {
250
+ deleting = true;
251
+ errorMessage = null;
252
+ successMessage = null;
253
+ try {
254
+ const result = await remotes.deleteOrderCmd({ orderId });
255
+ if (!result.success) {
256
+ errorMessage = result.error;
257
+ deleteDialogOpen = false;
258
+ return;
259
+ }
260
+ await goto('/admin/shop/orders');
261
+ } catch (err) {
262
+ errorMessage = err instanceof Error ? err.message : 'Nie udało się usunąć zamówienia';
263
+ deleteDialogOpen = false;
264
+ } finally {
265
+ deleting = false;
266
+ }
267
+ }
232
268
  </script>
233
269
 
234
270
  {#if !query.ready}
@@ -239,7 +275,7 @@
239
275
  <Button href="/admin/shop/orders" variant="outline">← Wróć do listy</Button>
240
276
  </div>
241
277
  {:else}
242
- {@const { order, items, history } = query.current}
278
+ {@const { order, items, history, coupon } = query.current}
243
279
  {@const address = order.shippingAddress as Record<string, string> | null}
244
280
  {@const consents = order.consents as Array<{
245
281
  id: string;
@@ -324,6 +360,14 @@
324
360
  >{formatCentsPrice(order.shippingGross, order.currency)}</td
325
361
  >
326
362
  </tr>
363
+ {#if coupon && coupon.discountAmount > 0}
364
+ <tr>
365
+ <td colspan="5" class="text-right text-sm">Rabat ({coupon.code}):</td>
366
+ <td class="text-primary text-right tabular-nums">
367
+ −{formatCentsPrice(coupon.discountAmount, order.currency)}
368
+ </td>
369
+ </tr>
370
+ {/if}
327
371
  <tr>
328
372
  <td colspan="5" class="text-right text-sm">Razem netto:</td>
329
373
  <td class="text-muted-foreground text-right tabular-nums">
@@ -704,6 +748,24 @@
704
748
  <p class="text-xs whitespace-pre-wrap">{order.notes}</p>
705
749
  </section>
706
750
  {/if}
751
+
752
+ {#if isAdmin && !order.deletedAt && DELETABLE_STATUSES.has(order.status as OrderStatusT)}
753
+ <section class="border-destructive/30 bg-card space-y-3 rounded-xl border p-3 text-sm sm:p-5">
754
+ <h2 class="text-base font-bold">Usuń zamówienie</h2>
755
+ <p class="text-muted-foreground text-xs">
756
+ Zamówienie zniknie z listy, ale zostanie zapisane — w razie potrzeby przywrócisz je
757
+ z kosza. Dostępne tylko dla zamówień bez płatności i faktury.
758
+ </p>
759
+ <Button
760
+ onclick={() => (deleteDialogOpen = true)}
761
+ variant="outline"
762
+ class="text-destructive hover:bg-destructive-bg w-full"
763
+ >
764
+ <TrashIcon class="size-4" />
765
+ Usuń zamówienie
766
+ </Button>
767
+ </section>
768
+ {/if}
707
769
  </div>
708
770
  </div>
709
771
  </div>
@@ -732,3 +794,11 @@
732
794
  loading={shipping}
733
795
  onConfirm={performCancelShipment}
734
796
  />
797
+
798
+ <ConfirmationDialog
799
+ bind:open={deleteDialogOpen}
800
+ title="Usunąć zamówienie?"
801
+ description="Zniknie z listy zamówień. Zostanie zapisane i w razie potrzeby przywrócisz je z kosza."
802
+ loading={deleting}
803
+ onConfirm={performDelete}
804
+ />