@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.
Files changed (69) hide show
  1. package/dist/generated/wspace-operations.d.ts +193 -76
  2. package/dist/generated/wspace-operations.d.ts.map +1 -1
  3. package/dist/generated/wspace-operations.js +859 -788
  4. package/dist/generated/wspace-operations.js.map +1 -1
  5. package/dist/generated/wspace-types.d.ts +56 -38
  6. package/dist/generated/wspace-types.d.ts.map +1 -1
  7. package/dist/generated/wspace-types.js.map +1 -1
  8. package/dist/index.js +5 -5
  9. package/dist/timecampus/pages/HomePage.js +1 -1
  10. package/dist/timecampus/pages/HomePage.js.map +1 -1
  11. package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +6 -3
  12. package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
  13. package/dist/timecampus/pages/client-portal/ClientPortalPage.js +4 -4
  14. package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
  15. package/dist/timecampus/pages/email-tracking/EmailTrackingPage.d.ts.map +1 -1
  16. package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +8 -3
  17. package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js.map +1 -1
  18. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.d.ts.map +1 -1
  19. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +19 -13
  20. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
  21. package/dist/timecampus/pages/focus-score/FocusScorePage.js +1 -1
  22. package/dist/timecampus/pages/focus-score/FocusScorePage.js.map +1 -1
  23. package/dist/timecampus/pages/geofencing/GeofencingPage.d.ts.map +1 -1
  24. package/dist/timecampus/pages/geofencing/GeofencingPage.js +4 -4
  25. package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
  26. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.d.ts.map +1 -1
  27. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +4 -4
  28. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
  29. package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.d.ts.map +1 -1
  30. package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js +4 -2
  31. package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js.map +1 -1
  32. package/dist/timecampus/pages/meetings/MeetingsPage.d.ts.map +1 -1
  33. package/dist/timecampus/pages/meetings/MeetingsPage.js +6 -4
  34. package/dist/timecampus/pages/meetings/MeetingsPage.js.map +1 -1
  35. package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js +1 -1
  36. package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js.map +1 -1
  37. package/dist/timecampus/pages/projects/ProjectsPage.js +2 -2
  38. package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
  39. package/dist/timecampus/pages/projects/TasksPage.js +4 -2
  40. package/dist/timecampus/pages/projects/TasksPage.js.map +1 -1
  41. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.d.ts.map +1 -1
  42. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +4 -4
  43. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
  44. package/dist/timecampus/pages/team/TeamAnalyticsPage.d.ts.map +1 -1
  45. package/dist/timecampus/pages/team/TeamAnalyticsPage.js +2 -2
  46. package/dist/timecampus/pages/team/TeamAnalyticsPage.js.map +1 -1
  47. package/dist/timecampus/pages/team/TeamMembersPage.js +1 -1
  48. package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
  49. package/dist/timecampus/pages/team-activity/TeamActivityPage.d.ts.map +1 -1
  50. package/dist/timecampus/pages/team-activity/TeamActivityPage.js +49 -45
  51. package/dist/timecampus/pages/team-activity/TeamActivityPage.js.map +1 -1
  52. package/dist/timecampus/pages/time/TimeEntriesPage.d.ts.map +1 -1
  53. package/dist/timecampus/pages/time/TimeEntriesPage.js +137 -132
  54. package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
  55. package/dist/timecampus/pages/time/TimeTrackingPage.d.ts.map +1 -1
  56. package/dist/timecampus/pages/time/TimeTrackingPage.js +2 -2
  57. package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
  58. package/dist/timecampus/pages/time/TimerPage.d.ts.map +1 -1
  59. package/dist/timecampus/pages/time/TimerPage.js +2 -2
  60. package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
  61. package/dist/timecampus/pages/time-approval/TimeApprovalPage.d.ts.map +1 -1
  62. package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +4 -4
  63. package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
  64. package/dist/timecampus/pages/timeline/TimelinePage.d.ts.map +1 -1
  65. package/dist/timecampus/pages/timeline/TimelinePage.js +224 -162
  66. package/dist/timecampus/pages/timeline/TimelinePage.js.map +1 -1
  67. package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +1 -1
  68. package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
  69. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ClientPortalPage.js","names":[],"sources":["../../../../src/timecampus/pages/client-portal/ClientPortalPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n Building2,\n Plus,\n Users,\n PowerOff,\n FolderOpen,\n Link2,\n Check,\n RefreshCw,\n Pencil,\n Trash2,\n Power,\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 { StatusBadge } from \"../../components/StatusBadge\";\nimport { Modal } from \"../../components/Modal\";\nimport { ToastContainer, useToast } from \"../../components/Toast\";\nimport {\n ChartCard,\n BarsChart,\n DonutChart,\n ProgressMeter,\n} from \"../../components\";\nimport { getInitials, cn, formatDate, formatNumber } from \"../../utils/helpers\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport {\n useListClientPortalConfigsQuery,\n useGetClientPortalStatsQuery,\n useRegenerateClientPortalShareTokenMutation,\n useCreateClientPortalConfigMutation,\n useUpdateClientPortalConfigMutation,\n useDeleteClientPortalConfigMutation,\n useToggleClientPortalMutation,\n type ClientPortalFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 25;\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\nconst fileInputClass =\n \"mt-1 block w-full text-sm text-text-secondary file:mr-3 file:rounded-md file:border-0 file:bg-action-primary-bg file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-action-primary-text hover:file:bg-action-primary-bg/90\";\n\n/**\n * Resolve a downloadable URL for a Files object. The URL is presigned and\n * expires after 7 days (604800s) — a known limitation: a stored logoUrl will\n * eventually 403 and the logo must be re-uploaded to refresh it.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampus_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).\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusClient_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\n/**\n * Build the public share URL for a portal token. The public client view is\n * served by the website at /portal/:token; only active portals resolve.\n */\nfunction shareUrlFor(token: string): string {\n const origin = typeof window !== \"undefined\" ? window.location.origin : \"\";\n return `${origin}/portal/${token}`;\n}\n\ninterface ClientFormState {\n clientName: string;\n contactEmail: string;\n contactPhone: string;\n description: string;\n logoUrl: string;\n}\n\nconst emptyForm: ClientFormState = {\n clientName: \"\",\n contactEmail: \"\",\n contactPhone: \"\",\n description: \"\",\n logoUrl: \"\",\n};\n\nexport function ClientPortalPage() {\n const { toasts, addToast, dismissToast } = useToast();\n const apollo = useApolloClient();\n const [copiedId, setCopiedId] = useState<string | null>(null);\n const [logoUploading, setLogoUploading] = useState(false);\n\n const {\n data: configsData,\n loading: configsLoading,\n error: configsError,\n refetch,\n fetchMore,\n } = useListClientPortalConfigsQuery({\n variables: { limit: PAGE_SIZE, offset: 0 },\n notifyOnNetworkStatusChange: true,\n });\n const { data: statsData, refetch: refetchStats } =\n useGetClientPortalStatsQuery();\n\n const [regenerate, { loading: regenerating }] =\n useRegenerateClientPortalShareTokenMutation();\n const [createClient, { loading: creating }] =\n useCreateClientPortalConfigMutation();\n const [updateClient, { loading: updating }] =\n useUpdateClientPortalConfigMutation();\n const [deleteClient, { loading: deleting }] =\n useDeleteClientPortalConfigMutation();\n const [toggleClient] = useToggleClientPortalMutation();\n\n const clients = configsData?.clientPortalConfigs?.items ?? [];\n const hasMore = configsData?.clientPortalConfigs?.hasMore ?? false;\n const stats = statsData?.clientPortalStats;\n\n const trackedHoursFor = (client: ClientPortalFieldsFragment): number =>\n client.projects.reduce((sum, p) => sum + p.trackedHours, 0);\n\n const inactiveClients =\n (stats?.totalClients ?? 0) - (stats?.activeClients ?? 0);\n\n // Real, already-fetched data only (loaded page of clients + workspace stats).\n const clientHoursData: Array<{ name: string; hours: number }> = clients\n .map((client: ClientPortalFieldsFragment) => ({\n name: client.clientName,\n hours: trackedHoursFor(client),\n }))\n .filter((entry: { name: string; hours: number }) => entry.hours > 0)\n .sort((a: { hours: number }, b: { hours: number }) => b.hours - a.hours)\n .slice(0, 8);\n\n const maxTrackedHours = clients.reduce(\n (max: number, client: ClientPortalFieldsFragment) =>\n Math.max(max, trackedHoursFor(client)),\n 0,\n );\n\n const statusMix = [\n { name: \"Active\", value: stats?.activeClients ?? 0 },\n { name: \"Inactive\", value: Math.max(inactiveClients, 0) },\n ];\n\n const [loadingMore, setLoadingMore] = useState(false);\n\n // Create / edit modal\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<ClientPortalFieldsFragment | null>(\n null,\n );\n const [form, setForm] = useState<ClientFormState>(emptyForm);\n\n // Delete confirm\n const [deleteTarget, setDeleteTarget] =\n useState<ClientPortalFieldsFragment | null>(null);\n\n // Per-row busy (toggle/regenerate)\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const handleCopy = async (client: ClientPortalFieldsFragment) => {\n if (!client.shareToken) return;\n try {\n await navigator.clipboard.writeText(shareUrlFor(client.shareToken));\n setCopiedId(client.id);\n window.setTimeout(() => setCopiedId(null), 2000);\n } catch {\n addToast({ title: \"Clipboard unavailable\", variant: \"error\" });\n }\n };\n\n const handleRegenerate = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await regenerate({ variables: { id: client.id } });\n addToast({ title: \"Share link regenerated\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Failed to regenerate link\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (client: ClientPortalFieldsFragment) => {\n setEditing(client);\n setForm({\n clientName: client.clientName,\n contactEmail: client.contactEmail ?? \"\",\n contactPhone: client.contactPhone ?? \"\",\n description: client.description ?? \"\",\n logoUrl: client.logoUrl ?? \"\",\n });\n setFormOpen(true);\n };\n\n /**\n * Upload a client logo to platform Files and store its presigned URL on the\n * form. Failures surface a toast but never block saving the client — the\n * logo is optional.\n */\n const handleLogoUpload = async (file: File) => {\n setLogoUploading(true);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/ClientLogos\" },\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: 604800 },\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, logoUrl: url }));\n } catch {\n addToast({ title: \"Failed to upload logo\", variant: \"error\" });\n } finally {\n setLogoUploading(false);\n }\n };\n\n const closeForm = () => {\n setFormOpen(false);\n setEditing(null);\n };\n\n const handleSubmit = async () => {\n if (!form.clientName.trim()) return;\n try {\n if (editing) {\n await updateClient({\n variables: {\n id: editing.id,\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n },\n },\n });\n addToast({ title: \"Client updated\", variant: \"success\" });\n } else {\n await createClient({\n variables: {\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n isActive: true,\n },\n },\n });\n addToast({ title: \"Client added\", variant: \"success\" });\n }\n closeForm();\n await refreshAll();\n } catch {\n addToast({\n title: editing ? \"Failed to update client\" : \"Failed to add client\",\n variant: \"error\",\n });\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteClient({ variables: { id: deleteTarget.id } });\n addToast({ title: \"Client deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to delete client\", variant: \"error\" });\n }\n };\n\n const handleToggle = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await toggleClient({\n variables: { id: client.id, isActive: !client.isActive },\n });\n addToast({\n title: client.isActive ? \"Client deactivated\" : \"Client activated\",\n variant: \"success\",\n });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to toggle client\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { limit: PAGE_SIZE, offset: clients.length },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n clientPortalConfigs: {\n ...fetchMoreResult.clientPortalConfigs,\n items: [\n ...prev.clientPortalConfigs.items,\n ...fetchMoreResult.clientPortalConfigs.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (configsLoading && clients.length === 0) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (configsError) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\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 client portal 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 Client\n </button>\n );\n\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n purpose=\"Give clients a transparent window into the work you do for them — the projects, hours and progress tied to each one — so billing conversations are backed by data and trust, not spreadsheets.\"\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=\"Active Clients\"\n value={String(stats?.activeClients ?? 0)}\n icon={<Building2 className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Projects\"\n value={String(stats?.totalProjectsShared ?? 0)}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Clients\"\n value={String(stats?.totalClients ?? 0)}\n icon={<Users className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Inactive\"\n value={String(Math.max(inactiveClients, 0))}\n icon={<PowerOff className=\"h-5 w-5\" />}\n iconBgColor=\"bg-bg-sunken text-text-secondary\"\n description=\"portals turned off\"\n />\n </div>\n\n {clients.length === 0 ? (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No clients configured yet\"\n description=\"Add your first client to share projects and tracked hours with them.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <ChartCard\n title=\"Tracked hours by client\"\n description=\"Top clients by logged hours (loaded page)\"\n className=\"lg:col-span-2\"\n >\n <BarsChart\n data={clientHoursData}\n xKey=\"name\"\n series={[{ key: \"hours\", label: \"Tracked hours\" }]}\n orientation=\"horizontal\"\n height={260}\n categoryWidth={120}\n valueFormat={(value) => `${value.toLocaleString()}h`}\n emptyLabel=\"No tracked hours logged yet.\"\n ariaLabel=\"Tracked hours by client\"\n />\n </ChartCard>\n <ChartCard\n title=\"Portal status\"\n description=\"Shared vs. private clients\"\n >\n <DonutChart\n data={statusMix}\n centerLabel=\"Clients\"\n centerValue={stats?.totalClients ?? 0}\n height={260}\n emptyLabel=\"No clients yet.\"\n ariaLabel=\"Clients by portal status\"\n />\n </ChartCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {clients.map((client: ClientPortalFieldsFragment) => {\n const totalTrackedHours = trackedHoursFor(client);\n const rowBusy = busyId === client.id;\n const copied = copiedId === client.id;\n return (\n <GlassCard\n key={client.id}\n treatment=\"insight\"\n glow={client.isActive}\n className=\"p-4\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n {client.logoUrl ? (\n <img\n src={client.logoUrl}\n alt={`${client.clientName} logo`}\n className=\"h-11 w-11 shrink-0 rounded-lg border border-border-seam object-cover\"\n />\n ) : (\n <div className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-sm font-semibold text-primary\">\n {getInitials(client.clientName)}\n </div>\n )}\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {client.clientName}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n Since {formatDate(client.createdAt)}\n </p>\n </div>\n </div>\n <StatusBadge\n label={client.isActive ? \"active\" : \"inactive\"}\n status={client.isActive ? \"active\" : \"inactive\"}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Projects\n </p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {client.projects.length}\n </p>\n </div>\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Tracked hours\n </p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {formatNumber(totalTrackedHours)}h\n </p>\n </div>\n </div>\n\n {maxTrackedHours > 0 && (\n <div className=\"mt-3\">\n <ProgressMeter\n value={totalTrackedHours}\n max={maxTrackedHours}\n tone=\"success\"\n size=\"sm\"\n showValue={false}\n label=\"Tracked volume vs. top client\"\n />\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <span className=\"text-xs font-medium text-text-secondary\">\n Public Access\n </span>\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => void handleToggle(client)}\n title={\n client.isActive\n ? \"Deactivate client portal\"\n : \"Activate client portal\"\n }\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors disabled:opacity-50\",\n client.isActive\n ? \"bg-status-success-bg text-status-success-text hover:bg-status-success-bg/80\"\n : \"border border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken/60\",\n )}\n >\n <Power className=\"h-3.5 w-3.5\" />\n {client.isActive ? \"Active\" : \"Inactive\"}\n </button>\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-2 border-t border-border-seam pt-3\">\n {client.isActive && client.shareToken && (\n <>\n <button\n type=\"button\"\n onClick={() => void handleCopy(client)}\n title=\"Copy the client's private share link\"\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/60\"\n >\n {copied ? (\n <>\n <Check className=\"h-3.5 w-3.5\" /> Copied\n </>\n ) : (\n <>\n <Link2 className=\"h-3.5 w-3.5\" /> Share link\n </>\n )}\n </button>\n <button\n type=\"button\"\n disabled={rowBusy && regenerating}\n onClick={() => void handleRegenerate(client)}\n title=\"Regenerate the link (invalidates the old one)\"\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/60 disabled:opacity-50\"\n >\n <RefreshCw\n className={cn(\n \"h-3.5 w-3.5\",\n rowBusy && regenerating && \"animate-spin\",\n )}\n />\n Regenerate\n </button>\n </>\n )}\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => openEdit(client)}\n title=\"Edit client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/60\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(client)}\n title=\"Delete client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {hasMore && (\n <div className=\"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 Client\" : \"Add Client\"}\n description={\n editing\n ? \"Update this client's portal details.\"\n : \"Set up a client so you can share projects and tracked hours with them.\"\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={creating || updating || !form.clientName.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 : \"Add Client\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Client Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Acme Corp\"\n value={form.clientName}\n onChange={(e) => setForm({ ...form, clientName: 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 Contact Email\n </label>\n <input\n type=\"email\"\n placeholder=\"contact@client.com\"\n value={form.contactEmail}\n onChange={(e) =>\n setForm({ ...form, contactEmail: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Phone\n </label>\n <input\n type=\"tel\"\n placeholder=\"Optional\"\n value={form.contactPhone}\n onChange={(e) =>\n setForm({ ...form, contactPhone: e.target.value })\n }\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Description\n </label>\n <textarea\n rows={3}\n placeholder=\"Notes about this client (optional)\"\n value={form.description}\n onChange={(e) =>\n setForm({ ...form, description: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Logo\n </label>\n <input\n type=\"file\"\n accept=\"image/*\"\n disabled={logoUploading}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleLogoUpload(file);\n // Reset so re-selecting the same file re-triggers onChange.\n e.target.value = \"\";\n }}\n className={fileInputClass}\n />\n {logoUploading && (\n <p className=\"mt-1 text-xs text-text-secondary\">Uploading…</p>\n )}\n {form.logoUrl && (\n <img\n src={form.logoUrl}\n alt=\"Client logo preview\"\n className=\"mt-2 h-16 w-16 rounded-md border border-border-seam object-cover\"\n />\n )}\n </div>\n </div>\n </Modal>\n\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete client?\"\n description={\n deleteTarget\n ? `\"${deleteTarget.clientName}\" and its share link 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 The client will lose access to their shared portal immediately. This\n cannot be undone.\n </p>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ClientPortalPage.js","names":[],"sources":["../../../../src/timecampus/pages/client-portal/ClientPortalPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n Building2,\n Plus,\n Users,\n PowerOff,\n FolderOpen,\n Link2,\n Check,\n RefreshCw,\n Pencil,\n Trash2,\n Power,\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 { StatusBadge } from \"../../components/StatusBadge\";\nimport { Modal } from \"../../components/Modal\";\nimport { ToastContainer, useToast } from \"../../components/Toast\";\nimport {\n ChartCard,\n BarsChart,\n DonutChart,\n ProgressMeter,\n} from \"../../components\";\nimport { getInitials, cn, formatDate, formatNumber } from \"../../utils/helpers\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport {\n useListClientPortalConfigsQuery,\n useGetClientPortalStatsQuery,\n useRegenerateClientPortalShareTokenMutation,\n useCreateClientPortalConfigMutation,\n useUpdateClientPortalConfigMutation,\n useDeleteClientPortalConfigMutation,\n useToggleClientPortalMutation,\n type ClientPortalFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 25;\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\nconst fileInputClass =\n \"mt-1 block w-full text-sm text-text-secondary file:mr-3 file:rounded-md file:border-0 file:bg-action-primary-bg file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-action-primary-text hover:file:bg-action-primary-bg/90\";\n\n/**\n * Resolve a downloadable URL for a Files object. The URL is presigned and\n * expires after 7 days (604800s) — a known limitation: a stored logoUrl will\n * eventually 403 and the logo must be re-uploaded to refresh it.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampus_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).\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusClient_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\n/**\n * Build the public share URL for a portal token. The public client view is\n * served by the website at /portal/:token; only active portals resolve.\n */\nfunction shareUrlFor(token: string): string {\n const origin = typeof window !== \"undefined\" ? window.location.origin : \"\";\n return `${origin}/portal/${token}`;\n}\n\ninterface ClientFormState {\n clientName: string;\n contactEmail: string;\n contactPhone: string;\n description: string;\n logoUrl: string;\n}\n\nconst emptyForm: ClientFormState = {\n clientName: \"\",\n contactEmail: \"\",\n contactPhone: \"\",\n description: \"\",\n logoUrl: \"\",\n};\n\nexport function ClientPortalPage() {\n const { toasts, addToast, dismissToast } = useToast();\n const apollo = useApolloClient();\n const [copiedId, setCopiedId] = useState<string | null>(null);\n const [logoUploading, setLogoUploading] = useState(false);\n\n const {\n data: configsData,\n loading: configsLoading,\n error: configsError,\n refetch,\n fetchMore,\n } = useListClientPortalConfigsQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n notifyOnNetworkStatusChange: true,\n });\n const { data: statsData, refetch: refetchStats } =\n useGetClientPortalStatsQuery();\n\n const [regenerate, { loading: regenerating }] =\n useRegenerateClientPortalShareTokenMutation();\n const [createClient, { loading: creating }] =\n useCreateClientPortalConfigMutation();\n const [updateClient, { loading: updating }] =\n useUpdateClientPortalConfigMutation();\n const [deleteClient, { loading: deleting }] =\n useDeleteClientPortalConfigMutation();\n const [toggleClient] = useToggleClientPortalMutation();\n\n const clients = configsData?.clientPortalConfigs?.items ?? [];\n const hasMore = configsData?.clientPortalConfigs?.hasMore ?? false;\n const stats = statsData?.clientPortalStats;\n\n const trackedHoursFor = (client: ClientPortalFieldsFragment): number =>\n client.projects.reduce((sum, p) => sum + p.trackedHours, 0);\n\n const inactiveClients =\n (stats?.totalClients ?? 0) - (stats?.activeClients ?? 0);\n\n // Real, already-fetched data only (loaded page of clients + workspace stats).\n const clientHoursData: Array<{ name: string; hours: number }> = clients\n .map((client: ClientPortalFieldsFragment) => ({\n name: client.clientName,\n hours: trackedHoursFor(client),\n }))\n .filter((entry: { name: string; hours: number }) => entry.hours > 0)\n .sort((a: { hours: number }, b: { hours: number }) => b.hours - a.hours)\n .slice(0, 8);\n\n const maxTrackedHours = clients.reduce(\n (max: number, client: ClientPortalFieldsFragment) =>\n Math.max(max, trackedHoursFor(client)),\n 0,\n );\n\n const statusMix = [\n { name: \"Active\", value: stats?.activeClients ?? 0 },\n { name: \"Inactive\", value: Math.max(inactiveClients, 0) },\n ];\n\n const [loadingMore, setLoadingMore] = useState(false);\n\n // Create / edit modal\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<ClientPortalFieldsFragment | null>(\n null,\n );\n const [form, setForm] = useState<ClientFormState>(emptyForm);\n\n // Delete confirm\n const [deleteTarget, setDeleteTarget] =\n useState<ClientPortalFieldsFragment | null>(null);\n\n // Per-row busy (toggle/regenerate)\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const handleCopy = async (client: ClientPortalFieldsFragment) => {\n if (!client.shareToken) return;\n try {\n await navigator.clipboard.writeText(shareUrlFor(client.shareToken));\n setCopiedId(client.id);\n window.setTimeout(() => setCopiedId(null), 2000);\n } catch {\n addToast({ title: \"Clipboard unavailable\", variant: \"error\" });\n }\n };\n\n const handleRegenerate = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await regenerate({ variables: { id: client.id } });\n addToast({ title: \"Share link regenerated\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Failed to regenerate link\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (client: ClientPortalFieldsFragment) => {\n setEditing(client);\n setForm({\n clientName: client.clientName,\n contactEmail: client.contactEmail ?? \"\",\n contactPhone: client.contactPhone ?? \"\",\n description: client.description ?? \"\",\n logoUrl: client.logoUrl ?? \"\",\n });\n setFormOpen(true);\n };\n\n /**\n * Upload a client logo to platform Files and store its presigned URL on the\n * form. Failures surface a toast but never block saving the client — the\n * logo is optional.\n */\n const handleLogoUpload = async (file: File) => {\n setLogoUploading(true);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/ClientLogos\" },\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: 604800 },\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, logoUrl: url }));\n } catch {\n addToast({ title: \"Failed to upload logo\", variant: \"error\" });\n } finally {\n setLogoUploading(false);\n }\n };\n\n const closeForm = () => {\n setFormOpen(false);\n setEditing(null);\n };\n\n const handleSubmit = async () => {\n if (!form.clientName.trim()) return;\n try {\n if (editing) {\n await updateClient({\n variables: {\n id: editing.id,\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n },\n },\n });\n addToast({ title: \"Client updated\", variant: \"success\" });\n } else {\n await createClient({\n variables: {\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n isActive: true,\n },\n },\n });\n addToast({ title: \"Client added\", variant: \"success\" });\n }\n closeForm();\n await refreshAll();\n } catch {\n addToast({\n title: editing ? \"Failed to update client\" : \"Failed to add client\",\n variant: \"error\",\n });\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteClient({ variables: { id: deleteTarget.id } });\n addToast({ title: \"Client deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to delete client\", variant: \"error\" });\n }\n };\n\n const handleToggle = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await toggleClient({\n variables: { id: client.id, isActive: !client.isActive },\n });\n addToast({\n title: client.isActive ? \"Client deactivated\" : \"Client activated\",\n variant: \"success\",\n });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to toggle client\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { limit: PAGE_SIZE, offset: clients.length } },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n clientPortalConfigs: {\n ...fetchMoreResult.clientPortalConfigs,\n items: [\n ...prev.clientPortalConfigs.items,\n ...fetchMoreResult.clientPortalConfigs.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (configsLoading && clients.length === 0) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (configsError) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\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 client portal 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 Client\n </button>\n );\n\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n purpose=\"Give clients a transparent window into the work you do for them — the projects, hours and progress tied to each one — so billing conversations are backed by data and trust, not spreadsheets.\"\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=\"Active Clients\"\n value={String(stats?.activeClients ?? 0)}\n icon={<Building2 className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Projects\"\n value={String(stats?.totalProjectsShared ?? 0)}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Clients\"\n value={String(stats?.totalClients ?? 0)}\n icon={<Users className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Inactive\"\n value={String(Math.max(inactiveClients, 0))}\n icon={<PowerOff className=\"h-5 w-5\" />}\n iconBgColor=\"bg-bg-sunken text-text-secondary\"\n description=\"portals turned off\"\n />\n </div>\n\n {clients.length === 0 ? (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No clients configured yet\"\n description=\"Add your first client to share projects and tracked hours with them.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <ChartCard\n title=\"Tracked hours by client\"\n description=\"Top clients by logged hours (loaded page)\"\n className=\"lg:col-span-2\"\n >\n <BarsChart\n data={clientHoursData}\n xKey=\"name\"\n series={[{ key: \"hours\", label: \"Tracked hours\" }]}\n orientation=\"horizontal\"\n height={260}\n categoryWidth={120}\n valueFormat={(value) => `${value.toLocaleString()}h`}\n emptyLabel=\"No tracked hours logged yet.\"\n ariaLabel=\"Tracked hours by client\"\n />\n </ChartCard>\n <ChartCard\n title=\"Portal status\"\n description=\"Shared vs. private clients\"\n >\n <DonutChart\n data={statusMix}\n centerLabel=\"Clients\"\n centerValue={stats?.totalClients ?? 0}\n height={260}\n emptyLabel=\"No clients yet.\"\n ariaLabel=\"Clients by portal status\"\n />\n </ChartCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {clients.map((client: ClientPortalFieldsFragment) => {\n const totalTrackedHours = trackedHoursFor(client);\n const rowBusy = busyId === client.id;\n const copied = copiedId === client.id;\n return (\n <GlassCard\n key={client.id}\n treatment=\"insight\"\n glow={client.isActive}\n className=\"p-4\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n {client.logoUrl ? (\n <img\n src={client.logoUrl}\n alt={`${client.clientName} logo`}\n className=\"h-11 w-11 shrink-0 rounded-lg border border-border-seam object-cover\"\n />\n ) : (\n <div className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-sm font-semibold text-primary\">\n {getInitials(client.clientName)}\n </div>\n )}\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {client.clientName}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n Since {formatDate(client.createdAt)}\n </p>\n </div>\n </div>\n <StatusBadge\n label={client.isActive ? \"active\" : \"inactive\"}\n status={client.isActive ? \"active\" : \"inactive\"}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Projects\n </p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {client.projects.length}\n </p>\n </div>\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Tracked hours\n </p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {formatNumber(totalTrackedHours)}h\n </p>\n </div>\n </div>\n\n {maxTrackedHours > 0 && (\n <div className=\"mt-3\">\n <ProgressMeter\n value={totalTrackedHours}\n max={maxTrackedHours}\n tone=\"success\"\n size=\"sm\"\n showValue={false}\n label=\"Tracked volume vs. top client\"\n />\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <span className=\"text-xs font-medium text-text-secondary\">\n Public Access\n </span>\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => void handleToggle(client)}\n title={\n client.isActive\n ? \"Deactivate client portal\"\n : \"Activate client portal\"\n }\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors disabled:opacity-50\",\n client.isActive\n ? \"bg-status-success-bg text-status-success-text hover:bg-status-success-bg/80\"\n : \"border border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken/60\",\n )}\n >\n <Power className=\"h-3.5 w-3.5\" />\n {client.isActive ? \"Active\" : \"Inactive\"}\n </button>\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-2 border-t border-border-seam pt-3\">\n {client.isActive && client.shareToken && (\n <>\n <button\n type=\"button\"\n onClick={() => void handleCopy(client)}\n title=\"Copy the client's private share link\"\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/60\"\n >\n {copied ? (\n <>\n <Check className=\"h-3.5 w-3.5\" /> Copied\n </>\n ) : (\n <>\n <Link2 className=\"h-3.5 w-3.5\" /> Share link\n </>\n )}\n </button>\n <button\n type=\"button\"\n disabled={rowBusy && regenerating}\n onClick={() => void handleRegenerate(client)}\n title=\"Regenerate the link (invalidates the old one)\"\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/60 disabled:opacity-50\"\n >\n <RefreshCw\n className={cn(\n \"h-3.5 w-3.5\",\n rowBusy && regenerating && \"animate-spin\",\n )}\n />\n Regenerate\n </button>\n </>\n )}\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => openEdit(client)}\n title=\"Edit client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/60\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(client)}\n title=\"Delete client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {hasMore && (\n <div className=\"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 Client\" : \"Add Client\"}\n description={\n editing\n ? \"Update this client's portal details.\"\n : \"Set up a client so you can share projects and tracked hours with them.\"\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={creating || updating || !form.clientName.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 : \"Add Client\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Client Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Acme Corp\"\n value={form.clientName}\n onChange={(e) => setForm({ ...form, clientName: 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 Contact Email\n </label>\n <input\n type=\"email\"\n placeholder=\"contact@client.com\"\n value={form.contactEmail}\n onChange={(e) =>\n setForm({ ...form, contactEmail: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Phone\n </label>\n <input\n type=\"tel\"\n placeholder=\"Optional\"\n value={form.contactPhone}\n onChange={(e) =>\n setForm({ ...form, contactPhone: e.target.value })\n }\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Description\n </label>\n <textarea\n rows={3}\n placeholder=\"Notes about this client (optional)\"\n value={form.description}\n onChange={(e) =>\n setForm({ ...form, description: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Logo\n </label>\n <input\n type=\"file\"\n accept=\"image/*\"\n disabled={logoUploading}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleLogoUpload(file);\n // Reset so re-selecting the same file re-triggers onChange.\n e.target.value = \"\";\n }}\n className={fileInputClass}\n />\n {logoUploading && (\n <p className=\"mt-1 text-xs text-text-secondary\">Uploading…</p>\n )}\n {form.logoUrl && (\n <img\n src={form.logoUrl}\n alt=\"Client logo preview\"\n className=\"mt-2 h-16 w-16 rounded-md border border-border-seam object-cover\"\n />\n )}\n </div>\n </div>\n </Modal>\n\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete client?\"\n description={\n deleteTarget\n ? `\"${deleteTarget.clientName}\" and its share link 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 The client will lose access to their shared portal immediately. This\n cannot be undone.\n </p>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"EmailTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/email-tracking/EmailTrackingPage.tsx"],"names":[],"mappings":"AA8LA,wBAAgB,iBAAiB,gCAmVhC"}
1
+ {"version":3,"file":"EmailTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/email-tracking/EmailTrackingPage.tsx"],"names":[],"mappings":"AA8LA,wBAAgB,iBAAiB,gCAkVhC"}
@@ -152,8 +152,10 @@ function A({ value: e, onChange: t }) {
152
152
  function j() {
153
153
  let { workspaceId: T = "" } = e(), [E, D] = p(!1), [O, j] = p(!1), { data: M, loading: ee, error: te, fetchMore: ne, refetch: N } = c({
154
154
  variables: {
155
- limit: C,
156
- offset: 0,
155
+ pagination: {
156
+ limit: C,
157
+ offset: 0
158
+ },
157
159
  filter: E ? { isImportant: !0 } : void 0
158
160
  },
159
161
  skip: !T,
@@ -277,7 +279,10 @@ function j() {
277
279
  j(!0);
278
280
  try {
279
281
  await ne({
280
- variables: { offset: P.length },
282
+ variables: { pagination: {
283
+ limit: C,
284
+ offset: P.length
285
+ } },
281
286
  updateQuery: (e, { fetchMoreResult: t }) => t ? {
282
287
  ...e,
283
288
  emailTimeEntries: [...e.emailTimeEntries, ...t.emailTimeEntries]
@@ -1 +1 @@
1
- {"version":3,"file":"EmailTrackingPage.js","names":[],"sources":["../../../../src/timecampus/pages/email-tracking/EmailTrackingPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { Mail, Filter, Plus, Pencil, Trash2 } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n getStatusColor,\n getPriorityColor,\n formatRelativeTime,\n getInitials,\n type BadgeVariant,\n} from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListEmailTimeEntriesQuery,\n useCreateEmailTimeEntryMutation,\n useUpdateEmailTimeEntryMutation,\n useDeleteEmailTimeEntryMutation,\n type EmailTimeEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 25;\n\nconst BADGE_CLASSES: Record<BadgeVariant, string> = {\n success: \"bg-status-success-bg text-status-success-text\",\n warning: \"bg-status-warning-bg text-status-warning-text\",\n error: \"bg-status-error-bg text-status-error-text\",\n info: \"bg-status-info-bg text-status-info-text\",\n default: \"bg-bg-sunken text-text-secondary\",\n muted: \"bg-bg-sunken text-text-secondary\",\n};\n\ninterface EmailFormState {\n subject: string;\n sender: string;\n recipients: string;\n category: string;\n date: string;\n duration: string;\n responseTime: string;\n isImportant: boolean;\n threadId: string;\n}\n\nconst inputClasses =\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\";\nconst labelClasses = \"block text-sm font-medium text-text-primary\";\n\nfunction toDatetimeLocal(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60000);\n return local.toISOString().slice(0, 16);\n}\n\nfunction nowDatetimeLocal(): string {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60000);\n return local.toISOString().slice(0, 16);\n}\n\nfunction toState(entry?: EmailTimeEntryFieldsFragment): EmailFormState {\n return {\n subject: entry?.subject ?? \"\",\n sender: entry?.sender ?? \"\",\n recipients: entry?.recipients ?? \"\",\n category: entry?.category ?? \"\",\n date: entry?.date ? toDatetimeLocal(entry.date) : nowDatetimeLocal(),\n duration: entry?.duration != null ? String(entry.duration) : \"\",\n responseTime: entry?.responseTime != null ? String(entry.responseTime) : \"\",\n isImportant: entry?.isImportant ?? false,\n threadId: entry?.threadId ?? \"\",\n };\n}\n\ninterface EmailEntryFormProps {\n value: EmailFormState;\n onChange: (next: EmailFormState) => void;\n}\n\nfunction EmailEntryForm({ value, onChange }: EmailEntryFormProps) {\n const set = <K extends keyof EmailFormState>(key: K, v: EmailFormState[K]) =>\n onChange({ ...value, [key]: v });\n\n return (\n <div className=\"space-y-4\">\n <div>\n <label className={labelClasses}>Subject</label>\n <input\n type=\"text\"\n placeholder=\"Email subject\"\n value={value.subject}\n onChange={(e) => set(\"subject\", e.target.value)}\n className={inputClasses}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClasses}>Sender</label>\n <input\n type=\"text\"\n placeholder=\"sender@example.com\"\n value={value.sender}\n onChange={(e) => set(\"sender\", e.target.value)}\n className={inputClasses}\n />\n </div>\n <div>\n <label className={labelClasses}>Recipients</label>\n <input\n type=\"text\"\n placeholder=\"recipient@example.com\"\n value={value.recipients}\n onChange={(e) => set(\"recipients\", e.target.value)}\n className={inputClasses}\n />\n </div>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClasses}>Category</label>\n <input\n type=\"text\"\n placeholder=\"e.g. work, newsletter\"\n value={value.category}\n onChange={(e) => set(\"category\", e.target.value)}\n className={inputClasses}\n />\n </div>\n <div>\n <label className={labelClasses}>Date &amp; Time</label>\n <input\n type=\"datetime-local\"\n value={value.date}\n onChange={(e) => set(\"date\", e.target.value)}\n className={inputClasses}\n />\n </div>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClasses}>Duration (minutes)</label>\n <input\n type=\"number\"\n min=\"0\"\n placeholder=\"0\"\n value={value.duration}\n onChange={(e) => set(\"duration\", e.target.value)}\n className={inputClasses}\n />\n </div>\n <div>\n <label className={labelClasses}>Response Time (minutes)</label>\n <input\n type=\"number\"\n min=\"0\"\n placeholder=\"0\"\n value={value.responseTime}\n onChange={(e) => set(\"responseTime\", e.target.value)}\n className={inputClasses}\n />\n </div>\n </div>\n <div>\n <label className={labelClasses}>Thread ID</label>\n <input\n type=\"text\"\n placeholder=\"Optional thread identifier\"\n value={value.threadId}\n onChange={(e) => set(\"threadId\", e.target.value)}\n className={inputClasses}\n />\n </div>\n <label className=\"flex items-center gap-2\">\n <input\n type=\"checkbox\"\n checked={value.isImportant}\n onChange={(e) => set(\"isImportant\", e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n <span className=\"text-sm font-medium text-text-primary\">\n Mark as important\n </span>\n </label>\n </div>\n );\n}\n\nexport function EmailTrackingPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [importantOnly, setImportantOnly] = useState(false);\n const [loadingMore, setLoadingMore] = useState(false);\n\n const { data, loading, error, fetchMore, refetch } =\n useListEmailTimeEntriesQuery({\n variables: {\n limit: PAGE_SIZE,\n offset: 0,\n filter: importantOnly ? { isImportant: true } : undefined,\n },\n skip: !workspaceId,\n notifyOnNetworkStatusChange: true,\n });\n\n const emails = data?.emailTimeEntries ?? [];\n const hasMore = emails.length > 0 && emails.length % PAGE_SIZE === 0;\n\n // Modal state\n const [createOpen, setCreateOpen] = useState(false);\n const [editEntry, setEditEntry] =\n useState<EmailTimeEntryFieldsFragment | null>(null);\n const [deleteEntry, setDeleteEntry] =\n useState<EmailTimeEntryFieldsFragment | null>(null);\n\n const [createForm, setCreateForm] = useState<EmailFormState>(toState());\n const [editForm, setEditForm] = useState<EmailFormState>(toState());\n\n const [createEntry, { loading: creating, error: createError }] =\n useCreateEmailTimeEntryMutation();\n const [updateEntry, { loading: updating, error: updateError }] =\n useUpdateEmailTimeEntryMutation();\n const [removeEntry, { loading: deleting, error: deleteError }] =\n useDeleteEmailTimeEntryMutation();\n\n const buildInput = (form: EmailFormState) => ({\n subject: form.subject.trim() || undefined,\n sender: form.sender.trim() || undefined,\n recipients: form.recipients.trim() || undefined,\n category: form.category.trim() || undefined,\n date: new Date(form.date).toISOString(),\n duration: form.duration ? Number(form.duration) : undefined,\n responseTime: form.responseTime ? Number(form.responseTime) : undefined,\n isImportant: form.isImportant,\n threadId: form.threadId.trim() || undefined,\n });\n\n const openCreate = () => {\n setCreateForm(toState());\n setCreateOpen(true);\n };\n\n const handleCreate = async () => {\n await createEntry({\n variables: { input: buildInput(createForm) },\n });\n setCreateOpen(false);\n await refetch();\n };\n\n const openEdit = (entry: EmailTimeEntryFieldsFragment) => {\n setEditForm(toState(entry));\n setEditEntry(entry);\n };\n\n const handleUpdate = async () => {\n if (!editEntry) return;\n await updateEntry({\n variables: { id: editEntry.id, input: buildInput(editForm) },\n });\n setEditEntry(null);\n await refetch();\n };\n\n const handleDelete = async () => {\n if (!deleteEntry) return;\n await removeEntry({ variables: { id: deleteEntry.id } });\n setDeleteEntry(null);\n await refetch();\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { offset: emails.length },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n emailTimeEntries: [\n ...prev.emailTimeEntries,\n ...fetchMoreResult.emailTimeEntries,\n ],\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <PageLayout\n title=\"Email Tracking\"\n description=\"Track time spent on emails\"\n purpose=\"Surface the hidden hours email steals from your day — every message linked to the time it took and the project it belonged to — so inbox work finally shows up in your timesheets and reports.\"\n actions={\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => setImportantOnly((v) => !v)}\n className={`inline-flex items-center gap-1.5 rounded-md border px-3 py-2 text-sm font-medium ${\n importantOnly\n ? \"border-action-primary-bg bg-action-primary-bg/10 text-action-primary-bg\"\n : \"border-border-subtle text-text-primary hover:bg-bg-sunken/50\"\n }`}\n >\n <Filter className=\"h-4 w-4\" />\n {importantOnly ? \"Important only\" : \"Filter\"}\n </button>\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Entry\n </button>\n </div>\n }\n >\n {loading && emails.length === 0 ? (\n <LoadingSkeleton variant=\"list\" count={4} />\n ) : error ? (\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 email entries. Please try again later.\n </div>\n ) : emails.length === 0 ? (\n <EmptyState\n illustration=\"empty-notifications\"\n title=\"No email entries found\"\n description=\"Log the time you spend in your inbox — or connect email tracking — and entries will appear here.\"\n action={\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Entry\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"glass\" glow className=\"overflow-hidden\">\n <div className=\"divide-y divide-border\">\n {emails.map((email: EmailTimeEntryFieldsFragment) => (\n <div\n key={email.id}\n className=\"flex items-center justify-between gap-3 p-4 transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n <div\n className={`relative flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-xs font-semibold text-primary ${\n email.isImportant\n ? \"ring-2 ring-action-primary-bg/40\"\n : \"\"\n }`}\n >\n {email.sender ? (\n getInitials(email.sender)\n ) : (\n <Mail className=\"h-4 w-4\" />\n )}\n </div>\n <div className=\"min-w-0\">\n <p\n className={`truncate text-sm text-text-primary ${email.isImportant ? \"font-semibold\" : \"font-medium\"}`}\n >\n {email.subject ?? \"(No subject)\"}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {email.sender ?? \"Unknown\"} &middot;{\" \"}\n {formatRelativeTime(email.date)}\n </p>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <span className=\"hidden rounded-full bg-bg-sunken px-2 py-0.5 font-mono text-xs tabular-nums text-text-secondary md:inline-flex\">\n ~{email.duration ?? 0}min\n </span>\n <span\n className={`hidden rounded-full px-2 py-0.5 text-xs font-medium sm:inline-flex ${BADGE_CLASSES[getPriorityColor(email.isImportant ? \"high\" : \"low\")]}`}\n >\n {email.isImportant ? \"high\" : \"normal\"}\n </span>\n <span\n className={`hidden rounded-full px-2 py-0.5 text-xs font-medium sm:inline-flex ${BADGE_CLASSES[getStatusColor(email.category ?? \"active\")]}`}\n >\n {email.category ?? \"uncategorized\"}\n </span>\n <button\n onClick={() => openEdit(email)}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Edit entry\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setDeleteEntry(email)}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Delete entry\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ))}\n </div>\n </GlassCard>\n {hasMore && (\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 {/* Create modal */}\n <Modal\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Add Email Entry\"\n description=\"Log time spent on an email\"\n size=\"xl\"\n footer={\n <>\n <button\n onClick={() => setCreateOpen(false)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleCreate}\n disabled={creating || !createForm.date}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating ? \"Saving...\" : \"Add Entry\"}\n </button>\n </>\n }\n >\n {createError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to add entry. Please try again.\n </div>\n )}\n <EmailEntryForm value={createForm} onChange={setCreateForm} />\n </Modal>\n\n {/* Edit modal */}\n <Modal\n open={editEntry !== null}\n onClose={() => setEditEntry(null)}\n title=\"Edit Email Entry\"\n description=\"Update this email time entry\"\n size=\"xl\"\n footer={\n <>\n <button\n onClick={() => setEditEntry(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleUpdate}\n disabled={updating || !editForm.date}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {updating ? \"Saving...\" : \"Save Changes\"}\n </button>\n </>\n }\n >\n {updateError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to update entry. Please try again.\n </div>\n )}\n <EmailEntryForm value={editForm} onChange={setEditForm} />\n </Modal>\n\n {/* Delete confirm modal */}\n <Modal\n open={deleteEntry !== null}\n onClose={() => setDeleteEntry(null)}\n title=\"Delete Email Entry\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteEntry(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n {deleteError && (\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 delete entry. Please try again.\n </div>\n )}\n <p className=\"text-sm text-text-secondary\">\n Are you sure you want to delete the entry{\" \"}\n <span className=\"font-medium text-text-primary\">\n &ldquo;{deleteEntry?.subject ?? \"(No subject)\"}&rdquo;\n </span>\n ? This action cannot be undone.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EmailTrackingPage.js","names":[],"sources":["../../../../src/timecampus/pages/email-tracking/EmailTrackingPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { Mail, Filter, Plus, Pencil, Trash2 } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n getStatusColor,\n getPriorityColor,\n formatRelativeTime,\n getInitials,\n type BadgeVariant,\n} from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListEmailTimeEntriesQuery,\n useCreateEmailTimeEntryMutation,\n useUpdateEmailTimeEntryMutation,\n useDeleteEmailTimeEntryMutation,\n type EmailTimeEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 25;\n\nconst BADGE_CLASSES: Record<BadgeVariant, string> = {\n success: \"bg-status-success-bg text-status-success-text\",\n warning: \"bg-status-warning-bg text-status-warning-text\",\n error: \"bg-status-error-bg text-status-error-text\",\n info: \"bg-status-info-bg text-status-info-text\",\n default: \"bg-bg-sunken text-text-secondary\",\n muted: \"bg-bg-sunken text-text-secondary\",\n};\n\ninterface EmailFormState {\n subject: string;\n sender: string;\n recipients: string;\n category: string;\n date: string;\n duration: string;\n responseTime: string;\n isImportant: boolean;\n threadId: string;\n}\n\nconst inputClasses =\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\";\nconst labelClasses = \"block text-sm font-medium text-text-primary\";\n\nfunction toDatetimeLocal(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60000);\n return local.toISOString().slice(0, 16);\n}\n\nfunction nowDatetimeLocal(): string {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60000);\n return local.toISOString().slice(0, 16);\n}\n\nfunction toState(entry?: EmailTimeEntryFieldsFragment): EmailFormState {\n return {\n subject: entry?.subject ?? \"\",\n sender: entry?.sender ?? \"\",\n recipients: entry?.recipients ?? \"\",\n category: entry?.category ?? \"\",\n date: entry?.date ? toDatetimeLocal(entry.date) : nowDatetimeLocal(),\n duration: entry?.duration != null ? String(entry.duration) : \"\",\n responseTime: entry?.responseTime != null ? String(entry.responseTime) : \"\",\n isImportant: entry?.isImportant ?? false,\n threadId: entry?.threadId ?? \"\",\n };\n}\n\ninterface EmailEntryFormProps {\n value: EmailFormState;\n onChange: (next: EmailFormState) => void;\n}\n\nfunction EmailEntryForm({ value, onChange }: EmailEntryFormProps) {\n const set = <K extends keyof EmailFormState>(key: K, v: EmailFormState[K]) =>\n onChange({ ...value, [key]: v });\n\n return (\n <div className=\"space-y-4\">\n <div>\n <label className={labelClasses}>Subject</label>\n <input\n type=\"text\"\n placeholder=\"Email subject\"\n value={value.subject}\n onChange={(e) => set(\"subject\", e.target.value)}\n className={inputClasses}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClasses}>Sender</label>\n <input\n type=\"text\"\n placeholder=\"sender@example.com\"\n value={value.sender}\n onChange={(e) => set(\"sender\", e.target.value)}\n className={inputClasses}\n />\n </div>\n <div>\n <label className={labelClasses}>Recipients</label>\n <input\n type=\"text\"\n placeholder=\"recipient@example.com\"\n value={value.recipients}\n onChange={(e) => set(\"recipients\", e.target.value)}\n className={inputClasses}\n />\n </div>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClasses}>Category</label>\n <input\n type=\"text\"\n placeholder=\"e.g. work, newsletter\"\n value={value.category}\n onChange={(e) => set(\"category\", e.target.value)}\n className={inputClasses}\n />\n </div>\n <div>\n <label className={labelClasses}>Date &amp; Time</label>\n <input\n type=\"datetime-local\"\n value={value.date}\n onChange={(e) => set(\"date\", e.target.value)}\n className={inputClasses}\n />\n </div>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClasses}>Duration (minutes)</label>\n <input\n type=\"number\"\n min=\"0\"\n placeholder=\"0\"\n value={value.duration}\n onChange={(e) => set(\"duration\", e.target.value)}\n className={inputClasses}\n />\n </div>\n <div>\n <label className={labelClasses}>Response Time (minutes)</label>\n <input\n type=\"number\"\n min=\"0\"\n placeholder=\"0\"\n value={value.responseTime}\n onChange={(e) => set(\"responseTime\", e.target.value)}\n className={inputClasses}\n />\n </div>\n </div>\n <div>\n <label className={labelClasses}>Thread ID</label>\n <input\n type=\"text\"\n placeholder=\"Optional thread identifier\"\n value={value.threadId}\n onChange={(e) => set(\"threadId\", e.target.value)}\n className={inputClasses}\n />\n </div>\n <label className=\"flex items-center gap-2\">\n <input\n type=\"checkbox\"\n checked={value.isImportant}\n onChange={(e) => set(\"isImportant\", e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n <span className=\"text-sm font-medium text-text-primary\">\n Mark as important\n </span>\n </label>\n </div>\n );\n}\n\nexport function EmailTrackingPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [importantOnly, setImportantOnly] = useState(false);\n const [loadingMore, setLoadingMore] = useState(false);\n\n const { data, loading, error, fetchMore, refetch } =\n useListEmailTimeEntriesQuery({\n variables: {\n pagination: { limit: PAGE_SIZE, offset: 0 },\n filter: importantOnly ? { isImportant: true } : undefined,\n },\n skip: !workspaceId,\n notifyOnNetworkStatusChange: true,\n });\n\n const emails = data?.emailTimeEntries ?? [];\n const hasMore = emails.length > 0 && emails.length % PAGE_SIZE === 0;\n\n // Modal state\n const [createOpen, setCreateOpen] = useState(false);\n const [editEntry, setEditEntry] =\n useState<EmailTimeEntryFieldsFragment | null>(null);\n const [deleteEntry, setDeleteEntry] =\n useState<EmailTimeEntryFieldsFragment | null>(null);\n\n const [createForm, setCreateForm] = useState<EmailFormState>(toState());\n const [editForm, setEditForm] = useState<EmailFormState>(toState());\n\n const [createEntry, { loading: creating, error: createError }] =\n useCreateEmailTimeEntryMutation();\n const [updateEntry, { loading: updating, error: updateError }] =\n useUpdateEmailTimeEntryMutation();\n const [removeEntry, { loading: deleting, error: deleteError }] =\n useDeleteEmailTimeEntryMutation();\n\n const buildInput = (form: EmailFormState) => ({\n subject: form.subject.trim() || undefined,\n sender: form.sender.trim() || undefined,\n recipients: form.recipients.trim() || undefined,\n category: form.category.trim() || undefined,\n date: new Date(form.date).toISOString(),\n duration: form.duration ? Number(form.duration) : undefined,\n responseTime: form.responseTime ? Number(form.responseTime) : undefined,\n isImportant: form.isImportant,\n threadId: form.threadId.trim() || undefined,\n });\n\n const openCreate = () => {\n setCreateForm(toState());\n setCreateOpen(true);\n };\n\n const handleCreate = async () => {\n await createEntry({\n variables: { input: buildInput(createForm) },\n });\n setCreateOpen(false);\n await refetch();\n };\n\n const openEdit = (entry: EmailTimeEntryFieldsFragment) => {\n setEditForm(toState(entry));\n setEditEntry(entry);\n };\n\n const handleUpdate = async () => {\n if (!editEntry) return;\n await updateEntry({\n variables: { id: editEntry.id, input: buildInput(editForm) },\n });\n setEditEntry(null);\n await refetch();\n };\n\n const handleDelete = async () => {\n if (!deleteEntry) return;\n await removeEntry({ variables: { id: deleteEntry.id } });\n setDeleteEntry(null);\n await refetch();\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { limit: PAGE_SIZE, offset: emails.length } },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n emailTimeEntries: [\n ...prev.emailTimeEntries,\n ...fetchMoreResult.emailTimeEntries,\n ],\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <PageLayout\n title=\"Email Tracking\"\n description=\"Track time spent on emails\"\n purpose=\"Surface the hidden hours email steals from your day — every message linked to the time it took and the project it belonged to — so inbox work finally shows up in your timesheets and reports.\"\n actions={\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => setImportantOnly((v) => !v)}\n className={`inline-flex items-center gap-1.5 rounded-md border px-3 py-2 text-sm font-medium ${\n importantOnly\n ? \"border-action-primary-bg bg-action-primary-bg/10 text-action-primary-bg\"\n : \"border-border-subtle text-text-primary hover:bg-bg-sunken/50\"\n }`}\n >\n <Filter className=\"h-4 w-4\" />\n {importantOnly ? \"Important only\" : \"Filter\"}\n </button>\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Entry\n </button>\n </div>\n }\n >\n {loading && emails.length === 0 ? (\n <LoadingSkeleton variant=\"list\" count={4} />\n ) : error ? (\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 email entries. Please try again later.\n </div>\n ) : emails.length === 0 ? (\n <EmptyState\n illustration=\"empty-notifications\"\n title=\"No email entries found\"\n description=\"Log the time you spend in your inbox — or connect email tracking — and entries will appear here.\"\n action={\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Entry\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"glass\" glow className=\"overflow-hidden\">\n <div className=\"divide-y divide-border\">\n {emails.map((email: EmailTimeEntryFieldsFragment) => (\n <div\n key={email.id}\n className=\"flex items-center justify-between gap-3 p-4 transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n <div\n className={`relative flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-xs font-semibold text-primary ${\n email.isImportant\n ? \"ring-2 ring-action-primary-bg/40\"\n : \"\"\n }`}\n >\n {email.sender ? (\n getInitials(email.sender)\n ) : (\n <Mail className=\"h-4 w-4\" />\n )}\n </div>\n <div className=\"min-w-0\">\n <p\n className={`truncate text-sm text-text-primary ${email.isImportant ? \"font-semibold\" : \"font-medium\"}`}\n >\n {email.subject ?? \"(No subject)\"}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {email.sender ?? \"Unknown\"} &middot;{\" \"}\n {formatRelativeTime(email.date)}\n </p>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <span className=\"hidden rounded-full bg-bg-sunken px-2 py-0.5 font-mono text-xs tabular-nums text-text-secondary md:inline-flex\">\n ~{email.duration ?? 0}min\n </span>\n <span\n className={`hidden rounded-full px-2 py-0.5 text-xs font-medium sm:inline-flex ${BADGE_CLASSES[getPriorityColor(email.isImportant ? \"high\" : \"low\")]}`}\n >\n {email.isImportant ? \"high\" : \"normal\"}\n </span>\n <span\n className={`hidden rounded-full px-2 py-0.5 text-xs font-medium sm:inline-flex ${BADGE_CLASSES[getStatusColor(email.category ?? \"active\")]}`}\n >\n {email.category ?? \"uncategorized\"}\n </span>\n <button\n onClick={() => openEdit(email)}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Edit entry\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setDeleteEntry(email)}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Delete entry\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ))}\n </div>\n </GlassCard>\n {hasMore && (\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 {/* Create modal */}\n <Modal\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title=\"Add Email Entry\"\n description=\"Log time spent on an email\"\n size=\"xl\"\n footer={\n <>\n <button\n onClick={() => setCreateOpen(false)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleCreate}\n disabled={creating || !createForm.date}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating ? \"Saving...\" : \"Add Entry\"}\n </button>\n </>\n }\n >\n {createError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to add entry. Please try again.\n </div>\n )}\n <EmailEntryForm value={createForm} onChange={setCreateForm} />\n </Modal>\n\n {/* Edit modal */}\n <Modal\n open={editEntry !== null}\n onClose={() => setEditEntry(null)}\n title=\"Edit Email Entry\"\n description=\"Update this email time entry\"\n size=\"xl\"\n footer={\n <>\n <button\n onClick={() => setEditEntry(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleUpdate}\n disabled={updating || !editForm.date}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {updating ? \"Saving...\" : \"Save Changes\"}\n </button>\n </>\n }\n >\n {updateError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to update entry. Please try again.\n </div>\n )}\n <EmailEntryForm value={editForm} onChange={setEditForm} />\n </Modal>\n\n {/* Delete confirm modal */}\n <Modal\n open={deleteEntry !== null}\n onClose={() => setDeleteEntry(null)}\n title=\"Delete Email Entry\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteEntry(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/90 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n {deleteError && (\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 delete entry. Please try again.\n </div>\n )}\n <p className=\"text-sm text-text-secondary\">\n Are you sure you want to delete the entry{\" \"}\n <span className=\"font-medium text-text-primary\">\n &ldquo;{deleteEntry?.subject ?? \"(No subject)\"}&rdquo;\n </span>\n ? This action cannot be undone.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpenseTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/expense-tracking/ExpenseTrackingPage.tsx"],"names":[],"mappings":"AAgIA,wBAAgB,mBAAmB,gCAwtBlC"}
1
+ {"version":3,"file":"ExpenseTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/expense-tracking/ExpenseTrackingPage.tsx"],"names":[],"mappings":"AAgIA,wBAAgB,mBAAmB,gCAsuBlC"}
@@ -40,7 +40,7 @@ var v = 25, y = [
40
40
  id
41
41
  }
42
42
  }
43
- `, De = 604800;
43
+ `, De = 3600;
44
44
  function Oe({ expense: e }) {
45
45
  return _({
46
46
  entityType: "TIMECAMPUS_EXPENSE",
@@ -57,14 +57,17 @@ var S = {
57
57
  description: "",
58
58
  projectId: "",
59
59
  date: x(),
60
- receiptUrl: ""
60
+ receiptFileId: "",
61
+ receiptPreviewUrl: ""
61
62
  };
62
63
  function C() {
63
64
  let { workspaceId: f = "" } = e(), { toasts: g, addToast: _, dismissToast: C } = ue(), w = Ce(), { data: ke, loading: T, error: Ae, refetch: je, fetchMore: Me } = re({
64
65
  variables: {
65
66
  workspaceId: f,
66
- limit: v,
67
- offset: 0
67
+ pagination: {
68
+ limit: v,
69
+ offset: 0
70
+ }
68
71
  },
69
72
  notifyOnNetworkStatusChange: !0
70
73
  }), { data: Ne, loading: Pe, error: Fe, refetch: Ie } = ne({ variables: { workspaceId: f } }), [Le, { loading: E }] = ee(), [Re, { loading: D }] = oe(), [ze, { loading: O }] = te(), [Be] = ae(), [Ve] = a(), [He, { loading: k }] = ie(), A = ke?.expenses ?? [], j = Ne?.expenseStats, Ue = A[0]?.currency ?? "USD", We = j?.byCategory ? Object.entries(j.byCategory).map(([e, t]) => ({
@@ -84,7 +87,8 @@ function C() {
84
87
  description: e.description ?? "",
85
88
  projectId: e.projectId ?? "",
86
89
  date: e.date ? e.date.slice(0, 10) : x(),
87
- receiptUrl: e.receiptUrl ?? ""
90
+ receiptFileId: e.receiptFileId ?? "",
91
+ receiptPreviewUrl: e.receiptDownloadUrl ?? ""
88
92
  }), F(!0);
89
93
  }, Ze = async (e) => {
90
94
  V(!0);
@@ -108,10 +112,10 @@ function C() {
108
112
  },
109
113
  fetchPolicy: "no-cache"
110
114
  }), a = i?.getFileDownloadUrl;
111
- if (!a) throw Error("No download URL returned");
112
115
  z((e) => ({
113
116
  ...e,
114
- receiptUrl: a
117
+ receiptFileId: r.id,
118
+ receiptPreviewUrl: a ?? ""
115
119
  })), _({
116
120
  title: "Receipt attached",
117
121
  variant: "success"
@@ -140,7 +144,7 @@ function C() {
140
144
  description: R.description.trim() || void 0,
141
145
  projectId: R.projectId.trim() || void 0,
142
146
  date: t,
143
- receiptUrl: R.receiptUrl || void 0
147
+ receiptFileId: R.receiptFileId || void 0
144
148
  }
145
149
  } }), _({
146
150
  title: "Expense updated",
@@ -152,7 +156,7 @@ function C() {
152
156
  description: R.description.trim() || void 0,
153
157
  projectId: R.projectId.trim() || void 0,
154
158
  date: t,
155
- receiptUrl: R.receiptUrl || void 0
159
+ receiptFileId: R.receiptFileId || void 0
156
160
  } } }), _({
157
161
  title: "Expense added",
158
162
  variant: "success"
@@ -236,8 +240,10 @@ function C() {
236
240
  ((await Me({
237
241
  variables: {
238
242
  workspaceId: f,
239
- limit: v,
240
- offset: A.length
243
+ pagination: {
244
+ limit: v,
245
+ offset: A.length
246
+ }
241
247
  },
242
248
  updateQuery: (e, { fetchMoreResult: t }) => t ? {
243
249
  ...e,
@@ -554,8 +560,8 @@ function C() {
554
560
  })
555
561
  ]
556
562
  }),
557
- R.receiptUrl && /* @__PURE__ */ h("a", {
558
- href: R.receiptUrl,
563
+ R.receiptFileId && R.receiptPreviewUrl && /* @__PURE__ */ h("a", {
564
+ href: R.receiptPreviewUrl,
559
565
  target: "_blank",
560
566
  rel: "noopener noreferrer",
561
567
  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",