@burdenoff/microfe-timecampus 2026.830.2 → 2026.904.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/timecampus/components/index.js +4 -4
- package/dist/timecampus/pages/HomePage.d.ts.map +1 -1
- package/dist/timecampus/pages/HomePage.js +149 -133
- package/dist/timecampus/pages/HomePage.js.map +1 -1
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +7 -7
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js +7 -7
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
- package/dist/timecampus/pages/dashboard/ReportsPage.js +13 -13
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +7 -7
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +7 -7
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js +13 -13
- package/dist/timecampus/pages/geofencing/GeofencingPage.js +135 -135
- package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
- package/dist/timecampus/pages/invoicing/InvoicingPage.js +7 -7
- package/dist/timecampus/pages/invoicing/InvoicingPage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/DeadTimeRadarPage.js +8 -8
- package/dist/timecampus/pages/lifetime/FiniteLifePage.js +4 -4
- package/dist/timecampus/pages/lifetime/FiniteLifePage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/LifeBalancePage.js +104 -104
- package/dist/timecampus/pages/lifetime/LifeBalancePage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/LifeTimelinePage.js +7 -7
- package/dist/timecampus/pages/lifetime/LifeTimelinePage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/PersonalCorporatePage.js +33 -33
- package/dist/timecampus/pages/lifetime/PersonalCorporatePage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/RecommendationsPage.js +80 -80
- package/dist/timecampus/pages/lifetime/RecommendationsPage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/TimeWellSpentPage.js +16 -16
- package/dist/timecampus/pages/lifetime/ValuesGoalsPage.js +11 -11
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +11 -11
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingDetailPage.js +14 -14
- package/dist/timecampus/pages/meetings/MeetingDetailPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.js +12 -12
- package/dist/timecampus/pages/projects/ProjectsPage.js +5 -5
- package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/TasksPage.js +8 -8
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +8 -8
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js +8 -8
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js +13 -13
- package/dist/timecampus/pages/time/TimeEntriesPage.js +5 -5
- package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.js +138 -138
- package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.js +14 -14
- package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +26 -26
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
- package/dist/timecampus/pages/timeline/TimelinePage.js +8 -8
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +79 -79
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
- 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 {
|
|
5
|
-
import {
|
|
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
|
|
8
|
-
import { useTimeCampusStore as
|
|
9
|
-
import { StatusBadge as
|
|
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 } =
|
|
19
|
-
{ query:
|
|
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:
|
|
21
|
+
query: o,
|
|
22
22
|
variables: { pagination: { limit: 50 } }
|
|
23
23
|
},
|
|
24
|
-
{ query:
|
|
25
|
-
], [K, { loading: q, error: J }] =
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
31
|
-
function
|
|
32
|
-
let { toasts: x, addToast: S, dismissToast:
|
|
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:
|
|
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([
|
|
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
|
|
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
|
-
}) :
|
|
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(
|
|
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 && !
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
452
|
+
onDismiss: T
|
|
453
453
|
})
|
|
454
454
|
]
|
|
455
455
|
});
|
|
456
456
|
}
|
|
457
457
|
//#endregion
|
|
458
|
-
export {
|
|
458
|
+
export { T as TimeApprovalPage };
|
|
459
459
|
|
|
460
460
|
//# sourceMappingURL=TimeApprovalPage.js.map
|