@burdenoff/microfe-timecampus 2026.719.4 → 2026.731.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 +527 -0
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-operations.js +1112 -880
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.d.ts +175 -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/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/lifetime/PersonalCorporatePage.d.ts.map +1 -1
- package/dist/timecampus/pages/lifetime/PersonalCorporatePage.js +88 -59
- package/dist/timecampus/pages/lifetime/PersonalCorporatePage.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 +1 -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":"PersonalCorporatePage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/lifetime/PersonalCorporatePage.tsx"],"names":[],"mappings":"AA8HA,wBAAgB,qBAAqB,
|
|
1
|
+
{"version":3,"file":"PersonalCorporatePage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/lifetime/PersonalCorporatePage.tsx"],"names":[],"mappings":"AA8HA,wBAAgB,qBAAqB,gCAijBpC"}
|
|
@@ -2,15 +2,15 @@ import { useTimeCampus as e } from "../../providers/TimeCampusProvider.js";
|
|
|
2
2
|
import { cn 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 { useGrantSourceConsentMutation as i, useMyContextSharingPolicyQuery as
|
|
6
|
-
import { PageLayout as
|
|
7
|
-
import { StatusBadge as
|
|
8
|
-
import { EmptyState as
|
|
9
|
-
import { ToastContainer as
|
|
5
|
+
import { useGrantSourceConsentMutation as i, useMyContextSharingPolicyQuery as a, useMyPersonalDataSummaryQuery as ee, useMySourceConsentsQuery as o, useRevokeSourceConsentMutation as s, useUpdateContextSharingPolicyMutation as c } from "../../../generated/wspace-operations.js";
|
|
6
|
+
import { PageLayout as l } from "../../components/PageLayout.js";
|
|
7
|
+
import { StatusBadge as u } from "../../components/StatusBadge.js";
|
|
8
|
+
import { EmptyState as d } from "../../components/EmptyState.js";
|
|
9
|
+
import { ToastContainer as te, useToast as ne } from "../../components/Toast.js";
|
|
10
10
|
import "../../components/index.js";
|
|
11
|
-
import { formatSecondsHuman as
|
|
12
|
-
import { useMemo as
|
|
13
|
-
import { ArrowUpRight as
|
|
11
|
+
import { formatSecondsHuman as re, isoDaysAgo as ie, isoNow as ae, lifeDomainLabel as oe } from "./domains.js";
|
|
12
|
+
import { useMemo as f, useState as p } from "react";
|
|
13
|
+
import { ArrowUpRight as m, Building2 as h, Info as g, Lock as _, Plug as v, Share2 as se, ShieldCheck as ce } from "lucide-react";
|
|
14
14
|
import { jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
15
15
|
import { GlassCard as x } from "@burdenoff/fe-libs/ui";
|
|
16
16
|
//#region src/timecampus/pages/lifetime/PersonalCorporatePage.tsx
|
|
@@ -48,21 +48,21 @@ function w({ checked: e, onChange: n, disabled: r = !1, ariaLabel: i }) {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
function T() {
|
|
51
|
-
let { workspaceId: T = "" } = e(), { toasts:
|
|
52
|
-
from:
|
|
53
|
-
to:
|
|
54
|
-
}), []), { data:
|
|
51
|
+
let { workspaceId: T = "" } = e(), { toasts: le, addToast: E, dismissToast: ue } = ne(), D = f(() => ({
|
|
52
|
+
from: ie(30),
|
|
53
|
+
to: ae()
|
|
54
|
+
}), []), { data: O, loading: k, error: A } = a({ skip: !T }), { data: j, loading: M, error: N, refetch: P } = ee({
|
|
55
55
|
variables: {
|
|
56
|
-
from:
|
|
57
|
-
to:
|
|
56
|
+
from: D.from,
|
|
57
|
+
to: D.to
|
|
58
58
|
},
|
|
59
59
|
skip: !T
|
|
60
|
-
}), { data:
|
|
60
|
+
}), { data: F, loading: I, error: L, refetch: R } = o({ skip: !T }), [z] = c(), [B] = i(), [V] = s(), [H, U] = p(null), [de, W] = p(null), [G, K] = p(null), q = O?.myContextSharingPolicy, J = j?.myPersonalDataSummary, Y = F?.mySourceConsents ?? [], X = f(() => [...J?.byDomain ?? []].sort((e, t) => t.totalSeconds - e.totalSeconds), [J]), Z = X[0]?.totalSeconds ?? 0, Q = J?.sharedAggregates ?? [], $ = {
|
|
61
61
|
title: "Personal & Corporate",
|
|
62
62
|
description: "Two contexts, one boundary you control",
|
|
63
63
|
purpose: "TimeCampus keeps two worlds apart: your private life record, which stays yours, and the workspace you work in. This is where you decide exactly which aggregate work signals — never your personal context — travel up to your employer."
|
|
64
64
|
};
|
|
65
|
-
if (!T ||
|
|
65
|
+
if (!T || k && !O || M && !j) return /* @__PURE__ */ y(l, {
|
|
66
66
|
...$,
|
|
67
67
|
children: /* @__PURE__ */ b("div", {
|
|
68
68
|
className: "grid grid-cols-1 gap-6 lg:grid-cols-3",
|
|
@@ -82,65 +82,89 @@ function T() {
|
|
|
82
82
|
]
|
|
83
83
|
})
|
|
84
84
|
});
|
|
85
|
-
if (
|
|
85
|
+
if (A && N && !q && !J) return /* @__PURE__ */ y(l, {
|
|
86
86
|
...$,
|
|
87
87
|
children: /* @__PURE__ */ y("div", {
|
|
88
88
|
className: "rounded-xl border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text",
|
|
89
89
|
children: "We couldn't load your privacy settings right now. Please try again in a moment."
|
|
90
90
|
})
|
|
91
91
|
});
|
|
92
|
-
let
|
|
92
|
+
let fe = async (e, t) => {
|
|
93
93
|
if (!q) return;
|
|
94
94
|
let n = S.find((t) => t.key === e)?.label ?? "signal";
|
|
95
|
-
|
|
95
|
+
U(e);
|
|
96
96
|
try {
|
|
97
|
-
let r = await
|
|
97
|
+
let r = await z({ variables: { input: {
|
|
98
98
|
sustainableHours: q.sustainableHours,
|
|
99
99
|
recoveryBand: q.recoveryBand,
|
|
100
100
|
workLifeBoundary: q.workLifeBoundary,
|
|
101
101
|
[e]: t
|
|
102
102
|
} } });
|
|
103
103
|
if (r.error) throw Error(r.error.message);
|
|
104
|
-
await
|
|
104
|
+
await P(), E({
|
|
105
105
|
title: t ? `Now sharing "${n}"` : `Stopped sharing "${n}"`,
|
|
106
106
|
description: "Your workspace only ever sees this as an aggregate signal.",
|
|
107
107
|
variant: "success"
|
|
108
108
|
});
|
|
109
109
|
} catch {
|
|
110
|
-
|
|
110
|
+
E({
|
|
111
111
|
title: "Couldn't update what you share",
|
|
112
112
|
description: "Nothing was changed. Please try again.",
|
|
113
113
|
variant: "error"
|
|
114
114
|
});
|
|
115
115
|
} finally {
|
|
116
|
-
|
|
116
|
+
U(null);
|
|
117
117
|
}
|
|
118
|
-
},
|
|
118
|
+
}, pe = async (e, t) => {
|
|
119
119
|
let n = C(e.source);
|
|
120
120
|
K(e.source);
|
|
121
|
+
try {
|
|
122
|
+
let r = await B({ variables: {
|
|
123
|
+
source: e.source,
|
|
124
|
+
mode: e.mode,
|
|
125
|
+
onDeviceOnly: t
|
|
126
|
+
} });
|
|
127
|
+
if (r.error) throw Error(r.error.message);
|
|
128
|
+
await R(), E({
|
|
129
|
+
title: t ? `${n} now on-device only` : `${n} on-device-only turned off`,
|
|
130
|
+
description: t ? "Only aggregated app/category/timing data reaches your workspace — no titles, URLs, or other details." : void 0,
|
|
131
|
+
variant: "success"
|
|
132
|
+
});
|
|
133
|
+
} catch {
|
|
134
|
+
E({
|
|
135
|
+
title: "Couldn't update on-device-only",
|
|
136
|
+
description: "Nothing was changed. Please try again.",
|
|
137
|
+
variant: "error"
|
|
138
|
+
});
|
|
139
|
+
} finally {
|
|
140
|
+
K(null);
|
|
141
|
+
}
|
|
142
|
+
}, me = async (e, t) => {
|
|
143
|
+
let n = C(e.source);
|
|
144
|
+
W(e.source);
|
|
121
145
|
try {
|
|
122
146
|
if (t) {
|
|
123
|
-
let t = await
|
|
147
|
+
let t = await B({ variables: { source: e.source } });
|
|
124
148
|
if (t.error) throw Error(t.error.message);
|
|
125
149
|
} else {
|
|
126
|
-
let t = await
|
|
150
|
+
let t = await V({ variables: { source: e.source } });
|
|
127
151
|
if (t.error) throw Error(t.error.message);
|
|
128
152
|
}
|
|
129
|
-
await
|
|
153
|
+
await R(), E({
|
|
130
154
|
title: t ? `${n} connected` : `${n} disconnected`,
|
|
131
155
|
variant: "success"
|
|
132
156
|
});
|
|
133
157
|
} catch {
|
|
134
|
-
|
|
158
|
+
E({
|
|
135
159
|
title: "Couldn't update this source",
|
|
136
160
|
description: "Nothing was changed. Please try again.",
|
|
137
161
|
variant: "error"
|
|
138
162
|
});
|
|
139
163
|
} finally {
|
|
140
|
-
|
|
164
|
+
W(null);
|
|
141
165
|
}
|
|
142
166
|
};
|
|
143
|
-
return /* @__PURE__ */ b(
|
|
167
|
+
return /* @__PURE__ */ b(l, {
|
|
144
168
|
...$,
|
|
145
169
|
children: [
|
|
146
170
|
/* @__PURE__ */ b("div", {
|
|
@@ -153,7 +177,7 @@ function T() {
|
|
|
153
177
|
className: "mb-4 flex items-start gap-3",
|
|
154
178
|
children: [/* @__PURE__ */ y("div", {
|
|
155
179
|
className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary",
|
|
156
|
-
children: /* @__PURE__ */ y(
|
|
180
|
+
children: /* @__PURE__ */ y(_, { className: "h-5 w-5" })
|
|
157
181
|
}), /* @__PURE__ */ b("div", {
|
|
158
182
|
className: "min-w-0",
|
|
159
183
|
children: [/* @__PURE__ */ b("h2", {
|
|
@@ -171,10 +195,10 @@ function T() {
|
|
|
171
195
|
children: "Everything you capture about your life. Locked to your account — it never leaves this side."
|
|
172
196
|
})]
|
|
173
197
|
})]
|
|
174
|
-
}),
|
|
198
|
+
}), N && !J ? /* @__PURE__ */ y("div", {
|
|
175
199
|
className: "rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-3 text-xs text-status-error-text",
|
|
176
200
|
children: "Couldn't load your private data summary right now."
|
|
177
|
-
}) : !J || J.totalPersonalEvents === 0 ? /* @__PURE__ */ y(
|
|
201
|
+
}) : !J || J.totalPersonalEvents === 0 ? /* @__PURE__ */ y(d, {
|
|
178
202
|
illustration: "empty-data",
|
|
179
203
|
title: "Nothing private captured yet",
|
|
180
204
|
description: "Log your first life entry and it will live here — always private to you."
|
|
@@ -183,7 +207,7 @@ function T() {
|
|
|
183
207
|
children: [/* @__PURE__ */ y(n, {
|
|
184
208
|
title: "Private life entries",
|
|
185
209
|
value: J.totalPersonalEvents.toLocaleString(),
|
|
186
|
-
icon: /* @__PURE__ */ y(
|
|
210
|
+
icon: /* @__PURE__ */ y(_, { className: "h-5 w-5" }),
|
|
187
211
|
description: "captured in the last 30 days"
|
|
188
212
|
}), X.length === 0 ? /* @__PURE__ */ y("p", {
|
|
189
213
|
className: "text-xs text-text-secondary",
|
|
@@ -200,11 +224,11 @@ function T() {
|
|
|
200
224
|
className: "min-w-0",
|
|
201
225
|
children: [/* @__PURE__ */ y("p", {
|
|
202
226
|
className: "truncate text-sm font-medium text-text-primary",
|
|
203
|
-
children:
|
|
227
|
+
children: oe(e.domain)
|
|
204
228
|
}), /* @__PURE__ */ b("p", {
|
|
205
229
|
className: "mt-0.5 text-xs text-text-secondary",
|
|
206
230
|
children: [
|
|
207
|
-
|
|
231
|
+
re(e.totalSeconds),
|
|
208
232
|
" ·",
|
|
209
233
|
" ",
|
|
210
234
|
e.eventCount.toLocaleString(),
|
|
@@ -212,7 +236,7 @@ function T() {
|
|
|
212
236
|
e.eventCount === 1 ? "entry" : "entries"
|
|
213
237
|
]
|
|
214
238
|
})]
|
|
215
|
-
}), /* @__PURE__ */ y(
|
|
239
|
+
}), /* @__PURE__ */ y(_, {
|
|
216
240
|
className: "h-3.5 w-3.5 shrink-0 text-text-muted",
|
|
217
241
|
"aria-label": "Private to you"
|
|
218
242
|
})]
|
|
@@ -238,7 +262,7 @@ function T() {
|
|
|
238
262
|
className: "mb-4 flex items-start gap-3",
|
|
239
263
|
children: [/* @__PURE__ */ y("div", {
|
|
240
264
|
className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-status-info-bg/40 text-status-info-text",
|
|
241
|
-
children: /* @__PURE__ */ y(
|
|
265
|
+
children: /* @__PURE__ */ y(se, { className: "h-5 w-5" })
|
|
242
266
|
}), /* @__PURE__ */ b("div", {
|
|
243
267
|
className: "min-w-0",
|
|
244
268
|
children: [/* @__PURE__ */ y("h2", {
|
|
@@ -249,13 +273,13 @@ function T() {
|
|
|
249
273
|
children: "Aggregate signals only. Every switch is off until you turn it on."
|
|
250
274
|
})]
|
|
251
275
|
})]
|
|
252
|
-
}),
|
|
276
|
+
}), A && !q ? /* @__PURE__ */ y("div", {
|
|
253
277
|
className: "rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-3 text-xs text-status-error-text",
|
|
254
278
|
children: "Couldn't load your sharing settings right now."
|
|
255
279
|
}) : /* @__PURE__ */ y("div", {
|
|
256
280
|
className: "space-y-3",
|
|
257
281
|
children: S.map((e) => {
|
|
258
|
-
let n = q ? q[e.key] : !1, r =
|
|
282
|
+
let n = q ? q[e.key] : !1, r = H === e.key;
|
|
259
283
|
return /* @__PURE__ */ y("div", {
|
|
260
284
|
className: "rounded-lg border border-border-subtle bg-bg-sunken/40 p-3",
|
|
261
285
|
children: /* @__PURE__ */ b("div", {
|
|
@@ -275,7 +299,7 @@ function T() {
|
|
|
275
299
|
checked: n,
|
|
276
300
|
disabled: r || !q,
|
|
277
301
|
ariaLabel: `Share "${e.label}" with your workspace`,
|
|
278
|
-
onChange: (t) =>
|
|
302
|
+
onChange: (t) => fe(e.key, t)
|
|
279
303
|
}), /* @__PURE__ */ y("span", {
|
|
280
304
|
className: t("text-[0.65rem] font-medium uppercase tracking-wide", n ? "text-status-success-text" : "text-text-muted"),
|
|
281
305
|
children: r ? "Saving…" : n ? "On" : "Off"
|
|
@@ -307,7 +331,7 @@ function T() {
|
|
|
307
331
|
className: "mb-4 flex items-start gap-3",
|
|
308
332
|
children: [/* @__PURE__ */ y("div", {
|
|
309
333
|
className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary",
|
|
310
|
-
children: /* @__PURE__ */ y(
|
|
334
|
+
children: /* @__PURE__ */ y(h, { className: "h-5 w-5" })
|
|
311
335
|
}), /* @__PURE__ */ b("div", {
|
|
312
336
|
className: "min-w-0",
|
|
313
337
|
children: [/* @__PURE__ */ b("h2", {
|
|
@@ -330,7 +354,7 @@ function T() {
|
|
|
330
354
|
children: [
|
|
331
355
|
/* @__PURE__ */ b("div", {
|
|
332
356
|
className: "flex items-start gap-2 rounded-lg border border-status-info-border/40 bg-status-info-bg/10 px-3 py-2.5 text-xs text-status-info-text",
|
|
333
|
-
children: [/* @__PURE__ */ y(
|
|
357
|
+
children: [/* @__PURE__ */ y(g, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ y("span", { children: "Your workspace only ever receives aggregate work signals — coarse yes/no bands, never entries, locations, health or timestamps." })]
|
|
334
358
|
}),
|
|
335
359
|
/* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("p", {
|
|
336
360
|
className: "mb-2 text-xs font-medium uppercase tracking-wide text-text-muted",
|
|
@@ -350,11 +374,11 @@ function T() {
|
|
|
350
374
|
className: "flex items-center justify-between gap-3 rounded-lg border border-border-subtle bg-bg-sunken/40 px-3 py-2.5",
|
|
351
375
|
children: [/* @__PURE__ */ b("span", {
|
|
352
376
|
className: "flex min-w-0 items-center gap-2 text-sm text-text-primary",
|
|
353
|
-
children: [/* @__PURE__ */ y(
|
|
377
|
+
children: [/* @__PURE__ */ y(m, { className: "h-4 w-4 shrink-0 text-status-success-text" }), /* @__PURE__ */ y("span", {
|
|
354
378
|
className: "truncate",
|
|
355
379
|
children: C(e)
|
|
356
380
|
})]
|
|
357
|
-
}), /* @__PURE__ */ y(
|
|
381
|
+
}), /* @__PURE__ */ y(u, {
|
|
358
382
|
variant: "success",
|
|
359
383
|
label: "Shared",
|
|
360
384
|
dot: !0
|
|
@@ -377,7 +401,7 @@ function T() {
|
|
|
377
401
|
className: "mb-4 flex items-start gap-3",
|
|
378
402
|
children: [/* @__PURE__ */ y("div", {
|
|
379
403
|
className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary",
|
|
380
|
-
children: /* @__PURE__ */ y(
|
|
404
|
+
children: /* @__PURE__ */ y(v, { className: "h-5 w-5" })
|
|
381
405
|
}), /* @__PURE__ */ b("div", {
|
|
382
406
|
className: "min-w-0",
|
|
383
407
|
children: [/* @__PURE__ */ y("h3", {
|
|
@@ -388,20 +412,20 @@ function T() {
|
|
|
388
412
|
children: "Each capture source only feeds your private record once you consent — and you can withdraw at any moment."
|
|
389
413
|
})]
|
|
390
414
|
})]
|
|
391
|
-
}),
|
|
415
|
+
}), L && Y.length === 0 ? /* @__PURE__ */ y("div", {
|
|
392
416
|
className: "rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-3 text-xs text-status-error-text",
|
|
393
417
|
children: "Couldn't load your connected sources right now."
|
|
394
|
-
}) :
|
|
418
|
+
}) : I && Y.length === 0 ? /* @__PURE__ */ y(r, {
|
|
395
419
|
variant: "list",
|
|
396
420
|
count: 3
|
|
397
|
-
}) : Y.length === 0 ? /* @__PURE__ */ y(
|
|
421
|
+
}) : Y.length === 0 ? /* @__PURE__ */ y(d, {
|
|
398
422
|
illustration: "empty-data",
|
|
399
423
|
title: "No sources connected yet",
|
|
400
424
|
description: "When you connect a capture source — a calendar, location or health signal — you'll grant and revoke exactly what it may feed here."
|
|
401
425
|
}) : /* @__PURE__ */ y("div", {
|
|
402
426
|
className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3",
|
|
403
427
|
children: Y.map((e) => {
|
|
404
|
-
let n =
|
|
428
|
+
let n = de === e.source;
|
|
405
429
|
return /* @__PURE__ */ y(x, {
|
|
406
430
|
treatment: "metric",
|
|
407
431
|
className: "p-4",
|
|
@@ -415,15 +439,20 @@ function T() {
|
|
|
415
439
|
}), /* @__PURE__ */ b("div", {
|
|
416
440
|
className: "mt-1.5 flex flex-wrap items-center gap-1.5",
|
|
417
441
|
children: [
|
|
418
|
-
/* @__PURE__ */ y(
|
|
442
|
+
/* @__PURE__ */ y(u, {
|
|
419
443
|
variant: "muted",
|
|
420
444
|
label: C(e.mode)
|
|
421
445
|
}),
|
|
422
|
-
e.
|
|
423
|
-
|
|
424
|
-
|
|
446
|
+
e.granted && /* @__PURE__ */ y("button", {
|
|
447
|
+
type: "button",
|
|
448
|
+
role: "switch",
|
|
449
|
+
"aria-checked": e.onDeviceOnly,
|
|
450
|
+
disabled: G === e.source,
|
|
451
|
+
onClick: () => pe(e, !e.onDeviceOnly),
|
|
452
|
+
className: t("rounded-full border px-2 py-0.5 text-[0.65rem] font-medium uppercase tracking-wide transition-colors", "focus:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg/50", e.onDeviceOnly ? "border-status-info-border/60 bg-status-info-bg/40 text-status-info-text" : "border-border-subtle bg-bg-sunken/40 text-text-muted hover:text-text-secondary", G === e.source && "cursor-not-allowed opacity-50"),
|
|
453
|
+
children: G === e.source ? "Saving…" : e.onDeviceOnly ? "On-device only" : "Turn on on-device only"
|
|
425
454
|
}),
|
|
426
|
-
e.retentionDays != null && /* @__PURE__ */ y(
|
|
455
|
+
e.retentionDays != null && /* @__PURE__ */ y(u, {
|
|
427
456
|
variant: "muted",
|
|
428
457
|
label: `Kept ${e.retentionDays}d`
|
|
429
458
|
})
|
|
@@ -435,7 +464,7 @@ function T() {
|
|
|
435
464
|
checked: e.granted,
|
|
436
465
|
disabled: n,
|
|
437
466
|
ariaLabel: `Consent for ${C(e.source)}`,
|
|
438
|
-
onChange: (t) =>
|
|
467
|
+
onChange: (t) => me(e, t)
|
|
439
468
|
}), /* @__PURE__ */ y("span", {
|
|
440
469
|
className: t("text-[0.65rem] font-medium uppercase tracking-wide", e.granted ? "text-status-success-text" : "text-text-muted"),
|
|
441
470
|
children: n ? "Saving…" : e.granted ? "Granted" : "Revoked"
|
|
@@ -446,9 +475,9 @@ function T() {
|
|
|
446
475
|
})
|
|
447
476
|
})]
|
|
448
477
|
}),
|
|
449
|
-
/* @__PURE__ */ y(
|
|
450
|
-
toasts:
|
|
451
|
-
onDismiss:
|
|
478
|
+
/* @__PURE__ */ y(te, {
|
|
479
|
+
toasts: le,
|
|
480
|
+
onDismiss: ue
|
|
452
481
|
})
|
|
453
482
|
]
|
|
454
483
|
});
|