@burdenoff/microfe-timecampus 2026.719.2 → 2026.719.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-operations.d.ts +193 -76
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-operations.js +859 -788
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.d.ts +56 -38
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/index.js +5 -5
- package/dist/timecampus/pages/HomePage.js +1 -1
- package/dist/timecampus/pages/HomePage.js.map +1 -1
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +6 -3
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js +4 -4
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +8 -3
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +19 -13
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.js +4 -4
- package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +4 -4
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js +4 -2
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.js +6 -4
- package/dist/timecampus/pages/meetings/MeetingsPage.js.map +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectsPage.js +2 -2
- package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/TasksPage.js +4 -2
- package/dist/timecampus/pages/projects/TasksPage.js.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.d.ts.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +4 -4
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamAnalyticsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/team/TeamAnalyticsPage.js +2 -2
- package/dist/timecampus/pages/team/TeamAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.d.ts.map +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js +49 -45
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.js +137 -132
- package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.js +2 -2
- package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.js +2 -2
- package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +4 -4
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
- package/dist/timecampus/pages/timeline/TimelinePage.d.ts.map +1 -1
- package/dist/timecampus/pages/timeline/TimelinePage.js +224 -162
- package/dist/timecampus/pages/timeline/TimelinePage.js.map +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
- package/package.json +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"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":";;;;;;;;;;;;;;;;;;;;;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;GAAE,OAAO;GAAW,QAAQ;GAAG;EAC1C,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;;;AA8FnB,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,SAtPK,YAAY;AACjC,UAAe,GAAK;AACpB,YAAI;AACF,eAAM,GAAU;UACd,WAAW;WAAE,OAAO;WAAW,QAAQ,EAAS;WAAQ;UACxD,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,SA7XS,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;;;KAmX3D,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,SAtZkB,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;;;KA4Y3D,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,SA/aW,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;;;KAwa3D,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"}
|
|
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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimelinePage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TimelinePage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"names":[],"mappings":"AAqOA,wBAAgB,YAAY,gCA+M3B"}
|
|
@@ -6,11 +6,12 @@ import { useGetTimeCampusTimelineQuery as a, useRecategorizeTimeCampusCaptureEve
|
|
|
6
6
|
import { PageLayout as s } from "../../components/PageLayout.js";
|
|
7
7
|
import { EmptyState as c } from "../../components/EmptyState.js";
|
|
8
8
|
import { useMemo as l, useState as u } from "react";
|
|
9
|
-
import { Activity as d, Brain as f, Calendar as p, Coffee as m, Github as h,
|
|
10
|
-
import { jsx as
|
|
11
|
-
import { GlassCard as
|
|
9
|
+
import { Activity as d, Brain as f, Calendar as p, Coffee as m, Github as h, GripVertical as g, MessageSquare as _, Users as v, Zap as y } from "lucide-react";
|
|
10
|
+
import { jsx as b, jsxs as x } from "react/jsx-runtime";
|
|
11
|
+
import { GlassCard as S } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
import { DndContext as C, DragOverlay as w, KeyboardSensor as T, PointerSensor as E, useDraggable as D, useDroppable as O, useSensor as k, useSensors as A } from "@dnd-kit/core";
|
|
12
13
|
//#region src/timecampus/pages/timeline/TimelinePage.tsx
|
|
13
|
-
var
|
|
14
|
+
var j = {
|
|
14
15
|
DEEP_WORK: {
|
|
15
16
|
label: "Deep work",
|
|
16
17
|
icon: f,
|
|
@@ -19,13 +20,13 @@ var S = {
|
|
|
19
20
|
},
|
|
20
21
|
MEETING: {
|
|
21
22
|
label: "Meeting",
|
|
22
|
-
icon:
|
|
23
|
+
icon: v,
|
|
23
24
|
dot: "bg-status-info-bg",
|
|
24
25
|
chip: "bg-status-info-bg text-status-info-text"
|
|
25
26
|
},
|
|
26
27
|
COMMUNICATION: {
|
|
27
28
|
label: "Communication",
|
|
28
|
-
icon:
|
|
29
|
+
icon: _,
|
|
29
30
|
dot: "bg-status-warning-bg",
|
|
30
31
|
chip: "bg-status-warning-bg text-status-warning-text"
|
|
31
32
|
},
|
|
@@ -41,210 +42,271 @@ var S = {
|
|
|
41
42
|
dot: "bg-bg-sunken",
|
|
42
43
|
chip: "bg-bg-sunken text-text-secondary"
|
|
43
44
|
}
|
|
44
|
-
},
|
|
45
|
-
TIME_ENTRY:
|
|
46
|
-
MEETING:
|
|
45
|
+
}, M = {
|
|
46
|
+
TIME_ENTRY: y,
|
|
47
|
+
MEETING: v,
|
|
47
48
|
POMODORO: f,
|
|
48
|
-
EMAIL:
|
|
49
|
+
EMAIL: _,
|
|
49
50
|
CAPTURE: h
|
|
50
|
-
},
|
|
51
|
+
}, N = [
|
|
51
52
|
"DEEP_WORK",
|
|
52
53
|
"MEETING",
|
|
53
54
|
"COMMUNICATION",
|
|
54
55
|
"BREAK",
|
|
55
56
|
"OTHER"
|
|
56
57
|
];
|
|
57
|
-
function
|
|
58
|
+
function P({ category: e, label: n, dotClassName: r, totalSeconds: i }) {
|
|
59
|
+
let { isOver: a, setNodeRef: o } = O({ id: `category:${e}` });
|
|
60
|
+
return /* @__PURE__ */ x("div", {
|
|
61
|
+
ref: o,
|
|
62
|
+
className: `flex items-center gap-2 rounded-lg border px-3 py-2 transition-colors ${a ? "border-action-primary-bg bg-action-primary-bg/10" : "border-border-seam bg-bg-surface"}`,
|
|
63
|
+
children: [
|
|
64
|
+
/* @__PURE__ */ b("span", {
|
|
65
|
+
className: `inline-block h-2.5 w-2.5 rounded-full ${r}`,
|
|
66
|
+
"aria-hidden": !0
|
|
67
|
+
}),
|
|
68
|
+
/* @__PURE__ */ b("span", {
|
|
69
|
+
className: "text-sm font-medium text-text-primary",
|
|
70
|
+
children: n
|
|
71
|
+
}),
|
|
72
|
+
/* @__PURE__ */ b("span", {
|
|
73
|
+
className: "text-sm tabular-nums text-text-secondary",
|
|
74
|
+
children: t(i)
|
|
75
|
+
}),
|
|
76
|
+
a && /* @__PURE__ */ b("span", {
|
|
77
|
+
className: "text-xs font-medium text-primary",
|
|
78
|
+
children: "Drop here"
|
|
79
|
+
})
|
|
80
|
+
]
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function F({ segment: e, category: r, recategorizing: i, onRecategorize: a }) {
|
|
84
|
+
let o = j[r] ?? j.OTHER, s = M[e.source] ?? d, { attributes: c, listeners: l, setNodeRef: u, isDragging: f } = D({
|
|
85
|
+
id: e.id,
|
|
86
|
+
data: { segment: e },
|
|
87
|
+
disabled: !e.isRecategorizable
|
|
88
|
+
});
|
|
89
|
+
return /* @__PURE__ */ x("li", {
|
|
90
|
+
ref: u,
|
|
91
|
+
className: `flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50 ${f ? "opacity-40" : ""}`,
|
|
92
|
+
children: [
|
|
93
|
+
/* @__PURE__ */ x("div", {
|
|
94
|
+
className: "flex w-16 shrink-0 flex-col items-end pt-0.5 text-xs tabular-nums text-text-secondary",
|
|
95
|
+
children: [/* @__PURE__ */ b("span", { children: n(e.startTime) }), /* @__PURE__ */ b("span", {
|
|
96
|
+
className: "text-text-tertiary",
|
|
97
|
+
children: t(e.durationSeconds)
|
|
98
|
+
})]
|
|
99
|
+
}),
|
|
100
|
+
e.isRecategorizable && /* @__PURE__ */ b("button", {
|
|
101
|
+
type: "button",
|
|
102
|
+
"aria-label": `Drag to recategorize ${e.title || o.label}`,
|
|
103
|
+
className: "mt-1.5 shrink-0 cursor-grab touch-none text-text-tertiary hover:text-text-secondary active:cursor-grabbing",
|
|
104
|
+
...c,
|
|
105
|
+
...l,
|
|
106
|
+
children: /* @__PURE__ */ b(g, { className: "h-4 w-4" })
|
|
107
|
+
}),
|
|
108
|
+
/* @__PURE__ */ b("div", {
|
|
109
|
+
className: `mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${o.chip}`,
|
|
110
|
+
children: /* @__PURE__ */ b(s, { className: "h-4 w-4" })
|
|
111
|
+
}),
|
|
112
|
+
/* @__PURE__ */ x("div", {
|
|
113
|
+
className: "min-w-0 flex-1",
|
|
114
|
+
children: [/* @__PURE__ */ b("p", {
|
|
115
|
+
className: "truncate text-sm font-medium text-text-primary",
|
|
116
|
+
children: e.title || o.label
|
|
117
|
+
}), /* @__PURE__ */ x("div", {
|
|
118
|
+
className: "mt-1 flex flex-wrap items-center gap-2",
|
|
119
|
+
children: [
|
|
120
|
+
/* @__PURE__ */ b("span", {
|
|
121
|
+
className: `inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${o.chip}`,
|
|
122
|
+
children: o.label
|
|
123
|
+
}),
|
|
124
|
+
e.app && /* @__PURE__ */ b("span", {
|
|
125
|
+
className: "text-xs text-text-secondary",
|
|
126
|
+
children: e.app
|
|
127
|
+
}),
|
|
128
|
+
e.isRecategorizable && /* @__PURE__ */ b("span", {
|
|
129
|
+
className: "text-xs text-text-tertiary",
|
|
130
|
+
children: "via capture"
|
|
131
|
+
})
|
|
132
|
+
]
|
|
133
|
+
})]
|
|
134
|
+
}),
|
|
135
|
+
e.isRecategorizable && /* @__PURE__ */ b("select", {
|
|
136
|
+
"aria-label": "Recategorize activity",
|
|
137
|
+
disabled: i,
|
|
138
|
+
value: r,
|
|
139
|
+
onChange: (t) => a(e, t.target.value),
|
|
140
|
+
className: "shrink-0 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40",
|
|
141
|
+
children: N.map((e) => /* @__PURE__ */ b("option", {
|
|
142
|
+
value: e,
|
|
143
|
+
children: j[e].label
|
|
144
|
+
}, e))
|
|
145
|
+
})
|
|
146
|
+
]
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function I(e) {
|
|
58
150
|
let t = e.getTimezoneOffset();
|
|
59
151
|
return (/* @__PURE__ */ new Date(e.getTime() - t * 60 * 1e3)).toISOString().slice(0, 10);
|
|
60
152
|
}
|
|
61
|
-
function
|
|
153
|
+
function L(e) {
|
|
62
154
|
let t = /* @__PURE__ */ new Date(`${e}T00:00:00`), n = /* @__PURE__ */ new Date(`${e}T23:59:59.999`);
|
|
63
155
|
return {
|
|
64
156
|
from: t.toISOString(),
|
|
65
157
|
to: n.toISOString()
|
|
66
158
|
};
|
|
67
159
|
}
|
|
68
|
-
function
|
|
69
|
-
let { workspaceId:
|
|
160
|
+
function R() {
|
|
161
|
+
let { workspaceId: n = "" } = e(), [m, h] = u(() => I(/* @__PURE__ */ new Date())), [_, v] = u(null), { from: D, to: O } = l(() => L(m), [m]), { data: M, loading: R, error: z, refetch: B } = a({
|
|
70
162
|
variables: {
|
|
71
|
-
from:
|
|
72
|
-
to:
|
|
163
|
+
from: D,
|
|
164
|
+
to: O
|
|
73
165
|
},
|
|
74
|
-
skip: !
|
|
166
|
+
skip: !n,
|
|
75
167
|
fetchPolicy: "cache-and-network"
|
|
76
|
-
}), [
|
|
168
|
+
}), [V, { loading: H }] = o(), U = A(k(E, { activationConstraint: { distance: 6 } }), k(T)), W = M?.timeCampusTimeline, G = W?.segments ?? [], K = W?.summary, q = l(() => {
|
|
77
169
|
let e = /* @__PURE__ */ new Map();
|
|
78
|
-
for (let t of
|
|
170
|
+
for (let t of K?.byCategory ?? []) e.set(t.category, {
|
|
79
171
|
totalSeconds: t.totalSeconds,
|
|
80
172
|
segmentCount: t.segmentCount
|
|
81
173
|
});
|
|
82
174
|
return e;
|
|
83
|
-
}, [
|
|
84
|
-
!e.isRecategorizable || e.category === t || (await
|
|
175
|
+
}, [K]), J = async (e, t) => {
|
|
176
|
+
!e.isRecategorizable || e.category === t || (await V({ variables: {
|
|
85
177
|
id: e.sourceId,
|
|
86
178
|
category: t
|
|
87
|
-
} }), await
|
|
88
|
-
},
|
|
89
|
-
|
|
179
|
+
} }), await B());
|
|
180
|
+
}, Y = (e) => {
|
|
181
|
+
let t = e.active.data.current?.segment;
|
|
182
|
+
v(t ?? null);
|
|
183
|
+
}, X = (e) => {
|
|
184
|
+
let t = e.active.data.current?.segment;
|
|
185
|
+
v(null);
|
|
186
|
+
let n = e.over?.id;
|
|
187
|
+
if (!t || typeof n != "string") return;
|
|
188
|
+
let r = n.replace(/^category:/, "");
|
|
189
|
+
N.includes(r) && J(t, r);
|
|
190
|
+
}, Z = K && K.focusRatio != null ? Math.round(K.focusRatio * 100) : 0;
|
|
191
|
+
return /* @__PURE__ */ b(s, {
|
|
90
192
|
title: "Timeline",
|
|
91
193
|
description: "Your day, assembled automatically",
|
|
92
194
|
purpose: "See where your time actually went — meetings, deep work, communication and breaks stitched into one honest, chronological view from everything TimeCampus captures, so you can spot the patterns worth changing.",
|
|
93
|
-
actions: /* @__PURE__ */
|
|
195
|
+
actions: /* @__PURE__ */ x("label", {
|
|
94
196
|
className: "flex items-center gap-2 text-sm text-text-secondary",
|
|
95
|
-
children: [/* @__PURE__ */
|
|
197
|
+
children: [/* @__PURE__ */ b(p, { className: "h-4 w-4" }), /* @__PURE__ */ b("input", {
|
|
96
198
|
type: "date",
|
|
97
|
-
value:
|
|
98
|
-
max:
|
|
99
|
-
onChange: (e) =>
|
|
199
|
+
value: m,
|
|
200
|
+
max: I(/* @__PURE__ */ new Date()),
|
|
201
|
+
onChange: (e) => h(e.target.value),
|
|
100
202
|
className: "rounded-md border border-border-seam bg-bg-surface px-3 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40"
|
|
101
203
|
})]
|
|
102
204
|
}),
|
|
103
|
-
children:
|
|
205
|
+
children: R && !W ? /* @__PURE__ */ x("div", {
|
|
104
206
|
className: "space-y-6",
|
|
105
|
-
children: [/* @__PURE__ */
|
|
207
|
+
children: [/* @__PURE__ */ b(i, {
|
|
106
208
|
variant: "card",
|
|
107
209
|
count: 4
|
|
108
|
-
}), /* @__PURE__ */
|
|
210
|
+
}), /* @__PURE__ */ b(i, {
|
|
109
211
|
variant: "list",
|
|
110
212
|
count: 6
|
|
111
213
|
})]
|
|
112
|
-
}) :
|
|
214
|
+
}) : z ? /* @__PURE__ */ b("div", {
|
|
113
215
|
className: "rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text",
|
|
114
216
|
children: "Failed to load your timeline. Please try again later."
|
|
115
|
-
}) : /* @__PURE__ */
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
className: "flex w-16 shrink-0 flex-col items-end pt-0.5 text-xs tabular-nums text-text-secondary",
|
|
195
|
-
children: [/* @__PURE__ */ y("span", { children: n(e.startTime) }), /* @__PURE__ */ y("span", {
|
|
196
|
-
className: "text-text-tertiary",
|
|
197
|
-
children: t(e.durationSeconds)
|
|
198
|
-
})]
|
|
199
|
-
}),
|
|
200
|
-
/* @__PURE__ */ y("div", {
|
|
201
|
-
className: `mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${i.chip}`,
|
|
202
|
-
children: /* @__PURE__ */ y(a, { className: "h-4 w-4" })
|
|
203
|
-
}),
|
|
204
|
-
/* @__PURE__ */ b("div", {
|
|
205
|
-
className: "min-w-0 flex-1",
|
|
206
|
-
children: [/* @__PURE__ */ y("p", {
|
|
207
|
-
className: "truncate text-sm font-medium text-text-primary",
|
|
208
|
-
children: e.title || i.label
|
|
209
|
-
}), /* @__PURE__ */ b("div", {
|
|
210
|
-
className: "mt-1 flex flex-wrap items-center gap-2",
|
|
211
|
-
children: [
|
|
212
|
-
/* @__PURE__ */ y("span", {
|
|
213
|
-
className: `inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${i.chip}`,
|
|
214
|
-
children: i.label
|
|
215
|
-
}),
|
|
216
|
-
e.app && /* @__PURE__ */ y("span", {
|
|
217
|
-
className: "text-xs text-text-secondary",
|
|
218
|
-
children: e.app
|
|
219
|
-
}),
|
|
220
|
-
e.isRecategorizable && /* @__PURE__ */ y("span", {
|
|
221
|
-
className: "text-xs text-text-tertiary",
|
|
222
|
-
children: "via capture"
|
|
223
|
-
})
|
|
224
|
-
]
|
|
225
|
-
})]
|
|
226
|
-
}),
|
|
227
|
-
e.isRecategorizable && /* @__PURE__ */ y("select", {
|
|
228
|
-
"aria-label": "Recategorize activity",
|
|
229
|
-
disabled: N,
|
|
230
|
-
value: r,
|
|
231
|
-
onChange: (t) => R(e, t.target.value),
|
|
232
|
-
className: "shrink-0 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40",
|
|
233
|
-
children: w.map((e) => /* @__PURE__ */ y("option", {
|
|
234
|
-
value: e,
|
|
235
|
-
children: S[e].label
|
|
236
|
-
}, e))
|
|
237
|
-
})
|
|
238
|
-
]
|
|
239
|
-
}, e.id);
|
|
217
|
+
}) : /* @__PURE__ */ x(C, {
|
|
218
|
+
sensors: U,
|
|
219
|
+
onDragStart: Y,
|
|
220
|
+
onDragEnd: X,
|
|
221
|
+
children: [/* @__PURE__ */ x("div", {
|
|
222
|
+
className: "space-y-6",
|
|
223
|
+
children: [
|
|
224
|
+
/* @__PURE__ */ x("div", {
|
|
225
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4",
|
|
226
|
+
children: [
|
|
227
|
+
/* @__PURE__ */ b(r, {
|
|
228
|
+
featured: !0,
|
|
229
|
+
title: "Tracked time",
|
|
230
|
+
value: t(K?.totalTrackedSeconds ?? 0),
|
|
231
|
+
icon: /* @__PURE__ */ b(d, { className: "h-5 w-5" }),
|
|
232
|
+
description: "Total activity across the day"
|
|
233
|
+
}),
|
|
234
|
+
/* @__PURE__ */ b(r, {
|
|
235
|
+
title: "Focus ratio",
|
|
236
|
+
value: `${Z}%`,
|
|
237
|
+
icon: /* @__PURE__ */ b(f, { className: "h-5 w-5" }),
|
|
238
|
+
description: "Share of time in deep work"
|
|
239
|
+
}),
|
|
240
|
+
/* @__PURE__ */ b(r, {
|
|
241
|
+
title: "Context switches",
|
|
242
|
+
value: K?.contextSwitches ?? 0,
|
|
243
|
+
icon: /* @__PURE__ */ b(y, { className: "h-5 w-5" }),
|
|
244
|
+
description: "Category changes through the day"
|
|
245
|
+
}),
|
|
246
|
+
/* @__PURE__ */ b(r, {
|
|
247
|
+
title: "Longest focus block",
|
|
248
|
+
value: t(K?.longestFocusBlockSeconds ?? 0),
|
|
249
|
+
icon: /* @__PURE__ */ b(f, { className: "h-5 w-5" }),
|
|
250
|
+
description: "Your deepest uninterrupted stretch"
|
|
251
|
+
})
|
|
252
|
+
]
|
|
253
|
+
}),
|
|
254
|
+
G.length > 0 && /* @__PURE__ */ x(S, {
|
|
255
|
+
treatment: "glass",
|
|
256
|
+
className: "p-4 sm:p-6",
|
|
257
|
+
children: [
|
|
258
|
+
/* @__PURE__ */ b("h2", {
|
|
259
|
+
className: "mb-1 text-sm font-semibold text-text-primary",
|
|
260
|
+
children: "Where the time went"
|
|
261
|
+
}),
|
|
262
|
+
/* @__PURE__ */ b("p", {
|
|
263
|
+
className: "mb-4 text-xs text-text-secondary",
|
|
264
|
+
children: "Drag a segment from the timeline below onto a category to recategorize it — or use its dropdown."
|
|
265
|
+
}),
|
|
266
|
+
/* @__PURE__ */ b("div", {
|
|
267
|
+
className: "flex flex-wrap gap-4",
|
|
268
|
+
children: N.filter((e) => q.has(e)).map((e) => {
|
|
269
|
+
let t = j[e], n = q.get(e);
|
|
270
|
+
return /* @__PURE__ */ b(P, {
|
|
271
|
+
category: e,
|
|
272
|
+
label: t.label,
|
|
273
|
+
dotClassName: t.dot,
|
|
274
|
+
totalSeconds: n.totalSeconds
|
|
275
|
+
}, e);
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
]
|
|
279
|
+
}),
|
|
280
|
+
G.length === 0 ? /* @__PURE__ */ b(c, {
|
|
281
|
+
illustration: "empty-generic",
|
|
282
|
+
title: "No activity captured for this day",
|
|
283
|
+
description: "As you track time, hold meetings, run focus sessions, and connect shared integrations, your day assembles here automatically — categorized and in order."
|
|
284
|
+
}) : /* @__PURE__ */ b(S, {
|
|
285
|
+
treatment: "glass",
|
|
286
|
+
glow: !0,
|
|
287
|
+
className: "overflow-hidden",
|
|
288
|
+
children: /* @__PURE__ */ b("ol", {
|
|
289
|
+
className: "divide-y divide-border-seam",
|
|
290
|
+
children: G.map((e) => /* @__PURE__ */ b(F, {
|
|
291
|
+
segment: e,
|
|
292
|
+
category: e.category ?? "OTHER",
|
|
293
|
+
recategorizing: H,
|
|
294
|
+
onRecategorize: J
|
|
295
|
+
}, e.id))
|
|
240
296
|
})
|
|
241
297
|
})
|
|
242
|
-
|
|
243
|
-
|
|
298
|
+
]
|
|
299
|
+
}), /* @__PURE__ */ b(w, { children: _ && /* @__PURE__ */ x("div", {
|
|
300
|
+
className: "flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 shadow-pop",
|
|
301
|
+
children: [/* @__PURE__ */ b(g, { className: "h-4 w-4 text-text-tertiary" }), /* @__PURE__ */ b("span", {
|
|
302
|
+
className: "text-sm font-medium text-text-primary",
|
|
303
|
+
children: _.title || j[_.category ?? "OTHER"].label
|
|
304
|
+
})]
|
|
305
|
+
}) })]
|
|
244
306
|
})
|
|
245
307
|
});
|
|
246
308
|
}
|
|
247
309
|
//#endregion
|
|
248
|
-
export {
|
|
310
|
+
export { R as TimelinePage };
|
|
249
311
|
|
|
250
312
|
//# sourceMappingURL=TimelinePage.js.map
|