@burdenoff/microfe-timecampus 2026.719.2 → 2026.719.4
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 +193 -76
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-operations.js +859 -788
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.d.ts +56 -38
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/index.js +5 -5
- 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 +19 -13
- 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/TeamAnalyticsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/team/TeamAnalyticsPage.js +2 -2
- package/dist/timecampus/pages/team/TeamAnalyticsPage.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.d.ts.map +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js +49 -45
- 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/timeline/TimelinePage.d.ts.map +1 -1
- package/dist/timecampus/pages/timeline/TimelinePage.js +224 -162
- package/dist/timecampus/pages/timeline/TimelinePage.js.map +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
- package/package.json +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpenseTrackingPage.js","names":[],"sources":["../../../../src/timecampus/pages/expense-tracking/ExpenseTrackingPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n Receipt,\n Plus,\n DollarSign,\n Pencil,\n Trash2,\n Send,\n Check,\n X,\n Upload,\n ExternalLink,\n MessageSquare,\n} from \"lucide-react\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport { ContextConversationWidget } from \"@burdenoff/fe-libs/shared\";\nimport { useRegisterPageConversationContext } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { Modal } from \"../../components/Modal\";\nimport { ToastContainer, useToast } from \"../../components/Toast\";\nimport {\n ChartCard,\n DonutChart,\n RadialGauge,\n type DonutSlice,\n} from \"../../components\";\nimport { formatCurrency, formatDate } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListExpensesQuery,\n useGetExpenseStatsQuery,\n useCreateExpenseMutation,\n useUpdateExpenseMutation,\n useDeleteExpenseMutation,\n useSubmitExpenseForApprovalMutation,\n useApproveExpenseMutation,\n useRejectExpenseMutation,\n type ExpenseFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { ExpenseStatus } from \"@/generated/wspace-types\";\n\nconst PAGE_SIZE = 25;\n\nconst CATEGORY_OPTIONS = [\n \"Travel\",\n \"Meals\",\n \"Software\",\n \"Hardware\",\n \"Office\",\n \"Training\",\n \"Other\",\n];\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\n/**\n * Resolves a presigned download URL for an uploaded receipt.\n *\n * KNOWN LIMITATION: `getFileDownloadUrl` returns a presigned URL that expires\n * after `expiresIn` seconds (capped at 604800 = 7 days by wspace-files-svc).\n * We persist that URL in the expense's `receiptUrl` scalar, so a stored link\n * stops resolving after 7 days. Re-uploading the receipt (or resolving a fresh\n * URL on read) would be needed for longer-lived access.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampusExpense_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// wspace-files requires a real folder id (File.folderId FK); resolve/create a\n// TimeCampus folder rather than passing an empty string.\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusExpense_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\nconst RECEIPT_URL_EXPIRES_IN = 604800; // 7 days — the max wspace-files allows.\n\ninterface ExpenseFormState {\n amount: string;\n category: string;\n description: string;\n projectId: string;\n date: string;\n receiptUrl: string;\n}\n\n/**\n * Child component that mounts the per-expense review conversation thread.\n * Kept as a separate component so `useRegisterPageConversationContext` runs\n * (registering + tearing down the entity context) exactly while the review\n * modal is mounted. The widget self-scopes the thread to this entity and\n * self-guards its own errors, so the existing scalar notes/reason flow is\n * left completely untouched.\n */\nfunction ExpenseReviewThread({ expense }: { expense: ExpenseFieldsFragment }) {\n useRegisterPageConversationContext({\n entityType: \"TIMECAMPUS_EXPENSE\",\n entityId: expense.id,\n contextTitle: `Expense · ${expense.description ?? expense.id}`,\n });\n return <ContextConversationWidget widgetKey=\"expense-review\" />;\n}\n\nfunction todayIso(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nconst emptyForm: ExpenseFormState = {\n amount: \"\",\n category: CATEGORY_OPTIONS[0],\n description: \"\",\n projectId: \"\",\n date: todayIso(),\n receiptUrl: \"\",\n};\n\nexport function ExpenseTrackingPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const { toasts, addToast, dismissToast } = useToast();\n const apollo = useApolloClient();\n\n const {\n data: expensesData,\n loading: expensesLoading,\n error: expensesError,\n refetch,\n fetchMore,\n } = useListExpensesQuery({\n variables: { workspaceId, limit: PAGE_SIZE, offset: 0 },\n notifyOnNetworkStatusChange: true,\n });\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n refetch: refetchStats,\n } = useGetExpenseStatsQuery({\n variables: { workspaceId },\n });\n\n const [createExpense, { loading: creating }] = useCreateExpenseMutation();\n const [updateExpense, { loading: updating }] = useUpdateExpenseMutation();\n const [deleteExpense, { loading: deleting }] = useDeleteExpenseMutation();\n const [submitExpense] = useSubmitExpenseForApprovalMutation();\n const [approveExpense] = useApproveExpenseMutation();\n const [rejectExpense, { loading: rejecting }] = useRejectExpenseMutation();\n\n const expenses = expensesData?.expenses ?? [];\n const stats = statsData?.expenseStats;\n\n // Chart data derived only from data the page already fetches.\n const displayCurrency = expenses[0]?.currency ?? \"USD\";\n\n const categorySpend: DonutSlice[] = stats?.byCategory\n ? Object.entries(stats.byCategory)\n .map(([name, raw]) => ({\n name,\n value: typeof raw === \"number\" ? raw : Number(raw),\n }))\n .filter((slice) => Number.isFinite(slice.value) && slice.value > 0)\n : [];\n\n const decidedCount = (stats?.approvedCount ?? 0) + (stats?.pendingCount ?? 0);\n\n const [loadingMore, setLoadingMore] = useState(false);\n const [canLoadMore, setCanLoadMore] = useState(true);\n\n // Create / edit modal\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<ExpenseFieldsFragment | null>(null);\n const [form, setForm] = useState<ExpenseFormState>(emptyForm);\n const [uploadingReceipt, setUploadingReceipt] = useState(false);\n\n // Per-expense review conversation thread\n const [reviewTarget, setReviewTarget] =\n useState<ExpenseFieldsFragment | null>(null);\n\n // Delete confirm\n const [deleteTarget, setDeleteTarget] =\n useState<ExpenseFieldsFragment | null>(null);\n\n // Reject reason\n const [rejectTarget, setRejectTarget] =\n useState<ExpenseFieldsFragment | null>(null);\n const [rejectReason, setRejectReason] = useState(\"\");\n\n // Per-row busy state for status transitions\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const openCreate = () => {\n setEditing(null);\n setForm({ ...emptyForm, date: todayIso() });\n setFormOpen(true);\n };\n\n const openEdit = (expense: ExpenseFieldsFragment) => {\n setEditing(expense);\n setForm({\n amount: String(expense.amount),\n category: expense.category,\n description: expense.description ?? \"\",\n projectId: expense.projectId ?? \"\",\n date: expense.date ? expense.date.slice(0, 10) : todayIso(),\n receiptUrl: expense.receiptUrl ?? \"\",\n });\n setFormOpen(true);\n };\n\n const handleReceiptUpload = async (file: File) => {\n setUploadingReceipt(true);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/Receipts\" },\n });\n const folderId = (\n folderData as { ensureFolderByPath?: { id: string } } | null\n )?.ensureFolderByPath?.id;\n if (!folderId) throw new Error(\"Could not resolve upload folder\");\n const uploaded = await uploadBlobToFiles({\n client: apollo,\n blob: file,\n filename: file.name,\n mimeType: file.type || \"application/octet-stream\",\n folderId,\n });\n const { data } = await apollo.query({\n query: GET_FILE_DOWNLOAD_URL,\n variables: { id: uploaded.id, expiresIn: RECEIPT_URL_EXPIRES_IN },\n fetchPolicy: \"no-cache\",\n });\n const url = (data as { getFileDownloadUrl?: string } | null | undefined)\n ?.getFileDownloadUrl;\n if (!url) throw new Error(\"No download URL returned\");\n setForm((f) => ({ ...f, receiptUrl: url }));\n addToast({ title: \"Receipt attached\", variant: \"success\" });\n } catch {\n // Upload is best-effort: never block the modal, just surface the failure.\n addToast({ title: \"Failed to upload receipt\", variant: \"error\" });\n } finally {\n setUploadingReceipt(false);\n }\n };\n\n const closeForm = () => {\n setFormOpen(false);\n setEditing(null);\n };\n\n const handleSubmit = async () => {\n const amount = Number(form.amount);\n if (!form.amount || Number.isNaN(amount) || !form.category.trim()) return;\n const isoDate = new Date(form.date).toISOString();\n try {\n if (editing) {\n await updateExpense({\n variables: {\n id: editing.id,\n workspaceId,\n input: {\n amount,\n category: form.category.trim(),\n description: form.description.trim() || undefined,\n projectId: form.projectId.trim() || undefined,\n date: isoDate,\n receiptUrl: form.receiptUrl || undefined,\n },\n },\n });\n addToast({ title: \"Expense updated\", variant: \"success\" });\n } else {\n await createExpense({\n variables: {\n input: {\n workspaceId,\n amount,\n category: form.category.trim(),\n description: form.description.trim() || undefined,\n projectId: form.projectId.trim() || undefined,\n date: isoDate,\n receiptUrl: form.receiptUrl || undefined,\n },\n },\n });\n addToast({ title: \"Expense added\", variant: \"success\" });\n }\n closeForm();\n await refreshAll();\n } catch {\n addToast({\n title: editing ? \"Failed to update expense\" : \"Failed to add expense\",\n variant: \"error\",\n });\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteExpense({\n variables: { id: deleteTarget.id, workspaceId },\n });\n addToast({ title: \"Expense deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to delete expense\", variant: \"error\" });\n }\n };\n\n const handleSubmitForApproval = async (expense: ExpenseFieldsFragment) => {\n setBusyId(expense.id);\n try {\n await submitExpense({ variables: { id: expense.id, workspaceId } });\n addToast({ title: \"Submitted for approval\", variant: \"success\" });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to submit expense\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleApprove = async (expense: ExpenseFieldsFragment) => {\n setBusyId(expense.id);\n try {\n await approveExpense({ variables: { id: expense.id, workspaceId } });\n addToast({ title: \"Expense approved\", variant: \"success\" });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to approve expense\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleReject = async () => {\n if (!rejectTarget) return;\n try {\n await rejectExpense({\n variables: {\n id: rejectTarget.id,\n workspaceId,\n reason: rejectReason.trim() || undefined,\n },\n });\n addToast({ title: \"Expense rejected\", variant: \"success\" });\n setRejectTarget(null);\n setRejectReason(\"\");\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to reject expense\", variant: \"error\" });\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n const res = await fetchMore({\n variables: { workspaceId, limit: PAGE_SIZE, offset: expenses.length },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n expenses: [...prev.expenses, ...fetchMoreResult.expenses],\n };\n },\n });\n if ((res.data?.expenses?.length ?? 0) < PAGE_SIZE) {\n setCanLoadMore(false);\n }\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (expensesLoading && statsLoading && expenses.length === 0) {\n return (\n <PageLayout\n title=\"Expense Tracking\"\n description=\"Track and manage work expenses\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (expensesError && statsError) {\n return (\n <PageLayout\n title=\"Expense Tracking\"\n description=\"Track and manage work expenses\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load expense data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const addButton = (\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 Add Expense\n </button>\n );\n\n return (\n <PageLayout\n title=\"Expense Tracking\"\n description=\"Track and manage work expenses\"\n purpose=\"Capture project-related costs alongside the hours you log — receipts, categories and approvals in one place — so the bill you send a client reflects the full cost of the work, not just time.\"\n actions={addButton}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total This Month\"\n value={formatCurrency(stats?.totalAmount ?? 0)}\n icon={<DollarSign className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Pending\"\n value={String(stats?.pendingCount ?? 0)}\n description=\"expenses awaiting approval\"\n />\n <StatCard\n title=\"Approved\"\n value={String(stats?.approvedCount ?? 0)}\n description=\"expenses approved\"\n />\n <StatCard\n title=\"Average\"\n value={formatCurrency(stats?.averageAmount ?? 0)}\n icon={<Receipt className=\"h-5 w-5\" />}\n description=\"per expense\"\n />\n </div>\n\n {expenses.length === 0 && !expensesLoading ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No expenses recorded yet\"\n description=\"Add your first expense to track project costs and route them for approval.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <ChartCard\n title=\"Spend by category\"\n description=\"Total tracked spend grouped by category\"\n className=\"lg:col-span-2\"\n >\n <DonutChart\n data={categorySpend}\n centerLabel=\"Total\"\n centerValue={stats?.totalAmount ?? 0}\n valueFormat={(value) => formatCurrency(value, displayCurrency)}\n height={240}\n emptyLabel=\"No categorised spend yet.\"\n ariaLabel=\"Spend by category\"\n />\n </ChartCard>\n <ChartCard\n title=\"Approval progress\"\n description=\"Approved vs. awaiting review\"\n >\n <RadialGauge\n value={stats?.approvedCount ?? 0}\n min={0}\n max={decidedCount > 0 ? decidedCount : 1}\n label=\"Approved\"\n sublabel={`of ${decidedCount} to review`}\n format={(value) => `${Math.round(value)}`}\n height={240}\n ariaLabel=\"Approved expenses share\"\n />\n </ChartCard>\n </div>\n\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop\">\n <div className=\"divide-y divide-border\">\n {expenses.map((expense: ExpenseFieldsFragment) => {\n const rowBusy = busyId === expense.id;\n const isPending = expense.status === ExpenseStatus.Pending;\n const isRejected = expense.status === ExpenseStatus.Rejected;\n return (\n <div\n key={expense.id}\n className=\"flex flex-col gap-3 p-4 hover:bg-bg-sunken/50 transition-colors sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"flex items-center gap-3\">\n <Receipt className=\"h-5 w-5 shrink-0 text-text-secondary\" />\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n {expense.description ?? \"Untitled expense\"}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {expense.category} ·{\" \"}\n {expense.projectId ?? \"No project\"} ·{\" \"}\n {formatDate(expense.date)}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {formatCurrency(expense.amount, expense.currency)}\n </p>\n <StatusBadge\n label={expense.status.toLowerCase()}\n status={expense.status.toLowerCase()}\n />\n <div className=\"flex items-center gap-1\">\n {isPending && (\n <>\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => handleApprove(expense)}\n title=\"Approve expense\"\n className=\"rounded-md p-1.5 text-status-success-text hover:bg-status-success-bg disabled:opacity-50\"\n >\n <Check className=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => {\n setRejectTarget(expense);\n setRejectReason(\"\");\n }}\n title=\"Reject expense\"\n className=\"rounded-md p-1.5 text-status-error-text hover:bg-status-error-bg disabled:opacity-50\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </>\n )}\n {isRejected && (\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => handleSubmitForApproval(expense)}\n title=\"Resubmit for approval\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Send className=\"h-4 w-4\" />\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => setReviewTarget(expense)}\n title=\"Review expense\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken\"\n >\n <MessageSquare className=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => openEdit(expense)}\n title=\"Edit expense\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(expense)}\n title=\"Delete expense\"\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 </div>\n </div>\n );\n })}\n </div>\n </div>\n\n {canLoadMore && expenses.length >= PAGE_SIZE && (\n <div className=\"mt-4 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n <Modal\n open={formOpen}\n onClose={closeForm}\n title={editing ? \"Edit Expense\" : \"Add Expense\"}\n description={\n editing\n ? \"Update this expense's details.\"\n : \"Record a project-related cost to track alongside your logged hours.\"\n }\n footer={\n <>\n <button\n onClick={closeForm}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={\n creating ||\n updating ||\n !form.amount ||\n Number.isNaN(Number(form.amount)) ||\n !form.category.trim()\n }\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 || updating\n ? \"Saving...\"\n : editing\n ? \"Save Changes\"\n : \"Add Expense\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\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 Amount\n </label>\n <input\n type=\"number\"\n min={0}\n step=\"0.01\"\n placeholder=\"0.00\"\n value={form.amount}\n onChange={(e) => setForm({ ...form, amount: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Category\n </label>\n <select\n value={form.category}\n onChange={(e) => setForm({ ...form, category: e.target.value })}\n className={inputClass}\n >\n {CATEGORY_OPTIONS.map((c) => (\n <option key={c} value={c}>\n {c}\n </option>\n ))}\n </select>\n </div>\n </div>\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 was this expense for?\"\n value={form.description}\n onChange={(e) =>\n setForm({ ...form, description: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Project ID\n </label>\n <input\n type=\"text\"\n placeholder=\"Optional project ID\"\n value={form.projectId}\n onChange={(e) =>\n setForm({ ...form, projectId: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Date\n </label>\n <input\n type=\"date\"\n value={form.date}\n onChange={(e) => setForm({ ...form, date: e.target.value })}\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Receipt\n </label>\n <div className=\"mt-1 flex flex-col gap-2\">\n <label\n className={`inline-flex w-fit cursor-pointer 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 uploadingReceipt ? \"opacity-50\" : \"\"\n }`}\n >\n <Upload className=\"h-4 w-4\" />\n {uploadingReceipt ? \"Uploading...\" : \"Upload receipt\"}\n <input\n type=\"file\"\n accept=\"image/*,application/pdf\"\n disabled={uploadingReceipt}\n className=\"hidden\"\n onChange={(e) => {\n const file = e.target.files?.[0];\n // Reset the input so re-selecting the same file re-triggers change.\n e.target.value = \"\";\n if (file) void handleReceiptUpload(file);\n }}\n />\n </label>\n {form.receiptUrl && (\n <a\n href={form.receiptUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex w-fit items-center gap-1.5 text-sm font-medium text-action-primary-text/90 underline decoration-border-subtle underline-offset-2 hover:text-action-primary-text\"\n >\n <ExternalLink className=\"h-3.5 w-3.5\" />\n View current receipt\n </a>\n )}\n <p className=\"text-xs text-text-secondary\">\n Attach an image or PDF. Uploading is optional and never blocks\n saving the expense.\n </p>\n </div>\n </div>\n </div>\n </Modal>\n\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete expense?\"\n description={\n deleteTarget\n ? `${formatCurrency(deleteTarget.amount, deleteTarget.currency)} · ${deleteTarget.category} will be removed.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(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-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n This permanently removes the expense. This action cannot be undone.\n </p>\n </Modal>\n\n <Modal\n open={rejectTarget !== null}\n onClose={() => setRejectTarget(null)}\n title=\"Reject expense\"\n description=\"Let the submitter know why this expense was rejected.\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRejectTarget(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={handleReject}\n disabled={rejecting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {rejecting ? \"Rejecting...\" : \"Reject Expense\"}\n </button>\n </>\n }\n >\n <label className=\"block text-sm font-medium text-text-primary\">\n Reason\n </label>\n <textarea\n rows={3}\n placeholder=\"Reason for rejection (optional)\"\n value={rejectReason}\n onChange={(e) => setRejectReason(e.target.value)}\n className={inputClass}\n />\n </Modal>\n\n <Modal\n open={reviewTarget !== null}\n onClose={() => setReviewTarget(null)}\n title=\"Expense review\"\n description={\n reviewTarget\n ? `Discuss ${reviewTarget.description ?? \"this expense\"} with your team.`\n : undefined\n }\n >\n {reviewTarget && <ExpenseReviewThread expense={reviewTarget} />}\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ExpenseTrackingPage.js","names":[],"sources":["../../../../src/timecampus/pages/expense-tracking/ExpenseTrackingPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n Receipt,\n Plus,\n DollarSign,\n Pencil,\n Trash2,\n Send,\n Check,\n X,\n Upload,\n ExternalLink,\n MessageSquare,\n} from \"lucide-react\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport { ContextConversationWidget } from \"@burdenoff/fe-libs/shared\";\nimport { useRegisterPageConversationContext } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { Modal } from \"../../components/Modal\";\nimport { ToastContainer, useToast } from \"../../components/Toast\";\nimport {\n ChartCard,\n DonutChart,\n RadialGauge,\n type DonutSlice,\n} from \"../../components\";\nimport { formatCurrency, formatDate } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListExpensesQuery,\n useGetExpenseStatsQuery,\n useCreateExpenseMutation,\n useUpdateExpenseMutation,\n useDeleteExpenseMutation,\n useSubmitExpenseForApprovalMutation,\n useApproveExpenseMutation,\n useRejectExpenseMutation,\n type ExpenseFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { ExpenseStatus } from \"@/generated/wspace-types\";\n\nconst PAGE_SIZE = 25;\n\nconst CATEGORY_OPTIONS = [\n \"Travel\",\n \"Meals\",\n \"Software\",\n \"Hardware\",\n \"Office\",\n \"Training\",\n \"Other\",\n];\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\n/**\n * Resolves a short-lived preview URL for a just-uploaded receipt (shown only\n * while the create/edit modal is open). The persisted value is the stable\n * `receiptFileId`, NOT this URL — the backend's `receiptDownloadUrl` field\n * resolver mints a fresh presigned URL on every read instead (BOFF-4038:\n * a URL persisted directly used to go dead after wspace-files' 7-day cap).\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampusExpense_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// wspace-files requires a real folder id (File.folderId FK); resolve/create a\n// TimeCampus folder rather than passing an empty string.\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusExpense_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\nconst RECEIPT_PREVIEW_URL_EXPIRES_IN = 3600; // Just for the modal preview link.\n\ninterface ExpenseFormState {\n amount: string;\n category: string;\n description: string;\n projectId: string;\n date: string;\n receiptFileId: string;\n receiptPreviewUrl: string;\n}\n\n/**\n * Child component that mounts the per-expense review conversation thread.\n * Kept as a separate component so `useRegisterPageConversationContext` runs\n * (registering + tearing down the entity context) exactly while the review\n * modal is mounted. The widget self-scopes the thread to this entity and\n * self-guards its own errors, so the existing scalar notes/reason flow is\n * left completely untouched.\n */\nfunction ExpenseReviewThread({ expense }: { expense: ExpenseFieldsFragment }) {\n useRegisterPageConversationContext({\n entityType: \"TIMECAMPUS_EXPENSE\",\n entityId: expense.id,\n contextTitle: `Expense · ${expense.description ?? expense.id}`,\n });\n return <ContextConversationWidget widgetKey=\"expense-review\" />;\n}\n\nfunction todayIso(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nconst emptyForm: ExpenseFormState = {\n amount: \"\",\n category: CATEGORY_OPTIONS[0],\n description: \"\",\n projectId: \"\",\n date: todayIso(),\n receiptFileId: \"\",\n receiptPreviewUrl: \"\",\n};\n\nexport function ExpenseTrackingPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const { toasts, addToast, dismissToast } = useToast();\n const apollo = useApolloClient();\n\n const {\n data: expensesData,\n loading: expensesLoading,\n error: expensesError,\n refetch,\n fetchMore,\n } = useListExpensesQuery({\n variables: { workspaceId, pagination: { limit: PAGE_SIZE, offset: 0 } },\n notifyOnNetworkStatusChange: true,\n });\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n refetch: refetchStats,\n } = useGetExpenseStatsQuery({\n variables: { workspaceId },\n });\n\n const [createExpense, { loading: creating }] = useCreateExpenseMutation();\n const [updateExpense, { loading: updating }] = useUpdateExpenseMutation();\n const [deleteExpense, { loading: deleting }] = useDeleteExpenseMutation();\n const [submitExpense] = useSubmitExpenseForApprovalMutation();\n const [approveExpense] = useApproveExpenseMutation();\n const [rejectExpense, { loading: rejecting }] = useRejectExpenseMutation();\n\n const expenses = expensesData?.expenses ?? [];\n const stats = statsData?.expenseStats;\n\n // Chart data derived only from data the page already fetches.\n const displayCurrency = expenses[0]?.currency ?? \"USD\";\n\n const categorySpend: DonutSlice[] = stats?.byCategory\n ? Object.entries(stats.byCategory)\n .map(([name, raw]) => ({\n name,\n value: typeof raw === \"number\" ? raw : Number(raw),\n }))\n .filter((slice) => Number.isFinite(slice.value) && slice.value > 0)\n : [];\n\n const decidedCount = (stats?.approvedCount ?? 0) + (stats?.pendingCount ?? 0);\n\n const [loadingMore, setLoadingMore] = useState(false);\n const [canLoadMore, setCanLoadMore] = useState(true);\n\n // Create / edit modal\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<ExpenseFieldsFragment | null>(null);\n const [form, setForm] = useState<ExpenseFormState>(emptyForm);\n const [uploadingReceipt, setUploadingReceipt] = useState(false);\n\n // Per-expense review conversation thread\n const [reviewTarget, setReviewTarget] =\n useState<ExpenseFieldsFragment | null>(null);\n\n // Delete confirm\n const [deleteTarget, setDeleteTarget] =\n useState<ExpenseFieldsFragment | null>(null);\n\n // Reject reason\n const [rejectTarget, setRejectTarget] =\n useState<ExpenseFieldsFragment | null>(null);\n const [rejectReason, setRejectReason] = useState(\"\");\n\n // Per-row busy state for status transitions\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const openCreate = () => {\n setEditing(null);\n setForm({ ...emptyForm, date: todayIso() });\n setFormOpen(true);\n };\n\n const openEdit = (expense: ExpenseFieldsFragment) => {\n setEditing(expense);\n setForm({\n amount: String(expense.amount),\n category: expense.category,\n description: expense.description ?? \"\",\n projectId: expense.projectId ?? \"\",\n date: expense.date ? expense.date.slice(0, 10) : todayIso(),\n receiptFileId: expense.receiptFileId ?? \"\",\n // Resolved fresh from the backend on every read — never a stale stored link.\n receiptPreviewUrl: expense.receiptDownloadUrl ?? \"\",\n });\n setFormOpen(true);\n };\n\n const handleReceiptUpload = async (file: File) => {\n setUploadingReceipt(true);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/Receipts\" },\n });\n const folderId = (\n folderData as { ensureFolderByPath?: { id: string } } | null\n )?.ensureFolderByPath?.id;\n if (!folderId) throw new Error(\"Could not resolve upload folder\");\n const uploaded = await uploadBlobToFiles({\n client: apollo,\n blob: file,\n filename: file.name,\n mimeType: file.type || \"application/octet-stream\",\n folderId,\n });\n // Persist the stable File id (BOFF-4038) — never a presigned URL, which\n // would go dead after wspace-files' 7-day cap. Only fetch a URL here for\n // the modal's own \"view current receipt\" preview link.\n const { data } = await apollo.query({\n query: GET_FILE_DOWNLOAD_URL,\n variables: {\n id: uploaded.id,\n expiresIn: RECEIPT_PREVIEW_URL_EXPIRES_IN,\n },\n fetchPolicy: \"no-cache\",\n });\n const url = (data as { getFileDownloadUrl?: string } | null | undefined)\n ?.getFileDownloadUrl;\n setForm((f) => ({\n ...f,\n receiptFileId: uploaded.id,\n receiptPreviewUrl: url ?? \"\",\n }));\n addToast({ title: \"Receipt attached\", variant: \"success\" });\n } catch {\n // Upload is best-effort: never block the modal, just surface the failure.\n addToast({ title: \"Failed to upload receipt\", variant: \"error\" });\n } finally {\n setUploadingReceipt(false);\n }\n };\n\n const closeForm = () => {\n setFormOpen(false);\n setEditing(null);\n };\n\n const handleSubmit = async () => {\n const amount = Number(form.amount);\n if (!form.amount || Number.isNaN(amount) || !form.category.trim()) return;\n const isoDate = new Date(form.date).toISOString();\n try {\n if (editing) {\n await updateExpense({\n variables: {\n id: editing.id,\n workspaceId,\n input: {\n amount,\n category: form.category.trim(),\n description: form.description.trim() || undefined,\n projectId: form.projectId.trim() || undefined,\n date: isoDate,\n receiptFileId: form.receiptFileId || undefined,\n },\n },\n });\n addToast({ title: \"Expense updated\", variant: \"success\" });\n } else {\n await createExpense({\n variables: {\n input: {\n workspaceId,\n amount,\n category: form.category.trim(),\n description: form.description.trim() || undefined,\n projectId: form.projectId.trim() || undefined,\n date: isoDate,\n receiptFileId: form.receiptFileId || undefined,\n },\n },\n });\n addToast({ title: \"Expense added\", variant: \"success\" });\n }\n closeForm();\n await refreshAll();\n } catch {\n addToast({\n title: editing ? \"Failed to update expense\" : \"Failed to add expense\",\n variant: \"error\",\n });\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteExpense({\n variables: { id: deleteTarget.id, workspaceId },\n });\n addToast({ title: \"Expense deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to delete expense\", variant: \"error\" });\n }\n };\n\n const handleSubmitForApproval = async (expense: ExpenseFieldsFragment) => {\n setBusyId(expense.id);\n try {\n await submitExpense({ variables: { id: expense.id, workspaceId } });\n addToast({ title: \"Submitted for approval\", variant: \"success\" });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to submit expense\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleApprove = async (expense: ExpenseFieldsFragment) => {\n setBusyId(expense.id);\n try {\n await approveExpense({ variables: { id: expense.id, workspaceId } });\n addToast({ title: \"Expense approved\", variant: \"success\" });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to approve expense\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleReject = async () => {\n if (!rejectTarget) return;\n try {\n await rejectExpense({\n variables: {\n id: rejectTarget.id,\n workspaceId,\n reason: rejectReason.trim() || undefined,\n },\n });\n addToast({ title: \"Expense rejected\", variant: \"success\" });\n setRejectTarget(null);\n setRejectReason(\"\");\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to reject expense\", variant: \"error\" });\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n const res = await fetchMore({\n variables: {\n workspaceId,\n pagination: { limit: PAGE_SIZE, offset: expenses.length },\n },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n expenses: [...prev.expenses, ...fetchMoreResult.expenses],\n };\n },\n });\n if ((res.data?.expenses?.length ?? 0) < PAGE_SIZE) {\n setCanLoadMore(false);\n }\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (expensesLoading && statsLoading && expenses.length === 0) {\n return (\n <PageLayout\n title=\"Expense Tracking\"\n description=\"Track and manage work expenses\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (expensesError && statsError) {\n return (\n <PageLayout\n title=\"Expense Tracking\"\n description=\"Track and manage work expenses\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load expense data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const addButton = (\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 Add Expense\n </button>\n );\n\n return (\n <PageLayout\n title=\"Expense Tracking\"\n description=\"Track and manage work expenses\"\n purpose=\"Capture project-related costs alongside the hours you log — receipts, categories and approvals in one place — so the bill you send a client reflects the full cost of the work, not just time.\"\n actions={addButton}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total This Month\"\n value={formatCurrency(stats?.totalAmount ?? 0)}\n icon={<DollarSign className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Pending\"\n value={String(stats?.pendingCount ?? 0)}\n description=\"expenses awaiting approval\"\n />\n <StatCard\n title=\"Approved\"\n value={String(stats?.approvedCount ?? 0)}\n description=\"expenses approved\"\n />\n <StatCard\n title=\"Average\"\n value={formatCurrency(stats?.averageAmount ?? 0)}\n icon={<Receipt className=\"h-5 w-5\" />}\n description=\"per expense\"\n />\n </div>\n\n {expenses.length === 0 && !expensesLoading ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No expenses recorded yet\"\n description=\"Add your first expense to track project costs and route them for approval.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <ChartCard\n title=\"Spend by category\"\n description=\"Total tracked spend grouped by category\"\n className=\"lg:col-span-2\"\n >\n <DonutChart\n data={categorySpend}\n centerLabel=\"Total\"\n centerValue={stats?.totalAmount ?? 0}\n valueFormat={(value) => formatCurrency(value, displayCurrency)}\n height={240}\n emptyLabel=\"No categorised spend yet.\"\n ariaLabel=\"Spend by category\"\n />\n </ChartCard>\n <ChartCard\n title=\"Approval progress\"\n description=\"Approved vs. awaiting review\"\n >\n <RadialGauge\n value={stats?.approvedCount ?? 0}\n min={0}\n max={decidedCount > 0 ? decidedCount : 1}\n label=\"Approved\"\n sublabel={`of ${decidedCount} to review`}\n format={(value) => `${Math.round(value)}`}\n height={240}\n ariaLabel=\"Approved expenses share\"\n />\n </ChartCard>\n </div>\n\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop\">\n <div className=\"divide-y divide-border\">\n {expenses.map((expense: ExpenseFieldsFragment) => {\n const rowBusy = busyId === expense.id;\n const isPending = expense.status === ExpenseStatus.Pending;\n const isRejected = expense.status === ExpenseStatus.Rejected;\n return (\n <div\n key={expense.id}\n className=\"flex flex-col gap-3 p-4 hover:bg-bg-sunken/50 transition-colors sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"flex items-center gap-3\">\n <Receipt className=\"h-5 w-5 shrink-0 text-text-secondary\" />\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n {expense.description ?? \"Untitled expense\"}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {expense.category} ·{\" \"}\n {expense.projectId ?? \"No project\"} ·{\" \"}\n {formatDate(expense.date)}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {formatCurrency(expense.amount, expense.currency)}\n </p>\n <StatusBadge\n label={expense.status.toLowerCase()}\n status={expense.status.toLowerCase()}\n />\n <div className=\"flex items-center gap-1\">\n {isPending && (\n <>\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => handleApprove(expense)}\n title=\"Approve expense\"\n className=\"rounded-md p-1.5 text-status-success-text hover:bg-status-success-bg disabled:opacity-50\"\n >\n <Check className=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => {\n setRejectTarget(expense);\n setRejectReason(\"\");\n }}\n title=\"Reject expense\"\n className=\"rounded-md p-1.5 text-status-error-text hover:bg-status-error-bg disabled:opacity-50\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </>\n )}\n {isRejected && (\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => handleSubmitForApproval(expense)}\n title=\"Resubmit for approval\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Send className=\"h-4 w-4\" />\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => setReviewTarget(expense)}\n title=\"Review expense\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken\"\n >\n <MessageSquare className=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => openEdit(expense)}\n title=\"Edit expense\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(expense)}\n title=\"Delete expense\"\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 </div>\n </div>\n );\n })}\n </div>\n </div>\n\n {canLoadMore && expenses.length >= PAGE_SIZE && (\n <div className=\"mt-4 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n <Modal\n open={formOpen}\n onClose={closeForm}\n title={editing ? \"Edit Expense\" : \"Add Expense\"}\n description={\n editing\n ? \"Update this expense's details.\"\n : \"Record a project-related cost to track alongside your logged hours.\"\n }\n footer={\n <>\n <button\n onClick={closeForm}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={\n creating ||\n updating ||\n !form.amount ||\n Number.isNaN(Number(form.amount)) ||\n !form.category.trim()\n }\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 || updating\n ? \"Saving...\"\n : editing\n ? \"Save Changes\"\n : \"Add Expense\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\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 Amount\n </label>\n <input\n type=\"number\"\n min={0}\n step=\"0.01\"\n placeholder=\"0.00\"\n value={form.amount}\n onChange={(e) => setForm({ ...form, amount: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Category\n </label>\n <select\n value={form.category}\n onChange={(e) => setForm({ ...form, category: e.target.value })}\n className={inputClass}\n >\n {CATEGORY_OPTIONS.map((c) => (\n <option key={c} value={c}>\n {c}\n </option>\n ))}\n </select>\n </div>\n </div>\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 was this expense for?\"\n value={form.description}\n onChange={(e) =>\n setForm({ ...form, description: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Project ID\n </label>\n <input\n type=\"text\"\n placeholder=\"Optional project ID\"\n value={form.projectId}\n onChange={(e) =>\n setForm({ ...form, projectId: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Date\n </label>\n <input\n type=\"date\"\n value={form.date}\n onChange={(e) => setForm({ ...form, date: e.target.value })}\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Receipt\n </label>\n <div className=\"mt-1 flex flex-col gap-2\">\n <label\n className={`inline-flex w-fit cursor-pointer 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 uploadingReceipt ? \"opacity-50\" : \"\"\n }`}\n >\n <Upload className=\"h-4 w-4\" />\n {uploadingReceipt ? \"Uploading...\" : \"Upload receipt\"}\n <input\n type=\"file\"\n accept=\"image/*,application/pdf\"\n disabled={uploadingReceipt}\n className=\"hidden\"\n onChange={(e) => {\n const file = e.target.files?.[0];\n // Reset the input so re-selecting the same file re-triggers change.\n e.target.value = \"\";\n if (file) void handleReceiptUpload(file);\n }}\n />\n </label>\n {form.receiptFileId && form.receiptPreviewUrl && (\n <a\n href={form.receiptPreviewUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex w-fit items-center gap-1.5 text-sm font-medium text-action-primary-text/90 underline decoration-border-subtle underline-offset-2 hover:text-action-primary-text\"\n >\n <ExternalLink className=\"h-3.5 w-3.5\" />\n View current receipt\n </a>\n )}\n <p className=\"text-xs text-text-secondary\">\n Attach an image or PDF. Uploading is optional and never blocks\n saving the expense.\n </p>\n </div>\n </div>\n </div>\n </Modal>\n\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete expense?\"\n description={\n deleteTarget\n ? `${formatCurrency(deleteTarget.amount, deleteTarget.currency)} · ${deleteTarget.category} will be removed.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(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-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n This permanently removes the expense. This action cannot be undone.\n </p>\n </Modal>\n\n <Modal\n open={rejectTarget !== null}\n onClose={() => setRejectTarget(null)}\n title=\"Reject expense\"\n description=\"Let the submitter know why this expense was rejected.\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRejectTarget(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={handleReject}\n disabled={rejecting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {rejecting ? \"Rejecting...\" : \"Reject Expense\"}\n </button>\n </>\n }\n >\n <label className=\"block text-sm font-medium text-text-primary\">\n Reason\n </label>\n <textarea\n rows={3}\n placeholder=\"Reason for rejection (optional)\"\n value={rejectReason}\n onChange={(e) => setRejectReason(e.target.value)}\n className={inputClass}\n />\n </Modal>\n\n <Modal\n open={reviewTarget !== null}\n onClose={() => setReviewTarget(null)}\n title=\"Expense review\"\n description={\n reviewTarget\n ? `Discuss ${reviewTarget.description ?? \"this expense\"} with your team.`\n : undefined\n }\n >\n {reviewTarget && <ExpenseReviewThread expense={reviewTarget} />}\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA+CA,IAAM,IAAY,IAEZ,IAAmB;CACvB;CACA;CACA;CACA;CACA;CACA;CACA;CACD,EAEK,IACJ,wIASI,KAAwB,CAAG;;;;GAQ3B,KAAgB,CAAG;;;;;;GAQnB,KAAiC;AAoBvC,SAAS,GAAoB,EAAE,cAA+C;AAM5E,QALA,EAAmC;EACjC,YAAY;EACZ,UAAU,EAAQ;EAClB,cAAc,aAAa,EAAQ,eAAe,EAAQ;EAC3D,CAAC,EACK,kBAAC,GAAD,EAA2B,WAAU,kBAAmB,CAAA;;AAGjE,SAAS,IAAmB;AAC1B,yBAAO,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,GAAG,GAAG;;AAG9C,IAAM,IAA8B;CAClC,QAAQ;CACR,UAAU,EAAiB;CAC3B,aAAa;CACb,WAAW;CACX,MAAM,GAAU;CAChB,eAAe;CACf,mBAAmB;CACpB;AAED,SAAgB,IAAsB;CACpC,IAAM,EAAE,iBAAc,OAAO,GAAe,EACtC,EAAE,WAAQ,aAAU,oBAAiB,IAAU,EAC/C,IAAS,IAAiB,EAE1B,EACJ,MAAM,IACN,SAAS,GACT,OAAO,IACP,aACA,kBACE,GAAqB;EACvB,WAAW;GAAE;GAAa,YAAY;IAAE,OAAO;IAAW,QAAQ;IAAG;GAAE;EACvE,6BAA6B;EAC9B,CAAC,EACI,EACJ,MAAM,IACN,SAAS,IACT,OAAO,IACP,SAAS,OACP,GAAwB,EAC1B,WAAW,EAAE,gBAAa,EAC3B,CAAC,EAEI,CAAC,IAAe,EAAE,SAAS,OAAc,IAA0B,EACnE,CAAC,IAAe,EAAE,SAAS,OAAc,IAA0B,EACnE,CAAC,IAAe,EAAE,SAAS,OAAc,IAA0B,EACnE,CAAC,MAAiB,IAAqC,EACvD,CAAC,MAAkB,GAA2B,EAC9C,CAAC,IAAe,EAAE,SAAS,OAAe,IAA0B,EAEpE,IAAW,IAAc,YAAY,EAAE,EACvC,IAAQ,IAAW,cAGnB,KAAkB,EAAS,IAAI,YAAY,OAE3C,KAA8B,GAAO,aACvC,OAAO,QAAQ,EAAM,WAAW,CAC7B,KAAK,CAAC,GAAM,QAAU;EACrB;EACA,OAAO,OAAO,KAAQ,WAAW,IAAM,OAAO,EAAI;EACnD,EAAE,CACF,QAAQ,MAAU,OAAO,SAAS,EAAM,MAAM,IAAI,EAAM,QAAQ,EAAE,GACrE,EAAE,EAEA,KAAgB,GAAO,iBAAiB,MAAM,GAAO,gBAAgB,IAErE,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,CAAC,IAAa,MAAkB,EAAS,GAAK,EAG9C,CAAC,IAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAS,KAAc,EAAuC,KAAK,EACpE,CAAC,GAAM,KAAW,EAA2B,EAAU,EACvD,CAAC,GAAkB,KAAuB,EAAS,GAAM,EAGzD,CAAC,GAAc,KACnB,EAAuC,KAAK,EAGxC,CAAC,GAAc,KACnB,EAAuC,KAAK,EAGxC,CAAC,GAAc,KACnB,EAAuC,KAAK,EACxC,CAAC,GAAc,KAAmB,EAAS,GAAG,EAG9C,CAAC,IAAQ,KAAa,EAAwB,KAAK,EAEnD,IAAa,YAAY;AAC7B,QAAM,QAAQ,IAAI,CAAC,IAAS,EAAE,IAAc,CAAC,CAAC;IAG1C,WAAmB;AAGvB,EAFA,EAAW,KAAK,EAChB,EAAQ;GAAE,GAAG;GAAW,MAAM,GAAU;GAAE,CAAC,EAC3C,EAAY,GAAK;IAGb,MAAY,MAAmC;AAYnD,EAXA,EAAW,EAAQ,EACnB,EAAQ;GACN,QAAQ,OAAO,EAAQ,OAAO;GAC9B,UAAU,EAAQ;GAClB,aAAa,EAAQ,eAAe;GACpC,WAAW,EAAQ,aAAa;GAChC,MAAM,EAAQ,OAAO,EAAQ,KAAK,MAAM,GAAG,GAAG,GAAG,GAAU;GAC3D,eAAe,EAAQ,iBAAiB;GAExC,mBAAmB,EAAQ,sBAAsB;GAClD,CAAC,EACF,EAAY,GAAK;IAGb,KAAsB,OAAO,MAAe;AAChD,IAAoB,GAAK;AACzB,MAAI;GACF,IAAM,EAAE,MAAM,MAAe,MAAM,EAAO,OAAO;IAC/C,UAAU;IACV,WAAW,EAAE,MAAM,wBAAwB;IAC5C,CAAC,EACI,IACJ,GACC,oBAAoB;AACvB,OAAI,CAAC,EAAU,OAAU,MAAM,kCAAkC;GACjE,IAAM,IAAW,MAAM,GAAkB;IACvC,QAAQ;IACR,MAAM;IACN,UAAU,EAAK;IACf,UAAU,EAAK,QAAQ;IACvB;IACD,CAAC,EAII,EAAE,YAAS,MAAM,EAAO,MAAM;IAClC,OAAO;IACP,WAAW;KACT,IAAI,EAAS;KACb,WAAW;KACZ;IACD,aAAa;IACd,CAAC,EACI,IAAO,GACT;AAMJ,GALA,GAAS,OAAO;IACd,GAAG;IACH,eAAe,EAAS;IACxB,mBAAmB,KAAO;IAC3B,EAAE,EACH,EAAS;IAAE,OAAO;IAAoB,SAAS;IAAW,CAAC;UACrD;AAEN,KAAS;IAAE,OAAO;IAA4B,SAAS;IAAS,CAAC;YACzD;AACR,KAAoB,GAAM;;IAIxB,UAAkB;AAEtB,EADA,EAAY,GAAM,EAClB,EAAW,KAAK;IAGZ,KAAe,YAAY;EAC/B,IAAM,IAAS,OAAO,EAAK,OAAO;AAClC,MAAI,CAAC,EAAK,UAAU,OAAO,MAAM,EAAO,IAAI,CAAC,EAAK,SAAS,MAAM,CAAE;EACnE,IAAM,IAAU,IAAI,KAAK,EAAK,KAAK,CAAC,aAAa;AACjD,MAAI;AAkCF,GAjCI,KACF,MAAM,GAAc,EAClB,WAAW;IACT,IAAI,EAAQ;IACZ;IACA,OAAO;KACL;KACA,UAAU,EAAK,SAAS,MAAM;KAC9B,aAAa,EAAK,YAAY,MAAM,IAAI,KAAA;KACxC,WAAW,EAAK,UAAU,MAAM,IAAI,KAAA;KACpC,MAAM;KACN,eAAe,EAAK,iBAAiB,KAAA;KACtC;IACF,EACF,CAAC,EACF,EAAS;IAAE,OAAO;IAAmB,SAAS;IAAW,CAAC,KAE1D,MAAM,GAAc,EAClB,WAAW,EACT,OAAO;IACL;IACA;IACA,UAAU,EAAK,SAAS,MAAM;IAC9B,aAAa,EAAK,YAAY,MAAM,IAAI,KAAA;IACxC,WAAW,EAAK,UAAU,MAAM,IAAI,KAAA;IACpC,MAAM;IACN,eAAe,EAAK,iBAAiB,KAAA;IACtC,EACF,EACF,CAAC,EACF,EAAS;IAAE,OAAO;IAAiB,SAAS;IAAW,CAAC,GAE1D,GAAW,EACX,MAAM,GAAY;UACZ;AACN,KAAS;IACP,OAAO,IAAU,6BAA6B;IAC9C,SAAS;IACV,CAAC;;IAIA,KAAe,YAAY;AAC1B,QACL,KAAI;AAMF,GALA,MAAM,GAAc,EAClB,WAAW;IAAE,IAAI,EAAa;IAAI;IAAa,EAChD,CAAC,EACF,EAAS;IAAE,OAAO;IAAmB,SAAS;IAAW,CAAC,EAC1D,EAAgB,KAAK,EACrB,MAAM,GAAY;UACZ;AACN,KAAS;IAAE,OAAO;IAA4B,SAAS;IAAS,CAAC;;IAI/D,KAA0B,OAAO,MAAmC;AACxE,IAAU,EAAQ,GAAG;AACrB,MAAI;AAGF,GAFA,MAAM,GAAc,EAAE,WAAW;IAAE,IAAI,EAAQ;IAAI;IAAa,EAAE,CAAC,EACnE,EAAS;IAAE,OAAO;IAA0B,SAAS;IAAW,CAAC,EACjE,MAAM,GAAY;UACZ;AACN,KAAS;IAAE,OAAO;IAA4B,SAAS;IAAS,CAAC;YACzD;AACR,KAAU,KAAK;;IAIb,KAAgB,OAAO,MAAmC;AAC9D,IAAU,EAAQ,GAAG;AACrB,MAAI;AAGF,GAFA,MAAM,GAAe,EAAE,WAAW;IAAE,IAAI,EAAQ;IAAI;IAAa,EAAE,CAAC,EACpE,EAAS;IAAE,OAAO;IAAoB,SAAS;IAAW,CAAC,EAC3D,MAAM,GAAY;UACZ;AACN,KAAS;IAAE,OAAO;IAA6B,SAAS;IAAS,CAAC;YAC1D;AACR,KAAU,KAAK;;IAIb,KAAe,YAAY;AAC1B,QACL,KAAI;AAWF,GAVA,MAAM,GAAc,EAClB,WAAW;IACT,IAAI,EAAa;IACjB;IACA,QAAQ,EAAa,MAAM,IAAI,KAAA;IAChC,EACF,CAAC,EACF,EAAS;IAAE,OAAO;IAAoB,SAAS;IAAW,CAAC,EAC3D,EAAgB,KAAK,EACrB,EAAgB,GAAG,EACnB,MAAM,GAAY;UACZ;AACN,KAAS;IAAE,OAAO;IAA4B,SAAS;IAAS,CAAC;;IAI/D,KAAiB,YAAY;AACjC,IAAe,GAAK;AACpB,MAAI;AAcF,KAbY,MAAM,GAAU;IAC1B,WAAW;KACT;KACA,YAAY;MAAE,OAAO;MAAW,QAAQ,EAAS;MAAQ;KAC1D;IACD,cAAc,GAAM,EAAE,yBACf,IACE;KACL,GAAG;KACH,UAAU,CAAC,GAAG,EAAK,UAAU,GAAG,EAAgB,SAAS;KAC1D,GAJ4B;IAMhC,CAAC,EACO,MAAM,UAAU,UAAU,KAAK,KACtC,GAAe,GAAM;YAEf;AACR,KAAe,GAAM;;;AAIzB,KAAI,KAAmB,MAAgB,EAAS,WAAW,EACzD,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;YAEZ,kBAAC,GAAD;GAAiB,SAAQ;GAAO,OAAO;GAAK,CAAA;EACjC,CAAA;AAIjB,KAAI,MAAiB,GACnB,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;YAEZ,kBAAC,OAAD;GAAK,WAAU;aAAoG;GAE7G,CAAA;EACK,CAAA;CAIjB,IAAM,IACJ,kBAAC,UAAD;EACE,SAAS;EACT,WAAU;YAFZ,CAIE,kBAAC,IAAD,EAAM,WAAU,WAAY,CAAA,EAAA,cAErB;;AAGX,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SAAQ;EACR,SAAS;YAJX;GAME,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,UAAA;MACA,OAAM;MACN,OAAO,EAAe,GAAO,eAAe,EAAE;MAC9C,MAAM,kBAAC,IAAD,EAAY,WAAU,WAAY,CAAA;MACxC,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,OAAO,GAAO,gBAAgB,EAAE;MACvC,aAAY;MACZ,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,OAAO,GAAO,iBAAiB,EAAE;MACxC,aAAY;MACZ,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,EAAe,GAAO,iBAAiB,EAAE;MAChD,MAAM,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;MACrC,aAAY;MACZ,CAAA;KACE;;GAEL,EAAS,WAAW,KAAK,CAAC,IACzB,kBAAC,IAAD;IACE,cAAa;IACb,OAAM;IACN,aAAY;IACZ,QAAQ;IACR,CAAA,GAEF,kBAAA,GAAA,EAAA,UAAA;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,OAAM;MACN,aAAY;MACZ,WAAU;gBAEV,kBAAC,IAAD;OACE,MAAM;OACN,aAAY;OACZ,aAAa,GAAO,eAAe;OACnC,cAAc,MAAU,EAAe,GAAO,GAAgB;OAC9D,QAAQ;OACR,YAAW;OACX,WAAU;OACV,CAAA;MACQ,CAAA,EACZ,kBAAC,GAAD;MACE,OAAM;MACN,aAAY;gBAEZ,kBAAC,IAAD;OACE,OAAO,GAAO,iBAAiB;OAC/B,KAAK;OACL,KAAK,IAAe,IAAI,IAAe;OACvC,OAAM;OACN,UAAU,MAAM,EAAa;OAC7B,SAAS,MAAU,GAAG,KAAK,MAAM,EAAM;OACvC,QAAQ;OACR,WAAU;OACV,CAAA;MACQ,CAAA,CACR;;IAEN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAS,KAAK,MAAmC;OAChD,IAAM,IAAU,OAAW,EAAQ,IAC7B,IAAY,EAAQ,WAAW,EAAc,SAC7C,IAAa,EAAQ,WAAW,EAAc;AACpD,cACE,kBAAC,OAAD;QAEE,WAAU;kBAFZ,CAIE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD,EAAS,WAAU,wCAAyC,CAAA,EAC5D,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAQ,eAAe;UACtB,CAAA,EACJ,kBAAC,KAAD;UAAG,WAAU;oBAAb;WACG,EAAQ;WAAS;WAAU;WAC3B,EAAQ,aAAa;WAAa;WAAU;WAC5C,EAAW,EAAQ,KAAK;WACvB;YACA,EAAA,CAAA,CACF;YACN,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAe,EAAQ,QAAQ,EAAQ,SAAS;WAC/C,CAAA;UACJ,kBAAC,IAAD;WACE,OAAO,EAAQ,OAAO,aAAa;WACnC,QAAQ,EAAQ,OAAO,aAAa;WACpC,CAAA;UACF,kBAAC,OAAD;WAAK,WAAU;qBAAf;YACG,KACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;aACE,MAAK;aACL,UAAU;aACV,eAAe,GAAc,EAAQ;aACrC,OAAM;aACN,WAAU;uBAEV,kBAAC,IAAD,EAAO,WAAU,WAAY,CAAA;aACtB,CAAA,EACT,kBAAC,UAAD;aACE,MAAK;aACL,UAAU;aACV,eAAe;AAEb,cADA,EAAgB,EAAQ,EACxB,EAAgB,GAAG;;aAErB,OAAM;aACN,WAAU;uBAEV,kBAAC,IAAD,EAAG,WAAU,WAAY,CAAA;aAClB,CAAA,CACR,EAAA,CAAA;YAEJ,KACC,kBAAC,UAAD;aACE,MAAK;aACL,UAAU;aACV,eAAe,GAAwB,EAAQ;aAC/C,OAAM;aACN,WAAU;uBAEV,kBAAC,IAAD,EAAM,WAAU,WAAY,CAAA;aACrB,CAAA;YAEX,kBAAC,UAAD;aACE,MAAK;aACL,eAAe,EAAgB,EAAQ;aACvC,OAAM;aACN,WAAU;uBAEV,kBAAC,IAAD,EAAe,WAAU,WAAY,CAAA;aAC9B,CAAA;YACT,kBAAC,UAAD;aACE,MAAK;aACL,eAAe,GAAS,EAAQ;aAChC,OAAM;aACN,WAAU;uBAEV,kBAAC,IAAD,EAAQ,WAAU,WAAY,CAAA;aACvB,CAAA;YACT,kBAAC,UAAD;aACE,MAAK;aACL,eAAe,EAAgB,EAAQ;aACvC,OAAM;aACN,WAAU;uBAEV,kBAAC,IAAD,EAAQ,WAAU,WAAY,CAAA;aACvB,CAAA;YACL;;UACF;WACF;UAvFC,EAAQ,GAuFT;QAER;MACE,CAAA;KACF,CAAA;IAEL,MAAe,EAAS,UAAU,KACjC,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,UAAD;MACE,SAAS;MACT,UAAU;MACV,WAAU;gBAET,IAAc,eAAe;MACvB,CAAA;KACL,CAAA;IAEP,EAAA,CAAA;GAGL,kBAAC,GAAD;IACE,MAAM;IACN,SAAS;IACT,OAAO,IAAU,iBAAiB;IAClC,aACE,IACI,mCACA;IAEN,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,SAAS;KACT,WAAU;eACX;KAEQ,CAAA,EACT,kBAAC,UAAD;KACE,SAAS;KACT,UACE,KACA,KACA,CAAC,EAAK,UACN,OAAO,MAAM,OAAO,EAAK,OAAO,CAAC,IACjC,CAAC,EAAK,SAAS,MAAM;KAEvB,WAAU;eAET,KAAY,IACT,cACA,IACE,iBACA;KACC,CAAA,CACR,EAAA,CAAA;cAGL,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,WAAU;kBAA8C;QAEvD,CAAA,EACR,kBAAC,SAAD;QACE,MAAK;QACL,KAAK;QACL,MAAK;QACL,aAAY;QACZ,OAAO,EAAK;QACZ,WAAW,MAAM,EAAQ;SAAE,GAAG;SAAM,QAAQ,EAAE,OAAO;SAAO,CAAC;QAC7D,WAAW;QACX,CAAA,CACE,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,WAAU;kBAA8C;QAEvD,CAAA,EACR,kBAAC,UAAD;QACE,OAAO,EAAK;QACZ,WAAW,MAAM,EAAQ;SAAE,GAAG;SAAM,UAAU,EAAE,OAAO;SAAO,CAAC;QAC/D,WAAW;kBAEV,EAAiB,KAAK,MACrB,kBAAC,UAAD;SAAgB,OAAO;mBACpB;SACM,EAFI,EAEJ,CACT;QACK,CAAA,CACL,EAAA,CAAA,CACF;;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,WAAU;iBAA8C;OAEvD,CAAA,EACR,kBAAC,SAAD;OACE,MAAK;OACL,aAAY;OACZ,OAAO,EAAK;OACZ,WAAW,MACT,EAAQ;QAAE,GAAG;QAAM,aAAa,EAAE,OAAO;QAAO,CAAC;OAEnD,WAAW;OACX,CAAA,CACE,EAAA,CAAA;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,WAAU;kBAA8C;QAEvD,CAAA,EACR,kBAAC,SAAD;QACE,MAAK;QACL,aAAY;QACZ,OAAO,EAAK;QACZ,WAAW,MACT,EAAQ;SAAE,GAAG;SAAM,WAAW,EAAE,OAAO;SAAO,CAAC;QAEjD,WAAW;QACX,CAAA,CACE,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,WAAU;kBAA8C;QAEvD,CAAA,EACR,kBAAC,SAAD;QACE,MAAK;QACL,OAAO,EAAK;QACZ,WAAW,MAAM,EAAQ;SAAE,GAAG;SAAM,MAAM,EAAE,OAAO;SAAO,CAAC;QAC3D,WAAW;QACX,CAAA,CACE,EAAA,CAAA,CACF;;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,WAAU;iBAA8C;OAEvD,CAAA,EACR,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,SAAD;SACE,WAAW,sKACT,IAAmB,eAAe;mBAFtC;UAKE,kBAAC,IAAD,EAAQ,WAAU,WAAY,CAAA;UAC7B,IAAmB,iBAAiB;UACrC,kBAAC,SAAD;WACE,MAAK;WACL,QAAO;WACP,UAAU;WACV,WAAU;WACV,WAAW,MAAM;YACf,IAAM,IAAO,EAAE,OAAO,QAAQ;AAG9B,YADA,EAAE,OAAO,QAAQ,IACb,KAAW,GAAoB,EAAK;;WAE1C,CAAA;UACI;;QACP,EAAK,iBAAiB,EAAK,qBAC1B,kBAAC,KAAD;SACE,MAAM,EAAK;SACX,QAAO;SACP,KAAI;SACJ,WAAU;mBAJZ,CAME,kBAAC,IAAD,EAAc,WAAU,eAAgB,CAAA,EAAA,uBAEtC;;QAEN,kBAAC,KAAD;SAAG,WAAU;mBAA8B;SAGvC,CAAA;QACA;SACF,EAAA,CAAA;MACF;;IACA,CAAA;GAER,kBAAC,GAAD;IACE,MAAM,MAAiB;IACvB,eAAe,EAAgB,KAAK;IACpC,OAAM;IACN,aACE,IACI,GAAG,EAAe,EAAa,QAAQ,EAAa,SAAS,CAAC,KAAK,EAAa,SAAS,qBACzF,KAAA;IAEN,MAAK;IACL,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,eAAe,EAAgB,KAAK;KACpC,WAAU;eACX;KAEQ,CAAA,EACT,kBAAC,UAAD;KACE,SAAS;KACT,UAAU;KACV,WAAU;eAET,IAAW,gBAAgB;KACrB,CAAA,CACR,EAAA,CAAA;cAGL,kBAAC,KAAD;KAAG,WAAU;eAA8B;KAEvC,CAAA;IACE,CAAA;GAER,kBAAC,GAAD;IACE,MAAM,MAAiB;IACvB,eAAe,EAAgB,KAAK;IACpC,OAAM;IACN,aAAY;IACZ,MAAK;IACL,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,eAAe,EAAgB,KAAK;KACpC,WAAU;eACX;KAEQ,CAAA,EACT,kBAAC,UAAD;KACE,SAAS;KACT,UAAU;KACV,WAAU;eAET,IAAY,iBAAiB;KACvB,CAAA,CACR,EAAA,CAAA;cArBP,CAwBE,kBAAC,SAAD;KAAO,WAAU;eAA8C;KAEvD,CAAA,EACR,kBAAC,YAAD;KACE,MAAM;KACN,aAAY;KACZ,OAAO;KACP,WAAW,MAAM,EAAgB,EAAE,OAAO,MAAM;KAChD,WAAW;KACX,CAAA,CACI;;GAER,kBAAC,GAAD;IACE,MAAM,MAAiB;IACvB,eAAe,EAAgB,KAAK;IACpC,OAAM;IACN,aACE,IACI,WAAW,EAAa,eAAe,eAAe,oBACtD,KAAA;cAGL,KAAgB,kBAAC,IAAD,EAAqB,SAAS,GAAgB,CAAA;IACzD,CAAA;GAER,kBAAC,IAAD;IAAwB;IAAQ,WAAW;IAAgB,CAAA;GAChD"}
|
|
@@ -17,7 +17,7 @@ import { GlassCard as S } from "@burdenoff/fe-libs/ui";
|
|
|
17
17
|
//#region src/timecampus/pages/focus-score/FocusScorePage.tsx
|
|
18
18
|
function C() {
|
|
19
19
|
let { workspaceId: C = "" } = e(), { data: w, loading: T, error: E } = o({
|
|
20
|
-
variables: { limit: 30 },
|
|
20
|
+
variables: { pagination: { limit: 30 } },
|
|
21
21
|
skip: !C
|
|
22
22
|
}), { data: D, loading: O, error: k } = i({ skip: !C }), { data: A, loading: j, error: M } = a({ skip: !C }), N = T || O || j, P = E || k || M, F = A?.todayFocusScore, I = D?.focusScoreSummary, L = w?.focusScores ?? [], R = F ? [
|
|
23
23
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FocusScorePage.js","names":[],"sources":["../../../../src/timecampus/pages/focus-score/FocusScorePage.tsx"],"sourcesContent":["import {\n Brain,\n Target,\n TrendingUp,\n Shield,\n Gauge,\n LineChart,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n StatusBadge,\n ChartCard,\n RadialGauge,\n TrendChart,\n ProgressMeter,\n} from \"../../components\";\nimport { formatDate } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListFocusScoresQuery,\n useGetFocusScoreSummaryQuery,\n useGetTodayFocusScoreQuery,\n type FocusScoreFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nexport function FocusScorePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: scoresData,\n loading: scoresLoading,\n error: scoresError,\n } = useListFocusScoresQuery({\n variables: { limit: 30 },\n skip: !workspaceId,\n });\n\n const {\n data: summaryData,\n loading: summaryLoading,\n error: summaryError,\n } = useGetFocusScoreSummaryQuery({\n skip: !workspaceId,\n });\n\n const {\n data: todayData,\n loading: todayLoading,\n error: todayError,\n } = useGetTodayFocusScoreQuery({\n skip: !workspaceId,\n });\n\n const loading = scoresLoading || summaryLoading || todayLoading;\n const error = scoresError || summaryError || todayError;\n const today = todayData?.todayFocusScore;\n const summary = summaryData?.focusScoreSummary;\n const scores = scoresData?.focusScores ?? [];\n\n // Build focus components from today's score.\n const focusComponents = today\n ? [\n { label: \"Deep Work\", score: today.deepWorkScore ?? 0 },\n { label: \"Consistency\", score: today.consistencyScore ?? 0 },\n { label: \"Task Completion\", score: today.taskCompletionScore ?? 0 },\n {\n label: \"Distraction Resistance\",\n score: today.distractionResistanceScore ?? 0,\n },\n {\n label: \"Energy Management\",\n score: today.energyManagementScore ?? 0,\n },\n ]\n : [];\n\n // Real daily series (oldest → newest) for the trend chart.\n const trendData = [...scores]\n .sort(\n (a: FocusScoreFieldsFragment, b: FocusScoreFieldsFragment) =>\n new Date(a.date).getTime() - new Date(b.date).getTime(),\n )\n .map((s: FocusScoreFieldsFragment) => ({\n date: s.date,\n score: s.overallScore,\n }));\n\n // Honest, data-backed stats.\n const daysTracked = scores.length;\n const highFocusDays = scores.filter(\n (s: FocusScoreFieldsFragment) => s.overallScore >= 70,\n ).length;\n const averageScore =\n summary?.averageScore != null\n ? `${Math.round(summary.averageScore)}`\n : \"--\";\n const highestScore =\n summary?.highestScore != null\n ? `${Math.round(summary.highestScore)}`\n : \"--\";\n const trendText = summary?.trend\n ? summary.trend.charAt(0).toUpperCase() + summary.trend.slice(1)\n : undefined;\n\n const shortDate = (value: string | number): string =>\n new Date(String(value)).toLocaleDateString(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n });\n\n if (loading) {\n return (\n <PageLayout\n title=\"Focus Score\"\n description=\"Measure and improve your focus\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Focus Score\"\n description=\"Measure and improve your focus\"\n >\n <div className=\"rounded-xl border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n Failed to load focus score data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Focus Score\"\n description=\"Measure and improve your focus\"\n purpose=\"Get a single, honest number for how focused you were today — built from deep work, consistency, follow-through and distraction resistance — so you can see what's improving and what to work on next.\"\n >\n {/* KPI row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Average Score\"\n value={averageScore}\n icon={<Brain className=\"h-5 w-5\" />}\n description={trendText ? `${trendText} trend` : undefined}\n iconBgColor=\"bg-status-info-bg text-status-info-text\"\n />\n <StatCard\n title=\"Highest Score\"\n value={highestScore}\n icon={<Shield className=\"h-5 w-5\" />}\n description=\"Personal best\"\n />\n <StatCard\n title=\"High-Focus Days\"\n value={String(highFocusDays)}\n icon={<Target className=\"h-5 w-5\" />}\n description=\"Scored 70+\"\n />\n <StatCard\n title=\"Days Tracked\"\n value={String(daysTracked)}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n description=\"Last 30 days\"\n />\n </div>\n\n {/* Gauge + daily trend */}\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <ChartCard\n title=\"Current Focus Score\"\n description={\n today ? `As of ${formatDate(today.date)}` : \"No score today yet\"\n }\n actions={<Gauge className=\"h-4 w-4 text-text-secondary\" />}\n >\n {today ? (\n <div className=\"space-y-3\">\n <RadialGauge\n value={today.overallScore}\n min={0}\n max={100}\n label=\"out of 100\"\n sublabel={trendText ? `${trendText} trend` : undefined}\n thresholds={{ warning: 75, danger: 50 }}\n height={220}\n />\n {(summary?.topFactor || summary?.improvementArea) && (\n <div className=\"flex flex-wrap items-center justify-center gap-2\">\n {summary?.topFactor && (\n <StatusBadge\n variant=\"success\"\n label={`Strength: ${summary.topFactor}`}\n />\n )}\n {summary?.improvementArea && (\n <StatusBadge\n variant=\"warning\"\n label={`Improve: ${summary.improvementArea}`}\n />\n )}\n </div>\n )}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No focus score recorded today\"\n description=\"Track some focused work today and your score will appear here.\"\n />\n )}\n </ChartCard>\n\n <ChartCard\n title=\"Daily Focus Trend\"\n description={\n daysTracked > 0\n ? `Overall score across your last ${daysTracked} tracked day${daysTracked === 1 ? \"\" : \"s\"}`\n : \"Your focus history over time\"\n }\n actions={<LineChart className=\"h-4 w-4 text-text-secondary\" />}\n className=\"lg:col-span-2\"\n >\n <TrendChart\n data={trendData}\n xKey=\"date\"\n series={[{ key: \"score\", label: \"Focus score\" }]}\n type=\"area\"\n height={260}\n xTickFormat={shortDate}\n emptyLabel=\"No focus history yet — track focused work to build your trend.\"\n ariaLabel=\"Daily overall focus score over time\"\n />\n </ChartCard>\n </div>\n\n {/* Factor breakdown */}\n <GlassCard treatment=\"glass\" glow className=\"mt-6 p-6\">\n <h3 className=\"mb-4 text-sm font-semibold text-text-primary\">\n Focus Components\n </h3>\n {focusComponents.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No focus score recorded today\"\n description=\"Track some focused work today and your score breakdown will appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {focusComponents.map((component) => (\n <div\n key={component.label}\n className=\"rounded-xl border border-border-seam bg-bg-sunken/40 p-4\"\n >\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">\n {component.label}\n </span>\n <span className=\"text-lg font-bold tabular-nums text-text-primary\">\n {component.score}\n </span>\n </div>\n <ProgressMeter\n value={component.score}\n max={100}\n higherIsWorse={false}\n showValue={false}\n size=\"md\"\n className=\"mt-2.5\"\n />\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FocusScorePage.js","names":[],"sources":["../../../../src/timecampus/pages/focus-score/FocusScorePage.tsx"],"sourcesContent":["import {\n Brain,\n Target,\n TrendingUp,\n Shield,\n Gauge,\n LineChart,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n StatusBadge,\n ChartCard,\n RadialGauge,\n TrendChart,\n ProgressMeter,\n} from \"../../components\";\nimport { formatDate } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListFocusScoresQuery,\n useGetFocusScoreSummaryQuery,\n useGetTodayFocusScoreQuery,\n type FocusScoreFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nexport function FocusScorePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: scoresData,\n loading: scoresLoading,\n error: scoresError,\n } = useListFocusScoresQuery({\n variables: { pagination: { limit: 30 } },\n skip: !workspaceId,\n });\n\n const {\n data: summaryData,\n loading: summaryLoading,\n error: summaryError,\n } = useGetFocusScoreSummaryQuery({\n skip: !workspaceId,\n });\n\n const {\n data: todayData,\n loading: todayLoading,\n error: todayError,\n } = useGetTodayFocusScoreQuery({\n skip: !workspaceId,\n });\n\n const loading = scoresLoading || summaryLoading || todayLoading;\n const error = scoresError || summaryError || todayError;\n const today = todayData?.todayFocusScore;\n const summary = summaryData?.focusScoreSummary;\n const scores = scoresData?.focusScores ?? [];\n\n // Build focus components from today's score.\n const focusComponents = today\n ? [\n { label: \"Deep Work\", score: today.deepWorkScore ?? 0 },\n { label: \"Consistency\", score: today.consistencyScore ?? 0 },\n { label: \"Task Completion\", score: today.taskCompletionScore ?? 0 },\n {\n label: \"Distraction Resistance\",\n score: today.distractionResistanceScore ?? 0,\n },\n {\n label: \"Energy Management\",\n score: today.energyManagementScore ?? 0,\n },\n ]\n : [];\n\n // Real daily series (oldest → newest) for the trend chart.\n const trendData = [...scores]\n .sort(\n (a: FocusScoreFieldsFragment, b: FocusScoreFieldsFragment) =>\n new Date(a.date).getTime() - new Date(b.date).getTime(),\n )\n .map((s: FocusScoreFieldsFragment) => ({\n date: s.date,\n score: s.overallScore,\n }));\n\n // Honest, data-backed stats.\n const daysTracked = scores.length;\n const highFocusDays = scores.filter(\n (s: FocusScoreFieldsFragment) => s.overallScore >= 70,\n ).length;\n const averageScore =\n summary?.averageScore != null\n ? `${Math.round(summary.averageScore)}`\n : \"--\";\n const highestScore =\n summary?.highestScore != null\n ? `${Math.round(summary.highestScore)}`\n : \"--\";\n const trendText = summary?.trend\n ? summary.trend.charAt(0).toUpperCase() + summary.trend.slice(1)\n : undefined;\n\n const shortDate = (value: string | number): string =>\n new Date(String(value)).toLocaleDateString(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n });\n\n if (loading) {\n return (\n <PageLayout\n title=\"Focus Score\"\n description=\"Measure and improve your focus\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Focus Score\"\n description=\"Measure and improve your focus\"\n >\n <div className=\"rounded-xl border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n Failed to load focus score data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Focus Score\"\n description=\"Measure and improve your focus\"\n purpose=\"Get a single, honest number for how focused you were today — built from deep work, consistency, follow-through and distraction resistance — so you can see what's improving and what to work on next.\"\n >\n {/* KPI row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Average Score\"\n value={averageScore}\n icon={<Brain className=\"h-5 w-5\" />}\n description={trendText ? `${trendText} trend` : undefined}\n iconBgColor=\"bg-status-info-bg text-status-info-text\"\n />\n <StatCard\n title=\"Highest Score\"\n value={highestScore}\n icon={<Shield className=\"h-5 w-5\" />}\n description=\"Personal best\"\n />\n <StatCard\n title=\"High-Focus Days\"\n value={String(highFocusDays)}\n icon={<Target className=\"h-5 w-5\" />}\n description=\"Scored 70+\"\n />\n <StatCard\n title=\"Days Tracked\"\n value={String(daysTracked)}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n description=\"Last 30 days\"\n />\n </div>\n\n {/* Gauge + daily trend */}\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <ChartCard\n title=\"Current Focus Score\"\n description={\n today ? `As of ${formatDate(today.date)}` : \"No score today yet\"\n }\n actions={<Gauge className=\"h-4 w-4 text-text-secondary\" />}\n >\n {today ? (\n <div className=\"space-y-3\">\n <RadialGauge\n value={today.overallScore}\n min={0}\n max={100}\n label=\"out of 100\"\n sublabel={trendText ? `${trendText} trend` : undefined}\n thresholds={{ warning: 75, danger: 50 }}\n height={220}\n />\n {(summary?.topFactor || summary?.improvementArea) && (\n <div className=\"flex flex-wrap items-center justify-center gap-2\">\n {summary?.topFactor && (\n <StatusBadge\n variant=\"success\"\n label={`Strength: ${summary.topFactor}`}\n />\n )}\n {summary?.improvementArea && (\n <StatusBadge\n variant=\"warning\"\n label={`Improve: ${summary.improvementArea}`}\n />\n )}\n </div>\n )}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No focus score recorded today\"\n description=\"Track some focused work today and your score will appear here.\"\n />\n )}\n </ChartCard>\n\n <ChartCard\n title=\"Daily Focus Trend\"\n description={\n daysTracked > 0\n ? `Overall score across your last ${daysTracked} tracked day${daysTracked === 1 ? \"\" : \"s\"}`\n : \"Your focus history over time\"\n }\n actions={<LineChart className=\"h-4 w-4 text-text-secondary\" />}\n className=\"lg:col-span-2\"\n >\n <TrendChart\n data={trendData}\n xKey=\"date\"\n series={[{ key: \"score\", label: \"Focus score\" }]}\n type=\"area\"\n height={260}\n xTickFormat={shortDate}\n emptyLabel=\"No focus history yet — track focused work to build your trend.\"\n ariaLabel=\"Daily overall focus score over time\"\n />\n </ChartCard>\n </div>\n\n {/* Factor breakdown */}\n <GlassCard treatment=\"glass\" glow className=\"mt-6 p-6\">\n <h3 className=\"mb-4 text-sm font-semibold text-text-primary\">\n Focus Components\n </h3>\n {focusComponents.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No focus score recorded today\"\n description=\"Track some focused work today and your score breakdown will appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {focusComponents.map((component) => (\n <div\n key={component.label}\n className=\"rounded-xl border border-border-seam bg-bg-sunken/40 p-4\"\n >\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">\n {component.label}\n </span>\n <span className=\"text-lg font-bold tabular-nums text-text-primary\">\n {component.score}\n </span>\n </div>\n <ProgressMeter\n value={component.score}\n max={100}\n higherIsWorse={false}\n showValue={false}\n size=\"md\"\n className=\"mt-2.5\"\n />\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GeofencingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/geofencing/GeofencingPage.tsx"],"names":[],"mappings":"AA4GA,wBAAgB,cAAc,
|
|
1
|
+
{"version":3,"file":"GeofencingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/geofencing/GeofencingPage.tsx"],"names":[],"mappings":"AA4GA,wBAAgB,cAAc,gCAwlB7B"}
|
|
@@ -37,10 +37,10 @@ function O({ checked: t, onChange: n, label: r, disabled: i }) {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
function k() {
|
|
40
|
-
let { data: k, loading: ae, error: oe, refetch: A, fetchMore: j } = o({ variables: {
|
|
40
|
+
let { data: k, loading: ae, error: oe, refetch: A, fetchMore: j } = o({ variables: { pagination: {
|
|
41
41
|
limit: T,
|
|
42
42
|
offset: 0
|
|
43
|
-
} }), [M, { loading: N, error: se }] = i(), [P, { loading: F, error: ce }] = c(), [le, { loading: I }] = a(), [L] = s(), [R, z] = g(!1), [B, V] = g(null), [H, U] = g(E), [W, G] = g(null), [K, q] = g(!1), J = k?.geofences?.items ?? [], ue = k?.geofences?.hasMore ?? !1, de = k?.geofences?.total ?? J.length, fe = J.filter((e) => e.isActive).length, Y = 0, X = 0;
|
|
43
|
+
} } }), [M, { loading: N, error: se }] = i(), [P, { loading: F, error: ce }] = c(), [le, { loading: I }] = a(), [L] = s(), [R, z] = g(!1), [B, V] = g(null), [H, U] = g(E), [W, G] = g(null), [K, q] = g(!1), J = k?.geofences?.items ?? [], ue = k?.geofences?.hasMore ?? !1, de = k?.geofences?.total ?? J.length, fe = J.filter((e) => e.isActive).length, Y = 0, X = 0;
|
|
44
44
|
for (let e of J) for (let t of e.events) t.type === m.Enter ? Y += 1 : X += 1;
|
|
45
45
|
let pe = [
|
|
46
46
|
{
|
|
@@ -103,10 +103,10 @@ function k() {
|
|
|
103
103
|
q(!0);
|
|
104
104
|
try {
|
|
105
105
|
await j({
|
|
106
|
-
variables: {
|
|
106
|
+
variables: { pagination: {
|
|
107
107
|
limit: T,
|
|
108
108
|
offset: J.length
|
|
109
|
-
},
|
|
109
|
+
} },
|
|
110
110
|
updateQuery: (e, { fetchMoreResult: t }) => t ? { geofences: {
|
|
111
111
|
...t.geofences,
|
|
112
112
|
items: [...e.geofences.items, ...t.geofences.items]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GeofencingPage.js","names":[],"sources":["../../../../src/timecampus/pages/geofencing/GeofencingPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n MapPin,\n Plus,\n Pencil,\n Trash2,\n LogIn,\n LogOut,\n Radio,\n ShieldCheck,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n ChartCard,\n DonutChart,\n BarsChart,\n type ChartDatum,\n} from \"../../components\";\nimport { cn, formatRelativeTime } from \"../../utils/helpers\";\nimport {\n useListGeofencesQuery,\n useCreateGeofenceMutation,\n useUpdateGeofenceMutation,\n useDeleteGeofenceMutation,\n useToggleGeofenceMutation,\n type ListGeofencesQuery,\n} from \"@/generated/wspace-operations\";\nimport { GeofenceTrigger, GeofenceEventType } from \"@/generated/wspace-types\";\n\ntype GeofenceItem = NonNullable<\n ListGeofencesQuery[\"geofences\"]\n>[\"items\"][number];\n\nconst PAGE_SIZE = 25;\n\ninterface GeofenceFormState {\n name: string;\n latitude: string;\n longitude: string;\n radius: string;\n trigger: GeofenceTrigger;\n address: string;\n isActive: boolean;\n}\n\nconst emptyForm: GeofenceFormState = {\n name: \"\",\n latitude: \"\",\n longitude: \"\",\n radius: \"100\",\n trigger: GeofenceTrigger.Both,\n address: \"\",\n isActive: true,\n};\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\ninterface ToggleSwitchProps {\n checked: boolean;\n onChange: (next: boolean) => void;\n label: string;\n disabled?: boolean;\n}\n\n/**\n * Local, token-styled switch (shared components are off-limits here). The knob\n * translates only on the checked state — never on hover — per the repo\n * no-hover-movement mandate.\n */\nfunction ToggleSwitch({\n checked,\n onChange,\n label,\n disabled,\n}: ToggleSwitchProps) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={label}\n disabled={disabled}\n onClick={() => onChange(!checked)}\n className={cn(\n \"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border transition-colors disabled:cursor-not-allowed disabled:opacity-50\",\n checked\n ? \"border-transparent bg-action-primary-bg\"\n : \"border-border-subtle bg-bg-sunken\",\n )}\n >\n <span\n className={cn(\n \"inline-block h-3.5 w-3.5 transform rounded-full shadow-sm transition-transform\",\n checked\n ? \"translate-x-4 bg-action-primary-text\"\n : \"translate-x-0.5 bg-text-secondary\",\n )}\n />\n </button>\n );\n}\n\nexport function GeofencingPage() {\n const { data, loading, error, refetch, fetchMore } = useListGeofencesQuery({\n variables: { limit: PAGE_SIZE, offset: 0 },\n });\n\n const [createGeofence, { loading: creating, error: createError }] =\n useCreateGeofenceMutation();\n const [updateGeofence, { loading: updating, error: updateError }] =\n useUpdateGeofenceMutation();\n const [deleteGeofence, { loading: deleting }] = useDeleteGeofenceMutation();\n const [toggleGeofence] = useToggleGeofenceMutation();\n\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<GeofenceItem | null>(null);\n const [form, setForm] = useState<GeofenceFormState>(emptyForm);\n const [confirmDelete, setConfirmDelete] = useState<GeofenceItem | null>(null);\n const [loadingMore, setLoadingMore] = useState(false);\n\n const geofences = data?.geofences?.items ?? [];\n const hasMore = data?.geofences?.hasMore ?? false;\n const totalZones = data?.geofences?.total ?? geofences.length;\n\n // ---- Derived, real-data summaries ---------------------------------------\n const activeZones = geofences.filter((f: GeofenceItem) => f.isActive).length;\n let enterEvents = 0;\n let exitEvents = 0;\n for (const fence of geofences as GeofenceItem[]) {\n for (const event of fence.events) {\n if (event.type === GeofenceEventType.Enter) enterEvents += 1;\n else exitEvents += 1;\n }\n }\n\n const triggerSlices = [\n {\n name: \"Entry & exit\",\n value: geofences.filter(\n (f: GeofenceItem) => f.trigger === GeofenceTrigger.Both,\n ).length,\n },\n {\n name: \"On entry\",\n value: geofences.filter(\n (f: GeofenceItem) => f.trigger === GeofenceTrigger.Enter,\n ).length,\n },\n {\n name: \"On exit\",\n value: geofences.filter(\n (f: GeofenceItem) => f.trigger === GeofenceTrigger.Exit,\n ).length,\n },\n ];\n\n const activityData: ChartDatum[] = geofences\n .map((f: GeofenceItem) => ({ zone: f.name, events: f.events.length }))\n .filter((d: { zone: string; events: number }) => d.events > 0)\n .sort((a: { events: number }, b: { events: number }) => b.events - a.events)\n .slice(0, 8);\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (fence: GeofenceItem) => {\n setEditing(fence);\n setForm({\n name: fence.name,\n latitude: String(fence.latitude),\n longitude: String(fence.longitude),\n radius: String(fence.radius),\n trigger: fence.trigger,\n address: fence.address ?? \"\",\n isActive: fence.isActive,\n });\n setFormOpen(true);\n };\n\n const handleSubmit = async () => {\n if (!form.name.trim()) return;\n const base = {\n name: form.name.trim(),\n latitude: Number(form.latitude),\n longitude: Number(form.longitude),\n radius: Number(form.radius),\n trigger: form.trigger,\n address: form.address.trim() || undefined,\n isActive: form.isActive,\n };\n if (editing) {\n await updateGeofence({ variables: { id: editing.id, input: base } });\n } else {\n await createGeofence({ variables: { input: base } });\n }\n setFormOpen(false);\n await refetch();\n };\n\n const handleDelete = async () => {\n if (!confirmDelete) return;\n await deleteGeofence({ variables: { id: confirmDelete.id } });\n setConfirmDelete(null);\n await refetch();\n };\n\n const handleToggle = async (fence: GeofenceItem) => {\n await toggleGeofence({\n variables: { id: fence.id, isActive: !fence.isActive },\n });\n await refetch();\n };\n\n /**\n * Enter/exit are edges of the single `trigger` enum. Recompute the enum from\n * the two switches and persist via updateGeofence. A zone must keep at least\n * one edge, so a change that would disable both is ignored.\n */\n const handleSetTrigger = async (\n fence: GeofenceItem,\n nextEnter: boolean,\n nextExit: boolean,\n ) => {\n if (!nextEnter && !nextExit) return;\n const trigger =\n nextEnter && nextExit\n ? GeofenceTrigger.Both\n : nextEnter\n ? GeofenceTrigger.Enter\n : GeofenceTrigger.Exit;\n if (trigger === fence.trigger) return;\n await updateGeofence({ variables: { id: fence.id, input: { trigger } } });\n await refetch();\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { limit: PAGE_SIZE, offset: geofences.length },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n geofences: {\n ...fetchMoreResult.geofences,\n items: [\n ...prev.geofences.items,\n ...fetchMoreResult.geofences.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n const createButton = (\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 Create Geofence\n </button>\n );\n\n if (loading) {\n return (\n <PageLayout\n title=\"Geofencing\"\n description=\"Set up location-based triggers and zones\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Geofencing\"\n description=\"Set up location-based triggers and zones\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load geofences. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Geofencing\"\n description=\"Set up location-based triggers and zones\"\n purpose=\"Draw zones around your worksites — offices, client sites, job locations — and have time tracking start and stop automatically as people move in and out, so on-site hours record themselves.\"\n actions={createButton}\n >\n {geofences.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No geofences configured yet\"\n description=\"Create your first zone to trigger automatic time tracking when people arrive or leave a location.\"\n action={createButton}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Zones\"\n value={String(totalZones)}\n icon={<MapPin className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Active Zones\"\n value={String(activeZones)}\n icon={<Radio className=\"h-5 w-5\" />}\n description=\"Currently tracking\"\n />\n <StatCard\n title=\"Entry Events\"\n value={String(enterEvents)}\n icon={<LogIn className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Exit Events\"\n value={String(exitEvents)}\n icon={<LogOut className=\"h-5 w-5\" />}\n />\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title=\"Trigger configuration\"\n description=\"How your zones fire\"\n >\n <DonutChart\n data={triggerSlices}\n centerLabel=\"Zones\"\n centerValue={geofences.length}\n ariaLabel=\"Geofence trigger configuration mix\"\n emptyLabel=\"No zones to summarize yet\"\n />\n </ChartCard>\n <ChartCard\n title=\"Activity by zone\"\n description=\"Recorded enter/exit events per zone\"\n >\n <BarsChart\n data={activityData}\n xKey=\"zone\"\n orientation=\"horizontal\"\n series={[{ key: \"events\", label: \"Events\" }]}\n categoryWidth={120}\n ariaLabel=\"Recorded events by zone\"\n emptyLabel=\"No geofence events recorded yet\"\n />\n </ChartCard>\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3\">\n {geofences.map((fence: GeofenceItem) => {\n const enterOn =\n fence.trigger === GeofenceTrigger.Enter ||\n fence.trigger === GeofenceTrigger.Both;\n const exitOn =\n fence.trigger === GeofenceTrigger.Exit ||\n fence.trigger === GeofenceTrigger.Both;\n const fenceEnter = fence.events.filter(\n (e) => e.type === GeofenceEventType.Enter,\n ).length;\n const fenceExit = fence.events.length - fenceEnter;\n const lastTs = fence.events.reduce(\n (latest, e) => (e.timestamp > latest ? e.timestamp : latest),\n \"\",\n );\n return (\n <GlassCard\n key={fence.id}\n treatment=\"glass\"\n glow={fence.isActive}\n className=\"flex flex-col p-5\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-start gap-2\">\n <span className=\"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <MapPin className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {fence.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {fence.address ?? \"No address\"} ·{\" \"}\n {fence.radius}m radius\n </p>\n </div>\n </div>\n <div className=\"flex shrink-0 flex-col items-end gap-1\">\n <ToggleSwitch\n checked={fence.isActive}\n onChange={() => handleToggle(fence)}\n label={`Toggle ${fence.name} active`}\n />\n <span\n className={cn(\n \"text-xs font-medium\",\n fence.isActive\n ? \"text-status-success-text\"\n : \"text-text-muted\",\n )}\n >\n {fence.isActive ? \"Active\" : \"Inactive\"}\n </span>\n </div>\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-2\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle bg-bg-surface/60 px-3 py-2\">\n <span className=\"flex items-center gap-1.5 text-xs font-medium text-text-secondary\">\n <LogIn className=\"h-3.5 w-3.5\" />\n Enter\n </span>\n <ToggleSwitch\n checked={enterOn}\n onChange={(next) =>\n handleSetTrigger(fence, next, exitOn)\n }\n label={`Enter trigger for ${fence.name}`}\n />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle bg-bg-surface/60 px-3 py-2\">\n <span className=\"flex items-center gap-1.5 text-xs font-medium text-text-secondary\">\n <LogOut className=\"h-3.5 w-3.5\" />\n Exit\n </span>\n <ToggleSwitch\n checked={exitOn}\n onChange={(next) =>\n handleSetTrigger(fence, enterOn, next)\n }\n label={`Exit trigger for ${fence.name}`}\n />\n </div>\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <span>\n Entries{\" \"}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {fenceEnter}\n </span>\n </span>\n <span>\n Exits{\" \"}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {fenceExit}\n </span>\n </span>\n {lastTs ? (\n <span className=\"ml-auto\">\n Last{\" \"}\n <span className=\"text-text-primary\">\n {formatRelativeTime(lastTs)}\n </span>\n </span>\n ) : (\n <span className=\"ml-auto text-text-muted\">\n No events yet\n </span>\n )}\n </div>\n\n <div className=\"mt-4 flex items-center gap-2 border-t border-border-seam pt-3\">\n <button\n onClick={() => openEdit(fence)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n Edit\n </button>\n <button\n onClick={() => setConfirmDelete(fence)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n Delete\n </button>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n <GlassCard treatment=\"insight\" className=\"mt-6 p-4\">\n <div className=\"flex items-start gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <ShieldCheck className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n Privacy first\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Time is recorded only when someone enters or leaves a zone you\n define — never through continuous background tracking.\n Location data stays encrypted and visible only within your\n workspace.\n </p>\n </div>\n </div>\n </GlassCard>\n\n {hasMore && (\n <div className=\"mt-6 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n <Modal\n open={formOpen}\n onClose={() => setFormOpen(false)}\n title={editing ? \"Edit Geofence\" : \"Create Geofence\"}\n description=\"Define a zone that triggers automatic time tracking.\"\n size=\"lg\"\n footer={\n <>\n <button\n onClick={() => setFormOpen(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={handleSubmit}\n disabled={creating || updating || !form.name.trim()}\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 || updating\n ? \"Saving...\"\n : editing\n ? \"Save Changes\"\n : \"Create Geofence\"}\n </button>\n </>\n }\n >\n {(createError || 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 save geofence. Please check the values and 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 Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Head Office\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Latitude\n </label>\n <input\n type=\"number\"\n step=\"any\"\n placeholder=\"0.000000\"\n value={form.latitude}\n onChange={(e) => setForm({ ...form, latitude: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Longitude\n </label>\n <input\n type=\"number\"\n step=\"any\"\n placeholder=\"0.000000\"\n value={form.longitude}\n onChange={(e) =>\n setForm({ ...form, longitude: e.target.value })\n }\n className={inputClass}\n />\n </div>\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 Radius (meters)\n </label>\n <input\n type=\"number\"\n placeholder=\"100\"\n value={form.radius}\n onChange={(e) => setForm({ ...form, radius: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Trigger\n </label>\n <select\n value={form.trigger}\n onChange={(e) =>\n setForm({\n ...form,\n trigger: e.target.value as GeofenceTrigger,\n })\n }\n className={inputClass}\n >\n <option value={GeofenceTrigger.Enter}>On entry</option>\n <option value={GeofenceTrigger.Exit}>On exit</option>\n <option value={GeofenceTrigger.Both}>Entry & exit</option>\n </select>\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Address (optional)\n </label>\n <input\n type=\"text\"\n placeholder=\"Street address for reference\"\n value={form.address}\n onChange={(e) => setForm({ ...form, address: e.target.value })}\n className={inputClass}\n />\n </div>\n <label className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">\n Active\n </span>\n <input\n type=\"checkbox\"\n checked={form.isActive}\n onChange={(e) => setForm({ ...form, isActive: e.target.checked })}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n </label>\n </div>\n </Modal>\n\n <Modal\n open={confirmDelete !== null}\n onClose={() => setConfirmDelete(null)}\n title=\"Delete Geofence\"\n description=\"This action cannot be undone.\"\n footer={\n <>\n <button\n onClick={() => setConfirmDelete(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-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Are you sure you want to delete{\" \"}\n <span className=\"font-medium text-text-primary\">\n {confirmDelete?.name}\n </span>\n ? Any automatic tracking tied to this zone will stop.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GeofencingPage.js","names":[],"sources":["../../../../src/timecampus/pages/geofencing/GeofencingPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n MapPin,\n Plus,\n Pencil,\n Trash2,\n LogIn,\n LogOut,\n Radio,\n ShieldCheck,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n ChartCard,\n DonutChart,\n BarsChart,\n type ChartDatum,\n} from \"../../components\";\nimport { cn, formatRelativeTime } from \"../../utils/helpers\";\nimport {\n useListGeofencesQuery,\n useCreateGeofenceMutation,\n useUpdateGeofenceMutation,\n useDeleteGeofenceMutation,\n useToggleGeofenceMutation,\n type ListGeofencesQuery,\n} from \"@/generated/wspace-operations\";\nimport { GeofenceTrigger, GeofenceEventType } from \"@/generated/wspace-types\";\n\ntype GeofenceItem = NonNullable<\n ListGeofencesQuery[\"geofences\"]\n>[\"items\"][number];\n\nconst PAGE_SIZE = 25;\n\ninterface GeofenceFormState {\n name: string;\n latitude: string;\n longitude: string;\n radius: string;\n trigger: GeofenceTrigger;\n address: string;\n isActive: boolean;\n}\n\nconst emptyForm: GeofenceFormState = {\n name: \"\",\n latitude: \"\",\n longitude: \"\",\n radius: \"100\",\n trigger: GeofenceTrigger.Both,\n address: \"\",\n isActive: true,\n};\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\ninterface ToggleSwitchProps {\n checked: boolean;\n onChange: (next: boolean) => void;\n label: string;\n disabled?: boolean;\n}\n\n/**\n * Local, token-styled switch (shared components are off-limits here). The knob\n * translates only on the checked state — never on hover — per the repo\n * no-hover-movement mandate.\n */\nfunction ToggleSwitch({\n checked,\n onChange,\n label,\n disabled,\n}: ToggleSwitchProps) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={label}\n disabled={disabled}\n onClick={() => onChange(!checked)}\n className={cn(\n \"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border transition-colors disabled:cursor-not-allowed disabled:opacity-50\",\n checked\n ? \"border-transparent bg-action-primary-bg\"\n : \"border-border-subtle bg-bg-sunken\",\n )}\n >\n <span\n className={cn(\n \"inline-block h-3.5 w-3.5 transform rounded-full shadow-sm transition-transform\",\n checked\n ? \"translate-x-4 bg-action-primary-text\"\n : \"translate-x-0.5 bg-text-secondary\",\n )}\n />\n </button>\n );\n}\n\nexport function GeofencingPage() {\n const { data, loading, error, refetch, fetchMore } = useListGeofencesQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n });\n\n const [createGeofence, { loading: creating, error: createError }] =\n useCreateGeofenceMutation();\n const [updateGeofence, { loading: updating, error: updateError }] =\n useUpdateGeofenceMutation();\n const [deleteGeofence, { loading: deleting }] = useDeleteGeofenceMutation();\n const [toggleGeofence] = useToggleGeofenceMutation();\n\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<GeofenceItem | null>(null);\n const [form, setForm] = useState<GeofenceFormState>(emptyForm);\n const [confirmDelete, setConfirmDelete] = useState<GeofenceItem | null>(null);\n const [loadingMore, setLoadingMore] = useState(false);\n\n const geofences = data?.geofences?.items ?? [];\n const hasMore = data?.geofences?.hasMore ?? false;\n const totalZones = data?.geofences?.total ?? geofences.length;\n\n // ---- Derived, real-data summaries ---------------------------------------\n const activeZones = geofences.filter((f: GeofenceItem) => f.isActive).length;\n let enterEvents = 0;\n let exitEvents = 0;\n for (const fence of geofences as GeofenceItem[]) {\n for (const event of fence.events) {\n if (event.type === GeofenceEventType.Enter) enterEvents += 1;\n else exitEvents += 1;\n }\n }\n\n const triggerSlices = [\n {\n name: \"Entry & exit\",\n value: geofences.filter(\n (f: GeofenceItem) => f.trigger === GeofenceTrigger.Both,\n ).length,\n },\n {\n name: \"On entry\",\n value: geofences.filter(\n (f: GeofenceItem) => f.trigger === GeofenceTrigger.Enter,\n ).length,\n },\n {\n name: \"On exit\",\n value: geofences.filter(\n (f: GeofenceItem) => f.trigger === GeofenceTrigger.Exit,\n ).length,\n },\n ];\n\n const activityData: ChartDatum[] = geofences\n .map((f: GeofenceItem) => ({ zone: f.name, events: f.events.length }))\n .filter((d: { zone: string; events: number }) => d.events > 0)\n .sort((a: { events: number }, b: { events: number }) => b.events - a.events)\n .slice(0, 8);\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (fence: GeofenceItem) => {\n setEditing(fence);\n setForm({\n name: fence.name,\n latitude: String(fence.latitude),\n longitude: String(fence.longitude),\n radius: String(fence.radius),\n trigger: fence.trigger,\n address: fence.address ?? \"\",\n isActive: fence.isActive,\n });\n setFormOpen(true);\n };\n\n const handleSubmit = async () => {\n if (!form.name.trim()) return;\n const base = {\n name: form.name.trim(),\n latitude: Number(form.latitude),\n longitude: Number(form.longitude),\n radius: Number(form.radius),\n trigger: form.trigger,\n address: form.address.trim() || undefined,\n isActive: form.isActive,\n };\n if (editing) {\n await updateGeofence({ variables: { id: editing.id, input: base } });\n } else {\n await createGeofence({ variables: { input: base } });\n }\n setFormOpen(false);\n await refetch();\n };\n\n const handleDelete = async () => {\n if (!confirmDelete) return;\n await deleteGeofence({ variables: { id: confirmDelete.id } });\n setConfirmDelete(null);\n await refetch();\n };\n\n const handleToggle = async (fence: GeofenceItem) => {\n await toggleGeofence({\n variables: { id: fence.id, isActive: !fence.isActive },\n });\n await refetch();\n };\n\n /**\n * Enter/exit are edges of the single `trigger` enum. Recompute the enum from\n * the two switches and persist via updateGeofence. A zone must keep at least\n * one edge, so a change that would disable both is ignored.\n */\n const handleSetTrigger = async (\n fence: GeofenceItem,\n nextEnter: boolean,\n nextExit: boolean,\n ) => {\n if (!nextEnter && !nextExit) return;\n const trigger =\n nextEnter && nextExit\n ? GeofenceTrigger.Both\n : nextEnter\n ? GeofenceTrigger.Enter\n : GeofenceTrigger.Exit;\n if (trigger === fence.trigger) return;\n await updateGeofence({ variables: { id: fence.id, input: { trigger } } });\n await refetch();\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: {\n pagination: { limit: PAGE_SIZE, offset: geofences.length },\n },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n geofences: {\n ...fetchMoreResult.geofences,\n items: [\n ...prev.geofences.items,\n ...fetchMoreResult.geofences.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n const createButton = (\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 Create Geofence\n </button>\n );\n\n if (loading) {\n return (\n <PageLayout\n title=\"Geofencing\"\n description=\"Set up location-based triggers and zones\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Geofencing\"\n description=\"Set up location-based triggers and zones\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load geofences. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Geofencing\"\n description=\"Set up location-based triggers and zones\"\n purpose=\"Draw zones around your worksites — offices, client sites, job locations — and have time tracking start and stop automatically as people move in and out, so on-site hours record themselves.\"\n actions={createButton}\n >\n {geofences.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No geofences configured yet\"\n description=\"Create your first zone to trigger automatic time tracking when people arrive or leave a location.\"\n action={createButton}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Zones\"\n value={String(totalZones)}\n icon={<MapPin className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Active Zones\"\n value={String(activeZones)}\n icon={<Radio className=\"h-5 w-5\" />}\n description=\"Currently tracking\"\n />\n <StatCard\n title=\"Entry Events\"\n value={String(enterEvents)}\n icon={<LogIn className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Exit Events\"\n value={String(exitEvents)}\n icon={<LogOut className=\"h-5 w-5\" />}\n />\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title=\"Trigger configuration\"\n description=\"How your zones fire\"\n >\n <DonutChart\n data={triggerSlices}\n centerLabel=\"Zones\"\n centerValue={geofences.length}\n ariaLabel=\"Geofence trigger configuration mix\"\n emptyLabel=\"No zones to summarize yet\"\n />\n </ChartCard>\n <ChartCard\n title=\"Activity by zone\"\n description=\"Recorded enter/exit events per zone\"\n >\n <BarsChart\n data={activityData}\n xKey=\"zone\"\n orientation=\"horizontal\"\n series={[{ key: \"events\", label: \"Events\" }]}\n categoryWidth={120}\n ariaLabel=\"Recorded events by zone\"\n emptyLabel=\"No geofence events recorded yet\"\n />\n </ChartCard>\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3\">\n {geofences.map((fence: GeofenceItem) => {\n const enterOn =\n fence.trigger === GeofenceTrigger.Enter ||\n fence.trigger === GeofenceTrigger.Both;\n const exitOn =\n fence.trigger === GeofenceTrigger.Exit ||\n fence.trigger === GeofenceTrigger.Both;\n const fenceEnter = fence.events.filter(\n (e) => e.type === GeofenceEventType.Enter,\n ).length;\n const fenceExit = fence.events.length - fenceEnter;\n const lastTs = fence.events.reduce(\n (latest, e) => (e.timestamp > latest ? e.timestamp : latest),\n \"\",\n );\n return (\n <GlassCard\n key={fence.id}\n treatment=\"glass\"\n glow={fence.isActive}\n className=\"flex flex-col p-5\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-start gap-2\">\n <span className=\"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <MapPin className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {fence.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {fence.address ?? \"No address\"} ·{\" \"}\n {fence.radius}m radius\n </p>\n </div>\n </div>\n <div className=\"flex shrink-0 flex-col items-end gap-1\">\n <ToggleSwitch\n checked={fence.isActive}\n onChange={() => handleToggle(fence)}\n label={`Toggle ${fence.name} active`}\n />\n <span\n className={cn(\n \"text-xs font-medium\",\n fence.isActive\n ? \"text-status-success-text\"\n : \"text-text-muted\",\n )}\n >\n {fence.isActive ? \"Active\" : \"Inactive\"}\n </span>\n </div>\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-2\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle bg-bg-surface/60 px-3 py-2\">\n <span className=\"flex items-center gap-1.5 text-xs font-medium text-text-secondary\">\n <LogIn className=\"h-3.5 w-3.5\" />\n Enter\n </span>\n <ToggleSwitch\n checked={enterOn}\n onChange={(next) =>\n handleSetTrigger(fence, next, exitOn)\n }\n label={`Enter trigger for ${fence.name}`}\n />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle bg-bg-surface/60 px-3 py-2\">\n <span className=\"flex items-center gap-1.5 text-xs font-medium text-text-secondary\">\n <LogOut className=\"h-3.5 w-3.5\" />\n Exit\n </span>\n <ToggleSwitch\n checked={exitOn}\n onChange={(next) =>\n handleSetTrigger(fence, enterOn, next)\n }\n label={`Exit trigger for ${fence.name}`}\n />\n </div>\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <span>\n Entries{\" \"}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {fenceEnter}\n </span>\n </span>\n <span>\n Exits{\" \"}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {fenceExit}\n </span>\n </span>\n {lastTs ? (\n <span className=\"ml-auto\">\n Last{\" \"}\n <span className=\"text-text-primary\">\n {formatRelativeTime(lastTs)}\n </span>\n </span>\n ) : (\n <span className=\"ml-auto text-text-muted\">\n No events yet\n </span>\n )}\n </div>\n\n <div className=\"mt-4 flex items-center gap-2 border-t border-border-seam pt-3\">\n <button\n onClick={() => openEdit(fence)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n Edit\n </button>\n <button\n onClick={() => setConfirmDelete(fence)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n Delete\n </button>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n <GlassCard treatment=\"insight\" className=\"mt-6 p-4\">\n <div className=\"flex items-start gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <ShieldCheck className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n Privacy first\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Time is recorded only when someone enters or leaves a zone you\n define — never through continuous background tracking.\n Location data stays encrypted and visible only within your\n workspace.\n </p>\n </div>\n </div>\n </GlassCard>\n\n {hasMore && (\n <div className=\"mt-6 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n <Modal\n open={formOpen}\n onClose={() => setFormOpen(false)}\n title={editing ? \"Edit Geofence\" : \"Create Geofence\"}\n description=\"Define a zone that triggers automatic time tracking.\"\n size=\"lg\"\n footer={\n <>\n <button\n onClick={() => setFormOpen(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={handleSubmit}\n disabled={creating || updating || !form.name.trim()}\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 || updating\n ? \"Saving...\"\n : editing\n ? \"Save Changes\"\n : \"Create Geofence\"}\n </button>\n </>\n }\n >\n {(createError || 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 save geofence. Please check the values and 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 Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Head Office\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Latitude\n </label>\n <input\n type=\"number\"\n step=\"any\"\n placeholder=\"0.000000\"\n value={form.latitude}\n onChange={(e) => setForm({ ...form, latitude: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Longitude\n </label>\n <input\n type=\"number\"\n step=\"any\"\n placeholder=\"0.000000\"\n value={form.longitude}\n onChange={(e) =>\n setForm({ ...form, longitude: e.target.value })\n }\n className={inputClass}\n />\n </div>\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 Radius (meters)\n </label>\n <input\n type=\"number\"\n placeholder=\"100\"\n value={form.radius}\n onChange={(e) => setForm({ ...form, radius: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Trigger\n </label>\n <select\n value={form.trigger}\n onChange={(e) =>\n setForm({\n ...form,\n trigger: e.target.value as GeofenceTrigger,\n })\n }\n className={inputClass}\n >\n <option value={GeofenceTrigger.Enter}>On entry</option>\n <option value={GeofenceTrigger.Exit}>On exit</option>\n <option value={GeofenceTrigger.Both}>Entry & exit</option>\n </select>\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Address (optional)\n </label>\n <input\n type=\"text\"\n placeholder=\"Street address for reference\"\n value={form.address}\n onChange={(e) => setForm({ ...form, address: e.target.value })}\n className={inputClass}\n />\n </div>\n <label className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">\n Active\n </span>\n <input\n type=\"checkbox\"\n checked={form.isActive}\n onChange={(e) => setForm({ ...form, isActive: e.target.checked })}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n </label>\n </div>\n </Modal>\n\n <Modal\n open={confirmDelete !== null}\n onClose={() => setConfirmDelete(null)}\n title=\"Delete Geofence\"\n description=\"This action cannot be undone.\"\n footer={\n <>\n <button\n onClick={() => setConfirmDelete(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-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Are you sure you want to delete{\" \"}\n <span className=\"font-medium text-text-primary\">\n {confirmDelete?.name}\n </span>\n ? Any automatic tracking tied to this zone will stop.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocationTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/location-tracking/LocationTrackingPage.tsx"],"names":[],"mappings":"AAqDA,wBAAgB,oBAAoB,
|
|
1
|
+
{"version":3,"file":"LocationTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/location-tracking/LocationTrackingPage.tsx"],"names":[],"mappings":"AAqDA,wBAAgB,oBAAoB,gCA+cnC"}
|
|
@@ -24,10 +24,10 @@ var w = 25, T = "mt-1 w-full rounded-md border border-border-subtle bg-bg-surfac
|
|
|
24
24
|
endTime: ""
|
|
25
25
|
};
|
|
26
26
|
function D() {
|
|
27
|
-
let { currentUser: D } = e(), { data: O, loading: k, error: A, refetch: j, fetchMore: ie } = c({ variables: {
|
|
27
|
+
let { currentUser: D } = e(), { data: O, loading: k, error: A, refetch: j, fetchMore: ie } = c({ variables: { pagination: {
|
|
28
28
|
limit: w,
|
|
29
29
|
offset: 0
|
|
30
|
-
} }), { data: ae, loading: oe, error: M, refetch: N } = ee(), [P, { loading: F, error: I }] = o(), [L, { loading: R }] = s(), [z, B] = h(!1), [V, H] = h(E), [U, W] = h(null), [G, K] = h(!1), q = O?.locationEntries?.items ?? [], se = O?.locationEntries?.hasMore ?? !1, J = ae?.locationStats, Y = q.filter((e) => e.geofenceId).length, ce = q.length - Y, le = [{
|
|
30
|
+
} } }), { data: ae, loading: oe, error: M, refetch: N } = ee(), [P, { loading: F, error: I }] = o(), [L, { loading: R }] = s(), [z, B] = h(!1), [V, H] = h(E), [U, W] = h(null), [G, K] = h(!1), q = O?.locationEntries?.items ?? [], se = O?.locationEntries?.hasMore ?? !1, J = ae?.locationStats, Y = q.filter((e) => e.geofenceId).length, ce = q.length - Y, le = [{
|
|
31
31
|
name: "Geofenced",
|
|
32
32
|
value: Y
|
|
33
33
|
}, {
|
|
@@ -66,10 +66,10 @@ function D() {
|
|
|
66
66
|
K(!0);
|
|
67
67
|
try {
|
|
68
68
|
await ie({
|
|
69
|
-
variables: {
|
|
69
|
+
variables: { pagination: {
|
|
70
70
|
limit: w,
|
|
71
71
|
offset: q.length
|
|
72
|
-
},
|
|
72
|
+
} },
|
|
73
73
|
updateQuery: (e, { fetchMoreResult: t }) => t ? { locationEntries: {
|
|
74
74
|
...t.locationEntries,
|
|
75
75
|
items: [...e.locationEntries.items, ...t.locationEntries.items]
|