@burdenoff/microfe-timecampus 2026.626.2 → 2026.703.2
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.js +996 -816
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.js +28 -0
- package/dist/generated/wspace-types.js.map +1 -0
- package/dist/index.js +4 -4
- package/dist/timecampus/TimeCampusRoutes.js +16 -12
- package/dist/timecampus/TimeCampusRoutes.js.map +1 -1
- package/dist/timecampus/components/Modal.js +65 -0
- package/dist/timecampus/components/Modal.js.map +1 -0
- package/dist/timecampus/components/StatusBadge.js +28 -0
- package/dist/timecampus/components/StatusBadge.js.map +1 -0
- package/dist/timecampus/components/Toast.js +87 -0
- package/dist/timecampus/components/Toast.js.map +1 -0
- package/dist/timecampus/index.js +1 -1
- package/dist/timecampus/pages/HomePage.js +143 -135
- package/dist/timecampus/pages/HomePage.js.map +1 -1
- package/dist/timecampus/pages/applets/AppletStorePage.js +14 -19
- package/dist/timecampus/pages/applets/AppletStorePage.js.map +1 -1
- package/dist/timecampus/pages/applets/InstalledAppletsPage.js +49 -37
- package/dist/timecampus/pages/applets/InstalledAppletsPage.js.map +1 -1
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +315 -87
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
- package/dist/timecampus/pages/channels/ChannelsPage.js +340 -46
- package/dist/timecampus/pages/channels/ChannelsPage.js.map +1 -1
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js +374 -92
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
- package/dist/timecampus/pages/dashboard/ReportsPage.js +70 -37
- package/dist/timecampus/pages/dashboard/ReportsPage.js.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +366 -60
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +461 -87
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.js +314 -55
- package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
- package/dist/timecampus/pages/industry-solutions/IndustrySolutionsPage.js +30 -27
- package/dist/timecampus/pages/industry-solutions/IndustrySolutionsPage.js.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +269 -74
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/ActionItemFormModal.js +127 -0
- package/dist/timecampus/pages/meetings/ActionItemFormModal.js.map +1 -0
- package/dist/timecampus/pages/meetings/MeetingCreatePage.js +62 -52
- package/dist/timecampus/pages/meetings/MeetingCreatePage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingDetailPage.js +361 -74
- package/dist/timecampus/pages/meetings/MeetingDetailPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingFormModal.js +156 -0
- package/dist/timecampus/pages/meetings/MeetingFormModal.js.map +1 -0
- package/dist/timecampus/pages/meetings/MeetingsPage.js +175 -74
- package/dist/timecampus/pages/meetings/MeetingsPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/ParticipantFormModal.js +105 -0
- package/dist/timecampus/pages/meetings/ParticipantFormModal.js.map +1 -0
- package/dist/timecampus/pages/pomodoro/PomodoroPage.js +307 -67
- package/dist/timecampus/pages/pomodoro/PomodoroPage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectCreatePage.js +51 -48
- package/dist/timecampus/pages/projects/ProjectCreatePage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectDetailPage.js +248 -51
- package/dist/timecampus/pages/projects/ProjectDetailPage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectsPage.js +354 -87
- package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/TasksPage.js +367 -65
- package/dist/timecampus/pages/projects/TasksPage.js.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +674 -104
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamAnalyticsPage.js +46 -34
- package/dist/timecampus/pages/team/TeamAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js +93 -69
- package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamPage.js +4 -3
- package/dist/timecampus/pages/team/TeamPage.js.map +1 -1
- package/dist/timecampus/pages/templates/TemplateDetailPage.js +72 -53
- package/dist/timecampus/pages/templates/TemplateDetailPage.js.map +1 -1
- package/dist/timecampus/pages/templates/TemplatesPage.js +40 -35
- package/dist/timecampus/pages/templates/TemplatesPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.js +484 -97
- package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.js +210 -99
- package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.js +229 -97
- package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +315 -110
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
- package/dist/timecampus/pages/timeline/TimelinePage.js +250 -0
- package/dist/timecampus/pages/timeline/TimelinePage.js.map +1 -0
- package/dist/timecampus/pages/widgets/WidgetDetailPage.js +158 -76
- package/dist/timecampus/pages/widgets/WidgetDetailPage.js.map +1 -1
- package/dist/timecampus/pages/widgets/WidgetsPage.js +181 -52
- package/dist/timecampus/pages/widgets/WidgetsPage.js.map +1 -1
- package/dist/timecampus/pages/workflows/WorkflowsPage.js +212 -61
- package/dist/timecampus/pages/workflows/WorkflowsPage.js.map +1 -1
- package/dist/timecampus/store/timecampusStore.js +13 -1
- package/dist/timecampus/store/timecampusStore.js.map +1 -1
- package/dist/timecampus/utils/helpers.js +20 -10
- package/dist/timecampus/utils/helpers.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 { Plus, Filter } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { formatHours, formatDate, getStatusColor } from \"../../utils/helpers\";\nimport {\n useListTimeEntriesQuery,\n type TimeEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nexport function TimeEntriesPage() {\n const { data, loading, error } = useListTimeEntriesQuery({\n variables: { limit: 50 },\n });\n\n const entries = data?.timeEntries?.items ?? [];\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 className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\">\n <Filter className=\"h-4 w-4\" />\n Filter\n </button>\n <button 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 <Plus className=\"h-4 w-4\" />\n Add Entry\n </button>\n </div>\n }\n >\n {loading ? (\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 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 <Plus className=\"h-4 w-4\" />\n Add Entry\n </button>\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 Date\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 Task\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 </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 text-sm text-text-secondary\">\n {formatDate(entry.startTime)}\n </td>\n <td className=\"px-4 py-3 text-sm font-medium text-text-primary\">\n {entry.project?.name ?? \"No Project\"}\n </td>\n <td className=\"px-4 py-3 text-sm text-text-secondary\">\n {entry.task?.title ??\n entry.description ??\n \"No description\"}\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 <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${entry.billable ? \"bg-status-success-bg text-status-success-text\" : \"bg-bg-sunken text-text-secondary\"}`}\n >\n {entry.billable ? \"Yes\" : \"No\"}\n </span>\n </td>\n <td className=\"px-4 py-3\">\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${getStatusColor(entry.isRunning ? \"running\" : \"completed\")}`}\n >\n {entry.isRunning ? \"running\" : \"completed\"}\n </span>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </GlassCard>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;AAWA,SAAgB,IAAkB;CAChC,IAAM,EAAE,SAAM,YAAS,aAAU,EAAwB,EACvD,WAAW,EAAE,OAAO,IAAI,EACzB,CAAC,EAEI,IAAU,GAAM,aAAa,SAAS,EAAE;AAe9C,QAbI,IAEA,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;YAEZ,kBAAC,OAAD;GAAK,WAAU;aAAoG;GAE7G,CAAA;EACK,CAAA,GAKf,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SAAQ;EACR,SACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,UAAD;IAAQ,WAAU;cAAlB,CACE,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA,EAAA,SAEvB;OACT,kBAAC,UAAD;IAAQ,WAAU;cAAlB,CACE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,YAErB;MACL;;YAGP,IACC,kBAAC,GAAD;GAAiB,SAAQ;GAAQ,OAAO;GAAK,CAAA,GAC3C,EAAQ,WAAW,IACrB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,UAAD;IAAQ,WAAU;cAAlB,CACE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,YAErB;;GAEX,CAAA,GAEF,kBAAC,GAAD;GAAW,WAAU;GAAQ,MAAA;GAAK,WAAU;aAC1C,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,SAAD;KAAO,WAAU;eAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACE,kBAAC,MAAD;QAAI,WAAU;kBAA8D;QAEvE,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAA8D;QAEvE,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAA8D;QAEvE,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAA8D;QAEvE,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAA8D;QAEvE,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBAA8D;QAEvE,CAAA;OACF;SACC,CAAA,EACR,kBAAC,SAAD;MAAO,WAAU;gBACd,EAAQ,KAAK,MACZ,kBAAC,MAAD;OAEE,WAAU;iBAFZ;QAIE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAW,EAAM,UAAU;SACzB,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAM,SAAS,QAAQ;SACrB,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAM,MAAM,SACX,EAAM,eACN;SACC,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACX,GAAa,EAAM,YAAY,KAAK,KAAK;SACvC,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACZ,kBAAC,QAAD;UACE,WAAW,4DAA4D,EAAM,WAAW,kDAAkD;oBAEzI,EAAM,WAAW,QAAQ;UACrB,CAAA;SACJ,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACZ,kBAAC,QAAD;UACE,WAAW,4DAA4D,EAAe,EAAM,YAAY,YAAY,YAAY;oBAE/H,EAAM,YAAY,YAAY;UAC1B,CAAA;SACJ,CAAA;QACF;SA/BE,EAAM,GA+BR,CACL;MACI,CAAA,CACF;;IACJ,CAAA;GACI,CAAA;EAEH,CAAA"}
|
|
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 { formatHours, formatDate, getStatusColor } 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 Date\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 Task\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 text-sm text-text-secondary\">\n {formatDate(entry.startTime)}\n </td>\n <td className=\"px-4 py-3 text-sm font-medium text-text-primary\">\n {entry.project?.name ?? \"No Project\"}\n </td>\n <td className=\"px-4 py-3 text-sm text-text-secondary\">\n {entry.task?.title ??\n entry.description ??\n \"No description\"}\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 <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${entry.billable ? \"bg-status-success-bg text-status-success-text\" : \"bg-bg-sunken text-text-secondary\"}`}\n >\n {entry.billable ? \"Yes\" : \"No\"}\n </span>\n </td>\n <td className=\"px-4 py-3\">\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${getStatusColor(entry.isRunning ? \"running\" : \"completed\")}`}\n >\n {entry.isRunning ? \"running\" : \"completed\"}\n </span>\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,113 +1,224 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
1
|
+
import { useTimeCampusNavigate as e } from "../../providers/TimeCampusProvider.js";
|
|
2
|
+
import { formatHours as t, formatTimerDuration as n } from "../../utils/helpers.js";
|
|
3
|
+
import { StatCard as r } from "../../components/StatCard.js";
|
|
4
|
+
import { LoadingSkeleton as i } from "../../components/LoadingSkeleton.js";
|
|
5
|
+
import { GetTimeEntryStatsDocument as ee, ListRunningTimersDocument as a, ListTimeEntriesDocument as o, useGetTimeEntryStatsQuery as s, useListProjectsQuery as c, useListRunningTimersQuery as l, useListTimeEntriesQuery as u, useStartTimerMutation as d, useStopTimerMutation as f } from "../../../generated/wspace-operations.js";
|
|
6
|
+
import { useTimeCampusStore as p } from "../../store/timecampusStore.js";
|
|
7
|
+
import { PageLayout as m } from "../../components/PageLayout.js";
|
|
8
|
+
import { EmptyState as te } from "../../components/EmptyState.js";
|
|
9
|
+
import { Modal as ne } from "../../components/Modal.js";
|
|
10
|
+
import { useEffect as h, useState as g } from "react";
|
|
11
|
+
import { Clock as _, Pause as re, Play as v, Square as y } from "lucide-react";
|
|
12
|
+
import { Fragment as b, jsx as x, jsxs as S } from "react/jsx-runtime";
|
|
13
|
+
import { useI18n as C } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
11
14
|
//#region src/timecampus/pages/time/TimeTrackingPage.tsx
|
|
12
|
-
function
|
|
13
|
-
let { t:
|
|
14
|
-
let n =
|
|
15
|
+
function w() {
|
|
16
|
+
let { t: w } = C(), T = (e, t) => {
|
|
17
|
+
let n = w(e);
|
|
15
18
|
return n === e ? t : n;
|
|
16
|
-
}, { activeTimer:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
}, { activeTimer: E, tickTimer: D, syncTimerFromServer: O } = p(), k = e(), [A, j] = g(!1), [M, N] = g(""), [P, F] = g(""), [I, L] = g(!1), { data: R, loading: z } = s(), { data: B, loading: V } = u({ variables: { limit: 10 } }), { data: H, loading: U, refetch: W } = l({ fetchPolicy: "cache-and-network" }), { data: G } = c({ variables: { limit: 50 } }), K = G?.projects?.items ?? [], q = H?.runningTimers?.[0] ?? null, J = [
|
|
20
|
+
{ query: a },
|
|
21
|
+
{
|
|
22
|
+
query: o,
|
|
23
|
+
variables: { limit: 10 }
|
|
24
|
+
},
|
|
25
|
+
{ query: ee }
|
|
26
|
+
], [Y, { loading: X, error: ie }] = d({ refetchQueries: J }), [ae, { loading: oe }] = f({ refetchQueries: J });
|
|
27
|
+
h(() => {
|
|
28
|
+
q ? O({
|
|
29
|
+
timeEntryId: q.timeEntry.id,
|
|
30
|
+
elapsedSeconds: q.elapsedSeconds,
|
|
31
|
+
projectId: q.timeEntry.projectId ?? void 0,
|
|
32
|
+
taskId: q.timeEntry.taskId ?? void 0,
|
|
33
|
+
description: q.timeEntry.description ?? void 0
|
|
34
|
+
}) : U || O(null);
|
|
35
|
+
}, [
|
|
36
|
+
q?.timeEntry.id,
|
|
37
|
+
q?.elapsedSeconds,
|
|
38
|
+
U
|
|
39
|
+
]), h(() => {
|
|
40
|
+
if (!E?.isRunning) return;
|
|
41
|
+
let e = setInterval(() => D(), 1e3);
|
|
42
|
+
return () => clearInterval(e);
|
|
43
|
+
}, [E?.isRunning, D]);
|
|
44
|
+
let Z = !!q, se = async () => {
|
|
45
|
+
await Y({ variables: { input: {
|
|
46
|
+
description: M.trim() || void 0,
|
|
47
|
+
projectId: P || void 0,
|
|
48
|
+
billable: I,
|
|
49
|
+
source: "timecampus"
|
|
50
|
+
} } }), j(!1), N(""), F(""), L(!1), await W();
|
|
51
|
+
}, ce = async () => {
|
|
52
|
+
q && (await ae({ variables: { id: q.timeEntry.id } }), await W());
|
|
53
|
+
}, Q = R?.timeEntryStats, $ = B?.timeEntries?.items ?? [];
|
|
54
|
+
return z ? /* @__PURE__ */ x(m, {
|
|
55
|
+
title: T("timecampus.timeTracking.title", "Time Tracking"),
|
|
56
|
+
description: T("timecampus.timeTracking.description", "Track and manage your work hours"),
|
|
57
|
+
children: /* @__PURE__ */ x(i, {
|
|
21
58
|
variant: "card",
|
|
22
59
|
count: 4
|
|
23
60
|
})
|
|
24
|
-
}) : /* @__PURE__ */
|
|
25
|
-
title:
|
|
26
|
-
description:
|
|
27
|
-
purpose:
|
|
28
|
-
actions: /* @__PURE__ */
|
|
29
|
-
onClick: () =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
61
|
+
}) : /* @__PURE__ */ S(m, {
|
|
62
|
+
title: T("timecampus.timeTracking.title", "Time Tracking"),
|
|
63
|
+
description: T("timecampus.timeTracking.description", "Track and manage your work hours"),
|
|
64
|
+
purpose: T("timecampus.timeTracking.purpose", "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."),
|
|
65
|
+
actions: /* @__PURE__ */ x("button", {
|
|
66
|
+
onClick: () => Z ? ce() : j(!0),
|
|
67
|
+
disabled: X || oe,
|
|
68
|
+
className: `inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium disabled:opacity-50 ${Z ? "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"}`,
|
|
69
|
+
children: Z ? /* @__PURE__ */ S(b, { children: [
|
|
70
|
+
/* @__PURE__ */ x(y, { className: "h-4 w-4" }),
|
|
71
|
+
T("timecampus.timeTracking.actions.stop", "Stop"),
|
|
34
72
|
" (",
|
|
35
|
-
|
|
73
|
+
n(E?.seconds ?? 0),
|
|
36
74
|
")"
|
|
37
|
-
] }) : /* @__PURE__ */
|
|
75
|
+
] }) : /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ x(v, { className: "h-4 w-4" }), T("timecampus.timeTracking.actions.startTimer", "Start Timer")] })
|
|
38
76
|
}),
|
|
39
|
-
children: [
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
className: "
|
|
74
|
-
children:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
className: "flex flex-col gap-2",
|
|
84
|
-
children: D.map((t) => /* @__PURE__ */ h("div", {
|
|
85
|
-
className: "flex items-center justify-between rounded-lg border border-border-subtle p-3 transition-colors hover:bg-bg-sunken/50",
|
|
86
|
-
children: [/* @__PURE__ */ h("div", {
|
|
87
|
-
className: "min-w-0 flex-1",
|
|
88
|
-
children: [/* @__PURE__ */ m("p", {
|
|
89
|
-
className: "text-sm font-medium text-text-primary",
|
|
90
|
-
children: t.project?.name ?? v("timecampus.timeTracking.entries.noProject", "No Project")
|
|
91
|
-
}), /* @__PURE__ */ m("p", {
|
|
92
|
-
className: "text-xs text-text-secondary",
|
|
93
|
-
children: t.description ?? v("timecampus.timeTracking.entries.noDescription", "No description")
|
|
94
|
-
})]
|
|
95
|
-
}), /* @__PURE__ */ h("div", {
|
|
96
|
-
className: "ml-4 text-right",
|
|
97
|
-
children: [/* @__PURE__ */ m("p", {
|
|
98
|
-
className: "text-sm font-mono font-medium text-text-primary",
|
|
99
|
-
children: e((t.duration ?? 0) / 3600)
|
|
100
|
-
}), /* @__PURE__ */ m("span", {
|
|
101
|
-
className: "inline-flex items-center rounded-full bg-status-success-bg px-2 py-0.5 text-xs font-medium text-status-success-text",
|
|
102
|
-
children: t.isRunning ? v("timecampus.timeTracking.entries.status.running", "running") : v("timecampus.timeTracking.entries.status.completed", "completed")
|
|
103
|
-
})]
|
|
77
|
+
children: [
|
|
78
|
+
/* @__PURE__ */ S("div", {
|
|
79
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4",
|
|
80
|
+
children: [
|
|
81
|
+
/* @__PURE__ */ x(r, {
|
|
82
|
+
featured: !0,
|
|
83
|
+
title: T("timecampus.timeTracking.stats.today", "Today"),
|
|
84
|
+
value: t(Q?.totalHours ?? 0),
|
|
85
|
+
icon: /* @__PURE__ */ x(_, { className: "h-5 w-5" }),
|
|
86
|
+
trend: {
|
|
87
|
+
value: 15,
|
|
88
|
+
label: T("timecampus.timeTracking.stats.vsYesterday", "vs yesterday")
|
|
89
|
+
}
|
|
90
|
+
}),
|
|
91
|
+
/* @__PURE__ */ x(r, {
|
|
92
|
+
title: T("timecampus.timeTracking.stats.billable", "Billable"),
|
|
93
|
+
value: t(Q?.billableHours ?? 0),
|
|
94
|
+
icon: /* @__PURE__ */ x(_, { className: "h-5 w-5" }),
|
|
95
|
+
description: `${Q?.totalEntries ?? 0} ${T("timecampus.timeTracking.stats.entries", "entries")}`
|
|
96
|
+
}),
|
|
97
|
+
/* @__PURE__ */ x(r, {
|
|
98
|
+
title: T("timecampus.timeTracking.stats.activeTimers", "Active Timers"),
|
|
99
|
+
value: Q?.activeTimers ?? 0,
|
|
100
|
+
icon: /* @__PURE__ */ x(_, { className: "h-5 w-5" }),
|
|
101
|
+
description: T("timecampus.timeTracking.stats.currentlyRunning", "Currently running")
|
|
102
|
+
}),
|
|
103
|
+
/* @__PURE__ */ x(r, {
|
|
104
|
+
title: T("timecampus.timeTracking.stats.activeTimer", "Active Timer"),
|
|
105
|
+
value: E ? n(E.seconds) : T("timecampus.timeTracking.stats.stopped", "Stopped"),
|
|
106
|
+
icon: x(E ? re : v, { className: "h-5 w-5" })
|
|
107
|
+
})
|
|
108
|
+
]
|
|
109
|
+
}),
|
|
110
|
+
/* @__PURE__ */ S("div", {
|
|
111
|
+
className: "rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6",
|
|
112
|
+
children: [/* @__PURE__ */ S("div", {
|
|
113
|
+
className: "mb-4 flex items-center justify-between",
|
|
114
|
+
children: [/* @__PURE__ */ x("h3", {
|
|
115
|
+
className: "text-sm font-semibold text-text-primary",
|
|
116
|
+
children: T("timecampus.timeTracking.entries.title", "Today's Entries")
|
|
117
|
+
}), /* @__PURE__ */ x("button", {
|
|
118
|
+
onClick: () => k("/time/entries"),
|
|
119
|
+
className: "text-sm font-medium text-primary hover:underline",
|
|
120
|
+
children: T("timecampus.timeTracking.entries.viewAll", "View all")
|
|
104
121
|
})]
|
|
105
|
-
},
|
|
106
|
-
|
|
107
|
-
|
|
122
|
+
}), V ? /* @__PURE__ */ x(i, {
|
|
123
|
+
variant: "list",
|
|
124
|
+
count: 4
|
|
125
|
+
}) : $.length === 0 ? /* @__PURE__ */ x(te, {
|
|
126
|
+
illustration: "empty-data",
|
|
127
|
+
title: T("timecampus.timeTracking.entries.empty.title", "No time entries yet"),
|
|
128
|
+
description: T("timecampus.timeTracking.entries.empty", "No time entries yet. Start a timer to begin tracking."),
|
|
129
|
+
action: /* @__PURE__ */ S("button", {
|
|
130
|
+
onClick: () => j(!0),
|
|
131
|
+
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",
|
|
132
|
+
children: [/* @__PURE__ */ x(v, { className: "h-4 w-4" }), T("timecampus.timeTracking.actions.startTimer", "Start Timer")]
|
|
133
|
+
})
|
|
134
|
+
}) : /* @__PURE__ */ x("div", {
|
|
135
|
+
className: "flex flex-col gap-2",
|
|
136
|
+
children: $.map((e) => /* @__PURE__ */ S("div", {
|
|
137
|
+
className: "flex items-center justify-between rounded-lg border border-border-subtle p-3 transition-colors hover:bg-bg-sunken/50",
|
|
138
|
+
children: [/* @__PURE__ */ S("div", {
|
|
139
|
+
className: "min-w-0 flex-1",
|
|
140
|
+
children: [/* @__PURE__ */ x("p", {
|
|
141
|
+
className: "text-sm font-medium text-text-primary",
|
|
142
|
+
children: e.project?.name ?? T("timecampus.timeTracking.entries.noProject", "No Project")
|
|
143
|
+
}), /* @__PURE__ */ x("p", {
|
|
144
|
+
className: "text-xs text-text-secondary",
|
|
145
|
+
children: e.description ?? T("timecampus.timeTracking.entries.noDescription", "No description")
|
|
146
|
+
})]
|
|
147
|
+
}), /* @__PURE__ */ S("div", {
|
|
148
|
+
className: "ml-4 text-right",
|
|
149
|
+
children: [/* @__PURE__ */ x("p", {
|
|
150
|
+
className: "text-sm font-mono font-medium text-text-primary",
|
|
151
|
+
children: t((e.duration ?? 0) / 3600)
|
|
152
|
+
}), /* @__PURE__ */ x("span", {
|
|
153
|
+
className: "inline-flex items-center rounded-full bg-status-success-bg px-2 py-0.5 text-xs font-medium text-status-success-text",
|
|
154
|
+
children: e.isRunning ? T("timecampus.timeTracking.entries.status.running", "running") : T("timecampus.timeTracking.entries.status.completed", "completed")
|
|
155
|
+
})]
|
|
156
|
+
})]
|
|
157
|
+
}, e.id))
|
|
158
|
+
})]
|
|
159
|
+
}),
|
|
160
|
+
/* @__PURE__ */ S(ne, {
|
|
161
|
+
open: A,
|
|
162
|
+
onClose: () => j(!1),
|
|
163
|
+
title: T("timecampus.timeTracking.startModal.title", "Start a timer"),
|
|
164
|
+
description: T("timecampus.timeTracking.startModal.description", "Track the task you're working on right now."),
|
|
165
|
+
footer: /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ x("button", {
|
|
166
|
+
onClick: () => j(!1),
|
|
167
|
+
className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50",
|
|
168
|
+
children: T("timecampus.common.cancel", "Cancel")
|
|
169
|
+
}), /* @__PURE__ */ x("button", {
|
|
170
|
+
onClick: se,
|
|
171
|
+
disabled: X,
|
|
172
|
+
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",
|
|
173
|
+
children: X ? T("timecampus.timeTracking.startModal.starting", "Starting...") : T("timecampus.timeTracking.actions.startTimer", "Start Timer")
|
|
174
|
+
})] }),
|
|
175
|
+
children: [ie && /* @__PURE__ */ x("div", {
|
|
176
|
+
className: "mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text",
|
|
177
|
+
children: T("timecampus.timeTracking.startModal.error", "Failed to start the timer. Please try again.")
|
|
178
|
+
}), /* @__PURE__ */ S("div", {
|
|
179
|
+
className: "space-y-4",
|
|
180
|
+
children: [
|
|
181
|
+
/* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
|
|
182
|
+
className: "block text-sm font-medium text-text-primary",
|
|
183
|
+
children: T("timecampus.timeTracking.startModal.descriptionLabel", "What are you working on?")
|
|
184
|
+
}), /* @__PURE__ */ x("input", {
|
|
185
|
+
type: "text",
|
|
186
|
+
placeholder: T("timecampus.timeTracking.startModal.descriptionPlaceholder", "Description"),
|
|
187
|
+
value: M,
|
|
188
|
+
onChange: (e) => N(e.target.value),
|
|
189
|
+
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"
|
|
190
|
+
})] }),
|
|
191
|
+
/* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
|
|
192
|
+
className: "block text-sm font-medium text-text-primary",
|
|
193
|
+
children: T("timecampus.timeTracking.startModal.projectLabel", "Project")
|
|
194
|
+
}), /* @__PURE__ */ S("select", {
|
|
195
|
+
value: P,
|
|
196
|
+
onChange: (e) => F(e.target.value),
|
|
197
|
+
className: "mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary",
|
|
198
|
+
children: [/* @__PURE__ */ x("option", {
|
|
199
|
+
value: "",
|
|
200
|
+
children: T("timecampus.timeTracking.entries.noProject", "No Project")
|
|
201
|
+
}), K.map((e) => /* @__PURE__ */ x("option", {
|
|
202
|
+
value: e.id,
|
|
203
|
+
children: e.name
|
|
204
|
+
}, e.id))]
|
|
205
|
+
})] }),
|
|
206
|
+
/* @__PURE__ */ S("label", {
|
|
207
|
+
className: "flex items-center gap-2 text-sm text-text-primary",
|
|
208
|
+
children: [/* @__PURE__ */ x("input", {
|
|
209
|
+
type: "checkbox",
|
|
210
|
+
checked: I,
|
|
211
|
+
onChange: (e) => L(e.target.checked),
|
|
212
|
+
className: "h-4 w-4 rounded border-border-subtle"
|
|
213
|
+
}), T("timecampus.timeTracking.startModal.billable", "Billable")]
|
|
214
|
+
})
|
|
215
|
+
]
|
|
216
|
+
})]
|
|
217
|
+
})
|
|
218
|
+
]
|
|
108
219
|
});
|
|
109
220
|
}
|
|
110
221
|
//#endregion
|
|
111
|
-
export {
|
|
222
|
+
export { w as TimeTrackingPage };
|
|
112
223
|
|
|
113
224
|
//# sourceMappingURL=TimeTrackingPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimeTrackingPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimeTrackingPage.tsx"],"sourcesContent":["import { Clock, Play, Pause, Square } from \"lucide-react\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { formatHours, formatTimerDuration } from \"../../utils/helpers\";\nimport { useTimeCampusStore } from \"../../store/timecampusStore\";\nimport {\n useGetTimeEntryStatsQuery,\n useListTimeEntriesQuery,\n type TimeEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\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, startTimer, stopTimer } = useTimeCampusStore();\n\n const { data: statsData, loading: statsLoading } =\n useGetTimeEntryStatsQuery();\n const { data: entriesData, loading: entriesLoading } =\n useListTimeEntriesQuery({\n variables: { limit: 10 },\n });\n\n const stats = statsData?.timeEntryStats;\n const todayEntries = entriesData?.timeEntries?.items ?? [];\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={() => (activeTimer ? stopTimer() : startTimer())}\n className={`inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium ${activeTimer ? \"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 {activeTimer ? (\n <>\n <Square className=\"h-4 w-4\" />\n {t(\"timecampus.timeTracking.actions.stop\", \"Stop\")} (\n {formatTimerDuration(activeTimer.seconds)})\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 <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(stats?.totalHours ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n trend={{\n value: 15,\n label: t(\n \"timecampus.timeTracking.stats.vsYesterday\",\n \"vs yesterday\",\n ),\n }}\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=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <h3 className=\"mb-4 text-sm font-semibold text-text-primary\">\n {t(\"timecampus.timeTracking.entries.title\", \"Today's Entries\")}\n </h3>\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 />\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 justify-between rounded-lg border border-border-subtle p-3 transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {entry.project?.name ??\n t(\n \"timecampus.timeTracking.entries.noProject\",\n \"No Project\",\n )}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {entry.description ??\n t(\n \"timecampus.timeTracking.entries.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <div className=\"ml-4 text-right\">\n <p className=\"text-sm font-mono font-medium text-text-primary\">\n {formatHours((entry.duration ?? 0) / 3600)}\n </p>\n <span className=\"inline-flex items-center rounded-full bg-status-success-bg px-2 py-0.5 text-xs font-medium text-status-success-text\">\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 </span>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\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 { 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 { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport { formatHours, formatTimerDuration } 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 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 <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(stats?.totalHours ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n trend={{\n value: 15,\n label: t(\n \"timecampus.timeTracking.stats.vsYesterday\",\n \"vs yesterday\",\n ),\n }}\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=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {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 justify-between rounded-lg border border-border-subtle p-3 transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {entry.project?.name ??\n t(\n \"timecampus.timeTracking.entries.noProject\",\n \"No Project\",\n )}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {entry.description ??\n t(\n \"timecampus.timeTracking.entries.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <div className=\"ml-4 text-right\">\n <p className=\"text-sm font-mono font-medium text-text-primary\">\n {formatHours((entry.duration ?? 0) / 3600)}\n </p>\n <span className=\"inline-flex items-center rounded-full bg-status-success-bg px-2 py-0.5 text-xs font-medium text-status-success-text\">\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 </span>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\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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"}
|