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.
- package/API.md +179 -3
- package/CHANGELOG.md +64 -0
- package/DOCS.md +28 -3
- package/ROADMAP.md +18 -0
- package/dist/admin/client/booking/booking-dashboard.svelte +40 -0
- package/dist/admin/client/booking/booking-dashboard.svelte.d.ts +3 -0
- package/dist/admin/client/booking/customer-panel.svelte +102 -0
- package/dist/admin/client/booking/customer-panel.svelte.d.ts +11 -0
- package/dist/admin/client/booking/customer-view.d.ts +52 -0
- package/dist/admin/client/booking/customer-view.js +134 -0
- package/dist/admin/client/booking/participant-data-panel.svelte +330 -0
- package/dist/admin/client/booking/participant-data-panel.svelte.d.ts +22 -0
- package/dist/admin/client/booking/reservation-detail-page.svelte +637 -0
- package/dist/admin/client/booking/reservation-detail-page.svelte.d.ts +3 -0
- package/dist/admin/client/booking/reservations-page.svelte +221 -0
- package/dist/admin/client/booking/reservations-page.svelte.d.ts +3 -0
- package/dist/admin/client/collection/collection-entries.svelte +21 -2
- package/dist/admin/client/collection/entry-link.svelte +6 -4
- package/dist/admin/client/collection/entry-link.svelte.d.ts +3 -1
- package/dist/admin/client/collection/row-actions.svelte +12 -4
- package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
- package/dist/admin/client/collection/status-badge.svelte +39 -3
- package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
- package/dist/admin/client/entry/entry.svelte +26 -9
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
- package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
- package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
- package/dist/admin/client/entry/preview-views.d.ts +27 -0
- package/dist/admin/client/entry/preview-views.js +33 -0
- package/dist/admin/client/index.d.ts +3 -0
- package/dist/admin/client/index.js +3 -0
- package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
- package/dist/admin/client/modules/dashboard-registry.js +11 -0
- package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
- package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
- package/dist/admin/client/modules/stat-card.svelte +22 -0
- package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
- package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
- package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
- package/dist/admin/components/fields/relation-field.svelte +11 -2
- package/dist/admin/components/icons/icon-map.js +8 -0
- package/dist/admin/components/layout/app-sidebar.svelte +45 -14
- package/dist/admin/components/layout/context-switcher.svelte +90 -0
- package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
- package/dist/admin/components/layout/lang.d.ts +4 -0
- package/dist/admin/components/layout/lang.js +8 -0
- package/dist/admin/components/layout/layout-renderer.svelte +2 -1
- package/dist/admin/components/layout/layout-tabs.svelte +14 -0
- package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
- package/dist/admin/components/layout/nav-collections.svelte +16 -9
- package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
- package/dist/admin/components/layout/nav-global.svelte +45 -0
- package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
- package/dist/admin/components/layout/nav-main.svelte +8 -32
- package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
- package/dist/admin/components/layout/nav-module.svelte +57 -0
- package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
- package/dist/admin/components/layout/nav-section.svelte +1 -1
- package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
- package/dist/admin/components/layout/nav-singletons.svelte +14 -7
- package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
- package/dist/admin/remote/booking.remote.d.ts +244 -0
- package/dist/admin/remote/booking.remote.js +283 -0
- package/dist/admin/remote/entry.remote.d.ts +3 -0
- package/dist/admin/remote/entry.remote.js +14 -3
- package/dist/admin/remote/index.d.ts +2 -0
- package/dist/admin/remote/index.js +2 -0
- package/dist/admin/remote/modules.remote.d.ts +10 -0
- package/dist/admin/remote/modules.remote.js +14 -0
- package/dist/admin/state/active-context-resolve.d.ts +37 -0
- package/dist/admin/state/active-context-resolve.js +66 -0
- package/dist/admin/state/active-context.svelte.d.ts +8 -0
- package/dist/admin/state/active-context.svelte.js +27 -0
- package/dist/booking/adjustments.d.ts +33 -0
- package/dist/booking/adjustments.js +26 -0
- package/dist/booking/capacity.d.ts +12 -0
- package/dist/booking/capacity.js +10 -0
- package/dist/booking/client/index.d.ts +39 -0
- package/dist/booking/client/index.js +44 -0
- package/dist/booking/client/use-booking.svelte.d.ts +25 -0
- package/dist/booking/client/use-booking.svelte.js +56 -0
- package/dist/booking/config.d.ts +156 -0
- package/dist/booking/config.js +36 -0
- package/dist/booking/draft.d.ts +77 -0
- package/dist/booking/draft.js +67 -0
- package/dist/booking/expiry-logic.d.ts +30 -0
- package/dist/booking/expiry-logic.js +33 -0
- package/dist/booking/index.d.ts +18 -0
- package/dist/booking/index.js +52 -0
- package/dist/booking/ledger.d.ts +46 -0
- package/dist/booking/ledger.js +60 -0
- package/dist/booking/number.d.ts +2 -0
- package/dist/booking/number.js +11 -0
- package/dist/booking/participant-fields/access.d.ts +10 -0
- package/dist/booking/participant-fields/access.js +14 -0
- package/dist/booking/participant-fields/age.d.ts +12 -0
- package/dist/booking/participant-fields/age.js +28 -0
- package/dist/booking/participant-fields/completeness.d.ts +8 -0
- package/dist/booking/participant-fields/completeness.js +13 -0
- package/dist/booking/participant-fields/group.d.ts +6 -0
- package/dist/booking/participant-fields/group.js +15 -0
- package/dist/booking/participant-fields/index.d.ts +6 -0
- package/dist/booking/participant-fields/index.js +6 -0
- package/dist/booking/participant-fields/types.d.ts +50 -0
- package/dist/booking/participant-fields/types.js +5 -0
- package/dist/booking/participant-fields/validate.d.ts +7 -0
- package/dist/booking/participant-fields/validate.js +44 -0
- package/dist/booking/participant-pricing.d.ts +93 -0
- package/dist/booking/participant-pricing.js +91 -0
- package/dist/booking/participants-view.d.ts +32 -0
- package/dist/booking/participants-view.js +22 -0
- package/dist/booking/payment-context.d.ts +19 -0
- package/dist/booking/payment-context.js +29 -0
- package/dist/booking/pricing-inputs.d.ts +28 -0
- package/dist/booking/pricing-inputs.js +35 -0
- package/dist/booking/pricing.d.ts +86 -0
- package/dist/booking/pricing.js +125 -0
- package/dist/booking/recompute.d.ts +22 -0
- package/dist/booking/recompute.js +31 -0
- package/dist/booking/reminders-schedule.d.ts +11 -0
- package/dist/booking/reminders-schedule.js +28 -0
- package/dist/booking/server/adjustments.d.ts +27 -0
- package/dist/booking/server/adjustments.js +136 -0
- package/dist/booking/server/bookings.d.ts +45 -0
- package/dist/booking/server/bookings.js +288 -0
- package/dist/booking/server/catalog.d.ts +9 -0
- package/dist/booking/server/catalog.js +20 -0
- package/dist/booking/server/db.d.ts +7 -0
- package/dist/booking/server/db.js +18 -0
- package/dist/booking/server/email.d.ts +23 -0
- package/dist/booking/server/email.js +59 -0
- package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
- package/dist/booking/server/emailTemplateRegistry.js +125 -0
- package/dist/booking/server/http/create-handler.d.ts +5 -0
- package/dist/booking/server/http/create-handler.js +68 -0
- package/dist/booking/server/http/index.d.ts +4 -0
- package/dist/booking/server/http/index.js +4 -0
- package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
- package/dist/booking/server/http/maintenance-handler.js +36 -0
- package/dist/booking/server/http/portal-handler.d.ts +13 -0
- package/dist/booking/server/http/portal-handler.js +99 -0
- package/dist/booking/server/http/webhook-handler.d.ts +9 -0
- package/dist/booking/server/http/webhook-handler.js +39 -0
- package/dist/booking/server/index.d.ts +12 -0
- package/dist/booking/server/index.js +14 -0
- package/dist/booking/server/maintenance.d.ts +28 -0
- package/dist/booking/server/maintenance.js +134 -0
- package/dist/booking/server/participants.d.ts +26 -0
- package/dist/booking/server/participants.js +92 -0
- package/dist/booking/server/payments.d.ts +34 -0
- package/dist/booking/server/payments.js +207 -0
- package/dist/booking/server/portal-session.d.ts +16 -0
- package/dist/booking/server/portal-session.js +42 -0
- package/dist/booking/server/portal.d.ts +207 -0
- package/dist/booking/server/portal.js +234 -0
- package/dist/booking/server/refunds.d.ts +7 -0
- package/dist/booking/server/refunds.js +58 -0
- package/dist/booking/server/reminders.d.ts +9 -0
- package/dist/booking/server/reminders.js +24 -0
- package/dist/booking/server/validation.d.ts +17 -0
- package/dist/booking/server/validation.js +55 -0
- package/dist/booking/state-machine.d.ts +41 -0
- package/dist/booking/state-machine.js +73 -0
- package/dist/booking/templates/_partials/footer.en.html +4 -0
- package/dist/booking/templates/_partials/footer.pl.html +4 -0
- package/dist/booking/templates/_partials/header.en.html +4 -0
- package/dist/booking/templates/_partials/header.pl.html +4 -0
- package/dist/booking/templates/balance-due.en.html +10 -0
- package/dist/booking/templates/balance-due.pl.html +10 -0
- package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
- package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
- package/dist/booking/templates/booking-received.en.html +9 -0
- package/dist/booking/templates/booking-received.pl.html +9 -0
- package/dist/booking/templates/deposit-paid.en.html +6 -0
- package/dist/booking/templates/deposit-paid.pl.html +6 -0
- package/dist/booking/templates/payment-link.en.html +5 -0
- package/dist/booking/templates/payment-link.pl.html +5 -0
- package/dist/booking/templates/payment-received.en.html +10 -0
- package/dist/booking/templates/payment-received.pl.html +10 -0
- package/dist/booking/totals.d.ts +38 -0
- package/dist/booking/totals.js +61 -0
- package/dist/booking/webhook-logic.d.ts +28 -0
- package/dist/booking/webhook-logic.js +50 -0
- package/dist/cli/create-user.d.ts +7 -1
- package/dist/cli/create-user.js +23 -15
- package/dist/cli/index.js +55 -7
- package/dist/cli/scaffold/admin.d.ts +6 -0
- package/dist/cli/scaffold/admin.js +5 -3
- package/dist/cli/scaffold/booking.d.ts +12 -0
- package/dist/cli/scaffold/booking.js +103 -0
- package/dist/core/cms.d.ts +24 -9
- package/dist/core/cms.js +72 -34
- package/dist/core/config/validatePreviews.d.ts +12 -0
- package/dist/core/config/validatePreviews.js +90 -0
- package/dist/core/fields/dotPath.d.ts +4 -0
- package/dist/core/fields/dotPath.js +18 -0
- package/dist/core/fields/slugPath.d.ts +9 -1
- package/dist/core/fields/slugPath.js +16 -2
- package/dist/core/fields/slugify.d.ts +3 -0
- package/dist/core/fields/slugify.js +4 -0
- package/dist/core/server/entries/operations/duplicate.d.ts +10 -0
- package/dist/core/server/entries/operations/duplicate.js +73 -0
- package/dist/core/server/entries/operations/slugNormalize.d.ts +8 -0
- package/dist/core/server/entries/operations/slugNormalize.js +20 -0
- package/dist/core/server/entries/operations/slugUniqueness.d.ts +17 -0
- package/dist/core/server/entries/operations/slugUniqueness.js +39 -0
- package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
- package/dist/core/server/fields/resolveRelationFields.js +12 -3
- package/dist/core/server/generator/generator.js +7 -2
- package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
- package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
- package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
- package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
- package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
- package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
- package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
- package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
- package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
- package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
- package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
- package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
- package/dist/db-postgres/schema/booking/bookings.d.ts +307 -0
- package/dist/db-postgres/schema/booking/bookings.js +40 -0
- package/dist/db-postgres/schema/booking/index.d.ts +7 -0
- package/dist/db-postgres/schema/booking/index.js +7 -0
- package/dist/db-postgres/schema/index.d.ts +1 -0
- package/dist/db-postgres/schema/index.js +1 -0
- package/dist/db-postgres/schema-booking.d.ts +1 -0
- package/dist/db-postgres/schema-booking.js +1 -0
- package/dist/server/security/rate-limit-handler.d.ts +19 -0
- package/dist/server/security/rate-limit-handler.js +34 -0
- package/dist/shop/adapters/payu/client.js +3 -1
- package/dist/shop/adapters/payu/index.js +37 -17
- package/dist/shop/adapters/payu/payload.d.ts +6 -0
- package/dist/shop/adapters/payu/payload.js +1 -1
- package/dist/shop/adapters/stripe/index.js +36 -21
- package/dist/shop/index.d.ts +11 -2
- package/dist/shop/index.js +43 -1
- package/dist/shop/rate-limit.d.ts +1 -9
- package/dist/shop/rate-limit.js +3 -28
- package/dist/shop/types.d.ts +19 -0
- package/dist/sveltekit/config.d.ts +4 -0
- package/dist/sveltekit/config.js +6 -0
- package/dist/types/cms.d.ts +15 -1
- package/dist/types/cms.schema.d.ts +96 -2
- package/dist/types/cms.schema.js +84 -1
- package/dist/types/config.d.ts +33 -0
- package/dist/types/layout.d.ts +6 -0
- package/dist/types/modules.d.ts +133 -0
- package/dist/types/modules.js +27 -0
- package/dist/updates/0.36.9/index.d.ts +2 -0
- package/dist/updates/0.36.9/index.js +12 -0
- package/dist/updates/0.37.0/index.d.ts +2 -0
- package/dist/updates/0.37.0/index.js +52 -0
- package/dist/updates/index.js +5 -1
- package/package.json +19 -1
- package/dist/admin/components/layout/nav-shop.svelte +0 -33
- package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
3
|
+
import { formatShortDateOnly } from '../../utils/formatters.js';
|
|
4
|
+
import { toCustomerView, type CustomerFieldHint, type CustomerValue } from './customer-view.js';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
customer: Record<string, unknown>;
|
|
8
|
+
/** Anchor-date key from the booking config. */
|
|
9
|
+
dateFieldKey?: string | null;
|
|
10
|
+
/** Presentation hints from `defineBooking({ customerFields })`: labels + `birthDate` role. */
|
|
11
|
+
fields?: CustomerFieldHint[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
let { customer, dateFieldKey = null, fields = [] }: Props = $props();
|
|
15
|
+
|
|
16
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
17
|
+
// Wiek liczony na „teraz" — ustalone raz na render, żeby wszystkie komórki miały tę samą datę.
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const view = $derived(toCustomerView(customer, { dateFieldKey, fields, now }));
|
|
20
|
+
|
|
21
|
+
const t = $derived(
|
|
22
|
+
interfaceLanguage.current === 'pl'
|
|
23
|
+
? { title: 'Klient', empty: 'Brak danych klienta.', years: 'l.' }
|
|
24
|
+
: { title: 'Customer', empty: 'No customer data.', years: 'yrs' }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function display(value: CustomerValue): string {
|
|
28
|
+
if (value.kind !== 'date') return value.raw;
|
|
29
|
+
const date = formatShortDateOnly(value.raw, interfaceLanguage.current);
|
|
30
|
+
return value.age === undefined ? date : `${date} (${value.age} ${t.years})`;
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<section class="bg-card rounded-xl border p-5 shadow-sm">
|
|
35
|
+
<h2 class="mb-4 text-lg font-semibold">{t.title}</h2>
|
|
36
|
+
|
|
37
|
+
{#if view.rows.length === 0 && view.tables.length === 0}
|
|
38
|
+
<p class="text-muted-foreground text-sm">{t.empty}</p>
|
|
39
|
+
{:else}
|
|
40
|
+
{#if view.rows.length > 0}
|
|
41
|
+
<dl class="grid gap-x-6 gap-y-3 sm:grid-cols-2">
|
|
42
|
+
{#each view.rows as row (row.key)}
|
|
43
|
+
<div>
|
|
44
|
+
<dt class="text-muted-foreground text-xs font-medium">{row.label}</dt>
|
|
45
|
+
<dd class="text-sm">
|
|
46
|
+
{#if row.value.kind === 'email'}
|
|
47
|
+
<a class="text-primary hover:underline" href="mailto:{row.value.raw}">
|
|
48
|
+
{row.value.raw}
|
|
49
|
+
</a>
|
|
50
|
+
{:else if row.value.kind === 'phone'}
|
|
51
|
+
<a
|
|
52
|
+
class="text-primary hover:underline"
|
|
53
|
+
href="tel:{row.value.raw.replace(/\s/g, '')}"
|
|
54
|
+
>
|
|
55
|
+
{row.value.raw}
|
|
56
|
+
</a>
|
|
57
|
+
{:else}
|
|
58
|
+
{display(row.value)}
|
|
59
|
+
{/if}
|
|
60
|
+
</dd>
|
|
61
|
+
</div>
|
|
62
|
+
{/each}
|
|
63
|
+
</dl>
|
|
64
|
+
{/if}
|
|
65
|
+
|
|
66
|
+
{#each view.tables as table (table.key)}
|
|
67
|
+
<div class="mt-5">
|
|
68
|
+
<h3 class="text-muted-foreground mb-2 text-xs font-medium">{table.label}</h3>
|
|
69
|
+
<div class="overflow-x-auto">
|
|
70
|
+
<table class="w-full text-sm">
|
|
71
|
+
<thead>
|
|
72
|
+
<tr class="text-muted-foreground border-border border-b text-left">
|
|
73
|
+
{#each table.columns as col (col.key)}
|
|
74
|
+
<th class="py-2 pr-4 font-medium">{col.label}</th>
|
|
75
|
+
{/each}
|
|
76
|
+
</tr>
|
|
77
|
+
</thead>
|
|
78
|
+
<tbody>
|
|
79
|
+
{#each table.rows as row, i (i)}
|
|
80
|
+
<tr class="border-border/60 border-b last:border-0">
|
|
81
|
+
{#each row as cell, j (j)}
|
|
82
|
+
<td class="py-2 pr-4">
|
|
83
|
+
{#if cell.kind === 'email'}
|
|
84
|
+
<a class="text-primary hover:underline" href="mailto:{cell.raw}">{cell.raw}</a>
|
|
85
|
+
{:else if cell.kind === 'phone'}
|
|
86
|
+
<a class="text-primary hover:underline" href="tel:{cell.raw.replace(/\s/g, '')}"
|
|
87
|
+
>{cell.raw}</a
|
|
88
|
+
>
|
|
89
|
+
{:else}
|
|
90
|
+
{display(cell) || '—'}
|
|
91
|
+
{/if}
|
|
92
|
+
</td>
|
|
93
|
+
{/each}
|
|
94
|
+
</tr>
|
|
95
|
+
{/each}
|
|
96
|
+
</tbody>
|
|
97
|
+
</table>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
{/each}
|
|
101
|
+
{/if}
|
|
102
|
+
</section>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type CustomerFieldHint } from './customer-view.js';
|
|
2
|
+
type Props = {
|
|
3
|
+
customer: Record<string, unknown>;
|
|
4
|
+
/** Anchor-date key from the booking config. */
|
|
5
|
+
dateFieldKey?: string | null;
|
|
6
|
+
/** Presentation hints from `defineBooking({ customerFields })`: labels + `birthDate` role. */
|
|
7
|
+
fields?: CustomerFieldHint[];
|
|
8
|
+
};
|
|
9
|
+
declare const CustomerPanel: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type CustomerPanel = ReturnType<typeof CustomerPanel>;
|
|
11
|
+
export default CustomerPanel;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type CustomerValue = {
|
|
2
|
+
kind: 'text' | 'email' | 'phone' | 'date';
|
|
3
|
+
raw: string;
|
|
4
|
+
/** Skończone lata na `now` — tylko dla pól o roli `birthDate`. */
|
|
5
|
+
age?: number;
|
|
6
|
+
};
|
|
7
|
+
export type CustomerRow = {
|
|
8
|
+
key: string;
|
|
9
|
+
label: string;
|
|
10
|
+
value: CustomerValue;
|
|
11
|
+
};
|
|
12
|
+
export type CustomerColumn = {
|
|
13
|
+
key: string;
|
|
14
|
+
label: string;
|
|
15
|
+
};
|
|
16
|
+
export type CustomerTable = {
|
|
17
|
+
key: string;
|
|
18
|
+
label: string;
|
|
19
|
+
columns: CustomerColumn[];
|
|
20
|
+
rows: CustomerValue[][];
|
|
21
|
+
};
|
|
22
|
+
/** Wskazówka prezentacyjna dla pola `customer` (podzbiór `FieldDef`). */
|
|
23
|
+
export type CustomerFieldHint = {
|
|
24
|
+
key: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
role?: string;
|
|
27
|
+
};
|
|
28
|
+
export interface CustomerViewOptions {
|
|
29
|
+
/** Pole daty-kotwicy z konfiguracji bookingu (dla formatów nie-ISO). */
|
|
30
|
+
dateFieldKey?: string | null;
|
|
31
|
+
/** Etykiety i role; klucze zagnieżdżone adresuje się kropką (`people.birthDate`). */
|
|
32
|
+
fields?: CustomerFieldHint[];
|
|
33
|
+
/** Data odniesienia dla wieku. Brak ⇒ wiek się nie liczy. */
|
|
34
|
+
now?: Date;
|
|
35
|
+
}
|
|
36
|
+
/** `referralSource` → „Referral source"; `departure_date` → „Departure date". */
|
|
37
|
+
export declare function humanizeKey(key: string): string;
|
|
38
|
+
/** Etykieta pola: z konfiguracji projektu, a gdy jej nie ma — uczłowieczony klucz. */
|
|
39
|
+
export declare function labelFor(key: string, fields?: CustomerFieldHint[]): string;
|
|
40
|
+
export declare function classifyValue(raw: unknown, key: string, options?: CustomerViewOptions): CustomerValue | null;
|
|
41
|
+
/**
|
|
42
|
+
* Skrót do kolumny „Klient" na liście: pierwszy e-mail i pierwsza wartość tekstowa wyglądająca
|
|
43
|
+
* na nazwę (zawiera literę).
|
|
44
|
+
*/
|
|
45
|
+
export declare function customerSummary(customer: Record<string, unknown>): {
|
|
46
|
+
name: string | null;
|
|
47
|
+
email: string | null;
|
|
48
|
+
};
|
|
49
|
+
export declare function toCustomerView(customer: Record<string, unknown>, options?: CustomerViewOptions): {
|
|
50
|
+
rows: CustomerRow[];
|
|
51
|
+
tables: CustomerTable[];
|
|
52
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel „Klient" — czysta logika prezentacji worka `booking.customer`.
|
|
3
|
+
*
|
|
4
|
+
* `customer` to `jsonb`, którego KSZTAŁT DEFINIUJE PROJEKT, nie rdzeń. Dlatego typ wartości
|
|
5
|
+
* rozpoznajemy po WARTOŚCI (e-mail zawiera `@`, telefon to wzorzec cyfr, data ISO to data),
|
|
6
|
+
* a NIE po nazwie klucza — `if (key === 'referralSource')` byłoby wpisaniem semantyki jednego
|
|
7
|
+
* projektu w rdzeń.
|
|
8
|
+
*
|
|
9
|
+
* Czego z wartości wyczytać się NIE DA: ludzkiej etykiety pola i tego, która data jest datą
|
|
10
|
+
* urodzenia (data przylotu też jest datą). To dostarcza projekt przez `defineBooking({ customerFields })`
|
|
11
|
+
* — wyłącznie jako wskazówki prezentacyjne. Bez nich panel nadal działa: etykietą jest
|
|
12
|
+
* uczłowieczony klucz, a wiek się nie liczy.
|
|
13
|
+
*
|
|
14
|
+
* Zegara nie czyta — `now` wstrzykuje wywołujący.
|
|
15
|
+
*/
|
|
16
|
+
import { ageAt } from '../../../booking/participant-fields/age.js';
|
|
17
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
18
|
+
/** Telefon: `+`, cyfry, spacje, myślniki, nawiasy — przy co najmniej 7 cyfrach. */
|
|
19
|
+
const PHONE_RE = /^\+?[\d\s()-]{7,}$/;
|
|
20
|
+
/**
|
|
21
|
+
* Data ISO (`2026-08-12`, opcjonalnie z częścią czasową). Sprawdzana PRZED telefonem, bo
|
|
22
|
+
* „2026-08-12" to same cyfry i myślniki — bez tego data dostawałaby klikalny link `tel:`.
|
|
23
|
+
*/
|
|
24
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$/;
|
|
25
|
+
/** `referralSource` → „Referral source"; `departure_date` → „Departure date". */
|
|
26
|
+
export function humanizeKey(key) {
|
|
27
|
+
const spaced = key
|
|
28
|
+
.replace(/[_.-]+/g, ' ')
|
|
29
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase();
|
|
32
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
33
|
+
}
|
|
34
|
+
function digitCount(s) {
|
|
35
|
+
return (s.match(/\d/g) ?? []).length;
|
|
36
|
+
}
|
|
37
|
+
function hintFor(key, fields) {
|
|
38
|
+
return fields?.find((f) => f.key === key);
|
|
39
|
+
}
|
|
40
|
+
/** Etykieta pola: z konfiguracji projektu, a gdy jej nie ma — uczłowieczony klucz. */
|
|
41
|
+
export function labelFor(key, fields) {
|
|
42
|
+
return hintFor(key, fields)?.label ?? humanizeKey(key);
|
|
43
|
+
}
|
|
44
|
+
export function classifyValue(raw, key, options = {}) {
|
|
45
|
+
if (raw === null || raw === undefined)
|
|
46
|
+
return null;
|
|
47
|
+
if (Array.isArray(raw)) {
|
|
48
|
+
const scalars = raw.filter((v) => typeof v !== 'object' || v === null);
|
|
49
|
+
if (scalars.length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
return { kind: 'text', raw: scalars.map(String).join(', ') };
|
|
52
|
+
}
|
|
53
|
+
if (typeof raw === 'object')
|
|
54
|
+
return null;
|
|
55
|
+
const text = String(raw).trim();
|
|
56
|
+
if (text === '')
|
|
57
|
+
return null;
|
|
58
|
+
const isAnchorDate = Boolean(options.dateFieldKey) && key === options.dateFieldKey;
|
|
59
|
+
const isDate = isAnchorDate || ISO_DATE_RE.test(text);
|
|
60
|
+
if (isDate) {
|
|
61
|
+
const value = { kind: 'date', raw: text };
|
|
62
|
+
// Wiek liczymy WYŁĄCZNIE dla pól, które projekt oznaczył rolą `birthDate` — z samej
|
|
63
|
+
// wartości nie da się odróżnić daty urodzenia od daty przylotu.
|
|
64
|
+
if (options.now && hintFor(key, options.fields)?.role === 'birthDate') {
|
|
65
|
+
const age = ageAt(text, options.now);
|
|
66
|
+
if (age !== null && age >= 0)
|
|
67
|
+
value.age = age;
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
if (EMAIL_RE.test(text))
|
|
72
|
+
return { kind: 'email', raw: text };
|
|
73
|
+
if (PHONE_RE.test(text) && digitCount(text) >= 7)
|
|
74
|
+
return { kind: 'phone', raw: text };
|
|
75
|
+
return { kind: 'text', raw: text };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Skrót do kolumny „Klient" na liście: pierwszy e-mail i pierwsza wartość tekstowa wyglądająca
|
|
79
|
+
* na nazwę (zawiera literę).
|
|
80
|
+
*/
|
|
81
|
+
export function customerSummary(customer) {
|
|
82
|
+
let name = null;
|
|
83
|
+
let email = null;
|
|
84
|
+
for (const [key, raw] of Object.entries(customer ?? {})) {
|
|
85
|
+
const v = classifyValue(raw, key);
|
|
86
|
+
if (!v)
|
|
87
|
+
continue;
|
|
88
|
+
if (!email && v.kind === 'email')
|
|
89
|
+
email = v.raw;
|
|
90
|
+
if (!name && v.kind === 'text' && /\p{L}/u.test(v.raw))
|
|
91
|
+
name = v.raw;
|
|
92
|
+
}
|
|
93
|
+
return { name, email };
|
|
94
|
+
}
|
|
95
|
+
/** Tablica obiektów (np. manifest osób) → tabelka; kolumny = unia kluczy wszystkich wierszy. */
|
|
96
|
+
function toTable(key, items, options) {
|
|
97
|
+
if (items.length === 0)
|
|
98
|
+
return null;
|
|
99
|
+
const colKeys = [];
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
for (const k of Object.keys(item)) {
|
|
102
|
+
if (!colKeys.includes(k))
|
|
103
|
+
colKeys.push(k);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (colKeys.length === 0)
|
|
107
|
+
return null;
|
|
108
|
+
// Kolumny tabeli adresuje się kropką: `people.birthDate`.
|
|
109
|
+
const columns = colKeys.map((c) => ({
|
|
110
|
+
key: c,
|
|
111
|
+
label: labelFor(`${key}.${c}`, options.fields) ?? humanizeKey(c)
|
|
112
|
+
}));
|
|
113
|
+
const rows = items.map((item) => colKeys.map((c) => classifyValue(item[c], `${key}.${c}`, options) ?? { kind: 'text', raw: '' }));
|
|
114
|
+
return { key, label: labelFor(key, options.fields), columns, rows };
|
|
115
|
+
}
|
|
116
|
+
export function toCustomerView(customer, options = {}) {
|
|
117
|
+
const rows = [];
|
|
118
|
+
const tables = [];
|
|
119
|
+
for (const [key, raw] of Object.entries(customer ?? {})) {
|
|
120
|
+
const isObjectArray = Array.isArray(raw) &&
|
|
121
|
+
raw.length > 0 &&
|
|
122
|
+
raw.every((v) => typeof v === 'object' && v !== null && !Array.isArray(v));
|
|
123
|
+
if (isObjectArray) {
|
|
124
|
+
const table = toTable(key, raw, options);
|
|
125
|
+
if (table)
|
|
126
|
+
tables.push(table);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const value = classifyValue(raw, key, options);
|
|
130
|
+
if (value)
|
|
131
|
+
rows.push({ key, label: labelFor(key, options.fields), value });
|
|
132
|
+
}
|
|
133
|
+
return { rows, tables };
|
|
134
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { FieldDef, AgeFlagConfig } from '../../../booking/participant-fields/types.js';
|
|
3
|
+
import {
|
|
4
|
+
ageAt,
|
|
5
|
+
canFill,
|
|
6
|
+
completeness,
|
|
7
|
+
deriveAgeFlag,
|
|
8
|
+
groupBySection
|
|
9
|
+
} from '../../../booking/participant-fields/index.js';
|
|
10
|
+
import { Button } from '../../../components/ui/button/index.js';
|
|
11
|
+
|
|
12
|
+
interface PanelItem {
|
|
13
|
+
id: string;
|
|
14
|
+
departureDate: Date | string | null;
|
|
15
|
+
participantFields: FieldDef[];
|
|
16
|
+
}
|
|
17
|
+
interface PanelParticipant {
|
|
18
|
+
id: string;
|
|
19
|
+
itemId: string;
|
|
20
|
+
data: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
items,
|
|
25
|
+
participants,
|
|
26
|
+
ageFlag,
|
|
27
|
+
lang,
|
|
28
|
+
onSave
|
|
29
|
+
}: {
|
|
30
|
+
items: PanelItem[];
|
|
31
|
+
participants: PanelParticipant[];
|
|
32
|
+
ageFlag: AgeFlagConfig | null;
|
|
33
|
+
lang: 'pl' | 'en';
|
|
34
|
+
/** When provided, staff/both fields become editable; saves the merged patch. */
|
|
35
|
+
onSave?: (participantId: string, data: Record<string, unknown>) => Promise<void>;
|
|
36
|
+
} = $props();
|
|
37
|
+
|
|
38
|
+
// Badge wieku — etykieta z configu (np. „Dziecko"/„Junior"), fallback domyślny.
|
|
39
|
+
const ageBadgeLabel = $derived(ageFlag?.label?.[lang] ?? (lang === 'pl' ? 'Dziecko' : 'Child'));
|
|
40
|
+
|
|
41
|
+
let editingId = $state<string | null>(null);
|
|
42
|
+
let draft = $state<Record<string, unknown>>({});
|
|
43
|
+
let savingEdit = $state(false);
|
|
44
|
+
let saveError = $state<string | null>(null);
|
|
45
|
+
|
|
46
|
+
const t = $derived(
|
|
47
|
+
lang === 'pl'
|
|
48
|
+
? {
|
|
49
|
+
participant: 'Uczestnik',
|
|
50
|
+
complete: 'Komplet',
|
|
51
|
+
missing: 'Brakuje',
|
|
52
|
+
pending: '— do uzupełnienia',
|
|
53
|
+
staff: 'tylko obsługa',
|
|
54
|
+
other: 'Pozostałe',
|
|
55
|
+
yes: 'Tak',
|
|
56
|
+
no: 'Nie',
|
|
57
|
+
years: 'l.',
|
|
58
|
+
noData: 'Brak danych uczestnika',
|
|
59
|
+
noParticipants: 'Brak uczestników',
|
|
60
|
+
edit: 'Edytuj',
|
|
61
|
+
save: 'Zapisz',
|
|
62
|
+
cancelEdit: 'Anuluj'
|
|
63
|
+
}
|
|
64
|
+
: {
|
|
65
|
+
participant: 'Participant',
|
|
66
|
+
complete: 'Complete',
|
|
67
|
+
missing: 'Missing',
|
|
68
|
+
pending: '— to be filled',
|
|
69
|
+
staff: 'staff only',
|
|
70
|
+
other: 'Other',
|
|
71
|
+
yes: 'Yes',
|
|
72
|
+
no: 'No',
|
|
73
|
+
years: 'yrs',
|
|
74
|
+
noData: 'No participant data',
|
|
75
|
+
noParticipants: 'No participants',
|
|
76
|
+
edit: 'Edit',
|
|
77
|
+
save: 'Save',
|
|
78
|
+
cancelEdit: 'Cancel'
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
function participantsForItem(itemId: string) {
|
|
83
|
+
return participants.filter((p) => p.itemId === itemId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function anchorDate(d: Date | string | null): Date | null {
|
|
87
|
+
if (!d) return null;
|
|
88
|
+
const date = d instanceof Date ? d : new Date(d);
|
|
89
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isEmptyVal(raw: unknown): boolean {
|
|
93
|
+
return raw == null || raw === '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// „Teraz" ustalone raz na render, żeby wszystkie karty liczyły wiek na tę samą datę.
|
|
97
|
+
const now = new Date();
|
|
98
|
+
|
|
99
|
+
function formatFieldValue(field: FieldDef, raw: unknown): string {
|
|
100
|
+
if (isEmptyVal(raw)) return t.pending;
|
|
101
|
+
if (field.type === 'boolean') return raw ? t.yes : t.no;
|
|
102
|
+
if (Array.isArray(raw)) return raw.join(', ');
|
|
103
|
+
|
|
104
|
+
// Data urodzenia dostaje skończone lata w nawiasie — obsługa nie musi liczyć w głowie.
|
|
105
|
+
// Rola, nie nazwa pola: z samej wartości nie odróżnisz daty urodzenia od innej daty.
|
|
106
|
+
if (field.role === 'birthDate' && typeof raw === 'string') {
|
|
107
|
+
const age = ageAt(raw, now);
|
|
108
|
+
if (age !== null && age >= 0) return `${raw} (${age} ${t.years})`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return String(raw);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Card title = value of the field flagged with role 'name' (no magic keys).
|
|
115
|
+
function displayName(data: Record<string, unknown>, fields: FieldDef[]): string | null {
|
|
116
|
+
const nameField = fields.find((f) => f.role === 'name');
|
|
117
|
+
const v = nameField ? data[nameField.key] : undefined;
|
|
118
|
+
return typeof v === 'string' && v.trim() ? v : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function humanizeKey(key: string): string {
|
|
122
|
+
return key
|
|
123
|
+
.replace(/[_-]+/g, ' ')
|
|
124
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
125
|
+
.trim()
|
|
126
|
+
.replace(/^./, (c) => c.toUpperCase());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function formatLegacy(value: unknown): string {
|
|
130
|
+
if (value == null || value === '') return '—';
|
|
131
|
+
if (Array.isArray(value)) return value.map(formatLegacy).join(', ');
|
|
132
|
+
if (typeof value === 'boolean') return value ? t.yes : t.no;
|
|
133
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
134
|
+
return String(value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Data keys not covered by the schema → flat list (graceful for legacy / no-schema
|
|
138
|
+
// bookings). Nested objects expand one level with a prefixed label.
|
|
139
|
+
function extraFields(
|
|
140
|
+
data: Record<string, unknown>,
|
|
141
|
+
fields: FieldDef[],
|
|
142
|
+
prefix = ''
|
|
143
|
+
): Array<{ label: string; value: string }> {
|
|
144
|
+
const known = new Set(fields.map((f) => f.key));
|
|
145
|
+
const out: Array<{ label: string; value: string }> = [];
|
|
146
|
+
for (const [key, value] of Object.entries(data ?? {})) {
|
|
147
|
+
if (prefix === '' && known.has(key)) continue; // already rendered from schema
|
|
148
|
+
if (value == null || value === '' || (Array.isArray(value) && value.length === 0)) continue;
|
|
149
|
+
const label = (prefix ? `${prefix} · ` : '') + humanizeKey(key);
|
|
150
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
151
|
+
out.push(...extraFields(value as Record<string, unknown>, fields, label));
|
|
152
|
+
} else {
|
|
153
|
+
out.push({ label, value: formatLegacy(value) });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function startEdit(p: PanelParticipant) {
|
|
160
|
+
editingId = p.id;
|
|
161
|
+
draft = { ...p.data };
|
|
162
|
+
saveError = null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function cancelEdit() {
|
|
166
|
+
editingId = null;
|
|
167
|
+
saveError = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function setDraft(key: string, value: unknown) {
|
|
171
|
+
draft[key] = value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function commitEdit(p: PanelParticipant) {
|
|
175
|
+
if (!onSave) return;
|
|
176
|
+
savingEdit = true;
|
|
177
|
+
saveError = null;
|
|
178
|
+
try {
|
|
179
|
+
await onSave(p.id, draft);
|
|
180
|
+
editingId = null;
|
|
181
|
+
} catch (e) {
|
|
182
|
+
saveError = e instanceof Error ? e.message : 'Error';
|
|
183
|
+
} finally {
|
|
184
|
+
savingEdit = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
{#if participants.length === 0}
|
|
190
|
+
<p class="text-muted-foreground text-sm">{t.noParticipants}</p>
|
|
191
|
+
{:else}
|
|
192
|
+
<div class="space-y-4">
|
|
193
|
+
{#each items as it (it.id)}
|
|
194
|
+
{@const fields = it.participantFields}
|
|
195
|
+
{#each participantsForItem(it.id) as p, pi (p.id)}
|
|
196
|
+
{@const groups = groupBySection(fields)}
|
|
197
|
+
{@const comp = completeness(p.data, fields)}
|
|
198
|
+
{@const ageInfo = deriveAgeFlag(p.data, fields, ageFlag, anchorDate(it.departureDate))}
|
|
199
|
+
{@const name = displayName(p.data, fields)}
|
|
200
|
+
{@const extra = extraFields(p.data, fields)}
|
|
201
|
+
{@const editing = editingId === p.id}
|
|
202
|
+
<article class="border-border space-y-3 rounded-xl border p-4">
|
|
203
|
+
<header class="flex flex-wrap items-center gap-2">
|
|
204
|
+
<span
|
|
205
|
+
class="bg-lavender-lighter text-primary flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold"
|
|
206
|
+
>{pi + 1}</span
|
|
207
|
+
>
|
|
208
|
+
<span class="text-foreground text-sm font-medium">{name ?? `${t.participant} ${pi + 1}`}</span>
|
|
209
|
+
{#if comp.total > 0}
|
|
210
|
+
{#if comp.complete}
|
|
211
|
+
<span class="bg-lavender-lighter text-primary rounded-full px-2 py-0.5 text-xs font-medium"
|
|
212
|
+
>{t.complete}</span
|
|
213
|
+
>
|
|
214
|
+
{:else}
|
|
215
|
+
<span class="border-border text-muted-foreground rounded-full border px-2 py-0.5 text-xs"
|
|
216
|
+
>{t.missing}: {comp.missing.join(', ')}</span
|
|
217
|
+
>
|
|
218
|
+
{/if}
|
|
219
|
+
{/if}
|
|
220
|
+
{#if ageInfo.flagged}
|
|
221
|
+
<span class="bg-lavender-lighter text-primary rounded-full px-2 py-0.5 text-xs font-medium">
|
|
222
|
+
{ageBadgeLabel}{ageInfo.age != null ? ` · ${ageInfo.age}` : ''}
|
|
223
|
+
</span>
|
|
224
|
+
{/if}
|
|
225
|
+
{#if onSave}
|
|
226
|
+
<div class="ml-auto flex gap-2">
|
|
227
|
+
{#if editing}
|
|
228
|
+
<Button size="sm" disabled={savingEdit} onclick={() => commitEdit(p)}>{t.save}</Button>
|
|
229
|
+
<Button size="sm" variant="outline" disabled={savingEdit} onclick={cancelEdit}>{t.cancelEdit}</Button>
|
|
230
|
+
{:else}
|
|
231
|
+
<Button size="sm" variant="outline" onclick={() => startEdit(p)}>{t.edit}</Button>
|
|
232
|
+
{/if}
|
|
233
|
+
</div>
|
|
234
|
+
{/if}
|
|
235
|
+
</header>
|
|
236
|
+
|
|
237
|
+
{#if editing && saveError}
|
|
238
|
+
<p class="text-destructive text-sm">{saveError}</p>
|
|
239
|
+
{/if}
|
|
240
|
+
|
|
241
|
+
{#each groups as g (g.section ?? '__none')}
|
|
242
|
+
{#if g.section}
|
|
243
|
+
<h4 class="text-muted-foreground text-xs font-semibold tracking-wide uppercase">{g.section}</h4>
|
|
244
|
+
{/if}
|
|
245
|
+
<dl class="grid grid-cols-1 gap-x-6 gap-y-1.5 sm:grid-cols-2">
|
|
246
|
+
{#each g.fields as f (f.key)}
|
|
247
|
+
<div class="flex items-baseline justify-between gap-4 text-sm">
|
|
248
|
+
<dt class="text-muted-foreground flex items-center gap-1.5">
|
|
249
|
+
<span>{f.label}</span>
|
|
250
|
+
{#if f.whoFills === 'staff'}
|
|
251
|
+
<span class="border-border text-muted-foreground rounded border px-1 text-[10px] uppercase">{t.staff}</span>
|
|
252
|
+
{/if}
|
|
253
|
+
</dt>
|
|
254
|
+
<dd
|
|
255
|
+
class="text-right {!editing && isEmptyVal(p.data[f.key])
|
|
256
|
+
? 'text-muted-foreground italic'
|
|
257
|
+
: 'text-foreground'}"
|
|
258
|
+
>
|
|
259
|
+
{#if editing && canFill(f, 'staff')}
|
|
260
|
+
{#if f.type === 'boolean'}
|
|
261
|
+
<input
|
|
262
|
+
type="checkbox"
|
|
263
|
+
checked={!!draft[f.key]}
|
|
264
|
+
onchange={(e) => setDraft(f.key, e.currentTarget.checked)}
|
|
265
|
+
/>
|
|
266
|
+
{:else if f.type === 'select'}
|
|
267
|
+
<select
|
|
268
|
+
value={String(draft[f.key] ?? '')}
|
|
269
|
+
onchange={(e) => setDraft(f.key, e.currentTarget.value)}
|
|
270
|
+
class="border-border rounded-md border px-2 py-1 text-sm"
|
|
271
|
+
>
|
|
272
|
+
<option value="">—</option>
|
|
273
|
+
{#each f.options ?? [] as opt (opt)}
|
|
274
|
+
<option value={opt}>{opt}</option>
|
|
275
|
+
{/each}
|
|
276
|
+
</select>
|
|
277
|
+
{:else if f.type === 'number'}
|
|
278
|
+
<input
|
|
279
|
+
type="number"
|
|
280
|
+
min={f.min}
|
|
281
|
+
max={f.max}
|
|
282
|
+
value={draft[f.key] == null ? '' : String(draft[f.key])}
|
|
283
|
+
oninput={(e) =>
|
|
284
|
+
setDraft(f.key, e.currentTarget.value === '' ? undefined : Number(e.currentTarget.value))}
|
|
285
|
+
class="border-border w-28 rounded-md border px-2 py-1 text-sm"
|
|
286
|
+
/>
|
|
287
|
+
{:else if f.type === 'date'}
|
|
288
|
+
<input
|
|
289
|
+
type="date"
|
|
290
|
+
value={String(draft[f.key] ?? '')}
|
|
291
|
+
oninput={(e) => setDraft(f.key, e.currentTarget.value)}
|
|
292
|
+
class="border-border rounded-md border px-2 py-1 text-sm"
|
|
293
|
+
/>
|
|
294
|
+
{:else}
|
|
295
|
+
<input
|
|
296
|
+
type="text"
|
|
297
|
+
value={String(draft[f.key] ?? '')}
|
|
298
|
+
oninput={(e) => setDraft(f.key, e.currentTarget.value)}
|
|
299
|
+
class="border-border w-full rounded-md border px-2 py-1 text-sm"
|
|
300
|
+
/>
|
|
301
|
+
{/if}
|
|
302
|
+
{:else}
|
|
303
|
+
{formatFieldValue(f, p.data[f.key])}
|
|
304
|
+
{/if}
|
|
305
|
+
</dd>
|
|
306
|
+
</div>
|
|
307
|
+
{/each}
|
|
308
|
+
</dl>
|
|
309
|
+
{/each}
|
|
310
|
+
|
|
311
|
+
{#if extra.length > 0}
|
|
312
|
+
<h4 class="text-muted-foreground text-xs font-semibold tracking-wide uppercase">{t.other}</h4>
|
|
313
|
+
<dl class="grid grid-cols-1 gap-x-6 gap-y-1.5 sm:grid-cols-2">
|
|
314
|
+
{#each extra as e (e.label)}
|
|
315
|
+
<div class="flex items-baseline justify-between gap-4 text-sm">
|
|
316
|
+
<dt class="text-muted-foreground">{e.label}</dt>
|
|
317
|
+
<dd class="text-foreground text-right">{e.value}</dd>
|
|
318
|
+
</div>
|
|
319
|
+
{/each}
|
|
320
|
+
</dl>
|
|
321
|
+
{/if}
|
|
322
|
+
|
|
323
|
+
{#if fields.length === 0 && extra.length === 0}
|
|
324
|
+
<p class="text-muted-foreground text-sm italic">{t.noData}</p>
|
|
325
|
+
{/if}
|
|
326
|
+
</article>
|
|
327
|
+
{/each}
|
|
328
|
+
{/each}
|
|
329
|
+
</div>
|
|
330
|
+
{/if}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FieldDef, AgeFlagConfig } from '../../../booking/participant-fields/types.js';
|
|
2
|
+
interface PanelItem {
|
|
3
|
+
id: string;
|
|
4
|
+
departureDate: Date | string | null;
|
|
5
|
+
participantFields: FieldDef[];
|
|
6
|
+
}
|
|
7
|
+
interface PanelParticipant {
|
|
8
|
+
id: string;
|
|
9
|
+
itemId: string;
|
|
10
|
+
data: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
type $$ComponentProps = {
|
|
13
|
+
items: PanelItem[];
|
|
14
|
+
participants: PanelParticipant[];
|
|
15
|
+
ageFlag: AgeFlagConfig | null;
|
|
16
|
+
lang: 'pl' | 'en';
|
|
17
|
+
/** When provided, staff/both fields become editable; saves the merged patch. */
|
|
18
|
+
onSave?: (participantId: string, data: Record<string, unknown>) => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
declare const ParticipantDataPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
21
|
+
type ParticipantDataPanel = ReturnType<typeof ParticipantDataPanel>;
|
|
22
|
+
export default ParticipantDataPanel;
|