@burdenoff/microfe-timecampus 2026.830.2 → 2026.903.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/timecampus/components/index.js +4 -4
- package/dist/timecampus/pages/HomePage.d.ts.map +1 -1
- package/dist/timecampus/pages/HomePage.js +142 -131
- package/dist/timecampus/pages/HomePage.js.map +1 -1
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +7 -7
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js +7 -7
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
- package/dist/timecampus/pages/dashboard/ReportsPage.js +13 -13
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +7 -7
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +7 -7
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js +13 -13
- package/dist/timecampus/pages/geofencing/GeofencingPage.js +135 -135
- package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
- package/dist/timecampus/pages/invoicing/InvoicingPage.js +7 -7
- package/dist/timecampus/pages/invoicing/InvoicingPage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/DeadTimeRadarPage.js +8 -8
- package/dist/timecampus/pages/lifetime/FiniteLifePage.js +4 -4
- package/dist/timecampus/pages/lifetime/FiniteLifePage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/LifeBalancePage.js +104 -104
- package/dist/timecampus/pages/lifetime/LifeBalancePage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/LifeTimelinePage.js +7 -7
- package/dist/timecampus/pages/lifetime/LifeTimelinePage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/PersonalCorporatePage.js +33 -33
- package/dist/timecampus/pages/lifetime/PersonalCorporatePage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/RecommendationsPage.js +80 -80
- package/dist/timecampus/pages/lifetime/RecommendationsPage.js.map +1 -1
- package/dist/timecampus/pages/lifetime/TimeWellSpentPage.js +16 -16
- package/dist/timecampus/pages/lifetime/ValuesGoalsPage.js +11 -11
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +11 -11
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingDetailPage.js +14 -14
- package/dist/timecampus/pages/meetings/MeetingDetailPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.js +12 -12
- package/dist/timecampus/pages/projects/ProjectsPage.js +5 -5
- package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/TasksPage.js +8 -8
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +8 -8
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js +8 -8
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js +13 -13
- package/dist/timecampus/pages/time/TimeEntriesPage.js +5 -5
- package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.js +138 -138
- package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.js +14 -14
- package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +26 -26
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
- package/dist/timecampus/pages/timeline/TimelinePage.js +8 -8
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +79 -79
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GeofencingPage.js","names":[],"sources":["../../../../src/timecampus/pages/geofencing/GeofencingPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n MapPin,\n Plus,\n Pencil,\n Trash2,\n LogIn,\n LogOut,\n Radio,\n ShieldCheck,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n ChartCard,\n DonutChart,\n BarsChart,\n type ChartDatum,\n} from \"../../components\";\nimport { cn, formatRelativeTime } from \"../../utils/helpers\";\nimport {\n useListGeofencesQuery,\n useCreateGeofenceMutation,\n useUpdateGeofenceMutation,\n useDeleteGeofenceMutation,\n useToggleGeofenceMutation,\n type ListGeofencesQuery,\n} from \"@/generated/wspace-operations\";\nimport { GeofenceTrigger, GeofenceEventType } from \"@/generated/wspace-types\";\n\ntype GeofenceItem = NonNullable<\n ListGeofencesQuery[\"geofences\"]\n>[\"items\"][number];\n\nconst PAGE_SIZE = 25;\n\ninterface GeofenceFormState {\n name: string;\n latitude: string;\n longitude: string;\n radius: string;\n trigger: GeofenceTrigger;\n address: string;\n isActive: boolean;\n}\n\nconst emptyForm: GeofenceFormState = {\n name: \"\",\n latitude: \"\",\n longitude: \"\",\n radius: \"100\",\n trigger: GeofenceTrigger.Both,\n address: \"\",\n isActive: true,\n};\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\ninterface ToggleSwitchProps {\n checked: boolean;\n onChange: (next: boolean) => void;\n label: string;\n disabled?: boolean;\n}\n\n/**\n * Local, token-styled switch (shared components are off-limits here). The knob\n * translates only on the checked state — never on hover — per the repo\n * no-hover-movement mandate.\n */\nfunction ToggleSwitch({\n checked,\n onChange,\n label,\n disabled,\n}: ToggleSwitchProps) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={label}\n disabled={disabled}\n onClick={() => onChange(!checked)}\n className={cn(\n \"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border transition-colors disabled:cursor-not-allowed disabled:opacity-50\",\n checked\n ? \"border-transparent bg-action-primary-bg\"\n : \"border-border-subtle bg-bg-sunken\",\n )}\n >\n <span\n className={cn(\n \"inline-block h-3.5 w-3.5 transform rounded-full shadow-sm transition-transform\",\n checked\n ? \"translate-x-4 bg-action-primary-text\"\n : \"translate-x-0.5 bg-text-secondary\",\n )}\n />\n </button>\n );\n}\n\nexport function GeofencingPage() {\n const { data, loading, error, refetch, fetchMore } = useListGeofencesQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n });\n\n const [createGeofence, { loading: creating, error: createError }] =\n useCreateGeofenceMutation();\n const [updateGeofence, { loading: updating, error: updateError }] =\n useUpdateGeofenceMutation();\n const [deleteGeofence, { loading: deleting }] = useDeleteGeofenceMutation();\n const [toggleGeofence] = useToggleGeofenceMutation();\n\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<GeofenceItem | null>(null);\n const [form, setForm] = useState<GeofenceFormState>(emptyForm);\n const [confirmDelete, setConfirmDelete] = useState<GeofenceItem | null>(null);\n const [loadingMore, setLoadingMore] = useState(false);\n\n const geofences = data?.geofences?.items ?? [];\n const hasMore = data?.geofences?.hasMore ?? false;\n const totalZones = data?.geofences?.total ?? geofences.length;\n\n // ---- Derived, real-data summaries ---------------------------------------\n const activeZones = geofences.filter((f: GeofenceItem) => f.isActive).length;\n let enterEvents = 0;\n let exitEvents = 0;\n for (const fence of geofences as GeofenceItem[]) {\n for (const event of fence.events) {\n if (event.type === GeofenceEventType.Enter) enterEvents += 1;\n else exitEvents += 1;\n }\n }\n\n const triggerSlices = [\n {\n name: \"Entry & exit\",\n value: geofences.filter(\n (f: GeofenceItem) => f.trigger === GeofenceTrigger.Both,\n ).length,\n },\n {\n name: \"On entry\",\n value: geofences.filter(\n (f: GeofenceItem) => f.trigger === GeofenceTrigger.Enter,\n ).length,\n },\n {\n name: \"On exit\",\n value: geofences.filter(\n (f: GeofenceItem) => f.trigger === GeofenceTrigger.Exit,\n ).length,\n },\n ];\n\n const activityData: ChartDatum[] = geofences\n .map((f: GeofenceItem) => ({ zone: f.name, events: f.events.length }))\n .filter((d: { zone: string; events: number }) => d.events > 0)\n .sort((a: { events: number }, b: { events: number }) => b.events - a.events)\n .slice(0, 8);\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (fence: GeofenceItem) => {\n setEditing(fence);\n setForm({\n name: fence.name,\n latitude: String(fence.latitude),\n longitude: String(fence.longitude),\n radius: String(fence.radius),\n trigger: fence.trigger,\n address: fence.address ?? \"\",\n isActive: fence.isActive,\n });\n setFormOpen(true);\n };\n\n const handleSubmit = async () => {\n if (!form.name.trim()) return;\n const base = {\n name: form.name.trim(),\n latitude: Number(form.latitude),\n longitude: Number(form.longitude),\n radius: Number(form.radius),\n trigger: form.trigger,\n address: form.address.trim() || undefined,\n isActive: form.isActive,\n };\n if (editing) {\n await updateGeofence({ variables: { id: editing.id, input: base } });\n } else {\n await createGeofence({ variables: { input: base } });\n }\n setFormOpen(false);\n await refetch();\n };\n\n const handleDelete = async () => {\n if (!confirmDelete) return;\n await deleteGeofence({ variables: { id: confirmDelete.id } });\n setConfirmDelete(null);\n await refetch();\n };\n\n const handleToggle = async (fence: GeofenceItem) => {\n await toggleGeofence({\n variables: { id: fence.id, isActive: !fence.isActive },\n });\n await refetch();\n };\n\n /**\n * Enter/exit are edges of the single `trigger` enum. Recompute the enum from\n * the two switches and persist via updateGeofence. A zone must keep at least\n * one edge, so a change that would disable both is ignored.\n */\n const handleSetTrigger = async (\n fence: GeofenceItem,\n nextEnter: boolean,\n nextExit: boolean,\n ) => {\n if (!nextEnter && !nextExit) return;\n const trigger =\n nextEnter && nextExit\n ? GeofenceTrigger.Both\n : nextEnter\n ? GeofenceTrigger.Enter\n : GeofenceTrigger.Exit;\n if (trigger === fence.trigger) return;\n await updateGeofence({ variables: { id: fence.id, input: { trigger } } });\n await refetch();\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: {\n pagination: { limit: PAGE_SIZE, offset: geofences.length },\n },\n updateQuery: (\n prev: ListGeofencesQuery,\n { fetchMoreResult }: { fetchMoreResult?: ListGeofencesQuery },\n ) => {\n if (!fetchMoreResult) return prev;\n return {\n geofences: {\n ...fetchMoreResult.geofences,\n items: [\n ...prev.geofences.items,\n ...fetchMoreResult.geofences.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n const createButton = (\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 Create Geofence\n </button>\n );\n\n if (loading) {\n return (\n <PageLayout\n title=\"Geofencing\"\n description=\"Set up location-based triggers and zones\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Geofencing\"\n description=\"Set up location-based triggers and zones\"\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 geofences. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Geofencing\"\n description=\"Set up location-based triggers and zones\"\n purpose=\"Draw zones around your worksites — offices, client sites, job locations — and have time tracking start and stop automatically as people move in and out, so on-site hours record themselves.\"\n actions={createButton}\n >\n {geofences.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No geofences configured yet\"\n description=\"Create your first zone to trigger automatic time tracking when people arrive or leave a location.\"\n action={createButton}\n />\n ) : (\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 Zones\"\n value={String(totalZones)}\n icon={<MapPin className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Active Zones\"\n value={String(activeZones)}\n icon={<Radio className=\"h-5 w-5\" />}\n description=\"Currently tracking\"\n />\n <StatCard\n title=\"Entry Events\"\n value={String(enterEvents)}\n icon={<LogIn className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Exit Events\"\n value={String(exitEvents)}\n icon={<LogOut className=\"h-5 w-5\" />}\n />\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title=\"Trigger configuration\"\n description=\"How your zones fire\"\n >\n <DonutChart\n data={triggerSlices}\n centerLabel=\"Zones\"\n centerValue={geofences.length}\n ariaLabel=\"Geofence trigger configuration mix\"\n emptyLabel=\"No zones to summarize yet\"\n />\n </ChartCard>\n <ChartCard\n title=\"Activity by zone\"\n description=\"Recorded enter/exit events per zone\"\n >\n <BarsChart\n data={activityData}\n xKey=\"zone\"\n orientation=\"horizontal\"\n series={[{ key: \"events\", label: \"Events\" }]}\n categoryWidth={120}\n ariaLabel=\"Recorded events by zone\"\n emptyLabel=\"No geofence events recorded yet\"\n />\n </ChartCard>\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3\">\n {geofences.map((fence: GeofenceItem) => {\n const enterOn =\n fence.trigger === GeofenceTrigger.Enter ||\n fence.trigger === GeofenceTrigger.Both;\n const exitOn =\n fence.trigger === GeofenceTrigger.Exit ||\n fence.trigger === GeofenceTrigger.Both;\n const fenceEnter = fence.events.filter(\n (e) => e.type === GeofenceEventType.Enter,\n ).length;\n const fenceExit = fence.events.length - fenceEnter;\n const lastTs = fence.events.reduce(\n (latest, e) => (e.timestamp > latest ? e.timestamp : latest),\n \"\",\n );\n return (\n <GlassCard\n key={fence.id}\n treatment=\"glass\"\n glow={fence.isActive}\n className=\"flex flex-col p-5\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-start gap-2\">\n <span className=\"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <MapPin className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {fence.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {fence.address ?? \"No address\"} ·{\" \"}\n {fence.radius}m radius\n </p>\n </div>\n </div>\n <div className=\"flex shrink-0 flex-col items-end gap-1\">\n <ToggleSwitch\n checked={fence.isActive}\n onChange={() => handleToggle(fence)}\n label={`Toggle ${fence.name} active`}\n />\n <span\n className={cn(\n \"text-xs font-medium\",\n fence.isActive\n ? \"text-status-success-text\"\n : \"text-text-muted\",\n )}\n >\n {fence.isActive ? \"Active\" : \"Inactive\"}\n </span>\n </div>\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-2\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle bg-bg-surface/60 px-3 py-2\">\n <span className=\"flex items-center gap-1.5 text-xs font-medium text-text-secondary\">\n <LogIn className=\"h-3.5 w-3.5\" />\n Enter\n </span>\n <ToggleSwitch\n checked={enterOn}\n onChange={(next) =>\n handleSetTrigger(fence, next, exitOn)\n }\n label={`Enter trigger for ${fence.name}`}\n />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle bg-bg-surface/60 px-3 py-2\">\n <span className=\"flex items-center gap-1.5 text-xs font-medium text-text-secondary\">\n <LogOut className=\"h-3.5 w-3.5\" />\n Exit\n </span>\n <ToggleSwitch\n checked={exitOn}\n onChange={(next) =>\n handleSetTrigger(fence, enterOn, next)\n }\n label={`Exit trigger for ${fence.name}`}\n />\n </div>\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <span>\n Entries{\" \"}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {fenceEnter}\n </span>\n </span>\n <span>\n Exits{\" \"}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {fenceExit}\n </span>\n </span>\n {lastTs ? (\n <span className=\"ml-auto\">\n Last{\" \"}\n <span className=\"text-text-primary\">\n {formatRelativeTime(lastTs)}\n </span>\n </span>\n ) : (\n <span className=\"ml-auto text-text-muted\">\n No events yet\n </span>\n )}\n </div>\n\n <div className=\"mt-4 flex items-center gap-2 border-t border-border-seam pt-3\">\n <button\n onClick={() => openEdit(fence)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n Edit\n </button>\n <button\n onClick={() => setConfirmDelete(fence)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n Delete\n </button>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n <GlassCard treatment=\"insight\" className=\"mt-6 p-4\">\n <div className=\"flex items-start gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <ShieldCheck className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n Privacy first\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Time is recorded only when someone enters or leaves a zone you\n define — never through continuous background tracking.\n Location data stays encrypted and visible only within your\n workspace.\n </p>\n </div>\n </div>\n </GlassCard>\n\n {hasMore && (\n <div className=\"mt-6 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={() => setFormOpen(false)}\n title={editing ? \"Edit Geofence\" : \"Create Geofence\"}\n description=\"Define a zone that triggers automatic time tracking.\"\n size=\"lg\"\n footer={\n <>\n <button\n onClick={() => setFormOpen(false)}\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.name.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 : \"Create Geofence\"}\n </button>\n </>\n }\n >\n {(createError || updateError) && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to save geofence. Please check the values and try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Head Office\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: 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 Latitude\n </label>\n <input\n type=\"number\"\n step=\"any\"\n placeholder=\"0.000000\"\n value={form.latitude}\n onChange={(e) => setForm({ ...form, latitude: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Longitude\n </label>\n <input\n type=\"number\"\n step=\"any\"\n placeholder=\"0.000000\"\n value={form.longitude}\n onChange={(e) =>\n setForm({ ...form, longitude: 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 Radius (meters)\n </label>\n <input\n type=\"number\"\n placeholder=\"100\"\n value={form.radius}\n onChange={(e) => setForm({ ...form, radius: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Trigger\n </label>\n <select\n value={form.trigger}\n onChange={(e) =>\n setForm({\n ...form,\n trigger: e.target.value as GeofenceTrigger,\n })\n }\n className={inputClass}\n >\n <option value={GeofenceTrigger.Enter}>On entry</option>\n <option value={GeofenceTrigger.Exit}>On exit</option>\n <option value={GeofenceTrigger.Both}>Entry & exit</option>\n </select>\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Address (optional)\n </label>\n <input\n type=\"text\"\n placeholder=\"Street address for reference\"\n value={form.address}\n onChange={(e) => setForm({ ...form, address: e.target.value })}\n className={inputClass}\n />\n </div>\n <label className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">\n Active\n </span>\n <input\n type=\"checkbox\"\n checked={form.isActive}\n onChange={(e) => setForm({ ...form, isActive: e.target.checked })}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n </label>\n </div>\n </Modal>\n\n <Modal\n open={confirmDelete !== null}\n onClose={() => setConfirmDelete(null)}\n title=\"Delete Geofence\"\n description=\"This action cannot be undone.\"\n footer={\n <>\n <button\n onClick={() => setConfirmDelete(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 Are you sure you want to delete{\" \"}\n <span className=\"font-medium text-text-primary\">\n {confirmDelete?.name}\n </span>\n ? Any automatic tracking tied to this zone will stop.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GeofencingPage.js","names":[],"sources":["../../../../src/timecampus/pages/geofencing/GeofencingPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n MapPin,\n Plus,\n Pencil,\n Trash2,\n LogIn,\n LogOut,\n Radio,\n ShieldCheck,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n ChartCard,\n DonutChart,\n BarsChart,\n type ChartDatum,\n} from \"../../components\";\nimport { cn, formatRelativeTime } from \"../../utils/helpers\";\nimport {\n useListGeofencesQuery,\n useCreateGeofenceMutation,\n useUpdateGeofenceMutation,\n useDeleteGeofenceMutation,\n useToggleGeofenceMutation,\n type ListGeofencesQuery,\n} from \"@/generated/wspace-operations\";\nimport { GeofenceTrigger, GeofenceEventType } from \"@/generated/wspace-types\";\n\ntype GeofenceItem = NonNullable<\n ListGeofencesQuery[\"geofences\"]\n>[\"items\"][number];\n\nconst PAGE_SIZE = 25;\n\ninterface GeofenceFormState {\n name: string;\n latitude: string;\n longitude: string;\n radius: string;\n trigger: GeofenceTrigger;\n address: string;\n isActive: boolean;\n}\n\nconst emptyForm: GeofenceFormState = {\n name: \"\",\n latitude: \"\",\n longitude: \"\",\n radius: \"100\",\n trigger: GeofenceTrigger.Both,\n address: \"\",\n isActive: true,\n};\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\ninterface ToggleSwitchProps {\n checked: boolean;\n onChange: (next: boolean) => void;\n label: string;\n disabled?: boolean;\n}\n\n/**\n * Local, token-styled switch (shared components are off-limits here). The knob\n * translates only on the checked state — never on hover — per the repo\n * no-hover-movement mandate.\n */\nfunction ToggleSwitch({\n checked,\n onChange,\n label,\n disabled,\n}: ToggleSwitchProps) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={label}\n disabled={disabled}\n onClick={() => onChange(!checked)}\n className={cn(\n \"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border transition-colors disabled:cursor-not-allowed disabled:opacity-50\",\n checked\n ? \"border-transparent bg-action-primary-bg\"\n : \"border-border-subtle bg-bg-sunken\",\n )}\n >\n <span\n className={cn(\n \"inline-block h-3.5 w-3.5 transform rounded-full shadow-sm transition-transform\",\n checked\n ? \"translate-x-4 bg-action-primary-text\"\n : \"translate-x-0.5 bg-text-secondary\",\n )}\n />\n </button>\n );\n}\n\nexport function GeofencingPage() {\n const { data, loading, error, refetch, fetchMore } = useListGeofencesQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n });\n\n const [createGeofence, { loading: creating, error: createError }] =\n useCreateGeofenceMutation();\n const [updateGeofence, { loading: updating, error: updateError }] =\n useUpdateGeofenceMutation();\n const [deleteGeofence, { loading: deleting }] = useDeleteGeofenceMutation();\n const [toggleGeofence] = useToggleGeofenceMutation();\n\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<GeofenceItem | null>(null);\n const [form, setForm] = useState<GeofenceFormState>(emptyForm);\n const [confirmDelete, setConfirmDelete] = useState<GeofenceItem | null>(null);\n const [loadingMore, setLoadingMore] = useState(false);\n\n const geofences = data?.geofences?.items ?? [];\n const hasMore = data?.geofences?.hasMore ?? false;\n const totalZones = data?.geofences?.total ?? geofences.length;\n\n // ---- Derived, real-data summaries ---------------------------------------\n const activeZones = geofences.filter((f: GeofenceItem) => f.isActive).length;\n let enterEvents = 0;\n let exitEvents = 0;\n for (const fence of geofences as GeofenceItem[]) {\n for (const event of fence.events) {\n if (event.type === GeofenceEventType.Enter) enterEvents += 1;\n else exitEvents += 1;\n }\n }\n\n const triggerSlices = [\n {\n name: \"Entry & exit\",\n value: geofences.filter(\n (f: GeofenceItem) => f.trigger === GeofenceTrigger.Both,\n ).length,\n },\n {\n name: \"On entry\",\n value: geofences.filter(\n (f: GeofenceItem) => f.trigger === GeofenceTrigger.Enter,\n ).length,\n },\n {\n name: \"On exit\",\n value: geofences.filter(\n (f: GeofenceItem) => f.trigger === GeofenceTrigger.Exit,\n ).length,\n },\n ];\n\n const activityData: ChartDatum[] = geofences\n .map((f: GeofenceItem) => ({ zone: f.name, events: f.events.length }))\n .filter((d: { zone: string; events: number }) => d.events > 0)\n .sort((a: { events: number }, b: { events: number }) => b.events - a.events)\n .slice(0, 8);\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (fence: GeofenceItem) => {\n setEditing(fence);\n setForm({\n name: fence.name,\n latitude: String(fence.latitude),\n longitude: String(fence.longitude),\n radius: String(fence.radius),\n trigger: fence.trigger,\n address: fence.address ?? \"\",\n isActive: fence.isActive,\n });\n setFormOpen(true);\n };\n\n const handleSubmit = async () => {\n if (!form.name.trim()) return;\n const base = {\n name: form.name.trim(),\n latitude: Number(form.latitude),\n longitude: Number(form.longitude),\n radius: Number(form.radius),\n trigger: form.trigger,\n address: form.address.trim() || undefined,\n isActive: form.isActive,\n };\n if (editing) {\n await updateGeofence({ variables: { id: editing.id, input: base } });\n } else {\n await createGeofence({ variables: { input: base } });\n }\n setFormOpen(false);\n await refetch();\n };\n\n const handleDelete = async () => {\n if (!confirmDelete) return;\n await deleteGeofence({ variables: { id: confirmDelete.id } });\n setConfirmDelete(null);\n await refetch();\n };\n\n const handleToggle = async (fence: GeofenceItem) => {\n await toggleGeofence({\n variables: { id: fence.id, isActive: !fence.isActive },\n });\n await refetch();\n };\n\n /**\n * Enter/exit are edges of the single `trigger` enum. Recompute the enum from\n * the two switches and persist via updateGeofence. A zone must keep at least\n * one edge, so a change that would disable both is ignored.\n */\n const handleSetTrigger = async (\n fence: GeofenceItem,\n nextEnter: boolean,\n nextExit: boolean,\n ) => {\n if (!nextEnter && !nextExit) return;\n const trigger =\n nextEnter && nextExit\n ? GeofenceTrigger.Both\n : nextEnter\n ? GeofenceTrigger.Enter\n : GeofenceTrigger.Exit;\n if (trigger === fence.trigger) return;\n await updateGeofence({ variables: { id: fence.id, input: { trigger } } });\n await refetch();\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: {\n pagination: { limit: PAGE_SIZE, offset: geofences.length },\n },\n updateQuery: (\n prev: ListGeofencesQuery,\n { fetchMoreResult }: { fetchMoreResult?: ListGeofencesQuery },\n ) => {\n if (!fetchMoreResult) return prev;\n return {\n geofences: {\n ...fetchMoreResult.geofences,\n items: [\n ...prev.geofences.items,\n ...fetchMoreResult.geofences.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n const createButton = (\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 Create Geofence\n </button>\n );\n\n if (loading) {\n return (\n <PageLayout\n title=\"Geofencing\"\n description=\"Set up location-based triggers and zones\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Geofencing\"\n description=\"Set up location-based triggers and zones\"\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 geofences. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Geofencing\"\n description=\"Set up location-based triggers and zones\"\n purpose=\"Draw zones around your worksites — offices, client sites, job locations — and have time tracking start and stop automatically as people move in and out, so on-site hours record themselves.\"\n actions={createButton}\n >\n {geofences.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No geofences configured yet\"\n description=\"Create your first zone to trigger automatic time tracking when people arrive or leave a location.\"\n action={createButton}\n />\n ) : (\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 Zones\"\n value={String(totalZones)}\n icon={<MapPin className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Active Zones\"\n value={String(activeZones)}\n icon={<Radio className=\"h-5 w-5\" />}\n description=\"Currently tracking\"\n />\n <StatCard\n title=\"Entry Events\"\n value={String(enterEvents)}\n icon={<LogIn className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Exit Events\"\n value={String(exitEvents)}\n icon={<LogOut className=\"h-5 w-5\" />}\n />\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title=\"Trigger configuration\"\n description=\"How your zones fire\"\n >\n <DonutChart\n data={triggerSlices}\n centerLabel=\"Zones\"\n centerValue={geofences.length}\n ariaLabel=\"Geofence trigger configuration mix\"\n emptyLabel=\"No zones to summarize yet\"\n />\n </ChartCard>\n <ChartCard\n title=\"Activity by zone\"\n description=\"Recorded enter/exit events per zone\"\n >\n <BarsChart\n data={activityData}\n xKey=\"zone\"\n orientation=\"horizontal\"\n series={[{ key: \"events\", label: \"Events\" }]}\n categoryWidth={120}\n ariaLabel=\"Recorded events by zone\"\n emptyLabel=\"No geofence events recorded yet\"\n />\n </ChartCard>\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3\">\n {geofences.map((fence: GeofenceItem) => {\n const enterOn =\n fence.trigger === GeofenceTrigger.Enter ||\n fence.trigger === GeofenceTrigger.Both;\n const exitOn =\n fence.trigger === GeofenceTrigger.Exit ||\n fence.trigger === GeofenceTrigger.Both;\n const fenceEnter = fence.events.filter(\n (e) => e.type === GeofenceEventType.Enter,\n ).length;\n const fenceExit = fence.events.length - fenceEnter;\n const lastTs = fence.events.reduce(\n (latest, e) => (e.timestamp > latest ? e.timestamp : latest),\n \"\",\n );\n return (\n <GlassCard\n key={fence.id}\n treatment=\"glass\"\n glow={fence.isActive}\n className=\"flex flex-col p-5\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-start gap-2\">\n <span className=\"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <MapPin className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {fence.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {fence.address ?? \"No address\"} ·{\" \"}\n {fence.radius}m radius\n </p>\n </div>\n </div>\n <div className=\"flex shrink-0 flex-col items-end gap-1\">\n <ToggleSwitch\n checked={fence.isActive}\n onChange={() => handleToggle(fence)}\n label={`Toggle ${fence.name} active`}\n />\n <span\n className={cn(\n \"text-xs font-medium\",\n fence.isActive\n ? \"text-status-success-text\"\n : \"text-text-muted\",\n )}\n >\n {fence.isActive ? \"Active\" : \"Inactive\"}\n </span>\n </div>\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-2\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle bg-bg-surface/60 px-3 py-2\">\n <span className=\"flex items-center gap-1.5 text-xs font-medium text-text-secondary\">\n <LogIn className=\"h-3.5 w-3.5\" />\n Enter\n </span>\n <ToggleSwitch\n checked={enterOn}\n onChange={(next) =>\n handleSetTrigger(fence, next, exitOn)\n }\n label={`Enter trigger for ${fence.name}`}\n />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-subtle bg-bg-surface/60 px-3 py-2\">\n <span className=\"flex items-center gap-1.5 text-xs font-medium text-text-secondary\">\n <LogOut className=\"h-3.5 w-3.5\" />\n Exit\n </span>\n <ToggleSwitch\n checked={exitOn}\n onChange={(next) =>\n handleSetTrigger(fence, enterOn, next)\n }\n label={`Exit trigger for ${fence.name}`}\n />\n </div>\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <span>\n Entries{\" \"}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {fenceEnter}\n </span>\n </span>\n <span>\n Exits{\" \"}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {fenceExit}\n </span>\n </span>\n {lastTs ? (\n <span className=\"ml-auto\">\n Last{\" \"}\n <span className=\"text-text-primary\">\n {formatRelativeTime(lastTs)}\n </span>\n </span>\n ) : (\n <span className=\"ml-auto text-text-muted\">\n No events yet\n </span>\n )}\n </div>\n\n <div className=\"mt-4 flex items-center gap-2 border-t border-border-seam pt-3\">\n <button\n onClick={() => openEdit(fence)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n Edit\n </button>\n <button\n onClick={() => setConfirmDelete(fence)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n Delete\n </button>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n <GlassCard treatment=\"insight\" className=\"mt-6 p-4\">\n <div className=\"flex items-start gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <ShieldCheck className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n Privacy first\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Time is recorded only when someone enters or leaves a zone you\n define — never through continuous background tracking.\n Location data stays encrypted and visible only within your\n workspace.\n </p>\n </div>\n </div>\n </GlassCard>\n\n {hasMore && (\n <div className=\"mt-6 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={() => setFormOpen(false)}\n title={editing ? \"Edit Geofence\" : \"Create Geofence\"}\n description=\"Define a zone that triggers automatic time tracking.\"\n size=\"lg\"\n footer={\n <>\n <button\n onClick={() => setFormOpen(false)}\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.name.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 : \"Create Geofence\"}\n </button>\n </>\n }\n >\n {(createError || updateError) && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to save geofence. Please check the values and try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Head Office\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: 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 Latitude\n </label>\n <input\n type=\"number\"\n step=\"any\"\n placeholder=\"0.000000\"\n value={form.latitude}\n onChange={(e) => setForm({ ...form, latitude: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Longitude\n </label>\n <input\n type=\"number\"\n step=\"any\"\n placeholder=\"0.000000\"\n value={form.longitude}\n onChange={(e) =>\n setForm({ ...form, longitude: 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 Radius (meters)\n </label>\n <input\n type=\"number\"\n placeholder=\"100\"\n value={form.radius}\n onChange={(e) => setForm({ ...form, radius: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Trigger\n </label>\n <select\n value={form.trigger}\n onChange={(e) =>\n setForm({\n ...form,\n trigger: e.target.value as GeofenceTrigger,\n })\n }\n className={inputClass}\n >\n <option value={GeofenceTrigger.Enter}>On entry</option>\n <option value={GeofenceTrigger.Exit}>On exit</option>\n <option value={GeofenceTrigger.Both}>Entry & exit</option>\n </select>\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Address (optional)\n </label>\n <input\n type=\"text\"\n placeholder=\"Street address for reference\"\n value={form.address}\n onChange={(e) => setForm({ ...form, address: e.target.value })}\n className={inputClass}\n />\n </div>\n <label className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">\n Active\n </span>\n <input\n type=\"checkbox\"\n checked={form.isActive}\n onChange={(e) => setForm({ ...form, isActive: e.target.checked })}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n </label>\n </div>\n </Modal>\n\n <Modal\n open={confirmDelete !== null}\n onClose={() => setConfirmDelete(null)}\n title=\"Delete Geofence\"\n description=\"This action cannot be undone.\"\n footer={\n <>\n <button\n onClick={() => setConfirmDelete(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 Are you sure you want to delete{\" \"}\n <span className=\"font-medium text-text-primary\">\n {confirmDelete?.name}\n </span>\n ? Any automatic tracking tied to this zone will stop.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -2,10 +2,10 @@ import { useTimeCampus as e } from "../../providers/TimeCampusProvider.js";
|
|
|
2
2
|
import { formatCurrency as t, formatDate as n } from "../../utils/helpers.js";
|
|
3
3
|
import { StatCard as r } from "../../components/StatCard.js";
|
|
4
4
|
import { LoadingSkeleton as i } from "../../components/LoadingSkeleton.js";
|
|
5
|
-
import {
|
|
5
|
+
import { EmptyState as a } from "../../components/EmptyState.js";
|
|
6
|
+
import { useDeleteInvoiceDraftMutation as o, useGenerateInvoiceDraftMutation as ee, useGetInvoiceStatsQuery as s, useListInvoicesQuery as te, useUpdateInvoiceStatusMutation as ne } from "../../../generated/wspace-operations.js";
|
|
6
7
|
import { PageLayout as c } from "../../components/PageLayout.js";
|
|
7
|
-
import { StatusBadge as
|
|
8
|
-
import { EmptyState as re } from "../../components/EmptyState.js";
|
|
8
|
+
import { StatusBadge as re } from "../../components/StatusBadge.js";
|
|
9
9
|
import { Modal as l } from "../../components/Modal.js";
|
|
10
10
|
import { ToastContainer as ie, useToast as ae } from "../../components/Toast.js";
|
|
11
11
|
import { TimeCampusInvoiceStatus as u } from "../../../generated/wspace-types.js";
|
|
@@ -50,7 +50,7 @@ function he(e, t) {
|
|
|
50
50
|
o.href = a, o.download = t, document.body.appendChild(o), o.click(), o.remove(), URL.revokeObjectURL(a);
|
|
51
51
|
}
|
|
52
52
|
function w() {
|
|
53
|
-
let { workspaceId: m = "" } = e(), { toasts: w, addToast: T, dismissToast: ge } = ae(), E = h(), { data: D, loading: O, error: _e, refetch: ve, fetchMore: ye } =
|
|
53
|
+
let { workspaceId: m = "" } = e(), { toasts: w, addToast: T, dismissToast: ge } = ae(), E = h(), { data: D, loading: O, error: _e, refetch: ve, fetchMore: ye } = te({
|
|
54
54
|
variables: {
|
|
55
55
|
workspaceId: m,
|
|
56
56
|
pagination: {
|
|
@@ -59,7 +59,7 @@ function w() {
|
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
61
|
notifyOnNetworkStatusChange: !0
|
|
62
|
-
}), { data: be, loading: xe, refetch: Se } =
|
|
62
|
+
}), { data: be, loading: xe, refetch: Se } = s({ variables: { workspaceId: m } }), [Ce, { loading: k }] = ee(), [A] = ne(), [we, { loading: j }] = o(), M = D?.timeCampusInvoices?.items ?? [], Te = D?.timeCampusInvoices?.total ?? 0, N = be?.timeCampusInvoiceStats, [P, F] = d(!1), [Ee, De] = d(!0), [Oe, I] = d(!1), [L, R] = d(C), [z, B] = d(null), [V, H] = d(null), [ke, U] = d(!1), [W, G] = d(null), [K, q] = d(null), [Ae, J] = d(null), Y = async () => {
|
|
63
63
|
await Promise.all([ve(), Se()]);
|
|
64
64
|
}, je = () => {
|
|
65
65
|
R({
|
|
@@ -271,7 +271,7 @@ function w() {
|
|
|
271
271
|
})
|
|
272
272
|
]
|
|
273
273
|
}),
|
|
274
|
-
M.length === 0 && !O ? /* @__PURE__ */ _(
|
|
274
|
+
M.length === 0 && !O ? /* @__PURE__ */ _(a, {
|
|
275
275
|
illustration: "empty-data",
|
|
276
276
|
title: "No invoices yet",
|
|
277
277
|
description: "Generate a draft invoice from real billable time entries to bill a client.",
|
|
@@ -310,7 +310,7 @@ function w() {
|
|
|
310
310
|
className: "text-sm font-medium text-text-primary",
|
|
311
311
|
children: t(e.total, e.currency)
|
|
312
312
|
}),
|
|
313
|
-
/* @__PURE__ */ _(
|
|
313
|
+
/* @__PURE__ */ _(re, {
|
|
314
314
|
label: e.status.toLowerCase(),
|
|
315
315
|
status: e.status.toLowerCase()
|
|
316
316
|
}),
|
|
@@ -1 +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 type ListInvoicesQuery,\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: (\n prev: ListInvoicesQuery,\n { fetchMoreResult }: { fetchMoreResult?: ListInvoicesQuery },\n ) => {\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
|
+
{"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 type ListInvoicesQuery,\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: (\n prev: ListInvoicesQuery,\n { fetchMoreResult }: { fetchMoreResult?: ListInvoicesQuery },\n ) => {\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,9 +1,9 @@
|
|
|
1
1
|
import { useTimeCampus as e } from "../../providers/TimeCampusProvider.js";
|
|
2
2
|
import { StatCard as t } from "../../components/StatCard.js";
|
|
3
3
|
import { LoadingSkeleton as n } from "../../components/LoadingSkeleton.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { EmptyState as r } from "../../components/EmptyState.js";
|
|
5
|
+
import { useMyDeadTimeRadarQuery as i } from "../../../generated/wspace-operations.js";
|
|
6
|
+
import { PageLayout as a } from "../../components/PageLayout.js";
|
|
7
7
|
import { ChartCard as o } from "../../components/ChartCard.js";
|
|
8
8
|
import { BarsChart as s } from "../../components/charts/BarsChart.js";
|
|
9
9
|
import { ProgressMeter as c } from "../../components/charts/ProgressMeter.js";
|
|
@@ -70,7 +70,7 @@ function N() {
|
|
|
70
70
|
let { workspaceId: m } = e(), [h, g] = p(30), { from: v, to: C } = f(() => ({
|
|
71
71
|
from: u(h),
|
|
72
72
|
to: d()
|
|
73
|
-
}), [h]), { data: k, loading: A, error: N } =
|
|
73
|
+
}), [h]), { data: k, loading: A, error: N } = i({
|
|
74
74
|
variables: {
|
|
75
75
|
from: v,
|
|
76
76
|
to: C
|
|
@@ -91,7 +91,7 @@ function N() {
|
|
|
91
91
|
}, e.value);
|
|
92
92
|
})
|
|
93
93
|
}), I = "Dead-Time Radar surfaces the friction hidden in your day — the queues, commutes and untracked gaps that quietly eat your hours — and quantifies how much of it you could realistically reclaim.";
|
|
94
|
-
if (!m || A) return /* @__PURE__ */ T(
|
|
94
|
+
if (!m || A) return /* @__PURE__ */ T(a, {
|
|
95
95
|
title: "Dead-Time Radar",
|
|
96
96
|
description: "Quantified reclaimable hours from friction and untracked gaps",
|
|
97
97
|
purpose: I,
|
|
@@ -101,7 +101,7 @@ function N() {
|
|
|
101
101
|
count: 4
|
|
102
102
|
})
|
|
103
103
|
});
|
|
104
|
-
if (N) return /* @__PURE__ */ T(
|
|
104
|
+
if (N) return /* @__PURE__ */ T(a, {
|
|
105
105
|
title: "Dead-Time Radar",
|
|
106
106
|
description: "Quantified reclaimable hours from friction and untracked gaps",
|
|
107
107
|
purpose: I,
|
|
@@ -116,7 +116,7 @@ function N() {
|
|
|
116
116
|
total: e.totalSeconds,
|
|
117
117
|
reclaimable: e.reclaimableSeconds
|
|
118
118
|
})), U = L.length === 0;
|
|
119
|
-
return /* @__PURE__ */ T(
|
|
119
|
+
return /* @__PURE__ */ T(a, {
|
|
120
120
|
title: "Dead-Time Radar",
|
|
121
121
|
description: "Quantified reclaimable hours from friction and untracked gaps",
|
|
122
122
|
purpose: I,
|
|
@@ -168,7 +168,7 @@ function N() {
|
|
|
168
168
|
})]
|
|
169
169
|
})
|
|
170
170
|
}),
|
|
171
|
-
U ? /* @__PURE__ */ T(
|
|
171
|
+
U ? /* @__PURE__ */ T(r, {
|
|
172
172
|
title: "No dead-time detected yet",
|
|
173
173
|
description: "Once you log friction moments — time spent waiting, commuting, or on-hold — and leave gaps in your tracked day, your reclaimable hours will show up here.",
|
|
174
174
|
action: /* @__PURE__ */ E("span", {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useTimeCampus as e } from "../../providers/TimeCampusProvider.js";
|
|
2
2
|
import { LoadingSkeleton as t } from "../../components/LoadingSkeleton.js";
|
|
3
|
-
import {
|
|
3
|
+
import { EmptyState as ee } from "../../components/EmptyState.js";
|
|
4
|
+
import { useMyLifeInWeeksQuery as te, useMyLifeProfileQuery as n, useUpdateMyLifeProfileMutation as r } from "../../../generated/wspace-operations.js";
|
|
4
5
|
import { PageLayout as i } from "../../components/PageLayout.js";
|
|
5
|
-
import { EmptyState as te } from "../../components/EmptyState.js";
|
|
6
6
|
import { Modal as ne } from "../../components/Modal.js";
|
|
7
7
|
import { ChartCard as a } from "../../components/ChartCard.js";
|
|
8
8
|
import { ToastContainer as re, useToast as ie } from "../../components/Toast.js";
|
|
@@ -26,7 +26,7 @@ function ce(e) {
|
|
|
26
26
|
}
|
|
27
27
|
var x = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
28
28
|
function S() {
|
|
29
|
-
let { workspaceId: S = "" } = e(), { toasts: le, addToast: C, dismissToast: w } = ie(), { data: T, loading: E, error: D, refetch: O } =
|
|
29
|
+
let { workspaceId: S = "" } = e(), { toasts: le, addToast: C, dismissToast: w } = ie(), { data: T, loading: E, error: D, refetch: O } = te({ skip: !S }), { data: k, loading: A, error: j, refetch: M } = n({ skip: !S }), [N, { loading: P }] = r(), F = T?.myLifeInWeeks, I = k?.myLifeProfile, [L, R] = u(!1), [z, B] = u(""), [V, H] = u(""), [U, W] = u(!1), [G, K] = u(null), q = () => {
|
|
30
30
|
B(ce(I?.birthDate ?? F?.birthDate)), H(I?.lifeExpectancyYears == null ? F?.lifeExpectancyYears == null ? "" : String(F.lifeExpectancyYears) : String(I.lifeExpectancyYears)), W(I?.hideRemainingWeeks ?? F?.hideRemainingWeeks ?? !1), K(null), R(!0);
|
|
31
31
|
}, ue = async (e) => {
|
|
32
32
|
if (e?.preventDefault(), K(null), !z) {
|
|
@@ -220,7 +220,7 @@ function S() {
|
|
|
220
220
|
ariaLabel: "Recent time by life domain"
|
|
221
221
|
})
|
|
222
222
|
}),
|
|
223
|
-
J && $.length === 0 && /* @__PURE__ */ _(
|
|
223
|
+
J && $.length === 0 && /* @__PURE__ */ _(ee, {
|
|
224
224
|
title: "Your weeks are yours to shape",
|
|
225
225
|
description: "As you capture how you spend your time, a quiet reflection of your recent weeks will appear here — only for you."
|
|
226
226
|
})
|