@burdenoff/microfe-timecampus 2026.703.3 → 2026.703.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js +192 -181
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +60 -49
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingFormModal.js +79 -68
- package/dist/timecampus/pages/meetings/MeetingFormModal.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientPortalPage.js","names":[],"sources":["../../../../src/timecampus/pages/client-portal/ClientPortalPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n Building2,\n Plus,\n DollarSign,\n FolderOpen,\n Link2,\n Check,\n RefreshCw,\n Pencil,\n Trash2,\n Power,\n} from \"lucide-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 { getInitials } from \"../../utils/helpers\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport {\n useListClientPortalConfigsQuery,\n useGetClientPortalStatsQuery,\n useRegenerateClientPortalShareTokenMutation,\n useCreateClientPortalConfigMutation,\n useUpdateClientPortalConfigMutation,\n useDeleteClientPortalConfigMutation,\n useToggleClientPortalMutation,\n type ClientPortalFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 25;\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\nconst fileInputClass =\n \"mt-1 block w-full text-sm text-text-secondary file:mr-3 file:rounded-md file:border-0 file:bg-action-primary-bg file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-action-primary-text hover:file:bg-action-primary-bg/90\";\n\n/**\n * Resolve a downloadable URL for a Files object. The URL is presigned and\n * expires after 7 days (604800s) — a known limitation: a stored logoUrl will\n * eventually 403 and the logo must be re-uploaded to refresh it.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampus_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n/**\n * Build the public share URL for a portal token. The public client view is\n * served by the website at /portal/:token; only active portals resolve.\n */\nfunction shareUrlFor(token: string): string {\n const origin = typeof window !== \"undefined\" ? window.location.origin : \"\";\n return `${origin}/portal/${token}`;\n}\n\ninterface ClientFormState {\n clientName: string;\n contactEmail: string;\n contactPhone: string;\n description: string;\n logoUrl: string;\n}\n\nconst emptyForm: ClientFormState = {\n clientName: \"\",\n contactEmail: \"\",\n contactPhone: \"\",\n description: \"\",\n logoUrl: \"\",\n};\n\nexport function ClientPortalPage() {\n const { toasts, addToast, dismissToast } = useToast();\n const apollo = useApolloClient();\n const [copiedId, setCopiedId] = useState<string | null>(null);\n const [logoUploading, setLogoUploading] = useState(false);\n\n const {\n data: configsData,\n loading: configsLoading,\n error: configsError,\n refetch,\n fetchMore,\n } = useListClientPortalConfigsQuery({\n variables: { limit: PAGE_SIZE, offset: 0 },\n notifyOnNetworkStatusChange: true,\n });\n const { data: statsData, refetch: refetchStats } =\n useGetClientPortalStatsQuery();\n\n const [regenerate, { loading: regenerating }] =\n useRegenerateClientPortalShareTokenMutation();\n const [createClient, { loading: creating }] =\n useCreateClientPortalConfigMutation();\n const [updateClient, { loading: updating }] =\n useUpdateClientPortalConfigMutation();\n const [deleteClient, { loading: deleting }] =\n useDeleteClientPortalConfigMutation();\n const [toggleClient] = useToggleClientPortalMutation();\n\n const clients = configsData?.clientPortalConfigs?.items ?? [];\n const hasMore = configsData?.clientPortalConfigs?.hasMore ?? false;\n const stats = statsData?.clientPortalStats;\n\n const [loadingMore, setLoadingMore] = useState(false);\n\n // Create / edit modal\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<ClientPortalFieldsFragment | null>(\n null,\n );\n const [form, setForm] = useState<ClientFormState>(emptyForm);\n\n // Delete confirm\n const [deleteTarget, setDeleteTarget] =\n useState<ClientPortalFieldsFragment | null>(null);\n\n // Per-row busy (toggle/regenerate)\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const handleCopy = async (client: ClientPortalFieldsFragment) => {\n if (!client.shareToken) return;\n try {\n await navigator.clipboard.writeText(shareUrlFor(client.shareToken));\n setCopiedId(client.id);\n window.setTimeout(() => setCopiedId(null), 2000);\n } catch {\n addToast({ title: \"Clipboard unavailable\", variant: \"error\" });\n }\n };\n\n const handleRegenerate = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await regenerate({ variables: { id: client.id } });\n addToast({ title: \"Share link regenerated\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Failed to regenerate link\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (client: ClientPortalFieldsFragment) => {\n setEditing(client);\n setForm({\n clientName: client.clientName,\n contactEmail: client.contactEmail ?? \"\",\n contactPhone: client.contactPhone ?? \"\",\n description: client.description ?? \"\",\n logoUrl: client.logoUrl ?? \"\",\n });\n setFormOpen(true);\n };\n\n /**\n * Upload a client logo to platform Files and store its presigned URL on the\n * form. Failures surface a toast but never block saving the client — the\n * logo is optional.\n */\n const handleLogoUpload = async (file: File) => {\n setLogoUploading(true);\n try {\n const uploaded = await uploadBlobToFiles({\n client: apollo,\n blob: file,\n filename: file.name,\n mimeType: file.type || \"application/octet-stream\",\n // Empty folderId = workspace root; backend treats it as unfoldered.\n folderId: \"\",\n });\n const { data } = await apollo.query({\n query: GET_FILE_DOWNLOAD_URL,\n variables: { id: uploaded.id, expiresIn: 604800 },\n fetchPolicy: \"no-cache\",\n });\n const url = (data as { getFileDownloadUrl?: string } | null | undefined)?.getFileDownloadUrl;\n if (!url) throw new Error(\"No download URL returned\");\n setForm((f) => ({ ...f, logoUrl: url }));\n } catch {\n addToast({ title: \"Failed to upload logo\", variant: \"error\" });\n } finally {\n setLogoUploading(false);\n }\n };\n\n const closeForm = () => {\n setFormOpen(false);\n setEditing(null);\n };\n\n const handleSubmit = async () => {\n if (!form.clientName.trim()) return;\n try {\n if (editing) {\n await updateClient({\n variables: {\n id: editing.id,\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n },\n },\n });\n addToast({ title: \"Client updated\", variant: \"success\" });\n } else {\n await createClient({\n variables: {\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n isActive: true,\n },\n },\n });\n addToast({ title: \"Client added\", variant: \"success\" });\n }\n closeForm();\n await refreshAll();\n } catch {\n addToast({\n title: editing ? \"Failed to update client\" : \"Failed to add client\",\n variant: \"error\",\n });\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteClient({ variables: { id: deleteTarget.id } });\n addToast({ title: \"Client deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to delete client\", variant: \"error\" });\n }\n };\n\n const handleToggle = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await toggleClient({\n variables: { id: client.id, isActive: !client.isActive },\n });\n addToast({\n title: client.isActive ? \"Client deactivated\" : \"Client activated\",\n variant: \"success\",\n });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to toggle client\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { limit: PAGE_SIZE, offset: clients.length },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n clientPortalConfigs: {\n ...fetchMoreResult.clientPortalConfigs,\n items: [\n ...prev.clientPortalConfigs.items,\n ...fetchMoreResult.clientPortalConfigs.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (configsLoading && clients.length === 0) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (configsError) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load client portal data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const addButton = (\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Client\n </button>\n );\n\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n purpose=\"Give clients a transparent window into the work you do for them — the projects, hours and progress tied to each one — so billing conversations are backed by data and trust, not spreadsheets.\"\n actions={addButton}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Active Clients\"\n value={String(stats?.activeClients ?? 0)}\n icon={<Building2 className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Projects\"\n value={String(stats?.totalProjectsShared ?? 0)}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Clients\"\n value={String(stats?.totalClients ?? 0)}\n icon={<DollarSign className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Active\"\n value={String(stats?.activeClients ?? 0)}\n trend={{ value: 0, label: \"clients\" }}\n />\n </div>\n\n {clients.length === 0 ? (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No clients configured yet\"\n description=\"Add your first client to share projects and tracked hours with them.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop\">\n <div className=\"divide-y divide-border\">\n {clients.map((client: ClientPortalFieldsFragment) => {\n const totalTrackedHours = client.projects.reduce(\n (sum: number, p: { trackedHours: number }) =>\n sum + p.trackedHours,\n 0,\n );\n const rowBusy = busyId === client.id;\n return (\n <div\n key={client.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 <div className=\"flex h-10 w-10 items-center justify-center rounded-lg bg-action-primary-bg/10 text-sm font-medium text-primary\">\n {getInitials(client.clientName)}\n </div>\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n {client.clientName}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {client.projects.length} projects ·{\" \"}\n {totalTrackedHours}h tracked\n {client.contactEmail && (\n <> · {client.contactEmail}</>\n )}\n </p>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n {client.isActive && client.shareToken && (\n <>\n <button\n type=\"button\"\n onClick={() => void handleCopy(client)}\n title=\"Copy the client's private share link\"\n className=\"inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken/50\"\n >\n {copiedId === client.id ? (\n <>\n <Check className=\"h-3.5 w-3.5\" /> Copied\n </>\n ) : (\n <>\n <Link2 className=\"h-3.5 w-3.5\" /> Share link\n </>\n )}\n </button>\n <button\n type=\"button\"\n disabled={rowBusy && regenerating}\n onClick={() => void handleRegenerate(client)}\n title=\"Regenerate the link (invalidates the old one)\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n <RefreshCw\n className={`h-3.5 w-3.5 ${rowBusy && regenerating ? \"animate-spin\" : \"\"}`}\n />\n </button>\n </>\n )}\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => void handleToggle(client)}\n title={\n client.isActive\n ? \"Deactivate client portal\"\n : \"Activate client portal\"\n }\n className=\"inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n <Power className=\"h-3.5 w-3.5\" />\n {client.isActive ? \"Deactivate\" : \"Activate\"}\n </button>\n <StatusBadge\n label={client.isActive ? \"active\" : \"inactive\"}\n status={client.isActive ? \"active\" : \"inactive\"}\n />\n <button\n type=\"button\"\n onClick={() => openEdit(client)}\n title=\"Edit client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/50\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(client)}\n title=\"Delete client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n\n {hasMore && (\n <div className=\"mt-4 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n <Modal\n open={formOpen}\n onClose={closeForm}\n title={editing ? \"Edit Client\" : \"Add Client\"}\n description={\n editing\n ? \"Update this client's portal details.\"\n : \"Set up a client so you can share projects and tracked hours with them.\"\n }\n footer={\n <>\n <button\n onClick={closeForm}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={creating || updating || !form.clientName.trim()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating || updating\n ? \"Saving...\"\n : editing\n ? \"Save Changes\"\n : \"Add Client\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Client Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Acme Corp\"\n value={form.clientName}\n onChange={(e) => setForm({ ...form, clientName: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Email\n </label>\n <input\n type=\"email\"\n placeholder=\"contact@client.com\"\n value={form.contactEmail}\n onChange={(e) =>\n setForm({ ...form, contactEmail: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Phone\n </label>\n <input\n type=\"tel\"\n placeholder=\"Optional\"\n value={form.contactPhone}\n onChange={(e) =>\n setForm({ ...form, contactPhone: e.target.value })\n }\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Description\n </label>\n <textarea\n rows={3}\n placeholder=\"Notes about this client (optional)\"\n value={form.description}\n onChange={(e) =>\n setForm({ ...form, description: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Logo\n </label>\n <input\n type=\"file\"\n accept=\"image/*\"\n disabled={logoUploading}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleLogoUpload(file);\n // Reset so re-selecting the same file re-triggers onChange.\n e.target.value = \"\";\n }}\n className={fileInputClass}\n />\n {logoUploading && (\n <p className=\"mt-1 text-xs text-text-secondary\">Uploading…</p>\n )}\n {form.logoUrl && (\n <img\n src={form.logoUrl}\n alt=\"Client logo preview\"\n className=\"mt-2 h-16 w-16 rounded-md border border-border-seam object-cover\"\n />\n )}\n </div>\n </div>\n </Modal>\n\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete client?\"\n description={\n deleteTarget\n ? `\"${deleteTarget.clientName}\" and its share link will be removed.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n The client will lose access to their shared portal immediately. This\n cannot be undone.\n </p>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ClientPortalPage.js","names":[],"sources":["../../../../src/timecampus/pages/client-portal/ClientPortalPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n Building2,\n Plus,\n DollarSign,\n FolderOpen,\n Link2,\n Check,\n RefreshCw,\n Pencil,\n Trash2,\n Power,\n} from \"lucide-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 { getInitials } from \"../../utils/helpers\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport {\n useListClientPortalConfigsQuery,\n useGetClientPortalStatsQuery,\n useRegenerateClientPortalShareTokenMutation,\n useCreateClientPortalConfigMutation,\n useUpdateClientPortalConfigMutation,\n useDeleteClientPortalConfigMutation,\n useToggleClientPortalMutation,\n type ClientPortalFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 25;\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\nconst fileInputClass =\n \"mt-1 block w-full text-sm text-text-secondary file:mr-3 file:rounded-md file:border-0 file:bg-action-primary-bg file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-action-primary-text hover:file:bg-action-primary-bg/90\";\n\n/**\n * Resolve a downloadable URL for a Files object. The URL is presigned and\n * expires after 7 days (604800s) — a known limitation: a stored logoUrl will\n * eventually 403 and the logo must be re-uploaded to refresh it.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampus_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// wspace-files requires a real folder id (File.folderId FK).\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusClient_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\n/**\n * Build the public share URL for a portal token. The public client view is\n * served by the website at /portal/:token; only active portals resolve.\n */\nfunction shareUrlFor(token: string): string {\n const origin = typeof window !== \"undefined\" ? window.location.origin : \"\";\n return `${origin}/portal/${token}`;\n}\n\ninterface ClientFormState {\n clientName: string;\n contactEmail: string;\n contactPhone: string;\n description: string;\n logoUrl: string;\n}\n\nconst emptyForm: ClientFormState = {\n clientName: \"\",\n contactEmail: \"\",\n contactPhone: \"\",\n description: \"\",\n logoUrl: \"\",\n};\n\nexport function ClientPortalPage() {\n const { toasts, addToast, dismissToast } = useToast();\n const apollo = useApolloClient();\n const [copiedId, setCopiedId] = useState<string | null>(null);\n const [logoUploading, setLogoUploading] = useState(false);\n\n const {\n data: configsData,\n loading: configsLoading,\n error: configsError,\n refetch,\n fetchMore,\n } = useListClientPortalConfigsQuery({\n variables: { limit: PAGE_SIZE, offset: 0 },\n notifyOnNetworkStatusChange: true,\n });\n const { data: statsData, refetch: refetchStats } =\n useGetClientPortalStatsQuery();\n\n const [regenerate, { loading: regenerating }] =\n useRegenerateClientPortalShareTokenMutation();\n const [createClient, { loading: creating }] =\n useCreateClientPortalConfigMutation();\n const [updateClient, { loading: updating }] =\n useUpdateClientPortalConfigMutation();\n const [deleteClient, { loading: deleting }] =\n useDeleteClientPortalConfigMutation();\n const [toggleClient] = useToggleClientPortalMutation();\n\n const clients = configsData?.clientPortalConfigs?.items ?? [];\n const hasMore = configsData?.clientPortalConfigs?.hasMore ?? false;\n const stats = statsData?.clientPortalStats;\n\n const [loadingMore, setLoadingMore] = useState(false);\n\n // Create / edit modal\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<ClientPortalFieldsFragment | null>(\n null,\n );\n const [form, setForm] = useState<ClientFormState>(emptyForm);\n\n // Delete confirm\n const [deleteTarget, setDeleteTarget] =\n useState<ClientPortalFieldsFragment | null>(null);\n\n // Per-row busy (toggle/regenerate)\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const handleCopy = async (client: ClientPortalFieldsFragment) => {\n if (!client.shareToken) return;\n try {\n await navigator.clipboard.writeText(shareUrlFor(client.shareToken));\n setCopiedId(client.id);\n window.setTimeout(() => setCopiedId(null), 2000);\n } catch {\n addToast({ title: \"Clipboard unavailable\", variant: \"error\" });\n }\n };\n\n const handleRegenerate = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await regenerate({ variables: { id: client.id } });\n addToast({ title: \"Share link regenerated\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Failed to regenerate link\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (client: ClientPortalFieldsFragment) => {\n setEditing(client);\n setForm({\n clientName: client.clientName,\n contactEmail: client.contactEmail ?? \"\",\n contactPhone: client.contactPhone ?? \"\",\n description: client.description ?? \"\",\n logoUrl: client.logoUrl ?? \"\",\n });\n setFormOpen(true);\n };\n\n /**\n * Upload a client logo to platform Files and store its presigned URL on the\n * form. Failures surface a toast but never block saving the client — the\n * logo is optional.\n */\n const handleLogoUpload = async (file: File) => {\n setLogoUploading(true);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/ClientLogos\" },\n });\n const folderId = (folderData as { ensureFolderByPath?: { id: string } } | null)\n ?.ensureFolderByPath?.id;\n if (!folderId) throw new Error(\"Could not resolve upload folder\");\n const uploaded = await uploadBlobToFiles({\n client: apollo,\n blob: file,\n filename: file.name,\n mimeType: file.type || \"application/octet-stream\",\n folderId,\n });\n const { data } = await apollo.query({\n query: GET_FILE_DOWNLOAD_URL,\n variables: { id: uploaded.id, expiresIn: 604800 },\n fetchPolicy: \"no-cache\",\n });\n const url = (data as { getFileDownloadUrl?: string } | null | undefined)?.getFileDownloadUrl;\n if (!url) throw new Error(\"No download URL returned\");\n setForm((f) => ({ ...f, logoUrl: url }));\n } catch {\n addToast({ title: \"Failed to upload logo\", variant: \"error\" });\n } finally {\n setLogoUploading(false);\n }\n };\n\n const closeForm = () => {\n setFormOpen(false);\n setEditing(null);\n };\n\n const handleSubmit = async () => {\n if (!form.clientName.trim()) return;\n try {\n if (editing) {\n await updateClient({\n variables: {\n id: editing.id,\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n },\n },\n });\n addToast({ title: \"Client updated\", variant: \"success\" });\n } else {\n await createClient({\n variables: {\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n isActive: true,\n },\n },\n });\n addToast({ title: \"Client added\", variant: \"success\" });\n }\n closeForm();\n await refreshAll();\n } catch {\n addToast({\n title: editing ? \"Failed to update client\" : \"Failed to add client\",\n variant: \"error\",\n });\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteClient({ variables: { id: deleteTarget.id } });\n addToast({ title: \"Client deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to delete client\", variant: \"error\" });\n }\n };\n\n const handleToggle = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await toggleClient({\n variables: { id: client.id, isActive: !client.isActive },\n });\n addToast({\n title: client.isActive ? \"Client deactivated\" : \"Client activated\",\n variant: \"success\",\n });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to toggle client\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { limit: PAGE_SIZE, offset: clients.length },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n clientPortalConfigs: {\n ...fetchMoreResult.clientPortalConfigs,\n items: [\n ...prev.clientPortalConfigs.items,\n ...fetchMoreResult.clientPortalConfigs.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (configsLoading && clients.length === 0) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (configsError) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load client portal data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const addButton = (\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Client\n </button>\n );\n\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n purpose=\"Give clients a transparent window into the work you do for them — the projects, hours and progress tied to each one — so billing conversations are backed by data and trust, not spreadsheets.\"\n actions={addButton}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Active Clients\"\n value={String(stats?.activeClients ?? 0)}\n icon={<Building2 className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Projects\"\n value={String(stats?.totalProjectsShared ?? 0)}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Clients\"\n value={String(stats?.totalClients ?? 0)}\n icon={<DollarSign className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Active\"\n value={String(stats?.activeClients ?? 0)}\n trend={{ value: 0, label: \"clients\" }}\n />\n </div>\n\n {clients.length === 0 ? (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No clients configured yet\"\n description=\"Add your first client to share projects and tracked hours with them.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop\">\n <div className=\"divide-y divide-border\">\n {clients.map((client: ClientPortalFieldsFragment) => {\n const totalTrackedHours = client.projects.reduce(\n (sum: number, p: { trackedHours: number }) =>\n sum + p.trackedHours,\n 0,\n );\n const rowBusy = busyId === client.id;\n return (\n <div\n key={client.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 <div className=\"flex h-10 w-10 items-center justify-center rounded-lg bg-action-primary-bg/10 text-sm font-medium text-primary\">\n {getInitials(client.clientName)}\n </div>\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n {client.clientName}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {client.projects.length} projects ·{\" \"}\n {totalTrackedHours}h tracked\n {client.contactEmail && (\n <> · {client.contactEmail}</>\n )}\n </p>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n {client.isActive && client.shareToken && (\n <>\n <button\n type=\"button\"\n onClick={() => void handleCopy(client)}\n title=\"Copy the client's private share link\"\n className=\"inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken/50\"\n >\n {copiedId === client.id ? (\n <>\n <Check className=\"h-3.5 w-3.5\" /> Copied\n </>\n ) : (\n <>\n <Link2 className=\"h-3.5 w-3.5\" /> Share link\n </>\n )}\n </button>\n <button\n type=\"button\"\n disabled={rowBusy && regenerating}\n onClick={() => void handleRegenerate(client)}\n title=\"Regenerate the link (invalidates the old one)\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n <RefreshCw\n className={`h-3.5 w-3.5 ${rowBusy && regenerating ? \"animate-spin\" : \"\"}`}\n />\n </button>\n </>\n )}\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => void handleToggle(client)}\n title={\n client.isActive\n ? \"Deactivate client portal\"\n : \"Activate client portal\"\n }\n className=\"inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n <Power className=\"h-3.5 w-3.5\" />\n {client.isActive ? \"Deactivate\" : \"Activate\"}\n </button>\n <StatusBadge\n label={client.isActive ? \"active\" : \"inactive\"}\n status={client.isActive ? \"active\" : \"inactive\"}\n />\n <button\n type=\"button\"\n onClick={() => openEdit(client)}\n title=\"Edit client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/50\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(client)}\n title=\"Delete client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n\n {hasMore && (\n <div className=\"mt-4 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n <Modal\n open={formOpen}\n onClose={closeForm}\n title={editing ? \"Edit Client\" : \"Add Client\"}\n description={\n editing\n ? \"Update this client's portal details.\"\n : \"Set up a client so you can share projects and tracked hours with them.\"\n }\n footer={\n <>\n <button\n onClick={closeForm}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={creating || updating || !form.clientName.trim()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating || updating\n ? \"Saving...\"\n : editing\n ? \"Save Changes\"\n : \"Add Client\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Client Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Acme Corp\"\n value={form.clientName}\n onChange={(e) => setForm({ ...form, clientName: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Email\n </label>\n <input\n type=\"email\"\n placeholder=\"contact@client.com\"\n value={form.contactEmail}\n onChange={(e) =>\n setForm({ ...form, contactEmail: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Phone\n </label>\n <input\n type=\"tel\"\n placeholder=\"Optional\"\n value={form.contactPhone}\n onChange={(e) =>\n setForm({ ...form, contactPhone: e.target.value })\n }\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Description\n </label>\n <textarea\n rows={3}\n placeholder=\"Notes about this client (optional)\"\n value={form.description}\n onChange={(e) =>\n setForm({ ...form, description: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Logo\n </label>\n <input\n type=\"file\"\n accept=\"image/*\"\n disabled={logoUploading}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleLogoUpload(file);\n // Reset so re-selecting the same file re-triggers onChange.\n e.target.value = \"\";\n }}\n className={fileInputClass}\n />\n {logoUploading && (\n <p className=\"mt-1 text-xs text-text-secondary\">Uploading…</p>\n )}\n {form.logoUrl && (\n <img\n src={form.logoUrl}\n alt=\"Client logo preview\"\n className=\"mt-2 h-16 w-16 rounded-md border border-border-seam object-cover\"\n />\n )}\n </div>\n </div>\n </Modal>\n\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete client?\"\n description={\n deleteTarget\n ? `\"${deleteTarget.clientName}\" and its share link will be removed.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n The client will lose access to their shared portal immediately. This\n cannot be undone.\n </p>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -30,8 +30,14 @@ var _ = 25, v = [
|
|
|
30
30
|
query TimeCampusExpense_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {
|
|
31
31
|
getFileDownloadUrl(id: $id, expiresIn: $expiresIn)
|
|
32
32
|
}
|
|
33
|
-
`, we =
|
|
34
|
-
|
|
33
|
+
`, we = d`
|
|
34
|
+
mutation TimeCampusExpense_EnsureFolder($path: String!) {
|
|
35
|
+
ensureFolderByPath(path: $path) {
|
|
36
|
+
id
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`, Te = 604800;
|
|
40
|
+
function Ee({ expense: e }) {
|
|
35
41
|
return g({
|
|
36
42
|
entityType: "TIMECAMPUS_EXPENSE",
|
|
37
43
|
entityId: e.id,
|
|
@@ -50,21 +56,21 @@ var x = {
|
|
|
50
56
|
receiptUrl: ""
|
|
51
57
|
};
|
|
52
58
|
function S() {
|
|
53
|
-
let { workspaceId: d = "" } = e(), { toasts: h, addToast: g, dismissToast: S } = ue(), C = xe(), { data:
|
|
59
|
+
let { workspaceId: d = "" } = e(), { toasts: h, addToast: g, dismissToast: S } = ue(), C = xe(), { data: De, loading: w, error: Oe, refetch: T, fetchMore: ke } = re({
|
|
54
60
|
variables: {
|
|
55
61
|
workspaceId: d,
|
|
56
62
|
limit: _,
|
|
57
63
|
offset: 0
|
|
58
64
|
},
|
|
59
65
|
notifyOnNetworkStatusChange: !0
|
|
60
|
-
}), { data:
|
|
61
|
-
await Promise.all([
|
|
62
|
-
},
|
|
66
|
+
}), { data: Ae, loading: je, error: Me, refetch: Ne } = ne({ variables: { workspaceId: d } }), [Pe, { loading: E }] = ee(), [Fe, { loading: D }] = oe(), [Ie, { loading: O }] = te(), [Le] = ae(), [Re] = a(), [ze, { loading: k }] = ie(), A = De?.expenses ?? [], j = Ae?.expenseStats, [M, N] = l(!1), [Be, Ve] = l(!0), [He, P] = l(!1), [F, I] = l(null), [L, R] = l(x), [z, B] = l(!1), [V, H] = l(null), [U, W] = l(null), [G, K] = l(null), [q, J] = l(""), [Ue, Y] = l(null), X = async () => {
|
|
67
|
+
await Promise.all([T(), Ne()]);
|
|
68
|
+
}, We = () => {
|
|
63
69
|
I(null), R({
|
|
64
70
|
...x,
|
|
65
71
|
date: b()
|
|
66
72
|
}), P(!0);
|
|
67
|
-
},
|
|
73
|
+
}, Ge = (e) => {
|
|
68
74
|
I(e), R({
|
|
69
75
|
amount: String(e.amount),
|
|
70
76
|
category: e.category,
|
|
@@ -73,27 +79,32 @@ function S() {
|
|
|
73
79
|
date: e.date ? e.date.slice(0, 10) : b(),
|
|
74
80
|
receiptUrl: e.receiptUrl ?? ""
|
|
75
81
|
}), P(!0);
|
|
76
|
-
},
|
|
82
|
+
}, Ke = async (e) => {
|
|
77
83
|
B(!0);
|
|
78
84
|
try {
|
|
79
|
-
let t = await
|
|
85
|
+
let { data: t } = await C.mutate({
|
|
86
|
+
mutation: we,
|
|
87
|
+
variables: { path: "/TimeCampus/Receipts" }
|
|
88
|
+
}), n = t?.ensureFolderByPath?.id;
|
|
89
|
+
if (!n) throw Error("Could not resolve upload folder");
|
|
90
|
+
let r = await Se({
|
|
80
91
|
client: C,
|
|
81
92
|
blob: e,
|
|
82
93
|
filename: e.name,
|
|
83
94
|
mimeType: e.type || "application/octet-stream",
|
|
84
|
-
folderId:
|
|
85
|
-
}), { data:
|
|
95
|
+
folderId: n
|
|
96
|
+
}), { data: i } = await C.query({
|
|
86
97
|
query: Ce,
|
|
87
98
|
variables: {
|
|
88
|
-
id:
|
|
89
|
-
expiresIn:
|
|
99
|
+
id: r.id,
|
|
100
|
+
expiresIn: Te
|
|
90
101
|
},
|
|
91
102
|
fetchPolicy: "no-cache"
|
|
92
|
-
}),
|
|
93
|
-
if (!
|
|
103
|
+
}), a = i?.getFileDownloadUrl;
|
|
104
|
+
if (!a) throw Error("No download URL returned");
|
|
94
105
|
R((e) => ({
|
|
95
106
|
...e,
|
|
96
|
-
receiptUrl:
|
|
107
|
+
receiptUrl: a
|
|
97
108
|
})), g({
|
|
98
109
|
title: "Receipt attached",
|
|
99
110
|
variant: "success"
|
|
@@ -106,14 +117,14 @@ function S() {
|
|
|
106
117
|
} finally {
|
|
107
118
|
B(!1);
|
|
108
119
|
}
|
|
109
|
-
},
|
|
120
|
+
}, Z = () => {
|
|
110
121
|
P(!1), I(null);
|
|
111
|
-
},
|
|
122
|
+
}, Q = async () => {
|
|
112
123
|
let e = Number(L.amount);
|
|
113
124
|
if (!L.amount || Number.isNaN(e) || !L.category.trim()) return;
|
|
114
125
|
let t = new Date(L.date).toISOString();
|
|
115
126
|
try {
|
|
116
|
-
F ? (await
|
|
127
|
+
F ? (await Fe({ variables: {
|
|
117
128
|
id: F.id,
|
|
118
129
|
workspaceId: d,
|
|
119
130
|
input: {
|
|
@@ -127,7 +138,7 @@ function S() {
|
|
|
127
138
|
} }), g({
|
|
128
139
|
title: "Expense updated",
|
|
129
140
|
variant: "success"
|
|
130
|
-
})) : (await
|
|
141
|
+
})) : (await Pe({ variables: { input: {
|
|
131
142
|
workspaceId: d,
|
|
132
143
|
amount: e,
|
|
133
144
|
category: L.category.trim(),
|
|
@@ -138,16 +149,16 @@ function S() {
|
|
|
138
149
|
} } }), g({
|
|
139
150
|
title: "Expense added",
|
|
140
151
|
variant: "success"
|
|
141
|
-
})),
|
|
152
|
+
})), Z(), await X();
|
|
142
153
|
} catch {
|
|
143
154
|
g({
|
|
144
155
|
title: F ? "Failed to update expense" : "Failed to add expense",
|
|
145
156
|
variant: "error"
|
|
146
157
|
});
|
|
147
158
|
}
|
|
148
|
-
},
|
|
159
|
+
}, qe = async () => {
|
|
149
160
|
if (U) try {
|
|
150
|
-
await
|
|
161
|
+
await Ie({ variables: {
|
|
151
162
|
id: U.id,
|
|
152
163
|
workspaceId: d
|
|
153
164
|
} }), g({
|
|
@@ -160,10 +171,10 @@ function S() {
|
|
|
160
171
|
variant: "error"
|
|
161
172
|
});
|
|
162
173
|
}
|
|
163
|
-
},
|
|
174
|
+
}, Je = async (e) => {
|
|
164
175
|
Y(e.id);
|
|
165
176
|
try {
|
|
166
|
-
await
|
|
177
|
+
await Le({ variables: {
|
|
167
178
|
id: e.id,
|
|
168
179
|
workspaceId: d
|
|
169
180
|
} }), g({
|
|
@@ -178,10 +189,10 @@ function S() {
|
|
|
178
189
|
} finally {
|
|
179
190
|
Y(null);
|
|
180
191
|
}
|
|
181
|
-
},
|
|
192
|
+
}, Ye = async (e) => {
|
|
182
193
|
Y(e.id);
|
|
183
194
|
try {
|
|
184
|
-
await
|
|
195
|
+
await Re({ variables: {
|
|
185
196
|
id: e.id,
|
|
186
197
|
workspaceId: d
|
|
187
198
|
} }), g({
|
|
@@ -196,9 +207,9 @@ function S() {
|
|
|
196
207
|
} finally {
|
|
197
208
|
Y(null);
|
|
198
209
|
}
|
|
199
|
-
},
|
|
210
|
+
}, Xe = async () => {
|
|
200
211
|
if (G) try {
|
|
201
|
-
await
|
|
212
|
+
await ze({ variables: {
|
|
202
213
|
id: G.id,
|
|
203
214
|
workspaceId: d,
|
|
204
215
|
reason: q.trim() || void 0
|
|
@@ -212,10 +223,10 @@ function S() {
|
|
|
212
223
|
variant: "error"
|
|
213
224
|
});
|
|
214
225
|
}
|
|
215
|
-
},
|
|
226
|
+
}, Ze = async () => {
|
|
216
227
|
N(!0);
|
|
217
228
|
try {
|
|
218
|
-
((await
|
|
229
|
+
((await ke({
|
|
219
230
|
variables: {
|
|
220
231
|
workspaceId: d,
|
|
221
232
|
limit: _,
|
|
@@ -225,12 +236,12 @@ function S() {
|
|
|
225
236
|
...e,
|
|
226
237
|
expenses: [...e.expenses, ...t.expenses]
|
|
227
238
|
} : e
|
|
228
|
-
})).data?.expenses?.length ?? 0) < _ &&
|
|
239
|
+
})).data?.expenses?.length ?? 0) < _ && Ve(!1);
|
|
229
240
|
} finally {
|
|
230
241
|
N(!1);
|
|
231
242
|
}
|
|
232
243
|
};
|
|
233
|
-
if (w &&
|
|
244
|
+
if (w && je && A.length === 0) return /* @__PURE__ */ p(o, {
|
|
234
245
|
title: "Expense Tracking",
|
|
235
246
|
description: "Track and manage work expenses",
|
|
236
247
|
children: /* @__PURE__ */ p(i, {
|
|
@@ -238,7 +249,7 @@ function S() {
|
|
|
238
249
|
count: 4
|
|
239
250
|
})
|
|
240
251
|
});
|
|
241
|
-
if (
|
|
252
|
+
if (Oe && Me) return /* @__PURE__ */ p(o, {
|
|
242
253
|
title: "Expense Tracking",
|
|
243
254
|
description: "Track and manage work expenses",
|
|
244
255
|
children: /* @__PURE__ */ p("div", {
|
|
@@ -247,7 +258,7 @@ function S() {
|
|
|
247
258
|
})
|
|
248
259
|
});
|
|
249
260
|
let $ = /* @__PURE__ */ m("button", {
|
|
250
|
-
onClick:
|
|
261
|
+
onClick: We,
|
|
251
262
|
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",
|
|
252
263
|
children: [/* @__PURE__ */ p(ge, { className: "h-4 w-4" }), "Add Expense"]
|
|
253
264
|
});
|
|
@@ -294,7 +305,7 @@ function S() {
|
|
|
294
305
|
children: /* @__PURE__ */ p("div", {
|
|
295
306
|
className: "divide-y divide-border",
|
|
296
307
|
children: A.map((e) => {
|
|
297
|
-
let r =
|
|
308
|
+
let r = Ue === e.id, i = e.status === s.Pending, a = e.status === s.Rejected;
|
|
298
309
|
return /* @__PURE__ */ m("div", {
|
|
299
310
|
className: "flex flex-col gap-3 p-4 hover:bg-bg-sunken/50 transition-colors sm:flex-row sm:items-center sm:justify-between",
|
|
300
311
|
children: [/* @__PURE__ */ m("div", {
|
|
@@ -331,7 +342,7 @@ function S() {
|
|
|
331
342
|
i && /* @__PURE__ */ m(f, { children: [/* @__PURE__ */ p("button", {
|
|
332
343
|
type: "button",
|
|
333
344
|
disabled: r,
|
|
334
|
-
onClick: () =>
|
|
345
|
+
onClick: () => Ye(e),
|
|
335
346
|
title: "Approve expense",
|
|
336
347
|
className: "rounded-md p-1.5 text-status-success-text hover:bg-status-success-bg disabled:opacity-50",
|
|
337
348
|
children: /* @__PURE__ */ p(de, { className: "h-4 w-4" })
|
|
@@ -348,7 +359,7 @@ function S() {
|
|
|
348
359
|
a && /* @__PURE__ */ p("button", {
|
|
349
360
|
type: "button",
|
|
350
361
|
disabled: r,
|
|
351
|
-
onClick: () =>
|
|
362
|
+
onClick: () => Je(e),
|
|
352
363
|
title: "Resubmit for approval",
|
|
353
364
|
className: "rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken disabled:opacity-50",
|
|
354
365
|
children: /* @__PURE__ */ p(_e, { className: "h-4 w-4" })
|
|
@@ -362,7 +373,7 @@ function S() {
|
|
|
362
373
|
}),
|
|
363
374
|
/* @__PURE__ */ p("button", {
|
|
364
375
|
type: "button",
|
|
365
|
-
onClick: () =>
|
|
376
|
+
onClick: () => Ge(e),
|
|
366
377
|
title: "Edit expense",
|
|
367
378
|
className: "rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken",
|
|
368
379
|
children: /* @__PURE__ */ p(he, { className: "h-4 w-4" })
|
|
@@ -381,26 +392,26 @@ function S() {
|
|
|
381
392
|
}, e.id);
|
|
382
393
|
})
|
|
383
394
|
})
|
|
384
|
-
}),
|
|
395
|
+
}), Be && A.length >= _ && /* @__PURE__ */ p("div", {
|
|
385
396
|
className: "mt-4 flex justify-center",
|
|
386
397
|
children: /* @__PURE__ */ p("button", {
|
|
387
|
-
onClick:
|
|
398
|
+
onClick: Ze,
|
|
388
399
|
disabled: M,
|
|
389
400
|
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",
|
|
390
401
|
children: M ? "Loading..." : "Load more"
|
|
391
402
|
})
|
|
392
403
|
})] }),
|
|
393
404
|
/* @__PURE__ */ p(c, {
|
|
394
|
-
open:
|
|
395
|
-
onClose:
|
|
405
|
+
open: He,
|
|
406
|
+
onClose: Z,
|
|
396
407
|
title: F ? "Edit Expense" : "Add Expense",
|
|
397
408
|
description: F ? "Update this expense's details." : "Record a project-related cost to track alongside your logged hours.",
|
|
398
409
|
footer: /* @__PURE__ */ m(f, { children: [/* @__PURE__ */ p("button", {
|
|
399
|
-
onClick:
|
|
410
|
+
onClick: Z,
|
|
400
411
|
className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50",
|
|
401
412
|
children: "Cancel"
|
|
402
413
|
}), /* @__PURE__ */ p("button", {
|
|
403
|
-
onClick:
|
|
414
|
+
onClick: Q,
|
|
404
415
|
disabled: E || D || !L.amount || Number.isNaN(Number(L.amount)) || !L.category.trim(),
|
|
405
416
|
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",
|
|
406
417
|
children: E || D ? "Saving..." : F ? "Save Changes" : "Add Expense"
|
|
@@ -498,7 +509,7 @@ function S() {
|
|
|
498
509
|
className: "hidden",
|
|
499
510
|
onChange: (e) => {
|
|
500
511
|
let t = e.target.files?.[0];
|
|
501
|
-
e.target.value = "", t &&
|
|
512
|
+
e.target.value = "", t && Ke(t);
|
|
502
513
|
}
|
|
503
514
|
})
|
|
504
515
|
]
|
|
@@ -530,7 +541,7 @@ function S() {
|
|
|
530
541
|
className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50",
|
|
531
542
|
children: "Cancel"
|
|
532
543
|
}), /* @__PURE__ */ p("button", {
|
|
533
|
-
onClick:
|
|
544
|
+
onClick: qe,
|
|
534
545
|
disabled: O,
|
|
535
546
|
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",
|
|
536
547
|
children: O ? "Deleting..." : "Delete"
|
|
@@ -551,7 +562,7 @@ function S() {
|
|
|
551
562
|
className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50",
|
|
552
563
|
children: "Cancel"
|
|
553
564
|
}), /* @__PURE__ */ p("button", {
|
|
554
|
-
onClick:
|
|
565
|
+
onClick: Xe,
|
|
555
566
|
disabled: k,
|
|
556
567
|
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",
|
|
557
568
|
children: k ? "Rejecting..." : "Reject Expense"
|
|
@@ -572,7 +583,7 @@ function S() {
|
|
|
572
583
|
onClose: () => H(null),
|
|
573
584
|
title: "Expense review",
|
|
574
585
|
description: V ? `Discuss ${V.description ?? "this expense"} with your team.` : void 0,
|
|
575
|
-
children: V && /* @__PURE__ */ p(
|
|
586
|
+
children: V && /* @__PURE__ */ p(Ee, { expense: V })
|
|
576
587
|
}),
|
|
577
588
|
/* @__PURE__ */ p(le, {
|
|
578
589
|
toasts: h,
|