includio-cms 0.36.9 → 0.37.1

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 (247) hide show
  1. package/API.md +178 -3
  2. package/CHANGELOG.md +77 -0
  3. package/DOCS.md +28 -3
  4. package/ROADMAP.md +32 -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 +1 -1
  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/status-badge.svelte +39 -3
  21. package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
  22. package/dist/admin/client/entry/entry.svelte +26 -9
  23. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
  24. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
  25. package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
  26. package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
  27. package/dist/admin/client/entry/preview-views.d.ts +27 -0
  28. package/dist/admin/client/entry/preview-views.js +33 -0
  29. package/dist/admin/client/index.d.ts +3 -0
  30. package/dist/admin/client/index.js +3 -0
  31. package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
  32. package/dist/admin/client/modules/dashboard-registry.js +11 -0
  33. package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
  34. package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
  35. package/dist/admin/client/modules/stat-card.svelte +22 -0
  36. package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
  37. package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
  38. package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
  39. package/dist/admin/components/fields/relation-field.svelte +11 -2
  40. package/dist/admin/components/icons/icon-map.js +8 -0
  41. package/dist/admin/components/layout/app-sidebar.svelte +45 -14
  42. package/dist/admin/components/layout/context-switcher.svelte +90 -0
  43. package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
  44. package/dist/admin/components/layout/lang.d.ts +4 -0
  45. package/dist/admin/components/layout/lang.js +8 -0
  46. package/dist/admin/components/layout/layout-renderer.svelte +2 -1
  47. package/dist/admin/components/layout/layout-tabs.svelte +14 -0
  48. package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
  49. package/dist/admin/components/layout/nav-collections.svelte +16 -9
  50. package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
  51. package/dist/admin/components/layout/nav-global.svelte +45 -0
  52. package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
  53. package/dist/admin/components/layout/nav-main.svelte +8 -32
  54. package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
  55. package/dist/admin/components/layout/nav-module.svelte +57 -0
  56. package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
  57. package/dist/admin/components/layout/nav-section.svelte +1 -1
  58. package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
  59. package/dist/admin/components/layout/nav-singletons.svelte +14 -7
  60. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
  61. package/dist/admin/remote/booking.remote.d.ts +247 -0
  62. package/dist/admin/remote/booking.remote.js +286 -0
  63. package/dist/admin/remote/index.d.ts +2 -0
  64. package/dist/admin/remote/index.js +2 -0
  65. package/dist/admin/remote/modules.remote.d.ts +10 -0
  66. package/dist/admin/remote/modules.remote.js +14 -0
  67. package/dist/admin/state/active-context-resolve.d.ts +37 -0
  68. package/dist/admin/state/active-context-resolve.js +66 -0
  69. package/dist/admin/state/active-context.svelte.d.ts +8 -0
  70. package/dist/admin/state/active-context.svelte.js +27 -0
  71. package/dist/booking/adjustments.d.ts +33 -0
  72. package/dist/booking/adjustments.js +26 -0
  73. package/dist/booking/capacity.d.ts +12 -0
  74. package/dist/booking/capacity.js +10 -0
  75. package/dist/booking/client/index.d.ts +42 -0
  76. package/dist/booking/client/index.js +48 -0
  77. package/dist/booking/client/use-booking.svelte.d.ts +24 -0
  78. package/dist/booking/client/use-booking.svelte.js +56 -0
  79. package/dist/booking/config.d.ts +176 -0
  80. package/dist/booking/config.js +37 -0
  81. package/dist/booking/draft.d.ts +96 -0
  82. package/dist/booking/draft.js +67 -0
  83. package/dist/booking/expiry-logic.d.ts +30 -0
  84. package/dist/booking/expiry-logic.js +33 -0
  85. package/dist/booking/index.d.ts +18 -0
  86. package/dist/booking/index.js +52 -0
  87. package/dist/booking/ledger.d.ts +46 -0
  88. package/dist/booking/ledger.js +60 -0
  89. package/dist/booking/number.d.ts +2 -0
  90. package/dist/booking/number.js +11 -0
  91. package/dist/booking/participant-fields/access.d.ts +10 -0
  92. package/dist/booking/participant-fields/access.js +14 -0
  93. package/dist/booking/participant-fields/age.d.ts +12 -0
  94. package/dist/booking/participant-fields/age.js +28 -0
  95. package/dist/booking/participant-fields/completeness.d.ts +8 -0
  96. package/dist/booking/participant-fields/completeness.js +13 -0
  97. package/dist/booking/participant-fields/group.d.ts +6 -0
  98. package/dist/booking/participant-fields/group.js +15 -0
  99. package/dist/booking/participant-fields/index.d.ts +6 -0
  100. package/dist/booking/participant-fields/index.js +6 -0
  101. package/dist/booking/participant-fields/types.d.ts +50 -0
  102. package/dist/booking/participant-fields/types.js +5 -0
  103. package/dist/booking/participant-fields/validate.d.ts +7 -0
  104. package/dist/booking/participant-fields/validate.js +44 -0
  105. package/dist/booking/participant-pricing.d.ts +93 -0
  106. package/dist/booking/participant-pricing.js +91 -0
  107. package/dist/booking/participants-view.d.ts +32 -0
  108. package/dist/booking/participants-view.js +22 -0
  109. package/dist/booking/payment-context.d.ts +19 -0
  110. package/dist/booking/payment-context.js +29 -0
  111. package/dist/booking/pricing-inputs.d.ts +28 -0
  112. package/dist/booking/pricing-inputs.js +35 -0
  113. package/dist/booking/pricing.d.ts +86 -0
  114. package/dist/booking/pricing.js +125 -0
  115. package/dist/booking/recompute.d.ts +22 -0
  116. package/dist/booking/recompute.js +31 -0
  117. package/dist/booking/reminders-schedule.d.ts +11 -0
  118. package/dist/booking/reminders-schedule.js +28 -0
  119. package/dist/booking/server/adjustments.d.ts +27 -0
  120. package/dist/booking/server/adjustments.js +139 -0
  121. package/dist/booking/server/bookings.d.ts +51 -0
  122. package/dist/booking/server/bookings.js +336 -0
  123. package/dist/booking/server/catalog.d.ts +9 -0
  124. package/dist/booking/server/catalog.js +20 -0
  125. package/dist/booking/server/db.d.ts +7 -0
  126. package/dist/booking/server/db.js +18 -0
  127. package/dist/booking/server/email.d.ts +23 -0
  128. package/dist/booking/server/email.js +59 -0
  129. package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
  130. package/dist/booking/server/emailTemplateRegistry.js +125 -0
  131. package/dist/booking/server/http/create-handler.d.ts +5 -0
  132. package/dist/booking/server/http/create-handler.js +70 -0
  133. package/dist/booking/server/http/index.d.ts +4 -0
  134. package/dist/booking/server/http/index.js +4 -0
  135. package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
  136. package/dist/booking/server/http/maintenance-handler.js +36 -0
  137. package/dist/booking/server/http/portal-handler.d.ts +13 -0
  138. package/dist/booking/server/http/portal-handler.js +103 -0
  139. package/dist/booking/server/http/webhook-handler.d.ts +9 -0
  140. package/dist/booking/server/http/webhook-handler.js +39 -0
  141. package/dist/booking/server/index.d.ts +12 -0
  142. package/dist/booking/server/index.js +14 -0
  143. package/dist/booking/server/maintenance.d.ts +28 -0
  144. package/dist/booking/server/maintenance.js +134 -0
  145. package/dist/booking/server/participants.d.ts +26 -0
  146. package/dist/booking/server/participants.js +92 -0
  147. package/dist/booking/server/payments.d.ts +34 -0
  148. package/dist/booking/server/payments.js +207 -0
  149. package/dist/booking/server/portal-session.d.ts +16 -0
  150. package/dist/booking/server/portal-session.js +42 -0
  151. package/dist/booking/server/portal.d.ts +206 -0
  152. package/dist/booking/server/portal.js +236 -0
  153. package/dist/booking/server/price-source.d.ts +73 -0
  154. package/dist/booking/server/price-source.js +94 -0
  155. package/dist/booking/server/refunds.d.ts +7 -0
  156. package/dist/booking/server/refunds.js +58 -0
  157. package/dist/booking/server/reminders.d.ts +9 -0
  158. package/dist/booking/server/reminders.js +24 -0
  159. package/dist/booking/server/validation.d.ts +17 -0
  160. package/dist/booking/server/validation.js +55 -0
  161. package/dist/booking/state-machine.d.ts +41 -0
  162. package/dist/booking/state-machine.js +73 -0
  163. package/dist/booking/templates/_partials/footer.en.html +4 -0
  164. package/dist/booking/templates/_partials/footer.pl.html +4 -0
  165. package/dist/booking/templates/_partials/header.en.html +4 -0
  166. package/dist/booking/templates/_partials/header.pl.html +4 -0
  167. package/dist/booking/templates/balance-due.en.html +10 -0
  168. package/dist/booking/templates/balance-due.pl.html +10 -0
  169. package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
  170. package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
  171. package/dist/booking/templates/booking-received.en.html +9 -0
  172. package/dist/booking/templates/booking-received.pl.html +9 -0
  173. package/dist/booking/templates/deposit-paid.en.html +6 -0
  174. package/dist/booking/templates/deposit-paid.pl.html +6 -0
  175. package/dist/booking/templates/payment-link.en.html +5 -0
  176. package/dist/booking/templates/payment-link.pl.html +5 -0
  177. package/dist/booking/templates/payment-received.en.html +10 -0
  178. package/dist/booking/templates/payment-received.pl.html +10 -0
  179. package/dist/booking/totals.d.ts +38 -0
  180. package/dist/booking/totals.js +61 -0
  181. package/dist/booking/webhook-logic.d.ts +28 -0
  182. package/dist/booking/webhook-logic.js +50 -0
  183. package/dist/cli/create-user.d.ts +7 -1
  184. package/dist/cli/create-user.js +23 -15
  185. package/dist/cli/index.js +55 -7
  186. package/dist/cli/scaffold/admin.d.ts +6 -0
  187. package/dist/cli/scaffold/admin.js +5 -3
  188. package/dist/cli/scaffold/booking.d.ts +12 -0
  189. package/dist/cli/scaffold/booking.js +103 -0
  190. package/dist/core/cms.d.ts +24 -9
  191. package/dist/core/cms.js +72 -34
  192. package/dist/core/config/validatePreviews.d.ts +12 -0
  193. package/dist/core/config/validatePreviews.js +90 -0
  194. package/dist/core/fields/slugPath.d.ts +9 -1
  195. package/dist/core/fields/slugPath.js +16 -2
  196. package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
  197. package/dist/core/server/fields/resolveRelationFields.js +12 -3
  198. package/dist/core/server/generator/generator.js +7 -2
  199. package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
  200. package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
  201. package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
  202. package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
  203. package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
  204. package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
  205. package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
  206. package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
  207. package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
  208. package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
  209. package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
  210. package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
  211. package/dist/db-postgres/schema/booking/bookings.d.ts +324 -0
  212. package/dist/db-postgres/schema/booking/bookings.js +52 -0
  213. package/dist/db-postgres/schema/booking/index.d.ts +7 -0
  214. package/dist/db-postgres/schema/booking/index.js +7 -0
  215. package/dist/db-postgres/schema/index.d.ts +1 -0
  216. package/dist/db-postgres/schema/index.js +1 -0
  217. package/dist/db-postgres/schema-booking.d.ts +1 -0
  218. package/dist/db-postgres/schema-booking.js +1 -0
  219. package/dist/server/security/rate-limit-handler.d.ts +19 -0
  220. package/dist/server/security/rate-limit-handler.js +34 -0
  221. package/dist/shop/adapters/payu/client.js +3 -1
  222. package/dist/shop/adapters/payu/index.js +37 -17
  223. package/dist/shop/adapters/payu/payload.d.ts +6 -0
  224. package/dist/shop/adapters/payu/payload.js +1 -1
  225. package/dist/shop/adapters/stripe/index.js +36 -21
  226. package/dist/shop/index.d.ts +11 -2
  227. package/dist/shop/index.js +43 -1
  228. package/dist/shop/rate-limit.d.ts +1 -9
  229. package/dist/shop/rate-limit.js +3 -28
  230. package/dist/shop/types.d.ts +19 -0
  231. package/dist/sveltekit/config.d.ts +4 -0
  232. package/dist/sveltekit/config.js +6 -0
  233. package/dist/types/cms.d.ts +15 -1
  234. package/dist/types/cms.schema.d.ts +96 -2
  235. package/dist/types/cms.schema.js +84 -1
  236. package/dist/types/config.d.ts +33 -0
  237. package/dist/types/layout.d.ts +6 -0
  238. package/dist/types/modules.d.ts +133 -0
  239. package/dist/types/modules.js +27 -0
  240. package/dist/updates/0.37.0/index.d.ts +2 -0
  241. package/dist/updates/0.37.0/index.js +52 -0
  242. package/dist/updates/0.37.1/index.d.ts +2 -0
  243. package/dist/updates/0.37.1/index.js +24 -0
  244. package/dist/updates/index.js +5 -1
  245. package/package.json +19 -1
  246. package/dist/admin/components/layout/nav-shop.svelte +0 -33
  247. 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;
@@ -317,7 +317,7 @@
317
317
  return renderComponent(EntryLink, {
318
318
  name: info.row.original.name,
319
319
  url: info.row.original.url,
320
- slug: info.row.original.slug
320
+ path: info.row.original.slug
321
321
  });
322
322
  }
323
323
  },
@@ -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>;
@@ -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}
@@ -1,26 +1,59 @@
1
1
  <script lang="ts">
2
2
  import * as ToggleGroup from '../../../../components/ui/toggle-group/index.js';
3
+ import * as Select from '../../../../components/ui/select/index.js';
3
4
  import Button from '../../../../components/ui/button/button.svelte';
4
5
  import DeviceTablet from '@tabler/icons-svelte/icons/device-tablet';
5
6
  import DeviceMobile from '@tabler/icons-svelte/icons/device-mobile';
6
7
  import DeviceDesktop from '@tabler/icons-svelte/icons/device-desktop';
7
8
  import ArrowsMaximize from '@tabler/icons-svelte/icons/arrows-maximize';
8
9
  import Link from '@tabler/icons-svelte/icons/link';
9
- import type { DbEntryVersion } from '../../../../types/entries.js';
10
+ import type { PreviewView } from '../../../../types/config.js';
11
+ import { getLocalizedLabel } from '../../../utils/collectionLabel.js';
12
+ import { useInterfaceLanguage } from '../../../state/interface-language.svelte.js';
10
13
  import type { ElementSize } from 'runed';
11
14
 
12
15
  type SizePreset = 'responsive' | 'desktop' | 'tablet' | 'mobile';
13
16
 
14
17
  type Props = {
15
- collection: { previewUrl?: string };
16
- editingEntry: DbEntryVersion;
18
+ /** Fully built preview URL of the active view. */
19
+ previewSrc: string;
20
+ views: PreviewView[];
21
+ activeName: string;
22
+ onSelectView: (name: string) => void;
17
23
  previewIframe?: HTMLIFrameElement;
18
24
  sizePreset: SizePreset;
19
25
  size: ElementSize;
20
26
  el?: HTMLElement;
21
27
  };
22
28
 
23
- let { collection, editingEntry, previewIframe = $bindable(), sizePreset = $bindable('responsive'), size, el = $bindable() }: Props = $props();
29
+ let {
30
+ previewSrc,
31
+ views,
32
+ activeName,
33
+ onSelectView,
34
+ previewIframe = $bindable(),
35
+ sizePreset = $bindable('responsive'),
36
+ size,
37
+ el = $bindable()
38
+ }: Props = $props();
39
+
40
+ const interfaceLanguage = useInterfaceLanguage();
41
+
42
+ const lang = {
43
+ pl: { view: 'Widok', openInNewTab: 'Otwórz podgląd w nowej karcie' },
44
+ en: { view: 'View', openInNewTab: 'Open preview in a new tab' }
45
+ } as const;
46
+
47
+ const t = $derived(lang[interfaceLanguage.current as 'pl' | 'en'] ?? lang.en);
48
+
49
+ function viewLabel(view: PreviewView): string {
50
+ return getLocalizedLabel(view.label, interfaceLanguage.current) || view.name;
51
+ }
52
+
53
+ const activeLabel = $derived.by(() => {
54
+ const view = views.find((v) => v.name === activeName) ?? views[0];
55
+ return view ? viewLabel(view) : '';
56
+ });
24
57
 
25
58
  const sizePresets = {
26
59
  responsive: null,
@@ -61,8 +94,36 @@
61
94
  </script>
62
95
 
63
96
  <div class="flex h-full flex-col overflow-hidden">
64
- <div class="flex items-center justify-center gap-2 border-b px-2 py-1.5">
65
- <ToggleGroup.Root type="single" bind:value={sizePreset} role="radiogroup">
97
+ <!--
98
+ Trzy strefy, nie jeden ciąg: kontekst (jaki widok) przy krawędzi wiodącej,
99
+ prezentacja (rozmiar urządzenia) na środku płótna, akcja poboczna (otwórz
100
+ w nowej karcie) przy końcowej. Grid `1fr auto 1fr` trzyma przełączniki
101
+ urządzeń dokładnie na środku niezależnie od długości nazwy widoku.
102
+ -->
103
+ <div class="grid grid-cols-[1fr_auto_1fr] items-center gap-2 border-b px-2 py-1.5">
104
+ <div class="justify-self-start">
105
+ {#if views.length > 1}
106
+ <Select.Root type="single" value={activeName} onValueChange={onSelectView}>
107
+ <Select.Trigger
108
+ class="hover:bg-accent hover:text-accent-foreground h-8 w-auto gap-1.5 border-0 bg-transparent px-2 text-sm font-medium shadow-none"
109
+ aria-label={t.view}
110
+ >
111
+ {activeLabel}
112
+ </Select.Trigger>
113
+ <Select.Content align="start">
114
+ {#each views as view (view.name)}
115
+ <Select.Item value={view.name} label={viewLabel(view)} />
116
+ {/each}
117
+ </Select.Content>
118
+ </Select.Root>
119
+ {/if}
120
+ </div>
121
+ <ToggleGroup.Root
122
+ type="single"
123
+ bind:value={sizePreset}
124
+ role="radiogroup"
125
+ class="justify-self-center"
126
+ >
66
127
  <ToggleGroup.Item value="responsive" aria-label="Responsive view">
67
128
  <ArrowsMaximize class="size-4" />
68
129
  </ToggleGroup.Item>
@@ -79,8 +140,10 @@
79
140
  <Button
80
141
  variant="ghost"
81
142
  size="icon"
82
- href="{collection.previewUrl}?preview={editingEntry.id}"
143
+ href={previewSrc}
83
144
  target="_blank"
145
+ aria-label={t.openInNewTab}
146
+ class="justify-self-end"
84
147
  >
85
148
  <Link class="size-4" />
86
149
  </Button>
@@ -90,7 +153,7 @@
90
153
  <iframe
91
154
  bind:this={previewIframe}
92
155
  class="h-full w-full border-0"
93
- src="{collection.previewUrl}?preview={editingEntry.id}"
156
+ src={previewSrc}
94
157
  title="preview"
95
158
  ></iframe>
96
159
  {:else if sizePreset === 'desktop'}
@@ -101,7 +164,7 @@
101
164
  class="shrink-0 border-0"
102
165
  width={sizePresets.desktop[0]}
103
166
  height={sizePresets.desktop[1]}
104
- src="{collection.previewUrl}?preview={editingEntry.id}"
167
+ src={previewSrc}
105
168
  title="preview"
106
169
  ></iframe>
107
170
  </div>
@@ -142,7 +205,7 @@
142
205
  "
143
206
  width={iw}
144
207
  height={ih}
145
- src="{collection.previewUrl}?preview={editingEntry.id}"
208
+ src={previewSrc}
146
209
  title="preview"
147
210
  ></iframe>
148
211
  </div>
@@ -1,11 +1,12 @@
1
- import type { DbEntryVersion } from '../../../../types/entries.js';
1
+ import type { PreviewView } from '../../../../types/config.js';
2
2
  import type { ElementSize } from 'runed';
3
3
  type SizePreset = 'responsive' | 'desktop' | 'tablet' | 'mobile';
4
4
  type Props = {
5
- collection: {
6
- previewUrl?: string;
7
- };
8
- editingEntry: DbEntryVersion;
5
+ /** Fully built preview URL of the active view. */
6
+ previewSrc: string;
7
+ views: PreviewView[];
8
+ activeName: string;
9
+ onSelectView: (name: string) => void;
9
10
  previewIframe?: HTMLIFrameElement;
10
11
  sizePreset: SizePreset;
11
12
  size: ElementSize;
@@ -0,0 +1,22 @@
1
+ import type { PreviewView } from '../../../types/config.js';
2
+ /**
3
+ * Holds which preview view is showing, plus the per-tab memory of manual picks:
4
+ * a view the user selected by hand on a tab wins over that tab's `preview`
5
+ * binding when they come back to it. Lives as long as the open entry — nothing
6
+ * is persisted to the URL or the database.
7
+ */
8
+ export declare function createPreviewViewsContext(views: PreviewView[]): {
9
+ readonly views: PreviewView[];
10
+ readonly activeName: string;
11
+ readonly activeView: PreviewView | undefined;
12
+ /** Manual pick — remember it for the tab the user is on. */
13
+ select(name: string): void;
14
+ /** A tab became active (on mount too, so a `?tab=` deep-link lands right). */
15
+ onTabActivate(tabValue: string, tabPreview?: string): void;
16
+ };
17
+ export type PreviewViewsContext = ReturnType<typeof createPreviewViewsContext>;
18
+ /**
19
+ * Optional — layout components also render outside the entry editor (Storybook,
20
+ * forms), where no preview context exists.
21
+ */
22
+ export declare function getPreviewViewsContext(): PreviewViewsContext | undefined;