@burdenoff/microfe-timecampus 2026.731.2 → 2026.820.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/generated/wspace-operations.d.ts +484 -861
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-operations.js +1377 -1377
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/index.js +5 -3
- package/dist/services/createSandboxAssistantTransport.js.map +1 -1
- package/dist/stories/EmptyState.stories.d.ts +0 -1
- package/dist/stories/LoadingSkeleton.stories.d.ts +0 -1
- package/dist/stories/StatCard.stories.d.ts +0 -1
- package/dist/stories/StatusBadge.stories.d.ts +0 -1
- package/dist/timecampus/AdminRoot.d.ts +8 -0
- package/dist/timecampus/AdminRoot.d.ts.map +1 -0
- package/dist/timecampus/AdminRoot.js +12 -0
- package/dist/timecampus/AdminRoot.js.map +1 -0
- package/dist/timecampus/AdminRoutes.d.ts +3 -0
- package/dist/timecampus/AdminRoutes.d.ts.map +1 -0
- package/dist/timecampus/AdminRoutes.js +8 -0
- package/dist/timecampus/AdminRoutes.js.map +1 -0
- package/dist/timecampus/TimeCampusRoot.d.ts +0 -1
- package/dist/timecampus/TimeCampusRoot.js +4 -2
- package/dist/timecampus/TimeCampusRoot.js.map +1 -1
- package/dist/timecampus/TimeCampusRoutes.js +1 -1
- package/dist/timecampus/TimeCampusRoutes.js.map +1 -1
- package/dist/timecampus/components/ChartCard.d.ts +0 -1
- package/dist/timecampus/components/ChartCard.js.map +1 -1
- package/dist/timecampus/components/DataTable.d.ts +0 -1
- package/dist/timecampus/components/EmptyState.d.ts +0 -1
- package/dist/timecampus/components/EmptyState.js.map +1 -1
- package/dist/timecampus/components/ErrorBoundary.d.ts +0 -1
- package/dist/timecampus/components/ErrorBoundary.js.map +1 -1
- package/dist/timecampus/components/FilterBar.d.ts +0 -1
- package/dist/timecampus/components/LoadingSkeleton.d.ts +0 -1
- package/dist/timecampus/components/LoadingSkeleton.js.map +1 -1
- package/dist/timecampus/components/Modal.d.ts +0 -1
- package/dist/timecampus/components/Modal.js +5 -3
- package/dist/timecampus/components/Modal.js.map +1 -1
- package/dist/timecampus/components/PageLayout.d.ts +0 -1
- package/dist/timecampus/components/PageLayout.js.map +1 -1
- package/dist/timecampus/components/StatCard.d.ts +0 -1
- package/dist/timecampus/components/StatCard.js.map +1 -1
- package/dist/timecampus/components/StatusBadge.d.ts +0 -1
- package/dist/timecampus/components/StatusBadge.js.map +1 -1
- package/dist/timecampus/components/Toast.d.ts +0 -1
- package/dist/timecampus/components/Toast.js.map +1 -1
- package/dist/timecampus/components/charts/BalanceWheel.js.map +1 -1
- package/dist/timecampus/components/charts/BarsChart.d.ts +0 -1
- package/dist/timecampus/components/charts/BarsChart.js.map +1 -1
- package/dist/timecampus/components/charts/ChartParts.d.ts +0 -1
- package/dist/timecampus/components/charts/ChartParts.js.map +1 -1
- package/dist/timecampus/components/charts/DonutChart.d.ts +0 -1
- package/dist/timecampus/components/charts/DonutChart.js.map +1 -1
- package/dist/timecampus/components/charts/Heatmap.d.ts +0 -1
- package/dist/timecampus/components/charts/Heatmap.js +29 -29
- package/dist/timecampus/components/charts/Heatmap.js.map +1 -1
- package/dist/timecampus/components/charts/ProgressMeter.d.ts +0 -1
- package/dist/timecampus/components/charts/ProgressMeter.js +2 -2
- package/dist/timecampus/components/charts/ProgressMeter.js.map +1 -1
- package/dist/timecampus/components/charts/RadialGauge.d.ts +0 -1
- package/dist/timecampus/components/charts/RadialGauge.js +2 -2
- package/dist/timecampus/components/charts/RadialGauge.js.map +1 -1
- package/dist/timecampus/components/charts/TrendChart.d.ts +0 -1
- package/dist/timecampus/components/charts/TrendChart.js.map +1 -1
- package/dist/timecampus/components/charts/WeeksGrid.js +2 -2
- package/dist/timecampus/components/charts/WeeksGrid.js.map +1 -1
- package/dist/timecampus/components/charts/chartTheme.js.map +1 -1
- package/dist/timecampus/components/charts/index.js +10 -9
- package/dist/timecampus/components/index.js +18 -17
- package/dist/timecampus/index.d.ts +3 -1
- package/dist/timecampus/index.d.ts.map +1 -1
- package/dist/timecampus/index.js +8 -5
- package/dist/timecampus/pages/HomePage.d.ts +0 -1
- package/dist/timecampus/pages/HomePage.js.map +1 -1
- package/dist/timecampus/pages/admin/AdminPage.d.ts +3 -0
- package/dist/timecampus/pages/admin/AdminPage.d.ts.map +1 -0
- package/dist/timecampus/pages/admin/AdminPage.js +235 -0
- package/dist/timecampus/pages/admin/AdminPage.js.map +1 -0
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.d.ts.map +1 -1
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
- package/dist/timecampus/pages/client-portal/ClientPortalPage.d.ts.map +1 -1
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
- package/dist/timecampus/pages/dashboard/AnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/dashboard/ReportsPage.js.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
- package/dist/timecampus/pages/invoicing/InvoicingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/invoicing/InvoicingPage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/DeadTimeRadarPage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/FiniteLifePage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/LifeBalancePage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/LifeTimelinePage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/PersonalCorporatePage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/RecommendationsPage.js +1 -1
- package/dist/timecampus/pages/lifetime/RecommendationsPage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/TimeWellSpentPage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/ValuesGoalsPage.js +9 -6
- package/dist/timecampus/pages/lifetime/ValuesGoalsPage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/domains.d.ts +0 -1
- package/dist/timecampus/pages/lifetime/domains.js +1 -1
- package/dist/timecampus/pages/lifetime/domains.js.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/ActionItemFormModal.d.ts +0 -1
- package/dist/timecampus/pages/meetings/ActionItemFormModal.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js +1 -1
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingCreatePage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingDetailPage.js +1 -1
- package/dist/timecampus/pages/meetings/MeetingDetailPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingFormModal.d.ts +0 -1
- package/dist/timecampus/pages/meetings/MeetingFormModal.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/ParticipantFormModal.d.ts +0 -1
- package/dist/timecampus/pages/meetings/ParticipantFormModal.js.map +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroPage.js.map +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroSettingsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectCreatePage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectDetailPage.js +1 -1
- package/dist/timecampus/pages/projects/ProjectDetailPage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/TasksPage.js +1 -1
- package/dist/timecampus/pages/projects/TasksPage.js.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.d.ts.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamPage.js.map +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.js +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.js +4 -4
- package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
- package/dist/timecampus/pages/timeline/TimelinePage.js.map +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
- package/dist/timecampus/providers/TimeCampusProvider.d.ts +0 -1
- package/dist/timecampus/providers/TimeCampusProvider.d.ts.map +1 -1
- package/dist/timecampus/providers/TimeCampusProvider.js +39 -36
- package/dist/timecampus/providers/TimeCampusProvider.js.map +1 -1
- package/dist/timecampus/store/timecampusStore.js.map +1 -1
- package/dist/timecampus/types/index.d.ts +4 -1
- package/dist/timecampus/types/index.d.ts.map +1 -1
- package/dist/timecampus/utils/helpers.d.ts +0 -1
- package/dist/timecampus/utils/helpers.js.map +1 -1
- package/dist/types/assistant.d.ts +0 -1
- package/dist/utils/pageContext.js +1 -1
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +61 -59
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MeetingCreatePage.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingCreatePage.tsx"],"sourcesContent":["import { useState, useCallback } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport { useCreateMeetingMutation } from \"@/generated/wspace-operations\";\nimport { MeetingType } from \"@/generated/wspace-types\";\n\nexport function MeetingCreatePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n const [createMeeting, { loading: creating, error }] =\n useCreateMeetingMutation();\n\n const [title, setTitle] = useState(\"\");\n const [date, setDate] = useState(\"\");\n const [duration, setDuration] = useState(30);\n const [description, setDescription] = useState(\"\");\n\n const goBack = useCallback(() => navigate(\"/meetings\"), [navigate]);\n\n const handleSubmit = useCallback(async () => {\n if (!workspaceId || !title || !date) return;\n const startTime = new Date(date).toISOString();\n const endTime =\n duration > 0\n ? new Date(new Date(date).getTime() + duration * 60_000).toISOString()\n : undefined;\n await createMeeting({\n variables: {\n input: {\n workspaceId,\n title: title.trim(),\n meetingType: MeetingType.Team,\n startTime,\n endTime,\n notes: description.trim() || undefined,\n },\n },\n refetchQueries: [\"ListMeetings\"],\n });\n navigate(\"/meetings\");\n }, [\n workspaceId,\n title,\n date,\n duration,\n description,\n createMeeting,\n navigate,\n ]);\n\n return (\n <PageLayout\n title=\"Schedule Meeting\"\n description=\"Create a new meeting\"\n purpose=\"Book a meeting with its time and agenda so it's tracked alongside your work — letting TimeCampus account for the hours it consumes and surface its cost and action items afterwards.\"\n actions={\n <button\n onClick={goBack}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back\n </button>\n }\n >\n <GlassCard treatment=\"glass\" glow className=\"mx-auto max-w-2xl p-6\">\n {error && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to schedule meeting. Please try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Meeting Title\n </label>\n <input\n type=\"text\"\n placeholder=\"Enter meeting title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Date\n </label>\n <input\n type=\"date\"\n value={date}\n onChange={(e) => setDate(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Duration (minutes)\n </label>\n <input\n type=\"number\"\n min={0}\n value={duration}\n onChange={(e) => setDuration(Number(e.target.value))}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Description\n </label>\n <textarea\n placeholder=\"Meeting agenda...\"\n rows={3}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div className=\"flex justify-end gap-2 pt-4\">\n <button\n onClick={goBack}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={creating || !title || !date}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating ? \"Scheduling...\" : \"Schedule\"}\n </button>\n </div>\n </div>\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;AAWA,SAAgB,IAAoB;CAClC,IAAM,EAAE,iBAAc,OAAO,
|
|
1
|
+
{"version":3,"file":"MeetingCreatePage.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingCreatePage.tsx"],"sourcesContent":["import { useState, useCallback } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport { useCreateMeetingMutation } from \"@/generated/wspace-operations\";\nimport { MeetingType } from \"@/generated/wspace-types\";\n\nexport function MeetingCreatePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n const [createMeeting, { loading: creating, error }] =\n useCreateMeetingMutation();\n\n const [title, setTitle] = useState(\"\");\n const [date, setDate] = useState(\"\");\n const [duration, setDuration] = useState(30);\n const [description, setDescription] = useState(\"\");\n\n const goBack = useCallback(() => navigate(\"/meetings\"), [navigate]);\n\n const handleSubmit = useCallback(async () => {\n if (!workspaceId || !title || !date) return;\n const startTime = new Date(date).toISOString();\n const endTime =\n duration > 0\n ? new Date(new Date(date).getTime() + duration * 60_000).toISOString()\n : undefined;\n await createMeeting({\n variables: {\n input: {\n workspaceId,\n title: title.trim(),\n meetingType: MeetingType.Team,\n startTime,\n endTime,\n notes: description.trim() || undefined,\n },\n },\n refetchQueries: [\"ListMeetings\"],\n });\n navigate(\"/meetings\");\n }, [\n workspaceId,\n title,\n date,\n duration,\n description,\n createMeeting,\n navigate,\n ]);\n\n return (\n <PageLayout\n title=\"Schedule Meeting\"\n description=\"Create a new meeting\"\n purpose=\"Book a meeting with its time and agenda so it's tracked alongside your work — letting TimeCampus account for the hours it consumes and surface its cost and action items afterwards.\"\n actions={\n <button\n onClick={goBack}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back\n </button>\n }\n >\n <GlassCard treatment=\"glass\" glow className=\"mx-auto max-w-2xl p-6\">\n {error && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to schedule meeting. Please try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Meeting Title\n </label>\n <input\n type=\"text\"\n placeholder=\"Enter meeting title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Date\n </label>\n <input\n type=\"date\"\n value={date}\n onChange={(e) => setDate(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Duration (minutes)\n </label>\n <input\n type=\"number\"\n min={0}\n value={duration}\n onChange={(e) => setDuration(Number(e.target.value))}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Description\n </label>\n <textarea\n placeholder=\"Meeting agenda...\"\n rows={3}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div className=\"flex justify-end gap-2 pt-4\">\n <button\n onClick={goBack}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={creating || !title || !date}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating ? \"Scheduling...\" : \"Schedule\"}\n </button>\n </div>\n </div>\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -13,7 +13,7 @@ import { ParticipantFormModal as ue } from "./ParticipantFormModal.js";
|
|
|
13
13
|
import { ActionItemFormModal as de } from "./ActionItemFormModal.js";
|
|
14
14
|
import { useState as m } from "react";
|
|
15
15
|
import { CalendarDays as h, Check as g, Clock as fe, Pencil as _, Plus as v, Star as pe, Trash2 as y, UserMinus as b, Users as x } from "lucide-react";
|
|
16
|
-
import { Link as S, useParams as C } from "react-router
|
|
16
|
+
import { Link as S, useParams as C } from "react-router";
|
|
17
17
|
import { Fragment as w, jsx as T, jsxs as E } from "react/jsx-runtime";
|
|
18
18
|
import { NextSteps as D } from "@burdenoff/fe-libs/ui";
|
|
19
19
|
//#region src/timecampus/pages/meetings/MeetingDetailPage.tsx
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MeetingDetailPage.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingDetailPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useParams, Link } from \"react-router-dom\";\nimport {\n Users,\n Clock,\n Pencil,\n Trash2,\n Plus,\n Star,\n Check,\n UserMinus,\n CalendarDays,\n} from \"lucide-react\";\nimport { NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport { cn, type BadgeVariant } from \"../../utils/helpers\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n useGetMeetingQuery,\n useUpdateMeetingMutation,\n useDeleteMeetingMutation,\n useRateMeetingMutation,\n useAddParticipantMutation,\n useRemoveParticipantMutation,\n useUpdateParticipantStatusMutation,\n useCreateActionItemMutation,\n useUpdateActionItemMutation,\n useCompleteActionItemMutation,\n useDeleteActionItemMutation,\n type MeetingParticipantFieldsFragment,\n type MeetingActionItemFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport {\n MeetingParticipantStatus,\n MeetingActionItemStatus,\n type UpdateMeetingInput,\n type AddMeetingParticipantInput,\n type CreateActionItemInput,\n type UpdateActionItemInput,\n} from \"@/generated/wspace-types\";\nimport { formatCurrency } from \"../../utils/helpers\";\nimport { MeetingFormModal } from \"./MeetingFormModal\";\nimport { ParticipantFormModal } from \"./ParticipantFormModal\";\nimport { ActionItemFormModal } from \"./ActionItemFormModal\";\n\nconst PARTICIPANT_STATUS_OPTIONS = [\n MeetingParticipantStatus.Pending,\n MeetingParticipantStatus.Accepted,\n MeetingParticipantStatus.Tentative,\n MeetingParticipantStatus.Declined,\n];\n\nconst ACTION_ITEM_VARIANT: Record<MeetingActionItemStatus, BadgeVariant> = {\n [MeetingActionItemStatus.Completed]: \"success\",\n [MeetingActionItemStatus.InProgress]: \"info\",\n [MeetingActionItemStatus.Pending]: \"warning\",\n [MeetingActionItemStatus.Cancelled]: \"muted\",\n};\n\n/** Deterministic 2-char monogram from an opaque participant id. */\nfunction idMonogram(id: string): string {\n return (\n id\n .replace(/[^a-zA-Z0-9]/g, \"\")\n .slice(0, 2)\n .toUpperCase() || \"?\"\n );\n}\n\nfunction prettyStatus(status: string): string {\n const lower = status.replace(/_/g, \" \").toLowerCase();\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\nexport function MeetingDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { workspaceId = \"\", currentUser } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n const meetingId = id ?? \"\";\n\n const { data, loading, error, refetch } = useGetMeetingQuery({\n variables: { id: meetingId, workspaceId },\n skip: !workspaceId || !id,\n });\n\n const [updateMeeting, { loading: updating, error: updateError }] =\n useUpdateMeetingMutation();\n const [deleteMeeting, { loading: deletingMeeting }] =\n useDeleteMeetingMutation();\n const [rateMeeting, { loading: rating }] = useRateMeetingMutation();\n const [addParticipant, { loading: addingParticipant, error: addPError }] =\n useAddParticipantMutation();\n const [removeParticipant] = useRemoveParticipantMutation();\n const [updateParticipantStatus] = useUpdateParticipantStatusMutation();\n const [createActionItem, { loading: creatingItem, error: createItemError }] =\n useCreateActionItemMutation();\n const [updateActionItem, { loading: updatingItem, error: updateItemError }] =\n useUpdateActionItemMutation();\n const [completeActionItem] = useCompleteActionItemMutation();\n const [deleteActionItem] = useDeleteActionItemMutation();\n\n const [editOpen, setEditOpen] = useState(false);\n const [deleteOpen, setDeleteOpen] = useState(false);\n const [participantOpen, setParticipantOpen] = useState(false);\n const [removingParticipant, setRemovingParticipant] =\n useState<MeetingParticipantFieldsFragment | null>(null);\n const [actionItemModal, setActionItemModal] = useState<\n | { mode: \"create\" }\n | { mode: \"edit\"; item: MeetingActionItemFieldsFragment }\n | null\n >(null);\n const [deletingItem, setDeletingItem] =\n useState<MeetingActionItemFieldsFragment | null>(null);\n\n const meeting = data?.meeting;\n\n if (loading) {\n return (\n <PageLayout title=\"Meeting Details\" description={`Meeting ID: ${id}`}>\n <LoadingSkeleton variant=\"detail\" />\n </PageLayout>\n );\n }\n\n if (error || !meeting) {\n return (\n <PageLayout title=\"Meeting Details\" description={`Meeting ID: ${id}`}>\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n {error\n ? \"Failed to load meeting details. Please try again later.\"\n : \"Meeting not found.\"}\n </div>\n </PageLayout>\n );\n }\n\n const pendingActions = meeting.actionItems.filter(\n (a: MeetingActionItemFieldsFragment) => a.status !== \"COMPLETED\",\n ).length;\n const currentRating = meeting.rating ?? 0;\n\n const handleUpdateMeeting = async (input: UpdateMeetingInput) => {\n await updateMeeting({ variables: { id: meetingId, workspaceId, input } });\n await refetch();\n setEditOpen(false);\n };\n\n const handleDeleteMeeting = async () => {\n await deleteMeeting({ variables: { id: meetingId, workspaceId } });\n setDeleteOpen(false);\n navigate(\"/meetings\");\n };\n\n const handleRate = async (value: number) => {\n await rateMeeting({\n variables: { id: meetingId, workspaceId, rating: value },\n });\n await refetch();\n };\n\n const handleAddParticipant = async (input: AddMeetingParticipantInput) => {\n await addParticipant({\n variables: { meetingId, workspaceId, input },\n });\n await refetch();\n setParticipantOpen(false);\n };\n\n const handleRemoveParticipant = async () => {\n if (!removingParticipant) return;\n await removeParticipant({\n variables: {\n meetingId,\n participantId: removingParticipant.id,\n workspaceId,\n },\n });\n setRemovingParticipant(null);\n await refetch();\n };\n\n const handleParticipantStatus = async (\n participantId: string,\n status: MeetingParticipantStatus,\n ) => {\n await updateParticipantStatus({\n variables: { meetingId, participantId, workspaceId, status },\n });\n await refetch();\n };\n\n const handleCreateItem = async (input: CreateActionItemInput) => {\n await createActionItem({ variables: { meetingId, workspaceId, input } });\n await refetch();\n setActionItemModal(null);\n };\n\n const handleUpdateItem = async (\n itemId: string,\n input: UpdateActionItemInput,\n ) => {\n await updateActionItem({ variables: { id: itemId, workspaceId, input } });\n await refetch();\n setActionItemModal(null);\n };\n\n const handleCompleteItem = async (itemId: string) => {\n await completeActionItem({ variables: { id: itemId, workspaceId } });\n await refetch();\n };\n\n const handleDeleteItem = async () => {\n if (!deletingItem) return;\n await deleteActionItem({ variables: { id: deletingItem.id, workspaceId } });\n setDeletingItem(null);\n await refetch();\n };\n\n return (\n <PageLayout\n title=\"Meeting Details\"\n description={meeting.title}\n purpose=\"The full record of one meeting — who attended, what it cost in people's time, and the action items it produced — so you can follow up on commitments and judge whether the meeting was worth it.\"\n actions={\n <>\n <button\n onClick={() => setEditOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n <Pencil className=\"h-4 w-4\" />\n Edit\n </button>\n <button\n onClick={() => setDeleteOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/40\"\n >\n <Trash2 className=\"h-4 w-4\" />\n Delete\n </button>\n </>\n }\n >\n <NextSteps\n storageKey=\"timecampus-meeting-detail\"\n className=\"mb-6\"\n steps={[\n {\n id: \"all-meetings\",\n label: \"Back to all meetings\",\n description: \"Browse and schedule other meetings.\",\n onClick: () => navigate(\"/meetings\"),\n },\n {\n id: \"analytics\",\n label: \"See meeting analytics\",\n description: \"Understand your overall meeting load and cost.\",\n onClick: () => navigate(\"/meetings/analytics\"),\n },\n ]}\n />\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Duration\"\n value={`${meeting.duration ?? 0} min`}\n icon={<Clock className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Participants\"\n value={String(meeting.participants.length)}\n icon={<Users className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Cost\"\n value={meeting.cost != null ? formatCurrency(meeting.cost) : \"N/A\"}\n description=\"Based on participant rates\"\n />\n <StatCard\n title=\"Action Items\"\n value={String(meeting.actionItems.length)}\n description={`${pendingActions} pending`}\n />\n </div>\n\n {/* Calendar link — surfaces the wspace-calendar event auto-linked on\n create (MeetingMeta.calendarEventId). Hidden entirely when the\n meeting has no linked event (e.g. calendar-svc was unreachable at\n creation time), rather than showing a dead/broken state. */}\n {meeting.calendarEventId && (\n <Link\n to={`/calendar/event/${meeting.calendarEventId}`}\n className=\"inline-flex w-fit items-center gap-1.5 rounded-full border border-border-subtle bg-bg-surface px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/50 hover:text-text-primary\"\n >\n <CalendarDays className=\"h-3.5 w-3.5\" />\n View in Calendar\n </Link>\n )}\n\n {/* Rating */}\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <h3 className=\"mb-1 text-sm font-semibold text-text-primary\">\n Rate this meeting\n </h3>\n <p className=\"mb-3 text-xs text-text-secondary\">\n Was it worth the time? Your rating feeds meeting analytics.\n </p>\n <div className=\"flex items-center gap-1\">\n {[1, 2, 3, 4, 5].map((value) => (\n <button\n key={value}\n onClick={() => handleRate(value)}\n disabled={rating}\n aria-label={`Rate ${value} star${value > 1 ? \"s\" : \"\"}`}\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Star\n className={cn(\n \"h-6 w-6\",\n value <= currentRating\n ? \"fill-status-warning-text text-status-warning-text\"\n : \"text-text-secondary\",\n )}\n />\n </button>\n ))}\n {currentRating > 0 && (\n <span className=\"ml-2 text-sm text-text-secondary\">\n {currentRating} / 5\n </span>\n )}\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Participants\n </h3>\n <button\n onClick={() => setParticipantOpen(true)}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-2.5 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-3.5 w-3.5\" />\n Add\n </button>\n </div>\n {meeting.participants.length > 0 ? (\n <div className=\"space-y-2\">\n {meeting.participants.map(\n (p: MeetingParticipantFieldsFragment) => (\n <div\n key={p.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-subtle px-3 py-2\"\n >\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-[11px] font-semibold text-primary\">\n {idMonogram(p.userId)}\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-primary\">\n {p.userId}\n </p>\n <p className=\"text-xs text-text-secondary\">{p.role}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={p.status}\n onChange={(e) =>\n handleParticipantStatus(\n p.id,\n e.target.value as MeetingParticipantStatus,\n )\n }\n className=\"rounded-md border border-border-subtle bg-bg-surface px-2 py-1 text-xs text-text-primary\"\n aria-label=\"Participant status\"\n >\n {PARTICIPANT_STATUS_OPTIONS.map((s) => (\n <option key={s} value={s}>\n {s}\n </option>\n ))}\n </select>\n <button\n onClick={() => setRemovingParticipant(p)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Remove participant\"\n >\n <UserMinus className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ),\n )}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No participants added\"\n description=\"Add attendees to capture who was in the room and what it cost.\"\n action={\n <button\n onClick={() => setParticipantOpen(true)}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add participant\n </button>\n }\n />\n )}\n </div>\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Action Items\n </h3>\n <button\n onClick={() => setActionItemModal({ mode: \"create\" })}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-2.5 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-3.5 w-3.5\" />\n Add\n </button>\n </div>\n {meeting.actionItems.length > 0 ? (\n <div className=\"space-y-2\">\n {meeting.actionItems.map(\n (item: MeetingActionItemFieldsFragment) => (\n <div\n key={item.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-subtle px-3 py-2\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-primary\">\n {item.title}\n </p>\n <div className=\"mt-1\">\n <StatusBadge\n label={prettyStatus(item.status)}\n variant={ACTION_ITEM_VARIANT[item.status]}\n />\n </div>\n </div>\n <div className=\"flex items-center gap-1\">\n {item.status !== MeetingActionItemStatus.Completed && (\n <button\n onClick={() => handleCompleteItem(item.id)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-success-bg hover:text-status-success-text\"\n aria-label=\"Complete action item\"\n >\n <Check className=\"h-4 w-4\" />\n </button>\n )}\n <button\n onClick={() =>\n setActionItemModal({ mode: \"edit\", item })\n }\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Edit action item\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setDeletingItem(item)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Delete action item\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ),\n )}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No action items\"\n description=\"Capture follow-ups here so commitments from the meeting don't get lost.\"\n action={\n <button\n onClick={() => setActionItemModal({ mode: \"create\" })}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add action item\n </button>\n }\n />\n )}\n </div>\n </div>\n\n {/* Edit meeting */}\n {editOpen && (\n <MeetingFormModal\n open\n onClose={() => setEditOpen(false)}\n workspaceId={workspaceId}\n meeting={meeting}\n submitting={updating}\n error={Boolean(updateError)}\n onUpdate={handleUpdateMeeting}\n />\n )}\n\n {/* Delete meeting */}\n <Modal\n open={deleteOpen}\n onClose={() => setDeleteOpen(false)}\n title=\"Delete meeting\"\n description=\"This permanently removes the meeting and its analytics.\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteOpen(false)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDeleteMeeting}\n disabled={deletingMeeting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90 disabled:opacity-50\"\n >\n {deletingMeeting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Are you sure you want to delete{\" \"}\n <span className=\"font-medium text-text-primary\">{meeting.title}</span>\n ? This action cannot be undone.\n </p>\n </Modal>\n\n {/* Add participant */}\n {participantOpen && (\n <ParticipantFormModal\n open\n onClose={() => setParticipantOpen(false)}\n defaultUserId={currentUser?.id}\n submitting={addingParticipant}\n error={Boolean(addPError)}\n onSubmit={handleAddParticipant}\n />\n )}\n\n {/* Remove participant confirm */}\n <Modal\n open={Boolean(removingParticipant)}\n onClose={() => setRemovingParticipant(null)}\n title=\"Remove participant\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRemovingParticipant(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleRemoveParticipant}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90\"\n >\n Remove\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Remove{\" \"}\n <span className=\"font-medium text-text-primary\">\n {removingParticipant?.userId}\n </span>{\" \"}\n from this meeting?\n </p>\n </Modal>\n\n {/* Create / edit action item */}\n {actionItemModal && (\n <ActionItemFormModal\n open\n onClose={() => setActionItemModal(null)}\n item={\n actionItemModal.mode === \"edit\" ? actionItemModal.item : undefined\n }\n submitting={\n actionItemModal.mode === \"edit\" ? updatingItem : creatingItem\n }\n error={Boolean(\n actionItemModal.mode === \"edit\" ? updateItemError : createItemError,\n )}\n onCreate={handleCreateItem}\n onUpdate={\n actionItemModal.mode === \"edit\"\n ? (input) => handleUpdateItem(actionItemModal.item.id, input)\n : undefined\n }\n />\n )}\n\n {/* Delete action item confirm */}\n <Modal\n open={Boolean(deletingItem)}\n onClose={() => setDeletingItem(null)}\n title=\"Delete action item\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeletingItem(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDeleteItem}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90\"\n >\n Delete\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Delete action item{\" \"}\n <span className=\"font-medium text-text-primary\">\n {deletingItem?.title}\n </span>\n ?\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MeetingDetailPage.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingDetailPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useParams, Link } from \"react-router\";\nimport {\n Users,\n Clock,\n Pencil,\n Trash2,\n Plus,\n Star,\n Check,\n UserMinus,\n CalendarDays,\n} from \"lucide-react\";\nimport { NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport { cn, type BadgeVariant } from \"../../utils/helpers\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n useGetMeetingQuery,\n useUpdateMeetingMutation,\n useDeleteMeetingMutation,\n useRateMeetingMutation,\n useAddParticipantMutation,\n useRemoveParticipantMutation,\n useUpdateParticipantStatusMutation,\n useCreateActionItemMutation,\n useUpdateActionItemMutation,\n useCompleteActionItemMutation,\n useDeleteActionItemMutation,\n type MeetingParticipantFieldsFragment,\n type MeetingActionItemFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport {\n MeetingParticipantStatus,\n MeetingActionItemStatus,\n type UpdateMeetingInput,\n type AddMeetingParticipantInput,\n type CreateActionItemInput,\n type UpdateActionItemInput,\n} from \"@/generated/wspace-types\";\nimport { formatCurrency } from \"../../utils/helpers\";\nimport { MeetingFormModal } from \"./MeetingFormModal\";\nimport { ParticipantFormModal } from \"./ParticipantFormModal\";\nimport { ActionItemFormModal } from \"./ActionItemFormModal\";\n\nconst PARTICIPANT_STATUS_OPTIONS = [\n MeetingParticipantStatus.Pending,\n MeetingParticipantStatus.Accepted,\n MeetingParticipantStatus.Tentative,\n MeetingParticipantStatus.Declined,\n];\n\nconst ACTION_ITEM_VARIANT: Record<MeetingActionItemStatus, BadgeVariant> = {\n [MeetingActionItemStatus.Completed]: \"success\",\n [MeetingActionItemStatus.InProgress]: \"info\",\n [MeetingActionItemStatus.Pending]: \"warning\",\n [MeetingActionItemStatus.Cancelled]: \"muted\",\n};\n\n/** Deterministic 2-char monogram from an opaque participant id. */\nfunction idMonogram(id: string): string {\n return (\n id\n .replace(/[^a-zA-Z0-9]/g, \"\")\n .slice(0, 2)\n .toUpperCase() || \"?\"\n );\n}\n\nfunction prettyStatus(status: string): string {\n const lower = status.replace(/_/g, \" \").toLowerCase();\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\nexport function MeetingDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { workspaceId = \"\", currentUser } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n const meetingId = id ?? \"\";\n\n const { data, loading, error, refetch } = useGetMeetingQuery({\n variables: { id: meetingId, workspaceId },\n skip: !workspaceId || !id,\n });\n\n const [updateMeeting, { loading: updating, error: updateError }] =\n useUpdateMeetingMutation();\n const [deleteMeeting, { loading: deletingMeeting }] =\n useDeleteMeetingMutation();\n const [rateMeeting, { loading: rating }] = useRateMeetingMutation();\n const [addParticipant, { loading: addingParticipant, error: addPError }] =\n useAddParticipantMutation();\n const [removeParticipant] = useRemoveParticipantMutation();\n const [updateParticipantStatus] = useUpdateParticipantStatusMutation();\n const [createActionItem, { loading: creatingItem, error: createItemError }] =\n useCreateActionItemMutation();\n const [updateActionItem, { loading: updatingItem, error: updateItemError }] =\n useUpdateActionItemMutation();\n const [completeActionItem] = useCompleteActionItemMutation();\n const [deleteActionItem] = useDeleteActionItemMutation();\n\n const [editOpen, setEditOpen] = useState(false);\n const [deleteOpen, setDeleteOpen] = useState(false);\n const [participantOpen, setParticipantOpen] = useState(false);\n const [removingParticipant, setRemovingParticipant] =\n useState<MeetingParticipantFieldsFragment | null>(null);\n const [actionItemModal, setActionItemModal] = useState<\n | { mode: \"create\" }\n | { mode: \"edit\"; item: MeetingActionItemFieldsFragment }\n | null\n >(null);\n const [deletingItem, setDeletingItem] =\n useState<MeetingActionItemFieldsFragment | null>(null);\n\n const meeting = data?.meeting;\n\n if (loading) {\n return (\n <PageLayout title=\"Meeting Details\" description={`Meeting ID: ${id}`}>\n <LoadingSkeleton variant=\"detail\" />\n </PageLayout>\n );\n }\n\n if (error || !meeting) {\n return (\n <PageLayout title=\"Meeting Details\" description={`Meeting ID: ${id}`}>\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n {error\n ? \"Failed to load meeting details. Please try again later.\"\n : \"Meeting not found.\"}\n </div>\n </PageLayout>\n );\n }\n\n const pendingActions = meeting.actionItems.filter(\n (a: MeetingActionItemFieldsFragment) => a.status !== \"COMPLETED\",\n ).length;\n const currentRating = meeting.rating ?? 0;\n\n const handleUpdateMeeting = async (input: UpdateMeetingInput) => {\n await updateMeeting({ variables: { id: meetingId, workspaceId, input } });\n await refetch();\n setEditOpen(false);\n };\n\n const handleDeleteMeeting = async () => {\n await deleteMeeting({ variables: { id: meetingId, workspaceId } });\n setDeleteOpen(false);\n navigate(\"/meetings\");\n };\n\n const handleRate = async (value: number) => {\n await rateMeeting({\n variables: { id: meetingId, workspaceId, rating: value },\n });\n await refetch();\n };\n\n const handleAddParticipant = async (input: AddMeetingParticipantInput) => {\n await addParticipant({\n variables: { meetingId, workspaceId, input },\n });\n await refetch();\n setParticipantOpen(false);\n };\n\n const handleRemoveParticipant = async () => {\n if (!removingParticipant) return;\n await removeParticipant({\n variables: {\n meetingId,\n participantId: removingParticipant.id,\n workspaceId,\n },\n });\n setRemovingParticipant(null);\n await refetch();\n };\n\n const handleParticipantStatus = async (\n participantId: string,\n status: MeetingParticipantStatus,\n ) => {\n await updateParticipantStatus({\n variables: { meetingId, participantId, workspaceId, status },\n });\n await refetch();\n };\n\n const handleCreateItem = async (input: CreateActionItemInput) => {\n await createActionItem({ variables: { meetingId, workspaceId, input } });\n await refetch();\n setActionItemModal(null);\n };\n\n const handleUpdateItem = async (\n itemId: string,\n input: UpdateActionItemInput,\n ) => {\n await updateActionItem({ variables: { id: itemId, workspaceId, input } });\n await refetch();\n setActionItemModal(null);\n };\n\n const handleCompleteItem = async (itemId: string) => {\n await completeActionItem({ variables: { id: itemId, workspaceId } });\n await refetch();\n };\n\n const handleDeleteItem = async () => {\n if (!deletingItem) return;\n await deleteActionItem({ variables: { id: deletingItem.id, workspaceId } });\n setDeletingItem(null);\n await refetch();\n };\n\n return (\n <PageLayout\n title=\"Meeting Details\"\n description={meeting.title}\n purpose=\"The full record of one meeting — who attended, what it cost in people's time, and the action items it produced — so you can follow up on commitments and judge whether the meeting was worth it.\"\n actions={\n <>\n <button\n onClick={() => setEditOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n <Pencil className=\"h-4 w-4\" />\n Edit\n </button>\n <button\n onClick={() => setDeleteOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/40\"\n >\n <Trash2 className=\"h-4 w-4\" />\n Delete\n </button>\n </>\n }\n >\n <NextSteps\n storageKey=\"timecampus-meeting-detail\"\n className=\"mb-6\"\n steps={[\n {\n id: \"all-meetings\",\n label: \"Back to all meetings\",\n description: \"Browse and schedule other meetings.\",\n onClick: () => navigate(\"/meetings\"),\n },\n {\n id: \"analytics\",\n label: \"See meeting analytics\",\n description: \"Understand your overall meeting load and cost.\",\n onClick: () => navigate(\"/meetings/analytics\"),\n },\n ]}\n />\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Duration\"\n value={`${meeting.duration ?? 0} min`}\n icon={<Clock className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Participants\"\n value={String(meeting.participants.length)}\n icon={<Users className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Cost\"\n value={meeting.cost != null ? formatCurrency(meeting.cost) : \"N/A\"}\n description=\"Based on participant rates\"\n />\n <StatCard\n title=\"Action Items\"\n value={String(meeting.actionItems.length)}\n description={`${pendingActions} pending`}\n />\n </div>\n\n {/* Calendar link — surfaces the wspace-calendar event auto-linked on\n create (MeetingMeta.calendarEventId). Hidden entirely when the\n meeting has no linked event (e.g. calendar-svc was unreachable at\n creation time), rather than showing a dead/broken state. */}\n {meeting.calendarEventId && (\n <Link\n to={`/calendar/event/${meeting.calendarEventId}`}\n className=\"inline-flex w-fit items-center gap-1.5 rounded-full border border-border-subtle bg-bg-surface px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/50 hover:text-text-primary\"\n >\n <CalendarDays className=\"h-3.5 w-3.5\" />\n View in Calendar\n </Link>\n )}\n\n {/* Rating */}\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <h3 className=\"mb-1 text-sm font-semibold text-text-primary\">\n Rate this meeting\n </h3>\n <p className=\"mb-3 text-xs text-text-secondary\">\n Was it worth the time? Your rating feeds meeting analytics.\n </p>\n <div className=\"flex items-center gap-1\">\n {[1, 2, 3, 4, 5].map((value) => (\n <button\n key={value}\n onClick={() => handleRate(value)}\n disabled={rating}\n aria-label={`Rate ${value} star${value > 1 ? \"s\" : \"\"}`}\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Star\n className={cn(\n \"h-6 w-6\",\n value <= currentRating\n ? \"fill-status-warning-text text-status-warning-text\"\n : \"text-text-secondary\",\n )}\n />\n </button>\n ))}\n {currentRating > 0 && (\n <span className=\"ml-2 text-sm text-text-secondary\">\n {currentRating} / 5\n </span>\n )}\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Participants\n </h3>\n <button\n onClick={() => setParticipantOpen(true)}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-2.5 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-3.5 w-3.5\" />\n Add\n </button>\n </div>\n {meeting.participants.length > 0 ? (\n <div className=\"space-y-2\">\n {meeting.participants.map(\n (p: MeetingParticipantFieldsFragment) => (\n <div\n key={p.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-subtle px-3 py-2\"\n >\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-[11px] font-semibold text-primary\">\n {idMonogram(p.userId)}\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-primary\">\n {p.userId}\n </p>\n <p className=\"text-xs text-text-secondary\">{p.role}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={p.status}\n onChange={(e) =>\n handleParticipantStatus(\n p.id,\n e.target.value as MeetingParticipantStatus,\n )\n }\n className=\"rounded-md border border-border-subtle bg-bg-surface px-2 py-1 text-xs text-text-primary\"\n aria-label=\"Participant status\"\n >\n {PARTICIPANT_STATUS_OPTIONS.map((s) => (\n <option key={s} value={s}>\n {s}\n </option>\n ))}\n </select>\n <button\n onClick={() => setRemovingParticipant(p)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Remove participant\"\n >\n <UserMinus className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ),\n )}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No participants added\"\n description=\"Add attendees to capture who was in the room and what it cost.\"\n action={\n <button\n onClick={() => setParticipantOpen(true)}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add participant\n </button>\n }\n />\n )}\n </div>\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Action Items\n </h3>\n <button\n onClick={() => setActionItemModal({ mode: \"create\" })}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-2.5 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-3.5 w-3.5\" />\n Add\n </button>\n </div>\n {meeting.actionItems.length > 0 ? (\n <div className=\"space-y-2\">\n {meeting.actionItems.map(\n (item: MeetingActionItemFieldsFragment) => (\n <div\n key={item.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-subtle px-3 py-2\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-primary\">\n {item.title}\n </p>\n <div className=\"mt-1\">\n <StatusBadge\n label={prettyStatus(item.status)}\n variant={ACTION_ITEM_VARIANT[item.status]}\n />\n </div>\n </div>\n <div className=\"flex items-center gap-1\">\n {item.status !== MeetingActionItemStatus.Completed && (\n <button\n onClick={() => handleCompleteItem(item.id)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-success-bg hover:text-status-success-text\"\n aria-label=\"Complete action item\"\n >\n <Check className=\"h-4 w-4\" />\n </button>\n )}\n <button\n onClick={() =>\n setActionItemModal({ mode: \"edit\", item })\n }\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Edit action item\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setDeletingItem(item)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Delete action item\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ),\n )}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No action items\"\n description=\"Capture follow-ups here so commitments from the meeting don't get lost.\"\n action={\n <button\n onClick={() => setActionItemModal({ mode: \"create\" })}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add action item\n </button>\n }\n />\n )}\n </div>\n </div>\n\n {/* Edit meeting */}\n {editOpen && (\n <MeetingFormModal\n open\n onClose={() => setEditOpen(false)}\n workspaceId={workspaceId}\n meeting={meeting}\n submitting={updating}\n error={Boolean(updateError)}\n onUpdate={handleUpdateMeeting}\n />\n )}\n\n {/* Delete meeting */}\n <Modal\n open={deleteOpen}\n onClose={() => setDeleteOpen(false)}\n title=\"Delete meeting\"\n description=\"This permanently removes the meeting and its analytics.\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteOpen(false)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDeleteMeeting}\n disabled={deletingMeeting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90 disabled:opacity-50\"\n >\n {deletingMeeting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Are you sure you want to delete{\" \"}\n <span className=\"font-medium text-text-primary\">{meeting.title}</span>\n ? This action cannot be undone.\n </p>\n </Modal>\n\n {/* Add participant */}\n {participantOpen && (\n <ParticipantFormModal\n open\n onClose={() => setParticipantOpen(false)}\n defaultUserId={currentUser?.id}\n submitting={addingParticipant}\n error={Boolean(addPError)}\n onSubmit={handleAddParticipant}\n />\n )}\n\n {/* Remove participant confirm */}\n <Modal\n open={Boolean(removingParticipant)}\n onClose={() => setRemovingParticipant(null)}\n title=\"Remove participant\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRemovingParticipant(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleRemoveParticipant}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90\"\n >\n Remove\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Remove{\" \"}\n <span className=\"font-medium text-text-primary\">\n {removingParticipant?.userId}\n </span>{\" \"}\n from this meeting?\n </p>\n </Modal>\n\n {/* Create / edit action item */}\n {actionItemModal && (\n <ActionItemFormModal\n open\n onClose={() => setActionItemModal(null)}\n item={\n actionItemModal.mode === \"edit\" ? actionItemModal.item : undefined\n }\n submitting={\n actionItemModal.mode === \"edit\" ? updatingItem : creatingItem\n }\n error={Boolean(\n actionItemModal.mode === \"edit\" ? updateItemError : createItemError,\n )}\n onCreate={handleCreateItem}\n onUpdate={\n actionItemModal.mode === \"edit\"\n ? (input) => handleUpdateItem(actionItemModal.item.id, input)\n : undefined\n }\n />\n )}\n\n {/* Delete action item confirm */}\n <Modal\n open={Boolean(deletingItem)}\n onClose={() => setDeletingItem(null)}\n title=\"Delete action item\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeletingItem(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDeleteItem}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90\"\n >\n Delete\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Delete action item{\" \"}\n <span className=\"font-medium text-text-primary\">\n {deletingItem?.title}\n </span>\n ?\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { FC } from 'react';
|
|
2
2
|
import { MeetingType, CreateMeetingInput, UpdateMeetingInput } from '../../../generated/wspace-types';
|
|
3
3
|
import { MeetingFieldsFragment } from '../../../generated/wspace-operations';
|
|
4
|
-
|
|
5
4
|
export interface MeetingFormValues {
|
|
6
5
|
title: string;
|
|
7
6
|
meetingType: MeetingType;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MeetingFormModal.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingFormModal.tsx"],"sourcesContent":["import { useState, type FC } from \"react\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n MeetingType,\n type CreateMeetingInput,\n type UpdateMeetingInput,\n} from \"@/generated/wspace-types\";\nimport type { MeetingFieldsFragment } from \"@/generated/wspace-operations\";\n\n/**\n * Resolve a downloadable URL for a Files object. The URL is presigned and\n * expires after 7 days (604800s) — a known limitation: a stored recordingUrl\n * will eventually 403 and the recording must be re-uploaded to refresh it.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampus_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// wspace-files requires a real folder id (File.folderId FK).\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusMeeting_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\nconst MEETING_TYPES: { value: MeetingType; label: string }[] = [\n { value: MeetingType.Team, label: \"Team\" },\n { value: MeetingType.Standup, label: \"Standup\" },\n { value: MeetingType.OneOnOne, label: \"One-on-one\" },\n { value: MeetingType.Planning, label: \"Planning\" },\n { value: MeetingType.Review, label: \"Review\" },\n { value: MeetingType.Retrospective, label: \"Retrospective\" },\n { value: MeetingType.External, label: \"External\" },\n];\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\nconst labelClass = \"block text-sm font-medium text-text-primary\";\n\n/** Convert an ISO string into a value usable by a datetime-local input. */\nfunction toLocalInput(iso?: string | null): string {\n if (!iso) return \"\";\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n const pad = (n: number) => String(n).padStart(2, \"0\");\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(\n d.getHours(),\n )}:${pad(d.getMinutes())}`;\n}\n\nexport interface MeetingFormValues {\n title: string;\n meetingType: MeetingType;\n startTime: string;\n endTime?: string;\n notes?: string;\n recordingUrl?: string;\n cost?: number;\n}\n\ninterface MeetingFormModalProps {\n open: boolean;\n onClose: () => void;\n /** Existing meeting when editing; undefined when creating. */\n meeting?: MeetingFieldsFragment;\n workspaceId: string;\n submitting: boolean;\n error?: boolean;\n /** Receives a ready-to-send input; caller wires it to the right mutation. */\n onCreate?: (input: CreateMeetingInput) => Promise<void> | void;\n onUpdate?: (input: UpdateMeetingInput) => Promise<void> | void;\n}\n\nexport const MeetingFormModal: FC<MeetingFormModalProps> = ({\n open,\n onClose,\n meeting,\n workspaceId,\n submitting,\n error,\n onCreate,\n onUpdate,\n}) => {\n const isEdit = Boolean(meeting);\n const [title, setTitle] = useState(meeting?.title ?? \"\");\n const [meetingType, setMeetingType] = useState<MeetingType>(\n (meeting?.meetingType as MeetingType | undefined) ?? MeetingType.Team,\n );\n const [start, setStart] = useState(toLocalInput(meeting?.startTime));\n const [duration, setDuration] = useState<number>(meeting?.duration ?? 30);\n const [notes, setNotes] = useState(meeting?.notes ?? \"\");\n const [recordingUrl, setRecordingUrl] = useState(meeting?.recordingUrl ?? \"\");\n const apollo = useApolloClient();\n const [recordingUploading, setRecordingUploading] = useState(false);\n const [recordingError, setRecordingError] = useState<string | null>(null);\n\n const canSubmit = Boolean(title.trim() && start) && !submitting;\n\n /**\n * Upload a recording (audio/video) to platform Files and store its presigned\n * URL in the existing recordingUrl state. Failures surface an inline error\n * but never block submitting the meeting — the recording is optional and the\n * manual URL field remains available as a fallback.\n */\n const handleRecordingUpload = async (file: File) => {\n setRecordingUploading(true);\n setRecordingError(null);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/Recordings\" },\n });\n const folderId = (\n folderData as { ensureFolderByPath?: { id: string } } | null\n )?.ensureFolderByPath?.id;\n if (!folderId) throw new Error(\"Could not resolve upload folder\");\n const uploaded = await uploadBlobToFiles({\n client: apollo,\n blob: file,\n filename: file.name,\n mimeType: file.type || \"application/octet-stream\",\n folderId,\n });\n const { data } = await apollo.query({\n query: GET_FILE_DOWNLOAD_URL,\n variables: { id: uploaded.id, expiresIn: 604800 },\n fetchPolicy: \"no-cache\",\n });\n const url = (data as { getFileDownloadUrl?: string } | null | undefined)\n ?.getFileDownloadUrl;\n if (!url) throw new Error(\"No download URL returned\");\n setRecordingUrl(url);\n } catch {\n setRecordingError(\n \"Failed to upload recording. You can paste a URL instead.\",\n );\n } finally {\n setRecordingUploading(false);\n }\n };\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n const startIso = new Date(start).toISOString();\n const endIso =\n duration > 0\n ? new Date(new Date(start).getTime() + duration * 60_000).toISOString()\n : undefined;\n\n if (isEdit && onUpdate) {\n await onUpdate({\n title: title.trim(),\n meetingType,\n startTime: startIso,\n endTime: endIso,\n notes: notes.trim() || undefined,\n recordingUrl: recordingUrl.trim() || undefined,\n });\n } else if (onCreate) {\n await onCreate({\n workspaceId,\n title: title.trim(),\n meetingType,\n startTime: startIso,\n endTime: endIso,\n notes: notes.trim() || undefined,\n recordingUrl: recordingUrl.trim() || undefined,\n });\n }\n };\n\n return (\n <Modal\n open={open}\n onClose={onClose}\n title={isEdit ? \"Edit Meeting\" : \"Schedule Meeting\"}\n description={\n isEdit\n ? \"Update this meeting's details.\"\n : \"Add a meeting to track its time, cost and follow-ups.\"\n }\n size=\"xl\"\n footer={\n <>\n <button\n onClick={onClose}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={!canSubmit}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {submitting\n ? isEdit\n ? \"Saving...\"\n : \"Scheduling...\"\n : isEdit\n ? \"Save Changes\"\n : \"Schedule\"}\n </button>\n </>\n }\n >\n {error && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n {isEdit\n ? \"Failed to update meeting. Please try again.\"\n : \"Failed to schedule meeting. Please try again.\"}\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className={labelClass}>Meeting Title</label>\n <input\n type=\"text\"\n placeholder=\"Enter meeting title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClass}>Type</label>\n <select\n value={meetingType}\n onChange={(e) => setMeetingType(e.target.value as MeetingType)}\n className={inputClass}\n >\n {MEETING_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className={labelClass}>Duration (minutes)</label>\n <input\n type=\"number\"\n min={0}\n value={duration}\n onChange={(e) => setDuration(Number(e.target.value))}\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className={labelClass}>Start</label>\n <input\n type=\"datetime-local\"\n value={start}\n onChange={(e) => setStart(e.target.value)}\n className={inputClass}\n />\n </div>\n <div>\n <label className={labelClass}>Recording</label>\n <input\n type=\"file\"\n accept=\"audio/*,video/*\"\n disabled={recordingUploading}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleRecordingUpload(file);\n // Reset so re-selecting the same file re-triggers onChange.\n e.target.value = \"\";\n }}\n className=\"mt-1 block w-full text-sm text-text-secondary file:mr-3 file:rounded-md file:border-0 file:bg-action-primary-bg file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-action-primary-text hover:file:bg-action-primary-bg/90\"\n />\n {recordingUploading && (\n <p className=\"mt-1 text-xs text-text-secondary\">Uploading…</p>\n )}\n {recordingError && (\n <p className=\"mt-1 text-xs text-status-error-text\">\n {recordingError}\n </p>\n )}\n <input\n type=\"url\"\n placeholder=\"…or paste a recording URL (https://...)\"\n value={recordingUrl}\n onChange={(e) => setRecordingUrl(e.target.value)}\n className={inputClass}\n />\n </div>\n <div>\n <label className={labelClass}>Notes / Agenda</label>\n <textarea\n placeholder=\"Meeting agenda...\"\n rows={3}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n className={inputClass}\n />\n </div>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MeetingFormModal.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingFormModal.tsx"],"sourcesContent":["import { useState, type FC } from \"react\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n MeetingType,\n type CreateMeetingInput,\n type UpdateMeetingInput,\n} from \"@/generated/wspace-types\";\nimport type { MeetingFieldsFragment } from \"@/generated/wspace-operations\";\n\n/**\n * Resolve a downloadable URL for a Files object. The URL is presigned and\n * expires after 7 days (604800s) — a known limitation: a stored recordingUrl\n * will eventually 403 and the recording must be re-uploaded to refresh it.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampus_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// wspace-files requires a real folder id (File.folderId FK).\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusMeeting_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\nconst MEETING_TYPES: { value: MeetingType; label: string }[] = [\n { value: MeetingType.Team, label: \"Team\" },\n { value: MeetingType.Standup, label: \"Standup\" },\n { value: MeetingType.OneOnOne, label: \"One-on-one\" },\n { value: MeetingType.Planning, label: \"Planning\" },\n { value: MeetingType.Review, label: \"Review\" },\n { value: MeetingType.Retrospective, label: \"Retrospective\" },\n { value: MeetingType.External, label: \"External\" },\n];\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\nconst labelClass = \"block text-sm font-medium text-text-primary\";\n\n/** Convert an ISO string into a value usable by a datetime-local input. */\nfunction toLocalInput(iso?: string | null): string {\n if (!iso) return \"\";\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n const pad = (n: number) => String(n).padStart(2, \"0\");\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(\n d.getHours(),\n )}:${pad(d.getMinutes())}`;\n}\n\nexport interface MeetingFormValues {\n title: string;\n meetingType: MeetingType;\n startTime: string;\n endTime?: string;\n notes?: string;\n recordingUrl?: string;\n cost?: number;\n}\n\ninterface MeetingFormModalProps {\n open: boolean;\n onClose: () => void;\n /** Existing meeting when editing; undefined when creating. */\n meeting?: MeetingFieldsFragment;\n workspaceId: string;\n submitting: boolean;\n error?: boolean;\n /** Receives a ready-to-send input; caller wires it to the right mutation. */\n onCreate?: (input: CreateMeetingInput) => Promise<void> | void;\n onUpdate?: (input: UpdateMeetingInput) => Promise<void> | void;\n}\n\nexport const MeetingFormModal: FC<MeetingFormModalProps> = ({\n open,\n onClose,\n meeting,\n workspaceId,\n submitting,\n error,\n onCreate,\n onUpdate,\n}) => {\n const isEdit = Boolean(meeting);\n const [title, setTitle] = useState(meeting?.title ?? \"\");\n const [meetingType, setMeetingType] = useState<MeetingType>(\n (meeting?.meetingType as MeetingType | undefined) ?? MeetingType.Team,\n );\n const [start, setStart] = useState(toLocalInput(meeting?.startTime));\n const [duration, setDuration] = useState<number>(meeting?.duration ?? 30);\n const [notes, setNotes] = useState(meeting?.notes ?? \"\");\n const [recordingUrl, setRecordingUrl] = useState(meeting?.recordingUrl ?? \"\");\n const apollo = useApolloClient();\n const [recordingUploading, setRecordingUploading] = useState(false);\n const [recordingError, setRecordingError] = useState<string | null>(null);\n\n const canSubmit = Boolean(title.trim() && start) && !submitting;\n\n /**\n * Upload a recording (audio/video) to platform Files and store its presigned\n * URL in the existing recordingUrl state. Failures surface an inline error\n * but never block submitting the meeting — the recording is optional and the\n * manual URL field remains available as a fallback.\n */\n const handleRecordingUpload = async (file: File) => {\n setRecordingUploading(true);\n setRecordingError(null);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/Recordings\" },\n });\n const folderId = (\n folderData as { ensureFolderByPath?: { id: string } } | null\n )?.ensureFolderByPath?.id;\n if (!folderId) throw new Error(\"Could not resolve upload folder\");\n const uploaded = await uploadBlobToFiles({\n client: apollo,\n blob: file,\n filename: file.name,\n mimeType: file.type || \"application/octet-stream\",\n folderId,\n });\n const { data } = await apollo.query({\n query: GET_FILE_DOWNLOAD_URL,\n variables: { id: uploaded.id, expiresIn: 604800 },\n fetchPolicy: \"no-cache\",\n });\n const url = (data as { getFileDownloadUrl?: string } | null | undefined)\n ?.getFileDownloadUrl;\n if (!url) throw new Error(\"No download URL returned\");\n setRecordingUrl(url);\n } catch {\n setRecordingError(\n \"Failed to upload recording. You can paste a URL instead.\",\n );\n } finally {\n setRecordingUploading(false);\n }\n };\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n const startIso = new Date(start).toISOString();\n const endIso =\n duration > 0\n ? new Date(new Date(start).getTime() + duration * 60_000).toISOString()\n : undefined;\n\n if (isEdit && onUpdate) {\n await onUpdate({\n title: title.trim(),\n meetingType,\n startTime: startIso,\n endTime: endIso,\n notes: notes.trim() || undefined,\n recordingUrl: recordingUrl.trim() || undefined,\n });\n } else if (onCreate) {\n await onCreate({\n workspaceId,\n title: title.trim(),\n meetingType,\n startTime: startIso,\n endTime: endIso,\n notes: notes.trim() || undefined,\n recordingUrl: recordingUrl.trim() || undefined,\n });\n }\n };\n\n return (\n <Modal\n open={open}\n onClose={onClose}\n title={isEdit ? \"Edit Meeting\" : \"Schedule Meeting\"}\n description={\n isEdit\n ? \"Update this meeting's details.\"\n : \"Add a meeting to track its time, cost and follow-ups.\"\n }\n size=\"xl\"\n footer={\n <>\n <button\n onClick={onClose}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={!canSubmit}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {submitting\n ? isEdit\n ? \"Saving...\"\n : \"Scheduling...\"\n : isEdit\n ? \"Save Changes\"\n : \"Schedule\"}\n </button>\n </>\n }\n >\n {error && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n {isEdit\n ? \"Failed to update meeting. Please try again.\"\n : \"Failed to schedule meeting. Please try again.\"}\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className={labelClass}>Meeting Title</label>\n <input\n type=\"text\"\n placeholder=\"Enter meeting title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClass}>Type</label>\n <select\n value={meetingType}\n onChange={(e) => setMeetingType(e.target.value as MeetingType)}\n className={inputClass}\n >\n {MEETING_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className={labelClass}>Duration (minutes)</label>\n <input\n type=\"number\"\n min={0}\n value={duration}\n onChange={(e) => setDuration(Number(e.target.value))}\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className={labelClass}>Start</label>\n <input\n type=\"datetime-local\"\n value={start}\n onChange={(e) => setStart(e.target.value)}\n className={inputClass}\n />\n </div>\n <div>\n <label className={labelClass}>Recording</label>\n <input\n type=\"file\"\n accept=\"audio/*,video/*\"\n disabled={recordingUploading}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleRecordingUpload(file);\n // Reset so re-selecting the same file re-triggers onChange.\n e.target.value = \"\";\n }}\n className=\"mt-1 block w-full text-sm text-text-secondary file:mr-3 file:rounded-md file:border-0 file:bg-action-primary-bg file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-action-primary-text hover:file:bg-action-primary-bg/90\"\n />\n {recordingUploading && (\n <p className=\"mt-1 text-xs text-text-secondary\">Uploading…</p>\n )}\n {recordingError && (\n <p className=\"mt-1 text-xs text-status-error-text\">\n {recordingError}\n </p>\n )}\n <input\n type=\"url\"\n placeholder=\"…or paste a recording URL (https://...)\"\n value={recordingUrl}\n onChange={(e) => setRecordingUrl(e.target.value)}\n className={inputClass}\n />\n </div>\n <div>\n <label className={labelClass}>Notes / Agenda</label>\n <textarea\n placeholder=\"Meeting agenda...\"\n rows={3}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n className={inputClass}\n />\n </div>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MeetingsPage.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingsPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { CalendarDays, Plus, Video, Pencil, Trash2, Star } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n cn,\n formatCurrency,\n formatDate,\n formatTime,\n} from \"../../utils/helpers\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n useListMeetingsQuery,\n useCreateMeetingMutation,\n useUpdateMeetingMutation,\n useDeleteMeetingMutation,\n type MeetingFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport {\n MeetingType,\n type CreateMeetingInput,\n type UpdateMeetingInput,\n} from \"@/generated/wspace-types\";\nimport { MeetingFormModal } from \"./MeetingFormModal\";\n\nconst PAGE_SIZE = 25;\n\nconst MEETING_TYPE_LABELS: Record<MeetingType, string> = {\n [MeetingType.Standup]: \"Standup\",\n [MeetingType.OneOnOne]: \"1-on-1\",\n [MeetingType.Team]: \"Team\",\n [MeetingType.Review]: \"Review\",\n [MeetingType.Planning]: \"Planning\",\n [MeetingType.Retrospective]: \"Retro\",\n [MeetingType.External]: \"External\",\n};\n\nfunction meetingTypeLabel(type: MeetingType): string {\n return MEETING_TYPE_LABELS[type] ?? type;\n}\n\n/** Deterministic 2-char monogram from an opaque participant id. */\nfunction idMonogram(id: string): string {\n return (\n id\n .replace(/[^a-zA-Z0-9]/g, \"\")\n .slice(0, 2)\n .toUpperCase() || \"?\"\n );\n}\n\nfunction ParticipantAvatars({\n participants,\n}: {\n participants: MeetingFieldsFragment[\"participants\"];\n}) {\n const shown = participants.slice(0, 4);\n const overflow = participants.length - shown.length;\n return (\n <div className=\"flex items-center -space-x-2\">\n {shown.map((p) => (\n <span\n key={p.id}\n title={p.userId}\n className=\"flex h-8 w-8 items-center justify-center rounded-full bg-action-primary-bg/10 text-[11px] font-semibold text-primary ring-2 ring-bg-surface\"\n >\n {idMonogram(p.userId)}\n </span>\n ))}\n {overflow > 0 && (\n <span className=\"flex h-8 w-8 items-center justify-center rounded-full bg-bg-sunken text-[11px] font-semibold text-text-secondary ring-2 ring-bg-surface\">\n +{overflow}\n </span>\n )}\n </div>\n );\n}\n\nfunction StarRating({ rating }: { rating: number | null | undefined }) {\n const value = rating ?? 0;\n return (\n <div className=\"flex items-center gap-0.5\">\n {[1, 2, 3, 4, 5].map((v) => (\n <Star\n key={v}\n className={cn(\n \"h-3.5 w-3.5\",\n v <= value\n ? \"fill-status-warning-text text-status-warning-text\"\n : \"text-border-seam\",\n )}\n />\n ))}\n <span className=\"ml-1 text-xs font-medium tabular-nums text-text-secondary\">\n {rating != null ? rating.toFixed(1) : \"—\"}\n </span>\n </div>\n );\n}\n\nexport function MeetingsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n\n const [limit, setLimit] = useState(PAGE_SIZE);\n const [createOpen, setCreateOpen] = useState(false);\n const [editing, setEditing] = useState<MeetingFieldsFragment | null>(null);\n const [deleting, setDeleting] = useState<MeetingFieldsFragment | null>(null);\n\n const { data, loading, error, refetch, fetchMore } = useListMeetingsQuery({\n variables: { workspaceId, pagination: { limit, offset: 0 } },\n skip: !workspaceId,\n notifyOnNetworkStatusChange: true,\n });\n\n const [createMeeting, { loading: creating, error: createError }] =\n useCreateMeetingMutation();\n const [updateMeeting, { loading: updating, error: updateError }] =\n useUpdateMeetingMutation();\n const [deleteMeeting, { loading: deletingLoading }] =\n useDeleteMeetingMutation();\n\n const meetings = data?.meetings ?? [];\n const hasMore = meetings.length >= limit;\n\n const handleCreate = async (input: CreateMeetingInput) => {\n await createMeeting({ variables: { input } });\n await refetch();\n setCreateOpen(false);\n };\n\n const handleUpdate = async (input: UpdateMeetingInput) => {\n if (!editing) return;\n await updateMeeting({\n variables: { id: editing.id, workspaceId, input },\n });\n await refetch();\n setEditing(null);\n };\n\n const handleDelete = async () => {\n if (!deleting) return;\n await deleteMeeting({\n variables: { id: deleting.id, workspaceId },\n });\n setDeleting(null);\n await refetch();\n };\n\n const handleLoadMore = async () => {\n const nextLimit = limit + PAGE_SIZE;\n setLimit(nextLimit);\n await fetchMore({\n variables: { pagination: { limit: nextLimit, offset: 0 } },\n });\n };\n\n const addButton = (\n <button\n onClick={() => setCreateOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Schedule Meeting\n </button>\n );\n\n if (loading && meetings.length === 0) {\n return (\n <PageLayout\n title=\"Meetings\"\n description=\"Schedule and track meetings\"\n actions={addButton}\n >\n <LoadingSkeleton variant=\"list\" count={5} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout title=\"Meetings\" description=\"Schedule and track meetings\">\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load meetings. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Meetings\"\n description=\"Schedule and track meetings\"\n purpose=\"Keep every meeting and its recording in one place and see how much of your week goes to them — so you can protect focus time, trim low-value calls, and account for meeting hours in your tracking.\"\n actions={addButton}\n >\n {meetings.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No meetings found\"\n description=\"Schedule your first meeting to start tracking calls and their time impact.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"space-y-3\">\n {meetings.map((meeting: MeetingFieldsFragment) => (\n <GlassCard\n key={meeting.id}\n treatment=\"glass\"\n role=\"button\"\n tabIndex={0}\n onClick={() => navigate(`/meetings/${meeting.id}`)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n navigate(`/meetings/${meeting.id}`);\n }\n }}\n className=\"cursor-pointer p-4 transition-colors hover:bg-bg-sunken/30 sm:p-5\"\n >\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between\">\n {/* Title + meta */}\n <div className=\"flex min-w-0 items-start gap-3\">\n <div className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <CalendarDays className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {meeting.title}\n </p>\n {meeting.recordingUrl && (\n <Video\n className=\"h-3.5 w-3.5 shrink-0 text-primary\"\n aria-label=\"Has recording\"\n />\n )}\n </div>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatDate(meeting.startTime)} ·{\" \"}\n {formatTime(meeting.startTime)}\n {meeting.endTime\n ? `–${formatTime(meeting.endTime)}`\n : \"\"}{\" \"}\n · {meeting.duration ?? 0} min\n </p>\n <div className=\"mt-2 flex flex-wrap items-center gap-1.5\">\n <StatusBadge\n label={meetingTypeLabel(meeting.meetingType)}\n variant=\"default\"\n />\n <StatusBadge\n label={meeting.endTime ? \"Completed\" : \"Scheduled\"}\n variant={meeting.endTime ? \"success\" : \"info\"}\n dot\n />\n </div>\n </div>\n </div>\n\n {/* Metrics + actions */}\n <div className=\"flex flex-wrap items-center gap-x-6 gap-y-3 lg:shrink-0 lg:justify-end\">\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Est. cost\n </p>\n <p className=\"text-sm font-semibold tabular-nums text-primary\">\n {meeting.cost != null\n ? formatCurrency(meeting.cost)\n : \"—\"}\n </p>\n </div>\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Rating\n </p>\n <StarRating rating={meeting.rating} />\n </div>\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n {meeting.participants.length}{\" \"}\n {meeting.participants.length === 1\n ? \"participant\"\n : \"participants\"}\n </p>\n {meeting.participants.length > 0 ? (\n <div className=\"mt-0.5\">\n <ParticipantAvatars\n participants={meeting.participants}\n />\n </div>\n ) : (\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n None yet\n </p>\n )}\n </div>\n <div className=\"flex items-center gap-1\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n setEditing(meeting);\n }}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Edit meeting\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n onClick={(e) => {\n e.stopPropagation();\n setDeleting(meeting);\n }}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Delete meeting\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </div>\n </GlassCard>\n ))}\n </div>\n {hasMore && (\n <div className=\"mt-4 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loading}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loading ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n {createOpen && (\n <MeetingFormModal\n open\n onClose={() => setCreateOpen(false)}\n workspaceId={workspaceId}\n submitting={creating}\n error={Boolean(createError)}\n onCreate={handleCreate}\n />\n )}\n\n {editing && (\n <MeetingFormModal\n open\n onClose={() => setEditing(null)}\n workspaceId={workspaceId}\n meeting={editing}\n submitting={updating}\n error={Boolean(updateError)}\n onUpdate={handleUpdate}\n />\n )}\n\n <Modal\n open={Boolean(deleting)}\n onClose={() => setDeleting(null)}\n title=\"Delete meeting\"\n description=\"This permanently removes the meeting and its analytics.\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleting(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deletingLoading}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90 disabled:opacity-50\"\n >\n {deletingLoading ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Are you sure you want to delete{\" \"}\n <span className=\"font-medium text-text-primary\">\n {deleting?.title}\n </span>\n ? This action cannot be undone.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MeetingsPage.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingsPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { CalendarDays, Plus, Video, Pencil, Trash2, Star } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n cn,\n formatCurrency,\n formatDate,\n formatTime,\n} from \"../../utils/helpers\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n useListMeetingsQuery,\n useCreateMeetingMutation,\n useUpdateMeetingMutation,\n useDeleteMeetingMutation,\n type MeetingFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport {\n MeetingType,\n type CreateMeetingInput,\n type UpdateMeetingInput,\n} from \"@/generated/wspace-types\";\nimport { MeetingFormModal } from \"./MeetingFormModal\";\n\nconst PAGE_SIZE = 25;\n\nconst MEETING_TYPE_LABELS: Record<MeetingType, string> = {\n [MeetingType.Standup]: \"Standup\",\n [MeetingType.OneOnOne]: \"1-on-1\",\n [MeetingType.Team]: \"Team\",\n [MeetingType.Review]: \"Review\",\n [MeetingType.Planning]: \"Planning\",\n [MeetingType.Retrospective]: \"Retro\",\n [MeetingType.External]: \"External\",\n};\n\nfunction meetingTypeLabel(type: MeetingType): string {\n return MEETING_TYPE_LABELS[type] ?? type;\n}\n\n/** Deterministic 2-char monogram from an opaque participant id. */\nfunction idMonogram(id: string): string {\n return (\n id\n .replace(/[^a-zA-Z0-9]/g, \"\")\n .slice(0, 2)\n .toUpperCase() || \"?\"\n );\n}\n\nfunction ParticipantAvatars({\n participants,\n}: {\n participants: MeetingFieldsFragment[\"participants\"];\n}) {\n const shown = participants.slice(0, 4);\n const overflow = participants.length - shown.length;\n return (\n <div className=\"flex items-center -space-x-2\">\n {shown.map((p) => (\n <span\n key={p.id}\n title={p.userId}\n className=\"flex h-8 w-8 items-center justify-center rounded-full bg-action-primary-bg/10 text-[11px] font-semibold text-primary ring-2 ring-bg-surface\"\n >\n {idMonogram(p.userId)}\n </span>\n ))}\n {overflow > 0 && (\n <span className=\"flex h-8 w-8 items-center justify-center rounded-full bg-bg-sunken text-[11px] font-semibold text-text-secondary ring-2 ring-bg-surface\">\n +{overflow}\n </span>\n )}\n </div>\n );\n}\n\nfunction StarRating({ rating }: { rating: number | null | undefined }) {\n const value = rating ?? 0;\n return (\n <div className=\"flex items-center gap-0.5\">\n {[1, 2, 3, 4, 5].map((v) => (\n <Star\n key={v}\n className={cn(\n \"h-3.5 w-3.5\",\n v <= value\n ? \"fill-status-warning-text text-status-warning-text\"\n : \"text-border-seam\",\n )}\n />\n ))}\n <span className=\"ml-1 text-xs font-medium tabular-nums text-text-secondary\">\n {rating != null ? rating.toFixed(1) : \"—\"}\n </span>\n </div>\n );\n}\n\nexport function MeetingsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n\n const [limit, setLimit] = useState(PAGE_SIZE);\n const [createOpen, setCreateOpen] = useState(false);\n const [editing, setEditing] = useState<MeetingFieldsFragment | null>(null);\n const [deleting, setDeleting] = useState<MeetingFieldsFragment | null>(null);\n\n const { data, loading, error, refetch, fetchMore } = useListMeetingsQuery({\n variables: { workspaceId, pagination: { limit, offset: 0 } },\n skip: !workspaceId,\n notifyOnNetworkStatusChange: true,\n });\n\n const [createMeeting, { loading: creating, error: createError }] =\n useCreateMeetingMutation();\n const [updateMeeting, { loading: updating, error: updateError }] =\n useUpdateMeetingMutation();\n const [deleteMeeting, { loading: deletingLoading }] =\n useDeleteMeetingMutation();\n\n const meetings = data?.meetings ?? [];\n const hasMore = meetings.length >= limit;\n\n const handleCreate = async (input: CreateMeetingInput) => {\n await createMeeting({ variables: { input } });\n await refetch();\n setCreateOpen(false);\n };\n\n const handleUpdate = async (input: UpdateMeetingInput) => {\n if (!editing) return;\n await updateMeeting({\n variables: { id: editing.id, workspaceId, input },\n });\n await refetch();\n setEditing(null);\n };\n\n const handleDelete = async () => {\n if (!deleting) return;\n await deleteMeeting({\n variables: { id: deleting.id, workspaceId },\n });\n setDeleting(null);\n await refetch();\n };\n\n const handleLoadMore = async () => {\n const nextLimit = limit + PAGE_SIZE;\n setLimit(nextLimit);\n await fetchMore({\n variables: { pagination: { limit: nextLimit, offset: 0 } },\n });\n };\n\n const addButton = (\n <button\n onClick={() => setCreateOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Schedule Meeting\n </button>\n );\n\n if (loading && meetings.length === 0) {\n return (\n <PageLayout\n title=\"Meetings\"\n description=\"Schedule and track meetings\"\n actions={addButton}\n >\n <LoadingSkeleton variant=\"list\" count={5} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout title=\"Meetings\" description=\"Schedule and track meetings\">\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load meetings. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Meetings\"\n description=\"Schedule and track meetings\"\n purpose=\"Keep every meeting and its recording in one place and see how much of your week goes to them — so you can protect focus time, trim low-value calls, and account for meeting hours in your tracking.\"\n actions={addButton}\n >\n {meetings.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No meetings found\"\n description=\"Schedule your first meeting to start tracking calls and their time impact.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"space-y-3\">\n {meetings.map((meeting: MeetingFieldsFragment) => (\n <GlassCard\n key={meeting.id}\n treatment=\"glass\"\n role=\"button\"\n tabIndex={0}\n onClick={() => navigate(`/meetings/${meeting.id}`)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n navigate(`/meetings/${meeting.id}`);\n }\n }}\n className=\"cursor-pointer p-4 transition-colors hover:bg-bg-sunken/30 sm:p-5\"\n >\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between\">\n {/* Title + meta */}\n <div className=\"flex min-w-0 items-start gap-3\">\n <div className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <CalendarDays className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {meeting.title}\n </p>\n {meeting.recordingUrl && (\n <Video\n className=\"h-3.5 w-3.5 shrink-0 text-primary\"\n aria-label=\"Has recording\"\n />\n )}\n </div>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatDate(meeting.startTime)} ·{\" \"}\n {formatTime(meeting.startTime)}\n {meeting.endTime\n ? `–${formatTime(meeting.endTime)}`\n : \"\"}{\" \"}\n · {meeting.duration ?? 0} min\n </p>\n <div className=\"mt-2 flex flex-wrap items-center gap-1.5\">\n <StatusBadge\n label={meetingTypeLabel(meeting.meetingType)}\n variant=\"default\"\n />\n <StatusBadge\n label={meeting.endTime ? \"Completed\" : \"Scheduled\"}\n variant={meeting.endTime ? \"success\" : \"info\"}\n dot\n />\n </div>\n </div>\n </div>\n\n {/* Metrics + actions */}\n <div className=\"flex flex-wrap items-center gap-x-6 gap-y-3 lg:shrink-0 lg:justify-end\">\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Est. cost\n </p>\n <p className=\"text-sm font-semibold tabular-nums text-primary\">\n {meeting.cost != null\n ? formatCurrency(meeting.cost)\n : \"—\"}\n </p>\n </div>\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Rating\n </p>\n <StarRating rating={meeting.rating} />\n </div>\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n {meeting.participants.length}{\" \"}\n {meeting.participants.length === 1\n ? \"participant\"\n : \"participants\"}\n </p>\n {meeting.participants.length > 0 ? (\n <div className=\"mt-0.5\">\n <ParticipantAvatars\n participants={meeting.participants}\n />\n </div>\n ) : (\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n None yet\n </p>\n )}\n </div>\n <div className=\"flex items-center gap-1\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n setEditing(meeting);\n }}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Edit meeting\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n onClick={(e) => {\n e.stopPropagation();\n setDeleting(meeting);\n }}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Delete meeting\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </div>\n </GlassCard>\n ))}\n </div>\n {hasMore && (\n <div className=\"mt-4 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loading}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loading ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n {createOpen && (\n <MeetingFormModal\n open\n onClose={() => setCreateOpen(false)}\n workspaceId={workspaceId}\n submitting={creating}\n error={Boolean(createError)}\n onCreate={handleCreate}\n />\n )}\n\n {editing && (\n <MeetingFormModal\n open\n onClose={() => setEditing(null)}\n workspaceId={workspaceId}\n meeting={editing}\n submitting={updating}\n error={Boolean(updateError)}\n onUpdate={handleUpdate}\n />\n )}\n\n <Modal\n open={Boolean(deleting)}\n onClose={() => setDeleting(null)}\n title=\"Delete meeting\"\n description=\"This permanently removes the meeting and its analytics.\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleting(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deletingLoading}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90 disabled:opacity-50\"\n >\n {deletingLoading ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Are you sure you want to delete{\" \"}\n <span className=\"font-medium text-text-primary\">\n {deleting?.title}\n </span>\n ? This action cannot be undone.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ParticipantFormModal.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/ParticipantFormModal.tsx"],"sourcesContent":["import { useState, type FC } from \"react\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n MeetingParticipantRole,\n MeetingParticipantStatus,\n type AddMeetingParticipantInput,\n} from \"@/generated/wspace-types\";\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\nconst labelClass = \"block text-sm font-medium text-text-primary\";\n\nconst ROLES: { value: MeetingParticipantRole; label: string }[] = [\n { value: MeetingParticipantRole.Required, label: \"Required\" },\n { value: MeetingParticipantRole.Optional, label: \"Optional\" },\n { value: MeetingParticipantRole.Organizer, label: \"Organizer\" },\n];\n\nconst STATUSES: { value: MeetingParticipantStatus; label: string }[] = [\n { value: MeetingParticipantStatus.Pending, label: \"Pending\" },\n { value: MeetingParticipantStatus.Accepted, label: \"Accepted\" },\n { value: MeetingParticipantStatus.Tentative, label: \"Tentative\" },\n { value: MeetingParticipantStatus.Declined, label: \"Declined\" },\n];\n\ninterface ParticipantFormModalProps {\n open: boolean;\n onClose: () => void;\n defaultUserId?: string;\n submitting: boolean;\n error?: boolean;\n onSubmit: (input: AddMeetingParticipantInput) => Promise<void> | void;\n}\n\nexport const ParticipantFormModal: FC<ParticipantFormModalProps> = ({\n open,\n onClose,\n defaultUserId,\n submitting,\n error,\n onSubmit,\n}) => {\n const [userId, setUserId] = useState(defaultUserId ?? \"\");\n const [role, setRole] = useState<MeetingParticipantRole>(\n MeetingParticipantRole.Required,\n );\n const [status, setStatus] = useState<MeetingParticipantStatus>(\n MeetingParticipantStatus.Pending,\n );\n\n const canSubmit = Boolean(userId.trim()) && !submitting;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n await onSubmit({ userId: userId.trim(), role, status });\n };\n\n return (\n <Modal\n open={open}\n onClose={onClose}\n title=\"Add participant\"\n description=\"Record who attended so their time counts toward this meeting's cost.\"\n footer={\n <>\n <button\n onClick={onClose}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={!canSubmit}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {submitting ? \"Adding...\" : \"Add participant\"}\n </button>\n </>\n }\n >\n {error && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to add participant. Please try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className={labelClass}>User ID</label>\n <input\n type=\"text\"\n placeholder=\"User ID\"\n value={userId}\n onChange={(e) => setUserId(e.target.value)}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClass}>Role</label>\n <select\n value={role}\n onChange={(e) =>\n setRole(e.target.value as MeetingParticipantRole)\n }\n className={inputClass}\n >\n {ROLES.map((r) => (\n <option key={r.value} value={r.value}>\n {r.label}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className={labelClass}>Status</label>\n <select\n value={status}\n onChange={(e) =>\n setStatus(e.target.value as MeetingParticipantStatus)\n }\n className={inputClass}\n >\n {STATUSES.map((s) => (\n <option key={s.value} value={s.value}>\n {s.label}\n </option>\n ))}\n </select>\n </div>\n </div>\n </div>\n </Modal>\n );\n};\n"],"mappings":";;;;;AAQA,IAAM,IACJ,wIACI,IAAa,+CAEb,IAA4D;CAChE;EAAE,OAAO,EAAuB;EAAU,OAAO;
|
|
1
|
+
{"version":3,"file":"ParticipantFormModal.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/ParticipantFormModal.tsx"],"sourcesContent":["import { useState, type FC } from \"react\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n MeetingParticipantRole,\n MeetingParticipantStatus,\n type AddMeetingParticipantInput,\n} from \"@/generated/wspace-types\";\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\nconst labelClass = \"block text-sm font-medium text-text-primary\";\n\nconst ROLES: { value: MeetingParticipantRole; label: string }[] = [\n { value: MeetingParticipantRole.Required, label: \"Required\" },\n { value: MeetingParticipantRole.Optional, label: \"Optional\" },\n { value: MeetingParticipantRole.Organizer, label: \"Organizer\" },\n];\n\nconst STATUSES: { value: MeetingParticipantStatus; label: string }[] = [\n { value: MeetingParticipantStatus.Pending, label: \"Pending\" },\n { value: MeetingParticipantStatus.Accepted, label: \"Accepted\" },\n { value: MeetingParticipantStatus.Tentative, label: \"Tentative\" },\n { value: MeetingParticipantStatus.Declined, label: \"Declined\" },\n];\n\ninterface ParticipantFormModalProps {\n open: boolean;\n onClose: () => void;\n defaultUserId?: string;\n submitting: boolean;\n error?: boolean;\n onSubmit: (input: AddMeetingParticipantInput) => Promise<void> | void;\n}\n\nexport const ParticipantFormModal: FC<ParticipantFormModalProps> = ({\n open,\n onClose,\n defaultUserId,\n submitting,\n error,\n onSubmit,\n}) => {\n const [userId, setUserId] = useState(defaultUserId ?? \"\");\n const [role, setRole] = useState<MeetingParticipantRole>(\n MeetingParticipantRole.Required,\n );\n const [status, setStatus] = useState<MeetingParticipantStatus>(\n MeetingParticipantStatus.Pending,\n );\n\n const canSubmit = Boolean(userId.trim()) && !submitting;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n await onSubmit({ userId: userId.trim(), role, status });\n };\n\n return (\n <Modal\n open={open}\n onClose={onClose}\n title=\"Add participant\"\n description=\"Record who attended so their time counts toward this meeting's cost.\"\n footer={\n <>\n <button\n onClick={onClose}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={!canSubmit}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {submitting ? \"Adding...\" : \"Add participant\"}\n </button>\n </>\n }\n >\n {error && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to add participant. Please try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className={labelClass}>User ID</label>\n <input\n type=\"text\"\n placeholder=\"User ID\"\n value={userId}\n onChange={(e) => setUserId(e.target.value)}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClass}>Role</label>\n <select\n value={role}\n onChange={(e) =>\n setRole(e.target.value as MeetingParticipantRole)\n }\n className={inputClass}\n >\n {ROLES.map((r) => (\n <option key={r.value} value={r.value}>\n {r.label}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className={labelClass}>Status</label>\n <select\n value={status}\n onChange={(e) =>\n setStatus(e.target.value as MeetingParticipantStatus)\n }\n className={inputClass}\n >\n {STATUSES.map((s) => (\n <option key={s.value} value={s.value}>\n {s.label}\n </option>\n ))}\n </select>\n </div>\n </div>\n </div>\n </Modal>\n );\n};\n"],"mappings":";;;;;AAQA,IAAM,IACJ,wIACI,IAAa,+CAEb,IAA4D;CAChE;EAAE,OAAO,EAAuB;EAAU,OAAO;CAAW;CAC5D;EAAE,OAAO,EAAuB;EAAU,OAAO;CAAW;CAC5D;EAAE,OAAO,EAAuB;EAAW,OAAO;CAAY;AAChE,GAEM,IAAiE;CACrE;EAAE,OAAO,EAAyB;EAAS,OAAO;CAAU;CAC5D;EAAE,OAAO,EAAyB;EAAU,OAAO;CAAW;CAC9D;EAAE,OAAO,EAAyB;EAAW,OAAO;CAAY;CAChE;EAAE,OAAO,EAAyB;EAAU,OAAO;CAAW;AAChE,GAWa,KAAuD,EAClE,SACA,YACA,kBACA,eACA,UACA,kBACI;CACJ,IAAM,CAAC,GAAQ,KAAa,EAAS,KAAiB,EAAE,GAClD,CAAC,GAAM,KAAW,EACtB,EAAuB,QACzB,GACM,CAAC,GAAQ,KAAa,EAC1B,EAAyB,OAC3B,GAEM,IAAY,EAAQ,EAAO,KAAK,KAAM,CAAC;CAO7C,OACE,kBAAC,GAAD;EACQ;EACG;EACT,OAAM;EACN,aAAY;EACZ,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;GACE,SAAS;GACT,WAAU;GACX,UAAA;EAEO,CAAA,GACR,kBAAC,UAAD;GACE,SAAS,YApBc;IAC1B,KACL,MAAM,EAAS;KAAE,QAAQ,EAAO,KAAK;KAAG;KAAM;IAAO,CAAC;GACxD;GAkBU,UAAU,CAAC;GACX,WAAU;GAET,UAAA,IAAa,cAAc;EACtB,CAAA,CACR,EAAA,CAAA;EApBN,UAAA,CAuBG,KACC,kBAAC,OAAD;GAAK,WAAU;GAAyG,UAAA;EAEnH,CAAA,GAEP,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;IAAO,WAAW;IAAY,UAAA;GAAc,CAAA,GAC5C,kBAAC,SAAD;IACE,MAAK;IACL,aAAY;IACZ,OAAO;IACP,WAAW,MAAM,EAAU,EAAE,OAAO,KAAK;IACzC,WAAW;GACZ,CAAA,CACE,EAAA,CAAA,GACL,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;KAAO,WAAW;KAAY,UAAA;IAAW,CAAA,GACzC,kBAAC,UAAD;KACE,OAAO;KACP,WAAW,MACT,EAAQ,EAAE,OAAO,KAA+B;KAElD,WAAW;KAEV,UAAA,EAAM,KAAK,MACV,kBAAC,UAAD;MAAsB,OAAO,EAAE;MAC5B,UAAA,EAAE;KACG,GAFK,EAAE,KAEP,CACT;IACK,CAAA,CACL,EAAA,CAAA,GACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;KAAO,WAAW;KAAY,UAAA;IAAa,CAAA,GAC3C,kBAAC,UAAD;KACE,OAAO;KACP,WAAW,MACT,EAAU,EAAE,OAAO,KAAiC;KAEtD,WAAW;KAEV,UAAA,EAAS,KAAK,MACb,kBAAC,UAAD;MAAsB,OAAO,EAAE;MAC5B,UAAA,EAAE;KACG,GAFK,EAAE,KAEP,CACT;IACK,CAAA,CACL,EAAA,CAAA,CACF;GACF,CAAA,CAAA;EACA,CAAA,CAAA;;AAEX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PomodoroAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/pomodoro/PomodoroAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, Target, Flame, Timer, Coffee } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n BarsChart,\n DonutChart,\n ProgressMeter,\n StatusBadge,\n LoadingSkeleton,\n formatNumber,\n type DonutSlice,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetPomodoroStatsQuery,\n useListPomodoroSessionsQuery,\n type PomodoroSessionFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { PomodoroSessionType } from \"@/generated/wspace-types\";\n\nfunction formatMinutes(min: number): string {\n const total = Math.max(0, Math.round(min));\n const h = Math.floor(total / 60);\n const m = total % 60;\n return h > 0 ? `${h}h ${m}m` : `${m}m`;\n}\n\nfunction shortDay(ts: number): string {\n return new Date(ts).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n });\n}\n\nfunction shortTime(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\nconst isBreak = (t: PomodoroSessionType): boolean =>\n t === PomodoroSessionType.ShortBreak || t === PomodoroSessionType.LongBreak;\n\nexport function PomodoroAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetPomodoroStatsQuery({\n variables: { workspaceId },\n skip: !workspaceId,\n });\n\n const { data: sessionsData, loading: sessionsLoading } =\n useListPomodoroSessionsQuery({\n variables: { workspaceId, pagination: { limit: 50 } },\n skip: !workspaceId,\n });\n\n const stats = statsData?.pomodoroStats;\n const sessions = sessionsData?.pomodoroSessions ?? [];\n\n if (statsLoading || sessionsLoading) {\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load pomodoro analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalFocusH = Math.floor((stats?.totalFocusMinutes ?? 0) / 60);\n const totalFocusM = (stats?.totalFocusMinutes ?? 0) % 60;\n const avgSessions = stats?.totalSessions\n ? (stats.totalSessions / 30).toFixed(1)\n : \"0\";\n\n // ---- Daily completed focus sessions (BarsChart) --------------------------\n const dailyMap = new Map<\n string,\n { ts: number; label: string; completed: number }\n >();\n for (const s of sessions) {\n if (s.type !== PomodoroSessionType.Focus) continue;\n const d = new Date(s.startTime);\n if (Number.isNaN(d.getTime())) continue;\n const day = new Date(d.getFullYear(), d.getMonth(), d.getDate());\n const key = String(day.getTime());\n const entry = dailyMap.get(key) ?? {\n ts: day.getTime(),\n label: shortDay(day.getTime()),\n completed: 0,\n };\n if (s.completed) entry.completed += 1;\n dailyMap.set(key, entry);\n }\n const dailyData = [...dailyMap.values()]\n .sort((a, b) => a.ts - b.ts)\n .slice(-14)\n .map((e) => ({ day: e.label, completed: e.completed }));\n\n // ---- Focus vs break split (DonutChart) -----------------------------------\n let focusSeconds = 0;\n let breakSeconds = 0;\n let focusCount = 0;\n let breakCount = 0;\n for (const s of sessions) {\n const dur =\n typeof s.duration === \"number\" && Number.isFinite(s.duration)\n ? s.duration\n : 0;\n if (isBreak(s.type)) {\n breakSeconds += dur;\n breakCount += 1;\n } else {\n focusSeconds += dur;\n focusCount += 1;\n }\n }\n const useDuration = focusSeconds + breakSeconds > 0;\n const focusValue = useDuration ? Math.round(focusSeconds / 60) : focusCount;\n const breakValue = useDuration ? Math.round(breakSeconds / 60) : breakCount;\n const donutSlices: DonutSlice[] = [\n { name: \"Focus\", value: focusValue },\n { name: \"Break\", value: breakValue },\n ];\n const donutFormat = useDuration ? formatMinutes : formatNumber;\n\n // ---- Recent sessions -----------------------------------------------------\n const recentSessions = [...sessions]\n .sort(\n (a, b) =>\n new Date(b.startTime).getTime() - new Date(a.startTime).getTime(),\n )\n .slice(0, 6);\n\n const completionRate = stats?.completionRate ?? 0;\n const completedSessions = stats?.completedSessions ?? 0;\n const totalSessions = stats?.totalSessions ?? 0;\n\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n purpose=\"See whether your focus habit is sticking — completion rate, streaks and focus minutes over time — so you can tell good-focus days from distracted ones and keep your momentum going.\"\n >\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Sessions\"\n value={String(stats?.totalSessions ?? 0)}\n trend={{\n value: stats?.completionRate ?? 0,\n label: \"completion rate\",\n }}\n />\n <StatCard\n title=\"Total Focus Time\"\n value={`${totalFocusH}h ${totalFocusM}m`}\n icon={<Timer className=\"h-5 w-5\" />}\n trend={{\n value: stats?.averageSessionMinutes ?? 0,\n label: \"avg min/session\",\n }}\n />\n <StatCard\n title=\"Avg. Sessions/Day\"\n value={avgSessions}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={`Target: ${stats?.totalSessions ?? 0} total`}\n />\n <StatCard\n title=\"Completion Rate\"\n value={`${stats?.completionRate ?? 0}%`}\n icon={<Target className=\"h-5 w-5\" />}\n trend={{ value: stats?.streakDays ?? 0, label: \"day streak\" }}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <ChartCard\n className=\"lg:col-span-2\"\n title=\"Daily Completed Focus Sessions\"\n description=\"Pomodoros you finished, by day\"\n >\n <BarsChart\n data={dailyData}\n xKey=\"day\"\n series={[{ key: \"completed\", label: \"Completed\" }]}\n height={260}\n emptyLabel=\"No completed focus sessions yet.\"\n ariaLabel=\"Completed focus sessions by day\"\n />\n </ChartCard>\n\n <GlassCard treatment=\"glass\" glow className=\"p-5 sm:p-6\">\n <div className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Flame className=\"h-4 w-4 text-primary\" />\n Current Focus Streak\n </div>\n <p className=\"mt-2 text-4xl font-bold tabular-nums text-text-primary\">\n {stats?.streakDays ?? 0}\n <span className=\"ml-1 text-lg font-medium text-text-secondary\">\n days\n </span>\n </p>\n <div className=\"mt-6 space-y-5\">\n <ProgressMeter\n value={completionRate}\n max={100}\n higherIsWorse={false}\n label=\"Completion rate\"\n format={(v) => `${Math.round(v)}%`}\n />\n <ProgressMeter\n value={completedSessions}\n max={totalSessions > 0 ? totalSessions : 1}\n higherIsWorse={false}\n label=\"Sessions completed\"\n />\n </div>\n </GlassCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Focus vs Break\"\n description={\n useDuration\n ? \"Share of tracked time spent focusing vs. on breaks\"\n : \"Share of sessions by type\"\n }\n >\n <DonutChart\n data={donutSlices}\n centerLabel={useDuration ? \"total tracked\" : \"sessions\"}\n valueFormat={donutFormat}\n height={260}\n emptyLabel=\"No sessions recorded yet.\"\n ariaLabel=\"Focus versus break split\"\n />\n </ChartCard>\n\n <GlassCard treatment=\"glass\" className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Timer className=\"h-4 w-4 text-primary\" />\n Recent Sessions\n </h3>\n {recentSessions.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No focus sessions yet. Start a pomodoro to see it here.\n </p>\n ) : (\n <div className=\"space-y-2.5\">\n {recentSessions.map((s: PomodoroSessionFieldsFragment) => {\n const sessionIsBreak = isBreak(s.type);\n const durationLabel =\n typeof s.duration === \"number\" &&\n Number.isFinite(s.duration)\n ? `${Math.round(s.duration / 60)}m`\n : \"--\";\n return (\n <div\n key={s.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface/40 p-3\"\n >\n <div\n className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${\n sessionIsBreak\n ? \"bg-status-info-bg text-status-info-text\"\n : \"bg-action-primary-bg/10 text-primary\"\n }`}\n >\n {sessionIsBreak ? (\n <Coffee className=\"h-4 w-4\" />\n ) : (\n <Timer className=\"h-4 w-4\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {s.notes?.trim()\n ? s.notes\n : sessionIsBreak\n ? \"Break\"\n : \"Focus session\"}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {durationLabel} · {shortTime(s.startTime)}\n </p>\n </div>\n <StatusBadge\n variant={\n s.completed\n ? \"success\"\n : s.interrupted\n ? \"warning\"\n : \"muted\"\n }\n label={\n s.completed\n ? \"Completed\"\n : s.interrupted\n ? \"Interrupted\"\n : \"In progress\"\n }\n />\n </div>\n );\n })}\n </div>\n )}\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PomodoroAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/pomodoro/PomodoroAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, Target, Flame, Timer, Coffee } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n BarsChart,\n DonutChart,\n ProgressMeter,\n StatusBadge,\n LoadingSkeleton,\n formatNumber,\n type DonutSlice,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetPomodoroStatsQuery,\n useListPomodoroSessionsQuery,\n type PomodoroSessionFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { PomodoroSessionType } from \"@/generated/wspace-types\";\n\nfunction formatMinutes(min: number): string {\n const total = Math.max(0, Math.round(min));\n const h = Math.floor(total / 60);\n const m = total % 60;\n return h > 0 ? `${h}h ${m}m` : `${m}m`;\n}\n\nfunction shortDay(ts: number): string {\n return new Date(ts).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n });\n}\n\nfunction shortTime(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\nconst isBreak = (t: PomodoroSessionType): boolean =>\n t === PomodoroSessionType.ShortBreak || t === PomodoroSessionType.LongBreak;\n\nexport function PomodoroAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetPomodoroStatsQuery({\n variables: { workspaceId },\n skip: !workspaceId,\n });\n\n const { data: sessionsData, loading: sessionsLoading } =\n useListPomodoroSessionsQuery({\n variables: { workspaceId, pagination: { limit: 50 } },\n skip: !workspaceId,\n });\n\n const stats = statsData?.pomodoroStats;\n const sessions = sessionsData?.pomodoroSessions ?? [];\n\n if (statsLoading || sessionsLoading) {\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load pomodoro analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalFocusH = Math.floor((stats?.totalFocusMinutes ?? 0) / 60);\n const totalFocusM = (stats?.totalFocusMinutes ?? 0) % 60;\n const avgSessions = stats?.totalSessions\n ? (stats.totalSessions / 30).toFixed(1)\n : \"0\";\n\n // ---- Daily completed focus sessions (BarsChart) --------------------------\n const dailyMap = new Map<\n string,\n { ts: number; label: string; completed: number }\n >();\n for (const s of sessions) {\n if (s.type !== PomodoroSessionType.Focus) continue;\n const d = new Date(s.startTime);\n if (Number.isNaN(d.getTime())) continue;\n const day = new Date(d.getFullYear(), d.getMonth(), d.getDate());\n const key = String(day.getTime());\n const entry = dailyMap.get(key) ?? {\n ts: day.getTime(),\n label: shortDay(day.getTime()),\n completed: 0,\n };\n if (s.completed) entry.completed += 1;\n dailyMap.set(key, entry);\n }\n const dailyData = [...dailyMap.values()]\n .sort((a, b) => a.ts - b.ts)\n .slice(-14)\n .map((e) => ({ day: e.label, completed: e.completed }));\n\n // ---- Focus vs break split (DonutChart) -----------------------------------\n let focusSeconds = 0;\n let breakSeconds = 0;\n let focusCount = 0;\n let breakCount = 0;\n for (const s of sessions) {\n const dur =\n typeof s.duration === \"number\" && Number.isFinite(s.duration)\n ? s.duration\n : 0;\n if (isBreak(s.type)) {\n breakSeconds += dur;\n breakCount += 1;\n } else {\n focusSeconds += dur;\n focusCount += 1;\n }\n }\n const useDuration = focusSeconds + breakSeconds > 0;\n const focusValue = useDuration ? Math.round(focusSeconds / 60) : focusCount;\n const breakValue = useDuration ? Math.round(breakSeconds / 60) : breakCount;\n const donutSlices: DonutSlice[] = [\n { name: \"Focus\", value: focusValue },\n { name: \"Break\", value: breakValue },\n ];\n const donutFormat = useDuration ? formatMinutes : formatNumber;\n\n // ---- Recent sessions -----------------------------------------------------\n const recentSessions = [...sessions]\n .sort(\n (a, b) =>\n new Date(b.startTime).getTime() - new Date(a.startTime).getTime(),\n )\n .slice(0, 6);\n\n const completionRate = stats?.completionRate ?? 0;\n const completedSessions = stats?.completedSessions ?? 0;\n const totalSessions = stats?.totalSessions ?? 0;\n\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n purpose=\"See whether your focus habit is sticking — completion rate, streaks and focus minutes over time — so you can tell good-focus days from distracted ones and keep your momentum going.\"\n >\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Sessions\"\n value={String(stats?.totalSessions ?? 0)}\n trend={{\n value: stats?.completionRate ?? 0,\n label: \"completion rate\",\n }}\n />\n <StatCard\n title=\"Total Focus Time\"\n value={`${totalFocusH}h ${totalFocusM}m`}\n icon={<Timer className=\"h-5 w-5\" />}\n trend={{\n value: stats?.averageSessionMinutes ?? 0,\n label: \"avg min/session\",\n }}\n />\n <StatCard\n title=\"Avg. Sessions/Day\"\n value={avgSessions}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={`Target: ${stats?.totalSessions ?? 0} total`}\n />\n <StatCard\n title=\"Completion Rate\"\n value={`${stats?.completionRate ?? 0}%`}\n icon={<Target className=\"h-5 w-5\" />}\n trend={{ value: stats?.streakDays ?? 0, label: \"day streak\" }}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <ChartCard\n className=\"lg:col-span-2\"\n title=\"Daily Completed Focus Sessions\"\n description=\"Pomodoros you finished, by day\"\n >\n <BarsChart\n data={dailyData}\n xKey=\"day\"\n series={[{ key: \"completed\", label: \"Completed\" }]}\n height={260}\n emptyLabel=\"No completed focus sessions yet.\"\n ariaLabel=\"Completed focus sessions by day\"\n />\n </ChartCard>\n\n <GlassCard treatment=\"glass\" glow className=\"p-5 sm:p-6\">\n <div className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Flame className=\"h-4 w-4 text-primary\" />\n Current Focus Streak\n </div>\n <p className=\"mt-2 text-4xl font-bold tabular-nums text-text-primary\">\n {stats?.streakDays ?? 0}\n <span className=\"ml-1 text-lg font-medium text-text-secondary\">\n days\n </span>\n </p>\n <div className=\"mt-6 space-y-5\">\n <ProgressMeter\n value={completionRate}\n max={100}\n higherIsWorse={false}\n label=\"Completion rate\"\n format={(v) => `${Math.round(v)}%`}\n />\n <ProgressMeter\n value={completedSessions}\n max={totalSessions > 0 ? totalSessions : 1}\n higherIsWorse={false}\n label=\"Sessions completed\"\n />\n </div>\n </GlassCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Focus vs Break\"\n description={\n useDuration\n ? \"Share of tracked time spent focusing vs. on breaks\"\n : \"Share of sessions by type\"\n }\n >\n <DonutChart\n data={donutSlices}\n centerLabel={useDuration ? \"total tracked\" : \"sessions\"}\n valueFormat={donutFormat}\n height={260}\n emptyLabel=\"No sessions recorded yet.\"\n ariaLabel=\"Focus versus break split\"\n />\n </ChartCard>\n\n <GlassCard treatment=\"glass\" className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Timer className=\"h-4 w-4 text-primary\" />\n Recent Sessions\n </h3>\n {recentSessions.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No focus sessions yet. Start a pomodoro to see it here.\n </p>\n ) : (\n <div className=\"space-y-2.5\">\n {recentSessions.map((s: PomodoroSessionFieldsFragment) => {\n const sessionIsBreak = isBreak(s.type);\n const durationLabel =\n typeof s.duration === \"number\" &&\n Number.isFinite(s.duration)\n ? `${Math.round(s.duration / 60)}m`\n : \"--\";\n return (\n <div\n key={s.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface/40 p-3\"\n >\n <div\n className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${\n sessionIsBreak\n ? \"bg-status-info-bg text-status-info-text\"\n : \"bg-action-primary-bg/10 text-primary\"\n }`}\n >\n {sessionIsBreak ? (\n <Coffee className=\"h-4 w-4\" />\n ) : (\n <Timer className=\"h-4 w-4\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {s.notes?.trim()\n ? s.notes\n : sessionIsBreak\n ? \"Break\"\n : \"Focus session\"}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {durationLabel} · {shortTime(s.startTime)}\n </p>\n </div>\n <StatusBadge\n variant={\n s.completed\n ? \"success\"\n : s.interrupted\n ? \"warning\"\n : \"muted\"\n }\n label={\n s.completed\n ? \"Completed\"\n : s.interrupted\n ? \"Interrupted\"\n : \"In progress\"\n }\n />\n </div>\n );\n })}\n </div>\n )}\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|