includio-cms 0.27.0 → 0.33.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 (115) hide show
  1. package/API.md +58 -14
  2. package/CHANGELOG.md +59 -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 +543 -543
  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 +156 -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 +2 -2
  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 +1 -1
  30. package/dist/admin/remote/shop.remote.d.ts +125 -40
  31. package/dist/admin/remote/shop.remote.js +59 -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/input/input.svelte.d.ts +1 -1
  37. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  38. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  39. package/dist/core/cms.d.ts +44 -2
  40. package/dist/core/cms.js +64 -0
  41. package/dist/core/index.d.ts +2 -4
  42. package/dist/core/index.js +1 -4
  43. package/dist/core/server/index.d.ts +4 -1
  44. package/dist/core/server/index.js +4 -1
  45. package/dist/db-postgres/schema/shop/index.d.ts +1 -0
  46. package/dist/db-postgres/schema/shop/index.js +1 -0
  47. package/dist/db-postgres/schema/shop/invoice.d.ts +254 -0
  48. package/dist/db-postgres/schema/shop/invoice.js +27 -0
  49. package/dist/db-postgres/schema/shop/order.d.ts +104 -0
  50. package/dist/db-postgres/schema/shop/order.js +8 -0
  51. package/dist/shop/adapters/fakturownia/client.d.ts +33 -0
  52. package/dist/shop/adapters/fakturownia/client.js +87 -0
  53. package/dist/shop/adapters/fakturownia/index.d.ts +27 -0
  54. package/dist/shop/adapters/fakturownia/index.js +47 -0
  55. package/dist/shop/adapters/fakturownia/payload.d.ts +35 -0
  56. package/dist/shop/adapters/fakturownia/payload.js +45 -0
  57. package/dist/shop/adapters/payu/index.js +11 -0
  58. package/dist/shop/client/index.d.ts +7 -0
  59. package/dist/shop/http/checkout-handler.js +11 -0
  60. package/dist/shop/index.d.ts +4 -1
  61. package/dist/shop/index.js +3 -0
  62. package/dist/shop/nip.d.ts +12 -0
  63. package/dist/shop/nip.js +23 -0
  64. package/dist/shop/server/coupons.d.ts +10 -0
  65. package/dist/shop/server/coupons.js +19 -0
  66. package/dist/shop/server/email.d.ts +7 -3
  67. package/dist/shop/server/email.js +86 -112
  68. package/dist/shop/server/emailTemplateRegistry.d.ts +47 -0
  69. package/dist/shop/server/emailTemplateRegistry.js +288 -0
  70. package/dist/shop/server/invoices.d.ts +64 -0
  71. package/dist/shop/server/invoices.js +237 -0
  72. package/dist/shop/server/orders.d.ts +64 -1
  73. package/dist/shop/server/orders.js +155 -15
  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 +130 -1
  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 +1 -0
  106. package/dist/sveltekit/server/index.js +1 -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.28.0/index.d.ts +2 -0
  111. package/dist/updates/0.28.0/index.js +38 -0
  112. package/dist/updates/0.34.0/index.d.ts +2 -0
  113. package/dist/updates/0.34.0/index.js +17 -0
  114. package/dist/updates/index.js +5 -1
  115. package/package.json +7 -2
@@ -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
 
@@ -36,6 +49,37 @@
36
49
  const refundsQuery = $derived(remotes.getOrderRefundsAdmin(orderId));
37
50
  let refundDialogOpen = $state(false);
38
51
 
52
+ const invoiceQuery = $derived(remotes.getOrderInvoiceAdmin(orderId));
53
+ let issuingInvoice = $state(false);
54
+ let invoiceError = $state<string | null>(null);
55
+
56
+ async function issueInvoice(force: boolean) {
57
+ issuingInvoice = true;
58
+ invoiceError = null;
59
+ try {
60
+ const result = await remotes.issueInvoiceCmd({ orderId, force });
61
+ if (!result.success) invoiceError = result.error;
62
+ await invoiceQuery.refresh();
63
+ } finally {
64
+ issuingInvoice = false;
65
+ }
66
+ }
67
+
68
+ function invoiceStatusLabel(status: string): string {
69
+ switch (status) {
70
+ case 'pending':
71
+ return 'W trakcie';
72
+ case 'issued':
73
+ return 'Wystawiona';
74
+ case 'sent':
75
+ return 'Wysłana';
76
+ case 'failed':
77
+ return 'Błąd';
78
+ default:
79
+ return status;
80
+ }
81
+ }
82
+
39
83
  type OrderStatus =
40
84
  | 'new'
41
85
  | 'awaitingPayment'
@@ -198,6 +242,29 @@
198
242
  balanceLinkBusy = false;
199
243
  }
200
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
+ }
201
268
  </script>
202
269
 
203
270
  {#if !query.ready}
@@ -208,7 +275,7 @@
208
275
  <Button href="/admin/shop/orders" variant="outline">← Wróć do listy</Button>
209
276
  </div>
210
277
  {:else}
211
- {@const { order, items, history } = query.current}
278
+ {@const { order, items, history, coupon } = query.current}
212
279
  {@const address = order.shippingAddress as Record<string, string> | null}
213
280
  {@const consents = order.consents as Array<{
214
281
  id: string;
@@ -293,6 +360,14 @@
293
360
  >{formatCentsPrice(order.shippingGross, order.currency)}</td
294
361
  >
295
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}
296
371
  <tr>
297
372
  <td colspan="5" class="text-right text-sm">Razem netto:</td>
298
373
  <td class="text-muted-foreground text-right tabular-nums">
@@ -501,6 +576,60 @@
501
576
  </section>
502
577
  {/if}
503
578
 
579
+ {#if invoiceQuery.ready && invoiceQuery.current?.invoicingEnabled}
580
+ {@const inv = invoiceQuery.current.invoice}
581
+ {@const orderPaid =
582
+ order.status === 'paid' ||
583
+ order.status === 'preparing' ||
584
+ order.status === 'sent' ||
585
+ order.status === 'done'}
586
+ <section class="border-border bg-card space-y-3 rounded-xl border p-3 text-sm sm:p-5">
587
+ <h2 class="text-base font-bold">Faktura</h2>
588
+ {#if inv}
589
+ <div class="space-y-1">
590
+ <div class="flex items-center justify-between gap-2">
591
+ <span class="font-medium">{inv.number ?? '—'}</span>
592
+ <span class="text-muted-foreground text-xs">{invoiceStatusLabel(inv.status)}</span>
593
+ </div>
594
+ {#if inv.pdfUrl}
595
+ <a
596
+ href={inv.pdfUrl}
597
+ target="_blank"
598
+ rel="noopener noreferrer"
599
+ class="text-primary text-xs underline"
600
+ >
601
+ Otwórz fakturę
602
+ </a>
603
+ {/if}
604
+ {#if inv.status === 'failed' && inv.lastError}
605
+ <p class="text-destructive text-xs">{inv.lastError}</p>
606
+ {/if}
607
+ </div>
608
+ {:else}
609
+ <p class="text-muted-foreground text-xs">Brak faktury dla tego zamówienia.</p>
610
+ {/if}
611
+ {#if invoiceError}
612
+ <p class="text-destructive text-xs">{invoiceError}</p>
613
+ {/if}
614
+ {#if orderPaid}
615
+ <Button
616
+ onclick={() => issueInvoice(true)}
617
+ variant="outline"
618
+ class="w-full"
619
+ disabled={issuingInvoice}
620
+ >
621
+ {inv && (inv.status === 'issued' || inv.status === 'sent')
622
+ ? 'Wystaw ponownie'
623
+ : 'Wystaw fakturę'}
624
+ </Button>
625
+ {:else}
626
+ <p class="text-muted-foreground text-xs">
627
+ Fakturę można wystawić po opłaceniu zamówienia.
628
+ </p>
629
+ {/if}
630
+ </section>
631
+ {/if}
632
+
504
633
  {#if order.carrierType && order.carrierType !== 'none'}
505
634
  <section class="border-border bg-card space-y-3 rounded-xl border p-3 text-sm sm:p-5">
506
635
  <h2 class="text-base font-bold">
@@ -619,6 +748,24 @@
619
748
  <p class="text-xs whitespace-pre-wrap">{order.notes}</p>
620
749
  </section>
621
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}
622
769
  </div>
623
770
  </div>
624
771
  </div>
@@ -647,3 +794,11 @@
647
794
  loading={shipping}
648
795
  onConfirm={performCancelShipment}
649
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
+ />