@burdenoff/microfe-timecampus 2026.719.2 → 2026.719.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-operations.d.ts +40 -76
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-operations.js +40 -50
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.d.ts +21 -38
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/timecampus/pages/HomePage.js +1 -1
- package/dist/timecampus/pages/HomePage.js.map +1 -1
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +6 -3
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js +4 -4
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +8 -3
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +8 -4
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.js +4 -4
- package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +4 -4
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js +4 -2
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.js +6 -4
- package/dist/timecampus/pages/meetings/MeetingsPage.js.map +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectsPage.js +2 -2
- package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/TasksPage.js +4 -2
- package/dist/timecampus/pages/projects/TasksPage.js.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.d.ts.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +4 -4
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.js +137 -132
- package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.js +2 -2
- package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.js +2 -2
- package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +4 -4
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimeEntriesPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimeEntriesPage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport { Plus, Filter, Download, Pencil, Copy, Trash2 } from \"lucide-react\";\nimport { GlassCard } 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 { formatHours, formatDate, getInitials } from \"../../utils/helpers\";\nimport {\n useListTimeEntriesQuery,\n useCreateTimeEntryMutation,\n useUpdateTimeEntryMutation,\n useDeleteTimeEntryMutation,\n useDuplicateTimeEntryMutation,\n useListProjectsQuery,\n type TimeEntryFieldsFragment,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\nimport type { TimeEntryFilterInput } from \"@/generated/wspace-types\";\n\ntype ProjectOption = ListProjectsQuery[\"projects\"][\"items\"][number];\n\nconst PAGE_SIZE = 50;\n\n/** Convert an ISO datetime to a value usable by <input type=\"datetime-local\">. */\nfunction toLocalInput(iso?: string | null): string {\n if (!iso) return \"\";\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60000);\n return local.toISOString().slice(0, 16);\n}\n\n/** Convert a datetime-local value to an ISO string, or undefined if empty. */\nfunction toISO(local: string): string | undefined {\n if (!local) return undefined;\n const d = new Date(local);\n if (Number.isNaN(d.getTime())) return undefined;\n return d.toISOString();\n}\n\ninterface EntryFormState {\n description: string;\n projectId: string;\n startTime: string;\n endTime: string;\n billable: boolean;\n}\n\nfunction emptyForm(): EntryFormState {\n const now = new Date();\n const local = new Date(now.getTime() - now.getTimezoneOffset() * 60000);\n return {\n description: \"\",\n projectId: \"\",\n startTime: local.toISOString().slice(0, 16),\n endTime: \"\",\n billable: false,\n };\n}\n\ninterface EntryFormFieldsProps {\n form: EntryFormState;\n onChange: (patch: Partial<EntryFormState>) => void;\n projects: ProjectOption[];\n}\n\nfunction EntryFormFields({ form, onChange, projects }: EntryFormFieldsProps) {\n return (\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Description\n </label>\n <input\n type=\"text\"\n placeholder=\"What did you work on?\"\n value={form.description}\n onChange={(e) => onChange({ description: 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 Project\n </label>\n <select\n value={form.projectId}\n onChange={(e) => onChange({ projectId: 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=\"\">No Project</option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Start\n </label>\n <input\n type=\"datetime-local\"\n value={form.startTime}\n onChange={(e) => onChange({ startTime: e.target.value })}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n End\n </label>\n <input\n type=\"datetime-local\"\n value={form.endTime}\n onChange={(e) => onChange({ endTime: e.target.value })}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </div>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={form.billable}\n onChange={(e) => onChange({ billable: e.target.checked })}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n Billable\n </label>\n </div>\n );\n}\n\nexport function TimeEntriesPage() {\n // Filters\n const [filter, setFilter] = useState<TimeEntryFilterInput>({});\n const [filterOpen, setFilterOpen] = useState(false);\n const [draftBillable, setDraftBillable] = useState<\"all\" | \"yes\" | \"no\">(\n \"all\",\n );\n const [draftProjectId, setDraftProjectId] = useState(\"\");\n const [draftSearch, setDraftSearch] = useState(\"\");\n\n // Create / edit / delete modal state\n const [createOpen, setCreateOpen] = useState(false);\n const [createForm, setCreateForm] = useState<EntryFormState>(emptyForm);\n const [editEntry, setEditEntry] = useState<TimeEntryFieldsFragment | null>(\n null,\n );\n const [editForm, setEditForm] = useState<EntryFormState>(emptyForm);\n const [deleteEntry, setDeleteEntry] =\n useState<TimeEntryFieldsFragment | null>(null);\n\n const hasFilter = Object.keys(filter).length > 0;\n\n const { data, loading, error, fetchMore, refetch } = useListTimeEntriesQuery({\n variables: {\n limit: PAGE_SIZE,\n offset: 0,\n filter: hasFilter ? filter : undefined,\n },\n notifyOnNetworkStatusChange: true,\n });\n\n const { data: projectsData } = useListProjectsQuery({\n variables: { limit: 100 },\n });\n const projects: ProjectOption[] = projectsData?.projects?.items ?? [];\n\n const entries: TimeEntryFieldsFragment[] = useMemo(\n () => data?.timeEntries?.items ?? [],\n [data?.timeEntries?.items],\n );\n const hasMore = data?.timeEntries?.hasMore ?? false;\n const total = data?.timeEntries?.total ?? 0;\n\n const [createEntry, { loading: creating, error: createError }] =\n useCreateTimeEntryMutation();\n const [updateEntry, { loading: updating, error: updateError }] =\n useUpdateTimeEntryMutation();\n const [removeEntry, { loading: deleting }] = useDeleteTimeEntryMutation();\n const [duplicateEntry, { loading: duplicating }] =\n useDuplicateTimeEntryMutation();\n\n const [loadingMore, setLoadingMore] = useState(false);\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { offset: entries.length },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...fetchMoreResult,\n timeEntries: {\n ...fetchMoreResult.timeEntries,\n items: [\n ...prev.timeEntries.items,\n ...fetchMoreResult.timeEntries.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n const openCreate = () => {\n setCreateForm(emptyForm());\n setCreateOpen(true);\n };\n\n const handleCreate = async () => {\n const startTime = toISO(createForm.startTime);\n if (!startTime) return;\n await createEntry({\n variables: {\n input: {\n description: createForm.description.trim() || undefined,\n projectId: createForm.projectId || undefined,\n startTime,\n endTime: toISO(createForm.endTime),\n billable: createForm.billable,\n source: \"manual\",\n },\n },\n });\n setCreateOpen(false);\n await refetch();\n };\n\n const openEdit = (entry: TimeEntryFieldsFragment) => {\n setEditEntry(entry);\n setEditForm({\n description: entry.description ?? \"\",\n projectId: entry.projectId ?? \"\",\n startTime: toLocalInput(entry.startTime),\n endTime: toLocalInput(entry.endTime),\n billable: entry.billable,\n });\n };\n\n const handleUpdate = async () => {\n if (!editEntry) return;\n const startTime = toISO(editForm.startTime);\n if (!startTime) return;\n await updateEntry({\n variables: {\n id: editEntry.id,\n input: {\n description: editForm.description.trim() || undefined,\n projectId: editForm.projectId || undefined,\n startTime,\n endTime: toISO(editForm.endTime),\n billable: editForm.billable,\n },\n },\n });\n setEditEntry(null);\n await refetch();\n };\n\n const handleDuplicate = async (entry: TimeEntryFieldsFragment) => {\n await duplicateEntry({ variables: { id: entry.id } });\n await refetch();\n };\n\n const handleDelete = async () => {\n if (!deleteEntry) return;\n await removeEntry({ variables: { id: deleteEntry.id } });\n setDeleteEntry(null);\n await refetch();\n };\n\n const applyFilter = () => {\n const next: TimeEntryFilterInput = {};\n if (draftBillable !== \"all\") next.billable = draftBillable === \"yes\";\n if (draftProjectId) next.projectId = draftProjectId;\n if (draftSearch.trim()) next.search = draftSearch.trim();\n setFilter(next);\n setFilterOpen(false);\n };\n\n const clearFilter = () => {\n setDraftBillable(\"all\");\n setDraftProjectId(\"\");\n setDraftSearch(\"\");\n setFilter({});\n setFilterOpen(false);\n };\n\n // Client-side CSV export of the currently loaded rows. There is no backend\n // export operation, so this exports what has been fetched into the table.\n const handleExport = () => {\n const header = [\n \"Date\",\n \"Project\",\n \"Task/Description\",\n \"Duration (hours)\",\n \"Billable\",\n \"Status\",\n ];\n const rows = entries.map((entry) => [\n formatDate(entry.startTime),\n entry.project?.name ?? \"No Project\",\n entry.task?.title ?? entry.description ?? \"\",\n ((entry.duration ?? 0) / 3600).toFixed(2),\n entry.billable ? \"Yes\" : \"No\",\n entry.isRunning ? \"running\" : \"completed\",\n ]);\n const escape = (value: string) => `\"${value.replace(/\"/g, '\"\"')}\"`;\n const csv = [header, ...rows]\n .map((row) => row.map((cell: string) => escape(String(cell))).join(\",\"))\n .join(\"\\n\");\n const blob = new Blob([csv], { type: \"text/csv;charset=utf-8;\" });\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = `time-entries-${new Date().toISOString().slice(0, 10)}.csv`;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n };\n\n if (error) {\n return (\n <PageLayout\n title=\"Time Entries\"\n description=\"View and manage all time entries\"\n >\n <div className=\"rounded-xl border border-status-error-bg/40 bg-status-error-bg p-6 text-sm text-status-error-text\">\n Failed to load time entries. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Time Entries\"\n description=\"View and manage all time entries\"\n purpose=\"Every tracked block of work in one auditable list — filter, correct and confirm entries here before they roll into timesheets, approvals and client invoices.\"\n actions={\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => {\n setDraftBillable(\n filter.billable === undefined\n ? \"all\"\n : filter.billable\n ? \"yes\"\n : \"no\",\n );\n setDraftProjectId(filter.projectId ?? \"\");\n setDraftSearch(filter.search ?? \"\");\n setFilterOpen(true);\n }}\n className={`inline-flex items-center gap-1.5 rounded-md border px-3 py-2 text-sm font-medium hover:bg-bg-sunken/50 ${hasFilter ? \"border-action-primary-border text-primary\" : \"border-border-subtle text-text-primary\"}`}\n >\n <Filter className=\"h-4 w-4\" />\n Filter{hasFilter ? ` (${Object.keys(filter).length})` : \"\"}\n </button>\n <button\n onClick={handleExport}\n disabled={entries.length === 0}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n <Download className=\"h-4 w-4\" />\n Export\n </button>\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n New Entry\n </button>\n </div>\n }\n >\n {loading && entries.length === 0 ? (\n <LoadingSkeleton variant=\"table\" count={5} />\n ) : entries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No time entries found\"\n description=\"Track time with a timer or add a manual entry — your entries will appear here.\"\n action={\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n New Entry\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"glass\" glow className=\"overflow-hidden\">\n <div className=\"overflow-x-auto\">\n <table className=\"w-full\">\n <thead>\n <tr className=\"border-b border-border-subtle\">\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-secondary\">\n Entry\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-secondary\">\n Project\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-secondary\">\n Duration\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-secondary\">\n Billable\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-secondary\">\n Status\n </th>\n <th className=\"px-4 py-3 text-right text-xs font-medium text-text-secondary\">\n Actions\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {entries.map((entry: TimeEntryFieldsFragment) => (\n <tr\n key={entry.id}\n className=\"hover:bg-bg-sunken/50 transition-colors\"\n >\n <td className=\"px-4 py-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {entry.task?.title ??\n entry.description ??\n \"No description\"}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {formatDate(entry.startTime)}\n </p>\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <span className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-action-primary-bg/10 text-xs font-semibold text-primary\">\n {getInitials(entry.project?.name ?? \"No Project\")}\n </span>\n <span className=\"text-sm font-medium text-text-primary\">\n {entry.project?.name ?? \"No Project\"}\n </span>\n </div>\n </td>\n <td className=\"px-4 py-3 text-sm font-mono text-text-primary\">\n {formatHours((entry.duration ?? 0) / 3600)}\n </td>\n <td className=\"px-4 py-3\">\n <StatusBadge\n label={entry.billable ? \"Billable\" : \"Non-billable\"}\n variant={entry.billable ? \"success\" : \"muted\"}\n />\n </td>\n <td className=\"px-4 py-3\">\n <StatusBadge\n status={entry.isRunning ? \"running\" : \"completed\"}\n dot={entry.isRunning}\n label={entry.isRunning ? \"running\" : \"completed\"}\n />\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center justify-end gap-1\">\n <button\n onClick={() => openEdit(entry)}\n title=\"Edit entry\"\n aria-label=\"Edit entry\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => handleDuplicate(entry)}\n disabled={duplicating}\n title=\"Duplicate entry\"\n aria-label=\"Duplicate entry\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary disabled:opacity-50\"\n >\n <Copy className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setDeleteEntry(entry)}\n title=\"Delete entry\"\n aria-label=\"Delete entry\"\n className=\"rounded-md p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </GlassCard>\n\n <div className=\"mt-4 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n Showing {entries.length} of {total}\n </p>\n {hasMore && (\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n )}\n </div>\n </>\n )}\n\n {/* Create modal */}\n <Modal\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"New Time Entry\"\n description=\"Manually log a block of work.\"\n size=\"lg\"\n footer={\n <>\n <button\n onClick={() => setCreateOpen(false)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleCreate}\n disabled={creating || !createForm.startTime}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating ? \"Saving...\" : \"Create Entry\"}\n </button>\n </>\n }\n >\n {createError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to create entry. Please try again.\n </div>\n )}\n <EntryFormFields\n form={createForm}\n onChange={(patch) => setCreateForm((f) => ({ ...f, ...patch }))}\n projects={projects}\n />\n </Modal>\n\n {/* Edit modal */}\n <Modal\n open={Boolean(editEntry)}\n onClose={() => setEditEntry(null)}\n title=\"Edit Time Entry\"\n description=\"Correct the details of this logged block.\"\n size=\"lg\"\n footer={\n <>\n <button\n onClick={() => setEditEntry(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleUpdate}\n disabled={updating || !editForm.startTime}\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 {updating ? \"Saving...\" : \"Save Changes\"}\n </button>\n </>\n }\n >\n {updateError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to update entry. Please try again.\n </div>\n )}\n <EntryFormFields\n form={editForm}\n onChange={(patch) => setEditForm((f) => ({ ...f, ...patch }))}\n projects={projects}\n />\n </Modal>\n\n {/* Delete confirm modal */}\n <Modal\n open={Boolean(deleteEntry)}\n onClose={() => setDeleteEntry(null)}\n title=\"Delete time entry?\"\n description=\"This permanently removes the entry. It cannot be undone.\"\n footer={\n <>\n <button\n onClick={() => setDeleteEntry(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={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 {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Delete the entry for{\" \"}\n <span className=\"font-medium text-text-primary\">\n {deleteEntry?.project?.name ??\n deleteEntry?.description ??\n \"this block of work\"}\n </span>\n ?\n </p>\n </Modal>\n\n {/* Filter modal */}\n <Modal\n open={filterOpen}\n onClose={() => setFilterOpen(false)}\n title=\"Filter entries\"\n description=\"Narrow the list to what you need.\"\n footer={\n <>\n <button\n onClick={clearFilter}\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 Clear\n </button>\n <button\n onClick={applyFilter}\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\"\n >\n Apply\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Search\n </label>\n <input\n type=\"text\"\n placeholder=\"Description contains...\"\n value={draftSearch}\n onChange={(e) => setDraftSearch(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 Project\n </label>\n <select\n value={draftProjectId}\n onChange={(e) => setDraftProjectId(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=\"\">All Projects</option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Billable\n </label>\n <select\n value={draftBillable}\n onChange={(e) =>\n setDraftBillable(e.target.value as \"all\" | \"yes\" | \"no\")\n }\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=\"all\">All</option>\n <option value=\"yes\">Billable only</option>\n <option value=\"no\">Non-billable only</option>\n </select>\n </div>\n </div>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TimeEntriesPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimeEntriesPage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport { Plus, Filter, Download, Pencil, Copy, Trash2 } from \"lucide-react\";\nimport { GlassCard } 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 { formatHours, formatDate, getInitials } from \"../../utils/helpers\";\nimport {\n useListTimeEntriesQuery,\n useCreateTimeEntryMutation,\n useUpdateTimeEntryMutation,\n useDeleteTimeEntryMutation,\n useDuplicateTimeEntryMutation,\n useListProjectsQuery,\n type TimeEntryFieldsFragment,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\nimport type { TimeEntryFilterInput } from \"@/generated/wspace-types\";\n\ntype ProjectOption = ListProjectsQuery[\"projects\"][\"items\"][number];\n\nconst PAGE_SIZE = 50;\n\n/** Convert an ISO datetime to a value usable by <input type=\"datetime-local\">. */\nfunction toLocalInput(iso?: string | null): string {\n if (!iso) return \"\";\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60000);\n return local.toISOString().slice(0, 16);\n}\n\n/** Convert a datetime-local value to an ISO string, or undefined if empty. */\nfunction toISO(local: string): string | undefined {\n if (!local) return undefined;\n const d = new Date(local);\n if (Number.isNaN(d.getTime())) return undefined;\n return d.toISOString();\n}\n\ninterface EntryFormState {\n description: string;\n projectId: string;\n startTime: string;\n endTime: string;\n billable: boolean;\n}\n\nfunction emptyForm(): EntryFormState {\n const now = new Date();\n const local = new Date(now.getTime() - now.getTimezoneOffset() * 60000);\n return {\n description: \"\",\n projectId: \"\",\n startTime: local.toISOString().slice(0, 16),\n endTime: \"\",\n billable: false,\n };\n}\n\ninterface EntryFormFieldsProps {\n form: EntryFormState;\n onChange: (patch: Partial<EntryFormState>) => void;\n projects: ProjectOption[];\n}\n\nfunction EntryFormFields({ form, onChange, projects }: EntryFormFieldsProps) {\n return (\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Description\n </label>\n <input\n type=\"text\"\n placeholder=\"What did you work on?\"\n value={form.description}\n onChange={(e) => onChange({ description: 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 Project\n </label>\n <select\n value={form.projectId}\n onChange={(e) => onChange({ projectId: 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=\"\">No Project</option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Start\n </label>\n <input\n type=\"datetime-local\"\n value={form.startTime}\n onChange={(e) => onChange({ startTime: e.target.value })}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n End\n </label>\n <input\n type=\"datetime-local\"\n value={form.endTime}\n onChange={(e) => onChange({ endTime: e.target.value })}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </div>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={form.billable}\n onChange={(e) => onChange({ billable: e.target.checked })}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n Billable\n </label>\n </div>\n );\n}\n\nexport function TimeEntriesPage() {\n // Filters\n const [filter, setFilter] = useState<TimeEntryFilterInput>({});\n const [filterOpen, setFilterOpen] = useState(false);\n const [draftBillable, setDraftBillable] = useState<\"all\" | \"yes\" | \"no\">(\n \"all\",\n );\n const [draftProjectId, setDraftProjectId] = useState(\"\");\n const [draftSearch, setDraftSearch] = useState(\"\");\n\n // Create / edit / delete modal state\n const [createOpen, setCreateOpen] = useState(false);\n const [createForm, setCreateForm] = useState<EntryFormState>(emptyForm);\n const [editEntry, setEditEntry] = useState<TimeEntryFieldsFragment | null>(\n null,\n );\n const [editForm, setEditForm] = useState<EntryFormState>(emptyForm);\n const [deleteEntry, setDeleteEntry] =\n useState<TimeEntryFieldsFragment | null>(null);\n\n const hasFilter = Object.keys(filter).length > 0;\n\n const { data, loading, error, fetchMore, refetch } = useListTimeEntriesQuery({\n variables: {\n pagination: { limit: PAGE_SIZE, offset: 0 },\n filter: hasFilter ? filter : undefined,\n },\n notifyOnNetworkStatusChange: true,\n });\n\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 100 } },\n });\n const projects: ProjectOption[] = projectsData?.projects?.items ?? [];\n\n const entries: TimeEntryFieldsFragment[] = useMemo(\n () => data?.timeEntries?.items ?? [],\n [data?.timeEntries?.items],\n );\n const hasMore = data?.timeEntries?.hasMore ?? false;\n const total = data?.timeEntries?.total ?? 0;\n\n const [createEntry, { loading: creating, error: createError }] =\n useCreateTimeEntryMutation();\n const [updateEntry, { loading: updating, error: updateError }] =\n useUpdateTimeEntryMutation();\n const [removeEntry, { loading: deleting }] = useDeleteTimeEntryMutation();\n const [duplicateEntry, { loading: duplicating }] =\n useDuplicateTimeEntryMutation();\n\n const [loadingMore, setLoadingMore] = useState(false);\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { limit: PAGE_SIZE, offset: entries.length } },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...fetchMoreResult,\n timeEntries: {\n ...fetchMoreResult.timeEntries,\n items: [\n ...prev.timeEntries.items,\n ...fetchMoreResult.timeEntries.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n const openCreate = () => {\n setCreateForm(emptyForm());\n setCreateOpen(true);\n };\n\n const handleCreate = async () => {\n const startTime = toISO(createForm.startTime);\n if (!startTime) return;\n await createEntry({\n variables: {\n input: {\n description: createForm.description.trim() || undefined,\n projectId: createForm.projectId || undefined,\n startTime,\n endTime: toISO(createForm.endTime),\n billable: createForm.billable,\n source: \"manual\",\n },\n },\n });\n setCreateOpen(false);\n await refetch();\n };\n\n const openEdit = (entry: TimeEntryFieldsFragment) => {\n setEditEntry(entry);\n setEditForm({\n description: entry.description ?? \"\",\n projectId: entry.projectId ?? \"\",\n startTime: toLocalInput(entry.startTime),\n endTime: toLocalInput(entry.endTime),\n billable: entry.billable,\n });\n };\n\n const handleUpdate = async () => {\n if (!editEntry) return;\n const startTime = toISO(editForm.startTime);\n if (!startTime) return;\n await updateEntry({\n variables: {\n id: editEntry.id,\n input: {\n description: editForm.description.trim() || undefined,\n projectId: editForm.projectId || undefined,\n startTime,\n endTime: toISO(editForm.endTime),\n billable: editForm.billable,\n },\n },\n });\n setEditEntry(null);\n await refetch();\n };\n\n const handleDuplicate = async (entry: TimeEntryFieldsFragment) => {\n await duplicateEntry({ variables: { id: entry.id } });\n await refetch();\n };\n\n const handleDelete = async () => {\n if (!deleteEntry) return;\n await removeEntry({ variables: { id: deleteEntry.id } });\n setDeleteEntry(null);\n await refetch();\n };\n\n const applyFilter = () => {\n const next: TimeEntryFilterInput = {};\n if (draftBillable !== \"all\") next.billable = draftBillable === \"yes\";\n if (draftProjectId) next.projectId = draftProjectId;\n if (draftSearch.trim()) next.search = draftSearch.trim();\n setFilter(next);\n setFilterOpen(false);\n };\n\n const clearFilter = () => {\n setDraftBillable(\"all\");\n setDraftProjectId(\"\");\n setDraftSearch(\"\");\n setFilter({});\n setFilterOpen(false);\n };\n\n // Client-side CSV export of the currently loaded rows. There is no backend\n // export operation, so this exports what has been fetched into the table.\n const handleExport = () => {\n const header = [\n \"Date\",\n \"Project\",\n \"Task/Description\",\n \"Duration (hours)\",\n \"Billable\",\n \"Status\",\n ];\n const rows = entries.map((entry) => [\n formatDate(entry.startTime),\n entry.project?.name ?? \"No Project\",\n entry.task?.title ?? entry.description ?? \"\",\n ((entry.duration ?? 0) / 3600).toFixed(2),\n entry.billable ? \"Yes\" : \"No\",\n entry.isRunning ? \"running\" : \"completed\",\n ]);\n const escape = (value: string) => `\"${value.replace(/\"/g, '\"\"')}\"`;\n const csv = [header, ...rows]\n .map((row) => row.map((cell: string) => escape(String(cell))).join(\",\"))\n .join(\"\\n\");\n const blob = new Blob([csv], { type: \"text/csv;charset=utf-8;\" });\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = `time-entries-${new Date().toISOString().slice(0, 10)}.csv`;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n };\n\n if (error) {\n return (\n <PageLayout\n title=\"Time Entries\"\n description=\"View and manage all time entries\"\n >\n <div className=\"rounded-xl border border-status-error-bg/40 bg-status-error-bg p-6 text-sm text-status-error-text\">\n Failed to load time entries. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Time Entries\"\n description=\"View and manage all time entries\"\n purpose=\"Every tracked block of work in one auditable list — filter, correct and confirm entries here before they roll into timesheets, approvals and client invoices.\"\n actions={\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => {\n setDraftBillable(\n filter.billable === undefined\n ? \"all\"\n : filter.billable\n ? \"yes\"\n : \"no\",\n );\n setDraftProjectId(filter.projectId ?? \"\");\n setDraftSearch(filter.search ?? \"\");\n setFilterOpen(true);\n }}\n className={`inline-flex items-center gap-1.5 rounded-md border px-3 py-2 text-sm font-medium hover:bg-bg-sunken/50 ${hasFilter ? \"border-action-primary-border text-primary\" : \"border-border-subtle text-text-primary\"}`}\n >\n <Filter className=\"h-4 w-4\" />\n Filter{hasFilter ? ` (${Object.keys(filter).length})` : \"\"}\n </button>\n <button\n onClick={handleExport}\n disabled={entries.length === 0}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n <Download className=\"h-4 w-4\" />\n Export\n </button>\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n New Entry\n </button>\n </div>\n }\n >\n {loading && entries.length === 0 ? (\n <LoadingSkeleton variant=\"table\" count={5} />\n ) : entries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No time entries found\"\n description=\"Track time with a timer or add a manual entry — your entries will appear here.\"\n action={\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n New Entry\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"glass\" glow className=\"overflow-hidden\">\n <div className=\"overflow-x-auto\">\n <table className=\"w-full\">\n <thead>\n <tr className=\"border-b border-border-subtle\">\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-secondary\">\n Entry\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-secondary\">\n Project\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-secondary\">\n Duration\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-secondary\">\n Billable\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-text-secondary\">\n Status\n </th>\n <th className=\"px-4 py-3 text-right text-xs font-medium text-text-secondary\">\n Actions\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {entries.map((entry: TimeEntryFieldsFragment) => (\n <tr\n key={entry.id}\n className=\"hover:bg-bg-sunken/50 transition-colors\"\n >\n <td className=\"px-4 py-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {entry.task?.title ??\n entry.description ??\n \"No description\"}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {formatDate(entry.startTime)}\n </p>\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <span className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-action-primary-bg/10 text-xs font-semibold text-primary\">\n {getInitials(entry.project?.name ?? \"No Project\")}\n </span>\n <span className=\"text-sm font-medium text-text-primary\">\n {entry.project?.name ?? \"No Project\"}\n </span>\n </div>\n </td>\n <td className=\"px-4 py-3 text-sm font-mono text-text-primary\">\n {formatHours((entry.duration ?? 0) / 3600)}\n </td>\n <td className=\"px-4 py-3\">\n <StatusBadge\n label={entry.billable ? \"Billable\" : \"Non-billable\"}\n variant={entry.billable ? \"success\" : \"muted\"}\n />\n </td>\n <td className=\"px-4 py-3\">\n <StatusBadge\n status={entry.isRunning ? \"running\" : \"completed\"}\n dot={entry.isRunning}\n label={entry.isRunning ? \"running\" : \"completed\"}\n />\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center justify-end gap-1\">\n <button\n onClick={() => openEdit(entry)}\n title=\"Edit entry\"\n aria-label=\"Edit entry\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => handleDuplicate(entry)}\n disabled={duplicating}\n title=\"Duplicate entry\"\n aria-label=\"Duplicate entry\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary disabled:opacity-50\"\n >\n <Copy className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setDeleteEntry(entry)}\n title=\"Delete entry\"\n aria-label=\"Delete entry\"\n className=\"rounded-md p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </GlassCard>\n\n <div className=\"mt-4 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n Showing {entries.length} of {total}\n </p>\n {hasMore && (\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n )}\n </div>\n </>\n )}\n\n {/* Create modal */}\n <Modal\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"New Time Entry\"\n description=\"Manually log a block of work.\"\n size=\"lg\"\n footer={\n <>\n <button\n onClick={() => setCreateOpen(false)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleCreate}\n disabled={creating || !createForm.startTime}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating ? \"Saving...\" : \"Create Entry\"}\n </button>\n </>\n }\n >\n {createError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to create entry. Please try again.\n </div>\n )}\n <EntryFormFields\n form={createForm}\n onChange={(patch) => setCreateForm((f) => ({ ...f, ...patch }))}\n projects={projects}\n />\n </Modal>\n\n {/* Edit modal */}\n <Modal\n open={Boolean(editEntry)}\n onClose={() => setEditEntry(null)}\n title=\"Edit Time Entry\"\n description=\"Correct the details of this logged block.\"\n size=\"lg\"\n footer={\n <>\n <button\n onClick={() => setEditEntry(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleUpdate}\n disabled={updating || !editForm.startTime}\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 {updating ? \"Saving...\" : \"Save Changes\"}\n </button>\n </>\n }\n >\n {updateError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to update entry. Please try again.\n </div>\n )}\n <EntryFormFields\n form={editForm}\n onChange={(patch) => setEditForm((f) => ({ ...f, ...patch }))}\n projects={projects}\n />\n </Modal>\n\n {/* Delete confirm modal */}\n <Modal\n open={Boolean(deleteEntry)}\n onClose={() => setDeleteEntry(null)}\n title=\"Delete time entry?\"\n description=\"This permanently removes the entry. It cannot be undone.\"\n footer={\n <>\n <button\n onClick={() => setDeleteEntry(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={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 {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Delete the entry for{\" \"}\n <span className=\"font-medium text-text-primary\">\n {deleteEntry?.project?.name ??\n deleteEntry?.description ??\n \"this block of work\"}\n </span>\n ?\n </p>\n </Modal>\n\n {/* Filter modal */}\n <Modal\n open={filterOpen}\n onClose={() => setFilterOpen(false)}\n title=\"Filter entries\"\n description=\"Narrow the list to what you need.\"\n footer={\n <>\n <button\n onClick={clearFilter}\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 Clear\n </button>\n <button\n onClick={applyFilter}\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\"\n >\n Apply\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Search\n </label>\n <input\n type=\"text\"\n placeholder=\"Description contains...\"\n value={draftSearch}\n onChange={(e) => setDraftSearch(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 Project\n </label>\n <select\n value={draftProjectId}\n onChange={(e) => setDraftProjectId(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=\"\">All Projects</option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Billable\n </label>\n <select\n value={draftBillable}\n onChange={(e) =>\n setDraftBillable(e.target.value as \"all\" | \"yes\" | \"no\")\n }\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=\"all\">All</option>\n <option value=\"yes\">Billable only</option>\n <option value=\"no\">Non-billable only</option>\n </select>\n </div>\n </div>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimeTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/time/TimeTrackingPage.tsx"],"names":[],"mappings":"AAsCA,wBAAgB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"TimeTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/time/TimeTrackingPage.tsx"],"names":[],"mappings":"AAsCA,wBAAgB,gBAAgB,gCAuhB/B"}
|
|
@@ -22,11 +22,11 @@ function b() {
|
|
|
22
22
|
let { t: b } = y(), x = (e, t) => {
|
|
23
23
|
let n = b(e);
|
|
24
24
|
return n === e ? t : n;
|
|
25
|
-
}, { activeTimer: S, tickTimer: C, syncTimerFromServer: w } = ue(), T = e(), [E, D] = u(!1), [O, k] = u(""), [A, j] = u(""), [M, N] = u(!1), { data: P, loading: F } = ie(), { data: I, loading: L } = se({ variables: { limit: 10 } }), { data: R, loading: z, refetch: B } = oe({ fetchPolicy: "cache-and-network" }), { data: me } = ae({ variables: { limit: 50 } }), he = me?.projects?.items ?? [], V = R?.runningTimers?.[0] ?? null, H = [
|
|
25
|
+
}, { activeTimer: S, tickTimer: C, syncTimerFromServer: w } = ue(), T = e(), [E, D] = u(!1), [O, k] = u(""), [A, j] = u(""), [M, N] = u(!1), { data: P, loading: F } = ie(), { data: I, loading: L } = se({ variables: { pagination: { limit: 10 } } }), { data: R, loading: z, refetch: B } = oe({ fetchPolicy: "cache-and-network" }), { data: me } = ae({ variables: { pagination: { limit: 50 } } }), he = me?.projects?.items ?? [], V = R?.runningTimers?.[0] ?? null, H = [
|
|
26
26
|
{ query: ne },
|
|
27
27
|
{
|
|
28
28
|
query: re,
|
|
29
|
-
variables: { limit: 10 }
|
|
29
|
+
variables: { pagination: { limit: 10 } }
|
|
30
30
|
},
|
|
31
31
|
{ query: te }
|
|
32
32
|
], [ge, { loading: U, error: _e }] = ce({ refetchQueries: H }), [ve, { loading: W }] = le({ refetchQueries: H });
|
|
@@ -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: { 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: { 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 { query: ListTimeEntriesDocument, variables: { limit: 10 } },\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimerPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"names":[],"mappings":"AA2BA,wBAAgB,SAAS,
|
|
1
|
+
{"version":3,"file":"TimerPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"names":[],"mappings":"AA2BA,wBAAgB,SAAS,gCA+UxB"}
|
|
@@ -13,11 +13,11 @@ import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
|
|
|
13
13
|
import { GlassCard as S, NextSteps as C } from "@burdenoff/fe-libs/ui";
|
|
14
14
|
//#region src/timecampus/pages/time/TimerPage.tsx
|
|
15
15
|
function w() {
|
|
16
|
-
let { activeTimer: w, tickTimer: T, syncTimerFromServer: E } = l(), D = e(), [O, k] = h(!1), [A, j] = h(""), [M, N] = h(""), [P, F] = h(!1), [I, L] = h(null), { data: R, loading: z, refetch: B } = o({ fetchPolicy: "cache-and-network" }), { data: V } = a({ variables: { limit: 50 } }), H = V?.projects?.items ?? [], U = R?.runningTimers ?? [], W = U[0] ?? null, G = [
|
|
16
|
+
let { activeTimer: w, tickTimer: T, syncTimerFromServer: E } = l(), D = e(), [O, k] = h(!1), [A, j] = h(""), [M, N] = h(""), [P, F] = h(!1), [I, L] = h(null), { data: R, loading: z, refetch: B } = o({ fetchPolicy: "cache-and-network" }), { data: V } = a({ variables: { pagination: { limit: 50 } } }), H = V?.projects?.items ?? [], U = R?.runningTimers ?? [], W = U[0] ?? null, G = [
|
|
17
17
|
{ query: r },
|
|
18
18
|
{
|
|
19
19
|
query: i,
|
|
20
|
-
variables: { limit: 50 }
|
|
20
|
+
variables: { pagination: { limit: 50 } }
|
|
21
21
|
},
|
|
22
22
|
{ query: te }
|
|
23
23
|
], [K, { loading: q, error: J }] = s({ refetchQueries: G }), [Y, { loading: X }] = c({ refetchQueries: G }), [re, { loading: Z }] = ne({ refetchQueries: G });
|
|
@@ -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 {\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\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: { 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 { query: ListTimeEntriesDocument, variables: { limit: 50 } },\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=\"Timer\"\n description=\"Focus timer for deep work sessions\"\n 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.\"\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 ? \"Stop the running timer\" : \"Start the timer\",\n description: isRunning\n ? \"End this session and save it as a time entry.\"\n : \"Begin tracking the task you're working on right now.\",\n onClick: () => (isRunning ? handleStop() : setStartOpen(true)),\n },\n {\n id: \"assign-project\",\n label: \"Assign work to a project\",\n description: \"Pick a project so this time bills correctly.\",\n onClick: () => navigate(\"/projects\"),\n },\n {\n id: \"review-entries\",\n label: \"Review your time entries\",\n description: \"Check and tidy everything you've logged.\",\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 : \"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 ? \"Stopping...\" : \"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 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=\"Discard the running timer without saving\"\n >\n <RotateCcw className=\"h-4 w-4\" />\n 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 Running Timers\n </h3>\n {runningTimers.length > 0 && (\n <StatusBadge\n label={`${runningTimers.length} active`}\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=\"No active timers on the server\"\n description=\"Timers you start sync here so you can pick them up on any device.\"\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(timer.timeEntry.project?.name ?? \"No Project\")}\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 ?? \"No Project\"}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {timer.timeEntry.description ?? \"No description\"}\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=\"Start a timer\"\n description=\"Track the task you're working on right now.\"\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 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 ? \"Starting...\" : \"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 Failed to start the timer. Please try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n What are you working on?\n </label>\n <input\n type=\"text\"\n placeholder=\"Description\"\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 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=\"\">No Project</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 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=\"Discard running timer?\"\n description=\"This stops the timer and deletes the in-progress entry. It cannot be undone.\"\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 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 ? \"Discarding...\" : \"Discard\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n The timer for{\" \"}\n <span className=\"font-medium text-text-primary\">\n {resetTarget?.timeEntry.project?.name ??\n resetTarget?.timeEntry.description ??\n \"this session\"}\n </span>{\" \"}\n will be discarded.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AA2BA,SAAgB,IAAY;CAC1B,IAAM,EAAE,gBAAa,cAAW,2BAAwB,GAAoB,EACtE,IAAW,GAAuB,EAElC,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAW,KAAgB,EAAS,GAAG,EACxC,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAa,KAAkB,EAAkC,KAAK,EAEvE,EACJ,MAAM,GACN,SAAS,GACT,SAAS,MACP,EAA0B,EAAE,aAAa,qBAAqB,CAAC,EAE7D,EAAE,MAAM,MAAiB,EAAqB,EAClD,WAAW,EAAE,OAAO,IAAI,EACzB,CAAC,EACI,IAA4B,GAAc,UAAU,SAAS,EAAE,EAE/D,IAAgB,GAAa,iBAAiB,EAAE,EAChD,IAAc,EAAc,MAAM,MAElC,IAAa;EACjB,EAAE,OAAO,GAA2B;EACpC;GAAE,OAAO;GAAyB,WAAW,EAAE,OAAO,IAAI;GAAE;EAC5D,EAAE,OAAO,IAA2B;EACrC,EAEK,CAAC,GAAoB,EAAE,SAAS,GAAU,OAAO,OACrD,EAAsB,EAAE,gBAAgB,GAAY,CAAC,EACjD,CAAC,GAAmB,EAAE,SAAS,OAAc,EAAqB,EACtE,gBAAgB,GACjB,CAAC,EACI,CAAC,IAAiB,EAAE,SAAS,OAAc,GAA2B,EAC1E,gBAAgB,GACjB,CAAC;AAqBF,CAjBA,QAAgB;AACd,EAAI,IACF,EAAoB;GAClB,aAAa,EAAY,UAAU;GACnC,gBAAgB,EAAY;GAC5B,WAAW,EAAY,UAAU,aAAa,KAAA;GAC9C,QAAQ,EAAY,UAAU,UAAU,KAAA;GACxC,aAAa,EAAY,UAAU,eAAe,KAAA;GACnD,CAAC,GACQ,KACV,EAAoB,KAAK;IAI1B;EAAC,GAAa,UAAU;EAAI,GAAa;EAAgB;EAAe,CAAC,EAG5E,QAAgB;AACd,MAAI,CAAC,GAAa,UAAW;EAC7B,IAAM,IAAW,kBAAkB,GAAW,EAAE,IAAK;AACrD,eAAa,cAAc,EAAS;IACnC,CAAC,GAAa,WAAW,EAAU,CAAC;CAEvC,IAAM,KAAc,YAAY;AAe9B,EAdA,MAAM,EAAmB,EACvB,WAAW,EACT,OAAO;GACL,aAAa,EAAY,MAAM,IAAI,KAAA;GACnC,WAAW,KAAa,KAAA;GACxB;GACA,QAAQ;GACT,EACF,EACF,CAAC,EACF,EAAa,GAAM,EACnB,EAAe,GAAG,EAClB,EAAa,GAAG,EAChB,EAAY,GAAM,EAClB,MAAM,GAAgB;IAGlB,IAAa,YAAY;AACxB,QACL,MAAM,EAAkB,EAAE,WAAW,EAAE,IAAI,EAAY,UAAU,IAAI,EAAE,CAAC,EACxE,MAAM,GAAgB;IAGlB,KAAc,YAAY;AACzB,QAEL,MAAM,EAAkB,EAAE,WAAW,EAAE,IAAI,EAAY,UAAU,IAAI,EAAE,CAAC,EACxE,MAAM,GAAgB,EAAE,WAAW,EAAE,IAAI,EAAY,UAAU,IAAI,EAAE,CAAC,EACtE,EAAe,KAAK,EACpB,MAAM,GAAgB;IAGlB,IAAY,EAAQ;AAE1B,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SAAQ;YAHV;GAKE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,YAAW;MACX,WAAU;MACV,OAAO;OACL;QACE,IAAI;QACJ,OAAO,IAAY,2BAA2B;QAC9C,aAAa,IACT,kDACA;QACJ,eAAgB,IAAY,GAAY,GAAG,EAAa,GAAK;QAC9D;OACD;QACE,IAAI;QACJ,OAAO;QACP,aAAa;QACb,eAAe,EAAS,YAAY;QACrC;OACD;QACE,IAAI;QACJ,OAAO;QACP,aAAa;QACb,eAAe,EAAS,gBAAgB;QACzC;OACF;MACD,CAAA;KACF,kBAAC,GAAD;MAAW,WAAU;MAAQ,MAAA;MAAK,WAAU;gBAA5C;OACE,kBAAC,KAAD;QAAG,WAAU;kBACV,IACG,EAAoB,EAAY,QAAQ,GACxC;QACF,CAAA;OACH,GAAa,eACZ,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAY;QACX,CAAA;OAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACG,IACC,kBAAC,UAAD;SACE,SAAS;SACT,UAAU;SACV,WAAU;mBAHZ,CAKE,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA,EAC7B,IAAW,gBAAgB,OACrB;aAET,kBAAC,UAAD;SACE,eAAe,EAAa,GAAK;SACjC,UAAU;SACV,WAAU;mBAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,QAErB;YAEX,kBAAC,UAAD;SACE,eAAe,KAAe,EAAe,EAAY;SACzD,UAAU,CAAC,KAAa;SACxB,WAAU;SACV,OAAM;mBAJR,CAME,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAAA,QAE1B;WACL;;OACI;;KAGZ,kBAAC,GAAD;MAAW,WAAU;MAAQ,WAAU;gBAAvC,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,MAAD;QAAI,WAAU;kBAA0C;QAEnD,CAAA,EACJ,EAAc,SAAS,KACtB,kBAAC,GAAD;QACE,OAAO,GAAG,EAAc,OAAO;QAC/B,SAAQ;QACR,KAAA;QACA,CAAA,CAEA;UACL,KAAkB,EAAc,WAAW,IAC1C,kBAAC,GAAD;OAAiB,SAAQ;OAAO,OAAO;OAAK,CAAA,GAC1C,EAAc,WAAW,IAC3B,kBAAC,GAAD;OACE,cAAa;OACb,OAAM;OACN,aAAY;OACZ,CAAA,GAEF,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAc,KAAK,MAClB,kBAAC,OAAD;QAEE,WAAU;kBAFZ;SAIE,kBAAC,QAAD;UAAM,WAAU;oBACb,GAAY,EAAM,UAAU,SAAS,QAAQ,aAAa;UACtD,CAAA;SACP,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAM,UAAU,SAAS,QAAQ;WAChC,CAAA,EACJ,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAM,UAAU,eAAe;WAC9B,CAAA,CACA;;SACN,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAoB,EAAM,eAAe;UACxC,CAAA;SACA;UAjBC,EAAM,UAAU,GAiBjB,CACN;OACE,CAAA,CAEE;;KACR;;GAGN,kBAAC,GAAD;IACE,MAAM;IACN,eAAe,EAAa,GAAM;IAClC,OAAM;IACN,aAAY;IACZ,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,eAAe,EAAa,GAAM;KAClC,WAAU;eACX;KAEQ,CAAA,EACT,kBAAC,UAAD;KACE,SAAS;KACT,UAAU;KACV,WAAU;eAET,IAAW,gBAAgB;KACrB,CAAA,CACR,EAAA,CAAA;cApBP,CAuBG,KACC,kBAAC,OAAD;KAAK,WAAU;eAAyG;KAElH,CAAA,EAER,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,WAAU;iBAA8C;OAEvD,CAAA,EACR,kBAAC,SAAD;OACE,MAAK;OACL,aAAY;OACZ,OAAO;OACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;OAC/C,WAAU;OACV,CAAA,CACE,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,WAAU;iBAA8C;OAEvD,CAAA,EACR,kBAAC,UAAD;OACE,OAAO;OACP,WAAW,MAAM,EAAa,EAAE,OAAO,MAAM;OAC7C,WAAU;iBAHZ,CAKE,kBAAC,UAAD;QAAQ,OAAM;kBAAG;QAAmB,CAAA,EACnC,EAAS,KAAK,MACb,kBAAC,UAAD;QAAyB,OAAO,EAAQ;kBACrC,EAAQ;QACF,EAFI,EAAQ,GAEZ,CACT,CACK;SACL,EAAA,CAAA;MACN,kBAAC,SAAD;OAAO,WAAU;iBAAjB,CACE,kBAAC,SAAD;QACE,MAAK;QACL,SAAS;QACT,WAAW,MAAM,EAAY,EAAE,OAAO,QAAQ;QAC9C,WAAU;QACV,CAAA,EAAA,WAEI;;MACJ;OACA;;GAGR,kBAAC,GAAD;IACE,MAAM,EAAQ;IACd,eAAe,EAAe,KAAK;IACnC,OAAM;IACN,aAAY;IACZ,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,eAAe,EAAe,KAAK;KACnC,WAAU;eACX;KAEQ,CAAA,EACT,kBAAC,UAAD;KACE,SAAS;KACT,UAAU,KAAY;KACtB,WAAU;eAET,KAAY,IAAW,kBAAkB;KACnC,CAAA,CACR,EAAA,CAAA;cAGL,kBAAC,KAAD;KAAG,WAAU;eAAb;MAA2C;MAC3B;MACd,kBAAC,QAAD;OAAM,WAAU;iBACb,GAAa,UAAU,SAAS,QAC/B,GAAa,UAAU,eACvB;OACG,CAAA;MAAC;MAAI;MAEV;;IACE,CAAA;GACG"}
|
|
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 {\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\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=\"Timer\"\n description=\"Focus timer for deep work sessions\"\n 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.\"\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 ? \"Stop the running timer\" : \"Start the timer\",\n description: isRunning\n ? \"End this session and save it as a time entry.\"\n : \"Begin tracking the task you're working on right now.\",\n onClick: () => (isRunning ? handleStop() : setStartOpen(true)),\n },\n {\n id: \"assign-project\",\n label: \"Assign work to a project\",\n description: \"Pick a project so this time bills correctly.\",\n onClick: () => navigate(\"/projects\"),\n },\n {\n id: \"review-entries\",\n label: \"Review your time entries\",\n description: \"Check and tidy everything you've logged.\",\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 : \"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 ? \"Stopping...\" : \"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 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=\"Discard the running timer without saving\"\n >\n <RotateCcw className=\"h-4 w-4\" />\n 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 Running Timers\n </h3>\n {runningTimers.length > 0 && (\n <StatusBadge\n label={`${runningTimers.length} active`}\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=\"No active timers on the server\"\n description=\"Timers you start sync here so you can pick them up on any device.\"\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(timer.timeEntry.project?.name ?? \"No Project\")}\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 ?? \"No Project\"}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {timer.timeEntry.description ?? \"No description\"}\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=\"Start a timer\"\n description=\"Track the task you're working on right now.\"\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 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 ? \"Starting...\" : \"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 Failed to start the timer. Please try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n What are you working on?\n </label>\n <input\n type=\"text\"\n placeholder=\"Description\"\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 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=\"\">No Project</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 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=\"Discard running timer?\"\n description=\"This stops the timer and deletes the in-progress entry. It cannot be undone.\"\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 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 ? \"Discarding...\" : \"Discard\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n The timer for{\" \"}\n <span className=\"font-medium text-text-primary\">\n {resetTarget?.timeEntry.project?.name ??\n resetTarget?.timeEntry.description ??\n \"this session\"}\n </span>{\" \"}\n will be discarded.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimeApprovalPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/time-approval/TimeApprovalPage.tsx"],"names":[],"mappings":"AAuDA,wBAAgB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"TimeApprovalPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/time-approval/TimeApprovalPage.tsx"],"names":[],"mappings":"AAuDA,wBAAgB,gBAAgB,gCA2iB/B"}
|
|
@@ -30,10 +30,10 @@ function w({ request: e }) {
|
|
|
30
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
31
|
function E() {
|
|
32
32
|
let { toasts: x, addToast: S, dismissToast: E } = se(), { data: D, loading: O, error: pe, refetch: me, fetchMore: he } = s({
|
|
33
|
-
variables: {
|
|
33
|
+
variables: { pagination: {
|
|
34
34
|
limit: C,
|
|
35
35
|
offset: 0
|
|
36
|
-
},
|
|
36
|
+
} },
|
|
37
37
|
notifyOnNetworkStatusChange: !0
|
|
38
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 = [
|
|
39
39
|
{
|
|
@@ -251,10 +251,10 @@ function E() {
|
|
|
251
251
|
z(!0);
|
|
252
252
|
try {
|
|
253
253
|
await he({
|
|
254
|
-
variables: {
|
|
254
|
+
variables: { pagination: {
|
|
255
255
|
limit: C,
|
|
256
256
|
offset: I.length
|
|
257
|
-
},
|
|
257
|
+
} },
|
|
258
258
|
updateQuery: (e, { fetchMoreResult: t }) => t ? {
|
|
259
259
|
...e,
|
|
260
260
|
approvalRequests: {
|