@burdenoff/microfe-timecampus 2026.719.3 → 2026.728.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-operations.d.ts +680 -0
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-operations.js +1262 -949
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.d.ts +210 -0
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/generated/wspace-types.js +5 -3
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/index.js +5 -5
- package/dist/timecampus/TimeCampusRoot.d.ts.map +1 -1
- package/dist/timecampus/TimeCampusRoot.js +81 -73
- package/dist/timecampus/TimeCampusRoot.js.map +1 -1
- package/dist/timecampus/TimeCampusRoutes.d.ts.map +1 -1
- package/dist/timecampus/TimeCampusRoutes.js +17 -13
- package/dist/timecampus/TimeCampusRoutes.js.map +1 -1
- package/dist/timecampus/components/StatusBadge.d.ts.map +1 -1
- package/dist/timecampus/components/StatusBadge.js.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +11 -9
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/invoicing/InvoicingPage.d.ts +2 -0
- package/dist/timecampus/pages/invoicing/InvoicingPage.d.ts.map +1 -0
- package/dist/timecampus/pages/invoicing/InvoicingPage.js +595 -0
- package/dist/timecampus/pages/invoicing/InvoicingPage.js.map +1 -0
- 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-activity/TeamActivityPage.d.ts.map +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js +48 -44
- package/dist/timecampus/pages/team-activity/TeamActivityPage.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/types/index.d.ts.map +1 -1
- package/dist/timecampus/utils/helpers.d.ts.map +1 -1
- package/dist/timecampus/utils/helpers.js +3 -1
- package/dist/timecampus/utils/helpers.js.map +1 -1
- package/dist/types/assistant.d.ts.map +1 -1
- package/dist/utils/pageContext.d.ts.map +1 -1
- package/dist/utils/pageContext.js +1 -0
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InvoicingPage.js","names":[],"sources":["../../../../src/timecampus/pages/invoicing/InvoicingPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n FileText,\n Plus,\n DollarSign,\n Send,\n CheckCircle2,\n Trash2,\n Eye,\n Download,\n AlertTriangle,\n} from \"lucide-react\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\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 { formatCurrency, formatDate } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListInvoicesQuery,\n useGetInvoiceStatsQuery,\n useGenerateInvoiceDraftMutation,\n useUpdateInvoiceStatusMutation,\n useDeleteInvoiceDraftMutation,\n type InvoiceFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { TimeCampusInvoiceStatus } from \"@/generated/wspace-types\";\n\nconst PAGE_SIZE = 25;\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\n// Fetched on-demand only (view / download), never on page load — same\n// apollo.query() pattern as the expense-tracking receipt-preview link, so we\n// don't need a generated lazy-query hook for a rarely-used, heavy payload.\nconst GET_INVOICE_HTML = gql`\n query TimeCampusInvoicing_GetInvoiceHtml($id: ID!, $workspaceId: ID!) {\n timeCampusInvoiceHtml(id: $id, workspaceId: $workspaceId)\n }\n`;\n\nconst GET_INVOICE_PDF = gql`\n query TimeCampusInvoicing_GetInvoicePdf($id: ID!, $workspaceId: ID!) {\n timeCampusInvoicePdf(id: $id, workspaceId: $workspaceId)\n }\n`;\n\nfunction todayIso(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction firstOfMonthIso(): string {\n const d = new Date();\n return new Date(d.getFullYear(), d.getMonth(), 1).toISOString().slice(0, 10);\n}\n\ninterface GenerateFormState {\n clientName: string;\n clientEmail: string;\n clientAddress: string;\n periodStart: string;\n periodEnd: string;\n taxRatePercent: string;\n currency: string;\n dueDate: string;\n notes: string;\n projectIds: string;\n}\n\nconst emptyForm: GenerateFormState = {\n clientName: \"\",\n clientEmail: \"\",\n clientAddress: \"\",\n periodStart: firstOfMonthIso(),\n periodEnd: todayIso(),\n taxRatePercent: \"0\",\n currency: \"USD\",\n dueDate: \"\",\n notes: \"\",\n projectIds: \"\",\n};\n\n/** Downloads a base64-encoded PDF payload as a file in the browser. */\nfunction downloadBase64Pdf(base64: string, filename: string): void {\n const bytes = atob(base64);\n const buffer = new Uint8Array(bytes.length);\n for (let i = 0; i < bytes.length; i++) buffer[i] = bytes.charCodeAt(i);\n const blob = new Blob([buffer], { type: \"application/pdf\" });\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n link.remove();\n URL.revokeObjectURL(url);\n}\n\nexport function InvoicingPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const { toasts, addToast, dismissToast } = useToast();\n const apollo = useApolloClient();\n\n const {\n data: invoicesData,\n loading: invoicesLoading,\n error: invoicesError,\n refetch,\n fetchMore,\n } = useListInvoicesQuery({\n variables: { workspaceId, pagination: { limit: PAGE_SIZE, offset: 0 } },\n notifyOnNetworkStatusChange: true,\n });\n const {\n data: statsData,\n loading: statsLoading,\n refetch: refetchStats,\n } = useGetInvoiceStatsQuery({ variables: { workspaceId } });\n\n const [generateDraft, { loading: generating }] =\n useGenerateInvoiceDraftMutation();\n const [updateStatus] = useUpdateInvoiceStatusMutation();\n const [deleteDraft, { loading: deleting }] = useDeleteInvoiceDraftMutation();\n\n const invoices = invoicesData?.timeCampusInvoices?.items ?? [];\n const total = invoicesData?.timeCampusInvoices?.total ?? 0;\n const stats = statsData?.timeCampusInvoiceStats;\n\n const [loadingMore, setLoadingMore] = useState(false);\n const [canLoadMore, setCanLoadMore] = useState(true);\n\n // Generate modal\n const [formOpen, setFormOpen] = useState(false);\n const [form, setForm] = useState<GenerateFormState>(emptyForm);\n\n // View/preview modal\n const [viewTarget, setViewTarget] = useState<InvoiceFieldsFragment | null>(\n null,\n );\n const [viewHtml, setViewHtml] = useState<string | null>(null);\n const [viewLoading, setViewLoading] = useState(false);\n const [downloadingId, setDownloadingId] = useState<string | null>(null);\n\n // Delete confirm\n const [deleteTarget, setDeleteTarget] =\n useState<InvoiceFieldsFragment | null>(null);\n\n // Per-row busy state for status transitions\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const openGenerate = () => {\n setForm({\n ...emptyForm,\n periodStart: firstOfMonthIso(),\n periodEnd: todayIso(),\n });\n setFormOpen(true);\n };\n const closeGenerate = () => setFormOpen(false);\n\n const handleGenerate = async () => {\n if (!form.clientName.trim() || !form.periodStart || !form.periodEnd) return;\n const taxRate = Number(form.taxRatePercent || \"0\") / 100;\n if (Number.isNaN(taxRate) || taxRate < 0 || taxRate > 1) {\n addToast({\n title: \"Tax rate must be between 0 and 100%\",\n variant: \"error\",\n });\n return;\n }\n try {\n await generateDraft({\n variables: {\n input: {\n workspaceId,\n clientName: form.clientName.trim(),\n clientEmail: form.clientEmail.trim() || undefined,\n clientAddress: form.clientAddress.trim() || undefined,\n periodStart: new Date(form.periodStart).toISOString(),\n periodEnd: new Date(\n new Date(form.periodEnd).getTime() + 86_400_000 - 1,\n ).toISOString(),\n taxRate,\n currency: form.currency.trim() || \"USD\",\n dueDate: form.dueDate\n ? new Date(form.dueDate).toISOString()\n : undefined,\n notes: form.notes.trim() || undefined,\n projectIds: form.projectIds.trim()\n ? form.projectIds\n .split(\",\")\n .map((p) => p.trim())\n .filter(Boolean)\n : undefined,\n },\n },\n });\n addToast({ title: \"Draft invoice generated\", variant: \"success\" });\n closeGenerate();\n await refreshAll();\n } catch (err) {\n addToast({\n title:\n err instanceof Error\n ? err.message\n : \"Failed to generate invoice — check the billing period has un-invoiced billable time\",\n variant: \"error\",\n });\n }\n };\n\n const handleMarkSent = async (invoice: InvoiceFieldsFragment) => {\n setBusyId(invoice.id);\n try {\n await updateStatus({\n variables: {\n input: {\n id: invoice.id,\n workspaceId,\n status: TimeCampusInvoiceStatus.Sent,\n },\n },\n });\n addToast({ title: \"Invoice marked as sent\", variant: \"success\" });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to update invoice status\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleMarkPaid = async (invoice: InvoiceFieldsFragment) => {\n setBusyId(invoice.id);\n try {\n await updateStatus({\n variables: {\n input: {\n id: invoice.id,\n workspaceId,\n status: TimeCampusInvoiceStatus.Paid,\n },\n },\n });\n addToast({ title: \"Invoice marked as paid\", variant: \"success\" });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to update invoice status\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteDraft({\n variables: { id: deleteTarget.id, workspaceId },\n });\n addToast({ title: \"Draft invoice deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to delete invoice\", variant: \"error\" });\n }\n };\n\n const openView = async (invoice: InvoiceFieldsFragment) => {\n setViewTarget(invoice);\n setViewHtml(null);\n setViewLoading(true);\n try {\n const { data } = await apollo.query({\n query: GET_INVOICE_HTML,\n variables: { id: invoice.id, workspaceId },\n fetchPolicy: \"no-cache\",\n });\n const html = (data as { timeCampusInvoiceHtml?: string } | null)\n ?.timeCampusInvoiceHtml;\n setViewHtml(html ?? null);\n } catch {\n addToast({ title: \"Failed to load invoice preview\", variant: \"error\" });\n } finally {\n setViewLoading(false);\n }\n };\n\n const handleDownloadPdf = async (invoice: InvoiceFieldsFragment) => {\n setDownloadingId(invoice.id);\n try {\n const { data } = await apollo.query({\n query: GET_INVOICE_PDF,\n variables: { id: invoice.id, workspaceId },\n fetchPolicy: \"no-cache\",\n });\n const base64 = (data as { timeCampusInvoicePdf?: string } | null)\n ?.timeCampusInvoicePdf;\n if (!base64) throw new Error(\"Empty PDF payload\");\n downloadBase64Pdf(base64, `${invoice.invoiceNumber}.pdf`);\n } catch {\n addToast({ title: \"Failed to download PDF\", variant: \"error\" });\n } finally {\n setDownloadingId(null);\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n const res = await fetchMore({\n variables: {\n workspaceId,\n pagination: { limit: PAGE_SIZE, offset: invoices.length },\n },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n timeCampusInvoices: {\n ...fetchMoreResult.timeCampusInvoices,\n items: [\n ...prev.timeCampusInvoices.items,\n ...fetchMoreResult.timeCampusInvoices.items,\n ],\n },\n };\n },\n });\n if ((res.data?.timeCampusInvoices?.items?.length ?? 0) < PAGE_SIZE) {\n setCanLoadMore(false);\n }\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (invoicesLoading && statsLoading && invoices.length === 0) {\n return (\n <PageLayout\n title=\"Invoicing\"\n description=\"Bill clients for billable time\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (invoicesError) {\n return (\n <PageLayout\n title=\"Invoicing\"\n description=\"Bill clients for billable time\"\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 invoices. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const generateButton = (\n <button\n onClick={openGenerate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Generate Invoice\n </button>\n );\n\n return (\n <PageLayout\n title=\"Invoicing\"\n description=\"Bill clients for billable time\"\n purpose=\"Turn logged, billable hours into a real invoice — pick a client and a billing period, and TimeCampus aggregates every un-invoiced billable time entry by project and task, applying each entry's own rate, into a draft you can review, send, and mark paid.\"\n actions={generateButton}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Invoiced\"\n value={formatCurrency(stats?.totalInvoiced ?? 0)}\n icon={<DollarSign className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Outstanding\"\n value={formatCurrency(stats?.totalOutstanding ?? 0)}\n description={`${stats?.sentCount ?? 0} sent, awaiting payment`}\n />\n <StatCard\n title=\"Paid\"\n value={formatCurrency(stats?.totalPaid ?? 0)}\n description={`${stats?.paidCount ?? 0} invoices paid`}\n />\n <StatCard\n title=\"Drafts\"\n value={String(stats?.draftCount ?? 0)}\n icon={<FileText className=\"h-5 w-5\" />}\n description=\"not yet sent\"\n />\n </div>\n\n {invoices.length === 0 && !invoicesLoading ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No invoices yet\"\n description=\"Generate a draft invoice from real billable time entries to bill a client.\"\n action={generateButton}\n />\n ) : (\n <>\n <div className=\"mt-4 rounded-xl border border-border-seam bg-gloss-card shadow-pop\">\n <div className=\"divide-y divide-border\">\n {invoices.map((invoice: InvoiceFieldsFragment) => {\n const rowBusy = busyId === invoice.id;\n const isDraft =\n invoice.status === TimeCampusInvoiceStatus.Draft;\n const isSent = invoice.status === TimeCampusInvoiceStatus.Sent;\n return (\n <div\n key={invoice.id}\n className=\"flex flex-col gap-3 p-4 hover:bg-bg-sunken/50 transition-colors sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"flex items-center gap-3\">\n <FileText className=\"h-5 w-5 shrink-0 text-text-secondary\" />\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n {invoice.invoiceNumber} · {invoice.clientName}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {formatDate(invoice.periodStart)} –{\" \"}\n {formatDate(invoice.periodEnd)}\n {invoice.dueDate && (\n <> · Due {formatDate(invoice.dueDate)}</>\n )}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {formatCurrency(invoice.total, invoice.currency)}\n </p>\n <StatusBadge\n label={invoice.status.toLowerCase()}\n status={invoice.status.toLowerCase()}\n />\n {invoice.isOverdue && (\n <span\n title=\"Past due date\"\n className=\"inline-flex items-center gap-1 text-xs font-medium text-status-error-text\"\n >\n <AlertTriangle className=\"h-3.5 w-3.5\" />\n Overdue\n </span>\n )}\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => void openView(invoice)}\n title=\"View invoice\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken\"\n >\n <Eye className=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n disabled={downloadingId === invoice.id}\n onClick={() => void handleDownloadPdf(invoice)}\n title=\"Download PDF\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Download className=\"h-4 w-4\" />\n </button>\n {isDraft && (\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => handleMarkSent(invoice)}\n title=\"Mark as sent\"\n className=\"rounded-md p-1.5 text-status-info-text hover:bg-status-info-bg disabled:opacity-50\"\n >\n <Send className=\"h-4 w-4\" />\n </button>\n )}\n {isSent && (\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => handleMarkPaid(invoice)}\n title=\"Mark as paid\"\n className=\"rounded-md p-1.5 text-status-success-text hover:bg-status-success-bg disabled:opacity-50\"\n >\n <CheckCircle2 className=\"h-4 w-4\" />\n </button>\n )}\n {isDraft && (\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(invoice)}\n title=\"Delete draft\"\n className=\"rounded-md p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n\n {canLoadMore && invoices.length < total && (\n <div className=\"mt-4 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n {/* Generate draft invoice */}\n <Modal\n open={formOpen}\n onClose={closeGenerate}\n title=\"Generate Invoice\"\n description=\"Aggregates every billable, un-invoiced time entry in the selected period into a draft invoice.\"\n footer={\n <>\n <button\n onClick={closeGenerate}\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={handleGenerate}\n disabled={\n generating ||\n !form.clientName.trim() ||\n !form.periodStart ||\n !form.periodEnd\n }\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 {generating ? \"Generating...\" : \"Generate Draft\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Client name\n </label>\n <input\n type=\"text\"\n placeholder=\"Acme Corp\"\n value={form.clientName}\n onChange={(e) => setForm({ ...form, clientName: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Client email\n </label>\n <input\n type=\"email\"\n placeholder=\"billing@acme.com\"\n value={form.clientEmail}\n onChange={(e) =>\n setForm({ ...form, clientEmail: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Currency\n </label>\n <input\n type=\"text\"\n placeholder=\"USD\"\n value={form.currency}\n onChange={(e) => setForm({ ...form, currency: e.target.value })}\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Client address\n </label>\n <input\n type=\"text\"\n placeholder=\"Optional billing address\"\n value={form.clientAddress}\n onChange={(e) =>\n setForm({ ...form, clientAddress: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Billing period start\n </label>\n <input\n type=\"date\"\n value={form.periodStart}\n onChange={(e) =>\n setForm({ ...form, periodStart: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Billing period end\n </label>\n <input\n type=\"date\"\n value={form.periodEnd}\n onChange={(e) =>\n setForm({ ...form, periodEnd: e.target.value })\n }\n className={inputClass}\n />\n </div>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Tax rate (%)\n </label>\n <input\n type=\"number\"\n min={0}\n max={100}\n step=\"0.1\"\n value={form.taxRatePercent}\n onChange={(e) =>\n setForm({ ...form, taxRatePercent: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Due date\n </label>\n <input\n type=\"date\"\n value={form.dueDate}\n onChange={(e) => setForm({ ...form, dueDate: e.target.value })}\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Project IDs\n </label>\n <input\n type=\"text\"\n placeholder=\"Optional, comma-separated. Leave blank for all projects.\"\n value={form.projectIds}\n onChange={(e) => setForm({ ...form, projectIds: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Notes\n </label>\n <textarea\n rows={2}\n placeholder=\"Optional notes printed on the invoice\"\n value={form.notes}\n onChange={(e) => setForm({ ...form, notes: e.target.value })}\n className={inputClass}\n />\n </div>\n </div>\n </Modal>\n\n {/* View invoice */}\n <Modal\n open={viewTarget !== null}\n onClose={() => setViewTarget(null)}\n title={viewTarget ? `Invoice ${viewTarget.invoiceNumber}` : \"Invoice\"}\n description={viewTarget?.clientName}\n size=\"xl\"\n footer={\n viewTarget && (\n <>\n <button\n onClick={() => setViewTarget(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 Close\n </button>\n <button\n onClick={() => void handleDownloadPdf(viewTarget)}\n disabled={downloadingId === viewTarget.id}\n className=\"inline-flex items-center gap-1.5 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 <Download className=\"h-4 w-4\" />\n {downloadingId === viewTarget.id\n ? \"Downloading...\"\n : \"Download PDF\"}\n </button>\n </>\n )\n }\n >\n {viewLoading ? (\n <LoadingSkeleton variant=\"detail\" count={1} />\n ) : viewHtml ? (\n <iframe\n title=\"Invoice preview\"\n srcDoc={viewHtml}\n sandbox=\"\"\n className=\"h-[60vh] w-full rounded-md border border-border-subtle bg-white\"\n />\n ) : (\n <p className=\"text-sm text-text-secondary\">\n Failed to load invoice preview.\n </p>\n )}\n </Modal>\n\n {/* Delete confirm */}\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete draft invoice?\"\n description={\n deleteTarget\n ? `${deleteTarget.invoiceNumber} · ${formatCurrency(deleteTarget.total, deleteTarget.currency)} will be removed and its time entries released back to the un-invoiced pool.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n This permanently removes the draft. This action cannot be undone.\n </p>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamAnalyticsPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/team/TeamAnalyticsPage.tsx"],"names":[],"mappings":"AAkCA,wBAAgB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"TeamAnalyticsPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/team/TeamAnalyticsPage.tsx"],"names":[],"mappings":"AAkCA,wBAAgB,iBAAiB,gCA0PhC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useTimeCampus as e, useTimeCampusNavigate as t } from "../../providers/TimeCampusProvider.js";
|
|
2
2
|
import { StatCard as n } from "../../components/StatCard.js";
|
|
3
3
|
import { LoadingSkeleton as r } from "../../components/LoadingSkeleton.js";
|
|
4
|
-
import {
|
|
4
|
+
import { useGetTeamWorkPatternSummaryQuery as i, useGetTimeEntryStatsQuery as a } from "../../../generated/wspace-operations.js";
|
|
5
5
|
import { PageLayout as o } from "../../components/PageLayout.js";
|
|
6
6
|
import { StatusBadge as s } from "../../components/StatusBadge.js";
|
|
7
7
|
import { ChartCard as c } from "../../components/ChartCard.js";
|
|
@@ -29,7 +29,7 @@ function S() {
|
|
|
29
29
|
className: "inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90",
|
|
30
30
|
children: [/* @__PURE__ */ _(g, { className: "h-4 w-4" }), "View Members"]
|
|
31
31
|
})]
|
|
32
|
-
}), { data: T, loading: E, error: D } =
|
|
32
|
+
}), { data: T, loading: E, error: D } = a({ skip: !S }), { data: O, loading: k } = i({ skip: !S }), A = T?.timeEntryStats, j = O?.teamWorkPatternSummary;
|
|
33
33
|
if (E || k) return /* @__PURE__ */ _(o, {
|
|
34
34
|
title: "Team Analytics",
|
|
35
35
|
description: "Team performance insights and metrics",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/team/TeamAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, Users, Clock, TrendingUp, Lightbulb } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n StatusBadge,\n LoadingSkeleton,\n BarsChart,\n DonutChart,\n RadialGauge,\n} from \"../../components\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useGetWorkPatternSummaryQuery,\n} from \"@/generated/wspace-operations\";\n\nconst formatHrs = (v: number): string =>\n `${Number.isInteger(v) ? v : v.toFixed(1)}h`;\n\nconst impactVariant = (\n impact?: string | null,\n): \"success\" | \"warning\" | \"info\" | \"muted\" => {\n const value = (impact ?? \"\").toLowerCase();\n if (value.includes(\"high\") || value.includes(\"critical\")) return \"warning\";\n if (value.includes(\"low\")) return \"muted\";\n if (value.includes(\"positive\") || value.includes(\"good\")) return \"success\";\n return \"info\";\n};\n\nexport function TeamAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n\n const analyticsActions = (\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => navigate(\"/team\")}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Team Overview\n </button>\n <button\n onClick={() => navigate(\"/team/members\")}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Users className=\"h-4 w-4\" />\n View Members\n </button>\n </div>\n );\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetTimeEntryStatsQuery({\n skip: !workspaceId,\n });\n\n const { data: patternData, loading: patternLoading } =\n useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const stats = statsData?.timeEntryStats;\n const pattern = patternData?.workPatternSummary;\n\n if (statsLoading || patternLoading) {\n return (\n <PageLayout\n title=\"Team Analytics\"\n description=\"Team performance insights and metrics\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Team Analytics\"\n description=\"Team performance insights and metrics\"\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 team analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalHours = stats?.totalHours ?? 0;\n const billableHours = stats?.billableHours ?? 0;\n const nonBillableHours = Math.max(0, totalHours - billableHours);\n const billablePercent =\n totalHours > 0 ? Math.round((billableHours / totalHours) * 100) : 0;\n\n const avgFocus = pattern?.averageFocusHours ?? 0;\n const avgMeeting = pattern?.averageMeetingHours ?? 0;\n const avgProductivity = pattern?.averageProductivity ?? 0;\n const insights = pattern?.insights ?? [];\n\n // Aggregate team composition — never individual — from the fetched stats.\n const distributionData = [\n {\n label: \"Logged\",\n billable: Math.round(billableHours * 10) / 10,\n nonBillable: Math.round(nonBillableHours * 10) / 10,\n },\n ];\n\n // Focus vs meeting balance from the work-pattern summary (avg per person).\n const balanceData = [\n { name: \"Deep Work / Focus\", value: Math.round(avgFocus * 10) / 10 },\n { name: \"Meetings\", value: Math.round(avgMeeting * 10) / 10 },\n ];\n\n return (\n <PageLayout\n title=\"Team Analytics\"\n description=\"Team performance insights and metrics\"\n purpose=\"Compare how your team works — hours by member, productive days, focus versus meeting time — so you can rebalance workload, recognise top contributors, and fix the patterns that slow everyone down.\"\n actions={analyticsActions}\n >\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Team Hours\"\n value={formatHrs(Math.round(totalHours))}\n icon={<Clock className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Billable Hours\"\n value={formatHrs(Math.round(billableHours))}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n trend={{ value: billablePercent, label: \"billable rate\" }}\n />\n <StatCard\n title=\"Avg Productivity\"\n value={`${Math.round(avgProductivity)}%`}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n <StatCard\n title=\"Meeting Load\"\n value={formatHrs(avgMeeting)}\n icon={<Users className=\"h-5 w-5\" />}\n description=\"Avg / person — the true cost of meetings\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Team Time Distribution\"\n description=\"All data aggregate, not individual.\"\n >\n <BarsChart\n data={distributionData}\n xKey=\"label\"\n orientation=\"horizontal\"\n stacked\n height={150}\n categoryWidth={72}\n valueFormat={formatHrs}\n series={[\n { key: \"billable\", label: \"Billable\" },\n { key: \"nonBillable\", label: \"Non-billable\" },\n ]}\n emptyLabel=\"No logged hours yet\"\n />\n <p className=\"mt-2 text-xs text-text-secondary\">\n {billablePercent}% of {formatHrs(Math.round(totalHours))} logged\n is billable.\n </p>\n </ChartCard>\n\n <ChartCard\n title=\"Focus vs Meeting Balance\"\n description=\"Average deep-work versus meeting time per person.\"\n >\n <DonutChart\n data={balanceData}\n centerValue={Math.round((avgFocus + avgMeeting) * 10) / 10}\n centerLabel=\"Avg / person\"\n valueFormat={formatHrs}\n height={220}\n emptyLabel=\"No work-pattern data yet\"\n />\n </ChartCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Team Productivity\"\n description=\"Aggregate productivity score across the team.\"\n >\n <RadialGauge\n value={Math.round(avgProductivity)}\n min={0}\n max={100}\n label=\"Productivity\"\n sublabel={\n pattern?.mostProductiveDay\n ? `Peak day: ${pattern.mostProductiveDay}`\n : undefined\n }\n format={(v) => `${Math.round(v)}%`}\n height={220}\n />\n <div className=\"mt-4 flex flex-wrap items-center justify-center gap-2\">\n {pattern?.mostProductiveDay && (\n <StatusBadge\n variant=\"success\"\n label={`Most productive: ${pattern.mostProductiveDay}`}\n />\n )}\n {pattern?.leastProductiveDay && (\n <StatusBadge\n variant=\"muted\"\n label={`Quietest: ${pattern.leastProductiveDay}`}\n />\n )}\n </div>\n </ChartCard>\n\n <GlassCard treatment=\"glass\" glow className=\"p-4 sm:p-6\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Lightbulb className=\"h-5 w-5 text-primary\" />\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Team Insights\n </h3>\n </div>\n {insights.length === 0 ? (\n <div className=\"flex h-40 items-center justify-center rounded-lg border border-border-subtle bg-bg-sunken/30 text-center\">\n <p className=\"max-w-xs text-sm text-text-secondary\">\n Insights appear here as your team logs more work patterns.\n </p>\n </div>\n ) : (\n <ul className=\"space-y-3\">\n {insights.map(\n (insight: (typeof insights)[number], index: number) => (\n <li\n key={`${insight.type}-${index}`}\n className=\"rounded-lg border border-border-subtle bg-bg-surface/40 p-3\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {insight.title}\n </p>\n {insight.impact && (\n <StatusBadge\n variant={impactVariant(insight.impact)}\n label={insight.impact}\n />\n )}\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {insight.description}\n </p>\n {insight.recommendation && (\n <p className=\"mt-2 text-xs text-primary\">\n {insight.recommendation}\n </p>\n )}\n </li>\n ),\n )}\n </ul>\n )}\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TeamAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/team/TeamAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, Users, Clock, TrendingUp, Lightbulb } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n StatusBadge,\n LoadingSkeleton,\n BarsChart,\n DonutChart,\n RadialGauge,\n} from \"../../components\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useGetTeamWorkPatternSummaryQuery,\n} from \"@/generated/wspace-operations\";\n\nconst formatHrs = (v: number): string =>\n `${Number.isInteger(v) ? v : v.toFixed(1)}h`;\n\nconst impactVariant = (\n impact?: string | null,\n): \"success\" | \"warning\" | \"info\" | \"muted\" => {\n const value = (impact ?? \"\").toLowerCase();\n if (value.includes(\"high\") || value.includes(\"critical\")) return \"warning\";\n if (value.includes(\"low\")) return \"muted\";\n if (value.includes(\"positive\") || value.includes(\"good\")) return \"success\";\n return \"info\";\n};\n\nexport function TeamAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n\n const analyticsActions = (\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => navigate(\"/team\")}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Team Overview\n </button>\n <button\n onClick={() => navigate(\"/team/members\")}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Users className=\"h-4 w-4\" />\n View Members\n </button>\n </div>\n );\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetTimeEntryStatsQuery({\n skip: !workspaceId,\n });\n\n // Team-wide (BOFF-4037): aggregates every active workspace member, never\n // just the calling actor — see workPatternSummary for the self-scoped\n // equivalent used by the personal dashboard.\n const { data: patternData, loading: patternLoading } =\n useGetTeamWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const stats = statsData?.timeEntryStats;\n const pattern = patternData?.teamWorkPatternSummary;\n\n if (statsLoading || patternLoading) {\n return (\n <PageLayout\n title=\"Team Analytics\"\n description=\"Team performance insights and metrics\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Team Analytics\"\n description=\"Team performance insights and metrics\"\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 team analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalHours = stats?.totalHours ?? 0;\n const billableHours = stats?.billableHours ?? 0;\n const nonBillableHours = Math.max(0, totalHours - billableHours);\n const billablePercent =\n totalHours > 0 ? Math.round((billableHours / totalHours) * 100) : 0;\n\n const avgFocus = pattern?.averageFocusHours ?? 0;\n const avgMeeting = pattern?.averageMeetingHours ?? 0;\n const avgProductivity = pattern?.averageProductivity ?? 0;\n const insights = pattern?.insights ?? [];\n\n // Aggregate team composition — never individual — from the fetched stats.\n const distributionData = [\n {\n label: \"Logged\",\n billable: Math.round(billableHours * 10) / 10,\n nonBillable: Math.round(nonBillableHours * 10) / 10,\n },\n ];\n\n // Focus vs meeting balance from the work-pattern summary (avg per person).\n const balanceData = [\n { name: \"Deep Work / Focus\", value: Math.round(avgFocus * 10) / 10 },\n { name: \"Meetings\", value: Math.round(avgMeeting * 10) / 10 },\n ];\n\n return (\n <PageLayout\n title=\"Team Analytics\"\n description=\"Team performance insights and metrics\"\n purpose=\"Compare how your team works — hours by member, productive days, focus versus meeting time — so you can rebalance workload, recognise top contributors, and fix the patterns that slow everyone down.\"\n actions={analyticsActions}\n >\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Team Hours\"\n value={formatHrs(Math.round(totalHours))}\n icon={<Clock className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Billable Hours\"\n value={formatHrs(Math.round(billableHours))}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n trend={{ value: billablePercent, label: \"billable rate\" }}\n />\n <StatCard\n title=\"Avg Productivity\"\n value={`${Math.round(avgProductivity)}%`}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n <StatCard\n title=\"Meeting Load\"\n value={formatHrs(avgMeeting)}\n icon={<Users className=\"h-5 w-5\" />}\n description=\"Avg / person — the true cost of meetings\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Team Time Distribution\"\n description=\"All data aggregate, not individual.\"\n >\n <BarsChart\n data={distributionData}\n xKey=\"label\"\n orientation=\"horizontal\"\n stacked\n height={150}\n categoryWidth={72}\n valueFormat={formatHrs}\n series={[\n { key: \"billable\", label: \"Billable\" },\n { key: \"nonBillable\", label: \"Non-billable\" },\n ]}\n emptyLabel=\"No logged hours yet\"\n />\n <p className=\"mt-2 text-xs text-text-secondary\">\n {billablePercent}% of {formatHrs(Math.round(totalHours))} logged\n is billable.\n </p>\n </ChartCard>\n\n <ChartCard\n title=\"Focus vs Meeting Balance\"\n description=\"Average deep-work versus meeting time per person.\"\n >\n <DonutChart\n data={balanceData}\n centerValue={Math.round((avgFocus + avgMeeting) * 10) / 10}\n centerLabel=\"Avg / person\"\n valueFormat={formatHrs}\n height={220}\n emptyLabel=\"No work-pattern data yet\"\n />\n </ChartCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Team Productivity\"\n description=\"Aggregate productivity score across the team.\"\n >\n <RadialGauge\n value={Math.round(avgProductivity)}\n min={0}\n max={100}\n label=\"Productivity\"\n sublabel={\n pattern?.mostProductiveDay\n ? `Peak day: ${pattern.mostProductiveDay}`\n : undefined\n }\n format={(v) => `${Math.round(v)}%`}\n height={220}\n />\n <div className=\"mt-4 flex flex-wrap items-center justify-center gap-2\">\n {pattern?.mostProductiveDay && (\n <StatusBadge\n variant=\"success\"\n label={`Most productive: ${pattern.mostProductiveDay}`}\n />\n )}\n {pattern?.leastProductiveDay && (\n <StatusBadge\n variant=\"muted\"\n label={`Quietest: ${pattern.leastProductiveDay}`}\n />\n )}\n </div>\n </ChartCard>\n\n <GlassCard treatment=\"glass\" glow className=\"p-4 sm:p-6\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Lightbulb className=\"h-5 w-5 text-primary\" />\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Team Insights\n </h3>\n </div>\n {insights.length === 0 ? (\n <div className=\"flex h-40 items-center justify-center rounded-lg border border-border-subtle bg-bg-sunken/30 text-center\">\n <p className=\"max-w-xs text-sm text-text-secondary\">\n Insights appear here as your team logs more work patterns.\n </p>\n </div>\n ) : (\n <ul className=\"space-y-3\">\n {insights.map(\n (insight: (typeof insights)[number], index: number) => (\n <li\n key={`${insight.type}-${index}`}\n className=\"rounded-lg border border-border-subtle bg-bg-surface/40 p-3\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {insight.title}\n </p>\n {insight.impact && (\n <StatusBadge\n variant={impactVariant(insight.impact)}\n label={insight.impact}\n />\n )}\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {insight.description}\n </p>\n {insight.recommendation && (\n <p className=\"mt-2 text-xs text-primary\">\n {insight.recommendation}\n </p>\n )}\n </li>\n ),\n )}\n </ul>\n )}\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamActivityPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/team-activity/TeamActivityPage.tsx"],"names":[],"mappings":"AAmCA,wBAAgB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"TeamActivityPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/team-activity/TeamActivityPage.tsx"],"names":[],"mappings":"AAmCA,wBAAgB,gBAAgB,gCAwP/B"}
|
|
@@ -2,7 +2,7 @@ import { useTimeCampus as e } from "../../providers/TimeCampusProvider.js";
|
|
|
2
2
|
import { formatRelativeTime as t } from "../../utils/helpers.js";
|
|
3
3
|
import { StatCard as n } from "../../components/StatCard.js";
|
|
4
4
|
import { LoadingSkeleton as r } from "../../components/LoadingSkeleton.js";
|
|
5
|
-
import { useGetTimeEntryStatsQuery as i,
|
|
5
|
+
import { useGetTimeEntryStatsQuery as i, useListTeamWorkPatternsQuery as a } from "../../../generated/wspace-operations.js";
|
|
6
6
|
import { PageLayout as o } from "../../components/PageLayout.js";
|
|
7
7
|
import { StatusBadge as s } from "../../components/StatusBadge.js";
|
|
8
8
|
import { EmptyState as c } from "../../components/EmptyState.js";
|
|
@@ -13,16 +13,16 @@ import "../../components/index.js";
|
|
|
13
13
|
import { Activity as f, CheckCircle as p, Clock as m, Users as h, Zap as g } from "lucide-react";
|
|
14
14
|
import { jsx as _, jsxs as v } from "react/jsx-runtime";
|
|
15
15
|
//#region src/timecampus/pages/team-activity/TeamActivityPage.tsx
|
|
16
|
-
var y = (e) => `${Number.isInteger(e) ? e : e.toFixed(1)}h`, b = (e) => e.slice(0, 2).toUpperCase(), x = (e) =>
|
|
16
|
+
var y = (e) => `${Number.isInteger(e) ? e : e.toFixed(1)}h`, b = (e) => e.slice(0, 2).toUpperCase(), x = (e) => new Date(e).toLocaleDateString(void 0, {
|
|
17
17
|
month: "short",
|
|
18
18
|
day: "numeric"
|
|
19
19
|
});
|
|
20
|
-
function
|
|
21
|
-
let { workspaceId:
|
|
20
|
+
function S() {
|
|
21
|
+
let { workspaceId: S = "" } = e(), { data: C, loading: w, error: T } = i({ skip: !S }), { data: E, loading: D, error: O } = a({
|
|
22
22
|
variables: { pagination: { limit: 10 } },
|
|
23
|
-
skip: !
|
|
24
|
-
}),
|
|
25
|
-
if (
|
|
23
|
+
skip: !S
|
|
24
|
+
}), k = C?.timeEntryStats, A = E?.teamWorkPatterns ?? [];
|
|
25
|
+
if (w && D) return /* @__PURE__ */ _(o, {
|
|
26
26
|
title: "Team Activity",
|
|
27
27
|
description: "Real-time team activity feed",
|
|
28
28
|
children: /* @__PURE__ */ _(r, {
|
|
@@ -30,7 +30,7 @@ function C() {
|
|
|
30
30
|
count: 5
|
|
31
31
|
})
|
|
32
32
|
});
|
|
33
|
-
if (
|
|
33
|
+
if (T && O) return /* @__PURE__ */ _(o, {
|
|
34
34
|
title: "Team Activity",
|
|
35
35
|
description: "Real-time team activity feed",
|
|
36
36
|
children: /* @__PURE__ */ _("div", {
|
|
@@ -38,8 +38,8 @@ function C() {
|
|
|
38
38
|
children: "Failed to load team activity data. Please try again later."
|
|
39
39
|
})
|
|
40
40
|
});
|
|
41
|
-
let
|
|
42
|
-
label:
|
|
41
|
+
let j = [...A].reverse().map(({ pattern: e }) => ({
|
|
42
|
+
label: x(e.date),
|
|
43
43
|
focus: Math.round(e.focusHours * 10) / 10,
|
|
44
44
|
meetings: Math.round(e.meetingHours * 10) / 10,
|
|
45
45
|
breaks: Math.round(e.breakHours * 10) / 10
|
|
@@ -57,34 +57,34 @@ function C() {
|
|
|
57
57
|
/* @__PURE__ */ _(n, {
|
|
58
58
|
featured: !0,
|
|
59
59
|
title: "Active Timers",
|
|
60
|
-
value: String(
|
|
60
|
+
value: String(k?.activeTimers ?? 0),
|
|
61
61
|
icon: /* @__PURE__ */ _(h, { className: "h-5 w-5" })
|
|
62
62
|
}),
|
|
63
63
|
/* @__PURE__ */ _(n, {
|
|
64
64
|
title: "Total Entries",
|
|
65
|
-
value: String(
|
|
65
|
+
value: String(k?.totalEntries ?? 0),
|
|
66
66
|
icon: /* @__PURE__ */ _(p, { className: "h-5 w-5" }),
|
|
67
67
|
description: "Today"
|
|
68
68
|
}),
|
|
69
69
|
/* @__PURE__ */ _(n, {
|
|
70
70
|
title: "Team Hours",
|
|
71
|
-
value: y(Math.round(
|
|
71
|
+
value: y(Math.round(k?.totalHours ?? 0)),
|
|
72
72
|
icon: /* @__PURE__ */ _(m, { className: "h-5 w-5" }),
|
|
73
73
|
description: "Total tracked"
|
|
74
74
|
}),
|
|
75
75
|
/* @__PURE__ */ _(n, {
|
|
76
76
|
title: "Billable Hours",
|
|
77
|
-
value: y(Math.round(
|
|
77
|
+
value: y(Math.round(k?.billableHours ?? 0)),
|
|
78
78
|
icon: /* @__PURE__ */ _(f, { className: "h-5 w-5" }),
|
|
79
79
|
iconBgColor: "bg-status-success-bg text-status-success-text"
|
|
80
80
|
})
|
|
81
81
|
]
|
|
82
82
|
}),
|
|
83
|
-
|
|
83
|
+
j.length > 0 && /* @__PURE__ */ _(l, {
|
|
84
84
|
title: "Recent Work Patterns",
|
|
85
85
|
description: "Focus, meeting and break hours across the latest logged patterns.",
|
|
86
86
|
children: /* @__PURE__ */ _(u, {
|
|
87
|
-
data:
|
|
87
|
+
data: j,
|
|
88
88
|
xKey: "label",
|
|
89
89
|
stacked: !0,
|
|
90
90
|
height: 240,
|
|
@@ -111,36 +111,40 @@ function C() {
|
|
|
111
111
|
children: [/* @__PURE__ */ _("h3", {
|
|
112
112
|
className: "mb-4 text-sm font-semibold text-text-primary",
|
|
113
113
|
children: "Activity Feed"
|
|
114
|
-
}),
|
|
114
|
+
}), A.length === 0 && !D ? /* @__PURE__ */ _(c, {
|
|
115
115
|
illustration: "empty-notifications",
|
|
116
116
|
title: "No recent activity yet",
|
|
117
117
|
description: "As your team tracks time and completes focus sessions, their activity will stream in here."
|
|
118
118
|
}) : /* @__PURE__ */ _("ul", {
|
|
119
119
|
className: "space-y-3",
|
|
120
|
-
children:
|
|
121
|
-
let
|
|
120
|
+
children: A.map(({ memberName: e, memberAvatar: n, pattern: r }, i) => {
|
|
121
|
+
let a = r.deepWorkSessions > 0, o = !!r.energyLevel || !!r.mood || !!r.stressLevel;
|
|
122
122
|
return /* @__PURE__ */ _("li", {
|
|
123
123
|
className: "rounded-lg border border-border-subtle bg-bg-surface/40 p-4",
|
|
124
124
|
children: /* @__PURE__ */ v("div", {
|
|
125
125
|
className: "flex items-start gap-3",
|
|
126
|
-
children: [/* @__PURE__ */ _("
|
|
126
|
+
children: [n ? /* @__PURE__ */ _("img", {
|
|
127
|
+
src: n,
|
|
128
|
+
alt: e,
|
|
129
|
+
className: "mt-0.5 h-9 w-9 shrink-0 rounded-full border border-border-seam object-cover"
|
|
130
|
+
}) : /* @__PURE__ */ _("div", {
|
|
127
131
|
className: "mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-xs font-semibold uppercase text-primary",
|
|
128
|
-
children: b(e
|
|
132
|
+
children: b(e)
|
|
129
133
|
}), /* @__PURE__ */ v("div", {
|
|
130
134
|
className: "min-w-0 flex-1",
|
|
131
135
|
children: [
|
|
132
136
|
/* @__PURE__ */ v("div", {
|
|
133
137
|
className: "flex flex-wrap items-center gap-x-2 gap-y-1",
|
|
134
138
|
children: [
|
|
135
|
-
/* @__PURE__ */
|
|
136
|
-
className: "
|
|
137
|
-
children:
|
|
139
|
+
/* @__PURE__ */ _("span", {
|
|
140
|
+
className: "text-sm font-medium text-text-primary",
|
|
141
|
+
children: e
|
|
138
142
|
}),
|
|
139
143
|
/* @__PURE__ */ _("span", {
|
|
140
144
|
className: "text-xs text-text-secondary",
|
|
141
|
-
children: t(
|
|
145
|
+
children: t(r.createdAt)
|
|
142
146
|
}),
|
|
143
|
-
|
|
147
|
+
a && /* @__PURE__ */ _(s, {
|
|
144
148
|
variant: "success",
|
|
145
149
|
label: "Deep work"
|
|
146
150
|
})
|
|
@@ -148,19 +152,19 @@ function C() {
|
|
|
148
152
|
}),
|
|
149
153
|
/* @__PURE__ */ v("p", {
|
|
150
154
|
className: "mt-1.5 flex items-center gap-1.5 text-sm font-medium text-text-primary",
|
|
151
|
-
children: [_(
|
|
155
|
+
children: [_(a ? g : f, { className: "h-3.5 w-3.5 text-primary" }), a ? "Completed deep work session" : "Logged work pattern"]
|
|
152
156
|
}),
|
|
153
157
|
/* @__PURE__ */ v("div", {
|
|
154
158
|
className: "mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-text-secondary",
|
|
155
159
|
children: [
|
|
156
|
-
/* @__PURE__ */ v("span", { children: [
|
|
157
|
-
/* @__PURE__ */ v("span", { children: [
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
/* @__PURE__ */ _("span", { children:
|
|
160
|
+
/* @__PURE__ */ v("span", { children: [r.focusHours.toFixed(1), "h focus"] }),
|
|
161
|
+
/* @__PURE__ */ v("span", { children: [r.meetingHours.toFixed(1), "h meetings"] }),
|
|
162
|
+
r.deepWorkSessions > 0 && /* @__PURE__ */ v("span", { children: [r.deepWorkSessions, " deep sessions"] }),
|
|
163
|
+
r.interruptions > 0 && /* @__PURE__ */ v("span", { children: [r.interruptions, " interruptions"] }),
|
|
164
|
+
/* @__PURE__ */ _("span", { children: x(r.date) })
|
|
161
165
|
]
|
|
162
166
|
}),
|
|
163
|
-
|
|
167
|
+
r.productivityScore != null && /* @__PURE__ */ v("div", {
|
|
164
168
|
className: "mt-3",
|
|
165
169
|
children: [/* @__PURE__ */ v("div", {
|
|
166
170
|
className: "mb-1 flex items-center justify-between text-xs",
|
|
@@ -169,10 +173,10 @@ function C() {
|
|
|
169
173
|
children: "Productivity"
|
|
170
174
|
}), /* @__PURE__ */ v("span", {
|
|
171
175
|
className: "tabular-nums text-text-primary",
|
|
172
|
-
children: [Math.round(
|
|
176
|
+
children: [Math.round(r.productivityScore), "%"]
|
|
173
177
|
})]
|
|
174
178
|
}), /* @__PURE__ */ _(d, {
|
|
175
|
-
value:
|
|
179
|
+
value: r.productivityScore,
|
|
176
180
|
max: 100,
|
|
177
181
|
higherIsWorse: !1,
|
|
178
182
|
thresholds: {
|
|
@@ -183,27 +187,27 @@ function C() {
|
|
|
183
187
|
size: "sm"
|
|
184
188
|
})]
|
|
185
189
|
}),
|
|
186
|
-
|
|
190
|
+
o && /* @__PURE__ */ v("div", {
|
|
187
191
|
className: "mt-3 flex flex-wrap gap-1.5",
|
|
188
192
|
children: [
|
|
189
|
-
|
|
193
|
+
r.energyLevel && /* @__PURE__ */ _(s, {
|
|
190
194
|
variant: "info",
|
|
191
|
-
label: `Energy: ${
|
|
195
|
+
label: `Energy: ${r.energyLevel}`
|
|
192
196
|
}),
|
|
193
|
-
|
|
197
|
+
r.mood && /* @__PURE__ */ _(s, {
|
|
194
198
|
variant: "muted",
|
|
195
|
-
label: `Mood: ${
|
|
199
|
+
label: `Mood: ${r.mood}`
|
|
196
200
|
}),
|
|
197
|
-
|
|
201
|
+
r.stressLevel && /* @__PURE__ */ _(s, {
|
|
198
202
|
variant: "warning",
|
|
199
|
-
label: `Stress: ${
|
|
203
|
+
label: `Stress: ${r.stressLevel}`
|
|
200
204
|
})
|
|
201
205
|
]
|
|
202
206
|
})
|
|
203
207
|
]
|
|
204
208
|
})]
|
|
205
209
|
})
|
|
206
|
-
},
|
|
210
|
+
}, `${r.id}-${i}`);
|
|
207
211
|
})
|
|
208
212
|
})]
|
|
209
213
|
})
|
|
@@ -212,6 +216,6 @@ function C() {
|
|
|
212
216
|
});
|
|
213
217
|
}
|
|
214
218
|
//#endregion
|
|
215
|
-
export {
|
|
219
|
+
export { S as TeamActivityPage };
|
|
216
220
|
|
|
217
221
|
//# sourceMappingURL=TeamActivityPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamActivityPage.js","names":[],"sources":["../../../../src/timecampus/pages/team-activity/TeamActivityPage.tsx"],"sourcesContent":["import { Activity, Users, Clock, CheckCircle, Zap } from \"lucide-react\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n BarsChart,\n ProgressMeter,\n} from \"../../components\";\nimport { formatRelativeTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useListWorkPatternsQuery,\n type WorkPatternFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst formatHrs = (v: number): string =>\n `${Number.isInteger(v) ? v : v.toFixed(1)}h`;\n\n// Member identity (name/avatar) is owned by the groups module and is NOT\n// fetched by this page — the work-pattern feed only carries a raw `userId`\n// UUID. Until a member-lookup field is available we render a stable monogram +\n// short-id chip instead of a bare UUID. See report FUNC-GAP note.\nconst monogram = (userId: string): string => userId.slice(0, 2).toUpperCase();\nconst shortId = (userId: string): string => userId.slice(0, 8);\n\nconst shortDate = (value: string): string =>\n new Date(value).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n });\n\nexport function TeamActivityPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetTimeEntryStatsQuery({ skip: !workspaceId });\n const {\n data: patternsData,\n loading: patternsLoading,\n error: patternsError,\n } = useListWorkPatternsQuery({\n variables: { pagination: { limit: 10 } },\n skip: !workspaceId,\n });\n\n const stats = statsData?.timeEntryStats;\n const patterns = patternsData?.workPatterns ?? [];\n\n if (statsLoading && patternsLoading) {\n return (\n <PageLayout\n title=\"Team Activity\"\n description=\"Real-time team activity feed\"\n >\n <LoadingSkeleton variant=\"list\" count={5} />\n </PageLayout>\n );\n }\n\n if (statsError && patternsError) {\n return (\n <PageLayout\n title=\"Team Activity\"\n description=\"Real-time team activity feed\"\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 team activity data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n // Recent work patterns as a real stacked composition — sourced only from the\n // rows already fetched above (oldest → newest for a left-to-right timeline).\n const patternBars = [...patterns].reverse().map((pattern) => ({\n label: shortDate(pattern.date),\n focus: Math.round(pattern.focusHours * 10) / 10,\n meetings: Math.round(pattern.meetingHours * 10) / 10,\n breaks: Math.round(pattern.breakHours * 10) / 10,\n }));\n\n return (\n <PageLayout\n title=\"Team Activity\"\n description=\"Real-time team activity feed\"\n purpose=\"A live pulse of what your team is doing right now — who's tracking, recent deep-work sessions, and today's hours — so you stay connected to the team's progress without interrupting anyone.\"\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=\"Active Timers\"\n value={String(stats?.activeTimers ?? 0)}\n icon={<Users className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Entries\"\n value={String(stats?.totalEntries ?? 0)}\n icon={<CheckCircle className=\"h-5 w-5\" />}\n description=\"Today\"\n />\n <StatCard\n title=\"Team Hours\"\n value={formatHrs(Math.round(stats?.totalHours ?? 0))}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"Total tracked\"\n />\n <StatCard\n title=\"Billable Hours\"\n value={formatHrs(Math.round(stats?.billableHours ?? 0))}\n icon={<Activity className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n </div>\n\n {patternBars.length > 0 && (\n <ChartCard\n title=\"Recent Work Patterns\"\n description=\"Focus, meeting and break hours across the latest logged patterns.\"\n >\n <BarsChart\n data={patternBars}\n xKey=\"label\"\n stacked\n height={240}\n valueFormat={formatHrs}\n series={[\n { key: \"focus\", label: \"Focus\" },\n { key: \"meetings\", label: \"Meetings\" },\n { key: \"breaks\", label: \"Breaks\" },\n ]}\n emptyLabel=\"No work patterns yet\"\n />\n </ChartCard>\n )}\n\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-4 sm:p-6\">\n <h3 className=\"mb-4 text-sm font-semibold text-text-primary\">\n Activity Feed\n </h3>\n {patterns.length === 0 && !patternsLoading ? (\n <EmptyState\n illustration=\"empty-notifications\"\n title=\"No recent activity yet\"\n description=\"As your team tracks time and completes focus sessions, their activity will stream in here.\"\n />\n ) : (\n <ul className=\"space-y-3\">\n {patterns.map((pattern: WorkPatternFieldsFragment) => {\n const isDeepWork = pattern.deepWorkSessions > 0;\n const hasVitals =\n Boolean(pattern.energyLevel) ||\n Boolean(pattern.mood) ||\n Boolean(pattern.stressLevel);\n return (\n <li\n key={pattern.id}\n className=\"rounded-lg border border-border-subtle bg-bg-surface/40 p-4\"\n >\n <div className=\"flex items-start gap-3\">\n <div className=\"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-xs font-semibold uppercase text-primary\">\n {monogram(pattern.userId)}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n #{shortId(pattern.userId)}\n </span>\n <span className=\"text-xs text-text-secondary\">\n {formatRelativeTime(pattern.createdAt)}\n </span>\n {isDeepWork && (\n <StatusBadge variant=\"success\" label=\"Deep work\" />\n )}\n </div>\n <p className=\"mt-1.5 flex items-center gap-1.5 text-sm font-medium text-text-primary\">\n {isDeepWork ? (\n <Zap className=\"h-3.5 w-3.5 text-primary\" />\n ) : (\n <Activity className=\"h-3.5 w-3.5 text-primary\" />\n )}\n {isDeepWork\n ? \"Completed deep work session\"\n : \"Logged work pattern\"}\n </p>\n <div className=\"mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <span>{pattern.focusHours.toFixed(1)}h focus</span>\n <span>\n {pattern.meetingHours.toFixed(1)}h meetings\n </span>\n {pattern.deepWorkSessions > 0 && (\n <span>\n {pattern.deepWorkSessions} deep sessions\n </span>\n )}\n {pattern.interruptions > 0 && (\n <span>{pattern.interruptions} interruptions</span>\n )}\n <span>{shortDate(pattern.date)}</span>\n </div>\n {pattern.productivityScore != null && (\n <div className=\"mt-3\">\n <div className=\"mb-1 flex items-center justify-between text-xs\">\n <span className=\"text-text-secondary\">\n Productivity\n </span>\n <span className=\"tabular-nums text-text-primary\">\n {Math.round(pattern.productivityScore)}%\n </span>\n </div>\n <ProgressMeter\n value={pattern.productivityScore}\n max={100}\n higherIsWorse={false}\n thresholds={{ warning: 0.6, danger: 0.4 }}\n showValue={false}\n size=\"sm\"\n />\n </div>\n )}\n {hasVitals && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {pattern.energyLevel && (\n <StatusBadge\n variant=\"info\"\n label={`Energy: ${pattern.energyLevel}`}\n />\n )}\n {pattern.mood && (\n <StatusBadge\n variant=\"muted\"\n label={`Mood: ${pattern.mood}`}\n />\n )}\n {pattern.stressLevel && (\n <StatusBadge\n variant=\"warning\"\n label={`Stress: ${pattern.stressLevel}`}\n />\n )}\n </div>\n )}\n </div>\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TeamActivityPage.js","names":[],"sources":["../../../../src/timecampus/pages/team-activity/TeamActivityPage.tsx"],"sourcesContent":["import { Activity, Users, Clock, CheckCircle, Zap } from \"lucide-react\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n BarsChart,\n ProgressMeter,\n} from \"../../components\";\nimport { formatRelativeTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useListTeamWorkPatternsQuery,\n type TeamWorkPatternEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst formatHrs = (v: number): string =>\n `${Number.isInteger(v) ? v : v.toFixed(1)}h`;\n\n// Fallback monogram/short-id for the rare case the identity service didn't\n// resolve a member (left the workspace, lookup failure, etc.) — the backend\n// (teamWorkPatterns) already substitutes a \"Member <shortId>\" display name in\n// that case, this just derives a matching two-letter monogram from it.\nconst monogram = (memberName: string): string =>\n memberName.slice(0, 2).toUpperCase();\n\nconst shortDate = (value: string): string =>\n new Date(value).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n });\n\nexport function TeamActivityPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetTimeEntryStatsQuery({ skip: !workspaceId });\n // Team-wide (BOFF-4037): every active workspace member's recent activity,\n // each resolved to that member's real display name/avatar — never just\n // the calling actor's own patterns repeated under everyone else's name.\n const {\n data: patternsData,\n loading: patternsLoading,\n error: patternsError,\n } = useListTeamWorkPatternsQuery({\n variables: { pagination: { limit: 10 } },\n skip: !workspaceId,\n });\n\n const stats = statsData?.timeEntryStats;\n const entries = patternsData?.teamWorkPatterns ?? [];\n\n if (statsLoading && patternsLoading) {\n return (\n <PageLayout\n title=\"Team Activity\"\n description=\"Real-time team activity feed\"\n >\n <LoadingSkeleton variant=\"list\" count={5} />\n </PageLayout>\n );\n }\n\n if (statsError && patternsError) {\n return (\n <PageLayout\n title=\"Team Activity\"\n description=\"Real-time team activity feed\"\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 team activity data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n // Recent work patterns as a real stacked composition — sourced only from the\n // rows already fetched above (oldest → newest for a left-to-right timeline).\n const patternBars = [...entries].reverse().map(({ pattern }) => ({\n label: shortDate(pattern.date),\n focus: Math.round(pattern.focusHours * 10) / 10,\n meetings: Math.round(pattern.meetingHours * 10) / 10,\n breaks: Math.round(pattern.breakHours * 10) / 10,\n }));\n\n return (\n <PageLayout\n title=\"Team Activity\"\n description=\"Real-time team activity feed\"\n purpose=\"A live pulse of what your team is doing right now — who's tracking, recent deep-work sessions, and today's hours — so you stay connected to the team's progress without interrupting anyone.\"\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=\"Active Timers\"\n value={String(stats?.activeTimers ?? 0)}\n icon={<Users className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Entries\"\n value={String(stats?.totalEntries ?? 0)}\n icon={<CheckCircle className=\"h-5 w-5\" />}\n description=\"Today\"\n />\n <StatCard\n title=\"Team Hours\"\n value={formatHrs(Math.round(stats?.totalHours ?? 0))}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"Total tracked\"\n />\n <StatCard\n title=\"Billable Hours\"\n value={formatHrs(Math.round(stats?.billableHours ?? 0))}\n icon={<Activity className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n </div>\n\n {patternBars.length > 0 && (\n <ChartCard\n title=\"Recent Work Patterns\"\n description=\"Focus, meeting and break hours across the latest logged patterns.\"\n >\n <BarsChart\n data={patternBars}\n xKey=\"label\"\n stacked\n height={240}\n valueFormat={formatHrs}\n series={[\n { key: \"focus\", label: \"Focus\" },\n { key: \"meetings\", label: \"Meetings\" },\n { key: \"breaks\", label: \"Breaks\" },\n ]}\n emptyLabel=\"No work patterns yet\"\n />\n </ChartCard>\n )}\n\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-4 sm:p-6\">\n <h3 className=\"mb-4 text-sm font-semibold text-text-primary\">\n Activity Feed\n </h3>\n {entries.length === 0 && !patternsLoading ? (\n <EmptyState\n illustration=\"empty-notifications\"\n title=\"No recent activity yet\"\n description=\"As your team tracks time and completes focus sessions, their activity will stream in here.\"\n />\n ) : (\n <ul className=\"space-y-3\">\n {entries.map(\n (\n {\n memberName,\n memberAvatar,\n pattern,\n }: TeamWorkPatternEntryFieldsFragment,\n index: number,\n ) => {\n const isDeepWork = pattern.deepWorkSessions > 0;\n const hasVitals =\n Boolean(pattern.energyLevel) ||\n Boolean(pattern.mood) ||\n Boolean(pattern.stressLevel);\n return (\n <li\n key={`${pattern.id}-${index}`}\n className=\"rounded-lg border border-border-subtle bg-bg-surface/40 p-4\"\n >\n <div className=\"flex items-start gap-3\">\n {memberAvatar ? (\n <img\n src={memberAvatar}\n alt={memberName}\n className=\"mt-0.5 h-9 w-9 shrink-0 rounded-full border border-border-seam object-cover\"\n />\n ) : (\n <div className=\"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-xs font-semibold uppercase text-primary\">\n {monogram(memberName)}\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n <span className=\"text-sm font-medium text-text-primary\">\n {memberName}\n </span>\n <span className=\"text-xs text-text-secondary\">\n {formatRelativeTime(pattern.createdAt)}\n </span>\n {isDeepWork && (\n <StatusBadge\n variant=\"success\"\n label=\"Deep work\"\n />\n )}\n </div>\n <p className=\"mt-1.5 flex items-center gap-1.5 text-sm font-medium text-text-primary\">\n {isDeepWork ? (\n <Zap className=\"h-3.5 w-3.5 text-primary\" />\n ) : (\n <Activity className=\"h-3.5 w-3.5 text-primary\" />\n )}\n {isDeepWork\n ? \"Completed deep work session\"\n : \"Logged work pattern\"}\n </p>\n <div className=\"mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <span>{pattern.focusHours.toFixed(1)}h focus</span>\n <span>\n {pattern.meetingHours.toFixed(1)}h meetings\n </span>\n {pattern.deepWorkSessions > 0 && (\n <span>\n {pattern.deepWorkSessions} deep sessions\n </span>\n )}\n {pattern.interruptions > 0 && (\n <span>{pattern.interruptions} interruptions</span>\n )}\n <span>{shortDate(pattern.date)}</span>\n </div>\n {pattern.productivityScore != null && (\n <div className=\"mt-3\">\n <div className=\"mb-1 flex items-center justify-between text-xs\">\n <span className=\"text-text-secondary\">\n Productivity\n </span>\n <span className=\"tabular-nums text-text-primary\">\n {Math.round(pattern.productivityScore)}%\n </span>\n </div>\n <ProgressMeter\n value={pattern.productivityScore}\n max={100}\n higherIsWorse={false}\n thresholds={{ warning: 0.6, danger: 0.4 }}\n showValue={false}\n size=\"sm\"\n />\n </div>\n )}\n {hasVitals && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {pattern.energyLevel && (\n <StatusBadge\n variant=\"info\"\n label={`Energy: ${pattern.energyLevel}`}\n />\n )}\n {pattern.mood && (\n <StatusBadge\n variant=\"muted\"\n label={`Mood: ${pattern.mood}`}\n />\n )}\n {pattern.stressLevel && (\n <StatusBadge\n variant=\"warning\"\n label={`Stress: ${pattern.stressLevel}`}\n />\n )}\n </div>\n )}\n </div>\n </div>\n </li>\n );\n },\n )}\n </ul>\n )}\n </div>\n </div>\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"}
|