@burdenoff/microfe-timecampus 2026.830.2 → 2026.904.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/timecampus/components/index.js +4 -4
  2. package/dist/timecampus/pages/HomePage.d.ts.map +1 -1
  3. package/dist/timecampus/pages/HomePage.js +149 -133
  4. package/dist/timecampus/pages/HomePage.js.map +1 -1
  5. package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +7 -7
  6. package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
  7. package/dist/timecampus/pages/client-portal/ClientPortalPage.js +7 -7
  8. package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
  9. package/dist/timecampus/pages/dashboard/ReportsPage.js +13 -13
  10. package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +7 -7
  11. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +7 -7
  12. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
  13. package/dist/timecampus/pages/focus-score/FocusScorePage.js +13 -13
  14. package/dist/timecampus/pages/geofencing/GeofencingPage.js +135 -135
  15. package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
  16. package/dist/timecampus/pages/invoicing/InvoicingPage.js +7 -7
  17. package/dist/timecampus/pages/invoicing/InvoicingPage.js.map +1 -1
  18. package/dist/timecampus/pages/lifetime/DeadTimeRadarPage.js +8 -8
  19. package/dist/timecampus/pages/lifetime/FiniteLifePage.js +4 -4
  20. package/dist/timecampus/pages/lifetime/FiniteLifePage.js.map +1 -1
  21. package/dist/timecampus/pages/lifetime/LifeBalancePage.js +104 -104
  22. package/dist/timecampus/pages/lifetime/LifeBalancePage.js.map +1 -1
  23. package/dist/timecampus/pages/lifetime/LifeTimelinePage.js +7 -7
  24. package/dist/timecampus/pages/lifetime/LifeTimelinePage.js.map +1 -1
  25. package/dist/timecampus/pages/lifetime/PersonalCorporatePage.js +33 -33
  26. package/dist/timecampus/pages/lifetime/PersonalCorporatePage.js.map +1 -1
  27. package/dist/timecampus/pages/lifetime/RecommendationsPage.js +80 -80
  28. package/dist/timecampus/pages/lifetime/RecommendationsPage.js.map +1 -1
  29. package/dist/timecampus/pages/lifetime/TimeWellSpentPage.js +16 -16
  30. package/dist/timecampus/pages/lifetime/ValuesGoalsPage.js +11 -11
  31. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +11 -11
  32. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
  33. package/dist/timecampus/pages/meetings/MeetingDetailPage.js +14 -14
  34. package/dist/timecampus/pages/meetings/MeetingDetailPage.js.map +1 -1
  35. package/dist/timecampus/pages/meetings/MeetingsPage.js +12 -12
  36. package/dist/timecampus/pages/projects/ProjectsPage.js +5 -5
  37. package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
  38. package/dist/timecampus/pages/projects/TasksPage.js +8 -8
  39. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +8 -8
  40. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
  41. package/dist/timecampus/pages/team/TeamMembersPage.js +8 -8
  42. package/dist/timecampus/pages/team-activity/TeamActivityPage.js +13 -13
  43. package/dist/timecampus/pages/time/TimeEntriesPage.js +5 -5
  44. package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
  45. package/dist/timecampus/pages/time/TimeTrackingPage.js +138 -138
  46. package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
  47. package/dist/timecampus/pages/time/TimerPage.js +14 -14
  48. package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
  49. package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +26 -26
  50. package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
  51. package/dist/timecampus/pages/timeline/TimelinePage.js +8 -8
  52. package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +79 -79
  53. package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
  54. package/package.json +1 -1
@@ -2,10 +2,10 @@ import { useTimeCampus as e } from "../../providers/TimeCampusProvider.js";
2
2
  import { formatHours as t, formatTime as n } from "../../utils/helpers.js";
3
3
  import { StatCard as r } from "../../components/StatCard.js";
4
4
  import { LoadingSkeleton as i } from "../../components/LoadingSkeleton.js";
5
- import { useCategorizeApplicationMutation as ee, useDeleteApplicationUsageMutation as te, useGetAppUsageSummaryQuery as ne, useListApplicationUsagesQuery as re, useRecordApplicationUsageMutation as ie } from "../../../generated/wspace-operations.js";
5
+ import { EmptyState as ee } from "../../components/EmptyState.js";
6
+ import { useCategorizeApplicationMutation as te, useDeleteApplicationUsageMutation as ne, useGetAppUsageSummaryQuery as re, useListApplicationUsagesQuery as ie, useRecordApplicationUsageMutation as ae } from "../../../generated/wspace-operations.js";
6
7
  import { PageLayout as a } from "../../components/PageLayout.js";
7
- import { StatusBadge as ae } from "../../components/StatusBadge.js";
8
- import { EmptyState as o } from "../../components/EmptyState.js";
8
+ import { StatusBadge as o } from "../../components/StatusBadge.js";
9
9
  import { Modal as s } from "../../components/Modal.js";
10
10
  import { ChartCard as c } from "../../components/ChartCard.js";
11
11
  import { chartTokens as l } from "../../components/charts/chartTheme.js";
@@ -45,14 +45,14 @@ function he(e, t) {
45
45
  return !e || !t ? null : `${n(e)} – ${n(t)}`;
46
46
  }
47
47
  function C() {
48
- let { workspaceId: n = "" } = e(), { data: l, loading: C, error: ge, fetchMore: _e, refetch: ve } = re({
48
+ let { workspaceId: n = "" } = e(), { data: l, loading: C, error: ge, fetchMore: _e, refetch: ve } = ie({
49
49
  variables: { pagination: {
50
50
  limit: v,
51
51
  offset: 0
52
52
  } },
53
53
  skip: !n,
54
54
  notifyOnNetworkStatusChange: !0
55
- }), { data: ye, loading: be, error: xe, refetch: Se } = ne({ skip: !n }), Ce = C || be, we = ge || xe, w = l?.applicationUsages ?? [], T = ye?.appUsageSummary, E = w.length > 0 && w.length % v === 0, [D, O] = u(!1), [k, A] = u(!1), [j, M] = u(""), [N, P] = u("productive"), [F, I] = u(""), [L, R] = u(""), [z, B] = u(null), [V, H] = u("productive"), [U, W] = u(null), [Te, { loading: G, error: Ee }] = ie(), [De, { loading: K, error: Oe }] = ee(), [ke, { loading: q, error: Ae }] = te(), J = async () => {
55
+ }), { data: ye, loading: be, error: xe, refetch: Se } = re({ skip: !n }), Ce = C || be, we = ge || xe, w = l?.applicationUsages ?? [], T = ye?.appUsageSummary, E = w.length > 0 && w.length % v === 0, [D, O] = u(!1), [k, A] = u(!1), [j, M] = u(""), [N, P] = u("productive"), [F, I] = u(""), [L, R] = u(""), [z, B] = u(null), [V, H] = u("productive"), [U, W] = u(null), [Te, { loading: G, error: Ee }] = ae(), [De, { loading: K, error: Oe }] = te(), [ke, { loading: q, error: Ae }] = ne(), J = async () => {
56
56
  await Promise.all([ve(), Se()]);
57
57
  }, Y = () => {
58
58
  M(""), P("productive"), I(""), R(""), A(!0);
@@ -166,7 +166,7 @@ function C() {
166
166
  description: "90+ min each"
167
167
  })
168
168
  ]
169
- }), w.length === 0 ? /* @__PURE__ */ g(o, {
169
+ }), w.length === 0 ? /* @__PURE__ */ g(ee, {
170
170
  illustration: "empty-data",
171
171
  title: "No application usage data found",
172
172
  description: "Record app usage manually or connect the desktop tracker, and your apps will appear here ranked by time spent.",
@@ -241,7 +241,7 @@ function C() {
241
241
  children: [r, "%"]
242
242
  })]
243
243
  }),
244
- /* @__PURE__ */ g(ae, {
244
+ /* @__PURE__ */ g(o, {
245
245
  label: e.category,
246
246
  variant: ue[e.category] ?? "muted",
247
247
  className: "hidden capitalize sm:inline-flex"
@@ -1 +1 @@
1
- {"version":3,"file":"ApplicationUsagePage.js","names":[],"sources":["../../../../src/timecampus/pages/application-usage/ApplicationUsagePage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { Monitor, Clock, Plus, Tag, Trash2 } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n ChartCard,\n DonutChart,\n TrendChart,\n chartTokens,\n type ChartDatum,\n type DonutSlice,\n type ValueFormatter,\n} from \"../../components\";\nimport { formatHours, formatTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListApplicationUsagesQuery,\n useGetAppUsageSummaryQuery,\n useRecordApplicationUsageMutation,\n useCategorizeApplicationMutation,\n useDeleteApplicationUsageMutation,\n type ApplicationUsageFieldsFragment,\n type ListApplicationUsagesQuery,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 50;\n\nconst CATEGORY_OPTIONS = [\n \"productive\",\n \"neutral\",\n \"communication\",\n \"distracting\",\n] as const;\n\nconst CATEGORY_VARIANT: Record<string, \"success\" | \"muted\" | \"info\" | \"error\"> =\n {\n productive: \"success\",\n neutral: \"muted\",\n communication: \"info\",\n distracting: \"error\",\n };\n\n/** Category ring hues — token-only, echoing productive→positive / distracting→negative. */\nconst CATEGORY_SLICE_COLOR: Record<string, string> = {\n productive: chartTokens.positive,\n neutral: chartTokens.series[3],\n distracting: chartTokens.negative,\n};\n\nconst formatChartHours: ValueFormatter = (value) =>\n `${value.toFixed(value >= 10 ? 0 : 1)}h`;\n\nconst formatSecondsAsHours: ValueFormatter = (value) =>\n formatHours(value / 3600);\n\nconst formatDayTick = (value: string | number): string => {\n const parsed = new Date(String(value));\n return Number.isNaN(parsed.getTime())\n ? String(value)\n : parsed.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" });\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 todayIso(): string {\n return new Date().toISOString();\n}\n\n/**\n * The window an app was actually in use for, when the capturing client\n * recorded one. Both timestamps are optional on `ApplicationUsage`, so a\n * half-known window is shown as nothing rather than as a guessed bound.\n */\nfunction usageWindow(\n firstUsed: string | null | undefined,\n lastUsed: string | null | undefined,\n): string | null {\n if (!firstUsed || !lastUsed) return null;\n return `${formatTime(firstUsed)} – ${formatTime(lastUsed)}`;\n}\n\nexport function ApplicationUsagePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: usagesData,\n loading: usagesLoading,\n error: usagesError,\n fetchMore,\n refetch: refetchUsages,\n } = useListApplicationUsagesQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n skip: !workspaceId,\n notifyOnNetworkStatusChange: true,\n });\n\n const {\n data: summaryData,\n loading: summaryLoading,\n error: summaryError,\n refetch: refetchSummary,\n } = useGetAppUsageSummaryQuery({\n skip: !workspaceId,\n });\n\n const loading = usagesLoading || summaryLoading;\n const error = usagesError || summaryError;\n const apps = usagesData?.applicationUsages ?? [];\n const summary = summaryData?.appUsageSummary;\n const hasMore = apps.length > 0 && apps.length % PAGE_SIZE === 0;\n\n const [loadingMore, setLoadingMore] = useState(false);\n\n // Add-usage modal\n const [addOpen, setAddOpen] = useState(false);\n const [appName, setAppName] = useState(\"\");\n const [appCategory, setAppCategory] = useState<string>(\"productive\");\n const [appMinutes, setAppMinutes] = useState(\"\");\n const [appSessions, setAppSessions] = useState(\"\");\n\n // Recategorize + delete\n const [recatApp, setRecatApp] =\n useState<ApplicationUsageFieldsFragment | null>(null);\n const [recatCategory, setRecatCategory] = useState<string>(\"productive\");\n const [deleteApp, setDeleteApp] =\n useState<ApplicationUsageFieldsFragment | null>(null);\n\n const [recordUsage, { loading: recording, error: recordError }] =\n useRecordApplicationUsageMutation();\n const [categorize, { loading: categorizing, error: categorizeError }] =\n useCategorizeApplicationMutation();\n const [removeUsage, { loading: deleting, error: deleteError }] =\n useDeleteApplicationUsageMutation();\n\n const refreshAll = async () => {\n await Promise.all([refetchUsages(), refetchSummary()]);\n };\n\n const openAdd = () => {\n setAppName(\"\");\n setAppCategory(\"productive\");\n setAppMinutes(\"\");\n setAppSessions(\"\");\n setAddOpen(true);\n };\n\n const handleRecord = async () => {\n if (!appName.trim() || !appMinutes) return;\n await recordUsage({\n variables: {\n input: {\n applicationName: appName.trim(),\n category: appCategory,\n timeSpent: Math.round(Number(appMinutes) * 60),\n sessions: appSessions ? Number(appSessions) : undefined,\n date: todayIso(),\n },\n },\n });\n setAddOpen(false);\n await refreshAll();\n };\n\n const openRecat = (app: ApplicationUsageFieldsFragment) => {\n setRecatApp(app);\n setRecatCategory(app.category);\n };\n\n const handleRecategorize = async () => {\n if (!recatApp) return;\n await categorize({\n variables: {\n applicationName: recatApp.applicationName,\n category: recatCategory,\n },\n });\n setRecatApp(null);\n await refreshAll();\n };\n\n const handleDelete = async () => {\n if (!deleteApp) return;\n await removeUsage({ variables: { id: deleteApp.id } });\n setDeleteApp(null);\n await refreshAll();\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { limit: PAGE_SIZE, offset: apps.length } },\n updateQuery: (\n prev: ListApplicationUsagesQuery,\n { fetchMoreResult }: { fetchMoreResult?: ListApplicationUsagesQuery },\n ) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n applicationUsages: [\n ...prev.applicationUsages,\n ...fetchMoreResult.applicationUsages,\n ],\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n const totalScreenTimeSeconds = apps.reduce(\n (sum: number, app: ApplicationUsageFieldsFragment) => sum + app.timeSpent,\n 0,\n );\n const totalSwitches = apps.reduce(\n (sum: number, app: ApplicationUsageFieldsFragment) => sum + app.switches,\n 0,\n );\n const productiveRatio =\n summary?.productivityRatio != null\n ? `${Math.round(summary.productivityRatio * 100)}%`\n : \"--\";\n\n // Count focus sessions (apps with > 90 min continuous usage)\n const focusSessions = apps.filter(\n (app: ApplicationUsageFieldsFragment) => app.timeSpent >= 5400,\n ).length;\n\n // Rank the loaded apps by time spent (drives the numbered list).\n const rankedApps = [...apps].sort(\n (a: ApplicationUsageFieldsFragment, b: ApplicationUsageFieldsFragment) =>\n b.timeSpent - a.timeSpent,\n );\n\n // Productive / neutral / distracting split from the real usage summary.\n const categorySlices: DonutSlice[] = summary\n ? [\n {\n name: \"Productive\",\n value: summary.productiveTime,\n color: CATEGORY_SLICE_COLOR.productive,\n },\n {\n name: \"Neutral\",\n value: summary.neutralTime,\n color: CATEGORY_SLICE_COLOR.neutral,\n },\n {\n name: \"Distracting\",\n value: summary.distractingTime,\n color: CATEGORY_SLICE_COLOR.distracting,\n },\n ]\n : [];\n const categoryTotalSeconds =\n (summary?.productiveTime ?? 0) +\n (summary?.neutralTime ?? 0) +\n (summary?.distractingTime ?? 0);\n\n // Daily usage trend — sum the loaded apps' tracked time per calendar day.\n const usageByDay = new Map<string, number>();\n for (const app of apps as ApplicationUsageFieldsFragment[]) {\n const day = (app.date ?? \"\").slice(0, 10);\n if (!day) continue;\n usageByDay.set(day, (usageByDay.get(day) ?? 0) + app.timeSpent);\n }\n const usageTrend: ChartDatum[] = Array.from(usageByDay.entries())\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([day, seconds]) => ({\n day,\n hours: Math.round((seconds / 3600) * 100) / 100,\n }));\n\n if (loading) {\n return (\n <PageLayout\n title=\"App Usage\"\n description=\"Track application usage and productivity\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"App Usage\"\n description=\"Track application usage and productivity\"\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 application usage. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"App Usage\"\n description=\"Track application usage and productivity\"\n purpose=\"See where your screen time actually goes — which apps are productive, how often you context-switch, and how many real focus sessions you get — so you can cut the digital distractions eating your day.\"\n actions={\n <button\n onClick={openAdd}\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 Usage\n </button>\n }\n >\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Screen Time\"\n value={formatHours(totalScreenTimeSeconds / 3600)}\n icon={<Monitor className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Productive Time\"\n value={productiveRatio}\n description=\"Share of tracked time\"\n />\n <StatCard\n title=\"App Switches\"\n value={String(totalSwitches)}\n description={`Avg. ${apps.length > 0 ? Math.round(totalSwitches / apps.length) : 0}/app`}\n />\n <StatCard\n title=\"Focus Sessions\"\n value={String(focusSessions)}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"90+ min each\"\n />\n </div>\n\n {apps.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No application usage data found\"\n description=\"Record app usage manually or connect the desktop tracker, and your apps will appear here ranked by time spent.\"\n action={\n <button\n onClick={openAdd}\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 Usage\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"glass\" className=\"overflow-hidden\">\n <div className=\"flex items-center justify-between gap-3 border-b border-border-seam px-4 py-3 sm:px-6\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Ranked Applications\n </h3>\n <span className=\"text-xs text-text-secondary\">\n {summary?.totalApps != null\n ? `${apps.length} of ${summary.totalApps} app${summary.totalApps === 1 ? \"\" : \"s\"} tracked`\n : `${apps.length} tracked`}\n </span>\n </div>\n <div className=\"divide-y divide-border\">\n {rankedApps.map(\n (app: ApplicationUsageFieldsFragment, index: number) => {\n const share = Math.min(\n 100,\n Math.round(\n (app.timeSpent / (totalScreenTimeSeconds || 1)) * 100,\n ),\n );\n const activeWindow = usageWindow(\n app.firstUsed,\n app.lastUsed,\n );\n return (\n <div\n key={app.id}\n className=\"flex items-center gap-3 p-4 transition-colors hover:bg-bg-sunken/50 sm:gap-4\"\n >\n <span className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold tabular-nums text-text-secondary\">\n {index + 1}\n </span>\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <Monitor className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {app.applicationName}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {app.sessions} sessions &middot; {app.switches}{\" \"}\n switches\n {activeWindow !== null && (\n <>\n {\" \"}\n &middot;{\" \"}\n <span className=\"tabular-nums\">\n {activeWindow}\n </span>\n </>\n )}\n </p>\n </div>\n <div className=\"hidden w-28 shrink-0 lg:block\">\n <div className=\"h-2 rounded-full bg-bg-sunken\">\n <div\n className=\"h-2 rounded-full bg-action-primary-bg\"\n style={{ width: `${share}%` }}\n />\n </div>\n <p className=\"mt-1 text-right text-xs tabular-nums text-text-secondary\">\n {share}%\n </p>\n </div>\n <StatusBadge\n label={app.category}\n variant={CATEGORY_VARIANT[app.category] ?? \"muted\"}\n className=\"hidden capitalize sm:inline-flex\"\n />\n <p className=\"shrink-0 font-mono text-sm font-medium tabular-nums text-text-primary\">\n {formatHours(app.timeSpent / 3600)}\n </p>\n <div className=\"flex shrink-0 items-center gap-1\">\n <button\n onClick={() => openRecat(app)}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Recategorize application\"\n >\n <Tag className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setDeleteApp(app)}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Delete usage entry\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n );\n },\n )}\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title=\"Productivity Split\"\n description=\"Where your tracked screen time goes\"\n >\n <DonutChart\n data={categorySlices}\n centerLabel=\"Total tracked\"\n centerValue={categoryTotalSeconds}\n valueFormat={formatSecondsAsHours}\n height={260}\n emptyLabel=\"No categorized usage yet\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Daily Usage Trend\"\n description=\"Tracked time per day\"\n >\n <TrendChart\n data={usageTrend}\n xKey=\"day\"\n series={[{ key: \"hours\", label: \"Screen time\" }]}\n type=\"area\"\n valueFormat={formatChartHours}\n xTickFormat={formatDayTick}\n height={260}\n emptyLabel=\"No usage recorded yet\"\n />\n </ChartCard>\n </div>\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 </div>\n\n {/* Add usage modal */}\n <Modal\n open={addOpen}\n onClose={() => setAddOpen(false)}\n title=\"Record App Usage\"\n description=\"Manually log time spent in an application\"\n footer={\n <>\n <button\n onClick={() => setAddOpen(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={handleRecord}\n disabled={recording || !appName.trim() || !appMinutes}\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 {recording ? \"Saving...\" : \"Record\"}\n </button>\n </>\n }\n >\n {recordError && (\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 record usage. Please try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className={labelClasses}>Application Name</label>\n <input\n type=\"text\"\n placeholder=\"e.g. Visual Studio Code\"\n value={appName}\n onChange={(e) => setAppName(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}>Category</label>\n <select\n value={appCategory}\n onChange={(e) => setAppCategory(e.target.value)}\n className={inputClasses}\n >\n {CATEGORY_OPTIONS.map((c) => (\n <option key={c} value={c} className=\"capitalize\">\n {c}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className={labelClasses}>Time Spent (minutes)</label>\n <input\n type=\"number\"\n min=\"0\"\n placeholder=\"0\"\n value={appMinutes}\n onChange={(e) => setAppMinutes(e.target.value)}\n className={inputClasses}\n />\n </div>\n </div>\n <div>\n <label className={labelClasses}>Sessions</label>\n <input\n type=\"number\"\n min=\"0\"\n placeholder=\"0\"\n value={appSessions}\n onChange={(e) => setAppSessions(e.target.value)}\n className={inputClasses}\n />\n </div>\n </div>\n </Modal>\n\n {/* Recategorize modal */}\n <Modal\n open={recatApp !== null}\n onClose={() => setRecatApp(null)}\n title=\"Recategorize Application\"\n description={recatApp?.applicationName}\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRecatApp(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={handleRecategorize}\n disabled={categorizing}\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 {categorizing ? \"Saving...\" : \"Save\"}\n </button>\n </>\n }\n >\n {categorizeError && (\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 recategorize. Please try again.\n </div>\n )}\n <label className={labelClasses}>Category</label>\n <select\n value={recatCategory}\n onChange={(e) => setRecatCategory(e.target.value)}\n className={inputClasses}\n >\n {CATEGORY_OPTIONS.map((c) => (\n <option key={c} value={c} className=\"capitalize\">\n {c}\n </option>\n ))}\n </select>\n <p className=\"mt-3 text-xs text-text-secondary\">\n This updates the category for all usage of &ldquo;\n {recatApp?.applicationName}&rdquo;.\n </p>\n </Modal>\n\n {/* Delete confirm modal */}\n <Modal\n open={deleteApp !== null}\n onClose={() => setDeleteApp(null)}\n title=\"Delete Usage Entry\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteApp(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 usage entry for{\" \"}\n <span className=\"font-medium text-text-primary\">\n {deleteApp?.applicationName}\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":"ApplicationUsagePage.js","names":[],"sources":["../../../../src/timecampus/pages/application-usage/ApplicationUsagePage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { Monitor, Clock, Plus, Tag, Trash2 } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n ChartCard,\n DonutChart,\n TrendChart,\n chartTokens,\n type ChartDatum,\n type DonutSlice,\n type ValueFormatter,\n} from \"../../components\";\nimport { formatHours, formatTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListApplicationUsagesQuery,\n useGetAppUsageSummaryQuery,\n useRecordApplicationUsageMutation,\n useCategorizeApplicationMutation,\n useDeleteApplicationUsageMutation,\n type ApplicationUsageFieldsFragment,\n type ListApplicationUsagesQuery,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 50;\n\nconst CATEGORY_OPTIONS = [\n \"productive\",\n \"neutral\",\n \"communication\",\n \"distracting\",\n] as const;\n\nconst CATEGORY_VARIANT: Record<string, \"success\" | \"muted\" | \"info\" | \"error\"> =\n {\n productive: \"success\",\n neutral: \"muted\",\n communication: \"info\",\n distracting: \"error\",\n };\n\n/** Category ring hues — token-only, echoing productive→positive / distracting→negative. */\nconst CATEGORY_SLICE_COLOR: Record<string, string> = {\n productive: chartTokens.positive,\n neutral: chartTokens.series[3],\n distracting: chartTokens.negative,\n};\n\nconst formatChartHours: ValueFormatter = (value) =>\n `${value.toFixed(value >= 10 ? 0 : 1)}h`;\n\nconst formatSecondsAsHours: ValueFormatter = (value) =>\n formatHours(value / 3600);\n\nconst formatDayTick = (value: string | number): string => {\n const parsed = new Date(String(value));\n return Number.isNaN(parsed.getTime())\n ? String(value)\n : parsed.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" });\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 todayIso(): string {\n return new Date().toISOString();\n}\n\n/**\n * The window an app was actually in use for, when the capturing client\n * recorded one. Both timestamps are optional on `ApplicationUsage`, so a\n * half-known window is shown as nothing rather than as a guessed bound.\n */\nfunction usageWindow(\n firstUsed: string | null | undefined,\n lastUsed: string | null | undefined,\n): string | null {\n if (!firstUsed || !lastUsed) return null;\n return `${formatTime(firstUsed)} – ${formatTime(lastUsed)}`;\n}\n\nexport function ApplicationUsagePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: usagesData,\n loading: usagesLoading,\n error: usagesError,\n fetchMore,\n refetch: refetchUsages,\n } = useListApplicationUsagesQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n skip: !workspaceId,\n notifyOnNetworkStatusChange: true,\n });\n\n const {\n data: summaryData,\n loading: summaryLoading,\n error: summaryError,\n refetch: refetchSummary,\n } = useGetAppUsageSummaryQuery({\n skip: !workspaceId,\n });\n\n const loading = usagesLoading || summaryLoading;\n const error = usagesError || summaryError;\n const apps = usagesData?.applicationUsages ?? [];\n const summary = summaryData?.appUsageSummary;\n const hasMore = apps.length > 0 && apps.length % PAGE_SIZE === 0;\n\n const [loadingMore, setLoadingMore] = useState(false);\n\n // Add-usage modal\n const [addOpen, setAddOpen] = useState(false);\n const [appName, setAppName] = useState(\"\");\n const [appCategory, setAppCategory] = useState<string>(\"productive\");\n const [appMinutes, setAppMinutes] = useState(\"\");\n const [appSessions, setAppSessions] = useState(\"\");\n\n // Recategorize + delete\n const [recatApp, setRecatApp] =\n useState<ApplicationUsageFieldsFragment | null>(null);\n const [recatCategory, setRecatCategory] = useState<string>(\"productive\");\n const [deleteApp, setDeleteApp] =\n useState<ApplicationUsageFieldsFragment | null>(null);\n\n const [recordUsage, { loading: recording, error: recordError }] =\n useRecordApplicationUsageMutation();\n const [categorize, { loading: categorizing, error: categorizeError }] =\n useCategorizeApplicationMutation();\n const [removeUsage, { loading: deleting, error: deleteError }] =\n useDeleteApplicationUsageMutation();\n\n const refreshAll = async () => {\n await Promise.all([refetchUsages(), refetchSummary()]);\n };\n\n const openAdd = () => {\n setAppName(\"\");\n setAppCategory(\"productive\");\n setAppMinutes(\"\");\n setAppSessions(\"\");\n setAddOpen(true);\n };\n\n const handleRecord = async () => {\n if (!appName.trim() || !appMinutes) return;\n await recordUsage({\n variables: {\n input: {\n applicationName: appName.trim(),\n category: appCategory,\n timeSpent: Math.round(Number(appMinutes) * 60),\n sessions: appSessions ? Number(appSessions) : undefined,\n date: todayIso(),\n },\n },\n });\n setAddOpen(false);\n await refreshAll();\n };\n\n const openRecat = (app: ApplicationUsageFieldsFragment) => {\n setRecatApp(app);\n setRecatCategory(app.category);\n };\n\n const handleRecategorize = async () => {\n if (!recatApp) return;\n await categorize({\n variables: {\n applicationName: recatApp.applicationName,\n category: recatCategory,\n },\n });\n setRecatApp(null);\n await refreshAll();\n };\n\n const handleDelete = async () => {\n if (!deleteApp) return;\n await removeUsage({ variables: { id: deleteApp.id } });\n setDeleteApp(null);\n await refreshAll();\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { limit: PAGE_SIZE, offset: apps.length } },\n updateQuery: (\n prev: ListApplicationUsagesQuery,\n { fetchMoreResult }: { fetchMoreResult?: ListApplicationUsagesQuery },\n ) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n applicationUsages: [\n ...prev.applicationUsages,\n ...fetchMoreResult.applicationUsages,\n ],\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n const totalScreenTimeSeconds = apps.reduce(\n (sum: number, app: ApplicationUsageFieldsFragment) => sum + app.timeSpent,\n 0,\n );\n const totalSwitches = apps.reduce(\n (sum: number, app: ApplicationUsageFieldsFragment) => sum + app.switches,\n 0,\n );\n const productiveRatio =\n summary?.productivityRatio != null\n ? `${Math.round(summary.productivityRatio * 100)}%`\n : \"--\";\n\n // Count focus sessions (apps with > 90 min continuous usage)\n const focusSessions = apps.filter(\n (app: ApplicationUsageFieldsFragment) => app.timeSpent >= 5400,\n ).length;\n\n // Rank the loaded apps by time spent (drives the numbered list).\n const rankedApps = [...apps].sort(\n (a: ApplicationUsageFieldsFragment, b: ApplicationUsageFieldsFragment) =>\n b.timeSpent - a.timeSpent,\n );\n\n // Productive / neutral / distracting split from the real usage summary.\n const categorySlices: DonutSlice[] = summary\n ? [\n {\n name: \"Productive\",\n value: summary.productiveTime,\n color: CATEGORY_SLICE_COLOR.productive,\n },\n {\n name: \"Neutral\",\n value: summary.neutralTime,\n color: CATEGORY_SLICE_COLOR.neutral,\n },\n {\n name: \"Distracting\",\n value: summary.distractingTime,\n color: CATEGORY_SLICE_COLOR.distracting,\n },\n ]\n : [];\n const categoryTotalSeconds =\n (summary?.productiveTime ?? 0) +\n (summary?.neutralTime ?? 0) +\n (summary?.distractingTime ?? 0);\n\n // Daily usage trend — sum the loaded apps' tracked time per calendar day.\n const usageByDay = new Map<string, number>();\n for (const app of apps as ApplicationUsageFieldsFragment[]) {\n const day = (app.date ?? \"\").slice(0, 10);\n if (!day) continue;\n usageByDay.set(day, (usageByDay.get(day) ?? 0) + app.timeSpent);\n }\n const usageTrend: ChartDatum[] = Array.from(usageByDay.entries())\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([day, seconds]) => ({\n day,\n hours: Math.round((seconds / 3600) * 100) / 100,\n }));\n\n if (loading) {\n return (\n <PageLayout\n title=\"App Usage\"\n description=\"Track application usage and productivity\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"App Usage\"\n description=\"Track application usage and productivity\"\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 application usage. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"App Usage\"\n description=\"Track application usage and productivity\"\n purpose=\"See where your screen time actually goes — which apps are productive, how often you context-switch, and how many real focus sessions you get — so you can cut the digital distractions eating your day.\"\n actions={\n <button\n onClick={openAdd}\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 Usage\n </button>\n }\n >\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Screen Time\"\n value={formatHours(totalScreenTimeSeconds / 3600)}\n icon={<Monitor className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Productive Time\"\n value={productiveRatio}\n description=\"Share of tracked time\"\n />\n <StatCard\n title=\"App Switches\"\n value={String(totalSwitches)}\n description={`Avg. ${apps.length > 0 ? Math.round(totalSwitches / apps.length) : 0}/app`}\n />\n <StatCard\n title=\"Focus Sessions\"\n value={String(focusSessions)}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"90+ min each\"\n />\n </div>\n\n {apps.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No application usage data found\"\n description=\"Record app usage manually or connect the desktop tracker, and your apps will appear here ranked by time spent.\"\n action={\n <button\n onClick={openAdd}\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 Usage\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"glass\" className=\"overflow-hidden\">\n <div className=\"flex items-center justify-between gap-3 border-b border-border-seam px-4 py-3 sm:px-6\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Ranked Applications\n </h3>\n <span className=\"text-xs text-text-secondary\">\n {summary?.totalApps != null\n ? `${apps.length} of ${summary.totalApps} app${summary.totalApps === 1 ? \"\" : \"s\"} tracked`\n : `${apps.length} tracked`}\n </span>\n </div>\n <div className=\"divide-y divide-border\">\n {rankedApps.map(\n (app: ApplicationUsageFieldsFragment, index: number) => {\n const share = Math.min(\n 100,\n Math.round(\n (app.timeSpent / (totalScreenTimeSeconds || 1)) * 100,\n ),\n );\n const activeWindow = usageWindow(\n app.firstUsed,\n app.lastUsed,\n );\n return (\n <div\n key={app.id}\n className=\"flex items-center gap-3 p-4 transition-colors hover:bg-bg-sunken/50 sm:gap-4\"\n >\n <span className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold tabular-nums text-text-secondary\">\n {index + 1}\n </span>\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <Monitor className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {app.applicationName}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {app.sessions} sessions &middot; {app.switches}{\" \"}\n switches\n {activeWindow !== null && (\n <>\n {\" \"}\n &middot;{\" \"}\n <span className=\"tabular-nums\">\n {activeWindow}\n </span>\n </>\n )}\n </p>\n </div>\n <div className=\"hidden w-28 shrink-0 lg:block\">\n <div className=\"h-2 rounded-full bg-bg-sunken\">\n <div\n className=\"h-2 rounded-full bg-action-primary-bg\"\n style={{ width: `${share}%` }}\n />\n </div>\n <p className=\"mt-1 text-right text-xs tabular-nums text-text-secondary\">\n {share}%\n </p>\n </div>\n <StatusBadge\n label={app.category}\n variant={CATEGORY_VARIANT[app.category] ?? \"muted\"}\n className=\"hidden capitalize sm:inline-flex\"\n />\n <p className=\"shrink-0 font-mono text-sm font-medium tabular-nums text-text-primary\">\n {formatHours(app.timeSpent / 3600)}\n </p>\n <div className=\"flex shrink-0 items-center gap-1\">\n <button\n onClick={() => openRecat(app)}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Recategorize application\"\n >\n <Tag className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setDeleteApp(app)}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Delete usage entry\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n );\n },\n )}\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title=\"Productivity Split\"\n description=\"Where your tracked screen time goes\"\n >\n <DonutChart\n data={categorySlices}\n centerLabel=\"Total tracked\"\n centerValue={categoryTotalSeconds}\n valueFormat={formatSecondsAsHours}\n height={260}\n emptyLabel=\"No categorized usage yet\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Daily Usage Trend\"\n description=\"Tracked time per day\"\n >\n <TrendChart\n data={usageTrend}\n xKey=\"day\"\n series={[{ key: \"hours\", label: \"Screen time\" }]}\n type=\"area\"\n valueFormat={formatChartHours}\n xTickFormat={formatDayTick}\n height={260}\n emptyLabel=\"No usage recorded yet\"\n />\n </ChartCard>\n </div>\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 </div>\n\n {/* Add usage modal */}\n <Modal\n open={addOpen}\n onClose={() => setAddOpen(false)}\n title=\"Record App Usage\"\n description=\"Manually log time spent in an application\"\n footer={\n <>\n <button\n onClick={() => setAddOpen(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={handleRecord}\n disabled={recording || !appName.trim() || !appMinutes}\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 {recording ? \"Saving...\" : \"Record\"}\n </button>\n </>\n }\n >\n {recordError && (\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 record usage. Please try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className={labelClasses}>Application Name</label>\n <input\n type=\"text\"\n placeholder=\"e.g. Visual Studio Code\"\n value={appName}\n onChange={(e) => setAppName(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}>Category</label>\n <select\n value={appCategory}\n onChange={(e) => setAppCategory(e.target.value)}\n className={inputClasses}\n >\n {CATEGORY_OPTIONS.map((c) => (\n <option key={c} value={c} className=\"capitalize\">\n {c}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className={labelClasses}>Time Spent (minutes)</label>\n <input\n type=\"number\"\n min=\"0\"\n placeholder=\"0\"\n value={appMinutes}\n onChange={(e) => setAppMinutes(e.target.value)}\n className={inputClasses}\n />\n </div>\n </div>\n <div>\n <label className={labelClasses}>Sessions</label>\n <input\n type=\"number\"\n min=\"0\"\n placeholder=\"0\"\n value={appSessions}\n onChange={(e) => setAppSessions(e.target.value)}\n className={inputClasses}\n />\n </div>\n </div>\n </Modal>\n\n {/* Recategorize modal */}\n <Modal\n open={recatApp !== null}\n onClose={() => setRecatApp(null)}\n title=\"Recategorize Application\"\n description={recatApp?.applicationName}\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRecatApp(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={handleRecategorize}\n disabled={categorizing}\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 {categorizing ? \"Saving...\" : \"Save\"}\n </button>\n </>\n }\n >\n {categorizeError && (\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 recategorize. Please try again.\n </div>\n )}\n <label className={labelClasses}>Category</label>\n <select\n value={recatCategory}\n onChange={(e) => setRecatCategory(e.target.value)}\n className={inputClasses}\n >\n {CATEGORY_OPTIONS.map((c) => (\n <option key={c} value={c} className=\"capitalize\">\n {c}\n </option>\n ))}\n </select>\n <p className=\"mt-3 text-xs text-text-secondary\">\n This updates the category for all usage of &ldquo;\n {recatApp?.applicationName}&rdquo;.\n </p>\n </Modal>\n\n {/* Delete confirm modal */}\n <Modal\n open={deleteApp !== null}\n onClose={() => setDeleteApp(null)}\n title=\"Delete Usage Entry\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteApp(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 usage entry for{\" \"}\n <span className=\"font-medium text-text-primary\">\n {deleteApp?.applicationName}\n </span>\n ? This action cannot be undone.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA8BA,IAAM,IAAY,IAEZ,IAAmB;CACvB;CACA;CACA;CACA;AACF,GAEM,KACJ;CACE,YAAY;CACZ,SAAS;CACT,eAAe;CACf,aAAa;AACf,GAGI,IAA+C;CACnD,YAAY,EAAY;CACxB,SAAS,EAAY,OAAO;CAC5B,aAAa,EAAY;AAC3B,GAEM,MAAoC,MACxC,GAAG,EAAM,QAAQ,KAAS,KAAK,IAAI,CAAC,EAAE,IAElC,MAAwC,MAC5C,EAAY,IAAQ,IAAI,GAEpB,MAAiB,MAAmC;CACxD,IAAM,IAAS,IAAI,KAAK,OAAO,CAAK,CAAC;CACrC,OAAO,OAAO,MAAM,EAAO,QAAQ,CAAC,IAChC,OAAO,CAAK,IACZ,EAAO,mBAAmB,SAAS;EAAE,OAAO;EAAS,KAAK;CAAU,CAAC;AAC3E,GAEM,IACJ,wIACI,IAAe;AAErB,SAAS,KAAmB;CAC1B,wBAAO,IAAI,KAAK,EAAA,CAAE,YAAY;AAChC;AAOA,SAAS,GACP,GACA,GACe;CAEf,OADI,CAAC,KAAa,CAAC,IAAiB,OAC7B,GAAG,EAAW,CAAS,EAAE,KAAK,EAAW,CAAQ;AAC1D;AAEA,SAAgB,IAAuB;CACrC,IAAM,EAAE,iBAAc,OAAO,EAAc,GAErC,EACJ,MAAM,GACN,SAAS,GACT,OAAO,IACP,eACA,SAAS,OACP,GAA8B;EAChC,WAAW,EAAE,YAAY;GAAE,OAAO;GAAW,QAAQ;EAAE,EAAE;EACzD,MAAM,CAAC;EACP,6BAA6B;CAC/B,CAAC,GAEK,EACJ,MAAM,IACN,SAAS,IACT,OAAO,IACP,SAAS,OACP,GAA2B,EAC7B,MAAM,CAAC,EACT,CAAC,GAEK,KAAU,KAAiB,IAC3B,KAAQ,MAAe,IACvB,IAAO,GAAY,qBAAqB,CAAC,GACzC,IAAU,IAAa,iBACvB,IAAU,EAAK,SAAS,KAAK,EAAK,SAAS,MAAc,GAEzD,CAAC,GAAa,KAAkB,EAAS,EAAK,GAG9C,CAAC,GAAS,KAAc,EAAS,EAAK,GACtC,CAAC,GAAS,KAAc,EAAS,EAAE,GACnC,CAAC,GAAa,KAAkB,EAAiB,YAAY,GAC7D,CAAC,GAAY,KAAiB,EAAS,EAAE,GACzC,CAAC,GAAa,KAAkB,EAAS,EAAE,GAG3C,CAAC,GAAU,KACf,EAAgD,IAAI,GAChD,CAAC,GAAe,KAAoB,EAAiB,YAAY,GACjE,CAAC,GAAW,KAChB,EAAgD,IAAI,GAEhD,CAAC,IAAa,EAAE,SAAS,GAAW,OAAO,QAC/C,GAAkC,GAC9B,CAAC,IAAY,EAAE,SAAS,GAAc,OAAO,QACjD,GAAiC,GAC7B,CAAC,IAAa,EAAE,SAAS,GAAU,OAAO,QAC9C,GAAkC,GAE9B,IAAa,YAAY;EAC7B,MAAM,QAAQ,IAAI,CAAC,GAAc,GAAG,GAAe,CAAC,CAAC;CACvD,GAEM,UAAgB;EAKpB,AAJA,EAAW,EAAE,GACb,EAAe,YAAY,GAC3B,EAAc,EAAE,GAChB,EAAe,EAAE,GACjB,EAAW,EAAI;CACjB,GAEM,KAAe,YAAY;EAC3B,CAAC,EAAQ,KAAK,KAAK,CAAC,MACxB,MAAM,GAAY,EAChB,WAAW,EACT,OAAO;GACL,iBAAiB,EAAQ,KAAK;GAC9B,UAAU;GACV,WAAW,KAAK,MAAM,OAAO,CAAU,IAAI,EAAE;GAC7C,UAAU,IAAc,OAAO,CAAW,IAAI,KAAA;GAC9C,MAAM,GAAS;EACjB,EACF,EACF,CAAC,GACD,EAAW,EAAK,GAChB,MAAM,EAAW;CACnB,GAEM,MAAa,MAAwC;EAEzD,AADA,EAAY,CAAG,GACf,EAAiB,EAAI,QAAQ;CAC/B,GAEM,KAAqB,YAAY;EAChC,MACL,MAAM,GAAW,EACf,WAAW;GACT,iBAAiB,EAAS;GAC1B,UAAU;EACZ,EACF,CAAC,GACD,EAAY,IAAI,GAChB,MAAM,EAAW;CACnB,GAEM,IAAe,YAAY;EAC1B,MACL,MAAM,GAAY,EAAE,WAAW,EAAE,IAAI,EAAU,GAAG,EAAE,CAAC,GACrD,EAAa,IAAI,GACjB,MAAM,EAAW;CACnB,GAEM,KAAiB,YAAY;EACjC,EAAe,EAAI;EACnB,IAAI;GACF,MAAM,GAAU;IACd,WAAW,EAAE,YAAY;KAAE,OAAO;KAAW,QAAQ,EAAK;IAAO,EAAE;IACnE,cACE,GACA,EAAE,yBAEG,IACE;KACL,GAAG;KACH,mBAAmB,CACjB,GAAG,EAAK,mBACR,GAAG,EAAgB,iBACrB;IACF,IAP6B;GASjC,CAAC;EACH,UAAU;GACR,EAAe,EAAK;EACtB;CACF,GAEM,IAAyB,EAAK,QACjC,GAAa,MAAwC,IAAM,EAAI,WAChE,CACF,GACM,IAAgB,EAAK,QACxB,GAAa,MAAwC,IAAM,EAAI,UAChE,CACF,GACM,KACJ,GAAS,qBAAqB,OAE1B,OADA,GAAG,KAAK,MAAM,EAAQ,oBAAoB,GAAG,EAAE,IAI/C,KAAgB,EAAK,QACxB,MAAwC,EAAI,aAAa,IAC5D,CAAC,CAAC,QAGI,KAAa,CAAC,GAAG,CAAI,CAAC,CAAC,MAC1B,GAAmC,MAClC,EAAE,YAAY,EAAE,SACpB,GAGM,KAA+B,IACjC;EACE;GACE,MAAM;GACN,OAAO,EAAQ;GACf,OAAO,EAAqB;EAC9B;EACA;GACE,MAAM;GACN,OAAO,EAAQ;GACf,OAAO,EAAqB;EAC9B;EACA;GACE,MAAM;GACN,OAAO,EAAQ;GACf,OAAO,EAAqB;EAC9B;CACF,IACA,CAAC,GACC,MACH,GAAS,kBAAkB,MAC3B,GAAS,eAAe,MACxB,GAAS,mBAAmB,IAGzB,oBAAa,IAAI,IAAoB;CAC3C,KAAK,IAAM,KAAO,GAA0C;EAC1D,IAAM,KAAO,EAAI,QAAQ,GAAA,CAAI,MAAM,GAAG,EAAE;EACnC,KACL,EAAW,IAAI,IAAM,EAAW,IAAI,CAAG,KAAK,KAAK,EAAI,SAAS;CAChE;CACA,IAAM,KAA2B,MAAM,KAAK,EAAW,QAAQ,CAAC,CAAC,CAC9D,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,CACtC,KAAK,CAAC,GAAK,QAAc;EACxB;EACA,OAAO,KAAK,MAAO,IAAU,OAAQ,GAAG,IAAI;CAC9C,EAAE;CA0BJ,OAxBI,KAEA,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EAEZ,UAAA,kBAAC,GAAD;GAAiB,SAAQ;GAAO,OAAO;EAAI,CAAA;CACjC,CAAA,IAIZ,KAEA,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EAEZ,UAAA,kBAAC,OAAD;GAAK,WAAU;GAA2G,UAAA;EAErH,CAAA;CACK,CAAA,IAKd,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SAAQ;EACR,SACE,kBAAC,UAAD;GACE,SAAS;GACT,WAAU;GAFZ,UAAA,CAIE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,GAAC,WAEtB;;EAXZ,UAAA;GAcE,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,kBAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,kBAAC,GAAD;OACE,UAAA;OACA,OAAM;OACN,OAAO,EAAY,IAAyB,IAAI;OAChD,MAAM,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;MACrC,CAAA;MACD,kBAAC,GAAD;OACE,OAAM;OACN,OAAO;OACP,aAAY;MACb,CAAA;MACD,kBAAC,GAAD;OACE,OAAM;OACN,OAAO,OAAO,CAAa;OAC3B,aAAa,QAAQ,EAAK,SAAS,IAAI,KAAK,MAAM,IAAgB,EAAK,MAAM,IAAI,EAAE;MACpF,CAAA;MACD,kBAAC,GAAD;OACE,OAAM;OACN,OAAO,OAAO,EAAa;OAC3B,MAAM,kBAAC,IAAD,EAAO,WAAU,UAAW,CAAA;OAClC,aAAY;MACb,CAAA;KACE;IAEJ,CAAA,GAAA,EAAK,WAAW,IACf,kBAAC,IAAD;KACE,cAAa;KACb,OAAM;KACN,aAAY;KACZ,QACE,kBAAC,UAAD;MACE,SAAS;MACT,WAAU;MAFZ,UAAA,CAIE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,GAAC,WAEtB;;IAEX,CAAA,IAED,kBAAA,GAAA,EAAA,UAAA;KACE,kBAAC,IAAD;MAAW,WAAU;MAAQ,WAAU;MAAvC,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CACE,kBAAC,MAAD;QAAI,WAAU;QAA0C,UAAA;OAEpD,CAAA,GACJ,kBAAC,QAAD;QAAM,WAAU;QACb,UAAA,GAAS,aAAa,OAEnB,GAAG,EAAK,OAAO,YADf,GAAG,EAAK,OAAO,MAAM,EAAQ,UAAU,MAAM,EAAQ,cAAc,IAAI,KAAK,IAAI;OAEhF,CAAA,CACH;MACL,CAAA,GAAA,kBAAC,OAAD;OAAK,WAAU;OACZ,UAAA,GAAW,KACT,GAAqC,MAAkB;QACtD,IAAM,IAAQ,KAAK,IACjB,KACA,KAAK,MACF,EAAI,aAAa,KAA0B,KAAM,GACpD,CACF,GACM,IAAe,GACnB,EAAI,WACJ,EAAI,QACN;QACA,OACE,kBAAC,OAAD;SAEE,WAAU;SAFZ,UAAA;UAIE,kBAAC,QAAD;WAAM,WAAU;WACb,UAAA,IAAQ;UACL,CAAA;UACN,kBAAC,OAAD;WAAK,WAAU;WACb,UAAA,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;UAC3B,CAAA;UACL,kBAAC,OAAD;WAAK,WAAU;WAAf,UAAA,CACE,kBAAC,KAAD;YAAG,WAAU;YACV,UAAA,EAAI;WACJ,CAAA,GACH,kBAAC,KAAD;YAAG,WAAU;YAAb,UAAA;aACG,EAAI;aAAS;aAAoB,EAAI;aAAU;aAAI;aAEnD,MAAiB,QAChB,kBAAA,GAAA,EAAA,UAAA;cACG;cAAI;cACI;cACT,kBAAC,QAAD;eAAM,WAAU;eACb,UAAA;cACG,CAAA;aACN,EAAA,CAAA;YAEH;WACA,CAAA,CAAA;;UACL,kBAAC,OAAD;WAAK,WAAU;WAAf,UAAA,CACE,kBAAC,OAAD;YAAK,WAAU;YACb,UAAA,kBAAC,OAAD;aACE,WAAU;aACV,OAAO,EAAE,OAAO,GAAG,EAAM,GAAG;YAC7B,CAAA;WACE,CAAA,GACL,kBAAC,KAAD;YAAG,WAAU;YAAb,UAAA,CACG,GAAM,GACN;WACA,CAAA,CAAA;;UACL,kBAAC,GAAD;WACE,OAAO,EAAI;WACX,SAAS,GAAiB,EAAI,aAAa;WAC3C,WAAU;UACX,CAAA;UACD,kBAAC,KAAD;WAAG,WAAU;WACV,UAAA,EAAY,EAAI,YAAY,IAAI;UAChC,CAAA;UACH,kBAAC,OAAD;WAAK,WAAU;WAAf,UAAA,CACE,kBAAC,UAAD;YACE,eAAe,GAAU,CAAG;YAC5B,WAAU;YACV,cAAW;YAEX,UAAA,kBAAC,GAAD,EAAK,WAAU,UAAW,CAAA;WACpB,CAAA,GACR,kBAAC,UAAD;YACE,eAAe,EAAa,CAAG;YAC/B,WAAU;YACV,cAAW;YAEX,UAAA,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;WACvB,CAAA,CACL;;SACF;QA9DE,GAAA,EAAI,EA8DN;OAET,CACF;MACG,CAAA,CACI;;KAEX,kBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,kBAAC,GAAD;OACE,OAAM;OACN,aAAY;OAEZ,UAAA,kBAAC,IAAD;QACE,MAAM;QACN,aAAY;QACZ,aAAa;QACb,aAAa;QACb,QAAQ;QACR,YAAW;OACZ,CAAA;MACQ,CAAA,GAEX,kBAAC,GAAD;OACE,OAAM;OACN,aAAY;OAEZ,UAAA,kBAAC,IAAD;QACE,MAAM;QACN,MAAK;QACL,QAAQ,CAAC;SAAE,KAAK;SAAS,OAAO;QAAc,CAAC;QAC/C,MAAK;QACL,aAAa;QACb,aAAa;QACb,QAAQ;QACR,YAAW;OACZ,CAAA;MACQ,CAAA,CACR;;KACJ,KACC,kBAAC,OAAD;MAAK,WAAU;MACb,UAAA,kBAAC,UAAD;OACE,SAAS;OACT,UAAU;OACV,WAAU;OAET,UAAA,IAAc,eAAe;MACxB,CAAA;KACL,CAAA;IAEP,EAAA,CAAA,CAED;;GAGL,kBAAC,GAAD;IACE,MAAM;IACN,eAAe,EAAW,EAAK;IAC/B,OAAM;IACN,aAAY;IACZ,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,eAAe,EAAW,EAAK;KAC/B,WAAU;KACX,UAAA;IAEO,CAAA,GACR,kBAAC,UAAD;KACE,SAAS;KACT,UAAU,KAAa,CAAC,EAAQ,KAAK,KAAK,CAAC;KAC3C,WAAU;KAET,UAAA,IAAY,cAAc;IACrB,CAAA,CACR,EAAA,CAAA;IApBN,UAAA,CAuBG,MACC,kBAAC,OAAD;KAAK,WAAU;KAAyG,UAAA;IAEnH,CAAA,GAEP,kBAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,WAAW;OAAc,UAAA;MAAuB,CAAA,GACvD,kBAAC,SAAD;OACE,MAAK;OACL,aAAY;OACZ,OAAO;OACP,WAAW,MAAM,EAAW,EAAE,OAAO,KAAK;OAC1C,WAAW;MACZ,CAAA,CACE,EAAA,CAAA;MACL,kBAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,WAAW;QAAc,UAAA;OAAe,CAAA,GAC/C,kBAAC,UAAD;QACE,OAAO;QACP,WAAW,MAAM,EAAe,EAAE,OAAO,KAAK;QAC9C,WAAW;QAEV,UAAA,EAAiB,KAAK,MACrB,kBAAC,UAAD;SAAgB,OAAO;SAAG,WAAU;SACjC,UAAA;QACK,GAFK,CAEL,CACT;OACK,CAAA,CACL,EAAA,CAAA,GACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,WAAW;QAAc,UAAA;OAA2B,CAAA,GAC3D,kBAAC,SAAD;QACE,MAAK;QACL,KAAI;QACJ,aAAY;QACZ,OAAO;QACP,WAAW,MAAM,EAAc,EAAE,OAAO,KAAK;QAC7C,WAAW;OACZ,CAAA,CACE,EAAA,CAAA,CACF;;MACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,WAAW;OAAc,UAAA;MAAe,CAAA,GAC/C,kBAAC,SAAD;OACE,MAAK;OACL,KAAI;OACJ,aAAY;OACZ,OAAO;OACP,WAAW,MAAM,EAAe,EAAE,OAAO,KAAK;OAC9C,WAAW;MACZ,CAAA,CACE,EAAA,CAAA;KACF;IACA,CAAA,CAAA;;GAGP,kBAAC,GAAD;IACE,MAAM,MAAa;IACnB,eAAe,EAAY,IAAI;IAC/B,OAAM;IACN,aAAa,GAAU;IACvB,MAAK;IACL,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,eAAe,EAAY,IAAI;KAC/B,WAAU;KACX,UAAA;IAEO,CAAA,GACR,kBAAC,UAAD;KACE,SAAS;KACT,UAAU;KACV,WAAU;KAET,UAAA,IAAe,cAAc;IACxB,CAAA,CACR,EAAA,CAAA;IArBN,UAAA;KAwBG,MACC,kBAAC,OAAD;MAAK,WAAU;MAAyG,UAAA;KAEnH,CAAA;KAEP,kBAAC,SAAD;MAAO,WAAW;MAAc,UAAA;KAAe,CAAA;KAC/C,kBAAC,UAAD;MACE,OAAO;MACP,WAAW,MAAM,EAAiB,EAAE,OAAO,KAAK;MAChD,WAAW;MAEV,UAAA,EAAiB,KAAK,MACrB,kBAAC,UAAD;OAAgB,OAAO;OAAG,WAAU;OACjC,UAAA;MACK,GAFK,CAEL,CACT;KACK,CAAA;KACR,kBAAC,KAAD;MAAG,WAAU;MAAb,UAAA;OAAgD;OAE7C,GAAU;OAAgB;MAC1B;;IACE;;GAGP,kBAAC,GAAD;IACE,MAAM,MAAc;IACpB,eAAe,EAAa,IAAI;IAChC,OAAM;IACN,MAAK;IACL,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,eAAe,EAAa,IAAI;KAChC,WAAU;KACX,UAAA;IAEO,CAAA,GACR,kBAAC,UAAD;KACE,SAAS;KACT,UAAU;KACV,WAAU;KAET,UAAA,IAAW,gBAAgB;IACtB,CAAA,CACR,EAAA,CAAA;IApBN,UAAA,CAuBG,MACC,kBAAC,OAAD;KAAK,WAAU;KAAyG,UAAA;IAEnH,CAAA,GAEP,kBAAC,KAAD;KAAG,WAAU;KAAb,UAAA;MAA2C;MACW;MACpD,kBAAC,QAAD;OAAM,WAAU;OACb,UAAA,GAAW;MACR,CAAA;MAAC;KAEN;IACE,CAAA,CAAA;;EACG;;AAEhB"}
@@ -1,10 +1,10 @@
1
1
  import { cn as e, formatDate as t, formatNumber as n, getInitials as r } from "../../utils/helpers.js";
2
2
  import { StatCard as i } from "../../components/StatCard.js";
3
3
  import { LoadingSkeleton as a } from "../../components/LoadingSkeleton.js";
4
- import { useCreateClientPortalConfigMutation as o, useDeleteClientPortalConfigMutation as s, useGetClientPortalStatsQuery as ee, useListClientPortalConfigsQuery as te, useRegenerateClientPortalShareTokenMutation as ne, useToggleClientPortalMutation as re, useUpdateClientPortalConfigMutation as ie } from "../../../generated/wspace-operations.js";
4
+ import { EmptyState as o } from "../../components/EmptyState.js";
5
+ import { useCreateClientPortalConfigMutation as s, useDeleteClientPortalConfigMutation as ee, useGetClientPortalStatsQuery as te, useListClientPortalConfigsQuery as ne, useRegenerateClientPortalShareTokenMutation as re, useToggleClientPortalMutation as ie, useUpdateClientPortalConfigMutation as ae } from "../../../generated/wspace-operations.js";
5
6
  import { PageLayout as c } from "../../components/PageLayout.js";
6
- import { StatusBadge as ae } from "../../components/StatusBadge.js";
7
- import { EmptyState as oe } from "../../components/EmptyState.js";
7
+ import { StatusBadge as oe } from "../../components/StatusBadge.js";
8
8
  import { Modal as l } from "../../components/Modal.js";
9
9
  import { ChartCard as u } from "../../components/ChartCard.js";
10
10
  import { ToastContainer as se, useToast as ce } from "../../components/Toast.js";
@@ -42,13 +42,13 @@ var x = {
42
42
  logoUrl: ""
43
43
  };
44
44
  function S() {
45
- let { toasts: h, addToast: S, dismissToast: C } = ce(), w = be(), [T, E] = d(null), [D, O] = d(!1), { data: k, loading: De, error: Oe, refetch: A, fetchMore: ke } = te({
45
+ let { toasts: h, addToast: S, dismissToast: C } = ce(), w = be(), [T, E] = d(null), [D, O] = d(!1), { data: k, loading: De, error: Oe, refetch: A, fetchMore: ke } = ne({
46
46
  variables: { pagination: {
47
47
  limit: y,
48
48
  offset: 0
49
49
  } },
50
50
  notifyOnNetworkStatusChange: !0
51
- }), { data: Ae, refetch: je } = ee(), [Me, { loading: j }] = ne(), [Ne, { loading: M }] = o(), [N, { loading: P }] = ie(), [Pe, { loading: F }] = s(), [Fe] = re(), I = k?.clientPortalConfigs?.items ?? [], Ie = k?.clientPortalConfigs?.hasMore ?? !1, L = Ae?.clientPortalStats, R = (e) => e.projects.reduce((e, t) => e + t.trackedHours, 0), z = (L?.totalClients ?? 0) - (L?.activeClients ?? 0), Le = I.map((e) => ({
51
+ }), { data: Ae, refetch: je } = te(), [Me, { loading: j }] = re(), [Ne, { loading: M }] = s(), [N, { loading: P }] = ae(), [Pe, { loading: F }] = ee(), [Fe] = ie(), I = k?.clientPortalConfigs?.items ?? [], Ie = k?.clientPortalConfigs?.hasMore ?? !1, L = Ae?.clientPortalStats, R = (e) => e.projects.reduce((e, t) => e + t.trackedHours, 0), z = (L?.totalClients ?? 0) - (L?.activeClients ?? 0), Le = I.map((e) => ({
52
52
  name: e.clientName,
53
53
  hours: R(e)
54
54
  })).filter((e) => e.hours > 0).sort((e, t) => t.hours - e.hours).slice(0, 8), B = I.reduce((e, t) => Math.max(e, R(t)), 0), Re = [{
@@ -266,7 +266,7 @@ function S() {
266
266
  })
267
267
  ]
268
268
  }),
269
- I.length === 0 ? /* @__PURE__ */ _(oe, {
269
+ I.length === 0 ? /* @__PURE__ */ _(o, {
270
270
  illustration: "empty-people",
271
271
  title: "No clients configured yet",
272
272
  description: "Add your first client to share projects and tracked hours with them.",
@@ -335,7 +335,7 @@ function S() {
335
335
  children: ["Since ", t(i.createdAt)]
336
336
  })]
337
337
  })]
338
- }), /* @__PURE__ */ _(ae, {
338
+ }), /* @__PURE__ */ _(oe, {
339
339
  label: i.isActive ? "active" : "inactive",
340
340
  status: i.isActive ? "active" : "inactive"
341
341
  })]
@@ -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 type ListClientPortalConfigsQuery,\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: (\n prev: ListClientPortalConfigsQuery,\n {\n fetchMoreResult,\n }: { fetchMoreResult?: ListClientPortalConfigsQuery },\n ) => {\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 type ListClientPortalConfigsQuery,\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: (\n prev: ListClientPortalConfigsQuery,\n {\n fetchMoreResult,\n }: { fetchMoreResult?: ListClientPortalConfigsQuery },\n ) => {\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,10 +1,10 @@
1
1
  import { useTimeCampus as e } from "../../providers/TimeCampusProvider.js";
2
2
  import { cn as t, formatHours as n } from "../../utils/helpers.js";
3
3
  import { LoadingSkeleton as r } from "../../components/LoadingSkeleton.js";
4
- import { useGetFocusScoreSummaryQuery as i, useGetTimeEntryStatsQuery as a, useGetWorkPatternSummaryQuery as o } from "../../../generated/wspace-operations.js";
5
- import { PageLayout as s } from "../../components/PageLayout.js";
6
- import { StatusBadge as c } from "../../components/StatusBadge.js";
7
- import { EmptyState as l } from "../../components/EmptyState.js";
4
+ import { EmptyState as i } from "../../components/EmptyState.js";
5
+ import { useGetFocusScoreSummaryQuery as a, useGetTimeEntryStatsQuery as o, useGetWorkPatternSummaryQuery as s } from "../../../generated/wspace-operations.js";
6
+ import { PageLayout as c } from "../../components/PageLayout.js";
7
+ import { StatusBadge as l } from "../../components/StatusBadge.js";
8
8
  import { BarsChart as u } from "../../components/charts/BarsChart.js";
9
9
  import { RadialGauge as d } from "../../components/charts/RadialGauge.js";
10
10
  import { ProgressMeter as f } from "../../components/charts/ProgressMeter.js";
@@ -19,7 +19,7 @@ function C(e, t) {
19
19
  o.href = a, o.download = e, document.body.appendChild(o), o.click(), document.body.removeChild(o), URL.revokeObjectURL(a);
20
20
  }
21
21
  function w() {
22
- let { workspaceId: w = "" } = e(), [T, E] = p(!1), { data: D, loading: O, error: k, refetch: A } = a({ skip: !w }), { data: j, loading: M, refetch: N } = o({ skip: !w }), { data: P, loading: F, refetch: I } = i({ skip: !w }), L = D?.timeEntryStats, R = j?.workPatternSummary, z = P?.focusScoreSummary, B = async () => {
22
+ let { workspaceId: w = "" } = e(), [T, E] = p(!1), { data: D, loading: O, error: k, refetch: A } = o({ skip: !w }), { data: j, loading: M, refetch: N } = s({ skip: !w }), { data: P, loading: F, refetch: I } = a({ skip: !w }), L = D?.timeEntryStats, R = j?.workPatternSummary, z = P?.focusScoreSummary, B = async () => {
23
23
  E(!0);
24
24
  try {
25
25
  await Promise.all([
@@ -42,7 +42,7 @@ function w() {
42
42
  ["Lowest score", Math.round(z.lowestScore)]
43
43
  ]);
44
44
  };
45
- if (O || M || F) return /* @__PURE__ */ b(s, {
45
+ if (O || M || F) return /* @__PURE__ */ b(c, {
46
46
  title: "Reports",
47
47
  description: "Generate and view time tracking reports",
48
48
  actions: /* @__PURE__ */ x("button", {
@@ -55,7 +55,7 @@ function w() {
55
55
  count: 4
56
56
  })
57
57
  });
58
- if (k) return /* @__PURE__ */ b(s, {
58
+ if (k) return /* @__PURE__ */ b(c, {
59
59
  title: "Reports",
60
60
  description: "Generate and view time tracking reports",
61
61
  children: /* @__PURE__ */ b("div", {
@@ -134,10 +134,10 @@ function w() {
134
134
  size: "md"
135
135
  }), (R.mostProductiveDay || R.leastProductiveDay) && /* @__PURE__ */ x("div", {
136
136
  className: "flex flex-wrap items-center gap-2",
137
- children: [R.mostProductiveDay && /* @__PURE__ */ b(c, {
137
+ children: [R.mostProductiveDay && /* @__PURE__ */ b(l, {
138
138
  variant: "success",
139
139
  label: `Peak: ${R.mostProductiveDay}`
140
- }), R.leastProductiveDay && /* @__PURE__ */ b(c, {
140
+ }), R.leastProductiveDay && /* @__PURE__ */ b(l, {
141
141
  variant: "muted",
142
142
  label: `Quietest: ${R.leastProductiveDay}`
143
143
  })]
@@ -206,7 +206,7 @@ function w() {
206
206
  })
207
207
  }] : []
208
208
  ];
209
- return /* @__PURE__ */ b(s, {
209
+ return /* @__PURE__ */ b(c, {
210
210
  title: "Reports",
211
211
  description: "Generate and view time tracking reports",
212
212
  purpose: "Package your tracked time into shareable, exportable reports — the deliverables you hand to clients for billing or to leadership for capacity and utilisation decisions. Reports build automatically from your data; refresh to pull the latest, and download any report as CSV.",
@@ -216,7 +216,7 @@ function w() {
216
216
  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 disabled:opacity-70",
217
217
  children: [/* @__PURE__ */ b(y, { className: `h-4 w-4 ${T ? "motion-safe:animate-spin" : ""}` }), T ? "Refreshing..." : "Refresh"]
218
218
  }),
219
- children: W.length === 0 ? /* @__PURE__ */ b(l, {
219
+ children: W.length === 0 ? /* @__PURE__ */ b(i, {
220
220
  illustration: "empty-data",
221
221
  title: "No report data available yet",
222
222
  description: "Track some time first — once you have entries, focus scores or work patterns, ready-to-export reports appear here."
@@ -239,7 +239,7 @@ function w() {
239
239
  children: [/* @__PURE__ */ b("p", {
240
240
  className: "text-sm font-semibold text-text-primary",
241
241
  children: e.name
242
- }), /* @__PURE__ */ b(c, {
242
+ }), /* @__PURE__ */ b(l, {
243
243
  variant: e.typeVariant,
244
244
  label: e.typeLabel
245
245
  })]
@@ -250,7 +250,7 @@ function w() {
250
250
  })]
251
251
  }), /* @__PURE__ */ x("div", {
252
252
  className: "flex shrink-0 items-center gap-2",
253
- children: [/* @__PURE__ */ b(c, {
253
+ children: [/* @__PURE__ */ b(l, {
254
254
  variant: "success",
255
255
  label: "Ready",
256
256
  dot: !0