@burdenoff/microfe-tours 2026.601.1 → 2026.624.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/dist/tours/ToursUserRoutes.js +3 -3
- package/dist/tours/ToursUserRoutes.js.map +1 -1
- package/dist/tours/components/RouteHelpers.js +6 -6
- package/dist/tours/components/RouteHelpers.js.map +1 -1
- package/dist/tours/pages/AnalyticsDashboardPage.js +21 -21
- package/dist/tours/pages/AnalyticsDashboardPage.js.map +1 -1
- package/dist/tours/pages/MediaLibraryPage.js +18 -18
- package/dist/tours/pages/MediaLibraryPage.js.map +1 -1
- package/dist/tours/pages/MyToursPage.js +10 -10
- package/dist/tours/pages/MyToursPage.js.map +1 -1
- package/dist/tours/pages/MyToursProgressPage.js +11 -11
- package/dist/tours/pages/MyToursProgressPage.js.map +1 -1
- package/dist/tours/pages/SchedulerPage.js +15 -15
- package/dist/tours/pages/SchedulerPage.js.map +1 -1
- package/dist/tours/pages/StepEditorPage.js +47 -47
- package/dist/tours/pages/StepEditorPage.js.map +1 -1
- package/dist/tours/pages/SystemAnalyticsPage.js +17 -17
- package/dist/tours/pages/SystemAnalyticsPage.js.map +1 -1
- package/dist/tours/pages/TemplateLibraryPage.js +19 -19
- package/dist/tours/pages/TemplateLibraryPage.js.map +1 -1
- package/dist/tours/pages/TourBuilderPage.js +39 -39
- package/dist/tours/pages/TourBuilderPage.js.map +1 -1
- package/dist/tours/pages/TourPreviewPage.js +26 -26
- package/dist/tours/pages/TourPreviewPage.js.map +1 -1
- package/dist/tours/pages/ToursListPage.js +24 -24
- package/dist/tours/pages/ToursListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MyToursPage.js","names":[],"sources":["../../../src/tours/pages/MyToursPage.tsx"],"sourcesContent":["import { useEffect, useRef } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { CheckCircle2, Circle, MinusCircle, Play } from 'lucide-react';\nimport {\n useGetToursForContextQuery,\n type GetToursForContextQuery,\n} from '../../generated/global-operations';\nimport { useToursContext } from '../providers/ToursProvider';\nimport { useToursI18n, type ToursTranslationKey } from '../i18n';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\n\ntype TourSummary = GetToursForContextQuery['toursForContext'][number];\n\n// Plain slug validator for productId and moduleId.\nconst SLUG_PATTERN = /^[a-zA-Z0-9_-]+$/;\n\n// pageId is documented with path-style values like \"settings/team\" per\n// ToursRootProps — tighten to URL-safe characters only, but permit `/`.\nconst PAGE_PATH_PATTERN = /^[a-zA-Z0-9_\\-/]+$/;\n\nfunction safeSlug<T extends string | undefined>(\n raw: string | null | undefined,\n fallback: T\n): string | T {\n if (raw && SLUG_PATTERN.test(raw)) return raw;\n return fallback;\n}\n\nfunction safePageId<T extends string | undefined>(\n raw: string | null | undefined,\n fallback: T\n): string | T {\n if (raw && PAGE_PATH_PATTERN.test(raw)) return raw;\n return fallback;\n}\n\n/**\n * MyToursPage — user-facing browse for available tours.\n *\n * Lists tours returned by `toursForContext` (filtered to the current\n * product/module/page surface) and decorates each with the requesting\n * user's `TourUserProgress` status (Completed / In progress / Skipped /\n * Not started). Read-only — tours are actually played via the SDK overlay,\n * not via mutations on this page.\n */\nexport function MyToursPage() {\n const { tr } = useToursI18n();\n const {\n productId: shellProductId,\n moduleId: shellModuleId,\n pageId: shellPageId,\n workspaceId,\n } = useToursContext();\n const [searchParams] = useSearchParams();\n const eventBus = useEventBus();\n\n // Emit page view event exactly once per mount. A ref guard keeps the effect\n // semantically mount-only while satisfying exhaustive-deps with the real\n // dependency list, so we don't have to disable the lint rule.\n const pageViewedRef = useRef(false);\n useEffect(() => {\n if (pageViewedRef.current) return;\n pageViewedRef.current = true;\n eventBus.emit('tours.page.viewed', {\n page: '/tours',\n productId: shellProductId,\n moduleId: shellModuleId,\n pageId: shellPageId,\n workspaceId,\n });\n }, [eventBus, shellProductId, shellModuleId, shellPageId, workspaceId]);\n\n // Preferred source is the shell's productId prop (the host app knows\n // which product it is — \"workspaces\", \"vibecontrols\", \"bigconsole\", etc.).\n // Fall back to a query-param override for deep-link scenarios, but only\n // after passing a plain-slug validator so we never forward arbitrary\n // strings into the GraphQL variables. No hardcoded product default —\n // if neither source provides one, skip the query entirely so non-\n // workspaces integrations don't get workspaces tours.\n const productId =\n safeSlug(shellProductId, undefined) ?? safeSlug(searchParams.get('productId'), undefined);\n const moduleId =\n safeSlug(shellModuleId, undefined) ?? safeSlug(searchParams.get('moduleId'), undefined);\n const pageId =\n safePageId(shellPageId, undefined) ?? safePageId(searchParams.get('pageId'), undefined);\n\n const { data: toursData, loading: toursLoading } = useGetToursForContextQuery({\n variables: productId ? { context: { productId, moduleId, pageId } } : undefined,\n skip: !productId,\n fetchPolicy: 'cache-and-network',\n });\n\n const tours = toursData?.toursForContext ?? [];\n const isLoading = toursLoading;\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-
|
|
1
|
+
{"version":3,"file":"MyToursPage.js","names":[],"sources":["../../../src/tours/pages/MyToursPage.tsx"],"sourcesContent":["import { useEffect, useRef } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { CheckCircle2, Circle, MinusCircle, Play } from 'lucide-react';\nimport {\n useGetToursForContextQuery,\n type GetToursForContextQuery,\n} from '../../generated/global-operations';\nimport { useToursContext } from '../providers/ToursProvider';\nimport { useToursI18n, type ToursTranslationKey } from '../i18n';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\n\ntype TourSummary = GetToursForContextQuery['toursForContext'][number];\n\n// Plain slug validator for productId and moduleId.\nconst SLUG_PATTERN = /^[a-zA-Z0-9_-]+$/;\n\n// pageId is documented with path-style values like \"settings/team\" per\n// ToursRootProps — tighten to URL-safe characters only, but permit `/`.\nconst PAGE_PATH_PATTERN = /^[a-zA-Z0-9_\\-/]+$/;\n\nfunction safeSlug<T extends string | undefined>(\n raw: string | null | undefined,\n fallback: T\n): string | T {\n if (raw && SLUG_PATTERN.test(raw)) return raw;\n return fallback;\n}\n\nfunction safePageId<T extends string | undefined>(\n raw: string | null | undefined,\n fallback: T\n): string | T {\n if (raw && PAGE_PATH_PATTERN.test(raw)) return raw;\n return fallback;\n}\n\n/**\n * MyToursPage — user-facing browse for available tours.\n *\n * Lists tours returned by `toursForContext` (filtered to the current\n * product/module/page surface) and decorates each with the requesting\n * user's `TourUserProgress` status (Completed / In progress / Skipped /\n * Not started). Read-only — tours are actually played via the SDK overlay,\n * not via mutations on this page.\n */\nexport function MyToursPage() {\n const { tr } = useToursI18n();\n const {\n productId: shellProductId,\n moduleId: shellModuleId,\n pageId: shellPageId,\n workspaceId,\n } = useToursContext();\n const [searchParams] = useSearchParams();\n const eventBus = useEventBus();\n\n // Emit page view event exactly once per mount. A ref guard keeps the effect\n // semantically mount-only while satisfying exhaustive-deps with the real\n // dependency list, so we don't have to disable the lint rule.\n const pageViewedRef = useRef(false);\n useEffect(() => {\n if (pageViewedRef.current) return;\n pageViewedRef.current = true;\n eventBus.emit('tours.page.viewed', {\n page: '/tours',\n productId: shellProductId,\n moduleId: shellModuleId,\n pageId: shellPageId,\n workspaceId,\n });\n }, [eventBus, shellProductId, shellModuleId, shellPageId, workspaceId]);\n\n // Preferred source is the shell's productId prop (the host app knows\n // which product it is — \"workspaces\", \"vibecontrols\", \"bigconsole\", etc.).\n // Fall back to a query-param override for deep-link scenarios, but only\n // after passing a plain-slug validator so we never forward arbitrary\n // strings into the GraphQL variables. No hardcoded product default —\n // if neither source provides one, skip the query entirely so non-\n // workspaces integrations don't get workspaces tours.\n const productId =\n safeSlug(shellProductId, undefined) ?? safeSlug(searchParams.get('productId'), undefined);\n const moduleId =\n safeSlug(shellModuleId, undefined) ?? safeSlug(searchParams.get('moduleId'), undefined);\n const pageId =\n safePageId(shellPageId, undefined) ?? safePageId(searchParams.get('pageId'), undefined);\n\n const { data: toursData, loading: toursLoading } = useGetToursForContextQuery({\n variables: productId ? { context: { productId, moduleId, pageId } } : undefined,\n skip: !productId,\n fetchPolicy: 'cache-and-network',\n });\n\n const tours = toursData?.toursForContext ?? [];\n const isLoading = toursLoading;\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-text-primary\">{tr('myTours.title')}</h1>\n <p className=\"text-sm sm:text-base text-text-secondary\">{tr('myTours.subtitle')}</p>\n </div>\n\n {isLoading && tours.length === 0 ? (\n <output className=\"text-sm text-text-secondary\">{tr('myTours.loading')}</output>\n ) : tours.length === 0 ? (\n <div className=\"rounded-lg border border-dashed border-border-subtle p-8 text-center\">\n <output className=\"block text-sm text-text-secondary\">{tr('myTours.empty')}</output>\n </div>\n ) : (\n <ul className=\"grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n {tours.map((tour: TourSummary) => (\n <li\n key={tour.id}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 flex flex-col gap-2\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <h2 className=\"font-semibold text-text-primary line-clamp-2\">{tour.name}</h2>\n <ProgressBadge status={tour.myProgress?.status} tr={tr} />\n </div>\n {tour.description ? (\n <p className=\"text-sm text-text-secondary line-clamp-3\">{tour.description}</p>\n ) : null}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction ProgressBadge({\n status,\n tr,\n}: {\n status: string | undefined;\n tr: (key: ToursTranslationKey) => string;\n}) {\n switch (status) {\n case 'COMPLETED':\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-success\">\n <CheckCircle2 size={14} />\n {tr('myTours.status.completed')}\n </span>\n );\n case 'IN_PROGRESS':\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-warning\">\n <Play size={14} />\n {tr('myTours.status.inProgress')}\n </span>\n );\n case 'SKIPPED':\n case 'DISMISSED':\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-secondary\">\n <MinusCircle size={14} />\n {tr('myTours.status.skipped')}\n </span>\n );\n default:\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-secondary\">\n <Circle size={14} />\n {tr('myTours.status.notStarted')}\n </span>\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -51,13 +51,13 @@ function p() {
|
|
|
51
51
|
return /* @__PURE__ */ d("div", {
|
|
52
52
|
className: "space-y-4 sm:space-y-6 p-3 sm:p-6",
|
|
53
53
|
children: [/* @__PURE__ */ d("div", { children: [/* @__PURE__ */ u("h1", {
|
|
54
|
-
className: "text-xl sm:text-2xl font-bold text-
|
|
54
|
+
className: "text-xl sm:text-2xl font-bold text-text-primary",
|
|
55
55
|
children: p("myToursProgress.title")
|
|
56
56
|
}), /* @__PURE__ */ u("p", {
|
|
57
|
-
className: "text-sm sm:text-base text-
|
|
57
|
+
className: "text-sm sm:text-base text-text-secondary",
|
|
58
58
|
children: p("myToursProgress.subtitle")
|
|
59
59
|
})] }), C && (S?.userTourProgresses?.length ?? 0) === 0 ? /* @__PURE__ */ u("output", {
|
|
60
|
-
className: "text-sm text-
|
|
60
|
+
className: "text-sm text-text-secondary",
|
|
61
61
|
children: p("myToursProgress.loading")
|
|
62
62
|
}) : /* @__PURE__ */ d("div", {
|
|
63
63
|
className: "space-y-6",
|
|
@@ -84,7 +84,7 @@ function p() {
|
|
|
84
84
|
title: p("myToursProgress.skipped"),
|
|
85
85
|
icon: /* @__PURE__ */ u(c, {
|
|
86
86
|
size: 16,
|
|
87
|
-
className: "text-
|
|
87
|
+
className: "text-text-secondary"
|
|
88
88
|
}),
|
|
89
89
|
rows: w.SKIPPED,
|
|
90
90
|
emptyText: p("myToursProgress.emptySkipped")
|
|
@@ -93,7 +93,7 @@ function p() {
|
|
|
93
93
|
title: p("myToursProgress.other"),
|
|
94
94
|
icon: /* @__PURE__ */ u(s, {
|
|
95
95
|
size: 16,
|
|
96
|
-
className: "text-
|
|
96
|
+
className: "text-text-secondary"
|
|
97
97
|
}),
|
|
98
98
|
rows: w.OTHER,
|
|
99
99
|
emptyText: p("myToursProgress.emptyOther")
|
|
@@ -106,12 +106,12 @@ function m({ title: e, icon: t, rows: n, emptyText: r }) {
|
|
|
106
106
|
return /* @__PURE__ */ d("section", {
|
|
107
107
|
className: "space-y-2",
|
|
108
108
|
children: [/* @__PURE__ */ d("h2", {
|
|
109
|
-
className: "flex items-center gap-2 text-sm font-semibold text-
|
|
109
|
+
className: "flex items-center gap-2 text-sm font-semibold text-text-primary",
|
|
110
110
|
children: [
|
|
111
111
|
t,
|
|
112
112
|
e,
|
|
113
113
|
/* @__PURE__ */ d("span", {
|
|
114
|
-
className: "text-xs text-
|
|
114
|
+
className: "text-xs text-text-secondary font-normal",
|
|
115
115
|
children: [
|
|
116
116
|
"(",
|
|
117
117
|
n.length,
|
|
@@ -120,19 +120,19 @@ function m({ title: e, icon: t, rows: n, emptyText: r }) {
|
|
|
120
120
|
})
|
|
121
121
|
]
|
|
122
122
|
}), n.length === 0 ? /* @__PURE__ */ u("p", {
|
|
123
|
-
className: "text-sm text-
|
|
123
|
+
className: "text-sm text-text-secondary",
|
|
124
124
|
role: "status",
|
|
125
125
|
"aria-live": "polite",
|
|
126
126
|
children: r
|
|
127
127
|
}) : /* @__PURE__ */ u("ul", {
|
|
128
|
-
className: "divide-y divide-border rounded-md border border-border",
|
|
128
|
+
className: "divide-y divide-border rounded-md border border-border-subtle",
|
|
129
129
|
children: n.map((e) => /* @__PURE__ */ d("li", {
|
|
130
130
|
className: "flex items-center justify-between gap-3 p-3",
|
|
131
131
|
children: [/* @__PURE__ */ u("span", {
|
|
132
|
-
className: "text-sm text-
|
|
132
|
+
className: "text-sm text-text-primary line-clamp-1",
|
|
133
133
|
children: e.tour.name
|
|
134
134
|
}), e.lastViewedAt ? /* @__PURE__ */ u("time", {
|
|
135
|
-
className: "text-xs text-
|
|
135
|
+
className: "text-xs text-text-secondary",
|
|
136
136
|
dateTime: e.lastViewedAt,
|
|
137
137
|
children: new Date(e.lastViewedAt).toLocaleDateString()
|
|
138
138
|
}) : null]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MyToursProgressPage.js","names":[],"sources":["../../../src/tours/pages/MyToursProgressPage.tsx"],"sourcesContent":["import { useMemo, useEffect, useRef } from 'react';\nimport { CheckCircle2, HelpCircle, MinusCircle, Play } from 'lucide-react';\nimport { useGetUserTourProgressesQuery } from '../../generated/global-operations';\nimport { useToursContext } from '../providers/ToursProvider';\nimport { useToursI18n } from '../i18n';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\n\ntype ProgressGroup = 'IN_PROGRESS' | 'COMPLETED' | 'SKIPPED' | 'OTHER';\n\ntype ProgressRow = {\n id: string;\n status: string;\n tour: { id: string; name: string; productId?: string; moduleId?: string; pageId?: string };\n lastViewedAt?: string | null;\n};\n\n/**\n * MyToursProgressPage — user-facing read-only view of the current user's\n * tour interactions, grouped by status (In progress / Completed / Skipped).\n */\nexport function MyToursProgressPage() {\n const { tr } = useToursI18n();\n const {\n currentUser,\n productId: shellProductId,\n moduleId: shellModuleId,\n pageId: shellPageId,\n workspaceId,\n } = useToursContext();\n const eventBus = useEventBus();\n\n // Emit page view event exactly once per mount. A ref guard keeps the effect\n // semantically mount-only while satisfying exhaustive-deps with the real\n // dependency list, so we don't have to disable the lint rule.\n const pageViewedRef = useRef(false);\n useEffect(() => {\n if (pageViewedRef.current) return;\n pageViewedRef.current = true;\n eventBus.emit('tours.page.viewed', {\n page: '/tours/progress',\n productId: shellProductId,\n moduleId: shellModuleId,\n pageId: shellPageId,\n workspaceId,\n });\n }, [eventBus, shellProductId, shellModuleId, shellPageId, workspaceId]);\n\n const { data, loading } = useGetUserTourProgressesQuery({\n variables: { userId: currentUser?.id, pagination: { limit: 100, offset: 0 } },\n skip: !currentUser?.id,\n fetchPolicy: 'cache-and-network',\n });\n\n const grouped = useMemo(() => {\n const result: Record<ProgressGroup, NonNullable<typeof data>['userTourProgresses']> = {\n IN_PROGRESS: [],\n COMPLETED: [],\n SKIPPED: [],\n OTHER: [],\n };\n // Filter the raw progress list to the shell's product/module/page context\n // so a user's vibecontrols session doesn't see workspaces / bigconsole\n // progress rows. `userTourProgresses` returns everything scoped by userId;\n // this MFE is mounted per-shell so we narrow to what the shell advertises.\n // When the shell didn't supply a filter, the corresponding level is not\n // checked (e.g., no shellModuleId → accept any moduleId).\n const rows = (data?.userTourProgresses ?? []).filter((row: ProgressRow) => {\n if (shellProductId && row.tour.productId !== shellProductId) return false;\n if (shellModuleId && row.tour.moduleId && row.tour.moduleId !== shellModuleId) return false;\n if (shellPageId && row.tour.pageId && row.tour.pageId !== shellPageId) return false;\n return true;\n });\n for (const row of rows) {\n if (row.status === 'COMPLETED') result.COMPLETED.push(row);\n else if (row.status === 'IN_PROGRESS') result.IN_PROGRESS.push(row);\n else if (row.status === 'SKIPPED' || row.status === 'DISMISSED') result.SKIPPED.push(row);\n // Unknown / future status values fall through here so nothing is\n // silently dropped if the backend introduces a new state.\n else result.OTHER.push(row);\n }\n return result;\n }, [data, shellProductId, shellModuleId, shellPageId]);\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-
|
|
1
|
+
{"version":3,"file":"MyToursProgressPage.js","names":[],"sources":["../../../src/tours/pages/MyToursProgressPage.tsx"],"sourcesContent":["import { useMemo, useEffect, useRef } from 'react';\nimport { CheckCircle2, HelpCircle, MinusCircle, Play } from 'lucide-react';\nimport { useGetUserTourProgressesQuery } from '../../generated/global-operations';\nimport { useToursContext } from '../providers/ToursProvider';\nimport { useToursI18n } from '../i18n';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\n\ntype ProgressGroup = 'IN_PROGRESS' | 'COMPLETED' | 'SKIPPED' | 'OTHER';\n\ntype ProgressRow = {\n id: string;\n status: string;\n tour: { id: string; name: string; productId?: string; moduleId?: string; pageId?: string };\n lastViewedAt?: string | null;\n};\n\n/**\n * MyToursProgressPage — user-facing read-only view of the current user's\n * tour interactions, grouped by status (In progress / Completed / Skipped).\n */\nexport function MyToursProgressPage() {\n const { tr } = useToursI18n();\n const {\n currentUser,\n productId: shellProductId,\n moduleId: shellModuleId,\n pageId: shellPageId,\n workspaceId,\n } = useToursContext();\n const eventBus = useEventBus();\n\n // Emit page view event exactly once per mount. A ref guard keeps the effect\n // semantically mount-only while satisfying exhaustive-deps with the real\n // dependency list, so we don't have to disable the lint rule.\n const pageViewedRef = useRef(false);\n useEffect(() => {\n if (pageViewedRef.current) return;\n pageViewedRef.current = true;\n eventBus.emit('tours.page.viewed', {\n page: '/tours/progress',\n productId: shellProductId,\n moduleId: shellModuleId,\n pageId: shellPageId,\n workspaceId,\n });\n }, [eventBus, shellProductId, shellModuleId, shellPageId, workspaceId]);\n\n const { data, loading } = useGetUserTourProgressesQuery({\n variables: { userId: currentUser?.id, pagination: { limit: 100, offset: 0 } },\n skip: !currentUser?.id,\n fetchPolicy: 'cache-and-network',\n });\n\n const grouped = useMemo(() => {\n const result: Record<ProgressGroup, NonNullable<typeof data>['userTourProgresses']> = {\n IN_PROGRESS: [],\n COMPLETED: [],\n SKIPPED: [],\n OTHER: [],\n };\n // Filter the raw progress list to the shell's product/module/page context\n // so a user's vibecontrols session doesn't see workspaces / bigconsole\n // progress rows. `userTourProgresses` returns everything scoped by userId;\n // this MFE is mounted per-shell so we narrow to what the shell advertises.\n // When the shell didn't supply a filter, the corresponding level is not\n // checked (e.g., no shellModuleId → accept any moduleId).\n const rows = (data?.userTourProgresses ?? []).filter((row: ProgressRow) => {\n if (shellProductId && row.tour.productId !== shellProductId) return false;\n if (shellModuleId && row.tour.moduleId && row.tour.moduleId !== shellModuleId) return false;\n if (shellPageId && row.tour.pageId && row.tour.pageId !== shellPageId) return false;\n return true;\n });\n for (const row of rows) {\n if (row.status === 'COMPLETED') result.COMPLETED.push(row);\n else if (row.status === 'IN_PROGRESS') result.IN_PROGRESS.push(row);\n else if (row.status === 'SKIPPED' || row.status === 'DISMISSED') result.SKIPPED.push(row);\n // Unknown / future status values fall through here so nothing is\n // silently dropped if the backend introduces a new state.\n else result.OTHER.push(row);\n }\n return result;\n }, [data, shellProductId, shellModuleId, shellPageId]);\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-text-primary\">\n {tr('myToursProgress.title')}\n </h1>\n <p className=\"text-sm sm:text-base text-text-secondary\">\n {tr('myToursProgress.subtitle')}\n </p>\n </div>\n\n {loading && (data?.userTourProgresses?.length ?? 0) === 0 ? (\n <output className=\"text-sm text-text-secondary\">{tr('myToursProgress.loading')}</output>\n ) : (\n <div className=\"space-y-6\">\n <ProgressSection\n title={tr('myToursProgress.inProgress')}\n icon={<Play size={16} className=\"text-warning\" />}\n rows={grouped.IN_PROGRESS}\n emptyText={tr('myToursProgress.emptyInProgress')}\n />\n <ProgressSection\n title={tr('myToursProgress.completed')}\n icon={<CheckCircle2 size={16} className=\"text-success\" />}\n rows={grouped.COMPLETED}\n emptyText={tr('myToursProgress.emptyCompleted')}\n />\n <ProgressSection\n title={tr('myToursProgress.skipped')}\n icon={<MinusCircle size={16} className=\"text-text-secondary\" />}\n rows={grouped.SKIPPED}\n emptyText={tr('myToursProgress.emptySkipped')}\n />\n {grouped.OTHER.length > 0 ? (\n <ProgressSection\n title={tr('myToursProgress.other')}\n icon={<HelpCircle size={16} className=\"text-text-secondary\" />}\n rows={grouped.OTHER}\n emptyText={tr('myToursProgress.emptyOther')}\n />\n ) : null}\n </div>\n )}\n </div>\n );\n}\n\nfunction ProgressSection({\n title,\n icon,\n rows,\n emptyText,\n}: {\n title: string;\n icon: React.ReactNode;\n rows: Array<{ id: string; tour: { id: string; name: string }; lastViewedAt?: string | null }>;\n emptyText: string;\n}) {\n return (\n <section className=\"space-y-2\">\n <h2 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n {icon}\n {title}\n <span className=\"text-xs text-text-secondary font-normal\">({rows.length})</span>\n </h2>\n {rows.length === 0 ? (\n <p className=\"text-sm text-text-secondary\" role=\"status\" aria-live=\"polite\">\n {emptyText}\n </p>\n ) : (\n <ul className=\"divide-y divide-border rounded-md border border-border-subtle\">\n {rows.map((row) => (\n <li key={row.id} className=\"flex items-center justify-between gap-3 p-3\">\n <span className=\"text-sm text-text-primary line-clamp-1\">{row.tour.name}</span>\n {row.lastViewedAt ? (\n <time className=\"text-xs text-text-secondary\" dateTime={row.lastViewedAt}>\n {new Date(row.lastViewedAt).toLocaleDateString()}\n </time>\n ) : null}\n </li>\n ))}\n </ul>\n )}\n </section>\n );\n}\n"],"mappings":"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"}
|
|
@@ -63,24 +63,24 @@ function y() {
|
|
|
63
63
|
className: "text-xl sm:text-2xl font-bold",
|
|
64
64
|
children: b("tourScheduler.title", "Schedule Tour")
|
|
65
65
|
}), /* @__PURE__ */ p("p", {
|
|
66
|
-
className: "text-sm text-
|
|
66
|
+
className: "text-sm text-text-secondary",
|
|
67
67
|
children: b("tourScheduler.subtitle", "Configure when this tour should be activated")
|
|
68
68
|
})] })]
|
|
69
69
|
}),
|
|
70
70
|
F.length > 0 && /* @__PURE__ */ m("div", {
|
|
71
|
-
className: "border border-border rounded-md p-4 sm:p-6 bg-
|
|
71
|
+
className: "border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface",
|
|
72
72
|
children: [/* @__PURE__ */ p("h2", {
|
|
73
73
|
className: "text-lg font-semibold mb-4",
|
|
74
74
|
children: b("tourScheduler.existingSchedules", "Existing Schedules")
|
|
75
75
|
}), /* @__PURE__ */ p("div", {
|
|
76
76
|
className: "space-y-2",
|
|
77
77
|
children: F.map((e) => /* @__PURE__ */ m("div", {
|
|
78
|
-
className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between p-3 border border-border rounded-md",
|
|
78
|
+
className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between p-3 border border-border-subtle rounded-md",
|
|
79
79
|
children: [/* @__PURE__ */ m("div", { children: [/* @__PURE__ */ m("div", {
|
|
80
80
|
className: "text-sm font-medium",
|
|
81
81
|
children: [new Date(e.startDate).toLocaleDateString(), e.endDate ? ` — ${new Date(e.endDate).toLocaleDateString()}` : ` — ${b("tourScheduler.noEndDate", "No end date")}`]
|
|
82
82
|
}), /* @__PURE__ */ m("div", {
|
|
83
|
-
className: "text-xs text-
|
|
83
|
+
className: "text-xs text-text-secondary",
|
|
84
84
|
children: [
|
|
85
85
|
e.recurrenceRule || b("tourScheduler.oneTime", "One-time"),
|
|
86
86
|
" ·",
|
|
@@ -89,7 +89,7 @@ function y() {
|
|
|
89
89
|
e.isActive ? ` · ${b("tourScheduler.active", "Active")}` : ` · ${b("tourScheduler.inactive", "Inactive")}`
|
|
90
90
|
]
|
|
91
91
|
})] }), /* @__PURE__ */ m("div", {
|
|
92
|
-
className: "text-xs text-
|
|
92
|
+
className: "text-xs text-text-secondary",
|
|
93
93
|
children: [
|
|
94
94
|
b("tourScheduler.runs", "Runs"),
|
|
95
95
|
": ",
|
|
@@ -104,7 +104,7 @@ function y() {
|
|
|
104
104
|
className: "space-y-6",
|
|
105
105
|
children: [
|
|
106
106
|
/* @__PURE__ */ m("div", {
|
|
107
|
-
className: "border border-border rounded-md p-4 sm:p-6 bg-
|
|
107
|
+
className: "border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface",
|
|
108
108
|
children: [/* @__PURE__ */ m("h2", {
|
|
109
109
|
className: "text-lg font-semibold mb-4 flex items-center gap-2",
|
|
110
110
|
children: [/* @__PURE__ */ p(u, { size: 20 }), b("tourScheduler.sectionDateRange", "Date Range")]
|
|
@@ -122,7 +122,7 @@ function y() {
|
|
|
122
122
|
...k,
|
|
123
123
|
startDate: e.target.value
|
|
124
124
|
}),
|
|
125
|
-
className: "w-full px-3 py-2 min-h-[44px] border border-border rounded-md bg-
|
|
125
|
+
className: "w-full px-3 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface",
|
|
126
126
|
required: !0,
|
|
127
127
|
"aria-label": b("tourScheduler.labelStartDate", "Start Date *")
|
|
128
128
|
})] }), /* @__PURE__ */ m("div", { children: [/* @__PURE__ */ p("label", {
|
|
@@ -137,13 +137,13 @@ function y() {
|
|
|
137
137
|
...k,
|
|
138
138
|
endDate: e.target.value
|
|
139
139
|
}),
|
|
140
|
-
className: "w-full px-3 py-2 min-h-[44px] border border-border rounded-md bg-
|
|
140
|
+
className: "w-full px-3 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface",
|
|
141
141
|
"aria-label": b("tourScheduler.labelEndDate", "End Date (Optional)")
|
|
142
142
|
})] })]
|
|
143
143
|
})]
|
|
144
144
|
}),
|
|
145
145
|
/* @__PURE__ */ m("div", {
|
|
146
|
-
className: "border border-border rounded-md p-4 sm:p-6 bg-
|
|
146
|
+
className: "border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface",
|
|
147
147
|
children: [/* @__PURE__ */ m("h2", {
|
|
148
148
|
className: "text-lg font-semibold mb-4 flex items-center gap-2",
|
|
149
149
|
children: [/* @__PURE__ */ p(d, { size: 20 }), b("tourScheduler.sectionRecurrence", "Recurrence")]
|
|
@@ -160,7 +160,7 @@ function y() {
|
|
|
160
160
|
...k,
|
|
161
161
|
recurrencePattern: e.target.value
|
|
162
162
|
}),
|
|
163
|
-
className: "w-full px-3 py-2 min-h-[44px] border border-border rounded-md bg-
|
|
163
|
+
className: "w-full px-3 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface",
|
|
164
164
|
"aria-label": b("tourScheduler.labelRecurrencePattern", "Recurrence Pattern"),
|
|
165
165
|
children: [
|
|
166
166
|
/* @__PURE__ */ p("option", {
|
|
@@ -192,14 +192,14 @@ function y() {
|
|
|
192
192
|
...k,
|
|
193
193
|
timezone: e.target.value
|
|
194
194
|
}),
|
|
195
|
-
className: "w-full px-3 py-2 min-h-[44px] border border-border rounded-md bg-
|
|
195
|
+
className: "w-full px-3 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface",
|
|
196
196
|
readOnly: !0,
|
|
197
197
|
"aria-label": b("tourScheduler.labelTimezone", "Timezone")
|
|
198
198
|
})] })]
|
|
199
199
|
})]
|
|
200
200
|
}),
|
|
201
201
|
/* @__PURE__ */ p("div", {
|
|
202
|
-
className: "border border-border rounded-md p-4 sm:p-6 bg-
|
|
202
|
+
className: "border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface",
|
|
203
203
|
children: /* @__PURE__ */ m("label", {
|
|
204
204
|
className: "flex items-center gap-3 cursor-pointer",
|
|
205
205
|
children: [/* @__PURE__ */ p("input", {
|
|
@@ -215,7 +215,7 @@ function y() {
|
|
|
215
215
|
className: "font-medium",
|
|
216
216
|
children: b("tourScheduler.statusActiveLabel", "Active")
|
|
217
217
|
}), /* @__PURE__ */ p("div", {
|
|
218
|
-
className: "text-sm text-
|
|
218
|
+
className: "text-sm text-text-secondary",
|
|
219
219
|
children: b("tourScheduler.statusActiveDescription", "Enable this schedule immediately after creation")
|
|
220
220
|
})] })]
|
|
221
221
|
})
|
|
@@ -225,14 +225,14 @@ function y() {
|
|
|
225
225
|
children: [/* @__PURE__ */ p("button", {
|
|
226
226
|
type: "button",
|
|
227
227
|
onClick: L,
|
|
228
|
-
className: "flex-1 px-4 py-2 min-h-[44px] border border-border rounded-md hover:bg-accent",
|
|
228
|
+
className: "flex-1 px-4 py-2 min-h-[44px] border border-border-subtle rounded-md hover:bg-accent",
|
|
229
229
|
children: b("tourScheduler.cancel", "Cancel")
|
|
230
230
|
}), /* @__PURE__ */ p(c, {
|
|
231
231
|
permission: i.TOUR_SCHEDULE_CREATE,
|
|
232
232
|
children: /* @__PURE__ */ m("button", {
|
|
233
233
|
type: "submit",
|
|
234
234
|
disabled: j || !D,
|
|
235
|
-
className: "flex-1 px-4 py-2 min-h-[44px] bg-primary text-primary-
|
|
235
|
+
className: "flex-1 px-4 py-2 min-h-[44px] bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 disabled:opacity-50 flex items-center justify-center gap-2",
|
|
236
236
|
children: [/* @__PURE__ */ p(f, { size: 20 }), j ? b("tourScheduler.creating", "Creating...") : b("tourScheduler.createSchedule", "Create Schedule")]
|
|
237
237
|
})
|
|
238
238
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SchedulerPage.js","names":[],"sources":["../../../src/tours/pages/SchedulerPage.tsx"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { useParams, useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Calendar, Clock, ArrowLeft, Save } from 'lucide-react';\nimport { PermissionGate } from '@burdenoff/fe-libs/shared/components';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { useToursPermissionAccess } from '../hooks/useToursPermissionAccess';\nimport { useToursContext } from '../providers/ToursProvider';\nimport { TOUR_PERMISSIONS } from '../../constants/permissions';\nimport type { RecurrencePattern } from '../types';\nimport { emitToursApiFailed } from '../telemetry';\nimport { useCreateTourScheduleMutation } from '../../generated/global-operations';\nimport { useGetTourSchedulesQuery } from '../../generated/global-operations';\n\n/**\n * Tour scheduler page\n * Configure tour scheduling and recurrence\n */\nexport function SchedulerPage() {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n const { tourId } = useParams();\n const navigate = useNavigate();\n const toursContext = useToursContext();\n const { basePath = '/' } = toursContext;\n const eventBus = useEventBus();\n const { hasPermission } = useToursPermissionAccess();\n const canCreateSchedule = hasPermission(TOUR_PERMISSIONS.TOUR_SCHEDULE_CREATE);\n const canListSchedules = hasPermission(TOUR_PERMISSIONS.TOUR_SCHEDULE_LIST);\n\n const [scheduleData, setScheduleData] = useState({\n startDate: '',\n endDate: '',\n recurrencePattern: 'DAILY' as RecurrencePattern,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n isActive: true,\n });\n\n const [saving, setSaving] = useState(false);\n\n const [createSchedule] = useCreateTourScheduleMutation();\n const { data: schedulesData } = useGetTourSchedulesQuery({\n variables: { tourId: tourId! },\n skip: !tourId || !canListSchedules,\n });\n const existingSchedules = schedulesData?.tourSchedules ?? [];\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!tourId) return;\n setSaving(true);\n try {\n // Convert recurrence pattern to RFC 5545 recurrence rule\n const recurrenceRule =\n scheduleData.recurrencePattern === 'DAILY'\n ? 'FREQ=DAILY'\n : scheduleData.recurrencePattern === 'WEEKLY'\n ? 'FREQ=WEEKLY'\n : scheduleData.recurrencePattern === 'MONTHLY'\n ? 'FREQ=MONTHLY'\n : undefined;\n\n await createSchedule({\n variables: {\n input: {\n tourId,\n startDate: new Date(scheduleData.startDate).toISOString(),\n endDate: scheduleData.endDate\n ? new Date(scheduleData.endDate).toISOString()\n : undefined,\n recurrenceRule,\n timezone: scheduleData.timezone,\n },\n },\n refetchQueries: ['GetTourSchedules'],\n });\n navigate(`${basePath}/${tourId}/edit`);\n } catch {\n emitToursApiFailed(eventBus, 'schedule.create', toursContext);\n } finally {\n setSaving(false);\n }\n };\n\n const handleBack = () => {\n navigate(`${basePath}/${tourId}/edit`);\n };\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6 max-w-3xl mx-auto\">\n {/* Header */}\n <div className=\"flex items-center gap-3 sm:gap-4\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"p-2 min-h-[44px] min-w-[44px] hover:bg-accent rounded-md\"\n aria-label={tr('tourScheduler.goBack', 'Go back')}\n >\n <ArrowLeft size={20} />\n </button>\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold\">\n {tr('tourScheduler.title', 'Schedule Tour')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {tr('tourScheduler.subtitle', 'Configure when this tour should be activated')}\n </p>\n </div>\n </div>\n\n {/* Existing Schedules */}\n {existingSchedules.length > 0 && (\n <div className=\"border border-border rounded-md p-4 sm:p-6 bg-card\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {tr('tourScheduler.existingSchedules', 'Existing Schedules')}\n </h2>\n <div className=\"space-y-2\">\n {existingSchedules.map((schedule: (typeof existingSchedules)[number]) => (\n <div\n key={schedule.id}\n className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between p-3 border border-border rounded-md\"\n >\n <div>\n <div className=\"text-sm font-medium\">\n {new Date(schedule.startDate).toLocaleDateString()}\n {schedule.endDate\n ? ` — ${new Date(schedule.endDate).toLocaleDateString()}`\n : ` — ${tr('tourScheduler.noEndDate', 'No end date')}`}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {schedule.recurrenceRule || tr('tourScheduler.oneTime', 'One-time')} ·{' '}\n {schedule.timezone}\n {schedule.isActive\n ? ` · ${tr('tourScheduler.active', 'Active')}`\n : ` · ${tr('tourScheduler.inactive', 'Inactive')}`}\n </div>\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {tr('tourScheduler.runs', 'Runs')}: {schedule.executionCount}\n </div>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {/* Form */}\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Date Range */}\n <div className=\"border border-border rounded-md p-4 sm:p-6 bg-card\">\n <h2 className=\"text-lg font-semibold mb-4 flex items-center gap-2\">\n <Calendar size={20} />\n {tr('tourScheduler.sectionDateRange', 'Date Range')}\n </h2>\n\n <div className=\"space-y-4\">\n <div>\n <label htmlFor=\"scheduler-start-date\" className=\"block text-sm font-medium mb-2\">\n {tr('tourScheduler.labelStartDate', 'Start Date *')}\n </label>\n <input\n id=\"scheduler-start-date\"\n type=\"datetime-local\"\n value={scheduleData.startDate}\n onChange={(e) => setScheduleData({ ...scheduleData, startDate: e.target.value })}\n className=\"w-full px-3 py-2 min-h-[44px] border border-border rounded-md bg-background\"\n required\n aria-label={tr('tourScheduler.labelStartDate', 'Start Date *')}\n />\n </div>\n\n <div>\n <label htmlFor=\"scheduler-end-date\" className=\"block text-sm font-medium mb-2\">\n {tr('tourScheduler.labelEndDate', 'End Date (Optional)')}\n </label>\n <input\n id=\"scheduler-end-date\"\n type=\"datetime-local\"\n value={scheduleData.endDate}\n onChange={(e) => setScheduleData({ ...scheduleData, endDate: e.target.value })}\n className=\"w-full px-3 py-2 min-h-[44px] border border-border rounded-md bg-background\"\n aria-label={tr('tourScheduler.labelEndDate', 'End Date (Optional)')}\n />\n </div>\n </div>\n </div>\n\n {/* Recurrence */}\n <div className=\"border border-border rounded-md p-4 sm:p-6 bg-card\">\n <h2 className=\"text-lg font-semibold mb-4 flex items-center gap-2\">\n <Clock size={20} />\n {tr('tourScheduler.sectionRecurrence', 'Recurrence')}\n </h2>\n\n <div className=\"space-y-4\">\n <div>\n <label htmlFor=\"scheduler-recurrence\" className=\"block text-sm font-medium mb-2\">\n {tr('tourScheduler.labelRecurrencePattern', 'Recurrence Pattern')}\n </label>\n <select\n id=\"scheduler-recurrence\"\n value={scheduleData.recurrencePattern}\n onChange={(e) =>\n setScheduleData({\n ...scheduleData,\n recurrencePattern: e.target.value as RecurrencePattern,\n })\n }\n className=\"w-full px-3 py-2 min-h-[44px] border border-border rounded-md bg-background\"\n aria-label={tr('tourScheduler.labelRecurrencePattern', 'Recurrence Pattern')}\n >\n <option value=\"DAILY\">{tr('tourScheduler.recurrenceDaily', 'Daily')}</option>\n <option value=\"WEEKLY\">{tr('tourScheduler.recurrenceWeekly', 'Weekly')}</option>\n <option value=\"MONTHLY\">{tr('tourScheduler.recurrenceMonthly', 'Monthly')}</option>\n <option value=\"CUSTOM\">{tr('tourScheduler.recurrenceCustom', 'Custom')}</option>\n </select>\n </div>\n\n <div>\n <label htmlFor=\"scheduler-timezone\" className=\"block text-sm font-medium mb-2\">\n {tr('tourScheduler.labelTimezone', 'Timezone')}\n </label>\n <input\n id=\"scheduler-timezone\"\n type=\"text\"\n value={scheduleData.timezone}\n onChange={(e) => setScheduleData({ ...scheduleData, timezone: e.target.value })}\n className=\"w-full px-3 py-2 min-h-[44px] border border-border rounded-md bg-background\"\n readOnly\n aria-label={tr('tourScheduler.labelTimezone', 'Timezone')}\n />\n </div>\n </div>\n </div>\n\n {/* Status */}\n <div className=\"border border-border rounded-md p-4 sm:p-6 bg-card\">\n <label className=\"flex items-center gap-3 cursor-pointer\">\n <input\n type=\"checkbox\"\n checked={scheduleData.isActive}\n onChange={(e) => setScheduleData({ ...scheduleData, isActive: e.target.checked })}\n className=\"size-5 rounded\"\n aria-label={tr('tourScheduler.statusActiveLabel', 'Active')}\n />\n <div>\n <div className=\"font-medium\">{tr('tourScheduler.statusActiveLabel', 'Active')}</div>\n <div className=\"text-sm text-muted-foreground\">\n {tr(\n 'tourScheduler.statusActiveDescription',\n 'Enable this schedule immediately after creation'\n )}\n </div>\n </div>\n </label>\n </div>\n\n {/* Actions */}\n <div className=\"flex flex-col sm:flex-row gap-3\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex-1 px-4 py-2 min-h-[44px] border border-border rounded-md hover:bg-accent\"\n >\n {tr('tourScheduler.cancel', 'Cancel')}\n </button>\n <PermissionGate permission={TOUR_PERMISSIONS.TOUR_SCHEDULE_CREATE}>\n <button\n type=\"submit\"\n disabled={saving || !canCreateSchedule}\n className=\"flex-1 px-4 py-2 min-h-[44px] bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50 flex items-center justify-center gap-2\"\n >\n <Save size={20} />\n {saving\n ? tr('tourScheduler.creating', 'Creating...')\n : tr('tourScheduler.createSchedule', 'Create Schedule')}\n </button>\n </PermissionGate>\n </div>\n </form>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SchedulerPage.js","names":[],"sources":["../../../src/tours/pages/SchedulerPage.tsx"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { useParams, useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Calendar, Clock, ArrowLeft, Save } from 'lucide-react';\nimport { PermissionGate } from '@burdenoff/fe-libs/shared/components';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { useToursPermissionAccess } from '../hooks/useToursPermissionAccess';\nimport { useToursContext } from '../providers/ToursProvider';\nimport { TOUR_PERMISSIONS } from '../../constants/permissions';\nimport type { RecurrencePattern } from '../types';\nimport { emitToursApiFailed } from '../telemetry';\nimport { useCreateTourScheduleMutation } from '../../generated/global-operations';\nimport { useGetTourSchedulesQuery } from '../../generated/global-operations';\n\n/**\n * Tour scheduler page\n * Configure tour scheduling and recurrence\n */\nexport function SchedulerPage() {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n const { tourId } = useParams();\n const navigate = useNavigate();\n const toursContext = useToursContext();\n const { basePath = '/' } = toursContext;\n const eventBus = useEventBus();\n const { hasPermission } = useToursPermissionAccess();\n const canCreateSchedule = hasPermission(TOUR_PERMISSIONS.TOUR_SCHEDULE_CREATE);\n const canListSchedules = hasPermission(TOUR_PERMISSIONS.TOUR_SCHEDULE_LIST);\n\n const [scheduleData, setScheduleData] = useState({\n startDate: '',\n endDate: '',\n recurrencePattern: 'DAILY' as RecurrencePattern,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n isActive: true,\n });\n\n const [saving, setSaving] = useState(false);\n\n const [createSchedule] = useCreateTourScheduleMutation();\n const { data: schedulesData } = useGetTourSchedulesQuery({\n variables: { tourId: tourId! },\n skip: !tourId || !canListSchedules,\n });\n const existingSchedules = schedulesData?.tourSchedules ?? [];\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!tourId) return;\n setSaving(true);\n try {\n // Convert recurrence pattern to RFC 5545 recurrence rule\n const recurrenceRule =\n scheduleData.recurrencePattern === 'DAILY'\n ? 'FREQ=DAILY'\n : scheduleData.recurrencePattern === 'WEEKLY'\n ? 'FREQ=WEEKLY'\n : scheduleData.recurrencePattern === 'MONTHLY'\n ? 'FREQ=MONTHLY'\n : undefined;\n\n await createSchedule({\n variables: {\n input: {\n tourId,\n startDate: new Date(scheduleData.startDate).toISOString(),\n endDate: scheduleData.endDate\n ? new Date(scheduleData.endDate).toISOString()\n : undefined,\n recurrenceRule,\n timezone: scheduleData.timezone,\n },\n },\n refetchQueries: ['GetTourSchedules'],\n });\n navigate(`${basePath}/${tourId}/edit`);\n } catch {\n emitToursApiFailed(eventBus, 'schedule.create', toursContext);\n } finally {\n setSaving(false);\n }\n };\n\n const handleBack = () => {\n navigate(`${basePath}/${tourId}/edit`);\n };\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6 max-w-3xl mx-auto\">\n {/* Header */}\n <div className=\"flex items-center gap-3 sm:gap-4\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"p-2 min-h-[44px] min-w-[44px] hover:bg-accent rounded-md\"\n aria-label={tr('tourScheduler.goBack', 'Go back')}\n >\n <ArrowLeft size={20} />\n </button>\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold\">\n {tr('tourScheduler.title', 'Schedule Tour')}\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {tr('tourScheduler.subtitle', 'Configure when this tour should be activated')}\n </p>\n </div>\n </div>\n\n {/* Existing Schedules */}\n {existingSchedules.length > 0 && (\n <div className=\"border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {tr('tourScheduler.existingSchedules', 'Existing Schedules')}\n </h2>\n <div className=\"space-y-2\">\n {existingSchedules.map((schedule: (typeof existingSchedules)[number]) => (\n <div\n key={schedule.id}\n className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between p-3 border border-border-subtle rounded-md\"\n >\n <div>\n <div className=\"text-sm font-medium\">\n {new Date(schedule.startDate).toLocaleDateString()}\n {schedule.endDate\n ? ` — ${new Date(schedule.endDate).toLocaleDateString()}`\n : ` — ${tr('tourScheduler.noEndDate', 'No end date')}`}\n </div>\n <div className=\"text-xs text-text-secondary\">\n {schedule.recurrenceRule || tr('tourScheduler.oneTime', 'One-time')} ·{' '}\n {schedule.timezone}\n {schedule.isActive\n ? ` · ${tr('tourScheduler.active', 'Active')}`\n : ` · ${tr('tourScheduler.inactive', 'Inactive')}`}\n </div>\n </div>\n <div className=\"text-xs text-text-secondary\">\n {tr('tourScheduler.runs', 'Runs')}: {schedule.executionCount}\n </div>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {/* Form */}\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Date Range */}\n <div className=\"border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface\">\n <h2 className=\"text-lg font-semibold mb-4 flex items-center gap-2\">\n <Calendar size={20} />\n {tr('tourScheduler.sectionDateRange', 'Date Range')}\n </h2>\n\n <div className=\"space-y-4\">\n <div>\n <label htmlFor=\"scheduler-start-date\" className=\"block text-sm font-medium mb-2\">\n {tr('tourScheduler.labelStartDate', 'Start Date *')}\n </label>\n <input\n id=\"scheduler-start-date\"\n type=\"datetime-local\"\n value={scheduleData.startDate}\n onChange={(e) => setScheduleData({ ...scheduleData, startDate: e.target.value })}\n className=\"w-full px-3 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface\"\n required\n aria-label={tr('tourScheduler.labelStartDate', 'Start Date *')}\n />\n </div>\n\n <div>\n <label htmlFor=\"scheduler-end-date\" className=\"block text-sm font-medium mb-2\">\n {tr('tourScheduler.labelEndDate', 'End Date (Optional)')}\n </label>\n <input\n id=\"scheduler-end-date\"\n type=\"datetime-local\"\n value={scheduleData.endDate}\n onChange={(e) => setScheduleData({ ...scheduleData, endDate: e.target.value })}\n className=\"w-full px-3 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface\"\n aria-label={tr('tourScheduler.labelEndDate', 'End Date (Optional)')}\n />\n </div>\n </div>\n </div>\n\n {/* Recurrence */}\n <div className=\"border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface\">\n <h2 className=\"text-lg font-semibold mb-4 flex items-center gap-2\">\n <Clock size={20} />\n {tr('tourScheduler.sectionRecurrence', 'Recurrence')}\n </h2>\n\n <div className=\"space-y-4\">\n <div>\n <label htmlFor=\"scheduler-recurrence\" className=\"block text-sm font-medium mb-2\">\n {tr('tourScheduler.labelRecurrencePattern', 'Recurrence Pattern')}\n </label>\n <select\n id=\"scheduler-recurrence\"\n value={scheduleData.recurrencePattern}\n onChange={(e) =>\n setScheduleData({\n ...scheduleData,\n recurrencePattern: e.target.value as RecurrencePattern,\n })\n }\n className=\"w-full px-3 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface\"\n aria-label={tr('tourScheduler.labelRecurrencePattern', 'Recurrence Pattern')}\n >\n <option value=\"DAILY\">{tr('tourScheduler.recurrenceDaily', 'Daily')}</option>\n <option value=\"WEEKLY\">{tr('tourScheduler.recurrenceWeekly', 'Weekly')}</option>\n <option value=\"MONTHLY\">{tr('tourScheduler.recurrenceMonthly', 'Monthly')}</option>\n <option value=\"CUSTOM\">{tr('tourScheduler.recurrenceCustom', 'Custom')}</option>\n </select>\n </div>\n\n <div>\n <label htmlFor=\"scheduler-timezone\" className=\"block text-sm font-medium mb-2\">\n {tr('tourScheduler.labelTimezone', 'Timezone')}\n </label>\n <input\n id=\"scheduler-timezone\"\n type=\"text\"\n value={scheduleData.timezone}\n onChange={(e) => setScheduleData({ ...scheduleData, timezone: e.target.value })}\n className=\"w-full px-3 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface\"\n readOnly\n aria-label={tr('tourScheduler.labelTimezone', 'Timezone')}\n />\n </div>\n </div>\n </div>\n\n {/* Status */}\n <div className=\"border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface\">\n <label className=\"flex items-center gap-3 cursor-pointer\">\n <input\n type=\"checkbox\"\n checked={scheduleData.isActive}\n onChange={(e) => setScheduleData({ ...scheduleData, isActive: e.target.checked })}\n className=\"size-5 rounded\"\n aria-label={tr('tourScheduler.statusActiveLabel', 'Active')}\n />\n <div>\n <div className=\"font-medium\">{tr('tourScheduler.statusActiveLabel', 'Active')}</div>\n <div className=\"text-sm text-text-secondary\">\n {tr(\n 'tourScheduler.statusActiveDescription',\n 'Enable this schedule immediately after creation'\n )}\n </div>\n </div>\n </label>\n </div>\n\n {/* Actions */}\n <div className=\"flex flex-col sm:flex-row gap-3\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex-1 px-4 py-2 min-h-[44px] border border-border-subtle rounded-md hover:bg-accent\"\n >\n {tr('tourScheduler.cancel', 'Cancel')}\n </button>\n <PermissionGate permission={TOUR_PERMISSIONS.TOUR_SCHEDULE_CREATE}>\n <button\n type=\"submit\"\n disabled={saving || !canCreateSchedule}\n className=\"flex-1 px-4 py-2 min-h-[44px] bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 disabled:opacity-50 flex items-center justify-center gap-2\"\n >\n <Save size={20} />\n {saving\n ? tr('tourScheduler.creating', 'Creating...')\n : tr('tourScheduler.createSchedule', 'Create Schedule')}\n </button>\n </PermissionGate>\n </div>\n </form>\n </div>\n );\n}\n"],"mappings":"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"}
|