@burdenoff/microfe-timecampus 2026.719.2 → 2026.719.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-operations.d.ts +40 -76
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-operations.js +40 -50
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.d.ts +21 -38
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/timecampus/pages/HomePage.js +1 -1
- package/dist/timecampus/pages/HomePage.js.map +1 -1
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +6 -3
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js +4 -4
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +8 -3
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +8 -4
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.js +4 -4
- package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +4 -4
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js +4 -2
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.js +6 -4
- package/dist/timecampus/pages/meetings/MeetingsPage.js.map +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectsPage.js +2 -2
- package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/TasksPage.js +4 -2
- package/dist/timecampus/pages/projects/TasksPage.js.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.d.ts.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +4 -4
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.js +137 -132
- package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.js +2 -2
- package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.js +2 -2
- package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +4 -4
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@ var C = () => {
|
|
|
12
12
|
let { t: C } = S(), w = (e, t) => {
|
|
13
13
|
let n = C(e);
|
|
14
14
|
return n === e ? t : n;
|
|
15
|
-
}, T = e(), { data: E, loading: D } = a(), { data: O, loading: k } = o({ variables: { limit: 4 } }), A = E?.timeEntryStats, j = O?.timeEntries?.items ?? [], M = /* @__PURE__ */ h(v, {
|
|
15
|
+
}, T = e(), { data: E, loading: D } = a(), { data: O, loading: k } = o({ variables: { pagination: { limit: 4 } } }), A = E?.timeEntryStats, j = O?.timeEntries?.items ?? [], M = /* @__PURE__ */ h(v, {
|
|
16
16
|
primary: {
|
|
17
17
|
label: w("timecampus.home.actions.startTimer", "Start Timer"),
|
|
18
18
|
onSelect: () => T("/time/timer"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HomePage.js","names":[],"sources":["../../../src/timecampus/pages/HomePage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport {\n Clock,\n FolderOpen,\n Users,\n TrendingUp,\n CheckCircle,\n BarChart3,\n Play,\n ArrowRight,\n} from \"lucide-react\";\nimport {\n GlassCard,\n EmphasisPanel,\n AuroraBackground,\n GradientText,\n CTAOverflowMenu,\n} from \"@burdenoff/fe-libs/ui\";\nimport { StatCard } from \"../components/StatCard\";\nimport { LoadingSkeleton } from \"../components/LoadingSkeleton\";\nimport { cn, formatHours } from \"../utils/helpers\";\nimport { useTimeCampusNavigate } from \"../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useListTimeEntriesQuery,\n type TimeEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\nexport const HomePage: FC = () => {\n const { t: translate } = useI18n();\n const t = (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n const navigate = useTimeCampusNavigate();\n\n const { data: statsData, loading: statsLoading } =\n useGetTimeEntryStatsQuery();\n const { data: entriesData, loading: entriesLoading } =\n useListTimeEntriesQuery({\n variables: { limit: 4 },\n });\n\n const stats = statsData?.timeEntryStats;\n const recentEntries = entriesData?.timeEntries?.items ?? [];\n\n const headerActions = (\n <CTAOverflowMenu\n primary={{\n label: t(\"timecampus.home.actions.startTimer\", \"Start Timer\"),\n onSelect: () => navigate(\"/time/timer\"),\n icon: <Play className=\"h-4 w-4\" />,\n }}\n actions={[\n {\n label: t(\n \"timecampus.home.quickActions.viewProjects.label\",\n \"View Projects\",\n ),\n onSelect: () => navigate(\"/projects\"),\n icon: <FolderOpen className=\"h-4 w-4\" />,\n },\n {\n label: t(\"timecampus.home.quickActions.reports.label\", \"Reports\"),\n onSelect: () => navigate(\"/reports\"),\n icon: <BarChart3 className=\"h-4 w-4\" />,\n },\n ]}\n />\n );\n\n const renderHeader = () => (\n <div className=\"relative overflow-hidden rounded-2xl border border-border-seam bg-gloss-card p-5 sm:p-6\">\n <AuroraBackground intensity=\"subtle\" />\n <div className=\"relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-bold text-text-primary\">\n <GradientText>\n {t(\"timecampus.home.title\", \"Dashboard\")}\n </GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {t(\n \"timecampus.home.description\",\n \"Overview of your time tracking and productivity\",\n )}\n </p>\n </div>\n {headerActions}\n </div>\n </div>\n );\n\n if (statsLoading && entriesLoading) {\n return (\n <div className=\"min-h-full space-y-6 p-4 sm:p-6\">\n {renderHeader()}\n <LoadingSkeleton variant=\"card\" count={4} />\n </div>\n );\n }\n\n return (\n <div className=\"min-h-full space-y-6 p-4 sm:p-6\">\n {renderHeader()}\n\n {/* Hero metric + supporting metrics */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n <StatCard\n featured\n title={t(\"timecampus.home.stats.today\", \"Today\")}\n value={formatHours(stats?.totalHours ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n trend={{\n value: 12.5,\n label: t(\"timecampus.home.stats.vsYesterday\", \"vs yesterday\"),\n }}\n />\n <StatCard\n title={t(\"timecampus.home.stats.billable\", \"Billable\")}\n value={formatHours(stats?.billableHours ?? 0)}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n description={`${stats?.totalEntries ?? 0} ${t(\"timecampus.home.stats.entries\", \"entries\")}`}\n />\n <StatCard\n title={t(\"timecampus.home.stats.activeTimers\", \"Active Timers\")}\n value={stats?.activeTimers ?? 0}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.home.stats.currentlyRunning\",\n \"Currently running\",\n )}\n />\n <StatCard\n title={t(\"timecampus.home.stats.totalEntries\", \"Entries\")}\n value={stats?.totalEntries ?? 0}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.home.stats.totalEntriesDescription\",\n \"Total time entries\",\n )}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Recent Time Entries */}\n <GlassCard\n treatment=\"plain\"\n className=\"border-border-seam p-6 lg:col-span-2\"\n >\n <div className=\"mb-4 flex items-center justify-between\">\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\n \"timecampus.home.recentEntries.title\",\n \"Recent Time Entries\",\n )}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.recentEntries.description\",\n \"Your latest tracked activities\",\n )}\n </p>\n </div>\n <button\n onClick={() => navigate(\"/time/entries\")}\n className=\"inline-flex min-h-[2.75rem] items-center gap-1 rounded-md px-2 text-xs font-medium text-primary transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\"\n >\n {t(\"timecampus.home.recentEntries.viewAll\", \"View all\")}{\" \"}\n <ArrowRight className=\"h-3 w-3\" />\n </button>\n </div>\n {entriesLoading ? (\n <LoadingSkeleton variant=\"list\" count={4} />\n ) : (\n <div className=\"flex flex-col gap-3\">\n {recentEntries.map((entry: TimeEntryFieldsFragment) => (\n <div\n key={entry.id}\n className=\"flex items-center justify-between rounded-lg border border-border-seam p-3 transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {entry.project?.name ??\n t(\n \"timecampus.home.recentEntries.noProject\",\n \"No Project\",\n )}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {entry.description ??\n t(\n \"timecampus.home.recentEntries.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <div className=\"ml-4 text-right\">\n <p className=\"font-mono text-sm font-medium tabular-nums text-text-primary\">\n {formatHours((entry.duration ?? 0) / 3600)}\n </p>\n <span\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium\",\n entry.isRunning\n ? \"bg-status-info-bg text-status-info-text\"\n : \"bg-status-success-bg text-status-success-text\",\n )}\n >\n {entry.isRunning && (\n <span className=\"h-1.5 w-1.5 rounded-full bg-status-info-text motion-safe:animate-vc-glow-pulse\" />\n )}\n {entry.isRunning\n ? t(\n \"timecampus.home.recentEntries.status.running\",\n \"running\",\n )\n : t(\n \"timecampus.home.recentEntries.status.completed\",\n \"completed\",\n )}\n </span>\n </div>\n </div>\n ))}\n {recentEntries.length === 0 && (\n <p className=\"py-4 text-center text-sm text-text-secondary\">\n {t(\n \"timecampus.home.recentEntries.empty\",\n \"No time entries yet\",\n )}\n </p>\n )}\n </div>\n )}\n </GlassCard>\n\n {/* Quick Summary — the one emphasis zone */}\n <EmphasisPanel className=\"p-6\">\n <div className=\"mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.home.summary.title\", \"Quick Summary\")}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.summary.description\",\n \"Time tracking overview\",\n )}\n </p>\n </div>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.totalHours\", \"Total Hours\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {formatHours(stats?.totalHours ?? 0)}\n </p>\n </div>\n <Clock className=\"h-5 w-5 text-text-secondary\" />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.billableHours\", \"Billable Hours\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {formatHours(stats?.billableHours ?? 0)}\n </p>\n </div>\n <TrendingUp className=\"h-5 w-5 text-text-secondary\" />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.activeTimers\", \"Active Timers\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {stats?.activeTimers ?? 0}\n </p>\n </div>\n <Users className=\"h-5 w-5 text-text-secondary\" />\n </div>\n </div>\n </EmphasisPanel>\n </div>\n\n {/* Quick Actions */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <div className=\"mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.home.quickActions.title\", \"Quick Actions\")}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.quickActions.description\",\n \"Frequently used features\",\n )}\n </p>\n </div>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6\">\n {[\n {\n label: t(\n \"timecampus.home.quickActions.startTimer.label\",\n \"Start Timer\",\n ),\n icon: Play,\n route: \"/time/timer\",\n description: t(\n \"timecampus.home.quickActions.startTimer.description\",\n \"Track your time\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.viewProjects.label\",\n \"View Projects\",\n ),\n icon: FolderOpen,\n route: \"/projects\",\n description: t(\n \"timecampus.home.quickActions.viewProjects.description\",\n \"Manage projects\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.timeEntries.label\",\n \"Time Entries\",\n ),\n icon: Clock,\n route: \"/time/entries\",\n description: t(\n \"timecampus.home.quickActions.timeEntries.description\",\n \"View all entries\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.teamActivity.label\",\n \"Team Activity\",\n ),\n icon: Users,\n route: \"/team-activity\",\n description: t(\n \"timecampus.home.quickActions.teamActivity.description\",\n \"See team status\",\n ),\n },\n {\n label: t(\"timecampus.home.quickActions.reports.label\", \"Reports\"),\n icon: BarChart3,\n route: \"/reports\",\n description: t(\n \"timecampus.home.quickActions.reports.description\",\n \"Generate reports\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.approvals.label\",\n \"Approvals\",\n ),\n icon: CheckCircle,\n route: \"/time-approval\",\n description: t(\n \"timecampus.home.quickActions.approvals.description\",\n \"Review timesheets\",\n ),\n },\n ].map(({ label, icon: Icon, description, route }) => (\n <button\n key={label}\n onClick={() => navigate(route)}\n className=\"flex min-h-[2.75rem] flex-col items-center gap-2 rounded-lg border border-border-seam bg-bg-surface p-4 text-center transition-colors hover:bg-bg-sunken/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\"\n >\n <div className=\"rounded-full bg-action-primary-bg/10 p-3\">\n <Icon className=\"h-5 w-5 text-primary\" />\n </div>\n <span className=\"text-sm font-medium text-text-primary\">\n {label}\n </span>\n <span className=\"text-xs text-text-secondary\">{description}</span>\n </button>\n ))}\n </div>\n </GlassCard>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HomePage.js","names":[],"sources":["../../../src/timecampus/pages/HomePage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport {\n Clock,\n FolderOpen,\n Users,\n TrendingUp,\n CheckCircle,\n BarChart3,\n Play,\n ArrowRight,\n} from \"lucide-react\";\nimport {\n GlassCard,\n EmphasisPanel,\n AuroraBackground,\n GradientText,\n CTAOverflowMenu,\n} from \"@burdenoff/fe-libs/ui\";\nimport { StatCard } from \"../components/StatCard\";\nimport { LoadingSkeleton } from \"../components/LoadingSkeleton\";\nimport { cn, formatHours } from \"../utils/helpers\";\nimport { useTimeCampusNavigate } from \"../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useListTimeEntriesQuery,\n type TimeEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\nexport const HomePage: FC = () => {\n const { t: translate } = useI18n();\n const t = (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n const navigate = useTimeCampusNavigate();\n\n const { data: statsData, loading: statsLoading } =\n useGetTimeEntryStatsQuery();\n const { data: entriesData, loading: entriesLoading } =\n useListTimeEntriesQuery({\n variables: { pagination: { limit: 4 } },\n });\n\n const stats = statsData?.timeEntryStats;\n const recentEntries = entriesData?.timeEntries?.items ?? [];\n\n const headerActions = (\n <CTAOverflowMenu\n primary={{\n label: t(\"timecampus.home.actions.startTimer\", \"Start Timer\"),\n onSelect: () => navigate(\"/time/timer\"),\n icon: <Play className=\"h-4 w-4\" />,\n }}\n actions={[\n {\n label: t(\n \"timecampus.home.quickActions.viewProjects.label\",\n \"View Projects\",\n ),\n onSelect: () => navigate(\"/projects\"),\n icon: <FolderOpen className=\"h-4 w-4\" />,\n },\n {\n label: t(\"timecampus.home.quickActions.reports.label\", \"Reports\"),\n onSelect: () => navigate(\"/reports\"),\n icon: <BarChart3 className=\"h-4 w-4\" />,\n },\n ]}\n />\n );\n\n const renderHeader = () => (\n <div className=\"relative overflow-hidden rounded-2xl border border-border-seam bg-gloss-card p-5 sm:p-6\">\n <AuroraBackground intensity=\"subtle\" />\n <div className=\"relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-bold text-text-primary\">\n <GradientText>\n {t(\"timecampus.home.title\", \"Dashboard\")}\n </GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {t(\n \"timecampus.home.description\",\n \"Overview of your time tracking and productivity\",\n )}\n </p>\n </div>\n {headerActions}\n </div>\n </div>\n );\n\n if (statsLoading && entriesLoading) {\n return (\n <div className=\"min-h-full space-y-6 p-4 sm:p-6\">\n {renderHeader()}\n <LoadingSkeleton variant=\"card\" count={4} />\n </div>\n );\n }\n\n return (\n <div className=\"min-h-full space-y-6 p-4 sm:p-6\">\n {renderHeader()}\n\n {/* Hero metric + supporting metrics */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n <StatCard\n featured\n title={t(\"timecampus.home.stats.today\", \"Today\")}\n value={formatHours(stats?.totalHours ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n trend={{\n value: 12.5,\n label: t(\"timecampus.home.stats.vsYesterday\", \"vs yesterday\"),\n }}\n />\n <StatCard\n title={t(\"timecampus.home.stats.billable\", \"Billable\")}\n value={formatHours(stats?.billableHours ?? 0)}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n description={`${stats?.totalEntries ?? 0} ${t(\"timecampus.home.stats.entries\", \"entries\")}`}\n />\n <StatCard\n title={t(\"timecampus.home.stats.activeTimers\", \"Active Timers\")}\n value={stats?.activeTimers ?? 0}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.home.stats.currentlyRunning\",\n \"Currently running\",\n )}\n />\n <StatCard\n title={t(\"timecampus.home.stats.totalEntries\", \"Entries\")}\n value={stats?.totalEntries ?? 0}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.home.stats.totalEntriesDescription\",\n \"Total time entries\",\n )}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Recent Time Entries */}\n <GlassCard\n treatment=\"plain\"\n className=\"border-border-seam p-6 lg:col-span-2\"\n >\n <div className=\"mb-4 flex items-center justify-between\">\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\n \"timecampus.home.recentEntries.title\",\n \"Recent Time Entries\",\n )}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.recentEntries.description\",\n \"Your latest tracked activities\",\n )}\n </p>\n </div>\n <button\n onClick={() => navigate(\"/time/entries\")}\n className=\"inline-flex min-h-[2.75rem] items-center gap-1 rounded-md px-2 text-xs font-medium text-primary transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\"\n >\n {t(\"timecampus.home.recentEntries.viewAll\", \"View all\")}{\" \"}\n <ArrowRight className=\"h-3 w-3\" />\n </button>\n </div>\n {entriesLoading ? (\n <LoadingSkeleton variant=\"list\" count={4} />\n ) : (\n <div className=\"flex flex-col gap-3\">\n {recentEntries.map((entry: TimeEntryFieldsFragment) => (\n <div\n key={entry.id}\n className=\"flex items-center justify-between rounded-lg border border-border-seam p-3 transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {entry.project?.name ??\n t(\n \"timecampus.home.recentEntries.noProject\",\n \"No Project\",\n )}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {entry.description ??\n t(\n \"timecampus.home.recentEntries.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <div className=\"ml-4 text-right\">\n <p className=\"font-mono text-sm font-medium tabular-nums text-text-primary\">\n {formatHours((entry.duration ?? 0) / 3600)}\n </p>\n <span\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium\",\n entry.isRunning\n ? \"bg-status-info-bg text-status-info-text\"\n : \"bg-status-success-bg text-status-success-text\",\n )}\n >\n {entry.isRunning && (\n <span className=\"h-1.5 w-1.5 rounded-full bg-status-info-text motion-safe:animate-vc-glow-pulse\" />\n )}\n {entry.isRunning\n ? t(\n \"timecampus.home.recentEntries.status.running\",\n \"running\",\n )\n : t(\n \"timecampus.home.recentEntries.status.completed\",\n \"completed\",\n )}\n </span>\n </div>\n </div>\n ))}\n {recentEntries.length === 0 && (\n <p className=\"py-4 text-center text-sm text-text-secondary\">\n {t(\n \"timecampus.home.recentEntries.empty\",\n \"No time entries yet\",\n )}\n </p>\n )}\n </div>\n )}\n </GlassCard>\n\n {/* Quick Summary — the one emphasis zone */}\n <EmphasisPanel className=\"p-6\">\n <div className=\"mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.home.summary.title\", \"Quick Summary\")}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.summary.description\",\n \"Time tracking overview\",\n )}\n </p>\n </div>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.totalHours\", \"Total Hours\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {formatHours(stats?.totalHours ?? 0)}\n </p>\n </div>\n <Clock className=\"h-5 w-5 text-text-secondary\" />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.billableHours\", \"Billable Hours\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {formatHours(stats?.billableHours ?? 0)}\n </p>\n </div>\n <TrendingUp className=\"h-5 w-5 text-text-secondary\" />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.activeTimers\", \"Active Timers\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {stats?.activeTimers ?? 0}\n </p>\n </div>\n <Users className=\"h-5 w-5 text-text-secondary\" />\n </div>\n </div>\n </EmphasisPanel>\n </div>\n\n {/* Quick Actions */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <div className=\"mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.home.quickActions.title\", \"Quick Actions\")}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.quickActions.description\",\n \"Frequently used features\",\n )}\n </p>\n </div>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6\">\n {[\n {\n label: t(\n \"timecampus.home.quickActions.startTimer.label\",\n \"Start Timer\",\n ),\n icon: Play,\n route: \"/time/timer\",\n description: t(\n \"timecampus.home.quickActions.startTimer.description\",\n \"Track your time\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.viewProjects.label\",\n \"View Projects\",\n ),\n icon: FolderOpen,\n route: \"/projects\",\n description: t(\n \"timecampus.home.quickActions.viewProjects.description\",\n \"Manage projects\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.timeEntries.label\",\n \"Time Entries\",\n ),\n icon: Clock,\n route: \"/time/entries\",\n description: t(\n \"timecampus.home.quickActions.timeEntries.description\",\n \"View all entries\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.teamActivity.label\",\n \"Team Activity\",\n ),\n icon: Users,\n route: \"/team-activity\",\n description: t(\n \"timecampus.home.quickActions.teamActivity.description\",\n \"See team status\",\n ),\n },\n {\n label: t(\"timecampus.home.quickActions.reports.label\", \"Reports\"),\n icon: BarChart3,\n route: \"/reports\",\n description: t(\n \"timecampus.home.quickActions.reports.description\",\n \"Generate reports\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.approvals.label\",\n \"Approvals\",\n ),\n icon: CheckCircle,\n route: \"/time-approval\",\n description: t(\n \"timecampus.home.quickActions.approvals.description\",\n \"Review timesheets\",\n ),\n },\n ].map(({ label, icon: Icon, description, route }) => (\n <button\n key={label}\n onClick={() => navigate(route)}\n className=\"flex min-h-[2.75rem] flex-col items-center gap-2 rounded-lg border border-border-seam bg-bg-surface p-4 text-center transition-colors hover:bg-bg-sunken/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\"\n >\n <div className=\"rounded-full bg-action-primary-bg/10 p-3\">\n <Icon className=\"h-5 w-5 text-primary\" />\n </div>\n <span className=\"text-sm font-medium text-text-primary\">\n {label}\n </span>\n <span className=\"text-xs text-text-secondary\">{description}</span>\n </button>\n ))}\n </div>\n </GlassCard>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -43,10 +43,10 @@ function pe() {
|
|
|
43
43
|
}
|
|
44
44
|
function C() {
|
|
45
45
|
let { workspaceId: l = "" } = e(), { data: C, loading: me, error: he, fetchMore: ge, refetch: _e } = ne({
|
|
46
|
-
variables: {
|
|
46
|
+
variables: { pagination: {
|
|
47
47
|
limit: v,
|
|
48
48
|
offset: 0
|
|
49
|
-
},
|
|
49
|
+
} },
|
|
50
50
|
skip: !l,
|
|
51
51
|
notifyOnNetworkStatusChange: !0
|
|
52
52
|
}), { data: ve, loading: ye, error: be, refetch: xe } = te({ skip: !l }), Se = me || ye, Ce = he || be, w = C?.applicationUsages ?? [], T = ve?.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), [we, { loading: G, error: Te }] = re(), [Ee, { loading: K, error: De }] = ee(), [Oe, { loading: q, error: ke }] = i(), J = async () => {
|
|
@@ -74,7 +74,10 @@ function C() {
|
|
|
74
74
|
O(!0);
|
|
75
75
|
try {
|
|
76
76
|
await ge({
|
|
77
|
-
variables: {
|
|
77
|
+
variables: { pagination: {
|
|
78
|
+
limit: v,
|
|
79
|
+
offset: w.length
|
|
80
|
+
} },
|
|
78
81
|
updateQuery: (e, { fetchMoreResult: t }) => t ? {
|
|
79
82
|
...e,
|
|
80
83
|
applicationUsages: [...e.applicationUsages, ...t.applicationUsages]
|
|
@@ -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 } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListApplicationUsagesQuery,\n useGetAppUsageSummaryQuery,\n useRecordApplicationUsageMutation,\n useCategorizeApplicationMutation,\n useDeleteApplicationUsageMutation,\n type ApplicationUsageFieldsFragment,\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\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: { 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: { offset: apps.length },\n updateQuery: (prev, { fetchMoreResult }) => {\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 {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 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 · {app.switches}{\" \"}\n switches\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 “\n {recatApp?.applicationName}”.\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 } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListApplicationUsagesQuery,\n useGetAppUsageSummaryQuery,\n useRecordApplicationUsageMutation,\n useCategorizeApplicationMutation,\n useDeleteApplicationUsageMutation,\n type ApplicationUsageFieldsFragment,\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\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: (prev, { fetchMoreResult }) => {\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 {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 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 · {app.switches}{\" \"}\n switches\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 “\n {recatApp?.applicationName}”.\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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"}
|
|
@@ -43,10 +43,10 @@ var x = {
|
|
|
43
43
|
};
|
|
44
44
|
function S() {
|
|
45
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({
|
|
46
|
-
variables: {
|
|
46
|
+
variables: { pagination: {
|
|
47
47
|
limit: y,
|
|
48
48
|
offset: 0
|
|
49
|
-
},
|
|
49
|
+
} },
|
|
50
50
|
notifyOnNetworkStatusChange: !0
|
|
51
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) => ({
|
|
52
52
|
name: e.clientName,
|
|
@@ -195,10 +195,10 @@ function S() {
|
|
|
195
195
|
H(!0);
|
|
196
196
|
try {
|
|
197
197
|
await ke({
|
|
198
|
-
variables: {
|
|
198
|
+
variables: { pagination: {
|
|
199
199
|
limit: y,
|
|
200
200
|
offset: I.length
|
|
201
|
-
},
|
|
201
|
+
} },
|
|
202
202
|
updateQuery: (e, { fetchMoreResult: t }) => t ? {
|
|
203
203
|
...e,
|
|
204
204
|
clientPortalConfigs: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientPortalPage.js","names":[],"sources":["../../../../src/timecampus/pages/client-portal/ClientPortalPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n Building2,\n Plus,\n Users,\n PowerOff,\n FolderOpen,\n Link2,\n Check,\n RefreshCw,\n Pencil,\n Trash2,\n Power,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { Modal } from \"../../components/Modal\";\nimport { ToastContainer, useToast } from \"../../components/Toast\";\nimport {\n ChartCard,\n BarsChart,\n DonutChart,\n ProgressMeter,\n} from \"../../components\";\nimport { getInitials, cn, formatDate, formatNumber } from \"../../utils/helpers\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport {\n useListClientPortalConfigsQuery,\n useGetClientPortalStatsQuery,\n useRegenerateClientPortalShareTokenMutation,\n useCreateClientPortalConfigMutation,\n useUpdateClientPortalConfigMutation,\n useDeleteClientPortalConfigMutation,\n useToggleClientPortalMutation,\n type ClientPortalFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 25;\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\nconst fileInputClass =\n \"mt-1 block w-full text-sm text-text-secondary file:mr-3 file:rounded-md file:border-0 file:bg-action-primary-bg file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-action-primary-text hover:file:bg-action-primary-bg/90\";\n\n/**\n * Resolve a downloadable URL for a Files object. The URL is presigned and\n * expires after 7 days (604800s) — a known limitation: a stored logoUrl will\n * eventually 403 and the logo must be re-uploaded to refresh it.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampus_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// wspace-files requires a real folder id (File.folderId FK).\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusClient_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\n/**\n * Build the public share URL for a portal token. The public client view is\n * served by the website at /portal/:token; only active portals resolve.\n */\nfunction shareUrlFor(token: string): string {\n const origin = typeof window !== \"undefined\" ? window.location.origin : \"\";\n return `${origin}/portal/${token}`;\n}\n\ninterface ClientFormState {\n clientName: string;\n contactEmail: string;\n contactPhone: string;\n description: string;\n logoUrl: string;\n}\n\nconst emptyForm: ClientFormState = {\n clientName: \"\",\n contactEmail: \"\",\n contactPhone: \"\",\n description: \"\",\n logoUrl: \"\",\n};\n\nexport function ClientPortalPage() {\n const { toasts, addToast, dismissToast } = useToast();\n const apollo = useApolloClient();\n const [copiedId, setCopiedId] = useState<string | null>(null);\n const [logoUploading, setLogoUploading] = useState(false);\n\n const {\n data: configsData,\n loading: configsLoading,\n error: configsError,\n refetch,\n fetchMore,\n } = useListClientPortalConfigsQuery({\n variables: { limit: PAGE_SIZE, offset: 0 },\n notifyOnNetworkStatusChange: true,\n });\n const { data: statsData, refetch: refetchStats } =\n useGetClientPortalStatsQuery();\n\n const [regenerate, { loading: regenerating }] =\n useRegenerateClientPortalShareTokenMutation();\n const [createClient, { loading: creating }] =\n useCreateClientPortalConfigMutation();\n const [updateClient, { loading: updating }] =\n useUpdateClientPortalConfigMutation();\n const [deleteClient, { loading: deleting }] =\n useDeleteClientPortalConfigMutation();\n const [toggleClient] = useToggleClientPortalMutation();\n\n const clients = configsData?.clientPortalConfigs?.items ?? [];\n const hasMore = configsData?.clientPortalConfigs?.hasMore ?? false;\n const stats = statsData?.clientPortalStats;\n\n const trackedHoursFor = (client: ClientPortalFieldsFragment): number =>\n client.projects.reduce((sum, p) => sum + p.trackedHours, 0);\n\n const inactiveClients =\n (stats?.totalClients ?? 0) - (stats?.activeClients ?? 0);\n\n // Real, already-fetched data only (loaded page of clients + workspace stats).\n const clientHoursData: Array<{ name: string; hours: number }> = clients\n .map((client: ClientPortalFieldsFragment) => ({\n name: client.clientName,\n hours: trackedHoursFor(client),\n }))\n .filter((entry: { name: string; hours: number }) => entry.hours > 0)\n .sort((a: { hours: number }, b: { hours: number }) => b.hours - a.hours)\n .slice(0, 8);\n\n const maxTrackedHours = clients.reduce(\n (max: number, client: ClientPortalFieldsFragment) =>\n Math.max(max, trackedHoursFor(client)),\n 0,\n );\n\n const statusMix = [\n { name: \"Active\", value: stats?.activeClients ?? 0 },\n { name: \"Inactive\", value: Math.max(inactiveClients, 0) },\n ];\n\n const [loadingMore, setLoadingMore] = useState(false);\n\n // Create / edit modal\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<ClientPortalFieldsFragment | null>(\n null,\n );\n const [form, setForm] = useState<ClientFormState>(emptyForm);\n\n // Delete confirm\n const [deleteTarget, setDeleteTarget] =\n useState<ClientPortalFieldsFragment | null>(null);\n\n // Per-row busy (toggle/regenerate)\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const handleCopy = async (client: ClientPortalFieldsFragment) => {\n if (!client.shareToken) return;\n try {\n await navigator.clipboard.writeText(shareUrlFor(client.shareToken));\n setCopiedId(client.id);\n window.setTimeout(() => setCopiedId(null), 2000);\n } catch {\n addToast({ title: \"Clipboard unavailable\", variant: \"error\" });\n }\n };\n\n const handleRegenerate = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await regenerate({ variables: { id: client.id } });\n addToast({ title: \"Share link regenerated\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Failed to regenerate link\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (client: ClientPortalFieldsFragment) => {\n setEditing(client);\n setForm({\n clientName: client.clientName,\n contactEmail: client.contactEmail ?? \"\",\n contactPhone: client.contactPhone ?? \"\",\n description: client.description ?? \"\",\n logoUrl: client.logoUrl ?? \"\",\n });\n setFormOpen(true);\n };\n\n /**\n * Upload a client logo to platform Files and store its presigned URL on the\n * form. Failures surface a toast but never block saving the client — the\n * logo is optional.\n */\n const handleLogoUpload = async (file: File) => {\n setLogoUploading(true);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/ClientLogos\" },\n });\n const folderId = (\n folderData as { ensureFolderByPath?: { id: string } } | null\n )?.ensureFolderByPath?.id;\n if (!folderId) throw new Error(\"Could not resolve upload folder\");\n const uploaded = await uploadBlobToFiles({\n client: apollo,\n blob: file,\n filename: file.name,\n mimeType: file.type || \"application/octet-stream\",\n folderId,\n });\n const { data } = await apollo.query({\n query: GET_FILE_DOWNLOAD_URL,\n variables: { id: uploaded.id, expiresIn: 604800 },\n fetchPolicy: \"no-cache\",\n });\n const url = (data as { getFileDownloadUrl?: string } | null | undefined)\n ?.getFileDownloadUrl;\n if (!url) throw new Error(\"No download URL returned\");\n setForm((f) => ({ ...f, logoUrl: url }));\n } catch {\n addToast({ title: \"Failed to upload logo\", variant: \"error\" });\n } finally {\n setLogoUploading(false);\n }\n };\n\n const closeForm = () => {\n setFormOpen(false);\n setEditing(null);\n };\n\n const handleSubmit = async () => {\n if (!form.clientName.trim()) return;\n try {\n if (editing) {\n await updateClient({\n variables: {\n id: editing.id,\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n },\n },\n });\n addToast({ title: \"Client updated\", variant: \"success\" });\n } else {\n await createClient({\n variables: {\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n isActive: true,\n },\n },\n });\n addToast({ title: \"Client added\", variant: \"success\" });\n }\n closeForm();\n await refreshAll();\n } catch {\n addToast({\n title: editing ? \"Failed to update client\" : \"Failed to add client\",\n variant: \"error\",\n });\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteClient({ variables: { id: deleteTarget.id } });\n addToast({ title: \"Client deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to delete client\", variant: \"error\" });\n }\n };\n\n const handleToggle = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await toggleClient({\n variables: { id: client.id, isActive: !client.isActive },\n });\n addToast({\n title: client.isActive ? \"Client deactivated\" : \"Client activated\",\n variant: \"success\",\n });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to toggle client\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { limit: PAGE_SIZE, offset: clients.length },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n clientPortalConfigs: {\n ...fetchMoreResult.clientPortalConfigs,\n items: [\n ...prev.clientPortalConfigs.items,\n ...fetchMoreResult.clientPortalConfigs.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (configsLoading && clients.length === 0) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (configsError) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load client portal data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const addButton = (\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Client\n </button>\n );\n\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n purpose=\"Give clients a transparent window into the work you do for them — the projects, hours and progress tied to each one — so billing conversations are backed by data and trust, not spreadsheets.\"\n actions={addButton}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Active Clients\"\n value={String(stats?.activeClients ?? 0)}\n icon={<Building2 className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Projects\"\n value={String(stats?.totalProjectsShared ?? 0)}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Clients\"\n value={String(stats?.totalClients ?? 0)}\n icon={<Users className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Inactive\"\n value={String(Math.max(inactiveClients, 0))}\n icon={<PowerOff className=\"h-5 w-5\" />}\n iconBgColor=\"bg-bg-sunken text-text-secondary\"\n description=\"portals turned off\"\n />\n </div>\n\n {clients.length === 0 ? (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No clients configured yet\"\n description=\"Add your first client to share projects and tracked hours with them.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <ChartCard\n title=\"Tracked hours by client\"\n description=\"Top clients by logged hours (loaded page)\"\n className=\"lg:col-span-2\"\n >\n <BarsChart\n data={clientHoursData}\n xKey=\"name\"\n series={[{ key: \"hours\", label: \"Tracked hours\" }]}\n orientation=\"horizontal\"\n height={260}\n categoryWidth={120}\n valueFormat={(value) => `${value.toLocaleString()}h`}\n emptyLabel=\"No tracked hours logged yet.\"\n ariaLabel=\"Tracked hours by client\"\n />\n </ChartCard>\n <ChartCard\n title=\"Portal status\"\n description=\"Shared vs. private clients\"\n >\n <DonutChart\n data={statusMix}\n centerLabel=\"Clients\"\n centerValue={stats?.totalClients ?? 0}\n height={260}\n emptyLabel=\"No clients yet.\"\n ariaLabel=\"Clients by portal status\"\n />\n </ChartCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {clients.map((client: ClientPortalFieldsFragment) => {\n const totalTrackedHours = trackedHoursFor(client);\n const rowBusy = busyId === client.id;\n const copied = copiedId === client.id;\n return (\n <GlassCard\n key={client.id}\n treatment=\"insight\"\n glow={client.isActive}\n className=\"p-4\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n {client.logoUrl ? (\n <img\n src={client.logoUrl}\n alt={`${client.clientName} logo`}\n className=\"h-11 w-11 shrink-0 rounded-lg border border-border-seam object-cover\"\n />\n ) : (\n <div className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-sm font-semibold text-primary\">\n {getInitials(client.clientName)}\n </div>\n )}\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {client.clientName}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n Since {formatDate(client.createdAt)}\n </p>\n </div>\n </div>\n <StatusBadge\n label={client.isActive ? \"active\" : \"inactive\"}\n status={client.isActive ? \"active\" : \"inactive\"}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Projects\n </p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {client.projects.length}\n </p>\n </div>\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Tracked hours\n </p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {formatNumber(totalTrackedHours)}h\n </p>\n </div>\n </div>\n\n {maxTrackedHours > 0 && (\n <div className=\"mt-3\">\n <ProgressMeter\n value={totalTrackedHours}\n max={maxTrackedHours}\n tone=\"success\"\n size=\"sm\"\n showValue={false}\n label=\"Tracked volume vs. top client\"\n />\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <span className=\"text-xs font-medium text-text-secondary\">\n Public Access\n </span>\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => void handleToggle(client)}\n title={\n client.isActive\n ? \"Deactivate client portal\"\n : \"Activate client portal\"\n }\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors disabled:opacity-50\",\n client.isActive\n ? \"bg-status-success-bg text-status-success-text hover:bg-status-success-bg/80\"\n : \"border border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken/60\",\n )}\n >\n <Power className=\"h-3.5 w-3.5\" />\n {client.isActive ? \"Active\" : \"Inactive\"}\n </button>\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-2 border-t border-border-seam pt-3\">\n {client.isActive && client.shareToken && (\n <>\n <button\n type=\"button\"\n onClick={() => void handleCopy(client)}\n title=\"Copy the client's private share link\"\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/60\"\n >\n {copied ? (\n <>\n <Check className=\"h-3.5 w-3.5\" /> Copied\n </>\n ) : (\n <>\n <Link2 className=\"h-3.5 w-3.5\" /> Share link\n </>\n )}\n </button>\n <button\n type=\"button\"\n disabled={rowBusy && regenerating}\n onClick={() => void handleRegenerate(client)}\n title=\"Regenerate the link (invalidates the old one)\"\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/60 disabled:opacity-50\"\n >\n <RefreshCw\n className={cn(\n \"h-3.5 w-3.5\",\n rowBusy && regenerating && \"animate-spin\",\n )}\n />\n Regenerate\n </button>\n </>\n )}\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => openEdit(client)}\n title=\"Edit client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/60\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(client)}\n title=\"Delete client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {hasMore && (\n <div className=\"flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n <Modal\n open={formOpen}\n onClose={closeForm}\n title={editing ? \"Edit Client\" : \"Add Client\"}\n description={\n editing\n ? \"Update this client's portal details.\"\n : \"Set up a client so you can share projects and tracked hours with them.\"\n }\n footer={\n <>\n <button\n onClick={closeForm}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={creating || updating || !form.clientName.trim()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating || updating\n ? \"Saving...\"\n : editing\n ? \"Save Changes\"\n : \"Add Client\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Client Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Acme Corp\"\n value={form.clientName}\n onChange={(e) => setForm({ ...form, clientName: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Email\n </label>\n <input\n type=\"email\"\n placeholder=\"contact@client.com\"\n value={form.contactEmail}\n onChange={(e) =>\n setForm({ ...form, contactEmail: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Phone\n </label>\n <input\n type=\"tel\"\n placeholder=\"Optional\"\n value={form.contactPhone}\n onChange={(e) =>\n setForm({ ...form, contactPhone: e.target.value })\n }\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Description\n </label>\n <textarea\n rows={3}\n placeholder=\"Notes about this client (optional)\"\n value={form.description}\n onChange={(e) =>\n setForm({ ...form, description: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Logo\n </label>\n <input\n type=\"file\"\n accept=\"image/*\"\n disabled={logoUploading}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleLogoUpload(file);\n // Reset so re-selecting the same file re-triggers onChange.\n e.target.value = \"\";\n }}\n className={fileInputClass}\n />\n {logoUploading && (\n <p className=\"mt-1 text-xs text-text-secondary\">Uploading…</p>\n )}\n {form.logoUrl && (\n <img\n src={form.logoUrl}\n alt=\"Client logo preview\"\n className=\"mt-2 h-16 w-16 rounded-md border border-border-seam object-cover\"\n />\n )}\n </div>\n </div>\n </Modal>\n\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete client?\"\n description={\n deleteTarget\n ? `\"${deleteTarget.clientName}\" and its share link will be removed.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n The client will lose access to their shared portal immediately. This\n cannot be undone.\n </p>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ClientPortalPage.js","names":[],"sources":["../../../../src/timecampus/pages/client-portal/ClientPortalPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n Building2,\n Plus,\n Users,\n PowerOff,\n FolderOpen,\n Link2,\n Check,\n RefreshCw,\n Pencil,\n Trash2,\n Power,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { Modal } from \"../../components/Modal\";\nimport { ToastContainer, useToast } from \"../../components/Toast\";\nimport {\n ChartCard,\n BarsChart,\n DonutChart,\n ProgressMeter,\n} from \"../../components\";\nimport { getInitials, cn, formatDate, formatNumber } from \"../../utils/helpers\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport {\n useListClientPortalConfigsQuery,\n useGetClientPortalStatsQuery,\n useRegenerateClientPortalShareTokenMutation,\n useCreateClientPortalConfigMutation,\n useUpdateClientPortalConfigMutation,\n useDeleteClientPortalConfigMutation,\n useToggleClientPortalMutation,\n type ClientPortalFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 25;\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\nconst fileInputClass =\n \"mt-1 block w-full text-sm text-text-secondary file:mr-3 file:rounded-md file:border-0 file:bg-action-primary-bg file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-action-primary-text hover:file:bg-action-primary-bg/90\";\n\n/**\n * Resolve a downloadable URL for a Files object. The URL is presigned and\n * expires after 7 days (604800s) — a known limitation: a stored logoUrl will\n * eventually 403 and the logo must be re-uploaded to refresh it.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampus_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// wspace-files requires a real folder id (File.folderId FK).\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusClient_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\n/**\n * Build the public share URL for a portal token. The public client view is\n * served by the website at /portal/:token; only active portals resolve.\n */\nfunction shareUrlFor(token: string): string {\n const origin = typeof window !== \"undefined\" ? window.location.origin : \"\";\n return `${origin}/portal/${token}`;\n}\n\ninterface ClientFormState {\n clientName: string;\n contactEmail: string;\n contactPhone: string;\n description: string;\n logoUrl: string;\n}\n\nconst emptyForm: ClientFormState = {\n clientName: \"\",\n contactEmail: \"\",\n contactPhone: \"\",\n description: \"\",\n logoUrl: \"\",\n};\n\nexport function ClientPortalPage() {\n const { toasts, addToast, dismissToast } = useToast();\n const apollo = useApolloClient();\n const [copiedId, setCopiedId] = useState<string | null>(null);\n const [logoUploading, setLogoUploading] = useState(false);\n\n const {\n data: configsData,\n loading: configsLoading,\n error: configsError,\n refetch,\n fetchMore,\n } = useListClientPortalConfigsQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n notifyOnNetworkStatusChange: true,\n });\n const { data: statsData, refetch: refetchStats } =\n useGetClientPortalStatsQuery();\n\n const [regenerate, { loading: regenerating }] =\n useRegenerateClientPortalShareTokenMutation();\n const [createClient, { loading: creating }] =\n useCreateClientPortalConfigMutation();\n const [updateClient, { loading: updating }] =\n useUpdateClientPortalConfigMutation();\n const [deleteClient, { loading: deleting }] =\n useDeleteClientPortalConfigMutation();\n const [toggleClient] = useToggleClientPortalMutation();\n\n const clients = configsData?.clientPortalConfigs?.items ?? [];\n const hasMore = configsData?.clientPortalConfigs?.hasMore ?? false;\n const stats = statsData?.clientPortalStats;\n\n const trackedHoursFor = (client: ClientPortalFieldsFragment): number =>\n client.projects.reduce((sum, p) => sum + p.trackedHours, 0);\n\n const inactiveClients =\n (stats?.totalClients ?? 0) - (stats?.activeClients ?? 0);\n\n // Real, already-fetched data only (loaded page of clients + workspace stats).\n const clientHoursData: Array<{ name: string; hours: number }> = clients\n .map((client: ClientPortalFieldsFragment) => ({\n name: client.clientName,\n hours: trackedHoursFor(client),\n }))\n .filter((entry: { name: string; hours: number }) => entry.hours > 0)\n .sort((a: { hours: number }, b: { hours: number }) => b.hours - a.hours)\n .slice(0, 8);\n\n const maxTrackedHours = clients.reduce(\n (max: number, client: ClientPortalFieldsFragment) =>\n Math.max(max, trackedHoursFor(client)),\n 0,\n );\n\n const statusMix = [\n { name: \"Active\", value: stats?.activeClients ?? 0 },\n { name: \"Inactive\", value: Math.max(inactiveClients, 0) },\n ];\n\n const [loadingMore, setLoadingMore] = useState(false);\n\n // Create / edit modal\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<ClientPortalFieldsFragment | null>(\n null,\n );\n const [form, setForm] = useState<ClientFormState>(emptyForm);\n\n // Delete confirm\n const [deleteTarget, setDeleteTarget] =\n useState<ClientPortalFieldsFragment | null>(null);\n\n // Per-row busy (toggle/regenerate)\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const handleCopy = async (client: ClientPortalFieldsFragment) => {\n if (!client.shareToken) return;\n try {\n await navigator.clipboard.writeText(shareUrlFor(client.shareToken));\n setCopiedId(client.id);\n window.setTimeout(() => setCopiedId(null), 2000);\n } catch {\n addToast({ title: \"Clipboard unavailable\", variant: \"error\" });\n }\n };\n\n const handleRegenerate = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await regenerate({ variables: { id: client.id } });\n addToast({ title: \"Share link regenerated\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Failed to regenerate link\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (client: ClientPortalFieldsFragment) => {\n setEditing(client);\n setForm({\n clientName: client.clientName,\n contactEmail: client.contactEmail ?? \"\",\n contactPhone: client.contactPhone ?? \"\",\n description: client.description ?? \"\",\n logoUrl: client.logoUrl ?? \"\",\n });\n setFormOpen(true);\n };\n\n /**\n * Upload a client logo to platform Files and store its presigned URL on the\n * form. Failures surface a toast but never block saving the client — the\n * logo is optional.\n */\n const handleLogoUpload = async (file: File) => {\n setLogoUploading(true);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/ClientLogos\" },\n });\n const folderId = (\n folderData as { ensureFolderByPath?: { id: string } } | null\n )?.ensureFolderByPath?.id;\n if (!folderId) throw new Error(\"Could not resolve upload folder\");\n const uploaded = await uploadBlobToFiles({\n client: apollo,\n blob: file,\n filename: file.name,\n mimeType: file.type || \"application/octet-stream\",\n folderId,\n });\n const { data } = await apollo.query({\n query: GET_FILE_DOWNLOAD_URL,\n variables: { id: uploaded.id, expiresIn: 604800 },\n fetchPolicy: \"no-cache\",\n });\n const url = (data as { getFileDownloadUrl?: string } | null | undefined)\n ?.getFileDownloadUrl;\n if (!url) throw new Error(\"No download URL returned\");\n setForm((f) => ({ ...f, logoUrl: url }));\n } catch {\n addToast({ title: \"Failed to upload logo\", variant: \"error\" });\n } finally {\n setLogoUploading(false);\n }\n };\n\n const closeForm = () => {\n setFormOpen(false);\n setEditing(null);\n };\n\n const handleSubmit = async () => {\n if (!form.clientName.trim()) return;\n try {\n if (editing) {\n await updateClient({\n variables: {\n id: editing.id,\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n },\n },\n });\n addToast({ title: \"Client updated\", variant: \"success\" });\n } else {\n await createClient({\n variables: {\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n isActive: true,\n },\n },\n });\n addToast({ title: \"Client added\", variant: \"success\" });\n }\n closeForm();\n await refreshAll();\n } catch {\n addToast({\n title: editing ? \"Failed to update client\" : \"Failed to add client\",\n variant: \"error\",\n });\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteClient({ variables: { id: deleteTarget.id } });\n addToast({ title: \"Client deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to delete client\", variant: \"error\" });\n }\n };\n\n const handleToggle = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await toggleClient({\n variables: { id: client.id, isActive: !client.isActive },\n });\n addToast({\n title: client.isActive ? \"Client deactivated\" : \"Client activated\",\n variant: \"success\",\n });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to toggle client\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { limit: PAGE_SIZE, offset: clients.length } },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n clientPortalConfigs: {\n ...fetchMoreResult.clientPortalConfigs,\n items: [\n ...prev.clientPortalConfigs.items,\n ...fetchMoreResult.clientPortalConfigs.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (configsLoading && clients.length === 0) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (configsError) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load client portal data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const addButton = (\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Client\n </button>\n );\n\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n purpose=\"Give clients a transparent window into the work you do for them — the projects, hours and progress tied to each one — so billing conversations are backed by data and trust, not spreadsheets.\"\n actions={addButton}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Active Clients\"\n value={String(stats?.activeClients ?? 0)}\n icon={<Building2 className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Projects\"\n value={String(stats?.totalProjectsShared ?? 0)}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Clients\"\n value={String(stats?.totalClients ?? 0)}\n icon={<Users className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Inactive\"\n value={String(Math.max(inactiveClients, 0))}\n icon={<PowerOff className=\"h-5 w-5\" />}\n iconBgColor=\"bg-bg-sunken text-text-secondary\"\n description=\"portals turned off\"\n />\n </div>\n\n {clients.length === 0 ? (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No clients configured yet\"\n description=\"Add your first client to share projects and tracked hours with them.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <ChartCard\n title=\"Tracked hours by client\"\n description=\"Top clients by logged hours (loaded page)\"\n className=\"lg:col-span-2\"\n >\n <BarsChart\n data={clientHoursData}\n xKey=\"name\"\n series={[{ key: \"hours\", label: \"Tracked hours\" }]}\n orientation=\"horizontal\"\n height={260}\n categoryWidth={120}\n valueFormat={(value) => `${value.toLocaleString()}h`}\n emptyLabel=\"No tracked hours logged yet.\"\n ariaLabel=\"Tracked hours by client\"\n />\n </ChartCard>\n <ChartCard\n title=\"Portal status\"\n description=\"Shared vs. private clients\"\n >\n <DonutChart\n data={statusMix}\n centerLabel=\"Clients\"\n centerValue={stats?.totalClients ?? 0}\n height={260}\n emptyLabel=\"No clients yet.\"\n ariaLabel=\"Clients by portal status\"\n />\n </ChartCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {clients.map((client: ClientPortalFieldsFragment) => {\n const totalTrackedHours = trackedHoursFor(client);\n const rowBusy = busyId === client.id;\n const copied = copiedId === client.id;\n return (\n <GlassCard\n key={client.id}\n treatment=\"insight\"\n glow={client.isActive}\n className=\"p-4\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n {client.logoUrl ? (\n <img\n src={client.logoUrl}\n alt={`${client.clientName} logo`}\n className=\"h-11 w-11 shrink-0 rounded-lg border border-border-seam object-cover\"\n />\n ) : (\n <div className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-sm font-semibold text-primary\">\n {getInitials(client.clientName)}\n </div>\n )}\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {client.clientName}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n Since {formatDate(client.createdAt)}\n </p>\n </div>\n </div>\n <StatusBadge\n label={client.isActive ? \"active\" : \"inactive\"}\n status={client.isActive ? \"active\" : \"inactive\"}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Projects\n </p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {client.projects.length}\n </p>\n </div>\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Tracked hours\n </p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {formatNumber(totalTrackedHours)}h\n </p>\n </div>\n </div>\n\n {maxTrackedHours > 0 && (\n <div className=\"mt-3\">\n <ProgressMeter\n value={totalTrackedHours}\n max={maxTrackedHours}\n tone=\"success\"\n size=\"sm\"\n showValue={false}\n label=\"Tracked volume vs. top client\"\n />\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <span className=\"text-xs font-medium text-text-secondary\">\n Public Access\n </span>\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => void handleToggle(client)}\n title={\n client.isActive\n ? \"Deactivate client portal\"\n : \"Activate client portal\"\n }\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors disabled:opacity-50\",\n client.isActive\n ? \"bg-status-success-bg text-status-success-text hover:bg-status-success-bg/80\"\n : \"border border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken/60\",\n )}\n >\n <Power className=\"h-3.5 w-3.5\" />\n {client.isActive ? \"Active\" : \"Inactive\"}\n </button>\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-2 border-t border-border-seam pt-3\">\n {client.isActive && client.shareToken && (\n <>\n <button\n type=\"button\"\n onClick={() => void handleCopy(client)}\n title=\"Copy the client's private share link\"\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/60\"\n >\n {copied ? (\n <>\n <Check className=\"h-3.5 w-3.5\" /> Copied\n </>\n ) : (\n <>\n <Link2 className=\"h-3.5 w-3.5\" /> Share link\n </>\n )}\n </button>\n <button\n type=\"button\"\n disabled={rowBusy && regenerating}\n onClick={() => void handleRegenerate(client)}\n title=\"Regenerate the link (invalidates the old one)\"\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/60 disabled:opacity-50\"\n >\n <RefreshCw\n className={cn(\n \"h-3.5 w-3.5\",\n rowBusy && regenerating && \"animate-spin\",\n )}\n />\n Regenerate\n </button>\n </>\n )}\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => openEdit(client)}\n title=\"Edit client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/60\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(client)}\n title=\"Delete client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {hasMore && (\n <div className=\"flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n <Modal\n open={formOpen}\n onClose={closeForm}\n title={editing ? \"Edit Client\" : \"Add Client\"}\n description={\n editing\n ? \"Update this client's portal details.\"\n : \"Set up a client so you can share projects and tracked hours with them.\"\n }\n footer={\n <>\n <button\n onClick={closeForm}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={creating || updating || !form.clientName.trim()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating || updating\n ? \"Saving...\"\n : editing\n ? \"Save Changes\"\n : \"Add Client\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Client Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Acme Corp\"\n value={form.clientName}\n onChange={(e) => setForm({ ...form, clientName: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Email\n </label>\n <input\n type=\"email\"\n placeholder=\"contact@client.com\"\n value={form.contactEmail}\n onChange={(e) =>\n setForm({ ...form, contactEmail: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Phone\n </label>\n <input\n type=\"tel\"\n placeholder=\"Optional\"\n value={form.contactPhone}\n onChange={(e) =>\n setForm({ ...form, contactPhone: e.target.value })\n }\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Description\n </label>\n <textarea\n rows={3}\n placeholder=\"Notes about this client (optional)\"\n value={form.description}\n onChange={(e) =>\n setForm({ ...form, description: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Logo\n </label>\n <input\n type=\"file\"\n accept=\"image/*\"\n disabled={logoUploading}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleLogoUpload(file);\n // Reset so re-selecting the same file re-triggers onChange.\n e.target.value = \"\";\n }}\n className={fileInputClass}\n />\n {logoUploading && (\n <p className=\"mt-1 text-xs text-text-secondary\">Uploading…</p>\n )}\n {form.logoUrl && (\n <img\n src={form.logoUrl}\n alt=\"Client logo preview\"\n className=\"mt-2 h-16 w-16 rounded-md border border-border-seam object-cover\"\n />\n )}\n </div>\n </div>\n </Modal>\n\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete client?\"\n description={\n deleteTarget\n ? `\"${deleteTarget.clientName}\" and its share link will be removed.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n The client will lose access to their shared portal immediately. This\n cannot be undone.\n </p>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/email-tracking/EmailTrackingPage.tsx"],"names":[],"mappings":"AA8LA,wBAAgB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"EmailTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/email-tracking/EmailTrackingPage.tsx"],"names":[],"mappings":"AA8LA,wBAAgB,iBAAiB,gCAkVhC"}
|
|
@@ -152,8 +152,10 @@ function A({ value: e, onChange: t }) {
|
|
|
152
152
|
function j() {
|
|
153
153
|
let { workspaceId: T = "" } = e(), [E, D] = p(!1), [O, j] = p(!1), { data: M, loading: ee, error: te, fetchMore: ne, refetch: N } = c({
|
|
154
154
|
variables: {
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
pagination: {
|
|
156
|
+
limit: C,
|
|
157
|
+
offset: 0
|
|
158
|
+
},
|
|
157
159
|
filter: E ? { isImportant: !0 } : void 0
|
|
158
160
|
},
|
|
159
161
|
skip: !T,
|
|
@@ -277,7 +279,10 @@ function j() {
|
|
|
277
279
|
j(!0);
|
|
278
280
|
try {
|
|
279
281
|
await ne({
|
|
280
|
-
variables: {
|
|
282
|
+
variables: { pagination: {
|
|
283
|
+
limit: C,
|
|
284
|
+
offset: P.length
|
|
285
|
+
} },
|
|
281
286
|
updateQuery: (e, { fetchMoreResult: t }) => t ? {
|
|
282
287
|
...e,
|
|
283
288
|
emailTimeEntries: [...e.emailTimeEntries, ...t.emailTimeEntries]
|