includio-cms 0.36.8 → 0.37.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 (259) hide show
  1. package/API.md +179 -3
  2. package/CHANGELOG.md +64 -0
  3. package/DOCS.md +28 -3
  4. package/ROADMAP.md +18 -0
  5. package/dist/admin/client/booking/booking-dashboard.svelte +40 -0
  6. package/dist/admin/client/booking/booking-dashboard.svelte.d.ts +3 -0
  7. package/dist/admin/client/booking/customer-panel.svelte +102 -0
  8. package/dist/admin/client/booking/customer-panel.svelte.d.ts +11 -0
  9. package/dist/admin/client/booking/customer-view.d.ts +52 -0
  10. package/dist/admin/client/booking/customer-view.js +134 -0
  11. package/dist/admin/client/booking/participant-data-panel.svelte +330 -0
  12. package/dist/admin/client/booking/participant-data-panel.svelte.d.ts +22 -0
  13. package/dist/admin/client/booking/reservation-detail-page.svelte +637 -0
  14. package/dist/admin/client/booking/reservation-detail-page.svelte.d.ts +3 -0
  15. package/dist/admin/client/booking/reservations-page.svelte +221 -0
  16. package/dist/admin/client/booking/reservations-page.svelte.d.ts +3 -0
  17. package/dist/admin/client/collection/collection-entries.svelte +21 -2
  18. package/dist/admin/client/collection/entry-link.svelte +6 -4
  19. package/dist/admin/client/collection/entry-link.svelte.d.ts +3 -1
  20. package/dist/admin/client/collection/row-actions.svelte +12 -4
  21. package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
  22. package/dist/admin/client/collection/status-badge.svelte +39 -3
  23. package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
  24. package/dist/admin/client/entry/entry.svelte +26 -9
  25. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
  26. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
  27. package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
  28. package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
  29. package/dist/admin/client/entry/preview-views.d.ts +27 -0
  30. package/dist/admin/client/entry/preview-views.js +33 -0
  31. package/dist/admin/client/index.d.ts +3 -0
  32. package/dist/admin/client/index.js +3 -0
  33. package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
  34. package/dist/admin/client/modules/dashboard-registry.js +11 -0
  35. package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
  36. package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
  37. package/dist/admin/client/modules/stat-card.svelte +22 -0
  38. package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
  39. package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
  40. package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
  41. package/dist/admin/components/fields/relation-field.svelte +11 -2
  42. package/dist/admin/components/icons/icon-map.js +8 -0
  43. package/dist/admin/components/layout/app-sidebar.svelte +45 -14
  44. package/dist/admin/components/layout/context-switcher.svelte +90 -0
  45. package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
  46. package/dist/admin/components/layout/lang.d.ts +4 -0
  47. package/dist/admin/components/layout/lang.js +8 -0
  48. package/dist/admin/components/layout/layout-renderer.svelte +2 -1
  49. package/dist/admin/components/layout/layout-tabs.svelte +14 -0
  50. package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
  51. package/dist/admin/components/layout/nav-collections.svelte +16 -9
  52. package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
  53. package/dist/admin/components/layout/nav-global.svelte +45 -0
  54. package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
  55. package/dist/admin/components/layout/nav-main.svelte +8 -32
  56. package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
  57. package/dist/admin/components/layout/nav-module.svelte +57 -0
  58. package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
  59. package/dist/admin/components/layout/nav-section.svelte +1 -1
  60. package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
  61. package/dist/admin/components/layout/nav-singletons.svelte +14 -7
  62. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
  63. package/dist/admin/remote/booking.remote.d.ts +244 -0
  64. package/dist/admin/remote/booking.remote.js +283 -0
  65. package/dist/admin/remote/entry.remote.d.ts +3 -0
  66. package/dist/admin/remote/entry.remote.js +14 -3
  67. package/dist/admin/remote/index.d.ts +2 -0
  68. package/dist/admin/remote/index.js +2 -0
  69. package/dist/admin/remote/modules.remote.d.ts +10 -0
  70. package/dist/admin/remote/modules.remote.js +14 -0
  71. package/dist/admin/state/active-context-resolve.d.ts +37 -0
  72. package/dist/admin/state/active-context-resolve.js +66 -0
  73. package/dist/admin/state/active-context.svelte.d.ts +8 -0
  74. package/dist/admin/state/active-context.svelte.js +27 -0
  75. package/dist/booking/adjustments.d.ts +33 -0
  76. package/dist/booking/adjustments.js +26 -0
  77. package/dist/booking/capacity.d.ts +12 -0
  78. package/dist/booking/capacity.js +10 -0
  79. package/dist/booking/client/index.d.ts +39 -0
  80. package/dist/booking/client/index.js +44 -0
  81. package/dist/booking/client/use-booking.svelte.d.ts +25 -0
  82. package/dist/booking/client/use-booking.svelte.js +56 -0
  83. package/dist/booking/config.d.ts +156 -0
  84. package/dist/booking/config.js +36 -0
  85. package/dist/booking/draft.d.ts +77 -0
  86. package/dist/booking/draft.js +67 -0
  87. package/dist/booking/expiry-logic.d.ts +30 -0
  88. package/dist/booking/expiry-logic.js +33 -0
  89. package/dist/booking/index.d.ts +18 -0
  90. package/dist/booking/index.js +52 -0
  91. package/dist/booking/ledger.d.ts +46 -0
  92. package/dist/booking/ledger.js +60 -0
  93. package/dist/booking/number.d.ts +2 -0
  94. package/dist/booking/number.js +11 -0
  95. package/dist/booking/participant-fields/access.d.ts +10 -0
  96. package/dist/booking/participant-fields/access.js +14 -0
  97. package/dist/booking/participant-fields/age.d.ts +12 -0
  98. package/dist/booking/participant-fields/age.js +28 -0
  99. package/dist/booking/participant-fields/completeness.d.ts +8 -0
  100. package/dist/booking/participant-fields/completeness.js +13 -0
  101. package/dist/booking/participant-fields/group.d.ts +6 -0
  102. package/dist/booking/participant-fields/group.js +15 -0
  103. package/dist/booking/participant-fields/index.d.ts +6 -0
  104. package/dist/booking/participant-fields/index.js +6 -0
  105. package/dist/booking/participant-fields/types.d.ts +50 -0
  106. package/dist/booking/participant-fields/types.js +5 -0
  107. package/dist/booking/participant-fields/validate.d.ts +7 -0
  108. package/dist/booking/participant-fields/validate.js +44 -0
  109. package/dist/booking/participant-pricing.d.ts +93 -0
  110. package/dist/booking/participant-pricing.js +91 -0
  111. package/dist/booking/participants-view.d.ts +32 -0
  112. package/dist/booking/participants-view.js +22 -0
  113. package/dist/booking/payment-context.d.ts +19 -0
  114. package/dist/booking/payment-context.js +29 -0
  115. package/dist/booking/pricing-inputs.d.ts +28 -0
  116. package/dist/booking/pricing-inputs.js +35 -0
  117. package/dist/booking/pricing.d.ts +86 -0
  118. package/dist/booking/pricing.js +125 -0
  119. package/dist/booking/recompute.d.ts +22 -0
  120. package/dist/booking/recompute.js +31 -0
  121. package/dist/booking/reminders-schedule.d.ts +11 -0
  122. package/dist/booking/reminders-schedule.js +28 -0
  123. package/dist/booking/server/adjustments.d.ts +27 -0
  124. package/dist/booking/server/adjustments.js +136 -0
  125. package/dist/booking/server/bookings.d.ts +45 -0
  126. package/dist/booking/server/bookings.js +288 -0
  127. package/dist/booking/server/catalog.d.ts +9 -0
  128. package/dist/booking/server/catalog.js +20 -0
  129. package/dist/booking/server/db.d.ts +7 -0
  130. package/dist/booking/server/db.js +18 -0
  131. package/dist/booking/server/email.d.ts +23 -0
  132. package/dist/booking/server/email.js +59 -0
  133. package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
  134. package/dist/booking/server/emailTemplateRegistry.js +125 -0
  135. package/dist/booking/server/http/create-handler.d.ts +5 -0
  136. package/dist/booking/server/http/create-handler.js +68 -0
  137. package/dist/booking/server/http/index.d.ts +4 -0
  138. package/dist/booking/server/http/index.js +4 -0
  139. package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
  140. package/dist/booking/server/http/maintenance-handler.js +36 -0
  141. package/dist/booking/server/http/portal-handler.d.ts +13 -0
  142. package/dist/booking/server/http/portal-handler.js +99 -0
  143. package/dist/booking/server/http/webhook-handler.d.ts +9 -0
  144. package/dist/booking/server/http/webhook-handler.js +39 -0
  145. package/dist/booking/server/index.d.ts +12 -0
  146. package/dist/booking/server/index.js +14 -0
  147. package/dist/booking/server/maintenance.d.ts +28 -0
  148. package/dist/booking/server/maintenance.js +134 -0
  149. package/dist/booking/server/participants.d.ts +26 -0
  150. package/dist/booking/server/participants.js +92 -0
  151. package/dist/booking/server/payments.d.ts +34 -0
  152. package/dist/booking/server/payments.js +207 -0
  153. package/dist/booking/server/portal-session.d.ts +16 -0
  154. package/dist/booking/server/portal-session.js +42 -0
  155. package/dist/booking/server/portal.d.ts +207 -0
  156. package/dist/booking/server/portal.js +234 -0
  157. package/dist/booking/server/refunds.d.ts +7 -0
  158. package/dist/booking/server/refunds.js +58 -0
  159. package/dist/booking/server/reminders.d.ts +9 -0
  160. package/dist/booking/server/reminders.js +24 -0
  161. package/dist/booking/server/validation.d.ts +17 -0
  162. package/dist/booking/server/validation.js +55 -0
  163. package/dist/booking/state-machine.d.ts +41 -0
  164. package/dist/booking/state-machine.js +73 -0
  165. package/dist/booking/templates/_partials/footer.en.html +4 -0
  166. package/dist/booking/templates/_partials/footer.pl.html +4 -0
  167. package/dist/booking/templates/_partials/header.en.html +4 -0
  168. package/dist/booking/templates/_partials/header.pl.html +4 -0
  169. package/dist/booking/templates/balance-due.en.html +10 -0
  170. package/dist/booking/templates/balance-due.pl.html +10 -0
  171. package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
  172. package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
  173. package/dist/booking/templates/booking-received.en.html +9 -0
  174. package/dist/booking/templates/booking-received.pl.html +9 -0
  175. package/dist/booking/templates/deposit-paid.en.html +6 -0
  176. package/dist/booking/templates/deposit-paid.pl.html +6 -0
  177. package/dist/booking/templates/payment-link.en.html +5 -0
  178. package/dist/booking/templates/payment-link.pl.html +5 -0
  179. package/dist/booking/templates/payment-received.en.html +10 -0
  180. package/dist/booking/templates/payment-received.pl.html +10 -0
  181. package/dist/booking/totals.d.ts +38 -0
  182. package/dist/booking/totals.js +61 -0
  183. package/dist/booking/webhook-logic.d.ts +28 -0
  184. package/dist/booking/webhook-logic.js +50 -0
  185. package/dist/cli/create-user.d.ts +7 -1
  186. package/dist/cli/create-user.js +23 -15
  187. package/dist/cli/index.js +55 -7
  188. package/dist/cli/scaffold/admin.d.ts +6 -0
  189. package/dist/cli/scaffold/admin.js +5 -3
  190. package/dist/cli/scaffold/booking.d.ts +12 -0
  191. package/dist/cli/scaffold/booking.js +103 -0
  192. package/dist/core/cms.d.ts +24 -9
  193. package/dist/core/cms.js +72 -34
  194. package/dist/core/config/validatePreviews.d.ts +12 -0
  195. package/dist/core/config/validatePreviews.js +90 -0
  196. package/dist/core/fields/dotPath.d.ts +4 -0
  197. package/dist/core/fields/dotPath.js +18 -0
  198. package/dist/core/fields/slugPath.d.ts +9 -1
  199. package/dist/core/fields/slugPath.js +16 -2
  200. package/dist/core/fields/slugify.d.ts +3 -0
  201. package/dist/core/fields/slugify.js +4 -0
  202. package/dist/core/server/entries/operations/duplicate.d.ts +10 -0
  203. package/dist/core/server/entries/operations/duplicate.js +73 -0
  204. package/dist/core/server/entries/operations/slugNormalize.d.ts +8 -0
  205. package/dist/core/server/entries/operations/slugNormalize.js +20 -0
  206. package/dist/core/server/entries/operations/slugUniqueness.d.ts +17 -0
  207. package/dist/core/server/entries/operations/slugUniqueness.js +39 -0
  208. package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
  209. package/dist/core/server/fields/resolveRelationFields.js +12 -3
  210. package/dist/core/server/generator/generator.js +7 -2
  211. package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
  212. package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
  213. package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
  214. package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
  215. package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
  216. package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
  217. package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
  218. package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
  219. package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
  220. package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
  221. package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
  222. package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
  223. package/dist/db-postgres/schema/booking/bookings.d.ts +307 -0
  224. package/dist/db-postgres/schema/booking/bookings.js +40 -0
  225. package/dist/db-postgres/schema/booking/index.d.ts +7 -0
  226. package/dist/db-postgres/schema/booking/index.js +7 -0
  227. package/dist/db-postgres/schema/index.d.ts +1 -0
  228. package/dist/db-postgres/schema/index.js +1 -0
  229. package/dist/db-postgres/schema-booking.d.ts +1 -0
  230. package/dist/db-postgres/schema-booking.js +1 -0
  231. package/dist/server/security/rate-limit-handler.d.ts +19 -0
  232. package/dist/server/security/rate-limit-handler.js +34 -0
  233. package/dist/shop/adapters/payu/client.js +3 -1
  234. package/dist/shop/adapters/payu/index.js +37 -17
  235. package/dist/shop/adapters/payu/payload.d.ts +6 -0
  236. package/dist/shop/adapters/payu/payload.js +1 -1
  237. package/dist/shop/adapters/stripe/index.js +36 -21
  238. package/dist/shop/index.d.ts +11 -2
  239. package/dist/shop/index.js +43 -1
  240. package/dist/shop/rate-limit.d.ts +1 -9
  241. package/dist/shop/rate-limit.js +3 -28
  242. package/dist/shop/types.d.ts +19 -0
  243. package/dist/sveltekit/config.d.ts +4 -0
  244. package/dist/sveltekit/config.js +6 -0
  245. package/dist/types/cms.d.ts +15 -1
  246. package/dist/types/cms.schema.d.ts +96 -2
  247. package/dist/types/cms.schema.js +84 -1
  248. package/dist/types/config.d.ts +33 -0
  249. package/dist/types/layout.d.ts +6 -0
  250. package/dist/types/modules.d.ts +133 -0
  251. package/dist/types/modules.js +27 -0
  252. package/dist/updates/0.36.9/index.d.ts +2 -0
  253. package/dist/updates/0.36.9/index.js +12 -0
  254. package/dist/updates/0.37.0/index.d.ts +2 -0
  255. package/dist/updates/0.37.0/index.js +52 -0
  256. package/dist/updates/index.js +5 -1
  257. package/package.json +19 -1
  258. package/dist/admin/components/layout/nav-shop.svelte +0 -33
  259. package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
@@ -0,0 +1,221 @@
1
+ <script lang="ts">
2
+ import { getRemotes } from '../../../sveltekit/index.js';
3
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
4
+ import { formatCentsPrice, formatDateTime } from '../../utils/formatters.js';
5
+ import PageHeader from '../../components/layout/page-header.svelte';
6
+ import DataTable from '../collection/data-table.svelte';
7
+ import TableToolbar from '../collection/table-toolbar.svelte';
8
+ import TablePagination from '../collection/table-pagination.svelte';
9
+ import StateDisplay from '../collection/state-display.svelte';
10
+ import StatusBadge from '../collection/status-badge.svelte';
11
+ import EntryLink from '../collection/entry-link.svelte';
12
+ import { renderComponent } from '../../../components/ui/data-table/render-helpers.js';
13
+ import { customerSummary } from './customer-view.js';
14
+ import type { ColumnDef, PaginationState } from '@tanstack/table-core';
15
+ import type { BookingStatus } from '../../../booking/state-machine.js';
16
+
17
+ type BookingRow = {
18
+ id: string;
19
+ number: string;
20
+ status: BookingStatus;
21
+ currency: string;
22
+ customer: Record<string, unknown>;
23
+ totals: { gross: number; balance: number };
24
+ createdAt: Date | string;
25
+ };
26
+
27
+ const remotes = getRemotes();
28
+ const interfaceLanguage = useInterfaceLanguage();
29
+
30
+ let statusFilter = $state<string | null>(null);
31
+ let searchQuery = $state('');
32
+ let appliedSearch = $state('');
33
+ let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 50 });
34
+ let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
35
+
36
+ const bookingsQuery = $derived(
37
+ remotes.listBookings({
38
+ status: (statusFilter as BookingStatus | null) ?? undefined,
39
+ search: appliedSearch || undefined,
40
+ limit: pagination.pageSize,
41
+ offset: pagination.pageIndex * pagination.pageSize
42
+ })
43
+ );
44
+
45
+ const items = $derived<BookingRow[]>((bookingsQuery.current?.items as BookingRow[]) ?? []);
46
+ const total = $derived(bookingsQuery.current?.total ?? 0);
47
+ const isLoading = $derived(!bookingsQuery.ready);
48
+ const isError = $derived(Boolean(bookingsQuery.error));
49
+ const pageCount = $derived(Math.max(1, Math.ceil(total / pagination.pageSize)));
50
+
51
+ const t = $derived(
52
+ interfaceLanguage.current === 'pl'
53
+ ? {
54
+ title: 'Rezerwacje',
55
+ search: 'Szukaj',
56
+ searchPlaceholder: 'Szukaj po numerze, imieniu lub e-mailu…',
57
+ filterStatus: 'Status',
58
+ number: 'Numer',
59
+ created: 'Utworzono',
60
+ customer: 'Klient',
61
+ total: 'Wartość',
62
+ balance: 'Saldo',
63
+ status: 'Status',
64
+ emptyTitle: 'Brak rezerwacji',
65
+ emptyDescription: 'Gdy ktoś zarezerwuje, pojawi się tutaj.',
66
+ noResults: 'Brak wyników.',
67
+ statusDraft: 'Czeka na zaliczkę',
68
+ statusReserved: 'Zarezerwowane',
69
+ statusPartiallyPaid: 'Częściowo opłacone',
70
+ statusPaid: 'Opłacone',
71
+ statusCompleted: 'Zakończone',
72
+ statusCancelled: 'Anulowane'
73
+ }
74
+ : {
75
+ title: 'Reservations',
76
+ search: 'Search',
77
+ searchPlaceholder: 'Search by number, name or email…',
78
+ filterStatus: 'Status',
79
+ number: 'Number',
80
+ created: 'Created',
81
+ customer: 'Customer',
82
+ total: 'Total',
83
+ balance: 'Balance',
84
+ status: 'Status',
85
+ emptyTitle: 'No reservations yet',
86
+ emptyDescription: 'Reservations will appear here as soon as someone books.',
87
+ noResults: 'No results.',
88
+ statusDraft: 'Awaiting deposit',
89
+ statusReserved: 'Reserved',
90
+ statusPartiallyPaid: 'Partially paid',
91
+ statusPaid: 'Paid',
92
+ statusCompleted: 'Completed',
93
+ statusCancelled: 'Cancelled'
94
+ }
95
+ );
96
+
97
+ const dataFilters = $derived([
98
+ {
99
+ slug: 'status',
100
+ label: t.filterStatus,
101
+ options: [
102
+ { value: 'draft', label: t.statusDraft },
103
+ { value: 'reserved', label: t.statusReserved },
104
+ { value: 'partially_paid', label: t.statusPartiallyPaid },
105
+ { value: 'paid', label: t.statusPaid },
106
+ { value: 'completed', label: t.statusCompleted },
107
+ { value: 'cancelled', label: t.statusCancelled }
108
+ ]
109
+ }
110
+ ]);
111
+ const activeDataFilters = $derived({ status: statusFilter });
112
+
113
+ function scheduleSearchUpdate(value: string) {
114
+ if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
115
+ searchDebounceTimer = setTimeout(() => {
116
+ appliedSearch = value.trim();
117
+ pagination = { ...pagination, pageIndex: 0 };
118
+ }, 350);
119
+ }
120
+
121
+ const columns = $derived.by<ColumnDef<BookingRow>[]>(() => [
122
+ {
123
+ accessorKey: 'number',
124
+ header: t.number,
125
+ cell: (info) =>
126
+ renderComponent(EntryLink, {
127
+ name: info.row.original.number,
128
+ url: `/admin/booking/reservations/${info.row.original.id}`
129
+ })
130
+ },
131
+ {
132
+ accessorKey: 'createdAt',
133
+ header: t.created,
134
+ cell: (info) => formatDateTime(info.row.original.createdAt, interfaceLanguage.current)
135
+ },
136
+ {
137
+ id: 'customer',
138
+ header: t.customer,
139
+ cell: (info) => {
140
+ const row = info.row.original;
141
+ const s = customerSummary(row.customer ?? {});
142
+ return renderComponent(EntryLink, {
143
+ name: s.name ?? s.email ?? '—',
144
+ url: `/admin/booking/reservations/${row.id}`,
145
+ slug: s.name && s.email ? s.email : undefined
146
+ });
147
+ }
148
+ },
149
+ {
150
+ id: 'gross',
151
+ header: t.total,
152
+ cell: (info) => formatCentsPrice(info.row.original.totals.gross, info.row.original.currency)
153
+ },
154
+ {
155
+ id: 'balance',
156
+ header: t.balance,
157
+ cell: (info) => formatCentsPrice(info.row.original.totals.balance, info.row.original.currency)
158
+ },
159
+ {
160
+ accessorKey: 'status',
161
+ header: t.status,
162
+ cell: (info) =>
163
+ renderComponent(StatusBadge, { variant: 'booking', status: info.row.original.status })
164
+ }
165
+ ]);
166
+ </script>
167
+
168
+ <div class="p-5 pb-24 md:p-7">
169
+ <PageHeader title={t.title} count={!isLoading && !isError ? total : undefined} />
170
+
171
+ {#if !isLoading && !isError && total === 0 && !statusFilter && !appliedSearch}
172
+ <StateDisplay kind="empty" title={t.emptyTitle} description={t.emptyDescription} />
173
+ {:else}
174
+ <TableToolbar
175
+ {searchQuery}
176
+ searchPlaceholder={t.searchPlaceholder}
177
+ searchLabel={t.search}
178
+ onSearchChange={(q) => {
179
+ searchQuery = q;
180
+ scheduleSearchUpdate(q);
181
+ }}
182
+ hideStatusFilter
183
+ hideViewToggle
184
+ {dataFilters}
185
+ {activeDataFilters}
186
+ onDataFilterChange={(_slug, value) => {
187
+ statusFilter = value;
188
+ pagination = { ...pagination, pageIndex: 0 };
189
+ }}
190
+ />
191
+
192
+ <div class="bg-card overflow-hidden rounded-xl border shadow-sm">
193
+ {#if isError}
194
+ <StateDisplay kind="error" />
195
+ {:else if isLoading && items.length === 0}
196
+ <StateDisplay kind="loading" />
197
+ {:else}
198
+ <DataTable
199
+ data={items}
200
+ {columns}
201
+ enablePagination
202
+ manualPagination
203
+ {pageCount}
204
+ rowCount={total}
205
+ {pagination}
206
+ onPaginationChange={(p) => (pagination = p)}
207
+ emptyTitle={t.noResults}
208
+ />
209
+
210
+ <TablePagination
211
+ pageIndex={pagination.pageIndex}
212
+ pageSize={pagination.pageSize}
213
+ {pageCount}
214
+ totalItems={total}
215
+ onPageChange={(p) => (pagination = { ...pagination, pageIndex: p })}
216
+ onPageSizeChange={(s) => (pagination = { pageIndex: 0, pageSize: s })}
217
+ />
218
+ {/if}
219
+ </div>
220
+ {/if}
221
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const ReservationsPage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ReservationsPage = ReturnType<typeof ReservationsPage>;
3
+ export default ReservationsPage;
@@ -4,6 +4,7 @@
4
4
  import { getEntryThumbnail, type MediaThumbnailLookup } from '../../utils/entryThumbnail.js';
5
5
  import { arrayMove } from '../../utils/arrayMove.js';
6
6
  import { getRemotes } from '../../../sveltekit/index.js';
7
+ import { goto } from '$app/navigation';
7
8
  import type { CollectionConfigWithType } from '../../../types/collections.js';
8
9
  import DataTable from './data-table.svelte';
9
10
  import type { ColumnDef, RowSelectionState, Table } from '@tanstack/table-core';
@@ -54,6 +55,8 @@
54
55
  entryRestored: string;
55
56
  entriesRestored: string;
56
57
  entryDeleted: string;
58
+ entryDuplicated: string;
59
+ entryDuplicateFailed: string;
57
60
  deleteConfirmTitle: string;
58
61
  deleteConfirmDescription: string;
59
62
  cancel: string;
@@ -74,6 +77,8 @@
74
77
  entryRestored: 'Entry restored',
75
78
  entriesRestored: 'Entries restored',
76
79
  entryDeleted: 'Entry permanently deleted',
80
+ entryDuplicated: 'Duplicate created (draft)',
81
+ entryDuplicateFailed: 'Failed to duplicate entry',
77
82
  deleteConfirmTitle: 'Delete entry permanently?',
78
83
  deleteConfirmDescription:
79
84
  'This action cannot be undone. The entry will be permanently deleted.',
@@ -94,6 +99,8 @@
94
99
  entryRestored: 'Wpis przywrócony',
95
100
  entriesRestored: 'Wpisy przywrócone',
96
101
  entryDeleted: 'Wpis trwale usunięty',
102
+ entryDuplicated: 'Utworzono duplikat (szkic)',
103
+ entryDuplicateFailed: 'Nie udało się zduplikować wpisu',
97
104
  deleteConfirmTitle: 'Usunąć wpis na stałe?',
98
105
  deleteConfirmDescription: 'Ta akcja jest nieodwracalna. Wpis zostanie trwale usunięty.',
99
106
  cancel: 'Anuluj',
@@ -310,7 +317,7 @@
310
317
  return renderComponent(EntryLink, {
311
318
  name: info.row.original.name,
312
319
  url: info.row.original.url,
313
- slug: info.row.original.slug
320
+ path: info.row.original.slug
314
321
  });
315
322
  }
316
323
  },
@@ -374,7 +381,9 @@
374
381
  entryUrl: info.row.original.url,
375
382
  entryName: info.row.original.name,
376
383
  onArchive: () => handleArchiveSingle(info.row.original.id),
377
- ...(isArchivedFilter ? { onDelete: () => handleDelete(info.row.original.id), onRestore: () => handleRestoreSingle(info.row.original.id) } : {})
384
+ ...(isArchivedFilter
385
+ ? { onDelete: () => handleDelete(info.row.original.id), onRestore: () => handleRestoreSingle(info.row.original.id) }
386
+ : { onDuplicate: () => handleDuplicate(info.row.original.id) })
378
387
  }),
379
388
  enableSorting: false,
380
389
  size: 50
@@ -393,6 +402,16 @@
393
402
  override.release();
394
403
  }
395
404
 
405
+ async function handleDuplicate(id: string) {
406
+ try {
407
+ const { id: newId } = await remotes.duplicateEntryCommand(id);
408
+ toast.success(lang[interfaceLanguage.current].entryDuplicated);
409
+ await goto(`/admin/entries/${newId}`);
410
+ } catch {
411
+ toast.error(lang[interfaceLanguage.current].entryDuplicateFailed);
412
+ }
413
+ }
414
+
396
415
  async function handleRestoreSingle(id: string) {
397
416
  const override = entriesQuery.withOverride((old) => ({
398
417
  ...old,
@@ -1,18 +1,20 @@
1
1
  <script lang="ts">
2
2
  type Props = {
3
3
  name: string;
4
+ /** Admin edit link for this entry. */
4
5
  url: string;
5
- slug?: string;
6
+ /** Public path of the entry — already absolute (`resolveEntryUrl`), never prefix it. */
7
+ path?: string;
6
8
  };
7
9
 
8
- let { name, url, slug }: Props = $props();
10
+ let { name, url, path }: Props = $props();
9
11
  </script>
10
12
 
11
13
  <div>
12
14
  <a href={url} class="font-medium text-foreground hover:text-primary hover:underline">
13
15
  {name}
14
16
  </a>
15
- {#if slug}
16
- <div class="text-[11px] text-muted-foreground font-mono mt-0.5 truncate max-w-[200px]">/{slug}</div>
17
+ {#if path}
18
+ <div class="text-[11px] text-muted-foreground font-mono mt-0.5 truncate max-w-[200px]">{path}</div>
17
19
  {/if}
18
20
  </div>
@@ -1,7 +1,9 @@
1
1
  type Props = {
2
2
  name: string;
3
+ /** Admin edit link for this entry. */
3
4
  url: string;
4
- slug?: string;
5
+ /** Public path of the entry — already absolute (`resolveEntryUrl`), never prefix it. */
6
+ path?: string;
5
7
  };
6
8
  declare const EntryLink: import("svelte").Component<Props, {}, "">;
7
9
  type EntryLink = ReturnType<typeof EntryLink>;
@@ -4,6 +4,7 @@
4
4
  import Archive from '@tabler/icons-svelte/icons/archive';
5
5
  import ArchiveOff from '@tabler/icons-svelte/icons/archive-off';
6
6
  import Trash from '@tabler/icons-svelte/icons/trash';
7
+ import Copy from '@tabler/icons-svelte/icons/copy';
7
8
  import Button from '../../../components/ui/button/button.svelte';
8
9
  import * as DropdownMenu from '../../../components/ui/dropdown-menu/index.js';
9
10
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
@@ -14,17 +15,18 @@
14
15
  entryUrl: string;
15
16
  entryName: string;
16
17
  onArchive: () => void;
18
+ onDuplicate?: () => void;
17
19
  onDelete?: () => void;
18
20
  onRestore?: () => void;
19
21
  };
20
22
 
21
- let { entryUrl, entryName, onArchive, onDelete, onRestore }: Props = $props();
23
+ let { entryUrl, entryName, onArchive, onDuplicate, onDelete, onRestore }: Props = $props();
22
24
 
23
25
  const interfaceLanguage = useInterfaceLanguage();
24
26
 
25
- const lang: Record<InterfaceLanguage, { open: string; archive: string; restore: string; delete: string; moreActions: string }> = {
26
- en: { open: 'Open', archive: 'Archive', restore: 'Restore', delete: 'Delete', moreActions: 'More actions for' },
27
- pl: { open: 'Otwórz', archive: 'Archiwizuj', restore: 'Przywróć', delete: 'Usuń', moreActions: 'Więcej akcji dla' }
27
+ const lang: Record<InterfaceLanguage, { open: string; duplicate: string; archive: string; restore: string; delete: string; moreActions: string }> = {
28
+ en: { open: 'Open', duplicate: 'Duplicate', archive: 'Archive', restore: 'Restore', delete: 'Delete', moreActions: 'More actions for' },
29
+ pl: { open: 'Otwórz', duplicate: 'Duplikuj', archive: 'Archiwizuj', restore: 'Przywróć', delete: 'Usuń', moreActions: 'Więcej akcji dla' }
28
30
  };
29
31
 
30
32
  const t = $derived(lang[interfaceLanguage.current]);
@@ -51,6 +53,12 @@
51
53
  {t.open}
52
54
  </DropdownMenu.Item>
53
55
  <DropdownMenu.Separator />
56
+ {#if onDuplicate}
57
+ <DropdownMenu.Item onclick={onDuplicate} data-testid="row-action-duplicate">
58
+ <Copy class="mr-2 size-4" />
59
+ {t.duplicate}
60
+ </DropdownMenu.Item>
61
+ {/if}
54
62
  {#if onRestore}
55
63
  <DropdownMenu.Item onclick={onRestore} data-testid="row-action-restore">
56
64
  <ArchiveOff class="mr-2 size-4" />
@@ -2,6 +2,7 @@ type Props = {
2
2
  entryUrl: string;
3
3
  entryName: string;
4
4
  onArchive: () => void;
5
+ onDuplicate?: () => void;
5
6
  onDelete?: () => void;
6
7
  onRestore?: () => void;
7
8
  };
@@ -1,21 +1,23 @@
1
1
  <script lang="ts">
2
2
  import type { EntryStatus } from '../../../types/entries.js';
3
3
  import type { OrderStatus } from '../../../shop/types.js';
4
+ import type { BookingStatus } from '../../../booking/state-machine.js';
4
5
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
5
6
  import type { InterfaceLanguage } from '../../../types/languages.js';
6
7
 
7
8
  /**
8
- * StatusBadge accepts three usage modes (variants):
9
+ * StatusBadge accepts four usage modes (variants):
9
10
  * - entry (default): pass `status` as `EntryStatus`
10
11
  * - order: pass `variant="order"` and `status` as `OrderStatus`
12
+ * - booking: pass `variant="booking"` and `status` as `BookingStatus`
11
13
  * - boolean: pass `variant="boolean"` and `active: boolean`
12
14
  *
13
15
  * Type stays permissive (FlatProps) so consumers can pass any combination;
14
16
  * runtime narrows by `variant` and falls back to muted style on unknown.
15
17
  */
16
18
  type Props = {
17
- variant?: 'entry' | 'order' | 'boolean';
18
- status?: EntryStatus | OrderStatus;
19
+ variant?: 'entry' | 'order' | 'boolean' | 'booking';
20
+ status?: EntryStatus | OrderStatus | BookingStatus;
19
21
  active?: boolean;
20
22
  activeLabel?: string;
21
23
  inactiveLabel?: string;
@@ -92,6 +94,35 @@
92
94
  }
93
95
  };
94
96
 
97
+ const bookingStyles: Record<BookingStatus, StatusStyle> = {
98
+ draft: { dot: '◐', classes: 'bg-warning-bg text-warning' },
99
+ reserved: { dot: '●', classes: 'bg-info-bg text-info' },
100
+ partially_paid: { dot: '◐', classes: 'bg-warning-bg text-warning' },
101
+ paid: { dot: '●', classes: 'bg-success-bg text-success' },
102
+ completed: { dot: '●', classes: 'bg-success-bg text-success' },
103
+ cancelled: { dot: '○', classes: 'bg-muted text-muted-foreground' }
104
+ };
105
+
106
+ const bookingLabels: Record<InterfaceLanguage, Record<BookingStatus, string>> = {
107
+ en: {
108
+ draft: 'Awaiting deposit',
109
+ reserved: 'Reserved',
110
+ partially_paid: 'Partially paid',
111
+ paid: 'Paid',
112
+ completed: 'Completed',
113
+ cancelled: 'Cancelled'
114
+ },
115
+ pl: {
116
+ // „Szkic" nic nie mówi osobie prowadzącej rezerwacje — ona myśli „czeka na zaliczkę".
117
+ draft: 'Czeka na zaliczkę',
118
+ reserved: 'Zarezerwowane',
119
+ partially_paid: 'Częściowo opłacone',
120
+ paid: 'Opłacone',
121
+ completed: 'Zakończone',
122
+ cancelled: 'Anulowane'
123
+ }
124
+ };
125
+
95
126
  const booleanDefaults: Record<InterfaceLanguage, { active: string; inactive: string }> = {
96
127
  en: { active: 'Active', inactive: 'Inactive' },
97
128
  pl: { active: 'Aktywny', inactive: 'Nieaktywny' }
@@ -106,6 +137,11 @@
106
137
  if (!s || !(s in orderStyles)) return { style: fallbackStyle, label: String(s ?? '') };
107
138
  return { style: orderStyles[s], label: orderLabels[lang][s] };
108
139
  }
140
+ if (variant === 'booking') {
141
+ const s = status as BookingStatus | undefined;
142
+ if (!s || !(s in bookingStyles)) return { style: fallbackStyle, label: String(s ?? '') };
143
+ return { style: bookingStyles[s], label: bookingLabels[lang][s] };
144
+ }
109
145
  if (variant === 'boolean') {
110
146
  const isActive = active === true;
111
147
  const fallback = booleanDefaults[lang];
@@ -1,17 +1,19 @@
1
1
  import type { EntryStatus } from '../../../types/entries.js';
2
2
  import type { OrderStatus } from '../../../shop/types.js';
3
+ import type { BookingStatus } from '../../../booking/state-machine.js';
3
4
  /**
4
- * StatusBadge accepts three usage modes (variants):
5
+ * StatusBadge accepts four usage modes (variants):
5
6
  * - entry (default): pass `status` as `EntryStatus`
6
7
  * - order: pass `variant="order"` and `status` as `OrderStatus`
8
+ * - booking: pass `variant="booking"` and `status` as `BookingStatus`
7
9
  * - boolean: pass `variant="boolean"` and `active: boolean`
8
10
  *
9
11
  * Type stays permissive (FlatProps) so consumers can pass any combination;
10
12
  * runtime narrows by `variant` and falls back to muted style on unknown.
11
13
  */
12
14
  type Props = {
13
- variant?: 'entry' | 'order' | 'boolean';
14
- status?: EntryStatus | OrderStatus;
15
+ variant?: 'entry' | 'order' | 'boolean' | 'booking';
16
+ status?: EntryStatus | OrderStatus | BookingStatus;
15
17
  active?: boolean;
16
18
  activeLabel?: string;
17
19
  inactiveLabel?: string;
@@ -33,6 +33,8 @@
33
33
  import { getCollectionEntryDisplayLabel } from '../../utils/entryLabel.js';
34
34
  import { getFieldsFromConfig, hasLayout } from '../../../core/fields/layoutUtils.js';
35
35
  import { createHybridContext } from './hybrid/hybrid-context.svelte.js';
36
+ import { createPreviewViewsContext } from './preview-views-context.svelte.js';
37
+ import { normalizePreviews, buildPreviewUrl } from './preview-views.js';
36
38
  import { onMount, setContext, tick } from 'svelte';
37
39
  import { get } from 'svelte/store';
38
40
  const contentLanguage = getContentLanguage();
@@ -111,6 +113,11 @@
111
113
  let { collection } = entry;
112
114
  const isArchived = $derived(!!entry.archivedAt);
113
115
 
116
+ // Preview views: `previewUrl` is normalized to a single view, so everything
117
+ // below only ever deals with an array. Empty array = collection has no preview.
118
+ const previewViews = createPreviewViewsContext(normalizePreviews(collection));
119
+ const hasPreview = previewViews.views.length > 0;
120
+
114
121
  setContext('cms-path-template', collection.pathTemplate || null);
115
122
  setContext('cms-entry-published', entry.publishedVersions[contentLanguage.current] != null);
116
123
  setContext('cms-entry-id', entry.id);
@@ -431,9 +438,17 @@
431
438
  }
432
439
  }
433
440
 
434
- const previewOrigin = collection.previewUrl
435
- ? getOriginFromUrl(collection.previewUrl)
436
- : window.location.origin;
441
+ // Src of the active view — switching a view swaps the iframe src, which reloads
442
+ // the preview page; it then asks for the current (unsaved) form data via
443
+ // `preview-ready`, so nothing in the editor is lost.
444
+ const previewSrc = $derived(
445
+ previewViews.activeView
446
+ ? buildPreviewUrl(previewViews.activeView, String(editingEntry.id))
447
+ : ''
448
+ );
449
+ const previewOrigin = $derived(
450
+ previewSrc ? getOriginFromUrl(previewSrc) : window.location.origin
451
+ );
437
452
 
438
453
  const updatePreview = useDebounce(
439
454
  async (window: Window, form: SuperForm<Record<string, unknown>>) => {
@@ -466,16 +481,16 @@
466
481
 
467
482
  // Subscribe to form changes for preview updates
468
483
  onMount(() => {
469
- if (!collection.previewUrl) return;
484
+ if (!hasPreview) return;
470
485
  const unsub = form.form.subscribe(() => {
471
486
  onUpdate();
472
487
  });
473
488
  return () => unsub();
474
489
  });
475
490
 
476
- // Enable hybrid mode when collection has previewUrl
491
+ // Enable hybrid mode when the collection has at least one preview view
477
492
  $effect(() => {
478
- hybridContext.enabled = !!collection.previewUrl;
493
+ hybridContext.enabled = hasPreview;
479
494
  });
480
495
 
481
496
  // Send current form data + hybrid mode state to iframe
@@ -605,7 +620,7 @@
605
620
  });
606
621
 
607
622
  const t = $derived(lang[interfaceLanguage.current]);
608
- const isHybrid = $derived(hybridContext.mode === 'hybrid' && !!collection.previewUrl);
623
+ const isHybrid = $derived(hybridContext.mode === 'hybrid' && hasPreview);
609
624
 
610
625
  const errorPaths = $derived(new Set(validationErrors.map((e) => e.path)));
611
626
 
@@ -752,8 +767,10 @@
752
767
  <div class="bg-accent h-full animate-pulse rounded-md"></div>
753
768
  {:then { default: HybridPreview }}
754
769
  <HybridPreview
755
- {collection}
756
- {editingEntry}
770
+ {previewSrc}
771
+ views={previewViews.views}
772
+ activeName={previewViews.activeName}
773
+ onSelectView={(name) => previewViews.select(name)}
757
774
  bind:previewIframe
758
775
  bind:sizePreset
759
776
  {size}