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.
- package/API.md +178 -3
- package/CHANGELOG.md +77 -0
- package/DOCS.md +28 -3
- package/ROADMAP.md +32 -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 +1 -1
- 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/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 +247 -0
- package/dist/admin/remote/booking.remote.js +286 -0
- 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 +42 -0
- package/dist/booking/client/index.js +48 -0
- package/dist/booking/client/use-booking.svelte.d.ts +24 -0
- package/dist/booking/client/use-booking.svelte.js +56 -0
- package/dist/booking/config.d.ts +176 -0
- package/dist/booking/config.js +37 -0
- package/dist/booking/draft.d.ts +96 -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 +139 -0
- package/dist/booking/server/bookings.d.ts +51 -0
- package/dist/booking/server/bookings.js +336 -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 +70 -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 +103 -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 +206 -0
- package/dist/booking/server/portal.js +236 -0
- package/dist/booking/server/price-source.d.ts +73 -0
- package/dist/booking/server/price-source.js +94 -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/slugPath.d.ts +9 -1
- package/dist/core/fields/slugPath.js +16 -2
- 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 +324 -0
- package/dist/db-postgres/schema/booking/bookings.js +52 -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.37.0/index.d.ts +2 -0
- package/dist/updates/0.37.0/index.js +52 -0
- package/dist/updates/0.37.1/index.d.ts +2 -0
- package/dist/updates/0.37.1/index.js +24 -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,286 @@
|
|
|
1
|
+
import { command, query } from '$app/server';
|
|
2
|
+
import z from 'zod';
|
|
3
|
+
import { getCMS } from '../../core/cms.js';
|
|
4
|
+
import { getBookingDb } from '../../booking/server/db.js';
|
|
5
|
+
import { bookingsTable, bookingItemsTable, bookingPersonsTable, bookingItemPersonsTable, bookingPaymentsTable, bookingRemindersTable, bookingAdjustmentsTable } from '../../db-postgres/schema/booking/index.js';
|
|
6
|
+
import { flattenAssignments } from '../../booking/participants-view.js';
|
|
7
|
+
import { and, desc, eq, ilike, inArray, isNull, or, sql } from 'drizzle-orm';
|
|
8
|
+
import { createBooking, editBooking } from '../../booking/server/bookings.js';
|
|
9
|
+
import { createInstallmentPayment, recordCash } from '../../booking/server/payments.js';
|
|
10
|
+
import { addBookingAdjustment as addAdjustmentSvc, voidBookingAdjustment as voidAdjustmentSvc } from '../../booking/server/adjustments.js';
|
|
11
|
+
import { refundPayment } from '../../booking/server/refunds.js';
|
|
12
|
+
import { saveParticipantData } from '../../booking/server/participants.js';
|
|
13
|
+
import { sendBookingEmail } from '../../booking/server/email.js';
|
|
14
|
+
import { assertTransition, BOOKING_STATUSES } from '../../booking/state-machine.js';
|
|
15
|
+
import { resolveParticipantFields } from '../../booking/config.js';
|
|
16
|
+
import { resolveEntry, resolveEntries } from '../../core/server/entries/operations/resolveEntry.js';
|
|
17
|
+
import { requireAuth } from './middleware/auth.js';
|
|
18
|
+
/**
|
|
19
|
+
* Best-effort display title of a catalog entry. The core does not know the catalog's shape, so it
|
|
20
|
+
* tries the conventional title-ish keys and falls back to the raw ref. Shared by the item picker
|
|
21
|
+
* and the booking detail so both name a trip the same way.
|
|
22
|
+
*/
|
|
23
|
+
function catalogTitle(entry, ref) {
|
|
24
|
+
const title = entry.title ?? entry.name ?? entry._slug;
|
|
25
|
+
return title ? String(title) : ref;
|
|
26
|
+
}
|
|
27
|
+
/** Whether the booking module is registered. @public */
|
|
28
|
+
export const getBookingEnabled = query(async () => {
|
|
29
|
+
return getCMS().hasModule('booking');
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Lists active (non-deleted) bookings, newest first — paginated server-side.
|
|
33
|
+
*
|
|
34
|
+
* Search spans the booking number AND the `customer` bag. The bag's shape is defined by the
|
|
35
|
+
* consumer, not by the core, so we match against its text form instead of guessing key names.
|
|
36
|
+
*
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export const listBookings = query(z
|
|
40
|
+
.object({
|
|
41
|
+
status: z.enum(BOOKING_STATUSES).optional(),
|
|
42
|
+
search: z.string().optional(),
|
|
43
|
+
limit: z.number().int().min(1).max(200).default(50),
|
|
44
|
+
offset: z.number().int().min(0).default(0)
|
|
45
|
+
})
|
|
46
|
+
.optional(), async (opts) => {
|
|
47
|
+
requireAuth();
|
|
48
|
+
if (!getCMS().hasModule('booking'))
|
|
49
|
+
return { items: [], total: 0 };
|
|
50
|
+
const { status, search, limit = 50, offset = 0 } = opts ?? {};
|
|
51
|
+
const db = getBookingDb();
|
|
52
|
+
const filters = [isNull(bookingsTable.deletedAt)];
|
|
53
|
+
if (status)
|
|
54
|
+
filters.push(eq(bookingsTable.status, status));
|
|
55
|
+
if (search?.trim()) {
|
|
56
|
+
const term = `%${search.trim()}%`;
|
|
57
|
+
filters.push(or(ilike(bookingsTable.number, term), sql `${bookingsTable.customer}::text ILIKE ${term}`));
|
|
58
|
+
}
|
|
59
|
+
const where = and(...filters);
|
|
60
|
+
const [items, counted] = await Promise.all([
|
|
61
|
+
db
|
|
62
|
+
.select()
|
|
63
|
+
.from(bookingsTable)
|
|
64
|
+
.where(where)
|
|
65
|
+
.orderBy(desc(bookingsTable.createdAt))
|
|
66
|
+
.limit(limit)
|
|
67
|
+
.offset(offset),
|
|
68
|
+
db.select({ count: sql `count(*)::int` }).from(bookingsTable).where(where)
|
|
69
|
+
]);
|
|
70
|
+
return { items, total: counted[0]?.count ?? 0 };
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* Aggregate stats for the booking dashboard, computed in SQL.
|
|
74
|
+
*
|
|
75
|
+
* Deliberately NOT derived from {@link listBookings}: that query is paginated, so summing its rows
|
|
76
|
+
* would silently report stats for the first page only.
|
|
77
|
+
*
|
|
78
|
+
* @public
|
|
79
|
+
*/
|
|
80
|
+
export const getBookingStats = query(async () => {
|
|
81
|
+
requireAuth();
|
|
82
|
+
if (!getCMS().hasModule('booking'))
|
|
83
|
+
return { active: 0, outstanding: 0, toConfirm: 0, currency: 'PLN' };
|
|
84
|
+
const db = getBookingDb();
|
|
85
|
+
const cfg = getCMS().getModuleConfig('booking') ?? null;
|
|
86
|
+
const [row] = await db
|
|
87
|
+
.select({
|
|
88
|
+
active: sql `count(*)::int`,
|
|
89
|
+
outstanding: sql `coalesce(sum((${bookingsTable.totals}->>'balance')::bigint), 0)::bigint`,
|
|
90
|
+
toConfirm: sql `count(*) filter (where ${bookingsTable.status} in ('draft','reserved'))::int`
|
|
91
|
+
})
|
|
92
|
+
.from(bookingsTable)
|
|
93
|
+
.where(isNull(bookingsTable.deletedAt));
|
|
94
|
+
return {
|
|
95
|
+
active: row?.active ?? 0,
|
|
96
|
+
outstanding: Number(row?.outstanding ?? 0),
|
|
97
|
+
toConfirm: row?.toConfirm ?? 0,
|
|
98
|
+
currency: cfg?.currency ?? 'PLN'
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
/** Full booking detail (booking + items + participants + ledger + reminders). @public */
|
|
102
|
+
export const getBooking = query(z.string().uuid(), async (id) => {
|
|
103
|
+
requireAuth();
|
|
104
|
+
if (!getCMS().hasModule('booking'))
|
|
105
|
+
return null;
|
|
106
|
+
const db = getBookingDb();
|
|
107
|
+
const [booking] = await db
|
|
108
|
+
.select()
|
|
109
|
+
.from(bookingsTable)
|
|
110
|
+
.where(and(eq(bookingsTable.id, id), isNull(bookingsTable.deletedAt)));
|
|
111
|
+
if (!booking)
|
|
112
|
+
return null;
|
|
113
|
+
const items = await db.select().from(bookingItemsTable).where(eq(bookingItemsTable.bookingId, id));
|
|
114
|
+
const itemIds = items.map((i) => i.id);
|
|
115
|
+
const persons = await db
|
|
116
|
+
.select()
|
|
117
|
+
.from(bookingPersonsTable)
|
|
118
|
+
.where(eq(bookingPersonsTable.bookingId, id));
|
|
119
|
+
const assignments = itemIds.length
|
|
120
|
+
? await db
|
|
121
|
+
.select()
|
|
122
|
+
.from(bookingItemPersonsTable)
|
|
123
|
+
.where(inArray(bookingItemPersonsTable.itemId, itemIds))
|
|
124
|
+
: [];
|
|
125
|
+
const participants = flattenAssignments(persons, assignments);
|
|
126
|
+
const payments = await db
|
|
127
|
+
.select()
|
|
128
|
+
.from(bookingPaymentsTable)
|
|
129
|
+
.where(eq(bookingPaymentsTable.bookingId, id));
|
|
130
|
+
const reminders = await db
|
|
131
|
+
.select()
|
|
132
|
+
.from(bookingRemindersTable)
|
|
133
|
+
.where(eq(bookingRemindersTable.bookingId, id));
|
|
134
|
+
const adjustments = await db
|
|
135
|
+
.select()
|
|
136
|
+
.from(bookingAdjustmentsTable)
|
|
137
|
+
.where(eq(bookingAdjustmentsTable.bookingId, id))
|
|
138
|
+
.orderBy(desc(bookingAdjustmentsTable.createdAt));
|
|
139
|
+
// Resolve the participant-field schema AND a display title per catalog entry (booking-agnostic).
|
|
140
|
+
const cfg = getCMS().getModuleConfig('booking') ?? null;
|
|
141
|
+
const ageFlag = cfg?.ageFlag ?? null;
|
|
142
|
+
// Resolve whenever a catalog exists: staff must see the trip's NAME, not its uuid.
|
|
143
|
+
const needsEntry = cfg != null && cfg.catalogCollection != null;
|
|
144
|
+
const schemaByRef = new Map();
|
|
145
|
+
const titleByRef = new Map();
|
|
146
|
+
if (cfg) {
|
|
147
|
+
for (const ref of new Set(items.map((i) => i.tripRef))) {
|
|
148
|
+
let entry = {};
|
|
149
|
+
if (needsEntry) {
|
|
150
|
+
const resolved = await resolveEntry({ id: ref, status: 'published' });
|
|
151
|
+
if (resolved)
|
|
152
|
+
entry = resolved;
|
|
153
|
+
}
|
|
154
|
+
schemaByRef.set(ref, resolveParticipantFields(cfg, entry));
|
|
155
|
+
titleByRef.set(ref, catalogTitle(entry, ref));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const itemsWithSchema = items.map((i) => ({
|
|
159
|
+
...i,
|
|
160
|
+
participantFields: schemaByRef.get(i.tripRef) ?? [],
|
|
161
|
+
/** Human title of the catalog entry; falls back to the raw ref when unresolvable. */
|
|
162
|
+
tripTitle: titleByRef.get(i.tripRef) ?? i.tripRef
|
|
163
|
+
}));
|
|
164
|
+
return {
|
|
165
|
+
booking,
|
|
166
|
+
items: itemsWithSchema,
|
|
167
|
+
participants,
|
|
168
|
+
persons,
|
|
169
|
+
assignments,
|
|
170
|
+
payments,
|
|
171
|
+
reminders,
|
|
172
|
+
adjustments,
|
|
173
|
+
ageFlag,
|
|
174
|
+
/**
|
|
175
|
+
* Anchor-date key inside the `customer` bag. The only piece of that bag's semantics the
|
|
176
|
+
* core knows on its own — everything else the customer panel infers from values.
|
|
177
|
+
*/
|
|
178
|
+
arrivalDateField: cfg?.arrivalDateField ?? null,
|
|
179
|
+
/** Optional presentation hints for the `customer` bag (labels + `birthDate` role). */
|
|
180
|
+
customerFields: cfg?.customerFields ?? []
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
/** Published catalog trips for the staff item-editor picker (ref + best-effort title). @public */
|
|
184
|
+
export const listBookingCatalog = query(async () => {
|
|
185
|
+
requireAuth();
|
|
186
|
+
const cfg = getCMS().getModuleConfig('booking');
|
|
187
|
+
if (!cfg?.catalogCollection)
|
|
188
|
+
return [];
|
|
189
|
+
const entries = await resolveEntries({ collection: cfg.catalogCollection, status: 'published' });
|
|
190
|
+
return entries.map((e) => {
|
|
191
|
+
const rec = e;
|
|
192
|
+
const ref = String(rec._id ?? '');
|
|
193
|
+
return { ref, title: catalogTitle(rec, ref) };
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
/** Staff: create a booking from the admin panel. @public */
|
|
197
|
+
export const createBookingStaff = command(z.any(), async (input) => {
|
|
198
|
+
requireAuth();
|
|
199
|
+
// `actor: 'staff'` — zalogowana obsługa może podać cenę ręcznie (korekta, rabat
|
|
200
|
+
// negocjowany telefonicznie). Publiczne ścieżki NIE mają tego prawa: bez tej flagi
|
|
201
|
+
// rdzeń bierze cenę wyłącznie z katalogu (`resolvePrice`).
|
|
202
|
+
return createBooking(input, { actor: 'staff' });
|
|
203
|
+
});
|
|
204
|
+
/** Staff: edit a booking's items/customer (full edit power). @public */
|
|
205
|
+
export const editBookingStaff = command(z.object({ bookingId: z.string().uuid(), changes: z.any() }), async ({ bookingId, changes }) => {
|
|
206
|
+
requireAuth();
|
|
207
|
+
return editBooking(bookingId, changes, { actor: 'staff' });
|
|
208
|
+
});
|
|
209
|
+
/** Staff: generate an installment/balance payment link for any amount. @public */
|
|
210
|
+
export const addPaymentLink = command(z.object({ bookingId: z.string().uuid(), amount: z.number().int().positive() }), async ({ bookingId, amount }) => {
|
|
211
|
+
requireAuth();
|
|
212
|
+
const result = await createInstallmentPayment(bookingId, amount);
|
|
213
|
+
if (result.paymentLink) {
|
|
214
|
+
await sendBookingEmail(bookingId, 'payment-link', {
|
|
215
|
+
amount,
|
|
216
|
+
paymentLink: result.paymentLink
|
|
217
|
+
}).catch((e) => console.error('[booking] payment-link email failed', e));
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
});
|
|
221
|
+
/** Staff: record a manual cash payment. @public */
|
|
222
|
+
export const recordCashPayment = command(z.object({ bookingId: z.string().uuid(), amount: z.number().int().positive() }), async ({ bookingId, amount }) => {
|
|
223
|
+
requireAuth();
|
|
224
|
+
await recordCash(bookingId, amount);
|
|
225
|
+
return { ok: true };
|
|
226
|
+
});
|
|
227
|
+
/** Staff: refund a paid ledger entry via the adapter. @public */
|
|
228
|
+
export const refundBookingPayment = command(z.object({ paymentId: z.string().uuid(), amount: z.number().int().positive().optional() }), async ({ paymentId, amount }) => {
|
|
229
|
+
requireAuth();
|
|
230
|
+
await refundPayment(paymentId, amount);
|
|
231
|
+
return { ok: true };
|
|
232
|
+
});
|
|
233
|
+
/** Staff: change booking status (validated transition). @public */
|
|
234
|
+
export const changeBookingStatus = command(z.object({ bookingId: z.string().uuid(), to: z.string() }), async ({ bookingId, to }) => {
|
|
235
|
+
requireAuth();
|
|
236
|
+
const db = getBookingDb();
|
|
237
|
+
const [b] = await db.select().from(bookingsTable).where(eq(bookingsTable.id, bookingId));
|
|
238
|
+
if (!b)
|
|
239
|
+
throw new Error('Booking not found');
|
|
240
|
+
assertTransition(b.status, to); // throws BookingStateError on illegal move
|
|
241
|
+
await db
|
|
242
|
+
.update(bookingsTable)
|
|
243
|
+
.set({ status: to, updatedAt: new Date() })
|
|
244
|
+
.where(eq(bookingsTable.id, bookingId));
|
|
245
|
+
return { ok: true };
|
|
246
|
+
});
|
|
247
|
+
/** Staff: cancel a booking (manual). @public */
|
|
248
|
+
export const cancelBooking = command(z.object({ bookingId: z.string().uuid(), reason: z.enum(['manual']).default('manual') }), async ({ bookingId, reason }) => {
|
|
249
|
+
requireAuth();
|
|
250
|
+
const db = getBookingDb();
|
|
251
|
+
await db
|
|
252
|
+
.update(bookingsTable)
|
|
253
|
+
.set({ status: 'cancelled', cancelReason: reason, updatedAt: new Date() })
|
|
254
|
+
.where(eq(bookingsTable.id, bookingId));
|
|
255
|
+
return { ok: true };
|
|
256
|
+
});
|
|
257
|
+
/** Staff: send a transactional booking email manually. @public */
|
|
258
|
+
export const sendBookingEmailCmd = command(z.object({ bookingId: z.string().uuid(), kind: z.string() }), async ({ bookingId, kind }) => {
|
|
259
|
+
requireAuth();
|
|
260
|
+
await sendBookingEmail(bookingId, kind);
|
|
261
|
+
return { ok: true };
|
|
262
|
+
});
|
|
263
|
+
/** Staff: edit a participant's custom-field data (schema-validated). @public */
|
|
264
|
+
export const updateParticipantData = command(z.object({ participantId: z.string().uuid(), data: z.record(z.string(), z.unknown()) }), async ({ participantId, data }) => {
|
|
265
|
+
requireAuth();
|
|
266
|
+
await saveParticipantData(participantId, data, 'staff');
|
|
267
|
+
return { ok: true };
|
|
268
|
+
});
|
|
269
|
+
/** Staff: add a manual adjustment (surcharge/discount). Amount is a positive magnitude in minor units. @public */
|
|
270
|
+
export const addAdjustment = command(z.object({
|
|
271
|
+
bookingId: z.string().uuid(),
|
|
272
|
+
kind: z.enum(['manual_surcharge', 'manual_discount']),
|
|
273
|
+
amount: z.number().int().positive(),
|
|
274
|
+
label: z.string().min(1),
|
|
275
|
+
reason: z.string().optional()
|
|
276
|
+
}), async ({ bookingId, kind, amount, label, reason }) => {
|
|
277
|
+
const { user } = requireAuth();
|
|
278
|
+
await addAdjustmentSvc(bookingId, { kind, magnitude: amount, label, reason }, { actor: user.email ?? 'staff' });
|
|
279
|
+
return { ok: true };
|
|
280
|
+
});
|
|
281
|
+
/** Staff: void (soft-delete) a manual adjustment. @public */
|
|
282
|
+
export const voidAdjustment = command(z.object({ adjustmentId: z.string().uuid() }), async ({ adjustmentId }) => {
|
|
283
|
+
const { user } = requireAuth();
|
|
284
|
+
await voidAdjustmentSvc(adjustmentId, { actor: user.email ?? 'staff' });
|
|
285
|
+
return { ok: true };
|
|
286
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CMSModuleDescriptor } from '../../types/modules.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the serializable descriptors of all registered modules (shop,
|
|
4
|
+
* booking, …) — id, label, icon, and context (nav sections + surfaced
|
|
5
|
+
* collections). Strips server-only `serverHooks` and the opaque `config`
|
|
6
|
+
* (which may carry adapter functions). Powers the admin context switcher and
|
|
7
|
+
* the generic module nav.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export declare const getModules: import("@sveltejs/kit").RemoteQueryFunction<void, CMSModuleDescriptor[]>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { query } from '$app/server';
|
|
2
|
+
import { getCMS } from '../../core/cms.js';
|
|
3
|
+
import { toModuleDescriptor } from '../../types/modules.js';
|
|
4
|
+
/**
|
|
5
|
+
* Returns the serializable descriptors of all registered modules (shop,
|
|
6
|
+
* booking, …) — id, label, icon, and context (nav sections + surfaced
|
|
7
|
+
* collections). Strips server-only `serverHooks` and the opaque `config`
|
|
8
|
+
* (which may carry adapter functions). Powers the admin context switcher and
|
|
9
|
+
* the generic module nav.
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export const getModules = query(async () => {
|
|
13
|
+
return Array.from(getCMS().modules.values()).map(toModuleDescriptor);
|
|
14
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active admin context resolution. Pure, deterministic, framework-free so it can
|
|
3
|
+
* be unit-tested without the browser/runed state.
|
|
4
|
+
*
|
|
5
|
+
* Precedence (highest first):
|
|
6
|
+
* 1. **Unambiguous module path** — `/admin/{id}` or `/admin/{id}/...` always
|
|
7
|
+
* wins, so a deep-link to `/admin/shop/orders` shows the shop context
|
|
8
|
+
* regardless of stored state.
|
|
9
|
+
* 2. **`?ctx=` query** — explicit intent, used when linking a surfaced core
|
|
10
|
+
* collection (`/admin/collections/{slug}?ctx=shop`) whose path doesn't
|
|
11
|
+
* encode a context.
|
|
12
|
+
* 3. **Content-core path** — the dashboard (`/admin`) and core CMS sections
|
|
13
|
+
* (collections, singles, entries, forms) resolve to content. Neutral/global
|
|
14
|
+
* routes (media, users, account, maintenance) are excluded: they fall through
|
|
15
|
+
* to the stored sticky context so they stay inside the active module workspace.
|
|
16
|
+
* 4. **Stored sticky** — the last context the user switched to (covers
|
|
17
|
+
* ambiguous routes like the entry editor `/admin/entries/{id}`).
|
|
18
|
+
* 5. **Content** — the default CMS core workspace.
|
|
19
|
+
*/
|
|
20
|
+
/** The base CMS-core context (content editing). */
|
|
21
|
+
export declare const CONTENT_CONTEXT = "content";
|
|
22
|
+
/** Whether `pathname` is the dashboard or a core CMS section (not a module). */
|
|
23
|
+
export declare function isContentCorePath(pathname: string): boolean;
|
|
24
|
+
export interface ResolveActiveContextInput {
|
|
25
|
+
/** Value of the `ctx` query param, or null/undefined. */
|
|
26
|
+
ctxQuery?: string | null;
|
|
27
|
+
/** Persisted "sticky" context, or null/undefined. */
|
|
28
|
+
stored?: string | null;
|
|
29
|
+
/** Current pathname, e.g. `/admin/shop/orders`. */
|
|
30
|
+
pathname: string;
|
|
31
|
+
/** Ids of registered modules, e.g. `['shop', 'booking']`. */
|
|
32
|
+
moduleIds: string[];
|
|
33
|
+
}
|
|
34
|
+
/** Whether `pathname` is the landing or a sub-route of module `id`. */
|
|
35
|
+
export declare function isModulePath(pathname: string, id: string): boolean;
|
|
36
|
+
/** Resolves the active context id from URL, stored state, and module registry. */
|
|
37
|
+
export declare function resolveActiveContext(input: ResolveActiveContextInput): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active admin context resolution. Pure, deterministic, framework-free so it can
|
|
3
|
+
* be unit-tested without the browser/runed state.
|
|
4
|
+
*
|
|
5
|
+
* Precedence (highest first):
|
|
6
|
+
* 1. **Unambiguous module path** — `/admin/{id}` or `/admin/{id}/...` always
|
|
7
|
+
* wins, so a deep-link to `/admin/shop/orders` shows the shop context
|
|
8
|
+
* regardless of stored state.
|
|
9
|
+
* 2. **`?ctx=` query** — explicit intent, used when linking a surfaced core
|
|
10
|
+
* collection (`/admin/collections/{slug}?ctx=shop`) whose path doesn't
|
|
11
|
+
* encode a context.
|
|
12
|
+
* 3. **Content-core path** — the dashboard (`/admin`) and core CMS sections
|
|
13
|
+
* (collections, singles, entries, forms) resolve to content. Neutral/global
|
|
14
|
+
* routes (media, users, account, maintenance) are excluded: they fall through
|
|
15
|
+
* to the stored sticky context so they stay inside the active module workspace.
|
|
16
|
+
* 4. **Stored sticky** — the last context the user switched to (covers
|
|
17
|
+
* ambiguous routes like the entry editor `/admin/entries/{id}`).
|
|
18
|
+
* 5. **Content** — the default CMS core workspace.
|
|
19
|
+
*/
|
|
20
|
+
/** The base CMS-core context (content editing). */
|
|
21
|
+
export const CONTENT_CONTEXT = 'content';
|
|
22
|
+
/**
|
|
23
|
+
* Route prefixes owned by the content (CMS core) context. Navigating to one of
|
|
24
|
+
* these from a module context exits back to content. **Neutral/global** routes
|
|
25
|
+
* (`/admin/media|users|account|maintenance`) are deliberately NOT here: they are
|
|
26
|
+
* reachable from every context, so they keep the `stored` sticky context instead
|
|
27
|
+
* of forcing content.
|
|
28
|
+
*/
|
|
29
|
+
const CONTENT_CORE_PREFIXES = [
|
|
30
|
+
'/admin/collections',
|
|
31
|
+
'/admin/singles',
|
|
32
|
+
'/admin/entries',
|
|
33
|
+
'/admin/forms'
|
|
34
|
+
];
|
|
35
|
+
/** Whether `pathname` is the dashboard or a core CMS section (not a module). */
|
|
36
|
+
export function isContentCorePath(pathname) {
|
|
37
|
+
if (pathname === '/admin' || pathname === '/admin/')
|
|
38
|
+
return true;
|
|
39
|
+
return CONTENT_CORE_PREFIXES.some((p) => pathname === p || pathname.startsWith(p + '/'));
|
|
40
|
+
}
|
|
41
|
+
/** Whether `pathname` is the landing or a sub-route of module `id`. */
|
|
42
|
+
export function isModulePath(pathname, id) {
|
|
43
|
+
const base = `/admin/${id}`;
|
|
44
|
+
return pathname === base || pathname.startsWith(base + '/');
|
|
45
|
+
}
|
|
46
|
+
/** Resolves the active context id from URL, stored state, and module registry. */
|
|
47
|
+
export function resolveActiveContext(input) {
|
|
48
|
+
const { ctxQuery, stored, pathname, moduleIds } = input;
|
|
49
|
+
// 1. Unambiguous module path wins (deep-link correctness).
|
|
50
|
+
for (const id of moduleIds) {
|
|
51
|
+
if (isModulePath(pathname, id))
|
|
52
|
+
return id;
|
|
53
|
+
}
|
|
54
|
+
const isValid = (id) => !!id && (id === CONTENT_CONTEXT || moduleIds.includes(id));
|
|
55
|
+
// 2. Explicit ?ctx intent.
|
|
56
|
+
if (isValid(ctxQuery))
|
|
57
|
+
return ctxQuery;
|
|
58
|
+
// 3. Content-core path → content (dashboard / collections / media / …).
|
|
59
|
+
if (isContentCorePath(pathname))
|
|
60
|
+
return CONTENT_CONTEXT;
|
|
61
|
+
// 4. Sticky stored context.
|
|
62
|
+
if (isValid(stored))
|
|
63
|
+
return stored;
|
|
64
|
+
// 5. Default to content.
|
|
65
|
+
return CONTENT_CONTEXT;
|
|
66
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive active-context id. Reads the URL (pathname + `?ctx`) and the sticky
|
|
3
|
+
* store. Call inside a `$derived`/`$effect` so it tracks `page` + store changes.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getActiveContext(moduleIds: string[]): string;
|
|
6
|
+
/** Persists the user's context choice (called by the context switcher). */
|
|
7
|
+
export declare function setActiveContext(id: string): void;
|
|
8
|
+
export { CONTENT_CONTEXT } from './active-context-resolve.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PersistedState } from 'runed';
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
import { resolveActiveContext, CONTENT_CONTEXT } from './active-context-resolve.js';
|
|
4
|
+
/**
|
|
5
|
+
* Persisted "sticky" admin context. Survives navigation to ambiguous routes
|
|
6
|
+
* (e.g. the entry editor of a surfaced collection) so the workspace doesn't
|
|
7
|
+
* flicker back to content. Module deep-links and `?ctx=` still take precedence
|
|
8
|
+
* — see {@link resolveActiveContext}.
|
|
9
|
+
*/
|
|
10
|
+
const stored = new PersistedState('admin-active-context', CONTENT_CONTEXT);
|
|
11
|
+
/**
|
|
12
|
+
* Reactive active-context id. Reads the URL (pathname + `?ctx`) and the sticky
|
|
13
|
+
* store. Call inside a `$derived`/`$effect` so it tracks `page` + store changes.
|
|
14
|
+
*/
|
|
15
|
+
export function getActiveContext(moduleIds) {
|
|
16
|
+
return resolveActiveContext({
|
|
17
|
+
ctxQuery: page.url.searchParams.get('ctx'),
|
|
18
|
+
stored: stored.current,
|
|
19
|
+
pathname: page.url.pathname,
|
|
20
|
+
moduleIds
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** Persists the user's context choice (called by the context switcher). */
|
|
24
|
+
export function setActiveContext(id) {
|
|
25
|
+
stored.current = id;
|
|
26
|
+
}
|
|
27
|
+
export { CONTENT_CONTEXT } from './active-context-resolve.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual booking adjustments — staff-entered surcharges (e.g. e-bike +200) and
|
|
3
|
+
* negotiated discounts, kept OUTSIDE the rule engine so a pricing recompute
|
|
4
|
+
* never wipes them. Signed minor units: surcharge > 0, discount < 0. Pure.
|
|
5
|
+
*/
|
|
6
|
+
export type AdjustmentKind = 'manual_surcharge' | 'manual_discount';
|
|
7
|
+
export interface BookingAdjustment {
|
|
8
|
+
id: string;
|
|
9
|
+
bookingId: string;
|
|
10
|
+
kind: AdjustmentKind;
|
|
11
|
+
/** Signed minor units: surcharge > 0, discount < 0. */
|
|
12
|
+
amount: number;
|
|
13
|
+
label: string;
|
|
14
|
+
reason: string | null;
|
|
15
|
+
createdBy: string;
|
|
16
|
+
createdAt: Date;
|
|
17
|
+
/** Soft-void: set → excluded from recompute, kept for audit. */
|
|
18
|
+
voidedAt: Date | null;
|
|
19
|
+
voidedBy: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface AdjustmentSums {
|
|
22
|
+
/** Σ positive amounts of active surcharges. */
|
|
23
|
+
surcharge: number;
|
|
24
|
+
/** Σ magnitudes of active discounts (positive). */
|
|
25
|
+
manualDiscount: number;
|
|
26
|
+
}
|
|
27
|
+
export declare const NO_ADJUSTMENTS: AdjustmentSums;
|
|
28
|
+
/** Convert a UI-entered positive magnitude + kind into a signed stored amount. */
|
|
29
|
+
export declare function signedAmount(kind: AdjustmentKind, magnitude: number): number;
|
|
30
|
+
/** Fold active (non-voided) adjustments into totals contributions. By sign. */
|
|
31
|
+
export declare function sumAdjustments(rows: Pick<BookingAdjustment, 'amount' | 'voidedAt'>[]): AdjustmentSums;
|
|
32
|
+
/** Reject a zero/negative magnitude at the write boundary. */
|
|
33
|
+
export declare function assertAdjustmentMagnitude(magnitude: number): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const NO_ADJUSTMENTS = { surcharge: 0, manualDiscount: 0 };
|
|
2
|
+
/** Convert a UI-entered positive magnitude + kind into a signed stored amount. */
|
|
3
|
+
export function signedAmount(kind, magnitude) {
|
|
4
|
+
const m = Math.abs(magnitude);
|
|
5
|
+
return kind === 'manual_discount' ? -m : m;
|
|
6
|
+
}
|
|
7
|
+
/** Fold active (non-voided) adjustments into totals contributions. By sign. */
|
|
8
|
+
export function sumAdjustments(rows) {
|
|
9
|
+
let surcharge = 0;
|
|
10
|
+
let manualDiscount = 0;
|
|
11
|
+
for (const r of rows) {
|
|
12
|
+
if (r.voidedAt)
|
|
13
|
+
continue;
|
|
14
|
+
if (r.amount >= 0)
|
|
15
|
+
surcharge += r.amount;
|
|
16
|
+
else
|
|
17
|
+
manualDiscount += -r.amount;
|
|
18
|
+
}
|
|
19
|
+
return { surcharge, manualDiscount };
|
|
20
|
+
}
|
|
21
|
+
/** Reject a zero/negative magnitude at the write boundary. */
|
|
22
|
+
export function assertAdjustmentMagnitude(magnitude) {
|
|
23
|
+
if (!Number.isFinite(magnitude) || magnitude <= 0) {
|
|
24
|
+
throw new Error(`Adjustment magnitude must be a positive amount, got ${magnitude}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether `requested` seats fit a trip. `capacity == null` means
|
|
3
|
+
* unlimited (no catalog capacity configured) → always ok. Pure.
|
|
4
|
+
*/
|
|
5
|
+
export declare function decideCapacity(input: {
|
|
6
|
+
capacity: number | null;
|
|
7
|
+
alreadyBooked: number;
|
|
8
|
+
requested: number;
|
|
9
|
+
}): {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
available: number | null;
|
|
12
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether `requested` seats fit a trip. `capacity == null` means
|
|
3
|
+
* unlimited (no catalog capacity configured) → always ok. Pure.
|
|
4
|
+
*/
|
|
5
|
+
export function decideCapacity(input) {
|
|
6
|
+
if (input.capacity == null)
|
|
7
|
+
return { ok: true, available: null };
|
|
8
|
+
const available = input.capacity - input.alreadyBooked;
|
|
9
|
+
return { ok: input.requested <= available, available };
|
|
10
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser SDK for the booking storefront + self-service portal. Thin fetch
|
|
3
|
+
* wrappers over the booking HTTP handlers. Isomorphic — no server imports.
|
|
4
|
+
*
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export interface BookingClientOptions {
|
|
8
|
+
/** Base URL prefix (default ''), e.g. when the API is on another origin. */
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
/** Custom fetch (e.g. SvelteKit's `fetch` during SSR). Defaults to global. */
|
|
11
|
+
fetch?: typeof globalThis.fetch;
|
|
12
|
+
}
|
|
13
|
+
/** Error thrown on a non-2xx response; `code` carries the server's error code. */
|
|
14
|
+
export declare class BookingClientError extends Error {
|
|
15
|
+
code?: string;
|
|
16
|
+
constructor(message: string, code?: string);
|
|
17
|
+
}
|
|
18
|
+
/** Create a booking API client bound to a base URL + fetch. @public */
|
|
19
|
+
export declare function createBookingClient(opts?: BookingClientOptions): {
|
|
20
|
+
/** Create a booking (public self-serve). Returns deposit redirect info. */
|
|
21
|
+
create: (input: unknown) => Promise<unknown>;
|
|
22
|
+
/** Fetch the portal view for an access token. */
|
|
23
|
+
getByToken: (token: string) => Promise<unknown>;
|
|
24
|
+
/** Self-service edit; rejects with `code: 'CONTACT_STAFF'` on below-paid edits. */
|
|
25
|
+
edit: (token: string, changes: unknown) => Promise<unknown>;
|
|
26
|
+
/** Create an installment/balance payment link. */
|
|
27
|
+
pay: (token: string, amount: number) => Promise<unknown>;
|
|
28
|
+
/** Self-service: save a participant's custom-field data (staff fields ignored). */
|
|
29
|
+
updateParticipant: (token: string, participantId: string, data: Record<string, unknown>) => Promise<unknown>;
|
|
30
|
+
/** Drugi czynnik: weryfikuj numer telefonu; sukces ustawia sesję portalu (cookie). */
|
|
31
|
+
verifyPhone: (token: string, phone: string) => Promise<unknown>;
|
|
32
|
+
/**
|
|
33
|
+
* Self-service: dołóż wycieczki istniejącym osobom (recompute salda).
|
|
34
|
+
*
|
|
35
|
+
* Cena **nie jest** parametrem — ustala ją serwer z katalogu (`resolvePrice`).
|
|
36
|
+
*/
|
|
37
|
+
addTrips: (token: string, additions: Array<{
|
|
38
|
+
tripRef: string;
|
|
39
|
+
assignments: string[];
|
|
40
|
+
}>) => Promise<unknown>;
|
|
41
|
+
};
|
|
42
|
+
export { useBooking } from './use-booking.svelte.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Error thrown on a non-2xx response; `code` carries the server's error code. */
|
|
2
|
+
export class BookingClientError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(message, code) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'BookingClientError';
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** Create a booking API client bound to a base URL + fetch. @public */
|
|
11
|
+
export function createBookingClient(opts = {}) {
|
|
12
|
+
const base = opts.baseUrl ?? '';
|
|
13
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
14
|
+
async function req(path, init) {
|
|
15
|
+
const res = await f(`${base}${path}`, init);
|
|
16
|
+
const data = (await res.json().catch(() => ({})));
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
throw new BookingClientError(typeof data.error === 'string' ? data.error : `HTTP ${res.status}`, typeof data.code === 'string' ? data.code : undefined);
|
|
19
|
+
}
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
const jsonInit = (method, body) => ({
|
|
23
|
+
method,
|
|
24
|
+
headers: { 'content-type': 'application/json' },
|
|
25
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
/** Create a booking (public self-serve). Returns deposit redirect info. */
|
|
29
|
+
create: (input) => req('/api/booking', jsonInit('POST', input)),
|
|
30
|
+
/** Fetch the portal view for an access token. */
|
|
31
|
+
getByToken: (token) => req(`/api/booking/portal/${token}`, jsonInit('GET')),
|
|
32
|
+
/** Self-service edit; rejects with `code: 'CONTACT_STAFF'` on below-paid edits. */
|
|
33
|
+
edit: (token, changes) => req(`/api/booking/portal/${token}?action=edit`, jsonInit('POST', changes)),
|
|
34
|
+
/** Create an installment/balance payment link. */
|
|
35
|
+
pay: (token, amount) => req(`/api/booking/portal/${token}?action=pay`, jsonInit('POST', { amount })),
|
|
36
|
+
/** Self-service: save a participant's custom-field data (staff fields ignored). */
|
|
37
|
+
updateParticipant: (token, participantId, data) => req(`/api/booking/portal/${token}?action=participant`, jsonInit('POST', { participantId, data })),
|
|
38
|
+
/** Drugi czynnik: weryfikuj numer telefonu; sukces ustawia sesję portalu (cookie). */
|
|
39
|
+
verifyPhone: (token, phone) => req(`/api/booking/portal/${token}?action=verify-phone`, jsonInit('POST', { phone })),
|
|
40
|
+
/**
|
|
41
|
+
* Self-service: dołóż wycieczki istniejącym osobom (recompute salda).
|
|
42
|
+
*
|
|
43
|
+
* Cena **nie jest** parametrem — ustala ją serwer z katalogu (`resolvePrice`).
|
|
44
|
+
*/
|
|
45
|
+
addTrips: (token, additions) => req(`/api/booking/portal/${token}?action=add-trips`, jsonInit('POST', { additions }))
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export { useBooking } from './use-booking.svelte.js';
|