@burdenoff/microfe-timecampus 2026.830.2 → 2026.903.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.
Files changed (54) hide show
  1. package/dist/timecampus/components/index.js +4 -4
  2. package/dist/timecampus/pages/HomePage.d.ts.map +1 -1
  3. package/dist/timecampus/pages/HomePage.js +142 -131
  4. package/dist/timecampus/pages/HomePage.js.map +1 -1
  5. package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +7 -7
  6. package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
  7. package/dist/timecampus/pages/client-portal/ClientPortalPage.js +7 -7
  8. package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
  9. package/dist/timecampus/pages/dashboard/ReportsPage.js +13 -13
  10. package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +7 -7
  11. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +7 -7
  12. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
  13. package/dist/timecampus/pages/focus-score/FocusScorePage.js +13 -13
  14. package/dist/timecampus/pages/geofencing/GeofencingPage.js +135 -135
  15. package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
  16. package/dist/timecampus/pages/invoicing/InvoicingPage.js +7 -7
  17. package/dist/timecampus/pages/invoicing/InvoicingPage.js.map +1 -1
  18. package/dist/timecampus/pages/lifetime/DeadTimeRadarPage.js +8 -8
  19. package/dist/timecampus/pages/lifetime/FiniteLifePage.js +4 -4
  20. package/dist/timecampus/pages/lifetime/FiniteLifePage.js.map +1 -1
  21. package/dist/timecampus/pages/lifetime/LifeBalancePage.js +104 -104
  22. package/dist/timecampus/pages/lifetime/LifeBalancePage.js.map +1 -1
  23. package/dist/timecampus/pages/lifetime/LifeTimelinePage.js +7 -7
  24. package/dist/timecampus/pages/lifetime/LifeTimelinePage.js.map +1 -1
  25. package/dist/timecampus/pages/lifetime/PersonalCorporatePage.js +33 -33
  26. package/dist/timecampus/pages/lifetime/PersonalCorporatePage.js.map +1 -1
  27. package/dist/timecampus/pages/lifetime/RecommendationsPage.js +80 -80
  28. package/dist/timecampus/pages/lifetime/RecommendationsPage.js.map +1 -1
  29. package/dist/timecampus/pages/lifetime/TimeWellSpentPage.js +16 -16
  30. package/dist/timecampus/pages/lifetime/ValuesGoalsPage.js +11 -11
  31. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +11 -11
  32. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
  33. package/dist/timecampus/pages/meetings/MeetingDetailPage.js +14 -14
  34. package/dist/timecampus/pages/meetings/MeetingDetailPage.js.map +1 -1
  35. package/dist/timecampus/pages/meetings/MeetingsPage.js +12 -12
  36. package/dist/timecampus/pages/projects/ProjectsPage.js +5 -5
  37. package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
  38. package/dist/timecampus/pages/projects/TasksPage.js +8 -8
  39. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +8 -8
  40. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
  41. package/dist/timecampus/pages/team/TeamMembersPage.js +8 -8
  42. package/dist/timecampus/pages/team-activity/TeamActivityPage.js +13 -13
  43. package/dist/timecampus/pages/time/TimeEntriesPage.js +5 -5
  44. package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
  45. package/dist/timecampus/pages/time/TimeTrackingPage.js +138 -138
  46. package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
  47. package/dist/timecampus/pages/time/TimerPage.js +14 -14
  48. package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
  49. package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +26 -26
  50. package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
  51. package/dist/timecampus/pages/timeline/TimelinePage.js +8 -8
  52. package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +79 -79
  53. package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
  54. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"TimeTrackingPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimeTrackingPage.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Clock, Play, Pause, Square } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { Modal } from \"../../components/Modal\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n ChartCard,\n DonutChart,\n BarsChart,\n StatusBadge,\n} from \"../../components\";\nimport { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport {\n formatHours,\n formatTimerDuration,\n getInitials,\n} from \"../../utils/helpers\";\nimport { useTimeCampusStore } from \"../../store/timecampusStore\";\nimport {\n useGetTimeEntryStatsQuery,\n useListTimeEntriesQuery,\n useListRunningTimersQuery,\n useStartTimerMutation,\n useStopTimerMutation,\n useListProjectsQuery,\n ListRunningTimersDocument,\n ListTimeEntriesDocument,\n GetTimeEntryStatsDocument,\n type TimeEntryFieldsFragment,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\ntype ProjectOption = ListProjectsQuery[\"projects\"][\"items\"][number];\n\nexport function TimeTrackingPage() {\n const { t: translate } = useI18n();\n const t = (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n const { activeTimer, tickTimer, syncTimerFromServer } = useTimeCampusStore();\n const navigate = useTimeCampusNavigate();\n\n const [startOpen, setStartOpen] = useState(false);\n const [description, setDescription] = useState(\"\");\n const [projectId, setProjectId] = useState(\"\");\n const [billable, setBillable] = useState(false);\n\n const { data: statsData, loading: statsLoading } =\n useGetTimeEntryStatsQuery();\n const { data: entriesData, loading: entriesLoading } =\n useListTimeEntriesQuery({\n variables: { pagination: { limit: 10 } },\n });\n const {\n data: runningData,\n loading: runningLoading,\n refetch: refetchRunning,\n } = useListRunningTimersQuery({ fetchPolicy: \"cache-and-network\" });\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 50 } },\n });\n const projects: ProjectOption[] = projectsData?.projects?.items ?? [];\n\n const serverTimer = runningData?.runningTimers?.[0] ?? null;\n\n const refetchAll = [\n { query: ListRunningTimersDocument },\n {\n query: ListTimeEntriesDocument,\n variables: { pagination: { limit: 10 } },\n },\n { query: GetTimeEntryStatsDocument },\n ];\n\n const [startTimerMutation, { loading: starting, error: startError }] =\n useStartTimerMutation({ refetchQueries: refetchAll });\n const [stopTimerMutation, { loading: stopping }] = useStopTimerMutation({\n refetchQueries: refetchAll,\n });\n\n // Backend is authoritative for \"is a timer running\" — mirror into the store.\n useEffect(() => {\n if (serverTimer) {\n syncTimerFromServer({\n timeEntryId: serverTimer.timeEntry.id,\n elapsedSeconds: serverTimer.elapsedSeconds,\n projectId: serverTimer.timeEntry.projectId ?? undefined,\n taskId: serverTimer.timeEntry.taskId ?? undefined,\n description: serverTimer.timeEntry.description ?? undefined,\n });\n } else if (!runningLoading) {\n syncTimerFromServer(null);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [serverTimer?.timeEntry.id, serverTimer?.elapsedSeconds, runningLoading]);\n\n // Live seconds ticker for the display.\n useEffect(() => {\n if (!activeTimer?.isRunning) return;\n const interval = setInterval(() => tickTimer(), 1000);\n return () => clearInterval(interval);\n }, [activeTimer?.isRunning, tickTimer]);\n\n const isRunning = Boolean(serverTimer);\n\n const handleStart = async () => {\n await startTimerMutation({\n variables: {\n input: {\n description: description.trim() || undefined,\n projectId: projectId || undefined,\n billable,\n source: \"timecampus\",\n },\n },\n });\n setStartOpen(false);\n setDescription(\"\");\n setProjectId(\"\");\n setBillable(false);\n await refetchRunning();\n };\n\n const handleStop = async () => {\n if (!serverTimer) return;\n await stopTimerMutation({ variables: { id: serverTimer.timeEntry.id } });\n await refetchRunning();\n };\n\n const stats = statsData?.timeEntryStats;\n const todayEntries = entriesData?.timeEntries?.items ?? [];\n\n const noProjectLabel = t(\n \"timecampus.timeTracking.entries.noProject\",\n \"No Project\",\n );\n\n // Real aggregates for the charts — derived only from data already fetched.\n const billableHours = stats?.billableHours ?? 0;\n const totalHours = stats?.totalHours ?? 0;\n const nonBillableHours = Math.max(0, totalHours - billableHours);\n const billablePct =\n totalHours > 0 ? Math.round((billableHours / totalHours) * 100) : 0;\n\n const billableSlices = [\n {\n name: t(\"timecampus.timeTracking.charts.billable\", \"Billable\"),\n value: Number(billableHours.toFixed(2)),\n },\n {\n name: t(\"timecampus.timeTracking.charts.nonBillable\", \"Non-billable\"),\n value: Number(nonBillableHours.toFixed(2)),\n },\n ];\n\n const projectHours = new Map<string, number>();\n for (const entry of todayEntries) {\n const name = entry.project?.name ?? noProjectLabel;\n projectHours.set(\n name,\n (projectHours.get(name) ?? 0) + (entry.duration ?? 0) / 3600,\n );\n }\n const hoursByProject = Array.from(projectHours, ([name, hours]) => ({\n name,\n hours: Number(hours.toFixed(2)),\n }))\n .filter((row) => row.hours > 0)\n .sort((a, b) => b.hours - a.hours)\n .slice(0, 6);\n\n if (statsLoading) {\n return (\n <PageLayout\n title={t(\"timecampus.timeTracking.title\", \"Time Tracking\")}\n description={t(\n \"timecampus.timeTracking.description\",\n \"Track and manage your work hours\",\n )}\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={t(\"timecampus.timeTracking.title\", \"Time Tracking\")}\n description={t(\n \"timecampus.timeTracking.description\",\n \"Track and manage your work hours\",\n )}\n purpose={t(\n \"timecampus.timeTracking.purpose\",\n \"Start a timer the moment you begin work and your hours are captured automatically — review today's totals, see what's billable, and stop guessing where your day went.\",\n )}\n actions={\n <button\n onClick={() => (isRunning ? handleStop() : setStartOpen(true))}\n disabled={starting || stopping}\n className={`inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium disabled:opacity-50 ${isRunning ? \"bg-action-danger-bg text-action-primary-text hover:bg-action-danger-bg/90\" : \"bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90\"}`}\n >\n {isRunning ? (\n <>\n <Square className=\"h-4 w-4\" />\n {t(\"timecampus.timeTracking.actions.stop\", \"Stop\")} (\n {formatTimerDuration(activeTimer?.seconds ?? 0)})\n </>\n ) : (\n <>\n <Play className=\"h-4 w-4\" />\n {t(\"timecampus.timeTracking.actions.startTimer\", \"Start Timer\")}\n </>\n )}\n </button>\n }\n >\n <GlassCard treatment=\"glass\" glow className=\"p-6 sm:p-8\">\n <div className=\"flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n <span\n className={`h-2 w-2 rounded-full ${isRunning ? \"bg-status-success-text motion-safe:animate-vc-glow-pulse\" : \"bg-muted-foreground\"}`}\n />\n {isRunning\n ? t(\"timecampus.timeTracking.hero.running\", \"Timer running\")\n : t(\"timecampus.timeTracking.hero.idle\", \"Timer idle\")}\n </div>\n <p className=\"mt-3 font-mono text-5xl font-bold tabular-nums text-text-primary sm:text-6xl\">\n {formatTimerDuration(activeTimer?.seconds ?? 0)}\n </p>\n <div className=\"mt-3 flex flex-wrap items-center gap-2 text-sm text-text-secondary\">\n <span className=\"min-w-0 truncate\">\n {activeTimer?.description ||\n (isRunning\n ? t(\n \"timecampus.timeTracking.entries.noDescription\",\n \"No description\",\n )\n : t(\n \"timecampus.timeTracking.hero.prompt\",\n \"Start a timer to begin tracking\",\n ))}\n </span>\n {serverTimer?.timeEntry.project?.name && (\n <StatusBadge\n label={serverTimer.timeEntry.project.name}\n variant=\"default\"\n />\n )}\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-3\">\n {isRunning ? (\n <button\n onClick={handleStop}\n disabled={stopping}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-danger-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n <Square className=\"h-4 w-4\" />\n {stopping\n ? t(\"timecampus.timeTracking.hero.stopping\", \"Stopping...\")\n : t(\"timecampus.timeTracking.actions.stop\", \"Stop\")}\n </button>\n ) : (\n <button\n onClick={() => setStartOpen(true)}\n disabled={starting}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" />\n {t(\"timecampus.timeTracking.actions.startTimer\", \"Start Timer\")}\n </button>\n )}\n </div>\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title={t(\"timecampus.timeTracking.stats.today\", \"Today\")}\n value={formatHours(totalHours)}\n icon={<Clock className=\"h-5 w-5\" />}\n description={`${billablePct}% ${t(\"timecampus.timeTracking.stats.billableSuffix\", \"billable\")}`}\n />\n <StatCard\n title={t(\"timecampus.timeTracking.stats.billable\", \"Billable\")}\n value={formatHours(stats?.billableHours ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n description={`${stats?.totalEntries ?? 0} ${t(\"timecampus.timeTracking.stats.entries\", \"entries\")}`}\n />\n <StatCard\n title={t(\n \"timecampus.timeTracking.stats.activeTimers\",\n \"Active Timers\",\n )}\n value={stats?.activeTimers ?? 0}\n icon={<Clock className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.timeTracking.stats.currentlyRunning\",\n \"Currently running\",\n )}\n />\n <StatCard\n title={t(\"timecampus.timeTracking.stats.activeTimer\", \"Active Timer\")}\n value={\n activeTimer\n ? formatTimerDuration(activeTimer.seconds)\n : t(\"timecampus.timeTracking.stats.stopped\", \"Stopped\")\n }\n icon={\n activeTimer ? (\n <Pause className=\"h-5 w-5\" />\n ) : (\n <Play className=\"h-5 w-5\" />\n )\n }\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title={t(\n \"timecampus.timeTracking.charts.billableTitle\",\n \"Billable mix\",\n )}\n description={t(\n \"timecampus.timeTracking.charts.billableDesc\",\n \"Billable vs non-billable hours\",\n )}\n >\n <DonutChart\n data={billableSlices}\n centerValue={`${billablePct}%`}\n centerLabel={t(\n \"timecampus.timeTracking.charts.billableCenter\",\n \"billable\",\n )}\n valueFormat={(value) => formatHours(value)}\n emptyLabel={t(\n \"timecampus.timeTracking.charts.noHours\",\n \"No hours tracked yet\",\n )}\n height={240}\n />\n </ChartCard>\n <ChartCard\n title={t(\n \"timecampus.timeTracking.charts.projectTitle\",\n \"Hours by project\",\n )}\n description={t(\n \"timecampus.timeTracking.charts.projectDesc\",\n \"Where your recent tracked time went\",\n )}\n >\n <BarsChart\n data={hoursByProject}\n xKey=\"name\"\n series={[\n {\n key: \"hours\",\n label: t(\"timecampus.timeTracking.charts.hours\", \"Hours\"),\n },\n ]}\n orientation=\"horizontal\"\n valueFormat={(value) => formatHours(value)}\n categoryWidth={110}\n emptyLabel={t(\n \"timecampus.timeTracking.charts.noProjectHours\",\n \"No project time yet\",\n )}\n height={240}\n />\n </ChartCard>\n </div>\n\n <GlassCard treatment=\"glass\" className=\"p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.timeTracking.entries.title\", \"Today's Entries\")}\n </h3>\n <button\n onClick={() => navigate(\"/time/entries\")}\n className=\"text-sm font-medium text-primary hover:underline\"\n >\n {t(\"timecampus.timeTracking.entries.viewAll\", \"View all\")}\n </button>\n </div>\n {entriesLoading ? (\n <LoadingSkeleton variant=\"list\" count={4} />\n ) : todayEntries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={t(\n \"timecampus.timeTracking.entries.empty.title\",\n \"No time entries yet\",\n )}\n description={t(\n \"timecampus.timeTracking.entries.empty\",\n \"No time entries yet. Start a timer to begin tracking.\",\n )}\n action={\n <button\n onClick={() => setStartOpen(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 <Play className=\"h-4 w-4\" />\n {t(\"timecampus.timeTracking.actions.startTimer\", \"Start Timer\")}\n </button>\n }\n />\n ) : (\n <div className=\"flex flex-col gap-2\">\n {todayEntries.map((entry: TimeEntryFieldsFragment) => (\n <div\n key={entry.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-3 transition-colors hover:bg-bg-sunken/50\"\n >\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-xs font-semibold text-primary\">\n {getInitials(entry.project?.name ?? noProjectLabel)}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <p className=\"min-w-0 truncate text-sm font-medium text-text-primary\">\n {entry.project?.name ?? noProjectLabel}\n </p>\n {entry.billable && (\n <StatusBadge\n label={t(\n \"timecampus.timeTracking.charts.billable\",\n \"Billable\",\n )}\n variant=\"success\"\n className=\"shrink-0\"\n />\n )}\n </div>\n <p className=\"truncate text-xs text-text-secondary\">\n {entry.task?.title ??\n entry.description ??\n t(\n \"timecampus.timeTracking.entries.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <div className=\"flex shrink-0 flex-col items-end gap-1\">\n <p className=\"font-mono text-sm font-medium text-text-primary\">\n {formatHours((entry.duration ?? 0) / 3600)}\n </p>\n <StatusBadge\n status={entry.isRunning ? \"running\" : \"completed\"}\n dot={entry.isRunning}\n label={\n entry.isRunning\n ? t(\n \"timecampus.timeTracking.entries.status.running\",\n \"running\",\n )\n : t(\n \"timecampus.timeTracking.entries.status.completed\",\n \"completed\",\n )\n }\n />\n </div>\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n\n {/* Start timer modal */}\n <Modal\n open={startOpen}\n onClose={() => setStartOpen(false)}\n title={t(\"timecampus.timeTracking.startModal.title\", \"Start a timer\")}\n description={t(\n \"timecampus.timeTracking.startModal.description\",\n \"Track the task you're working on right now.\",\n )}\n footer={\n <>\n <button\n onClick={() => setStartOpen(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 {t(\"timecampus.common.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleStart}\n disabled={starting}\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 {starting\n ? t(\n \"timecampus.timeTracking.startModal.starting\",\n \"Starting...\",\n )\n : t(\n \"timecampus.timeTracking.actions.startTimer\",\n \"Start Timer\",\n )}\n </button>\n </>\n }\n >\n {startError && (\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 {t(\n \"timecampus.timeTracking.startModal.error\",\n \"Failed to start the timer. Please try again.\",\n )}\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n {t(\n \"timecampus.timeTracking.startModal.descriptionLabel\",\n \"What are you working on?\",\n )}\n </label>\n <input\n type=\"text\"\n placeholder={t(\n \"timecampus.timeTracking.startModal.descriptionPlaceholder\",\n \"Description\",\n )}\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>\n <label className=\"block text-sm font-medium text-text-primary\">\n {t(\"timecampus.timeTracking.startModal.projectLabel\", \"Project\")}\n </label>\n <select\n value={projectId}\n onChange={(e) => setProjectId(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 <option value=\"\">\n {t(\"timecampus.timeTracking.entries.noProject\", \"No Project\")}\n </option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={billable}\n onChange={(e) => setBillable(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n {t(\"timecampus.timeTracking.startModal.billable\", \"Billable\")}\n </label>\n </div>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimeTrackingPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimeTrackingPage.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Clock, Play, Pause, Square } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { Modal } from \"../../components/Modal\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n ChartCard,\n DonutChart,\n BarsChart,\n StatusBadge,\n} from \"../../components\";\nimport { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport {\n formatHours,\n formatTimerDuration,\n getInitials,\n} from \"../../utils/helpers\";\nimport { useTimeCampusStore } from \"../../store/timecampusStore\";\nimport {\n useGetTimeEntryStatsQuery,\n useListTimeEntriesQuery,\n useListRunningTimersQuery,\n useStartTimerMutation,\n useStopTimerMutation,\n useListProjectsQuery,\n ListRunningTimersDocument,\n ListTimeEntriesDocument,\n GetTimeEntryStatsDocument,\n type TimeEntryFieldsFragment,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\ntype ProjectOption = ListProjectsQuery[\"projects\"][\"items\"][number];\n\nexport function TimeTrackingPage() {\n const { t: translate } = useI18n();\n const t = (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n const { activeTimer, tickTimer, syncTimerFromServer } = useTimeCampusStore();\n const navigate = useTimeCampusNavigate();\n\n const [startOpen, setStartOpen] = useState(false);\n const [description, setDescription] = useState(\"\");\n const [projectId, setProjectId] = useState(\"\");\n const [billable, setBillable] = useState(false);\n\n const { data: statsData, loading: statsLoading } =\n useGetTimeEntryStatsQuery();\n const { data: entriesData, loading: entriesLoading } =\n useListTimeEntriesQuery({\n variables: { pagination: { limit: 10 } },\n });\n const {\n data: runningData,\n loading: runningLoading,\n refetch: refetchRunning,\n } = useListRunningTimersQuery({ fetchPolicy: \"cache-and-network\" });\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 50 } },\n });\n const projects: ProjectOption[] = projectsData?.projects?.items ?? [];\n\n const serverTimer = runningData?.runningTimers?.[0] ?? null;\n\n const refetchAll = [\n { query: ListRunningTimersDocument },\n {\n query: ListTimeEntriesDocument,\n variables: { pagination: { limit: 10 } },\n },\n { query: GetTimeEntryStatsDocument },\n ];\n\n const [startTimerMutation, { loading: starting, error: startError }] =\n useStartTimerMutation({ refetchQueries: refetchAll });\n const [stopTimerMutation, { loading: stopping }] = useStopTimerMutation({\n refetchQueries: refetchAll,\n });\n\n // Backend is authoritative for \"is a timer running\" — mirror into the store.\n useEffect(() => {\n if (serverTimer) {\n syncTimerFromServer({\n timeEntryId: serverTimer.timeEntry.id,\n elapsedSeconds: serverTimer.elapsedSeconds,\n projectId: serverTimer.timeEntry.projectId ?? undefined,\n taskId: serverTimer.timeEntry.taskId ?? undefined,\n description: serverTimer.timeEntry.description ?? undefined,\n });\n } else if (!runningLoading) {\n syncTimerFromServer(null);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [serverTimer?.timeEntry.id, serverTimer?.elapsedSeconds, runningLoading]);\n\n // Live seconds ticker for the display.\n useEffect(() => {\n if (!activeTimer?.isRunning) return;\n const interval = setInterval(() => tickTimer(), 1000);\n return () => clearInterval(interval);\n }, [activeTimer?.isRunning, tickTimer]);\n\n const isRunning = Boolean(serverTimer);\n\n const handleStart = async () => {\n await startTimerMutation({\n variables: {\n input: {\n description: description.trim() || undefined,\n projectId: projectId || undefined,\n billable,\n source: \"timecampus\",\n },\n },\n });\n setStartOpen(false);\n setDescription(\"\");\n setProjectId(\"\");\n setBillable(false);\n await refetchRunning();\n };\n\n const handleStop = async () => {\n if (!serverTimer) return;\n await stopTimerMutation({ variables: { id: serverTimer.timeEntry.id } });\n await refetchRunning();\n };\n\n const stats = statsData?.timeEntryStats;\n const todayEntries = entriesData?.timeEntries?.items ?? [];\n\n const noProjectLabel = t(\n \"timecampus.timeTracking.entries.noProject\",\n \"No Project\",\n );\n\n // Real aggregates for the charts — derived only from data already fetched.\n const billableHours = stats?.billableHours ?? 0;\n const totalHours = stats?.totalHours ?? 0;\n const nonBillableHours = Math.max(0, totalHours - billableHours);\n const billablePct =\n totalHours > 0 ? Math.round((billableHours / totalHours) * 100) : 0;\n\n const billableSlices = [\n {\n name: t(\"timecampus.timeTracking.charts.billable\", \"Billable\"),\n value: Number(billableHours.toFixed(2)),\n },\n {\n name: t(\"timecampus.timeTracking.charts.nonBillable\", \"Non-billable\"),\n value: Number(nonBillableHours.toFixed(2)),\n },\n ];\n\n const projectHours = new Map<string, number>();\n for (const entry of todayEntries) {\n const name = entry.project?.name ?? noProjectLabel;\n projectHours.set(\n name,\n (projectHours.get(name) ?? 0) + (entry.duration ?? 0) / 3600,\n );\n }\n const hoursByProject = Array.from(projectHours, ([name, hours]) => ({\n name,\n hours: Number(hours.toFixed(2)),\n }))\n .filter((row) => row.hours > 0)\n .sort((a, b) => b.hours - a.hours)\n .slice(0, 6);\n\n if (statsLoading) {\n return (\n <PageLayout\n title={t(\"timecampus.timeTracking.title\", \"Time Tracking\")}\n description={t(\n \"timecampus.timeTracking.description\",\n \"Track and manage your work hours\",\n )}\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={t(\"timecampus.timeTracking.title\", \"Time Tracking\")}\n description={t(\n \"timecampus.timeTracking.description\",\n \"Track and manage your work hours\",\n )}\n purpose={t(\n \"timecampus.timeTracking.purpose\",\n \"Start a timer the moment you begin work and your hours are captured automatically — review today's totals, see what's billable, and stop guessing where your day went.\",\n )}\n actions={\n <button\n onClick={() => (isRunning ? handleStop() : setStartOpen(true))}\n disabled={starting || stopping}\n className={`inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium disabled:opacity-50 ${isRunning ? \"bg-action-danger-bg text-action-primary-text hover:bg-action-danger-bg/90\" : \"bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90\"}`}\n >\n {isRunning ? (\n <>\n <Square className=\"h-4 w-4\" />\n {t(\"timecampus.timeTracking.actions.stop\", \"Stop\")} (\n {formatTimerDuration(activeTimer?.seconds ?? 0)})\n </>\n ) : (\n <>\n <Play className=\"h-4 w-4\" />\n {t(\"timecampus.timeTracking.actions.startTimer\", \"Start Timer\")}\n </>\n )}\n </button>\n }\n >\n <GlassCard treatment=\"glass\" glow className=\"p-6 sm:p-8\">\n <div className=\"flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n <span\n className={`h-2 w-2 rounded-full ${isRunning ? \"bg-status-success-text motion-safe:animate-vc-glow-pulse\" : \"bg-muted-foreground\"}`}\n />\n {isRunning\n ? t(\"timecampus.timeTracking.hero.running\", \"Timer running\")\n : t(\"timecampus.timeTracking.hero.idle\", \"Timer idle\")}\n </div>\n <p className=\"mt-3 font-mono text-5xl font-bold tabular-nums text-text-primary sm:text-6xl\">\n {formatTimerDuration(activeTimer?.seconds ?? 0)}\n </p>\n <div className=\"mt-3 flex flex-wrap items-center gap-2 text-sm text-text-secondary\">\n <span className=\"min-w-0 truncate\">\n {activeTimer?.description ||\n (isRunning\n ? t(\n \"timecampus.timeTracking.entries.noDescription\",\n \"No description\",\n )\n : t(\n \"timecampus.timeTracking.hero.prompt\",\n \"Start a timer to begin tracking\",\n ))}\n </span>\n {serverTimer?.timeEntry.project?.name && (\n <StatusBadge\n label={serverTimer.timeEntry.project.name}\n variant=\"default\"\n />\n )}\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-3\">\n {isRunning ? (\n <button\n onClick={handleStop}\n disabled={stopping}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-danger-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n <Square className=\"h-4 w-4\" />\n {stopping\n ? t(\"timecampus.timeTracking.hero.stopping\", \"Stopping...\")\n : t(\"timecampus.timeTracking.actions.stop\", \"Stop\")}\n </button>\n ) : (\n <button\n onClick={() => setStartOpen(true)}\n disabled={starting}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" />\n {t(\"timecampus.timeTracking.actions.startTimer\", \"Start Timer\")}\n </button>\n )}\n </div>\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title={t(\"timecampus.timeTracking.stats.today\", \"Today\")}\n value={formatHours(totalHours)}\n icon={<Clock className=\"h-5 w-5\" />}\n description={`${billablePct}% ${t(\"timecampus.timeTracking.stats.billableSuffix\", \"billable\")}`}\n />\n <StatCard\n title={t(\"timecampus.timeTracking.stats.billable\", \"Billable\")}\n value={formatHours(stats?.billableHours ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n description={`${stats?.totalEntries ?? 0} ${t(\"timecampus.timeTracking.stats.entries\", \"entries\")}`}\n />\n <StatCard\n title={t(\n \"timecampus.timeTracking.stats.activeTimers\",\n \"Active Timers\",\n )}\n value={stats?.activeTimers ?? 0}\n icon={<Clock className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.timeTracking.stats.currentlyRunning\",\n \"Currently running\",\n )}\n />\n <StatCard\n title={t(\"timecampus.timeTracking.stats.activeTimer\", \"Active Timer\")}\n value={\n activeTimer\n ? formatTimerDuration(activeTimer.seconds)\n : t(\"timecampus.timeTracking.stats.stopped\", \"Stopped\")\n }\n icon={\n activeTimer ? (\n <Pause className=\"h-5 w-5\" />\n ) : (\n <Play className=\"h-5 w-5\" />\n )\n }\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title={t(\n \"timecampus.timeTracking.charts.billableTitle\",\n \"Billable mix\",\n )}\n description={t(\n \"timecampus.timeTracking.charts.billableDesc\",\n \"Billable vs non-billable hours\",\n )}\n >\n <DonutChart\n data={billableSlices}\n centerValue={`${billablePct}%`}\n centerLabel={t(\n \"timecampus.timeTracking.charts.billableCenter\",\n \"billable\",\n )}\n valueFormat={(value) => formatHours(value)}\n emptyLabel={t(\n \"timecampus.timeTracking.charts.noHours\",\n \"No hours tracked yet\",\n )}\n height={240}\n />\n </ChartCard>\n <ChartCard\n title={t(\n \"timecampus.timeTracking.charts.projectTitle\",\n \"Hours by project\",\n )}\n description={t(\n \"timecampus.timeTracking.charts.projectDesc\",\n \"Where your recent tracked time went\",\n )}\n >\n <BarsChart\n data={hoursByProject}\n xKey=\"name\"\n series={[\n {\n key: \"hours\",\n label: t(\"timecampus.timeTracking.charts.hours\", \"Hours\"),\n },\n ]}\n orientation=\"horizontal\"\n valueFormat={(value) => formatHours(value)}\n categoryWidth={110}\n emptyLabel={t(\n \"timecampus.timeTracking.charts.noProjectHours\",\n \"No project time yet\",\n )}\n height={240}\n />\n </ChartCard>\n </div>\n\n <GlassCard treatment=\"glass\" className=\"p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.timeTracking.entries.title\", \"Today's Entries\")}\n </h3>\n <button\n onClick={() => navigate(\"/time/entries\")}\n className=\"text-sm font-medium text-primary hover:underline\"\n >\n {t(\"timecampus.timeTracking.entries.viewAll\", \"View all\")}\n </button>\n </div>\n {entriesLoading ? (\n <LoadingSkeleton variant=\"list\" count={4} />\n ) : todayEntries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={t(\n \"timecampus.timeTracking.entries.empty.title\",\n \"No time entries yet\",\n )}\n description={t(\n \"timecampus.timeTracking.entries.empty\",\n \"No time entries yet. Start a timer to begin tracking.\",\n )}\n action={\n <button\n onClick={() => setStartOpen(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 <Play className=\"h-4 w-4\" />\n {t(\"timecampus.timeTracking.actions.startTimer\", \"Start Timer\")}\n </button>\n }\n />\n ) : (\n <div className=\"flex flex-col gap-2\">\n {todayEntries.map((entry: TimeEntryFieldsFragment) => (\n <div\n key={entry.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-3 transition-colors hover:bg-bg-sunken/50\"\n >\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-xs font-semibold text-primary\">\n {getInitials(entry.project?.name ?? noProjectLabel)}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <p className=\"min-w-0 truncate text-sm font-medium text-text-primary\">\n {entry.project?.name ?? noProjectLabel}\n </p>\n {entry.billable && (\n <StatusBadge\n label={t(\n \"timecampus.timeTracking.charts.billable\",\n \"Billable\",\n )}\n variant=\"success\"\n className=\"shrink-0\"\n />\n )}\n </div>\n <p className=\"truncate text-xs text-text-secondary\">\n {entry.task?.title ??\n entry.description ??\n t(\n \"timecampus.timeTracking.entries.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <div className=\"flex shrink-0 flex-col items-end gap-1\">\n <p className=\"font-mono text-sm font-medium text-text-primary\">\n {formatHours((entry.duration ?? 0) / 3600)}\n </p>\n <StatusBadge\n status={entry.isRunning ? \"running\" : \"completed\"}\n dot={entry.isRunning}\n label={\n entry.isRunning\n ? t(\n \"timecampus.timeTracking.entries.status.running\",\n \"running\",\n )\n : t(\n \"timecampus.timeTracking.entries.status.completed\",\n \"completed\",\n )\n }\n />\n </div>\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n\n {/* Start timer modal */}\n <Modal\n open={startOpen}\n onClose={() => setStartOpen(false)}\n title={t(\"timecampus.timeTracking.startModal.title\", \"Start a timer\")}\n description={t(\n \"timecampus.timeTracking.startModal.description\",\n \"Track the task you're working on right now.\",\n )}\n footer={\n <>\n <button\n onClick={() => setStartOpen(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 {t(\"timecampus.common.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleStart}\n disabled={starting}\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 {starting\n ? t(\n \"timecampus.timeTracking.startModal.starting\",\n \"Starting...\",\n )\n : t(\n \"timecampus.timeTracking.actions.startTimer\",\n \"Start Timer\",\n )}\n </button>\n </>\n }\n >\n {startError && (\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 {t(\n \"timecampus.timeTracking.startModal.error\",\n \"Failed to start the timer. Please try again.\",\n )}\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n {t(\n \"timecampus.timeTracking.startModal.descriptionLabel\",\n \"What are you working on?\",\n )}\n </label>\n <input\n type=\"text\"\n placeholder={t(\n \"timecampus.timeTracking.startModal.descriptionPlaceholder\",\n \"Description\",\n )}\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>\n <label className=\"block text-sm font-medium text-text-primary\">\n {t(\"timecampus.timeTracking.startModal.projectLabel\", \"Project\")}\n </label>\n <select\n value={projectId}\n onChange={(e) => setProjectId(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 <option value=\"\">\n {t(\"timecampus.timeTracking.entries.noProject\", \"No Project\")}\n </option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={billable}\n onChange={(e) => setBillable(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n {t(\"timecampus.timeTracking.startModal.billable\", \"Billable\")}\n </label>\n </div>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1,13 +1,13 @@
1
1
  import { useTimeCampusNavigate as e } from "../../providers/TimeCampusProvider.js";
2
2
  import { formatTimerDuration as t, getInitials as n } from "../../utils/helpers.js";
3
3
  import { LoadingSkeleton as r } from "../../components/LoadingSkeleton.js";
4
- import { GetTimeEntryStatsDocument as ee, ListRunningTimersDocument as i, ListTimeEntriesDocument as a, useDeleteTimeEntryMutation as o, useListProjectsQuery as s, useListRunningTimersQuery as c, useStartTimerMutation as l, useStopTimerMutation as u } from "../../../generated/wspace-operations.js";
5
- import { useTr as d } from "../../shared/hooks/useTr.js";
4
+ import { EmptyState as ee } from "../../components/EmptyState.js";
5
+ import { GetTimeEntryStatsDocument as i, ListRunningTimersDocument as a, ListTimeEntriesDocument as o, useDeleteTimeEntryMutation as s, useListProjectsQuery as c, useListRunningTimersQuery as l, useStartTimerMutation as u, useStopTimerMutation as d } from "../../../generated/wspace-operations.js";
6
+ import { useTr as f } from "../../shared/hooks/useTr.js";
6
7
  import "../../hooks/index.js";
7
- import { PageLayout as f } from "../../components/PageLayout.js";
8
- import { useTimeCampusStore as te } from "../../store/timecampusStore.js";
9
- import { StatusBadge as ne } from "../../components/StatusBadge.js";
10
- import { EmptyState as re } from "../../components/EmptyState.js";
8
+ import { PageLayout as te } from "../../components/PageLayout.js";
9
+ import { useTimeCampusStore as ne } from "../../store/timecampusStore.js";
10
+ import { StatusBadge as re } from "../../components/StatusBadge.js";
11
11
  import { Modal as p } from "../../components/Modal.js";
12
12
  import { useEffect as m, useState as h } from "react";
13
13
  import { Play as g, RotateCcw as _, Square as v } from "lucide-react";
@@ -15,14 +15,14 @@ import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
15
15
  import { GlassCard as S, NextSteps as C } from "@burdenoff/fe-libs/ui";
16
16
  //#region src/timecampus/pages/time/TimerPage.tsx
17
17
  function w() {
18
- let { activeTimer: w, tickTimer: T, syncTimerFromServer: E } = te(), D = e(), O = d(), [ie, k] = h(!1), [A, j] = h(""), [M, N] = h(""), [P, F] = h(!1), [I, L] = h(null), { data: R, loading: z, refetch: B } = c({ fetchPolicy: "cache-and-network" }), { data: V } = s({ variables: { pagination: { limit: 50 } } }), H = V?.projects?.items ?? [], U = R?.runningTimers ?? [], W = U[0] ?? null, G = [
19
- { query: i },
18
+ let { activeTimer: w, tickTimer: T, syncTimerFromServer: E } = ne(), D = e(), O = f(), [ie, k] = h(!1), [A, j] = h(""), [M, N] = h(""), [P, F] = h(!1), [I, L] = h(null), { data: R, loading: z, refetch: B } = l({ fetchPolicy: "cache-and-network" }), { data: V } = c({ variables: { pagination: { limit: 50 } } }), H = V?.projects?.items ?? [], U = R?.runningTimers ?? [], W = U[0] ?? null, G = [
19
+ { query: a },
20
20
  {
21
- query: a,
21
+ query: o,
22
22
  variables: { pagination: { limit: 50 } }
23
23
  },
24
- { query: ee }
25
- ], [K, { loading: q, error: J }] = l({ refetchQueries: G }), [Y, { loading: X }] = u({ refetchQueries: G }), [ae, { loading: Z }] = o({ refetchQueries: G });
24
+ { query: i }
25
+ ], [K, { loading: q, error: J }] = u({ refetchQueries: G }), [Y, { loading: X }] = d({ refetchQueries: G }), [ae, { loading: Z }] = s({ refetchQueries: G });
26
26
  m(() => {
27
27
  W ? E({
28
28
  timeEntryId: W.timeEntry.id,
@@ -52,7 +52,7 @@ function w() {
52
52
  }, se = async () => {
53
53
  I && (await Y({ variables: { id: I.timeEntry.id } }), await ae({ variables: { id: I.timeEntry.id } }), L(null), await B());
54
54
  }, $ = !!W;
55
- return /* @__PURE__ */ x(f, {
55
+ return /* @__PURE__ */ x(te, {
56
56
  title: O("timecampus.timer.title", "Timer"),
57
57
  description: O("timecampus.timer.description", "Focus timer for deep work sessions"),
58
58
  purpose: O("timecampus.timer.purpose", "A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later."),
@@ -127,7 +127,7 @@ function w() {
127
127
  children: [/* @__PURE__ */ b("h3", {
128
128
  className: "text-sm font-semibold text-text-primary",
129
129
  children: O("timecampus.timer.runningTimers.title", "Running Timers")
130
- }), U.length > 0 && /* @__PURE__ */ b(ne, {
130
+ }), U.length > 0 && /* @__PURE__ */ b(re, {
131
131
  label: O("timecampus.timer.runningTimers.active", "{{count}} active", { count: U.length }),
132
132
  variant: "success",
133
133
  dot: !0
@@ -135,7 +135,7 @@ function w() {
135
135
  }), z && U.length === 0 ? /* @__PURE__ */ b(r, {
136
136
  variant: "list",
137
137
  count: 2
138
- }) : U.length === 0 ? /* @__PURE__ */ b(re, {
138
+ }) : U.length === 0 ? /* @__PURE__ */ b(ee, {
139
139
  illustration: "empty-sessions",
140
140
  title: O("timecampus.timer.runningTimers.emptyTitle", "No active timers on the server"),
141
141
  description: O("timecampus.timer.runningTimers.emptyDescription", "Timers you start sync here so you can pick them up on any device.")
@@ -1 +1 @@
1
- {"version":3,"file":"TimerPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Play, Square, RotateCcw } from \"lucide-react\";\nimport { GlassCard, NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { Modal } from \"../../components/Modal\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport { formatTimerDuration, getInitials } from \"../../utils/helpers\";\nimport { useTimeCampusStore } from \"../../store/timecampusStore\";\nimport { useTr } from \"../../hooks\";\nimport {\n useListRunningTimersQuery,\n useStartTimerMutation,\n useStopTimerMutation,\n useDeleteTimeEntryMutation,\n useListProjectsQuery,\n ListRunningTimersDocument,\n ListTimeEntriesDocument,\n GetTimeEntryStatsDocument,\n type ListRunningTimersQuery,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\n\ntype RunningTimerItem = ListRunningTimersQuery[\"runningTimers\"][number];\ntype ProjectOption = ListProjectsQuery[\"projects\"][\"items\"][number];\n\nexport function TimerPage() {\n const { activeTimer, tickTimer, syncTimerFromServer } = useTimeCampusStore();\n const navigate = useTimeCampusNavigate();\n const t = useTr();\n\n const [startOpen, setStartOpen] = useState(false);\n const [description, setDescription] = useState(\"\");\n const [projectId, setProjectId] = useState(\"\");\n const [billable, setBillable] = useState(false);\n const [resetTarget, setResetTarget] = useState<RunningTimerItem | null>(null);\n\n const {\n data: runningData,\n loading: runningLoading,\n refetch: refetchRunning,\n } = useListRunningTimersQuery({ fetchPolicy: \"cache-and-network\" });\n\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 50 } },\n });\n const projects: ProjectOption[] = projectsData?.projects?.items ?? [];\n\n const runningTimers = runningData?.runningTimers ?? [];\n const serverTimer = runningTimers[0] ?? null;\n\n const refetchAll = [\n { query: ListRunningTimersDocument },\n {\n query: ListTimeEntriesDocument,\n variables: { pagination: { limit: 50 } },\n },\n { query: GetTimeEntryStatsDocument },\n ];\n\n const [startTimerMutation, { loading: starting, error: startError }] =\n useStartTimerMutation({ refetchQueries: refetchAll });\n const [stopTimerMutation, { loading: stopping }] = useStopTimerMutation({\n refetchQueries: refetchAll,\n });\n const [deleteTimeEntry, { loading: deleting }] = useDeleteTimeEntryMutation({\n refetchQueries: refetchAll,\n });\n\n // Backend is the source of truth for whether a timer is running — mirror the\n // server running timer into the local display store whenever it changes.\n useEffect(() => {\n if (serverTimer) {\n syncTimerFromServer({\n timeEntryId: serverTimer.timeEntry.id,\n elapsedSeconds: serverTimer.elapsedSeconds,\n projectId: serverTimer.timeEntry.projectId ?? undefined,\n taskId: serverTimer.timeEntry.taskId ?? undefined,\n description: serverTimer.timeEntry.description ?? undefined,\n });\n } else if (!runningLoading) {\n syncTimerFromServer(null);\n }\n // Only re-sync when the server's running timer identity/elapsed changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [serverTimer?.timeEntry.id, serverTimer?.elapsedSeconds, runningLoading]);\n\n // Local ticking display for the live seconds counter.\n useEffect(() => {\n if (!activeTimer?.isRunning) return;\n const interval = setInterval(() => tickTimer(), 1000);\n return () => clearInterval(interval);\n }, [activeTimer?.isRunning, tickTimer]);\n\n const handleStart = async () => {\n await startTimerMutation({\n variables: {\n input: {\n description: description.trim() || undefined,\n projectId: projectId || undefined,\n billable,\n source: \"timer\",\n },\n },\n });\n setStartOpen(false);\n setDescription(\"\");\n setProjectId(\"\");\n setBillable(false);\n await refetchRunning();\n };\n\n const handleStop = async () => {\n if (!serverTimer) return;\n await stopTimerMutation({ variables: { id: serverTimer.timeEntry.id } });\n await refetchRunning();\n };\n\n const handleReset = async () => {\n if (!resetTarget) return;\n // Discard the in-progress session: stop it, then delete the entry.\n await stopTimerMutation({ variables: { id: resetTarget.timeEntry.id } });\n await deleteTimeEntry({ variables: { id: resetTarget.timeEntry.id } });\n setResetTarget(null);\n await refetchRunning();\n };\n\n const isRunning = Boolean(serverTimer);\n\n return (\n <PageLayout\n title={t(\"timecampus.timer.title\", \"Timer\")}\n description={t(\n \"timecampus.timer.description\",\n \"Focus timer for deep work sessions\",\n )}\n purpose={t(\n \"timecampus.timer.purpose\",\n \"A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later.\",\n )}\n >\n <div className=\"mx-auto max-w-lg\">\n <NextSteps\n storageKey=\"timecampus-timer\"\n className=\"mb-6\"\n steps={[\n {\n id: \"start\",\n label: isRunning\n ? t(\n \"timecampus.timer.nextSteps.stopRunning\",\n \"Stop the running timer\",\n )\n : t(\"timecampus.timer.nextSteps.start\", \"Start the timer\"),\n description: isRunning\n ? t(\n \"timecampus.timer.nextSteps.stopDescription\",\n \"End this session and save it as a time entry.\",\n )\n : t(\n \"timecampus.timer.nextSteps.startDescription\",\n \"Begin tracking the task you're working on right now.\",\n ),\n onClick: () => (isRunning ? handleStop() : setStartOpen(true)),\n },\n {\n id: \"assign-project\",\n label: t(\n \"timecampus.timer.nextSteps.assignProject\",\n \"Assign work to a project\",\n ),\n description: t(\n \"timecampus.timer.nextSteps.assignDescription\",\n \"Pick a project so this time bills correctly.\",\n ),\n onClick: () => navigate(\"/projects\"),\n },\n {\n id: \"review-entries\",\n label: t(\n \"timecampus.timer.nextSteps.review\",\n \"Review your time entries\",\n ),\n description: t(\n \"timecampus.timer.nextSteps.reviewDescription\",\n \"Check and tidy everything you've logged.\",\n ),\n onClick: () => navigate(\"/time/entries\"),\n },\n ]}\n />\n <GlassCard treatment=\"glass\" glow className=\"p-6 text-center sm:p-8\">\n <p className=\"font-mono text-5xl font-bold tabular-nums text-text-primary sm:text-6xl\">\n {activeTimer\n ? formatTimerDuration(activeTimer.seconds)\n : t(\"timecampus.timer.elapsedDefault\", \"00:00:00\")}\n </p>\n {activeTimer?.description && (\n <p className=\"mt-2 text-sm text-text-secondary\">\n {activeTimer.description}\n </p>\n )}\n <div className=\"mt-6 flex flex-wrap items-center justify-center gap-3\">\n {isRunning ? (\n <button\n onClick={handleStop}\n disabled={stopping}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-danger-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n <Square className=\"h-4 w-4\" />\n {stopping\n ? t(\"timecampus.timer.stopping\", \"Stopping...\")\n : t(\"timecampus.timer.stop\", \"Stop\")}\n </button>\n ) : (\n <button\n onClick={() => setStartOpen(true)}\n disabled={starting}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" />\n {t(\"timecampus.timer.start\", \"Start\")}\n </button>\n )}\n <button\n onClick={() => serverTimer && setResetTarget(serverTimer)}\n disabled={!isRunning || deleting}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-3 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n title={t(\n \"timecampus.timer.resetTooltip\",\n \"Discard the running timer without saving\",\n )}\n >\n <RotateCcw className=\"h-4 w-4\" />\n {t(\"timecampus.timer.reset\", \"Reset\")}\n </button>\n </div>\n </GlassCard>\n\n {/* Server-side running timers */}\n <GlassCard treatment=\"glass\" className=\"mt-6 p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.timer.runningTimers.title\", \"Running Timers\")}\n </h3>\n {runningTimers.length > 0 && (\n <StatusBadge\n label={t(\n \"timecampus.timer.runningTimers.active\",\n \"{{count}} active\",\n { count: runningTimers.length },\n )}\n variant=\"success\"\n dot\n />\n )}\n </div>\n {runningLoading && runningTimers.length === 0 ? (\n <LoadingSkeleton variant=\"list\" count={2} />\n ) : runningTimers.length === 0 ? (\n <EmptyState\n illustration=\"empty-sessions\"\n title={t(\n \"timecampus.timer.runningTimers.emptyTitle\",\n \"No active timers on the server\",\n )}\n description={t(\n \"timecampus.timer.runningTimers.emptyDescription\",\n \"Timers you start sync here so you can pick them up on any device.\",\n )}\n />\n ) : (\n <div className=\"flex flex-col gap-2\">\n {runningTimers.map((timer: RunningTimerItem) => (\n <div\n key={timer.timeEntry.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-3\"\n >\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-xs font-semibold text-primary\">\n {getInitials(\n timer.timeEntry.project?.name ??\n t(\n \"timecampus.timer.runningTimers.noProject\",\n \"No Project\",\n ),\n )}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {timer.timeEntry.project?.name ??\n t(\n \"timecampus.timer.runningTimers.noProject\",\n \"No Project\",\n )}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {timer.timeEntry.description ??\n t(\n \"timecampus.timer.runningTimers.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <p className=\"ml-2 shrink-0 font-mono text-sm font-medium text-text-primary\">\n {formatTimerDuration(timer.elapsedSeconds)}\n </p>\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Start timer modal */}\n <Modal\n open={startOpen}\n onClose={() => setStartOpen(false)}\n title={t(\"timecampus.timer.modal.startTitle\", \"Start a timer\")}\n description={t(\n \"timecampus.timer.modal.startDescription\",\n \"Track the task you're working on right now.\",\n )}\n footer={\n <>\n <button\n onClick={() => setStartOpen(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 {t(\"timecampus.timer.modal.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleStart}\n disabled={starting}\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 {starting\n ? t(\"timecampus.timer.modal.starting\", \"Starting...\")\n : t(\"timecampus.timer.modal.startTimer\", \"Start Timer\")}\n </button>\n </>\n }\n >\n {startError && (\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 {t(\n \"timecampus.timer.modal.startError\",\n \"Failed to start the timer. Please try again.\",\n )}\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n {t(\n \"timecampus.timer.modal.descriptionLabel\",\n \"What are you working on?\",\n )}\n </label>\n <input\n type=\"text\"\n placeholder={t(\n \"timecampus.timer.modal.descriptionPlaceholder\",\n \"Description\",\n )}\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>\n <label className=\"block text-sm font-medium text-text-primary\">\n {t(\"timecampus.timer.modal.projectLabel\", \"Project\")}\n </label>\n <select\n value={projectId}\n onChange={(e) => setProjectId(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 <option value=\"\">\n {t(\"timecampus.timer.modal.noProject\", \"No Project\")}\n </option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={billable}\n onChange={(e) => setBillable(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n {t(\"timecampus.timer.modal.billable\", \"Billable\")}\n </label>\n </div>\n </Modal>\n\n {/* Reset / discard confirm modal */}\n <Modal\n open={Boolean(resetTarget)}\n onClose={() => setResetTarget(null)}\n title={t(\n \"timecampus.timer.modal.discardTitle\",\n \"Discard running timer?\",\n )}\n description={t(\n \"timecampus.timer.modal.discardDescription\",\n \"This stops the timer and deletes the in-progress entry. It cannot be undone.\",\n )}\n footer={\n <>\n <button\n onClick={() => setResetTarget(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 {t(\"timecampus.timer.modal.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleReset}\n disabled={stopping || deleting}\n className=\"rounded-md bg-action-danger-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n {stopping || deleting\n ? t(\"timecampus.timer.modal.discarding\", \"Discarding...\")\n : t(\"timecampus.timer.modal.discard\", \"Discard\")}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n {t(\"timecampus.timer.modal.discardMessagePrefix\", \"The timer for\")}{\" \"}\n <span className=\"font-medium text-text-primary\">\n {resetTarget?.timeEntry.project?.name ??\n resetTarget?.timeEntry.description ??\n t(\"timecampus.timer.modal.sessionFallback\", \"this session\")}\n </span>{\" \"}\n {t(\n \"timecampus.timer.modal.discardMessageSuffix\",\n \"will be discarded.\",\n )}\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimerPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Play, Square, RotateCcw } from \"lucide-react\";\nimport { GlassCard, NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { Modal } from \"../../components/Modal\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport { formatTimerDuration, getInitials } from \"../../utils/helpers\";\nimport { useTimeCampusStore } from \"../../store/timecampusStore\";\nimport { useTr } from \"../../hooks\";\nimport {\n useListRunningTimersQuery,\n useStartTimerMutation,\n useStopTimerMutation,\n useDeleteTimeEntryMutation,\n useListProjectsQuery,\n ListRunningTimersDocument,\n ListTimeEntriesDocument,\n GetTimeEntryStatsDocument,\n type ListRunningTimersQuery,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\n\ntype RunningTimerItem = ListRunningTimersQuery[\"runningTimers\"][number];\ntype ProjectOption = ListProjectsQuery[\"projects\"][\"items\"][number];\n\nexport function TimerPage() {\n const { activeTimer, tickTimer, syncTimerFromServer } = useTimeCampusStore();\n const navigate = useTimeCampusNavigate();\n const t = useTr();\n\n const [startOpen, setStartOpen] = useState(false);\n const [description, setDescription] = useState(\"\");\n const [projectId, setProjectId] = useState(\"\");\n const [billable, setBillable] = useState(false);\n const [resetTarget, setResetTarget] = useState<RunningTimerItem | null>(null);\n\n const {\n data: runningData,\n loading: runningLoading,\n refetch: refetchRunning,\n } = useListRunningTimersQuery({ fetchPolicy: \"cache-and-network\" });\n\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 50 } },\n });\n const projects: ProjectOption[] = projectsData?.projects?.items ?? [];\n\n const runningTimers = runningData?.runningTimers ?? [];\n const serverTimer = runningTimers[0] ?? null;\n\n const refetchAll = [\n { query: ListRunningTimersDocument },\n {\n query: ListTimeEntriesDocument,\n variables: { pagination: { limit: 50 } },\n },\n { query: GetTimeEntryStatsDocument },\n ];\n\n const [startTimerMutation, { loading: starting, error: startError }] =\n useStartTimerMutation({ refetchQueries: refetchAll });\n const [stopTimerMutation, { loading: stopping }] = useStopTimerMutation({\n refetchQueries: refetchAll,\n });\n const [deleteTimeEntry, { loading: deleting }] = useDeleteTimeEntryMutation({\n refetchQueries: refetchAll,\n });\n\n // Backend is the source of truth for whether a timer is running — mirror the\n // server running timer into the local display store whenever it changes.\n useEffect(() => {\n if (serverTimer) {\n syncTimerFromServer({\n timeEntryId: serverTimer.timeEntry.id,\n elapsedSeconds: serverTimer.elapsedSeconds,\n projectId: serverTimer.timeEntry.projectId ?? undefined,\n taskId: serverTimer.timeEntry.taskId ?? undefined,\n description: serverTimer.timeEntry.description ?? undefined,\n });\n } else if (!runningLoading) {\n syncTimerFromServer(null);\n }\n // Only re-sync when the server's running timer identity/elapsed changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [serverTimer?.timeEntry.id, serverTimer?.elapsedSeconds, runningLoading]);\n\n // Local ticking display for the live seconds counter.\n useEffect(() => {\n if (!activeTimer?.isRunning) return;\n const interval = setInterval(() => tickTimer(), 1000);\n return () => clearInterval(interval);\n }, [activeTimer?.isRunning, tickTimer]);\n\n const handleStart = async () => {\n await startTimerMutation({\n variables: {\n input: {\n description: description.trim() || undefined,\n projectId: projectId || undefined,\n billable,\n source: \"timer\",\n },\n },\n });\n setStartOpen(false);\n setDescription(\"\");\n setProjectId(\"\");\n setBillable(false);\n await refetchRunning();\n };\n\n const handleStop = async () => {\n if (!serverTimer) return;\n await stopTimerMutation({ variables: { id: serverTimer.timeEntry.id } });\n await refetchRunning();\n };\n\n const handleReset = async () => {\n if (!resetTarget) return;\n // Discard the in-progress session: stop it, then delete the entry.\n await stopTimerMutation({ variables: { id: resetTarget.timeEntry.id } });\n await deleteTimeEntry({ variables: { id: resetTarget.timeEntry.id } });\n setResetTarget(null);\n await refetchRunning();\n };\n\n const isRunning = Boolean(serverTimer);\n\n return (\n <PageLayout\n title={t(\"timecampus.timer.title\", \"Timer\")}\n description={t(\n \"timecampus.timer.description\",\n \"Focus timer for deep work sessions\",\n )}\n purpose={t(\n \"timecampus.timer.purpose\",\n \"A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later.\",\n )}\n >\n <div className=\"mx-auto max-w-lg\">\n <NextSteps\n storageKey=\"timecampus-timer\"\n className=\"mb-6\"\n steps={[\n {\n id: \"start\",\n label: isRunning\n ? t(\n \"timecampus.timer.nextSteps.stopRunning\",\n \"Stop the running timer\",\n )\n : t(\"timecampus.timer.nextSteps.start\", \"Start the timer\"),\n description: isRunning\n ? t(\n \"timecampus.timer.nextSteps.stopDescription\",\n \"End this session and save it as a time entry.\",\n )\n : t(\n \"timecampus.timer.nextSteps.startDescription\",\n \"Begin tracking the task you're working on right now.\",\n ),\n onClick: () => (isRunning ? handleStop() : setStartOpen(true)),\n },\n {\n id: \"assign-project\",\n label: t(\n \"timecampus.timer.nextSteps.assignProject\",\n \"Assign work to a project\",\n ),\n description: t(\n \"timecampus.timer.nextSteps.assignDescription\",\n \"Pick a project so this time bills correctly.\",\n ),\n onClick: () => navigate(\"/projects\"),\n },\n {\n id: \"review-entries\",\n label: t(\n \"timecampus.timer.nextSteps.review\",\n \"Review your time entries\",\n ),\n description: t(\n \"timecampus.timer.nextSteps.reviewDescription\",\n \"Check and tidy everything you've logged.\",\n ),\n onClick: () => navigate(\"/time/entries\"),\n },\n ]}\n />\n <GlassCard treatment=\"glass\" glow className=\"p-6 text-center sm:p-8\">\n <p className=\"font-mono text-5xl font-bold tabular-nums text-text-primary sm:text-6xl\">\n {activeTimer\n ? formatTimerDuration(activeTimer.seconds)\n : t(\"timecampus.timer.elapsedDefault\", \"00:00:00\")}\n </p>\n {activeTimer?.description && (\n <p className=\"mt-2 text-sm text-text-secondary\">\n {activeTimer.description}\n </p>\n )}\n <div className=\"mt-6 flex flex-wrap items-center justify-center gap-3\">\n {isRunning ? (\n <button\n onClick={handleStop}\n disabled={stopping}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-danger-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n <Square className=\"h-4 w-4\" />\n {stopping\n ? t(\"timecampus.timer.stopping\", \"Stopping...\")\n : t(\"timecampus.timer.stop\", \"Stop\")}\n </button>\n ) : (\n <button\n onClick={() => setStartOpen(true)}\n disabled={starting}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" />\n {t(\"timecampus.timer.start\", \"Start\")}\n </button>\n )}\n <button\n onClick={() => serverTimer && setResetTarget(serverTimer)}\n disabled={!isRunning || deleting}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-3 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n title={t(\n \"timecampus.timer.resetTooltip\",\n \"Discard the running timer without saving\",\n )}\n >\n <RotateCcw className=\"h-4 w-4\" />\n {t(\"timecampus.timer.reset\", \"Reset\")}\n </button>\n </div>\n </GlassCard>\n\n {/* Server-side running timers */}\n <GlassCard treatment=\"glass\" className=\"mt-6 p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.timer.runningTimers.title\", \"Running Timers\")}\n </h3>\n {runningTimers.length > 0 && (\n <StatusBadge\n label={t(\n \"timecampus.timer.runningTimers.active\",\n \"{{count}} active\",\n { count: runningTimers.length },\n )}\n variant=\"success\"\n dot\n />\n )}\n </div>\n {runningLoading && runningTimers.length === 0 ? (\n <LoadingSkeleton variant=\"list\" count={2} />\n ) : runningTimers.length === 0 ? (\n <EmptyState\n illustration=\"empty-sessions\"\n title={t(\n \"timecampus.timer.runningTimers.emptyTitle\",\n \"No active timers on the server\",\n )}\n description={t(\n \"timecampus.timer.runningTimers.emptyDescription\",\n \"Timers you start sync here so you can pick them up on any device.\",\n )}\n />\n ) : (\n <div className=\"flex flex-col gap-2\">\n {runningTimers.map((timer: RunningTimerItem) => (\n <div\n key={timer.timeEntry.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-3\"\n >\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-xs font-semibold text-primary\">\n {getInitials(\n timer.timeEntry.project?.name ??\n t(\n \"timecampus.timer.runningTimers.noProject\",\n \"No Project\",\n ),\n )}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {timer.timeEntry.project?.name ??\n t(\n \"timecampus.timer.runningTimers.noProject\",\n \"No Project\",\n )}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {timer.timeEntry.description ??\n t(\n \"timecampus.timer.runningTimers.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <p className=\"ml-2 shrink-0 font-mono text-sm font-medium text-text-primary\">\n {formatTimerDuration(timer.elapsedSeconds)}\n </p>\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Start timer modal */}\n <Modal\n open={startOpen}\n onClose={() => setStartOpen(false)}\n title={t(\"timecampus.timer.modal.startTitle\", \"Start a timer\")}\n description={t(\n \"timecampus.timer.modal.startDescription\",\n \"Track the task you're working on right now.\",\n )}\n footer={\n <>\n <button\n onClick={() => setStartOpen(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 {t(\"timecampus.timer.modal.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleStart}\n disabled={starting}\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 {starting\n ? t(\"timecampus.timer.modal.starting\", \"Starting...\")\n : t(\"timecampus.timer.modal.startTimer\", \"Start Timer\")}\n </button>\n </>\n }\n >\n {startError && (\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 {t(\n \"timecampus.timer.modal.startError\",\n \"Failed to start the timer. Please try again.\",\n )}\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n {t(\n \"timecampus.timer.modal.descriptionLabel\",\n \"What are you working on?\",\n )}\n </label>\n <input\n type=\"text\"\n placeholder={t(\n \"timecampus.timer.modal.descriptionPlaceholder\",\n \"Description\",\n )}\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>\n <label className=\"block text-sm font-medium text-text-primary\">\n {t(\"timecampus.timer.modal.projectLabel\", \"Project\")}\n </label>\n <select\n value={projectId}\n onChange={(e) => setProjectId(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 <option value=\"\">\n {t(\"timecampus.timer.modal.noProject\", \"No Project\")}\n </option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={billable}\n onChange={(e) => setBillable(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n {t(\"timecampus.timer.modal.billable\", \"Billable\")}\n </label>\n </div>\n </Modal>\n\n {/* Reset / discard confirm modal */}\n <Modal\n open={Boolean(resetTarget)}\n onClose={() => setResetTarget(null)}\n title={t(\n \"timecampus.timer.modal.discardTitle\",\n \"Discard running timer?\",\n )}\n description={t(\n \"timecampus.timer.modal.discardDescription\",\n \"This stops the timer and deletes the in-progress entry. It cannot be undone.\",\n )}\n footer={\n <>\n <button\n onClick={() => setResetTarget(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 {t(\"timecampus.timer.modal.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleReset}\n disabled={stopping || deleting}\n className=\"rounded-md bg-action-danger-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n {stopping || deleting\n ? t(\"timecampus.timer.modal.discarding\", \"Discarding...\")\n : t(\"timecampus.timer.modal.discard\", \"Discard\")}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n {t(\"timecampus.timer.modal.discardMessagePrefix\", \"The timer for\")}{\" \"}\n <span className=\"font-medium text-text-primary\">\n {resetTarget?.timeEntry.project?.name ??\n resetTarget?.timeEntry.description ??\n t(\"timecampus.timer.modal.sessionFallback\", \"this session\")}\n </span>{\" \"}\n {t(\n \"timecampus.timer.modal.discardMessageSuffix\",\n \"will be discarded.\",\n )}\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1,10 +1,10 @@
1
1
  import { cn as e, formatDate as t, formatHours as n, getInitials as ee } from "../../utils/helpers.js";
2
2
  import { StatCard as r } from "../../components/StatCard.js";
3
3
  import { LoadingSkeleton as i } from "../../components/LoadingSkeleton.js";
4
- import { useApproveRequestMutation as a, useGetApprovalStatsQuery as o, useListApprovalRequestsQuery as s, useRejectRequestMutation as te, useRequestRevisionMutation as ne, useWithdrawRequestMutation as re } from "../../../generated/wspace-operations.js";
5
- import { PageLayout as c } from "../../components/PageLayout.js";
6
- import { StatusBadge as l } from "../../components/StatusBadge.js";
7
- import { EmptyState as ie } from "../../components/EmptyState.js";
4
+ import { EmptyState as a } from "../../components/EmptyState.js";
5
+ import { useApproveRequestMutation as o, useGetApprovalStatsQuery as s, useListApprovalRequestsQuery as c, useRejectRequestMutation as te, useRequestRevisionMutation as ne, useWithdrawRequestMutation as re } from "../../../generated/wspace-operations.js";
6
+ import { PageLayout as l } from "../../components/PageLayout.js";
7
+ import { StatusBadge as ie } from "../../components/StatusBadge.js";
8
8
  import { Modal as u } from "../../components/Modal.js";
9
9
  import { ChartCard as ae } from "../../components/ChartCard.js";
10
10
  import { ToastContainer as oe, useToast as se } from "../../components/Toast.js";
@@ -13,29 +13,29 @@ import { ProgressMeter as le } from "../../components/charts/ProgressMeter.js";
13
13
  import "../../components/index.js";
14
14
  import { ApprovalStatus as ue } from "../../../generated/wspace-types.js";
15
15
  import { useState as d } from "react";
16
- import { CheckCircle as f, Clock as p, MessageSquare as de, MessagesSquare as m, Undo2 as h, X as fe, XCircle as g } from "lucide-react";
16
+ import { CheckCircle as f, Clock as p, MessageSquare as m, MessagesSquare as h, Undo2 as de, X as fe, XCircle as g } from "lucide-react";
17
17
  import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
18
18
  import { GlassCard as b } from "@burdenoff/fe-libs/ui";
19
19
  import { ContextConversationWidget as x } from "@burdenoff/fe-libs/shared";
20
20
  import { useRegisterPageConversationContext as S } from "@burdenoff/fe-libs/shared/providers/shell";
21
21
  //#region src/timecampus/pages/time-approval/TimeApprovalPage.tsx
22
22
  var C = 25;
23
- function w({ request: e }) {
23
+ function pe({ request: e }) {
24
24
  return S({
25
25
  entityType: "TIMECAMPUS_APPROVAL_REQUEST",
26
26
  entityId: e.id,
27
27
  contextTitle: `Approval · ${e.period ?? e.id}`
28
28
  }), /* @__PURE__ */ v(x, { widgetKey: "approval-review" });
29
29
  }
30
- var T = "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";
31
- function E() {
32
- let { toasts: x, addToast: S, dismissToast: E } = se(), { data: D, loading: O, error: pe, refetch: me, fetchMore: he } = s({
30
+ var w = "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";
31
+ function T() {
32
+ let { toasts: x, addToast: S, dismissToast: T } = se(), { data: E, loading: D, error: me, refetch: he, fetchMore: ge } = c({
33
33
  variables: { pagination: {
34
34
  limit: C,
35
35
  offset: 0
36
36
  } },
37
37
  notifyOnNetworkStatusChange: !0
38
- }), { data: ge, loading: _e, error: k, refetch: A } = o(), [j] = a(), [M, { loading: N }] = te(), [P, { loading: F }] = ne(), [ve] = re(), I = D?.approvalRequests?.items ?? [], ye = D?.approvalRequests?.total ?? 0, be = D?.approvalRequests?.hasMore ?? !1, L = ge?.approvalStats, xe = [
38
+ }), { data: _e, loading: O, error: k, refetch: A } = s(), [j] = o(), [M, { loading: N }] = te(), [P, { loading: F }] = ne(), [ve] = re(), I = E?.approvalRequests?.items ?? [], ye = E?.approvalRequests?.total ?? 0, be = E?.approvalRequests?.hasMore ?? !1, L = _e?.approvalStats, xe = [
39
39
  {
40
40
  name: "Pending",
41
41
  value: L?.pendingCount ?? 0
@@ -49,7 +49,7 @@ function E() {
49
49
  value: L?.rejectedCount ?? 0
50
50
  }
51
51
  ], [R, z] = d(!1), [Se, B] = d(null), [V, H] = d(null), [U, W] = d(""), [G, K] = d(null), [q, J] = d(""), [Y, X] = d(null), [Z, Q] = d(null), $ = async () => {
52
- await Promise.all([me(), A()]);
52
+ await Promise.all([he(), A()]);
53
53
  }, Ce = async (e) => {
54
54
  B(e);
55
55
  try {
@@ -66,21 +66,21 @@ function E() {
66
66
  B(null);
67
67
  }
68
68
  };
69
- return O && _e && I.length === 0 ? /* @__PURE__ */ v(c, {
69
+ return D && O && I.length === 0 ? /* @__PURE__ */ v(l, {
70
70
  title: "Time Approval",
71
71
  description: "Review and approve team timesheets",
72
72
  children: /* @__PURE__ */ v(i, {
73
73
  variant: "list",
74
74
  count: 4
75
75
  })
76
- }) : pe && k ? /* @__PURE__ */ v(c, {
76
+ }) : me && k ? /* @__PURE__ */ v(l, {
77
77
  title: "Time Approval",
78
78
  description: "Review and approve team timesheets",
79
79
  children: /* @__PURE__ */ v("div", {
80
80
  className: "rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text",
81
81
  children: "Failed to load approval data. Please try again later."
82
82
  })
83
- }) : /* @__PURE__ */ y(c, {
83
+ }) : /* @__PURE__ */ y(l, {
84
84
  title: "Time Approval",
85
85
  description: "Review and approve team timesheets",
86
86
  purpose: "The reviewer's queue for signing off team timesheets before they're billed or paid — approve, reject or request changes in one place so payroll and invoices go out accurate and on time.",
@@ -132,7 +132,7 @@ function E() {
132
132
  ye
133
133
  ]
134
134
  })]
135
- }), I.length === 0 && !O ? /* @__PURE__ */ v(ie, {
135
+ }), I.length === 0 && !D ? /* @__PURE__ */ v(a, {
136
136
  illustration: "empty-notifications",
137
137
  title: "No approval requests found",
138
138
  description: "You're all caught up — submitted timesheets waiting on your review will appear here."
@@ -186,7 +186,7 @@ function E() {
186
186
  }), /* @__PURE__ */ y("div", {
187
187
  className: "flex flex-wrap items-center gap-2",
188
188
  children: [
189
- /* @__PURE__ */ v(l, {
189
+ /* @__PURE__ */ v(ie, {
190
190
  label: r.status.toLowerCase(),
191
191
  status: r.status.toLowerCase()
192
192
  }),
@@ -212,21 +212,21 @@ function E() {
212
212
  disabled: a,
213
213
  title: "Request revision",
214
214
  className: "rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-warning-text hover:bg-status-warning-bg disabled:opacity-50",
215
- children: /* @__PURE__ */ v(de, { className: "h-4 w-4" })
215
+ children: /* @__PURE__ */ v(m, { className: "h-4 w-4" })
216
216
  }),
217
217
  /* @__PURE__ */ v("button", {
218
218
  onClick: () => X(r),
219
219
  disabled: a,
220
220
  title: "Withdraw request",
221
221
  className: "rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken disabled:opacity-50",
222
- children: /* @__PURE__ */ v(h, { className: "h-4 w-4" })
222
+ children: /* @__PURE__ */ v(de, { className: "h-4 w-4" })
223
223
  })
224
224
  ] }),
225
225
  /* @__PURE__ */ v("button", {
226
226
  onClick: () => Q(o ? null : r),
227
227
  title: "Review / Discussion",
228
228
  className: e("rounded-md border p-1.5", o ? "border-action-primary-bg bg-action-primary-bg/10 text-primary" : "border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken"),
229
- children: /* @__PURE__ */ v(m, { className: "h-4 w-4" })
229
+ children: /* @__PURE__ */ v(h, { className: "h-4 w-4" })
230
230
  })
231
231
  ]
232
232
  })]
@@ -250,7 +250,7 @@ function E() {
250
250
  onClick: async () => {
251
251
  z(!0);
252
252
  try {
253
- await he({
253
+ await ge({
254
254
  variables: { pagination: {
255
255
  limit: C,
256
256
  offset: I.length
@@ -309,13 +309,13 @@ function E() {
309
309
  " ",
310
310
  Z.period
311
311
  ]
312
- }), /* @__PURE__ */ v(w, { request: Z })] }) : /* @__PURE__ */ v(b, {
312
+ }), /* @__PURE__ */ v(pe, { request: Z })] }) : /* @__PURE__ */ v(b, {
313
313
  treatment: "insight",
314
314
  className: "border-border-seam p-6",
315
315
  children: /* @__PURE__ */ y("div", {
316
316
  className: "flex flex-col items-center justify-center gap-2 text-center",
317
317
  children: [
318
- /* @__PURE__ */ v(m, { className: "h-6 w-6 text-text-secondary" }),
318
+ /* @__PURE__ */ v(h, { className: "h-6 w-6 text-text-secondary" }),
319
319
  /* @__PURE__ */ v("p", {
320
320
  className: "text-sm font-medium text-text-primary",
321
321
  children: "No timesheet selected"
@@ -370,7 +370,7 @@ function E() {
370
370
  placeholder: "Explain why this timesheet is being rejected",
371
371
  value: U,
372
372
  onChange: (e) => W(e.target.value),
373
- className: T
373
+ className: w
374
374
  })]
375
375
  }),
376
376
  /* @__PURE__ */ y(u, {
@@ -412,7 +412,7 @@ function E() {
412
412
  placeholder: "Describe the changes needed before this can be approved",
413
413
  value: q,
414
414
  onChange: (e) => J(e.target.value),
415
- className: T
415
+ className: w
416
416
  })]
417
417
  }),
418
418
  /* @__PURE__ */ v(u, {
@@ -449,12 +449,12 @@ function E() {
449
449
  }),
450
450
  /* @__PURE__ */ v(oe, {
451
451
  toasts: x,
452
- onDismiss: E
452
+ onDismiss: T
453
453
  })
454
454
  ]
455
455
  });
456
456
  }
457
457
  //#endregion
458
- export { E as TimeApprovalPage };
458
+ export { T as TimeApprovalPage };
459
459
 
460
460
  //# sourceMappingURL=TimeApprovalPage.js.map