@ticketboothapp/booking 0.1.13 → 0.1.15
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/package.json
CHANGED
|
@@ -1,36 +1,48 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useRef,
|
|
7
|
+
useCallback,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import type { Availability, PricingConfig, PrecomputedPricesByCategory } from '@/lib/booking-api';
|
|
4
11
|
|
|
12
|
+
/** Cache TTL in milliseconds (5 minutes). Entries older than this are considered stale. */
|
|
5
13
|
export const AVAILABILITIES_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
6
14
|
|
|
7
|
-
export type AvailabilityLike = Record<string, unknown>;
|
|
8
|
-
export type PricingConfigLike = Record<string, unknown>;
|
|
9
|
-
export type PrecomputedPricesByCategoryLike = Record<string, unknown>;
|
|
10
|
-
|
|
11
15
|
export interface CachedAvailabilitiesData {
|
|
12
16
|
fetchedRanges: Array<{ start: Date; end: Date }>;
|
|
13
|
-
availabilities:
|
|
14
|
-
pricingConfig:
|
|
15
|
-
precomputedPricesByOption: Record<string,
|
|
16
|
-
|
|
17
|
+
availabilities: Availability[];
|
|
18
|
+
pricingConfig: PricingConfig | null;
|
|
19
|
+
precomputedPricesByOption: Record<string, PrecomputedPricesByCategory> | null;
|
|
20
|
+
/** Private Shuttle: shared precomputed prices (category -> currency -> price). */
|
|
21
|
+
precomputedPrices?: PrecomputedPricesByCategory | null;
|
|
22
|
+
/** Private Shuttle: resource price by currency. */
|
|
17
23
|
resourcePriceByCurrency?: Record<string, number> | null;
|
|
24
|
+
/** Private Shuttle: resource price by option (optionId -> currency -> price). */
|
|
18
25
|
resourcePriceByOption?: Record<string, Record<string, number>> | null;
|
|
26
|
+
/** Timestamp when this entry was cached (for TTL / stale-while-revalidate). */
|
|
19
27
|
cachedAt: number;
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
interface AvailabilitiesCacheContextValue {
|
|
31
|
+
/** Get cached data for a product+options+promo. Returns undefined if not cached. */
|
|
23
32
|
get: (cacheKey: string) => CachedAvailabilitiesData | undefined;
|
|
33
|
+
/** Check if cached entry is stale (older than TTL). Use for stale-while-revalidate. */
|
|
24
34
|
isStale: (cached: CachedAvailabilitiesData) => boolean;
|
|
35
|
+
/** Replace or set cache entry. */
|
|
25
36
|
set: (cacheKey: string, data: CachedAvailabilitiesData) => void;
|
|
37
|
+
/** Merge new availabilities into existing cache (or create new entry). */
|
|
26
38
|
merge: (
|
|
27
39
|
cacheKey: string,
|
|
28
40
|
update: {
|
|
29
41
|
fetchedRanges?: Array<{ start: Date; end: Date }>;
|
|
30
|
-
availabilities?:
|
|
31
|
-
pricingConfig?:
|
|
32
|
-
precomputedPricesByOption?: Record<string,
|
|
33
|
-
precomputedPrices?:
|
|
42
|
+
availabilities?: Availability[];
|
|
43
|
+
pricingConfig?: PricingConfig | null;
|
|
44
|
+
precomputedPricesByOption?: Record<string, PrecomputedPricesByCategory> | null;
|
|
45
|
+
precomputedPrices?: PrecomputedPricesByCategory | null;
|
|
34
46
|
resourcePriceByCurrency?: Record<string, number> | null;
|
|
35
47
|
resourcePriceByOption?: Record<string, Record<string, number>> | null;
|
|
36
48
|
}
|
|
@@ -41,9 +53,10 @@ const AvailabilitiesCacheContext = createContext<AvailabilitiesCacheContextValue
|
|
|
41
53
|
|
|
42
54
|
export function useAvailabilitiesCache() {
|
|
43
55
|
const ctx = useContext(AvailabilitiesCacheContext);
|
|
44
|
-
return ctx;
|
|
56
|
+
return ctx; // May be null if not wrapped
|
|
45
57
|
}
|
|
46
58
|
|
|
59
|
+
/** Build cache key from product + options + promo + optional partner pricing profile. */
|
|
47
60
|
export function buildAvailabilitiesCacheKey(
|
|
48
61
|
productId: string,
|
|
49
62
|
optionIdsKey: string,
|
|
@@ -57,7 +70,9 @@ export function buildAvailabilitiesCacheKey(
|
|
|
57
70
|
export function AvailabilitiesCacheProvider({ children }: { children: ReactNode }) {
|
|
58
71
|
const cacheRef = useRef<Map<string, CachedAvailabilitiesData>>(new Map());
|
|
59
72
|
|
|
60
|
-
const get = useCallback((cacheKey: string) =>
|
|
73
|
+
const get = useCallback((cacheKey: string) => {
|
|
74
|
+
return cacheRef.current.get(cacheKey);
|
|
75
|
+
}, []);
|
|
61
76
|
|
|
62
77
|
const isStale = useCallback((cached: CachedAvailabilitiesData) => {
|
|
63
78
|
const age = Date.now() - (cached.cachedAt ?? 0);
|
|
@@ -73,10 +88,10 @@ export function AvailabilitiesCacheProvider({ children }: { children: ReactNode
|
|
|
73
88
|
cacheKey: string,
|
|
74
89
|
update: {
|
|
75
90
|
fetchedRanges?: Array<{ start: Date; end: Date }>;
|
|
76
|
-
availabilities?:
|
|
77
|
-
pricingConfig?:
|
|
78
|
-
precomputedPricesByOption?: Record<string,
|
|
79
|
-
precomputedPrices?:
|
|
91
|
+
availabilities?: Availability[];
|
|
92
|
+
pricingConfig?: PricingConfig | null;
|
|
93
|
+
precomputedPricesByOption?: Record<string, PrecomputedPricesByCategory> | null;
|
|
94
|
+
precomputedPrices?: PrecomputedPricesByCategory | null;
|
|
80
95
|
resourcePriceByCurrency?: Record<string, number> | null;
|
|
81
96
|
resourcePriceByOption?: Record<string, Record<string, number>> | null;
|
|
82
97
|
}
|
|
@@ -91,10 +106,8 @@ export function AvailabilitiesCacheProvider({ children }: { children: ReactNode
|
|
|
91
106
|
? update.precomputedPricesByOption
|
|
92
107
|
: existing?.precomputedPricesByOption ?? null,
|
|
93
108
|
precomputedPrices: update.precomputedPrices !== undefined ? update.precomputedPrices : existing?.precomputedPrices ?? null,
|
|
94
|
-
resourcePriceByCurrency:
|
|
95
|
-
|
|
96
|
-
resourcePriceByOption:
|
|
97
|
-
update.resourcePriceByOption !== undefined ? update.resourcePriceByOption : existing?.resourcePriceByOption ?? null,
|
|
109
|
+
resourcePriceByCurrency: update.resourcePriceByCurrency !== undefined ? update.resourcePriceByCurrency : existing?.resourcePriceByCurrency ?? null,
|
|
110
|
+
resourcePriceByOption: update.resourcePriceByOption !== undefined ? update.resourcePriceByOption : existing?.resourcePriceByOption ?? null,
|
|
98
111
|
cachedAt: Date.now(),
|
|
99
112
|
};
|
|
100
113
|
cacheRef.current.set(cacheKey, next);
|
|
@@ -103,5 +116,10 @@ export function AvailabilitiesCacheProvider({ children }: { children: ReactNode
|
|
|
103
116
|
);
|
|
104
117
|
|
|
105
118
|
const value: AvailabilitiesCacheContextValue = { get, set, merge, isStale };
|
|
106
|
-
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<AvailabilitiesCacheContext.Provider value={value}>
|
|
122
|
+
{children}
|
|
123
|
+
</AvailabilitiesCacheContext.Provider>
|
|
124
|
+
);
|
|
107
125
|
}
|
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { createContext, useContext,
|
|
3
|
+
import { createContext, useContext, ReactNode } from 'react';
|
|
4
4
|
|
|
5
5
|
/** Host app / embedding context (standalone site, provider-dashboard, etc.). */
|
|
6
6
|
export type BookingAppMode = 'standalone' | 'provider-dashboard' | (string & Record<never, never>);
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Viewer role for the booking app. Drives pricing display and (in future) other feature levels.
|
|
10
|
+
* - public: customer-facing (e.g. standalone site); simplified pricing (dynamic increases rolled into base).
|
|
11
|
+
* - reseller: same as public for pricing; reserved for future reseller-specific behaviour.
|
|
12
|
+
* - admin: full detail (e.g. provider dashboard); full price breakdown, no roll-up.
|
|
13
|
+
*/
|
|
9
14
|
export type ViewerRole = 'public' | 'reseller' | 'admin';
|
|
10
15
|
|
|
11
16
|
/** Feature flags and permissions for the booking app in this context. */
|
|
12
17
|
export interface BookingAppPermissions {
|
|
18
|
+
/** When true, the price breakdown hover tooltip is shown on itinerary line items. */
|
|
13
19
|
canViewPriceBreakdown?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Viewer role. For pricing: public and reseller use simplified view; admin uses full detail.
|
|
22
|
+
* Use this for future feature gating (e.g. reseller vs public vs admin).
|
|
23
|
+
*/
|
|
14
24
|
viewerRole?: ViewerRole;
|
|
15
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* @deprecated Prefer viewerRole. When viewerRole is set it takes precedence.
|
|
27
|
+
* How much detail to show in the price breakdown tooltip.
|
|
28
|
+
*/
|
|
16
29
|
priceBreakdownDetail?: 'full' | 'simplified';
|
|
17
30
|
}
|
|
18
31
|
|
|
32
|
+
/** Params for showing the manage-booking UI (e.g. in a dialog when embedded in provider-dashboard). */
|
|
19
33
|
export interface ManageParams {
|
|
20
34
|
ref?: string;
|
|
21
35
|
reservationRef?: string;
|
|
@@ -25,11 +39,17 @@ export interface ManageParams {
|
|
|
25
39
|
export interface BookingAppContextValue {
|
|
26
40
|
mode: BookingAppMode;
|
|
27
41
|
permissions: BookingAppPermissions;
|
|
42
|
+
/** True when pricing should show simplified view (dynamic increases rolled into base). Derived from viewerRole (public/reseller) or priceBreakdownDetail. */
|
|
28
43
|
isSimplifiedPricingView: boolean;
|
|
44
|
+
/** When set (e.g. by host), used for Google Maps instead of NEXT_PUBLIC_GOOGLE_MAPS_API_KEY. Fixes prod when embedded code doesn't get build-time env. */
|
|
29
45
|
googleMapsApiKey?: string;
|
|
46
|
+
/** When set (e.g. provider-dashboard), called instead of redirecting to /manage after free booking or when payment success should show manage UI in-host. */
|
|
30
47
|
onShowManage?: (params: ManageParams) => void;
|
|
48
|
+
/** When set, used as Stripe return_url so payment success lands on the host (e.g. dashboard) instead of /manage. */
|
|
31
49
|
getSuccessUrl?: (params: { reservationRef: string; lastName: string; focusDate?: string }) => string;
|
|
50
|
+
/** When false, the language selector is hidden. Host-controlled; default true. */
|
|
32
51
|
showLanguageSelector: boolean;
|
|
52
|
+
/** Partner portal / embedded host: skip auto-scroll after choosing a calendar date. */
|
|
33
53
|
suppressCalendarDateScroll?: boolean;
|
|
34
54
|
}
|
|
35
55
|
|
|
@@ -52,15 +72,26 @@ const DEFAULT_STANDALONE: BookingAppContextValue = {
|
|
|
52
72
|
|
|
53
73
|
export interface BookingAppProviderProps {
|
|
54
74
|
children: ReactNode;
|
|
75
|
+
/** Which app or page is hosting the booking UI. */
|
|
55
76
|
mode?: BookingAppMode;
|
|
77
|
+
/** Permissions for this context (e.g. show price breakdown only for staff). */
|
|
56
78
|
permissions?: Partial<BookingAppPermissions>;
|
|
79
|
+
/** Google Maps API key from the host (use when embedding so the key from the host build is used). */
|
|
57
80
|
googleMapsApiKey?: string;
|
|
81
|
+
/** When set (e.g. provider-dashboard), called instead of redirecting to /manage; host can show manage UI in a dialog. */
|
|
58
82
|
onShowManage?: (params: ManageParams) => void;
|
|
83
|
+
/** When set, used as Stripe return_url so payment success lands on the host. */
|
|
59
84
|
getSuccessUrl?: (params: { reservationRef: string; lastName: string; focusDate?: string }) => string;
|
|
85
|
+
/** When false, the language selector is hidden. Default true. */
|
|
60
86
|
showLanguageSelector?: boolean;
|
|
87
|
+
/** When true, calendar date selection does not auto-scroll the page/dialog. */
|
|
61
88
|
suppressCalendarDateScroll?: boolean;
|
|
62
89
|
}
|
|
63
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Provides the current "mode" (which app is using the booking app) and permissions.
|
|
93
|
+
* Wrap the booking app at the host level: standalone page or BookingWidget when embedded.
|
|
94
|
+
*/
|
|
64
95
|
export function BookingAppProvider({
|
|
65
96
|
children,
|
|
66
97
|
mode = 'standalone',
|
|
@@ -83,11 +114,21 @@ export function BookingAppProvider({
|
|
|
83
114
|
suppressCalendarDateScroll,
|
|
84
115
|
};
|
|
85
116
|
|
|
86
|
-
return
|
|
117
|
+
return (
|
|
118
|
+
<BookingAppContext.Provider value={value}>
|
|
119
|
+
{children}
|
|
120
|
+
</BookingAppContext.Provider>
|
|
121
|
+
);
|
|
87
122
|
}
|
|
88
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Hook to access booking app context (mode and permissions).
|
|
126
|
+
* Use this to gate features like the price breakdown tooltip based on who is using the app.
|
|
127
|
+
*/
|
|
89
128
|
export function useBookingApp(): BookingAppContextValue {
|
|
90
129
|
const context = useContext(BookingAppContext);
|
|
91
|
-
if (context === undefined)
|
|
130
|
+
if (context === undefined) {
|
|
131
|
+
return DEFAULT_STANDALONE;
|
|
132
|
+
}
|
|
92
133
|
return context;
|
|
93
134
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { createContext, useContext, useState, useEffect,
|
|
4
|
-
import { getCompany, type Company } from '@/lib/api';
|
|
3
|
+
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
|
4
|
+
import { getCompany, type Company } from '@/lib/booking-api';
|
|
5
5
|
import { ENV } from '@/lib/env';
|
|
6
6
|
|
|
7
7
|
interface CompanyContextType {
|
|
@@ -14,41 +14,56 @@ const CompanyContext = createContext<CompanyContextType | undefined>(undefined);
|
|
|
14
14
|
|
|
15
15
|
interface CompanyProviderProps {
|
|
16
16
|
children: ReactNode;
|
|
17
|
-
/** Optional company ID. When provided (e.g. from provider dashboard), use instead of ENV.COMPANY_ID. */
|
|
18
|
-
companyId?: string;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Provider component that fetches company data once and makes it available via context
|
|
21
|
+
*/
|
|
22
|
+
export function CompanyProvider({ children }: CompanyProviderProps) {
|
|
22
23
|
const [company, setCompany] = useState<Company | null>(null);
|
|
23
24
|
const [loading, setLoading] = useState(true);
|
|
24
25
|
const [error, setError] = useState<string | null>(null);
|
|
25
|
-
const companyId = companyIdProp ?? ENV.COMPANY_ID;
|
|
26
26
|
|
|
27
27
|
useEffect(() => {
|
|
28
28
|
async function fetchCompany() {
|
|
29
29
|
try {
|
|
30
|
-
const companyData = await getCompany(
|
|
30
|
+
const companyData = await getCompany(ENV.COMPANY_ID);
|
|
31
31
|
setCompany(companyData);
|
|
32
32
|
setError(null);
|
|
33
33
|
} catch (err) {
|
|
34
34
|
console.warn('Failed to fetch company data, using defaults:', err);
|
|
35
35
|
setError(err instanceof Error ? err.message : 'Failed to fetch company');
|
|
36
|
+
// Don't set company to null - let components use defaults
|
|
36
37
|
} finally {
|
|
37
38
|
setLoading(false);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
41
|
+
|
|
40
42
|
fetchCompany();
|
|
41
|
-
}, [
|
|
43
|
+
}, []);
|
|
42
44
|
|
|
43
|
-
return
|
|
45
|
+
return (
|
|
46
|
+
<CompanyContext.Provider value={{ company, loading, error }}>
|
|
47
|
+
{children}
|
|
48
|
+
</CompanyContext.Provider>
|
|
49
|
+
);
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Hook to access company context
|
|
54
|
+
* Returns company data, loading state, and error
|
|
55
|
+
*/
|
|
46
56
|
export function useCompany() {
|
|
47
57
|
const context = useContext(CompanyContext);
|
|
48
|
-
if (context === undefined)
|
|
58
|
+
if (context === undefined) {
|
|
59
|
+
throw new Error('useCompany must be used within a CompanyProvider');
|
|
60
|
+
}
|
|
49
61
|
return context;
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Hook to get company timezone with fallback
|
|
66
|
+
*/
|
|
52
67
|
export function useCompanyTimezone(): string {
|
|
53
68
|
const { company } = useCompany();
|
|
54
69
|
return company?.settings?.timezone || 'America/Edmonton';
|