@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimeApprovalPage.js","names":[],"sources":["../../../../src/timecampus/pages/time-approval/TimeApprovalPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n CheckCircle,\n XCircle,\n X,\n Clock,\n MessageSquare,\n MessagesSquare,\n Undo2,\n} from \"lucide-react\";\nimport { ContextConversationWidget } from \"@burdenoff/fe-libs/shared\";\nimport { useRegisterPageConversationContext } from \"@burdenoff/fe-libs/shared/providers/shell\";\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 { ChartCard, DonutChart, ProgressMeter } from \"../../components\";\nimport { getInitials, formatHours, formatDate, cn } from \"../../utils/helpers\";\nimport {\n useListApprovalRequestsQuery,\n useGetApprovalStatsQuery,\n useApproveRequestMutation,\n useRejectRequestMutation,\n useRequestRevisionMutation,\n useWithdrawRequestMutation,\n type ApprovalRequestFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { ApprovalStatus } from \"@/generated/wspace-types\";\n\nconst PAGE_SIZE = 25;\n\n// Rendered as a child of the review Modal so the registration hook only runs\n// while the modal is mounted — it scopes the shared page-conversation widget to\n// the selected approval request, then releases the context on close/unmount.\nfunction ApprovalReviewThread({\n request,\n}: {\n request: ApprovalRequestFieldsFragment;\n}) {\n useRegisterPageConversationContext({\n entityType: \"TIMECAMPUS_APPROVAL_REQUEST\",\n entityId: request.id,\n contextTitle: `Approval · ${request.period ?? request.id}`,\n });\n\n return <ContextConversationWidget widgetKey=\"approval-review\" />;\n}\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\nexport function TimeApprovalPage() {\n const { toasts, addToast, dismissToast } = useToast();\n\n const {\n data: requestsData,\n loading: requestsLoading,\n error: requestsError,\n refetch,\n fetchMore,\n } = useListApprovalRequestsQuery({\n variables: { limit: PAGE_SIZE, offset: 0 },\n notifyOnNetworkStatusChange: true,\n });\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n refetch: refetchStats,\n } = useGetApprovalStatsQuery();\n\n const [approveRequest] = useApproveRequestMutation();\n const [rejectRequest, { loading: rejecting }] = useRejectRequestMutation();\n const [requestRevision, { loading: requestingRevision }] =\n useRequestRevisionMutation();\n const [withdrawRequest] = useWithdrawRequestMutation();\n\n const requests = requestsData?.approvalRequests?.items ?? [];\n const total = requestsData?.approvalRequests?.total ?? 0;\n const hasMore = requestsData?.approvalRequests?.hasMore ?? false;\n const stats = statsData?.approvalStats;\n\n const approvalMix = [\n { name: \"Pending\", value: stats?.pendingCount ?? 0 },\n { name: \"Approved\", value: stats?.approvedCount ?? 0 },\n { name: \"Rejected\", value: stats?.rejectedCount ?? 0 },\n ];\n\n const [loadingMore, setLoadingMore] = useState(false);\n const [busyId, setBusyId] = useState<string | null>(null);\n\n // Reject reason modal\n const [rejectTarget, setRejectTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n const [rejectReason, setRejectReason] = useState(\"\");\n\n // Request revision modal\n const [revisionTarget, setRevisionTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n const [revisionNotes, setRevisionNotes] = useState(\"\");\n\n // Withdraw confirm modal\n const [withdrawTarget, setWithdrawTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n\n // Review / discussion thread modal\n const [reviewTarget, setReviewTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const handleApprove = async (id: string) => {\n setBusyId(id);\n try {\n await approveRequest({ variables: { id } });\n addToast({ title: \"Timesheet approved\", variant: \"success\" });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to approve timesheet\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleReject = async () => {\n if (!rejectTarget || !rejectReason.trim()) return;\n try {\n await rejectRequest({\n variables: { id: rejectTarget.id, reason: rejectReason.trim() },\n });\n addToast({ title: \"Timesheet rejected\", variant: \"success\" });\n setRejectTarget(null);\n setRejectReason(\"\");\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to reject timesheet\", variant: \"error\" });\n }\n };\n\n const handleRequestRevision = async () => {\n if (!revisionTarget || !revisionNotes.trim()) return;\n try {\n await requestRevision({\n variables: { id: revisionTarget.id, notes: revisionNotes.trim() },\n });\n addToast({ title: \"Revision requested\", variant: \"success\" });\n setRevisionTarget(null);\n setRevisionNotes(\"\");\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to request revision\", variant: \"error\" });\n }\n };\n\n const handleWithdraw = async () => {\n if (!withdrawTarget) return;\n try {\n await withdrawRequest({ variables: { id: withdrawTarget.id } });\n addToast({ title: \"Request withdrawn\", variant: \"success\" });\n setWithdrawTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to withdraw request\", variant: \"error\" });\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { limit: PAGE_SIZE, offset: requests.length },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n approvalRequests: {\n ...fetchMoreResult.approvalRequests,\n items: [\n ...prev.approvalRequests.items,\n ...fetchMoreResult.approvalRequests.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (requestsLoading && statsLoading && requests.length === 0) {\n return (\n <PageLayout\n title=\"Time Approval\"\n description=\"Review and approve team timesheets\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (requestsError && statsError) {\n return (\n <PageLayout\n title=\"Time Approval\"\n description=\"Review and approve team timesheets\"\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 approval data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Time Approval\"\n description=\"Review and approve team timesheets\"\n purpose=\"The reviewer's queue for signing off team timesheets before they're billed or paid — approve, reject or request changes in one place so payroll and invoices go out accurate and on time.\"\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Pending Approvals\"\n value={String(stats?.pendingCount ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-warning-bg text-status-warning-text\"\n />\n <StatCard\n title=\"Approved This Week\"\n value={String(stats?.approvedCount ?? 0)}\n icon={<CheckCircle className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n <StatCard\n title=\"Rejected\"\n value={String(stats?.rejectedCount ?? 0)}\n icon={<XCircle className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-error-bg text-status-error-text\"\n />\n <StatCard\n title=\"Avg. Review Time\"\n value={\n stats?.averageReviewTime\n ? `${Math.round(stats.averageReviewTime)}h`\n : \"N/A\"\n }\n description=\"Target: 24h\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"space-y-4 lg:col-span-2\">\n <div className=\"flex items-center justify-between gap-3\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n Pending Requests\n </h2>\n <p className=\"text-xs text-text-secondary\">\n Showing {requests.length} of {total}\n </p>\n </div>\n\n {requests.length === 0 && !requestsLoading ? (\n <EmptyState\n illustration=\"empty-notifications\"\n title=\"No approval requests found\"\n description=\"You're all caught up — submitted timesheets waiting on your review will appear here.\"\n />\n ) : (\n <>\n <div className=\"space-y-3\">\n {requests.map((request: ApprovalRequestFieldsFragment) => {\n const isPending = request.status === ApprovalStatus.Pending;\n const rowBusy = busyId === request.id;\n const selected = reviewTarget?.id === request.id;\n const billablePct =\n request.totalHours > 0\n ? Math.round(\n (request.billableHours / request.totalHours) * 100,\n )\n : 0;\n return (\n <GlassCard\n key={request.id}\n treatment=\"plain\"\n glow={selected}\n className={cn(\n \"p-4\",\n selected && \"border-action-primary-bg/50\",\n )}\n >\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-sm font-medium text-primary\">\n {getInitials(request.userId)}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {request.userId}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {request.period} · Submitted{\" \"}\n {formatDate(request.submittedAt)}\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-5\">\n <div className=\"flex items-center gap-6\">\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Total submitted\n </p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatHours(request.totalHours)}\n </p>\n </div>\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Billable\n </p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatHours(request.billableHours)}\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge\n label={request.status.toLowerCase()}\n status={request.status.toLowerCase()}\n />\n {isPending && (\n <>\n <button\n onClick={() => handleApprove(request.id)}\n disabled={rowBusy}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-status-success-bg px-3 py-1.5 text-sm font-medium text-status-success-text hover:bg-status-success-bg/80 disabled:opacity-50\"\n >\n <CheckCircle className=\"h-4 w-4\" />\n Approve\n </button>\n <button\n onClick={() => {\n setRejectTarget(request);\n setRejectReason(\"\");\n }}\n disabled={rowBusy}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-status-error-bg px-3 py-1.5 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n <XCircle className=\"h-4 w-4\" />\n Reject\n </button>\n <button\n onClick={() => {\n setRevisionTarget(request);\n setRevisionNotes(\"\");\n }}\n disabled={rowBusy}\n title=\"Request revision\"\n className=\"rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-warning-text hover:bg-status-warning-bg disabled:opacity-50\"\n >\n <MessageSquare className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setWithdrawTarget(request)}\n disabled={rowBusy}\n title=\"Withdraw request\"\n className=\"rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Undo2 className=\"h-4 w-4\" />\n </button>\n </>\n )}\n <button\n onClick={() =>\n setReviewTarget(selected ? null : request)\n }\n title=\"Review / Discussion\"\n className={cn(\n \"rounded-md border p-1.5\",\n selected\n ? \"border-action-primary-bg bg-action-primary-bg/10 text-primary\"\n : \"border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken\",\n )}\n >\n <MessagesSquare className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </div>\n\n {request.totalHours > 0 && (\n <div className=\"mt-4\">\n <ProgressMeter\n value={billablePct}\n max={100}\n tone=\"success\"\n size=\"sm\"\n showValue={false}\n label={`Billable share · ${billablePct}%`}\n />\n </div>\n )}\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 </div>\n\n <div className=\"lg:col-span-1\">\n <div className=\"space-y-4 lg:sticky lg:top-4\">\n <ChartCard\n title=\"Approval mix\"\n description=\"Requests by current status\"\n >\n <DonutChart\n data={approvalMix}\n centerLabel=\"Total\"\n centerValue={stats?.totalRequests ?? 0}\n height={220}\n emptyLabel=\"No requests to summarise yet.\"\n ariaLabel=\"Approval requests by status\"\n />\n </ChartCard>\n\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between px-1\">\n <h3 className=\"text-sm font-medium text-text-primary\">\n Review discussion\n </h3>\n {reviewTarget && (\n <button\n onClick={() => setReviewTarget(null)}\n title=\"Close thread\"\n className=\"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken\"\n >\n <X className=\"h-3.5 w-3.5\" />\n Clear\n </button>\n )}\n </div>\n {reviewTarget ? (\n <>\n <p className=\"px-1 text-xs text-text-secondary\">\n Discussing {reviewTarget.userId} ·{\" \"}\n {reviewTarget.period}\n </p>\n <ApprovalReviewThread request={reviewTarget} />\n </>\n ) : (\n <GlassCard\n treatment=\"insight\"\n className=\"border-border-seam p-6\"\n >\n <div className=\"flex flex-col items-center justify-center gap-2 text-center\">\n <MessagesSquare className=\"h-6 w-6 text-text-secondary\" />\n <p className=\"text-sm font-medium text-text-primary\">\n No timesheet selected\n </p>\n <p className=\"text-xs text-text-secondary\">\n Open a request's discussion icon to review it with the\n submitter.\n </p>\n </div>\n </GlassCard>\n )}\n </div>\n </div>\n </div>\n </div>\n\n <Modal\n open={rejectTarget !== null}\n onClose={() => setRejectTarget(null)}\n title=\"Reject timesheet\"\n description={\n rejectTarget\n ? `Rejecting ${rejectTarget.userId}'s ${rejectTarget.period} timesheet.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRejectTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleReject}\n disabled={rejecting || !rejectReason.trim()}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {rejecting ? \"Rejecting...\" : \"Reject\"}\n </button>\n </>\n }\n >\n <label className=\"block text-sm font-medium text-text-primary\">\n Rejection reason\n </label>\n <textarea\n rows={3}\n placeholder=\"Explain why this timesheet is being rejected\"\n value={rejectReason}\n onChange={(e) => setRejectReason(e.target.value)}\n className={inputClass}\n />\n </Modal>\n\n <Modal\n open={revisionTarget !== null}\n onClose={() => setRevisionTarget(null)}\n title=\"Request revision\"\n description={\n revisionTarget\n ? `Ask ${revisionTarget.userId} to revise their ${revisionTarget.period} timesheet.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRevisionTarget(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={handleRequestRevision}\n disabled={requestingRevision || !revisionNotes.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 {requestingRevision ? \"Sending...\" : \"Request Revision\"}\n </button>\n </>\n }\n >\n <label className=\"block text-sm font-medium text-text-primary\">\n What needs changing?\n </label>\n <textarea\n rows={3}\n placeholder=\"Describe the changes needed before this can be approved\"\n value={revisionNotes}\n onChange={(e) => setRevisionNotes(e.target.value)}\n className={inputClass}\n />\n </Modal>\n\n <Modal\n open={withdrawTarget !== null}\n onClose={() => setWithdrawTarget(null)}\n title=\"Withdraw request?\"\n description={\n withdrawTarget\n ? `${withdrawTarget.userId}'s ${withdrawTarget.period} timesheet will be pulled from the review queue.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setWithdrawTarget(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={handleWithdraw}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n Withdraw\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Withdrawing removes the request from the approval queue. It can be\n resubmitted later.\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":"TimeApprovalPage.js","names":[],"sources":["../../../../src/timecampus/pages/time-approval/TimeApprovalPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n CheckCircle,\n XCircle,\n X,\n Clock,\n MessageSquare,\n MessagesSquare,\n Undo2,\n} from \"lucide-react\";\nimport { ContextConversationWidget } from \"@burdenoff/fe-libs/shared\";\nimport { useRegisterPageConversationContext } from \"@burdenoff/fe-libs/shared/providers/shell\";\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 { ChartCard, DonutChart, ProgressMeter } from \"../../components\";\nimport { getInitials, formatHours, formatDate, cn } from \"../../utils/helpers\";\nimport {\n useListApprovalRequestsQuery,\n useGetApprovalStatsQuery,\n useApproveRequestMutation,\n useRejectRequestMutation,\n useRequestRevisionMutation,\n useWithdrawRequestMutation,\n type ApprovalRequestFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { ApprovalStatus } from \"@/generated/wspace-types\";\n\nconst PAGE_SIZE = 25;\n\n// Rendered as a child of the review Modal so the registration hook only runs\n// while the modal is mounted — it scopes the shared page-conversation widget to\n// the selected approval request, then releases the context on close/unmount.\nfunction ApprovalReviewThread({\n request,\n}: {\n request: ApprovalRequestFieldsFragment;\n}) {\n useRegisterPageConversationContext({\n entityType: \"TIMECAMPUS_APPROVAL_REQUEST\",\n entityId: request.id,\n contextTitle: `Approval · ${request.period ?? request.id}`,\n });\n\n return <ContextConversationWidget widgetKey=\"approval-review\" />;\n}\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\nexport function TimeApprovalPage() {\n const { toasts, addToast, dismissToast } = useToast();\n\n const {\n data: requestsData,\n loading: requestsLoading,\n error: requestsError,\n refetch,\n fetchMore,\n } = useListApprovalRequestsQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n notifyOnNetworkStatusChange: true,\n });\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n refetch: refetchStats,\n } = useGetApprovalStatsQuery();\n\n const [approveRequest] = useApproveRequestMutation();\n const [rejectRequest, { loading: rejecting }] = useRejectRequestMutation();\n const [requestRevision, { loading: requestingRevision }] =\n useRequestRevisionMutation();\n const [withdrawRequest] = useWithdrawRequestMutation();\n\n const requests = requestsData?.approvalRequests?.items ?? [];\n const total = requestsData?.approvalRequests?.total ?? 0;\n const hasMore = requestsData?.approvalRequests?.hasMore ?? false;\n const stats = statsData?.approvalStats;\n\n const approvalMix = [\n { name: \"Pending\", value: stats?.pendingCount ?? 0 },\n { name: \"Approved\", value: stats?.approvedCount ?? 0 },\n { name: \"Rejected\", value: stats?.rejectedCount ?? 0 },\n ];\n\n const [loadingMore, setLoadingMore] = useState(false);\n const [busyId, setBusyId] = useState<string | null>(null);\n\n // Reject reason modal\n const [rejectTarget, setRejectTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n const [rejectReason, setRejectReason] = useState(\"\");\n\n // Request revision modal\n const [revisionTarget, setRevisionTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n const [revisionNotes, setRevisionNotes] = useState(\"\");\n\n // Withdraw confirm modal\n const [withdrawTarget, setWithdrawTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n\n // Review / discussion thread modal\n const [reviewTarget, setReviewTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const handleApprove = async (id: string) => {\n setBusyId(id);\n try {\n await approveRequest({ variables: { id } });\n addToast({ title: \"Timesheet approved\", variant: \"success\" });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to approve timesheet\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleReject = async () => {\n if (!rejectTarget || !rejectReason.trim()) return;\n try {\n await rejectRequest({\n variables: { id: rejectTarget.id, reason: rejectReason.trim() },\n });\n addToast({ title: \"Timesheet rejected\", variant: \"success\" });\n setRejectTarget(null);\n setRejectReason(\"\");\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to reject timesheet\", variant: \"error\" });\n }\n };\n\n const handleRequestRevision = async () => {\n if (!revisionTarget || !revisionNotes.trim()) return;\n try {\n await requestRevision({\n variables: { id: revisionTarget.id, notes: revisionNotes.trim() },\n });\n addToast({ title: \"Revision requested\", variant: \"success\" });\n setRevisionTarget(null);\n setRevisionNotes(\"\");\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to request revision\", variant: \"error\" });\n }\n };\n\n const handleWithdraw = async () => {\n if (!withdrawTarget) return;\n try {\n await withdrawRequest({ variables: { id: withdrawTarget.id } });\n addToast({ title: \"Request withdrawn\", variant: \"success\" });\n setWithdrawTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to withdraw request\", variant: \"error\" });\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: {\n pagination: { limit: PAGE_SIZE, offset: requests.length },\n },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n approvalRequests: {\n ...fetchMoreResult.approvalRequests,\n items: [\n ...prev.approvalRequests.items,\n ...fetchMoreResult.approvalRequests.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (requestsLoading && statsLoading && requests.length === 0) {\n return (\n <PageLayout\n title=\"Time Approval\"\n description=\"Review and approve team timesheets\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (requestsError && statsError) {\n return (\n <PageLayout\n title=\"Time Approval\"\n description=\"Review and approve team timesheets\"\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 approval data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Time Approval\"\n description=\"Review and approve team timesheets\"\n purpose=\"The reviewer's queue for signing off team timesheets before they're billed or paid — approve, reject or request changes in one place so payroll and invoices go out accurate and on time.\"\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Pending Approvals\"\n value={String(stats?.pendingCount ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-warning-bg text-status-warning-text\"\n />\n <StatCard\n title=\"Approved This Week\"\n value={String(stats?.approvedCount ?? 0)}\n icon={<CheckCircle className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n <StatCard\n title=\"Rejected\"\n value={String(stats?.rejectedCount ?? 0)}\n icon={<XCircle className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-error-bg text-status-error-text\"\n />\n <StatCard\n title=\"Avg. Review Time\"\n value={\n stats?.averageReviewTime\n ? `${Math.round(stats.averageReviewTime)}h`\n : \"N/A\"\n }\n description=\"Target: 24h\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"space-y-4 lg:col-span-2\">\n <div className=\"flex items-center justify-between gap-3\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n Pending Requests\n </h2>\n <p className=\"text-xs text-text-secondary\">\n Showing {requests.length} of {total}\n </p>\n </div>\n\n {requests.length === 0 && !requestsLoading ? (\n <EmptyState\n illustration=\"empty-notifications\"\n title=\"No approval requests found\"\n description=\"You're all caught up — submitted timesheets waiting on your review will appear here.\"\n />\n ) : (\n <>\n <div className=\"space-y-3\">\n {requests.map((request: ApprovalRequestFieldsFragment) => {\n const isPending = request.status === ApprovalStatus.Pending;\n const rowBusy = busyId === request.id;\n const selected = reviewTarget?.id === request.id;\n const billablePct =\n request.totalHours > 0\n ? Math.round(\n (request.billableHours / request.totalHours) * 100,\n )\n : 0;\n return (\n <GlassCard\n key={request.id}\n treatment=\"plain\"\n glow={selected}\n className={cn(\n \"p-4\",\n selected && \"border-action-primary-bg/50\",\n )}\n >\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-sm font-medium text-primary\">\n {getInitials(request.userId)}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {request.userId}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {request.period} · Submitted{\" \"}\n {formatDate(request.submittedAt)}\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-5\">\n <div className=\"flex items-center gap-6\">\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Total submitted\n </p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatHours(request.totalHours)}\n </p>\n </div>\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Billable\n </p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatHours(request.billableHours)}\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge\n label={request.status.toLowerCase()}\n status={request.status.toLowerCase()}\n />\n {isPending && (\n <>\n <button\n onClick={() => handleApprove(request.id)}\n disabled={rowBusy}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-status-success-bg px-3 py-1.5 text-sm font-medium text-status-success-text hover:bg-status-success-bg/80 disabled:opacity-50\"\n >\n <CheckCircle className=\"h-4 w-4\" />\n Approve\n </button>\n <button\n onClick={() => {\n setRejectTarget(request);\n setRejectReason(\"\");\n }}\n disabled={rowBusy}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-status-error-bg px-3 py-1.5 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n <XCircle className=\"h-4 w-4\" />\n Reject\n </button>\n <button\n onClick={() => {\n setRevisionTarget(request);\n setRevisionNotes(\"\");\n }}\n disabled={rowBusy}\n title=\"Request revision\"\n className=\"rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-warning-text hover:bg-status-warning-bg disabled:opacity-50\"\n >\n <MessageSquare className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setWithdrawTarget(request)}\n disabled={rowBusy}\n title=\"Withdraw request\"\n className=\"rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Undo2 className=\"h-4 w-4\" />\n </button>\n </>\n )}\n <button\n onClick={() =>\n setReviewTarget(selected ? null : request)\n }\n title=\"Review / Discussion\"\n className={cn(\n \"rounded-md border p-1.5\",\n selected\n ? \"border-action-primary-bg bg-action-primary-bg/10 text-primary\"\n : \"border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken\",\n )}\n >\n <MessagesSquare className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </div>\n\n {request.totalHours > 0 && (\n <div className=\"mt-4\">\n <ProgressMeter\n value={billablePct}\n max={100}\n tone=\"success\"\n size=\"sm\"\n showValue={false}\n label={`Billable share · ${billablePct}%`}\n />\n </div>\n )}\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 </div>\n\n <div className=\"lg:col-span-1\">\n <div className=\"space-y-4 lg:sticky lg:top-4\">\n <ChartCard\n title=\"Approval mix\"\n description=\"Requests by current status\"\n >\n <DonutChart\n data={approvalMix}\n centerLabel=\"Total\"\n centerValue={stats?.totalRequests ?? 0}\n height={220}\n emptyLabel=\"No requests to summarise yet.\"\n ariaLabel=\"Approval requests by status\"\n />\n </ChartCard>\n\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between px-1\">\n <h3 className=\"text-sm font-medium text-text-primary\">\n Review discussion\n </h3>\n {reviewTarget && (\n <button\n onClick={() => setReviewTarget(null)}\n title=\"Close thread\"\n className=\"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken\"\n >\n <X className=\"h-3.5 w-3.5\" />\n Clear\n </button>\n )}\n </div>\n {reviewTarget ? (\n <>\n <p className=\"px-1 text-xs text-text-secondary\">\n Discussing {reviewTarget.userId} ·{\" \"}\n {reviewTarget.period}\n </p>\n <ApprovalReviewThread request={reviewTarget} />\n </>\n ) : (\n <GlassCard\n treatment=\"insight\"\n className=\"border-border-seam p-6\"\n >\n <div className=\"flex flex-col items-center justify-center gap-2 text-center\">\n <MessagesSquare className=\"h-6 w-6 text-text-secondary\" />\n <p className=\"text-sm font-medium text-text-primary\">\n No timesheet selected\n </p>\n <p className=\"text-xs text-text-secondary\">\n Open a request's discussion icon to review it with the\n submitter.\n </p>\n </div>\n </GlassCard>\n )}\n </div>\n </div>\n </div>\n </div>\n\n <Modal\n open={rejectTarget !== null}\n onClose={() => setRejectTarget(null)}\n title=\"Reject timesheet\"\n description={\n rejectTarget\n ? `Rejecting ${rejectTarget.userId}'s ${rejectTarget.period} timesheet.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRejectTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleReject}\n disabled={rejecting || !rejectReason.trim()}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {rejecting ? \"Rejecting...\" : \"Reject\"}\n </button>\n </>\n }\n >\n <label className=\"block text-sm font-medium text-text-primary\">\n Rejection reason\n </label>\n <textarea\n rows={3}\n placeholder=\"Explain why this timesheet is being rejected\"\n value={rejectReason}\n onChange={(e) => setRejectReason(e.target.value)}\n className={inputClass}\n />\n </Modal>\n\n <Modal\n open={revisionTarget !== null}\n onClose={() => setRevisionTarget(null)}\n title=\"Request revision\"\n description={\n revisionTarget\n ? `Ask ${revisionTarget.userId} to revise their ${revisionTarget.period} timesheet.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRevisionTarget(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={handleRequestRevision}\n disabled={requestingRevision || !revisionNotes.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 {requestingRevision ? \"Sending...\" : \"Request Revision\"}\n </button>\n </>\n }\n >\n <label className=\"block text-sm font-medium text-text-primary\">\n What needs changing?\n </label>\n <textarea\n rows={3}\n placeholder=\"Describe the changes needed before this can be approved\"\n value={revisionNotes}\n onChange={(e) => setRevisionNotes(e.target.value)}\n className={inputClass}\n />\n </Modal>\n\n <Modal\n open={withdrawTarget !== null}\n onClose={() => setWithdrawTarget(null)}\n title=\"Withdraw request?\"\n description={\n withdrawTarget\n ? `${withdrawTarget.userId}'s ${withdrawTarget.period} timesheet will be pulled from the review queue.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setWithdrawTarget(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={handleWithdraw}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n Withdraw\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Withdrawing removes the request from the approval queue. It can be\n resubmitted later.\n </p>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAiCA,IAAM,IAAY;AAKlB,SAAS,EAAqB,EAC5B,cAGC;AAOD,QANA,EAAmC;EACjC,YAAY;EACZ,UAAU,EAAQ;EAClB,cAAc,cAAc,EAAQ,UAAU,EAAQ;EACvD,CAAC,EAEK,kBAAC,GAAD,EAA2B,WAAU,mBAAoB,CAAA;;AAGlE,IAAM,IACJ;AAEF,SAAgB,IAAmB;CACjC,IAAM,EAAE,WAAQ,aAAU,oBAAiB,IAAU,EAE/C,EACJ,MAAM,GACN,SAAS,GACT,OAAO,IACP,aACA,kBACE,EAA6B;EAC/B,WAAW,EAAE,YAAY;GAAE,OAAO;GAAW,QAAQ;GAAG,EAAE;EAC1D,6BAA6B;EAC9B,CAAC,EACI,EACJ,MAAM,IACN,SAAS,IACT,OAAO,GACP,SAAS,MACP,GAA0B,EAExB,CAAC,KAAkB,GAA2B,EAC9C,CAAC,GAAe,EAAE,SAAS,OAAe,IAA0B,EACpE,CAAC,GAAiB,EAAE,SAAS,OACjC,IAA4B,EACxB,CAAC,MAAmB,IAA4B,EAEhD,IAAW,GAAc,kBAAkB,SAAS,EAAE,EACtD,KAAQ,GAAc,kBAAkB,SAAS,GACjD,KAAU,GAAc,kBAAkB,WAAW,IACrD,IAAQ,IAAW,eAEnB,KAAc;EAClB;GAAE,MAAM;GAAW,OAAO,GAAO,gBAAgB;GAAG;EACpD;GAAE,MAAM;GAAY,OAAO,GAAO,iBAAiB;GAAG;EACtD;GAAE,MAAM;GAAY,OAAO,GAAO,iBAAiB;GAAG;EACvD,EAEK,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,CAAC,IAAQ,KAAa,EAAwB,KAAK,EAGnD,CAAC,GAAc,KACnB,EAA+C,KAAK,EAChD,CAAC,GAAc,KAAmB,EAAS,GAAG,EAG9C,CAAC,GAAgB,KACrB,EAA+C,KAAK,EAChD,CAAC,GAAe,KAAoB,EAAS,GAAG,EAGhD,CAAC,GAAgB,KACrB,EAA+C,KAAK,EAGhD,CAAC,GAAc,KACnB,EAA+C,KAAK,EAEhD,IAAa,YAAY;AAC7B,QAAM,QAAQ,IAAI,CAAC,IAAS,EAAE,GAAc,CAAC,CAAC;IAG1C,KAAgB,OAAO,MAAe;AAC1C,IAAU,EAAG;AACb,MAAI;AAGF,GAFA,MAAM,EAAe,EAAE,WAAW,EAAE,OAAI,EAAE,CAAC,EAC3C,EAAS;IAAE,OAAO;IAAsB,SAAS;IAAW,CAAC,EAC7D,MAAM,GAAY;UACZ;AACN,KAAS;IAAE,OAAO;IAA+B,SAAS;IAAS,CAAC;YAC5D;AACR,KAAU,KAAK;;;AAgGnB,QAxBI,KAAmB,MAAgB,EAAS,WAAW,IAEvD,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;YAEZ,kBAAC,GAAD;GAAiB,SAAQ;GAAO,OAAO;GAAK,CAAA;EACjC,CAAA,GAIb,MAAiB,IAEjB,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;YAEZ,kBAAC,OAAD;GAAK,WAAU;aAAoG;GAE7G,CAAA;EACK,CAAA,GAKf,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SAAQ;YAHV;GAKE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,UAAA;MACA,OAAM;MACN,OAAO,OAAO,GAAO,gBAAgB,EAAE;MACvC,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;MACnC,aAAY;MACZ,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,OAAO,GAAO,iBAAiB,EAAE;MACxC,MAAM,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;MACzC,aAAY;MACZ,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,OAAO,GAAO,iBAAiB,EAAE;MACxC,MAAM,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;MACrC,aAAY;MACZ,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OACE,GAAO,oBACH,GAAG,KAAK,MAAM,EAAM,kBAAkB,CAAC,KACvC;MAEN,aAAY;MACZ,CAAA;KACE;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAA0C;OAEnD,CAAA,EACL,kBAAC,KAAD;OAAG,WAAU;iBAAb;QAA2C;QAChC,EAAS;QAAO;QAAK;QAC5B;SACA;SAEL,EAAS,WAAW,KAAK,CAAC,IACzB,kBAAC,IAAD;MACE,cAAa;MACb,OAAM;MACN,aAAY;MACZ,CAAA,GAEF,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAS,KAAK,MAA2C;OACxD,IAAM,IAAY,EAAQ,WAAW,GAAe,SAC9C,IAAU,OAAW,EAAQ,IAC7B,IAAW,GAAc,OAAO,EAAQ,IACxC,IACJ,EAAQ,aAAa,IACjB,KAAK,MACF,EAAQ,gBAAgB,EAAQ,aAAc,IAChD,GACD;AACN,cACE,kBAAC,GAAD;QAEE,WAAU;QACV,MAAM;QACN,WAAW,EACT,OACA,KAAY,8BACb;kBAPH,CASE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,OAAD;WAAK,WAAU;qBACZ,GAAY,EAAQ,OAAO;WACxB,CAAA,EACN,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAQ;YACP,CAAA,EACJ,kBAAC,KAAD;YAAG,WAAU;sBAAb;aACG,EAAQ;aAAO;aAAoB;aACnC,EAAW,EAAQ,YAAY;aAC9B;cACA;aACF;aAEN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;YAAG,WAAU;sBAAsE;YAE/E,CAAA,EACJ,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAY,EAAQ,WAAW;YAC9B,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;YAAG,WAAU;sBAAsE;YAE/E,CAAA,EACJ,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAY,EAAQ,cAAc;YACjC,CAAA,CACA,EAAA,CAAA,CACF;cAEN,kBAAC,OAAD;WAAK,WAAU;qBAAf;YACE,kBAAC,GAAD;aACE,OAAO,EAAQ,OAAO,aAAa;aACnC,QAAQ,EAAQ,OAAO,aAAa;aACpC,CAAA;YACD,KACC,kBAAA,GAAA,EAAA,UAAA;aACE,kBAAC,UAAD;cACE,eAAe,GAAc,EAAQ,GAAG;cACxC,UAAU;cACV,WAAU;wBAHZ,CAKE,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA,EAAA,UAE5B;;aACT,kBAAC,UAAD;cACE,eAAe;AAEb,eADA,EAAgB,EAAQ,EACxB,EAAgB,GAAG;;cAErB,UAAU;cACV,WAAU;wBANZ,CAQE,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA,EAAA,SAExB;;aACT,kBAAC,UAAD;cACE,eAAe;AAEb,eADA,EAAkB,EAAQ,EAC1B,EAAiB,GAAG;;cAEtB,UAAU;cACV,OAAM;cACN,WAAU;wBAEV,kBAAC,IAAD,EAAe,WAAU,WAAY,CAAA;cAC9B,CAAA;aACT,kBAAC,UAAD;cACE,eAAe,EAAkB,EAAQ;cACzC,UAAU;cACV,OAAM;cACN,WAAU;wBAEV,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;cACtB,CAAA;aACR,EAAA,CAAA;YAEL,kBAAC,UAAD;aACE,eACE,EAAgB,IAAW,OAAO,EAAQ;aAE5C,OAAM;aACN,WAAW,EACT,2BACA,IACI,kEACA,0EACL;uBAED,kBAAC,GAAD,EAAgB,WAAU,WAAY,CAAA;aAC/B,CAAA;YACL;aACF;YACF;YAEL,EAAQ,aAAa,KACpB,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,IAAD;UACE,OAAO;UACP,KAAK;UACL,MAAK;UACL,MAAK;UACL,WAAW;UACX,OAAO,oBAAoB,EAAY;UACvC,CAAA;SACE,CAAA,CAEE;UAzHL,EAAQ,GAyHH;QAEd;MACE,CAAA,EAEL,MACC,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,UAAD;OACE,SAxPK,YAAY;AACjC,UAAe,GAAK;AACpB,YAAI;AACF,eAAM,GAAU;UACd,WAAW,EACT,YAAY;WAAE,OAAO;WAAW,QAAQ,EAAS;WAAQ,EAC1D;UACD,cAAc,GAAM,EAAE,yBACf,IACE;WACL,GAAG;WACH,kBAAkB;YAChB,GAAG,EAAgB;YACnB,OAAO,CACL,GAAG,EAAK,iBAAiB,OACzB,GAAG,EAAgB,iBAAiB,MACrC;YACF;WACF,GAV4B;UAYhC,CAAC;kBACM;AACR,WAAe,GAAM;;;OAmOP,UAAU;OACV,WAAU;iBAET,IAAc,eAAe;OACvB,CAAA;MACL,CAAA,CAEP,EAAA,CAAA,CAED;QAEN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,IAAD;OACE,OAAM;OACN,aAAY;iBAEZ,kBAAC,IAAD;QACE,MAAM;QACN,aAAY;QACZ,aAAa,GAAO,iBAAiB;QACrC,QAAQ;QACR,YAAW;QACX,WAAU;QACV,CAAA;OACQ,CAAA,EAEZ,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,MAAD;SAAI,WAAU;mBAAwC;SAEjD,CAAA,EACJ,KACC,kBAAC,UAAD;SACE,eAAe,EAAgB,KAAK;SACpC,OAAM;SACN,WAAU;mBAHZ,CAKE,kBAAC,IAAD,EAAG,WAAU,eAAgB,CAAA,EAAA,QAEtB;WAEP;WACL,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAb;SAAgD;SAClC,EAAa;SAAO;SAAU;SACzC,EAAa;SACZ;WACJ,kBAAC,GAAD,EAAsB,SAAS,GAAgB,CAAA,CAC9C,EAAA,CAAA,GAEH,kBAAC,GAAD;QACE,WAAU;QACV,WAAU;kBAEV,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,GAAD,EAAgB,WAAU,+BAAgC,CAAA;UAC1D,kBAAC,KAAD;WAAG,WAAU;qBAAwC;WAEjD,CAAA;UACJ,kBAAC,KAAD;WAAG,WAAU;qBAA8B;WAGvC,CAAA;UACA;;QACI,CAAA,CAEV;SACF;;KACF,CAAA,CACF;;GAEN,kBAAC,GAAD;IACE,MAAM,MAAiB;IACvB,eAAe,EAAgB,KAAK;IACpC,OAAM;IACN,aACE,IACI,aAAa,EAAa,OAAO,KAAK,EAAa,OAAO,eAC1D,KAAA;IAEN,MAAK;IACL,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,eAAe,EAAgB,KAAK;KACpC,WAAU;eACX;KAEQ,CAAA,EACT,kBAAC,UAAD;KACE,SA/XS,YAAY;AAC3B,aAAC,KAAgB,CAAC,EAAa,MAAM,EACzC,KAAI;AAOF,OANA,MAAM,EAAc,EAClB,WAAW;QAAE,IAAI,EAAa;QAAI,QAAQ,EAAa,MAAM;QAAE,EAChE,CAAC,EACF,EAAS;QAAE,OAAO;QAAsB,SAAS;QAAW,CAAC,EAC7D,EAAgB,KAAK,EACrB,EAAgB,GAAG,EACnB,MAAM,GAAY;cACZ;AACN,SAAS;QAAE,OAAO;QAA8B,SAAS;QAAS,CAAC;;;KAqX3D,UAAU,KAAa,CAAC,EAAa,MAAM;KAC3C,WAAU;eAET,IAAY,iBAAiB;KACvB,CAAA,CACR,EAAA,CAAA;cAzBP,CA4BE,kBAAC,SAAD;KAAO,WAAU;eAA8C;KAEvD,CAAA,EACR,kBAAC,YAAD;KACE,MAAM;KACN,aAAY;KACZ,OAAO;KACP,WAAW,MAAM,EAAgB,EAAE,OAAO,MAAM;KAChD,WAAW;KACX,CAAA,CACI;;GAER,kBAAC,GAAD;IACE,MAAM,MAAmB;IACzB,eAAe,EAAkB,KAAK;IACtC,OAAM;IACN,aACE,IACI,OAAO,EAAe,OAAO,mBAAmB,EAAe,OAAO,eACtE,KAAA;IAEN,MAAK;IACL,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,eAAe,EAAkB,KAAK;KACtC,WAAU;eACX;KAEQ,CAAA,EACT,kBAAC,UAAD;KACE,SAxZkB,YAAY;AACpC,aAAC,KAAkB,CAAC,EAAc,MAAM,EAC5C,KAAI;AAOF,OANA,MAAM,EAAgB,EACpB,WAAW;QAAE,IAAI,EAAe;QAAI,OAAO,EAAc,MAAM;QAAE,EAClE,CAAC,EACF,EAAS;QAAE,OAAO;QAAsB,SAAS;QAAW,CAAC,EAC7D,EAAkB,KAAK,EACvB,EAAiB,GAAG,EACpB,MAAM,GAAY;cACZ;AACN,SAAS;QAAE,OAAO;QAA8B,SAAS;QAAS,CAAC;;;KA8Y3D,UAAU,KAAsB,CAAC,EAAc,MAAM;KACrD,WAAU;eAET,IAAqB,eAAe;KAC9B,CAAA,CACR,EAAA,CAAA;cAzBP,CA4BE,kBAAC,SAAD;KAAO,WAAU;eAA8C;KAEvD,CAAA,EACR,kBAAC,YAAD;KACE,MAAM;KACN,aAAY;KACZ,OAAO;KACP,WAAW,MAAM,EAAiB,EAAE,OAAO,MAAM;KACjD,WAAW;KACX,CAAA,CACI;;GAER,kBAAC,GAAD;IACE,MAAM,MAAmB;IACzB,eAAe,EAAkB,KAAK;IACtC,OAAM;IACN,aACE,IACI,GAAG,EAAe,OAAO,KAAK,EAAe,OAAO,oDACpD,KAAA;IAEN,MAAK;IACL,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,eAAe,EAAkB,KAAK;KACtC,WAAU;eACX;KAEQ,CAAA,EACT,kBAAC,UAAD;KACE,SAjbW,YAAY;AAC5B,YACL,KAAI;AAIF,OAHA,MAAM,GAAgB,EAAE,WAAW,EAAE,IAAI,EAAe,IAAI,EAAE,CAAC,EAC/D,EAAS;QAAE,OAAO;QAAqB,SAAS;QAAW,CAAC,EAC5D,EAAkB,KAAK,EACvB,MAAM,GAAY;cACZ;AACN,SAAS;QAAE,OAAO;QAA8B,SAAS;QAAS,CAAC;;;KA0a3D,WAAU;eACX;KAEQ,CAAA,CACR,EAAA,CAAA;cAGL,kBAAC,KAAD;KAAG,WAAU;eAA8B;KAGvC,CAAA;IACE,CAAA;GAER,kBAAC,IAAD;IAAwB;IAAQ,WAAW;IAAgB,CAAA;GAChD"}
|
|
@@ -64,7 +64,7 @@ var re = {
|
|
|
64
64
|
};
|
|
65
65
|
function w() {
|
|
66
66
|
let { workspaceId: w = "" } = e(), { data: ue, loading: T, error: E } = ee({
|
|
67
|
-
variables: { limit: 30 },
|
|
67
|
+
variables: { pagination: { limit: 30 } },
|
|
68
68
|
skip: !w
|
|
69
69
|
}), { data: D, loading: O, error: k } = r({ skip: !w }), A = T || O, j = E || k, M = D?.workPatternSummary, N = ue?.workPatterns ?? [], P = M?.insights ?? [], F = [...N].sort((e, t) => {
|
|
70
70
|
let n = new Date(e.date).getTime(), r = new Date(t.date).getTime();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkPatternsPage.js","names":[],"sources":["../../../../src/timecampus/pages/work-patterns/WorkPatternsPage.tsx"],"sourcesContent":["import {\n TrendingUp,\n Clock,\n Zap,\n Brain,\n AlertTriangle,\n ShieldCheck,\n Lightbulb,\n ArrowRight,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n Heatmap,\n TrendChart,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n type HeatmapValue,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListWorkPatternsQuery,\n useGetWorkPatternSummaryQuery,\n type WorkPatternFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport {\n WorkPatternInsightType,\n type WorkPatternInsight,\n} from \"@/generated/wspace-types\";\n\nconst DAY_LABELS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"] as const;\n\n/** JS `getDay()` is 0=Sun..6=Sat; remap so Monday is index 0. */\nfunction dayOfWeekIndex(d: Date): number {\n return (d.getDay() + 6) % 7;\n}\n\n/** Monday (local midnight) of the week containing `d`. */\nfunction weekStart(d: Date): Date {\n const s = new Date(d);\n s.setDate(d.getDate() - dayOfWeekIndex(d));\n s.setHours(0, 0, 0, 0);\n return s;\n}\n\nfunction shortDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\n// Self-reported ordinals mapped onto a shared 1 (worst) – 5 (best) scale so\n// energy and mood can be trended together. Only real, fetched enum values map;\n// anything else stays null (a gap in the line, never a fabricated point).\nconst ENERGY_SCORE: Record<string, number> = {\n VERY_LOW: 1,\n LOW: 2,\n MEDIUM: 3,\n HIGH: 4,\n VERY_HIGH: 5,\n};\nconst MOOD_SCORE: Record<string, number> = {\n VERY_NEGATIVE: 1,\n NEGATIVE: 2,\n NEUTRAL: 3,\n POSITIVE: 4,\n VERY_POSITIVE: 5,\n};\n\nconst HIGH_STRESS = new Set([\"HIGH\", \"VERY_HIGH\"]);\nconst LOW_ENERGY = new Set([\"LOW\", \"VERY_LOW\"]);\n\nconst formatHoursValue = (v: number) => `${v.toFixed(1)}h`;\n\ntype BurnoutTone = \"danger\" | \"warning\" | \"success\";\n\nconst INSIGHT_VARIANT: Record<\n WorkPatternInsightType,\n \"success\" | \"error\" | \"warning\" | \"info\" | \"muted\"\n> = {\n [WorkPatternInsightType.Positive]: \"success\",\n [WorkPatternInsightType.Negative]: \"error\",\n [WorkPatternInsightType.Warning]: \"warning\",\n [WorkPatternInsightType.Recommendation]: \"info\",\n [WorkPatternInsightType.Neutral]: \"muted\",\n};\n\nconst INSIGHT_LABEL: Record<WorkPatternInsightType, string> = {\n [WorkPatternInsightType.Positive]: \"Positive\",\n [WorkPatternInsightType.Negative]: \"Negative\",\n [WorkPatternInsightType.Warning]: \"Warning\",\n [WorkPatternInsightType.Recommendation]: \"Tip\",\n [WorkPatternInsightType.Neutral]: \"Neutral\",\n};\n\nexport function WorkPatternsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const {\n data: patternsData,\n loading: patternsLoading,\n error: patternsError,\n } = useListWorkPatternsQuery({\n variables: { limit: 30 },\n skip: !workspaceId,\n });\n\n const {\n data: summaryData,\n loading: summaryLoading,\n error: summaryError,\n } = useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const loading = patternsLoading || summaryLoading;\n const error = patternsError || summaryError;\n const summary = summaryData?.workPatternSummary;\n const patterns = patternsData?.workPatterns ?? [];\n const insights = summary?.insights ?? [];\n\n // Chronological order for time-based visuals; the list itself is newest-first.\n const chronological = [...patterns].sort((a, b) => {\n const ta = new Date(a.date).getTime();\n const tb = new Date(b.date).getTime();\n return (Number.isNaN(ta) ? 0 : ta) - (Number.isNaN(tb) ? 0 : tb);\n });\n\n // Derive stats from the latest pattern entry\n const latestPattern = patterns[0];\n const productivityScore =\n summary?.averageProductivity != null\n ? `${Math.round(summary.averageProductivity)}%`\n : \"--\";\n const focusHours =\n latestPattern?.focusHours != null\n ? `${latestPattern.focusHours.toFixed(1)}h`\n : \"--\";\n const totalHours = latestPattern\n ? (\n latestPattern.focusHours +\n latestPattern.meetingHours +\n latestPattern.breakHours\n ).toFixed(0)\n : \"8\";\n const deepWorkSessions =\n latestPattern?.deepWorkSessions != null\n ? String(latestPattern.deepWorkSessions)\n : \"--\";\n const mostProductiveDay = summary?.mostProductiveDay ?? \"--\";\n\n // ---- Heatmap: focus (productive) hours by weekday × week -----------------\n const weekKeys: number[] = [];\n for (const p of chronological) {\n const d = new Date(p.date);\n if (Number.isNaN(d.getTime())) continue;\n const key = weekStart(d).getTime();\n if (!weekKeys.includes(key)) weekKeys.push(key);\n }\n const selectedWeeks = weekKeys.slice(-6); // most recent 6 weeks\n const columnLabels = selectedWeeks.map((ts) =>\n shortDate(new Date(ts).toISOString()),\n );\n const heatmapData: HeatmapValue[][] = DAY_LABELS.map(() =>\n selectedWeeks.map<HeatmapValue>(() => null),\n );\n for (const p of chronological) {\n const d = new Date(p.date);\n if (Number.isNaN(d.getTime())) continue;\n const weekIdx = selectedWeeks.indexOf(weekStart(d).getTime());\n if (weekIdx < 0) continue;\n heatmapData[dayOfWeekIndex(d)][weekIdx] = p.focusHours;\n }\n\n // ---- Energy & mood trend --------------------------------------------------\n const trendData = chronological.map((p) => ({\n date: shortDate(p.date),\n energy:\n p.energyLevel != null ? (ENERGY_SCORE[p.energyLevel] ?? null) : null,\n mood: p.mood != null ? (MOOD_SCORE[p.mood] ?? null) : null,\n }));\n const hasEnergyMood = trendData.some(\n (d) => d.energy != null || d.mood != null,\n );\n\n // ---- Burnout signal (derived from real stress/energy + backend insights) --\n const recent = patterns.slice(0, 7); // most recent logged days\n const windowSize = recent.length;\n const highStressDays = recent.filter(\n (p: WorkPatternFieldsFragment) =>\n p.stressLevel != null && HIGH_STRESS.has(p.stressLevel),\n ).length;\n const lowEnergyDays = recent.filter(\n (p: WorkPatternFieldsFragment) =>\n p.energyLevel != null && LOW_ENERGY.has(p.energyLevel),\n ).length;\n const warningInsight = insights.find(\n (i: WorkPatternInsight) =>\n i.type === WorkPatternInsightType.Warning ||\n i.type === WorkPatternInsightType.Negative,\n );\n const half = Math.ceil(Math.max(windowSize, 1) / 2);\n\n let burnoutTone: BurnoutTone = \"success\";\n if (windowSize > 0 && highStressDays >= half) burnoutTone = \"danger\";\n else if (highStressDays > 0 || lowEnergyDays >= half || warningInsight)\n burnoutTone = \"warning\";\n if (warningInsight?.type === WorkPatternInsightType.Negative)\n burnoutTone = \"danger\";\n\n const stressEvidence: string[] = [];\n if (highStressDays > 0)\n stressEvidence.push(\n `${highStressDays} of your last ${windowSize} logged day${windowSize === 1 ? \"\" : \"s\"} showed high stress`,\n );\n if (lowEnergyDays > 0)\n stressEvidence.push(`${lowEnergyDays} showed low energy`);\n\n const burnoutHeading =\n warningInsight?.title ??\n (burnoutTone === \"danger\"\n ? \"Sustained overload detected\"\n : burnoutTone === \"warning\"\n ? \"Watch your workload\"\n : \"Balanced workload\");\n const burnoutBody =\n warningInsight?.description ??\n (burnoutTone === \"success\"\n ? \"Your recent stress and energy levels look healthy — keep protecting your focus blocks.\"\n : stressEvidence.length > 0\n ? `${stressEvidence.join(\" and \")}. Protect your deep-work time before it erodes.`\n : \"Recent signals suggest an uneven balance — schedule focus time to stay ahead of overload.\");\n const burnoutRecommendation = warningInsight?.recommendation ?? null;\n\n const burnoutAccent =\n burnoutTone === \"danger\"\n ? \"bg-status-error-bg text-status-error-text\"\n : burnoutTone === \"warning\"\n ? \"bg-status-warning-bg text-status-warning-text\"\n : \"bg-status-success-bg text-status-success-text\";\n\n if (loading) {\n return (\n <PageLayout\n title=\"Work Patterns\"\n description=\"Analyze your work habits and productivity patterns\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Work Patterns\"\n description=\"Analyze your work habits and productivity patterns\"\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 work patterns. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Work Patterns\"\n description=\"Analyze your work habits and productivity patterns\"\n purpose=\"Learn how you actually work across a typical week — when you focus best, where meetings pile up, and which day is your peak — so you can schedule deep work for the hours you're sharpest.\"\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=\"Productivity Score\"\n value={productivityScore}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n trend={\n summary?.averageProductivity != null\n ? { value: 5, label: \"vs last week\" }\n : undefined\n }\n />\n <StatCard\n title=\"Focus Hours\"\n value={focusHours}\n icon={<Brain className=\"h-5 w-5\" />}\n description={`Out of ${totalHours}h total`}\n />\n <StatCard\n title=\"Peak Day\"\n value={mostProductiveDay}\n icon={<Zap className=\"h-5 w-5\" />}\n description=\"Most productive day\"\n />\n <StatCard\n title=\"Deep Work\"\n value={`${deepWorkSessions} sessions`}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"90+ min uninterrupted\"\n />\n </div>\n\n {/* Burnout signal — honest, derived from recent stress/energy + insights */}\n <GlassCard\n treatment=\"glass\"\n glow={burnoutTone !== \"success\"}\n className=\"p-5 sm:p-6\"\n >\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start\">\n <div\n className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-xl ${burnoutAccent}`}\n >\n {burnoutTone === \"success\" ? (\n <ShieldCheck className=\"h-5 w-5\" />\n ) : (\n <AlertTriangle className=\"h-5 w-5\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n Burnout Signal\n </h3>\n <StatusBadge\n variant={\n burnoutTone === \"danger\"\n ? \"error\"\n : burnoutTone === \"warning\"\n ? \"warning\"\n : \"success\"\n }\n label={burnoutHeading}\n />\n </div>\n <p className=\"mt-1.5 text-sm text-text-secondary\">\n {burnoutBody}\n </p>\n {burnoutRecommendation && (\n <p className=\"mt-2 flex items-start gap-1.5 text-sm text-text-primary\">\n <Lightbulb className=\"mt-0.5 h-4 w-4 shrink-0 text-primary\" />\n <span>{burnoutRecommendation}</span>\n </p>\n )}\n </div>\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Weekly Productive Hours\"\n description=\"Focused hours logged by weekday across recent weeks\"\n >\n <Heatmap\n data={heatmapData}\n rowLabels={[...DAY_LABELS]}\n columnLabels={columnLabels}\n rowsLabel=\"Day\"\n valueFormat={formatHoursValue}\n legendLabels={[\"Fewer\", \"More\"]}\n height={240}\n emptyLabel=\"No work-pattern days logged yet.\"\n ariaLabel=\"Focused hours by weekday and week\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Energy & Mood Trends\"\n description=\"Self-reported energy and mood, each on a 1 (low) – 5 (high) scale\"\n >\n {hasEnergyMood ? (\n <TrendChart\n data={trendData}\n xKey=\"date\"\n series={[\n { key: \"energy\", label: \"Energy\" },\n { key: \"mood\", label: \"Mood\" },\n ]}\n type=\"area\"\n height={240}\n emptyLabel=\"No energy or mood check-ins yet.\"\n ariaLabel=\"Energy and mood over time\"\n />\n ) : (\n <EmptyState\n title=\"No energy or mood data yet\"\n description=\"Log how you feel on your work-pattern entries to see the trend here.\"\n />\n )}\n </ChartCard>\n </div>\n\n {insights.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Lightbulb className=\"h-4 w-4 text-primary\" />\n Insights\n </h3>\n <div className=\"space-y-3\">\n {insights.map((insight: WorkPatternInsight, idx: number) => (\n <div\n key={`${insight.type}-${idx}`}\n className=\"rounded-lg border border-border-subtle bg-bg-surface/40 p-4\"\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge\n variant={INSIGHT_VARIANT[insight.type]}\n label={INSIGHT_LABEL[insight.type]}\n />\n <p className=\"text-sm font-medium text-text-primary\">\n {insight.title}\n </p>\n </div>\n <p className=\"mt-1.5 text-sm text-text-secondary\">\n {insight.description}\n </p>\n {insight.recommendation && (\n <p className=\"mt-2 flex items-start gap-1.5 text-xs text-text-primary\">\n <ArrowRight className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-primary\" />\n <span>{insight.recommendation}</span>\n </p>\n )}\n </div>\n ))}\n </div>\n </GlassCard>\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WorkPatternsPage.js","names":[],"sources":["../../../../src/timecampus/pages/work-patterns/WorkPatternsPage.tsx"],"sourcesContent":["import {\n TrendingUp,\n Clock,\n Zap,\n Brain,\n AlertTriangle,\n ShieldCheck,\n Lightbulb,\n ArrowRight,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n Heatmap,\n TrendChart,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n type HeatmapValue,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListWorkPatternsQuery,\n useGetWorkPatternSummaryQuery,\n type WorkPatternFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport {\n WorkPatternInsightType,\n type WorkPatternInsight,\n} from \"@/generated/wspace-types\";\n\nconst DAY_LABELS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"] as const;\n\n/** JS `getDay()` is 0=Sun..6=Sat; remap so Monday is index 0. */\nfunction dayOfWeekIndex(d: Date): number {\n return (d.getDay() + 6) % 7;\n}\n\n/** Monday (local midnight) of the week containing `d`. */\nfunction weekStart(d: Date): Date {\n const s = new Date(d);\n s.setDate(d.getDate() - dayOfWeekIndex(d));\n s.setHours(0, 0, 0, 0);\n return s;\n}\n\nfunction shortDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\n// Self-reported ordinals mapped onto a shared 1 (worst) – 5 (best) scale so\n// energy and mood can be trended together. Only real, fetched enum values map;\n// anything else stays null (a gap in the line, never a fabricated point).\nconst ENERGY_SCORE: Record<string, number> = {\n VERY_LOW: 1,\n LOW: 2,\n MEDIUM: 3,\n HIGH: 4,\n VERY_HIGH: 5,\n};\nconst MOOD_SCORE: Record<string, number> = {\n VERY_NEGATIVE: 1,\n NEGATIVE: 2,\n NEUTRAL: 3,\n POSITIVE: 4,\n VERY_POSITIVE: 5,\n};\n\nconst HIGH_STRESS = new Set([\"HIGH\", \"VERY_HIGH\"]);\nconst LOW_ENERGY = new Set([\"LOW\", \"VERY_LOW\"]);\n\nconst formatHoursValue = (v: number) => `${v.toFixed(1)}h`;\n\ntype BurnoutTone = \"danger\" | \"warning\" | \"success\";\n\nconst INSIGHT_VARIANT: Record<\n WorkPatternInsightType,\n \"success\" | \"error\" | \"warning\" | \"info\" | \"muted\"\n> = {\n [WorkPatternInsightType.Positive]: \"success\",\n [WorkPatternInsightType.Negative]: \"error\",\n [WorkPatternInsightType.Warning]: \"warning\",\n [WorkPatternInsightType.Recommendation]: \"info\",\n [WorkPatternInsightType.Neutral]: \"muted\",\n};\n\nconst INSIGHT_LABEL: Record<WorkPatternInsightType, string> = {\n [WorkPatternInsightType.Positive]: \"Positive\",\n [WorkPatternInsightType.Negative]: \"Negative\",\n [WorkPatternInsightType.Warning]: \"Warning\",\n [WorkPatternInsightType.Recommendation]: \"Tip\",\n [WorkPatternInsightType.Neutral]: \"Neutral\",\n};\n\nexport function WorkPatternsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const {\n data: patternsData,\n loading: patternsLoading,\n error: patternsError,\n } = useListWorkPatternsQuery({\n variables: { pagination: { limit: 30 } },\n skip: !workspaceId,\n });\n\n const {\n data: summaryData,\n loading: summaryLoading,\n error: summaryError,\n } = useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const loading = patternsLoading || summaryLoading;\n const error = patternsError || summaryError;\n const summary = summaryData?.workPatternSummary;\n const patterns = patternsData?.workPatterns ?? [];\n const insights = summary?.insights ?? [];\n\n // Chronological order for time-based visuals; the list itself is newest-first.\n const chronological = [...patterns].sort((a, b) => {\n const ta = new Date(a.date).getTime();\n const tb = new Date(b.date).getTime();\n return (Number.isNaN(ta) ? 0 : ta) - (Number.isNaN(tb) ? 0 : tb);\n });\n\n // Derive stats from the latest pattern entry\n const latestPattern = patterns[0];\n const productivityScore =\n summary?.averageProductivity != null\n ? `${Math.round(summary.averageProductivity)}%`\n : \"--\";\n const focusHours =\n latestPattern?.focusHours != null\n ? `${latestPattern.focusHours.toFixed(1)}h`\n : \"--\";\n const totalHours = latestPattern\n ? (\n latestPattern.focusHours +\n latestPattern.meetingHours +\n latestPattern.breakHours\n ).toFixed(0)\n : \"8\";\n const deepWorkSessions =\n latestPattern?.deepWorkSessions != null\n ? String(latestPattern.deepWorkSessions)\n : \"--\";\n const mostProductiveDay = summary?.mostProductiveDay ?? \"--\";\n\n // ---- Heatmap: focus (productive) hours by weekday × week -----------------\n const weekKeys: number[] = [];\n for (const p of chronological) {\n const d = new Date(p.date);\n if (Number.isNaN(d.getTime())) continue;\n const key = weekStart(d).getTime();\n if (!weekKeys.includes(key)) weekKeys.push(key);\n }\n const selectedWeeks = weekKeys.slice(-6); // most recent 6 weeks\n const columnLabels = selectedWeeks.map((ts) =>\n shortDate(new Date(ts).toISOString()),\n );\n const heatmapData: HeatmapValue[][] = DAY_LABELS.map(() =>\n selectedWeeks.map<HeatmapValue>(() => null),\n );\n for (const p of chronological) {\n const d = new Date(p.date);\n if (Number.isNaN(d.getTime())) continue;\n const weekIdx = selectedWeeks.indexOf(weekStart(d).getTime());\n if (weekIdx < 0) continue;\n heatmapData[dayOfWeekIndex(d)][weekIdx] = p.focusHours;\n }\n\n // ---- Energy & mood trend --------------------------------------------------\n const trendData = chronological.map((p) => ({\n date: shortDate(p.date),\n energy:\n p.energyLevel != null ? (ENERGY_SCORE[p.energyLevel] ?? null) : null,\n mood: p.mood != null ? (MOOD_SCORE[p.mood] ?? null) : null,\n }));\n const hasEnergyMood = trendData.some(\n (d) => d.energy != null || d.mood != null,\n );\n\n // ---- Burnout signal (derived from real stress/energy + backend insights) --\n const recent = patterns.slice(0, 7); // most recent logged days\n const windowSize = recent.length;\n const highStressDays = recent.filter(\n (p: WorkPatternFieldsFragment) =>\n p.stressLevel != null && HIGH_STRESS.has(p.stressLevel),\n ).length;\n const lowEnergyDays = recent.filter(\n (p: WorkPatternFieldsFragment) =>\n p.energyLevel != null && LOW_ENERGY.has(p.energyLevel),\n ).length;\n const warningInsight = insights.find(\n (i: WorkPatternInsight) =>\n i.type === WorkPatternInsightType.Warning ||\n i.type === WorkPatternInsightType.Negative,\n );\n const half = Math.ceil(Math.max(windowSize, 1) / 2);\n\n let burnoutTone: BurnoutTone = \"success\";\n if (windowSize > 0 && highStressDays >= half) burnoutTone = \"danger\";\n else if (highStressDays > 0 || lowEnergyDays >= half || warningInsight)\n burnoutTone = \"warning\";\n if (warningInsight?.type === WorkPatternInsightType.Negative)\n burnoutTone = \"danger\";\n\n const stressEvidence: string[] = [];\n if (highStressDays > 0)\n stressEvidence.push(\n `${highStressDays} of your last ${windowSize} logged day${windowSize === 1 ? \"\" : \"s\"} showed high stress`,\n );\n if (lowEnergyDays > 0)\n stressEvidence.push(`${lowEnergyDays} showed low energy`);\n\n const burnoutHeading =\n warningInsight?.title ??\n (burnoutTone === \"danger\"\n ? \"Sustained overload detected\"\n : burnoutTone === \"warning\"\n ? \"Watch your workload\"\n : \"Balanced workload\");\n const burnoutBody =\n warningInsight?.description ??\n (burnoutTone === \"success\"\n ? \"Your recent stress and energy levels look healthy — keep protecting your focus blocks.\"\n : stressEvidence.length > 0\n ? `${stressEvidence.join(\" and \")}. Protect your deep-work time before it erodes.`\n : \"Recent signals suggest an uneven balance — schedule focus time to stay ahead of overload.\");\n const burnoutRecommendation = warningInsight?.recommendation ?? null;\n\n const burnoutAccent =\n burnoutTone === \"danger\"\n ? \"bg-status-error-bg text-status-error-text\"\n : burnoutTone === \"warning\"\n ? \"bg-status-warning-bg text-status-warning-text\"\n : \"bg-status-success-bg text-status-success-text\";\n\n if (loading) {\n return (\n <PageLayout\n title=\"Work Patterns\"\n description=\"Analyze your work habits and productivity patterns\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Work Patterns\"\n description=\"Analyze your work habits and productivity patterns\"\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 work patterns. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Work Patterns\"\n description=\"Analyze your work habits and productivity patterns\"\n purpose=\"Learn how you actually work across a typical week — when you focus best, where meetings pile up, and which day is your peak — so you can schedule deep work for the hours you're sharpest.\"\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=\"Productivity Score\"\n value={productivityScore}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n trend={\n summary?.averageProductivity != null\n ? { value: 5, label: \"vs last week\" }\n : undefined\n }\n />\n <StatCard\n title=\"Focus Hours\"\n value={focusHours}\n icon={<Brain className=\"h-5 w-5\" />}\n description={`Out of ${totalHours}h total`}\n />\n <StatCard\n title=\"Peak Day\"\n value={mostProductiveDay}\n icon={<Zap className=\"h-5 w-5\" />}\n description=\"Most productive day\"\n />\n <StatCard\n title=\"Deep Work\"\n value={`${deepWorkSessions} sessions`}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"90+ min uninterrupted\"\n />\n </div>\n\n {/* Burnout signal — honest, derived from recent stress/energy + insights */}\n <GlassCard\n treatment=\"glass\"\n glow={burnoutTone !== \"success\"}\n className=\"p-5 sm:p-6\"\n >\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start\">\n <div\n className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-xl ${burnoutAccent}`}\n >\n {burnoutTone === \"success\" ? (\n <ShieldCheck className=\"h-5 w-5\" />\n ) : (\n <AlertTriangle className=\"h-5 w-5\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n Burnout Signal\n </h3>\n <StatusBadge\n variant={\n burnoutTone === \"danger\"\n ? \"error\"\n : burnoutTone === \"warning\"\n ? \"warning\"\n : \"success\"\n }\n label={burnoutHeading}\n />\n </div>\n <p className=\"mt-1.5 text-sm text-text-secondary\">\n {burnoutBody}\n </p>\n {burnoutRecommendation && (\n <p className=\"mt-2 flex items-start gap-1.5 text-sm text-text-primary\">\n <Lightbulb className=\"mt-0.5 h-4 w-4 shrink-0 text-primary\" />\n <span>{burnoutRecommendation}</span>\n </p>\n )}\n </div>\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Weekly Productive Hours\"\n description=\"Focused hours logged by weekday across recent weeks\"\n >\n <Heatmap\n data={heatmapData}\n rowLabels={[...DAY_LABELS]}\n columnLabels={columnLabels}\n rowsLabel=\"Day\"\n valueFormat={formatHoursValue}\n legendLabels={[\"Fewer\", \"More\"]}\n height={240}\n emptyLabel=\"No work-pattern days logged yet.\"\n ariaLabel=\"Focused hours by weekday and week\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Energy & Mood Trends\"\n description=\"Self-reported energy and mood, each on a 1 (low) – 5 (high) scale\"\n >\n {hasEnergyMood ? (\n <TrendChart\n data={trendData}\n xKey=\"date\"\n series={[\n { key: \"energy\", label: \"Energy\" },\n { key: \"mood\", label: \"Mood\" },\n ]}\n type=\"area\"\n height={240}\n emptyLabel=\"No energy or mood check-ins yet.\"\n ariaLabel=\"Energy and mood over time\"\n />\n ) : (\n <EmptyState\n title=\"No energy or mood data yet\"\n description=\"Log how you feel on your work-pattern entries to see the trend here.\"\n />\n )}\n </ChartCard>\n </div>\n\n {insights.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Lightbulb className=\"h-4 w-4 text-primary\" />\n Insights\n </h3>\n <div className=\"space-y-3\">\n {insights.map((insight: WorkPatternInsight, idx: number) => (\n <div\n key={`${insight.type}-${idx}`}\n className=\"rounded-lg border border-border-subtle bg-bg-surface/40 p-4\"\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge\n variant={INSIGHT_VARIANT[insight.type]}\n label={INSIGHT_LABEL[insight.type]}\n />\n <p className=\"text-sm font-medium text-text-primary\">\n {insight.title}\n </p>\n </div>\n <p className=\"mt-1.5 text-sm text-text-secondary\">\n {insight.description}\n </p>\n {insight.recommendation && (\n <p className=\"mt-2 flex items-start gap-1.5 text-xs text-text-primary\">\n <ArrowRight className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-primary\" />\n <span>{insight.recommendation}</span>\n </p>\n )}\n </div>\n ))}\n </div>\n </GlassCard>\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED