@burdenoff/microfe-timecampus 2026.719.2 → 2026.719.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-operations.d.ts +40 -76
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-operations.js +40 -50
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.d.ts +21 -38
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/timecampus/pages/HomePage.js +1 -1
- package/dist/timecampus/pages/HomePage.js.map +1 -1
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +6 -3
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js +4 -4
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +8 -3
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +8 -4
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.js +4 -4
- package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +4 -4
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js +4 -2
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.js +6 -4
- package/dist/timecampus/pages/meetings/MeetingsPage.js.map +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectsPage.js +2 -2
- package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/TasksPage.js +4 -2
- package/dist/timecampus/pages/projects/TasksPage.js.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.d.ts.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +4 -4
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.js +137 -132
- package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.js +2 -2
- package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.js +2 -2
- package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +4 -4
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +1 -1
- 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: { 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: { limit: PAGE_SIZE, offset: geofences.length },\n updateQuery: (prev, { fetchMoreResult }) => {\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: (prev, { fetchMoreResult }) => {\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocationTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/location-tracking/LocationTrackingPage.tsx"],"names":[],"mappings":"AAqDA,wBAAgB,oBAAoB,
|
|
1
|
+
{"version":3,"file":"LocationTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/location-tracking/LocationTrackingPage.tsx"],"names":[],"mappings":"AAqDA,wBAAgB,oBAAoB,gCA+cnC"}
|
|
@@ -24,10 +24,10 @@ var w = 25, T = "mt-1 w-full rounded-md border border-border-subtle bg-bg-surfac
|
|
|
24
24
|
endTime: ""
|
|
25
25
|
};
|
|
26
26
|
function D() {
|
|
27
|
-
let { currentUser: D } = e(), { data: O, loading: k, error: A, refetch: j, fetchMore: ie } = c({ variables: {
|
|
27
|
+
let { currentUser: D } = e(), { data: O, loading: k, error: A, refetch: j, fetchMore: ie } = c({ variables: { pagination: {
|
|
28
28
|
limit: w,
|
|
29
29
|
offset: 0
|
|
30
|
-
} }), { data: ae, loading: oe, error: M, refetch: N } = ee(), [P, { loading: F, error: I }] = o(), [L, { loading: R }] = s(), [z, B] = h(!1), [V, H] = h(E), [U, W] = h(null), [G, K] = h(!1), q = O?.locationEntries?.items ?? [], se = O?.locationEntries?.hasMore ?? !1, J = ae?.locationStats, Y = q.filter((e) => e.geofenceId).length, ce = q.length - Y, le = [{
|
|
30
|
+
} } }), { data: ae, loading: oe, error: M, refetch: N } = ee(), [P, { loading: F, error: I }] = o(), [L, { loading: R }] = s(), [z, B] = h(!1), [V, H] = h(E), [U, W] = h(null), [G, K] = h(!1), q = O?.locationEntries?.items ?? [], se = O?.locationEntries?.hasMore ?? !1, J = ae?.locationStats, Y = q.filter((e) => e.geofenceId).length, ce = q.length - Y, le = [{
|
|
31
31
|
name: "Geofenced",
|
|
32
32
|
value: Y
|
|
33
33
|
}, {
|
|
@@ -66,10 +66,10 @@ function D() {
|
|
|
66
66
|
K(!0);
|
|
67
67
|
try {
|
|
68
68
|
await ie({
|
|
69
|
-
variables: {
|
|
69
|
+
variables: { pagination: {
|
|
70
70
|
limit: w,
|
|
71
71
|
offset: q.length
|
|
72
|
-
},
|
|
72
|
+
} },
|
|
73
73
|
updateQuery: (e, { fetchMoreResult: t }) => t ? { locationEntries: {
|
|
74
74
|
...t.locationEntries,
|
|
75
75
|
items: [...e.locationEntries.items, ...t.locationEntries.items]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocationTrackingPage.js","names":[],"sources":["../../../../src/timecampus/pages/location-tracking/LocationTrackingPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n MapPin,\n Clock,\n Plus,\n Trash2,\n Building2,\n Satellite,\n} from \"lucide-react\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n ChartCard,\n DonutChart,\n BarsChart,\n ProgressMeter,\n type ChartDatum,\n} from \"../../components\";\nimport { formatHours, formatDate, formatTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListLocationEntriesQuery,\n useGetLocationStatsQuery,\n useCreateLocationEntryMutation,\n useDeleteLocationEntryMutation,\n type LocationEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 25;\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\ninterface EntryFormState {\n address: string;\n latitude: string;\n longitude: string;\n startTime: string;\n endTime: string;\n}\n\nconst emptyForm: EntryFormState = {\n address: \"\",\n latitude: \"\",\n longitude: \"\",\n startTime: \"\",\n endTime: \"\",\n};\n\nexport function LocationTrackingPage() {\n const { currentUser } = useTimeCampus();\n\n const {\n data: entriesData,\n loading: entriesLoading,\n error: entriesError,\n refetch: refetchEntries,\n fetchMore,\n } = useListLocationEntriesQuery({\n variables: { limit: PAGE_SIZE, offset: 0 },\n });\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n refetch: refetchStats,\n } = useGetLocationStatsQuery();\n\n const [createEntry, { loading: creating, error: createError }] =\n useCreateLocationEntryMutation();\n const [deleteEntry, { loading: deleting }] = useDeleteLocationEntryMutation();\n\n const [formOpen, setFormOpen] = useState(false);\n const [form, setForm] = useState<EntryFormState>(emptyForm);\n const [confirmDelete, setConfirmDelete] =\n useState<LocationEntryFieldsFragment | null>(null);\n const [loadingMore, setLoadingMore] = useState(false);\n\n const locations = entriesData?.locationEntries?.items ?? [];\n const hasMore = entriesData?.locationEntries?.hasMore ?? false;\n const stats = statsData?.locationStats;\n\n // ---- Derived, real-data views over the loaded entries -------------------\n const geofencedCount = locations.filter(\n (l: LocationEntryFieldsFragment) => l.geofenceId,\n ).length;\n const manualCount = locations.length - geofencedCount;\n const sourceSlices = [\n { name: \"Geofenced\", value: geofencedCount },\n { name: \"Manual\", value: manualCount },\n ];\n\n const maxDuration = locations.reduce(\n (max: number, l: LocationEntryFieldsFragment) =>\n Math.max(max, l.duration ?? 0),\n 0,\n );\n\n const durationByAddress = new Map<string, number>();\n for (const l of locations as LocationEntryFieldsFragment[]) {\n if (!l.duration) continue;\n const key = l.address?.trim() || \"Unknown location\";\n durationByAddress.set(key, (durationByAddress.get(key) ?? 0) + l.duration);\n }\n const topLocations: ChartDatum[] = Array.from(durationByAddress.entries())\n .map(([location, seconds]) => ({ location, hours: seconds / 3600 }))\n .sort((a, b) => (b.hours as number) - (a.hours as number))\n .slice(0, 6);\n\n const openCreate = () => {\n const now = new Date();\n const localIso = new Date(now.getTime() - now.getTimezoneOffset() * 60000)\n .toISOString()\n .slice(0, 16);\n setForm({ ...emptyForm, startTime: localIso });\n setFormOpen(true);\n };\n\n const canSubmit =\n !!currentUser?.id &&\n form.latitude.trim() !== \"\" &&\n form.longitude.trim() !== \"\" &&\n form.startTime.trim() !== \"\";\n\n const handleSubmit = async () => {\n if (!canSubmit || !currentUser?.id) return;\n const startTime = new Date(form.startTime).toISOString();\n const endTime = form.endTime\n ? new Date(form.endTime).toISOString()\n : undefined;\n const duration = endTime\n ? Math.max(\n 0,\n Math.round(\n (new Date(endTime).getTime() - new Date(startTime).getTime()) /\n 1000,\n ),\n )\n : undefined;\n await createEntry({\n variables: {\n input: {\n userId: currentUser.id,\n latitude: Number(form.latitude),\n longitude: Number(form.longitude),\n address: form.address.trim() || undefined,\n startTime,\n endTime,\n duration,\n },\n },\n });\n setFormOpen(false);\n await Promise.all([refetchEntries(), refetchStats()]);\n };\n\n const handleDelete = async () => {\n if (!confirmDelete) return;\n await deleteEntry({ variables: { id: confirmDelete.id } });\n setConfirmDelete(null);\n await Promise.all([refetchEntries(), refetchStats()]);\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { limit: PAGE_SIZE, offset: locations.length },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n locationEntries: {\n ...fetchMoreResult.locationEntries,\n items: [\n ...prev.locationEntries.items,\n ...fetchMoreResult.locationEntries.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n const addButton = (\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Location Entry\n </button>\n );\n\n if (entriesLoading && statsLoading) {\n return (\n <PageLayout\n title=\"Location Tracking\"\n description=\"Track work locations and productivity\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (entriesError && statsError) {\n return (\n <PageLayout\n title=\"Location Tracking\"\n description=\"Track work locations and productivity\"\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 location data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Location Tracking\"\n description=\"Track work locations and productivity\"\n purpose=\"See where work actually happens — office, home, client sites — and how long you spend at each, so you can verify on-site time, support field teams, and back up location-based billing.\"\n actions={addButton}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Locations\"\n value={String(stats?.totalLocations ?? 0)}\n icon={<MapPin className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Unique Addresses\"\n value={String(stats?.uniqueAddresses ?? 0)}\n icon={<Building2 className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Most Frequent\"\n value={stats?.mostFrequentLocation ?? \"N/A\"}\n />\n <StatCard\n title=\"Total Duration\"\n value={formatHours((stats?.totalDuration ?? 0) / 3600)}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"Tracked time\"\n />\n </div>\n\n {locations.length === 0 && !entriesLoading ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No location entries found\"\n description=\"With location tracking enabled, the places you work from will show up here with time spent at each. You can also add an entry manually.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"mt-6 grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title=\"Tracking source\"\n description=\"How your loaded entries were captured\"\n >\n <DonutChart\n data={sourceSlices}\n centerLabel=\"Entries\"\n centerValue={locations.length}\n ariaLabel=\"Geofenced vs manual location entries\"\n emptyLabel=\"No entries to summarize yet\"\n />\n </ChartCard>\n <ChartCard\n title=\"Time by location\"\n description=\"Hours logged across your loaded entries\"\n >\n <BarsChart\n data={topLocations}\n xKey=\"location\"\n orientation=\"horizontal\"\n series={[{ key: \"hours\", label: \"Hours\" }]}\n categoryWidth={120}\n valueFormat={(v) => formatHours(v)}\n ariaLabel=\"Hours logged by location\"\n emptyLabel=\"No completed entries with a duration yet\"\n />\n </ChartCard>\n </div>\n\n <div className=\"mt-6 space-y-3\">\n {locations.map((location: LocationEntryFieldsFragment) => {\n const geofenced = Boolean(location.geofenceId);\n const durationSeconds = location.duration ?? 0;\n return (\n <div\n key={location.id}\n className=\"rounded-xl border border-border-seam bg-gloss-card p-4 shadow-pop transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex min-w-0 items-start gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n {geofenced ? (\n <Satellite className=\"h-5 w-5\" />\n ) : (\n <MapPin className=\"h-5 w-5\" />\n )}\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {location.address ?? \"Unknown Location\"}\n </p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n <StatusBadge\n label={geofenced ? \"Geofenced\" : \"Manual\"}\n variant={geofenced ? \"info\" : \"muted\"}\n />\n <span>{formatDate(location.startTime)}</span>\n <span className=\"text-text-muted\">\n {formatTime(location.startTime)}\n {location.endTime\n ? ` – ${formatTime(location.endTime)}`\n : \"\"}\n </span>\n </div>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"text-right\">\n <p className=\"text-sm font-semibold tabular-nums text-text-primary\">\n {location.duration\n ? formatHours(location.duration / 3600)\n : \"Active\"}\n </p>\n <p className=\"text-xs text-text-secondary\">duration</p>\n </div>\n <button\n onClick={() => setConfirmDelete(location)}\n aria-label=\"Delete entry\"\n className=\"rounded-md border border-border-subtle p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n {location.duration && maxDuration > 0 ? (\n <div className=\"mt-3\">\n <ProgressMeter\n value={durationSeconds}\n max={maxDuration}\n tone=\"success\"\n showValue={false}\n size=\"sm\"\n />\n </div>\n ) : null}\n </div>\n );\n })}\n </div>\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=\"Add Location Entry\"\n description=\"Manually log a work location. Only you and people you share with can see this.\"\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 || !canSubmit}\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 ? \"Saving...\" : \"Add Entry\"}\n </button>\n </>\n }\n >\n {createError && (\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 add location entry. Please check the values and try again.\n </div>\n )}\n {!currentUser?.id && (\n <div className=\"mb-4 rounded-lg border border-status-warning-bg/40 bg-status-warning-bg p-3 text-sm text-status-warning-text\">\n You must be signed in to add a location entry.\n </div>\n )}\n <div className=\"space-y-4\">\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=\"e.g. Client site, Chennai\"\n value={form.address}\n onChange={(e) => setForm({ ...form, address: 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 Start Time\n </label>\n <input\n type=\"datetime-local\"\n value={form.startTime}\n onChange={(e) =>\n setForm({ ...form, startTime: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n End Time (optional)\n </label>\n <input\n type=\"datetime-local\"\n value={form.endTime}\n onChange={(e) => setForm({ ...form, endTime: e.target.value })}\n className={inputClass}\n />\n </div>\n </div>\n </div>\n </Modal>\n\n <Modal\n open={confirmDelete !== null}\n onClose={() => setConfirmDelete(null)}\n title=\"Delete Location Entry\"\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 this location entry\n {confirmDelete?.address ? ` at ${confirmDelete.address}` : \"\"}?\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"LocationTrackingPage.js","names":[],"sources":["../../../../src/timecampus/pages/location-tracking/LocationTrackingPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n MapPin,\n Clock,\n Plus,\n Trash2,\n Building2,\n Satellite,\n} from \"lucide-react\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n ChartCard,\n DonutChart,\n BarsChart,\n ProgressMeter,\n type ChartDatum,\n} from \"../../components\";\nimport { formatHours, formatDate, formatTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListLocationEntriesQuery,\n useGetLocationStatsQuery,\n useCreateLocationEntryMutation,\n useDeleteLocationEntryMutation,\n type LocationEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 25;\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\ninterface EntryFormState {\n address: string;\n latitude: string;\n longitude: string;\n startTime: string;\n endTime: string;\n}\n\nconst emptyForm: EntryFormState = {\n address: \"\",\n latitude: \"\",\n longitude: \"\",\n startTime: \"\",\n endTime: \"\",\n};\n\nexport function LocationTrackingPage() {\n const { currentUser } = useTimeCampus();\n\n const {\n data: entriesData,\n loading: entriesLoading,\n error: entriesError,\n refetch: refetchEntries,\n fetchMore,\n } = useListLocationEntriesQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n });\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n refetch: refetchStats,\n } = useGetLocationStatsQuery();\n\n const [createEntry, { loading: creating, error: createError }] =\n useCreateLocationEntryMutation();\n const [deleteEntry, { loading: deleting }] = useDeleteLocationEntryMutation();\n\n const [formOpen, setFormOpen] = useState(false);\n const [form, setForm] = useState<EntryFormState>(emptyForm);\n const [confirmDelete, setConfirmDelete] =\n useState<LocationEntryFieldsFragment | null>(null);\n const [loadingMore, setLoadingMore] = useState(false);\n\n const locations = entriesData?.locationEntries?.items ?? [];\n const hasMore = entriesData?.locationEntries?.hasMore ?? false;\n const stats = statsData?.locationStats;\n\n // ---- Derived, real-data views over the loaded entries -------------------\n const geofencedCount = locations.filter(\n (l: LocationEntryFieldsFragment) => l.geofenceId,\n ).length;\n const manualCount = locations.length - geofencedCount;\n const sourceSlices = [\n { name: \"Geofenced\", value: geofencedCount },\n { name: \"Manual\", value: manualCount },\n ];\n\n const maxDuration = locations.reduce(\n (max: number, l: LocationEntryFieldsFragment) =>\n Math.max(max, l.duration ?? 0),\n 0,\n );\n\n const durationByAddress = new Map<string, number>();\n for (const l of locations as LocationEntryFieldsFragment[]) {\n if (!l.duration) continue;\n const key = l.address?.trim() || \"Unknown location\";\n durationByAddress.set(key, (durationByAddress.get(key) ?? 0) + l.duration);\n }\n const topLocations: ChartDatum[] = Array.from(durationByAddress.entries())\n .map(([location, seconds]) => ({ location, hours: seconds / 3600 }))\n .sort((a, b) => (b.hours as number) - (a.hours as number))\n .slice(0, 6);\n\n const openCreate = () => {\n const now = new Date();\n const localIso = new Date(now.getTime() - now.getTimezoneOffset() * 60000)\n .toISOString()\n .slice(0, 16);\n setForm({ ...emptyForm, startTime: localIso });\n setFormOpen(true);\n };\n\n const canSubmit =\n !!currentUser?.id &&\n form.latitude.trim() !== \"\" &&\n form.longitude.trim() !== \"\" &&\n form.startTime.trim() !== \"\";\n\n const handleSubmit = async () => {\n if (!canSubmit || !currentUser?.id) return;\n const startTime = new Date(form.startTime).toISOString();\n const endTime = form.endTime\n ? new Date(form.endTime).toISOString()\n : undefined;\n const duration = endTime\n ? Math.max(\n 0,\n Math.round(\n (new Date(endTime).getTime() - new Date(startTime).getTime()) /\n 1000,\n ),\n )\n : undefined;\n await createEntry({\n variables: {\n input: {\n userId: currentUser.id,\n latitude: Number(form.latitude),\n longitude: Number(form.longitude),\n address: form.address.trim() || undefined,\n startTime,\n endTime,\n duration,\n },\n },\n });\n setFormOpen(false);\n await Promise.all([refetchEntries(), refetchStats()]);\n };\n\n const handleDelete = async () => {\n if (!confirmDelete) return;\n await deleteEntry({ variables: { id: confirmDelete.id } });\n setConfirmDelete(null);\n await Promise.all([refetchEntries(), refetchStats()]);\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: {\n pagination: { limit: PAGE_SIZE, offset: locations.length },\n },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n locationEntries: {\n ...fetchMoreResult.locationEntries,\n items: [\n ...prev.locationEntries.items,\n ...fetchMoreResult.locationEntries.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n const addButton = (\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Location Entry\n </button>\n );\n\n if (entriesLoading && statsLoading) {\n return (\n <PageLayout\n title=\"Location Tracking\"\n description=\"Track work locations and productivity\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (entriesError && statsError) {\n return (\n <PageLayout\n title=\"Location Tracking\"\n description=\"Track work locations and productivity\"\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 location data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Location Tracking\"\n description=\"Track work locations and productivity\"\n purpose=\"See where work actually happens — office, home, client sites — and how long you spend at each, so you can verify on-site time, support field teams, and back up location-based billing.\"\n actions={addButton}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Locations\"\n value={String(stats?.totalLocations ?? 0)}\n icon={<MapPin className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Unique Addresses\"\n value={String(stats?.uniqueAddresses ?? 0)}\n icon={<Building2 className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Most Frequent\"\n value={stats?.mostFrequentLocation ?? \"N/A\"}\n />\n <StatCard\n title=\"Total Duration\"\n value={formatHours((stats?.totalDuration ?? 0) / 3600)}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"Tracked time\"\n />\n </div>\n\n {locations.length === 0 && !entriesLoading ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No location entries found\"\n description=\"With location tracking enabled, the places you work from will show up here with time spent at each. You can also add an entry manually.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"mt-6 grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title=\"Tracking source\"\n description=\"How your loaded entries were captured\"\n >\n <DonutChart\n data={sourceSlices}\n centerLabel=\"Entries\"\n centerValue={locations.length}\n ariaLabel=\"Geofenced vs manual location entries\"\n emptyLabel=\"No entries to summarize yet\"\n />\n </ChartCard>\n <ChartCard\n title=\"Time by location\"\n description=\"Hours logged across your loaded entries\"\n >\n <BarsChart\n data={topLocations}\n xKey=\"location\"\n orientation=\"horizontal\"\n series={[{ key: \"hours\", label: \"Hours\" }]}\n categoryWidth={120}\n valueFormat={(v) => formatHours(v)}\n ariaLabel=\"Hours logged by location\"\n emptyLabel=\"No completed entries with a duration yet\"\n />\n </ChartCard>\n </div>\n\n <div className=\"mt-6 space-y-3\">\n {locations.map((location: LocationEntryFieldsFragment) => {\n const geofenced = Boolean(location.geofenceId);\n const durationSeconds = location.duration ?? 0;\n return (\n <div\n key={location.id}\n className=\"rounded-xl border border-border-seam bg-gloss-card p-4 shadow-pop transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex min-w-0 items-start gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n {geofenced ? (\n <Satellite className=\"h-5 w-5\" />\n ) : (\n <MapPin className=\"h-5 w-5\" />\n )}\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {location.address ?? \"Unknown Location\"}\n </p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n <StatusBadge\n label={geofenced ? \"Geofenced\" : \"Manual\"}\n variant={geofenced ? \"info\" : \"muted\"}\n />\n <span>{formatDate(location.startTime)}</span>\n <span className=\"text-text-muted\">\n {formatTime(location.startTime)}\n {location.endTime\n ? ` – ${formatTime(location.endTime)}`\n : \"\"}\n </span>\n </div>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"text-right\">\n <p className=\"text-sm font-semibold tabular-nums text-text-primary\">\n {location.duration\n ? formatHours(location.duration / 3600)\n : \"Active\"}\n </p>\n <p className=\"text-xs text-text-secondary\">duration</p>\n </div>\n <button\n onClick={() => setConfirmDelete(location)}\n aria-label=\"Delete entry\"\n className=\"rounded-md border border-border-subtle p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n {location.duration && maxDuration > 0 ? (\n <div className=\"mt-3\">\n <ProgressMeter\n value={durationSeconds}\n max={maxDuration}\n tone=\"success\"\n showValue={false}\n size=\"sm\"\n />\n </div>\n ) : null}\n </div>\n );\n })}\n </div>\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=\"Add Location Entry\"\n description=\"Manually log a work location. Only you and people you share with can see this.\"\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 || !canSubmit}\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 ? \"Saving...\" : \"Add Entry\"}\n </button>\n </>\n }\n >\n {createError && (\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 add location entry. Please check the values and try again.\n </div>\n )}\n {!currentUser?.id && (\n <div className=\"mb-4 rounded-lg border border-status-warning-bg/40 bg-status-warning-bg p-3 text-sm text-status-warning-text\">\n You must be signed in to add a location entry.\n </div>\n )}\n <div className=\"space-y-4\">\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=\"e.g. Client site, Chennai\"\n value={form.address}\n onChange={(e) => setForm({ ...form, address: 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 Start Time\n </label>\n <input\n type=\"datetime-local\"\n value={form.startTime}\n onChange={(e) =>\n setForm({ ...form, startTime: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n End Time (optional)\n </label>\n <input\n type=\"datetime-local\"\n value={form.endTime}\n onChange={(e) => setForm({ ...form, endTime: e.target.value })}\n className={inputClass}\n />\n </div>\n </div>\n </div>\n </Modal>\n\n <Modal\n open={confirmDelete !== null}\n onClose={() => setConfirmDelete(null)}\n title=\"Delete Location Entry\"\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 this location entry\n {confirmDelete?.address ? ` at ${confirmDelete.address}` : \"\"}?\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MeetingAnalyticsPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/meetings/MeetingAnalyticsPage.tsx"],"names":[],"mappings":"AAuDA,wBAAgB,oBAAoB,
|
|
1
|
+
{"version":3,"file":"MeetingAnalyticsPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/meetings/MeetingAnalyticsPage.tsx"],"names":[],"mappings":"AAuDA,wBAAgB,oBAAoB,gCAmNnC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MeetingAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, CalendarDays, Clock, DollarSign } from \"lucide-react\";\nimport {\n PageLayout,\n StatCard,\n LoadingSkeleton,\n ChartCard,\n TrendChart,\n BarsChart,\n DonutChart,\n RadialGauge,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetMeetingStatsQuery,\n useListMeetingsQuery,\n} from \"@/generated/wspace-operations\";\nimport { MeetingType } from \"@/generated/wspace-types\";\nimport { formatCurrency, formatNumber } from \"../../utils/helpers\";\n\nconst MEETING_TYPE_LABELS: Record<MeetingType, string> = {\n [MeetingType.Standup]: \"Standups\",\n [MeetingType.OneOnOne]: \"1-on-1s\",\n [MeetingType.Team]: \"Team\",\n [MeetingType.Review]: \"Reviews\",\n [MeetingType.Planning]: \"Planning\",\n [MeetingType.Retrospective]: \"Retros\",\n [MeetingType.External]: \"External\",\n};\n\nconst WEEKS_IN_TREND = 6;\nconst MEETINGS_SAMPLE = 250;\n\nfunction meetingTypeLabel(type: MeetingType): string {\n return MEETING_TYPE_LABELS[type] ?? type;\n}\n\n/** Monday 00:00 of the week containing `date`. */\nfunction startOfWeek(date: Date): Date {\n const d = new Date(date);\n d.setHours(0, 0, 0, 0);\n const dayFromMonday = (d.getDay() + 6) % 7;\n d.setDate(d.getDate() - dayFromMonday);\n return d;\n}\n\nfunction weekLabel(date: Date): string {\n return date.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" });\n}\n\nconst roundHours = (minutes: number): number =>\n Math.round((minutes / 60) * 10) / 10;\n\nconst formatHoursValue = (value: number): string =>\n `${Number.isInteger(value) ? value : value.toFixed(1)}h`;\n\nexport function MeetingAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const { data, loading, error } = useGetMeetingStatsQuery({\n variables: { workspaceId },\n skip: !workspaceId,\n });\n\n const { data: meetingsData, loading: meetingsLoading } = useListMeetingsQuery(\n {\n variables: { workspaceId, limit: MEETINGS_SAMPLE, offset: 0 },\n skip: !workspaceId,\n },\n );\n\n const stats = data?.meetingStats;\n const meetings = meetingsData?.meetings ?? [];\n\n if (loading || (meetingsLoading && !meetingsData)) {\n return (\n <PageLayout\n title=\"Meeting Analytics\"\n description=\"Insights into meeting time and cost\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Meeting Analytics\"\n description=\"Insights into meeting time and cost\"\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 meeting analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalHours = Math.round((stats?.totalDuration ?? 0) / 60);\n const avgDuration = Math.round(stats?.averageDuration ?? 0);\n const avgRating = stats?.averageRating ?? 0;\n const actionItemsTotal = stats?.actionItemsTotal ?? 0;\n const actionItemsCompleted = stats?.actionItemsCompleted ?? 0;\n const actionItemsOutstanding = Math.max(\n 0,\n actionItemsTotal - actionItemsCompleted,\n );\n\n // --- Meeting load trend: real per-meeting startTime bucketed into weeks ---\n const latestMs = meetings.reduce(\n (max: number, m: (typeof meetings)[number]) => {\n const t = new Date(m.startTime).getTime();\n return Number.isFinite(t) && t > max ? t : max;\n },\n Number.NEGATIVE_INFINITY,\n );\n const anchorWeek = startOfWeek(\n Number.isFinite(latestMs) ? new Date(latestMs) : new Date(),\n );\n\n const weekBuckets = Array.from({ length: WEEKS_IN_TREND }, (_, i) => {\n const start = new Date(anchorWeek);\n start.setDate(start.getDate() - (WEEKS_IN_TREND - 1 - i) * 7);\n return { start: start.getTime(), label: weekLabel(start), minutes: 0 };\n });\n const bucketByStart = new Map(weekBuckets.map((b) => [b.start, b]));\n for (const m of meetings) {\n const t = new Date(m.startTime).getTime();\n if (!Number.isFinite(t)) continue;\n const bucket = bucketByStart.get(startOfWeek(new Date(t)).getTime());\n if (bucket) bucket.minutes += m.duration ?? 0;\n }\n const trendData = weekBuckets.map((b) => ({\n week: b.label,\n hours: roundHours(b.minutes),\n }));\n const hasTrend = meetings.length > 0;\n\n // --- Meeting type breakdown: real hours grouped by type ---\n const typeMinutes = new Map<MeetingType, number>();\n for (const m of meetings) {\n typeMinutes.set(\n m.meetingType,\n (typeMinutes.get(m.meetingType) ?? 0) + (m.duration ?? 0),\n );\n }\n const byTypeData = Array.from(typeMinutes.entries())\n .map(([type, minutes]) => ({\n type: meetingTypeLabel(type),\n hours: roundHours(minutes),\n }))\n .filter((d) => d.hours > 0)\n .sort((a, b) => b.hours - a.hours);\n\n // --- Action-item follow-through (aggregate) ---\n const actionSlices = [\n { name: \"Completed\", value: actionItemsCompleted },\n { name: \"Outstanding\", value: actionItemsOutstanding },\n ];\n\n return (\n <PageLayout\n title=\"Meeting Analytics\"\n description=\"Insights into meeting time and cost\"\n purpose=\"Put a number on your meeting habit — total hours, dollar cost and follow-through on action items — so you can cut the meetings that don't pay for themselves and reclaim time for focused work.\"\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 Meetings\"\n value={String(stats?.totalMeetings ?? 0)}\n icon={<CalendarDays className=\"h-5 w-5\" />}\n description={`${formatNumber(actionItemsTotal)} action items tracked`}\n />\n <StatCard\n title=\"Meeting Hours\"\n value={`${totalHours}h`}\n icon={<Clock className=\"h-5 w-5\" />}\n description={`${formatNumber(stats?.totalDuration ?? 0)} total minutes`}\n />\n <StatCard\n title=\"True Cost of Meetings\"\n value={formatCurrency(stats?.totalCost ?? 0)}\n icon={<DollarSign className=\"h-5 w-5\" />}\n description=\"Estimated productivity cost\"\n />\n <StatCard\n title=\"Avg. Duration\"\n value={`${avgDuration} min`}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={`${avgRating.toFixed(1)}★ average rating`}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Meeting Load Trend\"\n description=\"Meeting hours per week over the last 6 weeks\"\n >\n <TrendChart\n data={hasTrend ? trendData : []}\n xKey=\"week\"\n series={[{ key: \"hours\", label: \"Meeting hours\" }]}\n type=\"area\"\n height={240}\n valueFormat={formatHoursValue}\n emptyLabel=\"No meeting activity to chart yet.\"\n ariaLabel=\"Meeting hours per week\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Meeting Type Breakdown\"\n description=\"Hours spent by meeting type\"\n >\n <BarsChart\n data={byTypeData}\n xKey=\"type\"\n series={[{ key: \"hours\", label: \"Hours\" }]}\n orientation=\"horizontal\"\n height={240}\n valueFormat={formatHoursValue}\n categoryWidth={88}\n emptyLabel=\"No meetings recorded yet.\"\n ariaLabel=\"Meeting hours by type\"\n />\n </ChartCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Action Item Follow-through\"\n description=\"Completed vs. outstanding across all meetings\"\n >\n <DonutChart\n data={actionSlices}\n centerValue={actionItemsTotal}\n centerLabel=\"action items\"\n height={240}\n emptyLabel=\"No action items captured yet.\"\n ariaLabel=\"Action item completion\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Average Meeting Rating\"\n description=\"How worthwhile meetings have felt, out of 5\"\n >\n <div className=\"flex items-center justify-center\">\n <RadialGauge\n value={avgRating}\n min={0}\n max={5}\n label=\"out of 5\"\n sublabel={`${formatNumber(stats?.totalMeetings ?? 0)} meetings`}\n height={240}\n format={(v) => v.toFixed(1)}\n ariaLabel=\"Average meeting rating out of 5\"\n />\n </div>\n </ChartCard>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MeetingAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, CalendarDays, Clock, DollarSign } from \"lucide-react\";\nimport {\n PageLayout,\n StatCard,\n LoadingSkeleton,\n ChartCard,\n TrendChart,\n BarsChart,\n DonutChart,\n RadialGauge,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetMeetingStatsQuery,\n useListMeetingsQuery,\n} from \"@/generated/wspace-operations\";\nimport { MeetingType } from \"@/generated/wspace-types\";\nimport { formatCurrency, formatNumber } from \"../../utils/helpers\";\n\nconst MEETING_TYPE_LABELS: Record<MeetingType, string> = {\n [MeetingType.Standup]: \"Standups\",\n [MeetingType.OneOnOne]: \"1-on-1s\",\n [MeetingType.Team]: \"Team\",\n [MeetingType.Review]: \"Reviews\",\n [MeetingType.Planning]: \"Planning\",\n [MeetingType.Retrospective]: \"Retros\",\n [MeetingType.External]: \"External\",\n};\n\nconst WEEKS_IN_TREND = 6;\nconst MEETINGS_SAMPLE = 250;\n\nfunction meetingTypeLabel(type: MeetingType): string {\n return MEETING_TYPE_LABELS[type] ?? type;\n}\n\n/** Monday 00:00 of the week containing `date`. */\nfunction startOfWeek(date: Date): Date {\n const d = new Date(date);\n d.setHours(0, 0, 0, 0);\n const dayFromMonday = (d.getDay() + 6) % 7;\n d.setDate(d.getDate() - dayFromMonday);\n return d;\n}\n\nfunction weekLabel(date: Date): string {\n return date.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" });\n}\n\nconst roundHours = (minutes: number): number =>\n Math.round((minutes / 60) * 10) / 10;\n\nconst formatHoursValue = (value: number): string =>\n `${Number.isInteger(value) ? value : value.toFixed(1)}h`;\n\nexport function MeetingAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const { data, loading, error } = useGetMeetingStatsQuery({\n variables: { workspaceId },\n skip: !workspaceId,\n });\n\n const { data: meetingsData, loading: meetingsLoading } = useListMeetingsQuery(\n {\n variables: {\n workspaceId,\n pagination: { limit: MEETINGS_SAMPLE, offset: 0 },\n },\n skip: !workspaceId,\n },\n );\n\n const stats = data?.meetingStats;\n const meetings = meetingsData?.meetings ?? [];\n\n if (loading || (meetingsLoading && !meetingsData)) {\n return (\n <PageLayout\n title=\"Meeting Analytics\"\n description=\"Insights into meeting time and cost\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Meeting Analytics\"\n description=\"Insights into meeting time and cost\"\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 meeting analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalHours = Math.round((stats?.totalDuration ?? 0) / 60);\n const avgDuration = Math.round(stats?.averageDuration ?? 0);\n const avgRating = stats?.averageRating ?? 0;\n const actionItemsTotal = stats?.actionItemsTotal ?? 0;\n const actionItemsCompleted = stats?.actionItemsCompleted ?? 0;\n const actionItemsOutstanding = Math.max(\n 0,\n actionItemsTotal - actionItemsCompleted,\n );\n\n // --- Meeting load trend: real per-meeting startTime bucketed into weeks ---\n const latestMs = meetings.reduce(\n (max: number, m: (typeof meetings)[number]) => {\n const t = new Date(m.startTime).getTime();\n return Number.isFinite(t) && t > max ? t : max;\n },\n Number.NEGATIVE_INFINITY,\n );\n const anchorWeek = startOfWeek(\n Number.isFinite(latestMs) ? new Date(latestMs) : new Date(),\n );\n\n const weekBuckets = Array.from({ length: WEEKS_IN_TREND }, (_, i) => {\n const start = new Date(anchorWeek);\n start.setDate(start.getDate() - (WEEKS_IN_TREND - 1 - i) * 7);\n return { start: start.getTime(), label: weekLabel(start), minutes: 0 };\n });\n const bucketByStart = new Map(weekBuckets.map((b) => [b.start, b]));\n for (const m of meetings) {\n const t = new Date(m.startTime).getTime();\n if (!Number.isFinite(t)) continue;\n const bucket = bucketByStart.get(startOfWeek(new Date(t)).getTime());\n if (bucket) bucket.minutes += m.duration ?? 0;\n }\n const trendData = weekBuckets.map((b) => ({\n week: b.label,\n hours: roundHours(b.minutes),\n }));\n const hasTrend = meetings.length > 0;\n\n // --- Meeting type breakdown: real hours grouped by type ---\n const typeMinutes = new Map<MeetingType, number>();\n for (const m of meetings) {\n typeMinutes.set(\n m.meetingType,\n (typeMinutes.get(m.meetingType) ?? 0) + (m.duration ?? 0),\n );\n }\n const byTypeData = Array.from(typeMinutes.entries())\n .map(([type, minutes]) => ({\n type: meetingTypeLabel(type),\n hours: roundHours(minutes),\n }))\n .filter((d) => d.hours > 0)\n .sort((a, b) => b.hours - a.hours);\n\n // --- Action-item follow-through (aggregate) ---\n const actionSlices = [\n { name: \"Completed\", value: actionItemsCompleted },\n { name: \"Outstanding\", value: actionItemsOutstanding },\n ];\n\n return (\n <PageLayout\n title=\"Meeting Analytics\"\n description=\"Insights into meeting time and cost\"\n purpose=\"Put a number on your meeting habit — total hours, dollar cost and follow-through on action items — so you can cut the meetings that don't pay for themselves and reclaim time for focused work.\"\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 Meetings\"\n value={String(stats?.totalMeetings ?? 0)}\n icon={<CalendarDays className=\"h-5 w-5\" />}\n description={`${formatNumber(actionItemsTotal)} action items tracked`}\n />\n <StatCard\n title=\"Meeting Hours\"\n value={`${totalHours}h`}\n icon={<Clock className=\"h-5 w-5\" />}\n description={`${formatNumber(stats?.totalDuration ?? 0)} total minutes`}\n />\n <StatCard\n title=\"True Cost of Meetings\"\n value={formatCurrency(stats?.totalCost ?? 0)}\n icon={<DollarSign className=\"h-5 w-5\" />}\n description=\"Estimated productivity cost\"\n />\n <StatCard\n title=\"Avg. Duration\"\n value={`${avgDuration} min`}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={`${avgRating.toFixed(1)}★ average rating`}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Meeting Load Trend\"\n description=\"Meeting hours per week over the last 6 weeks\"\n >\n <TrendChart\n data={hasTrend ? trendData : []}\n xKey=\"week\"\n series={[{ key: \"hours\", label: \"Meeting hours\" }]}\n type=\"area\"\n height={240}\n valueFormat={formatHoursValue}\n emptyLabel=\"No meeting activity to chart yet.\"\n ariaLabel=\"Meeting hours per week\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Meeting Type Breakdown\"\n description=\"Hours spent by meeting type\"\n >\n <BarsChart\n data={byTypeData}\n xKey=\"type\"\n series={[{ key: \"hours\", label: \"Hours\" }]}\n orientation=\"horizontal\"\n height={240}\n valueFormat={formatHoursValue}\n categoryWidth={88}\n emptyLabel=\"No meetings recorded yet.\"\n ariaLabel=\"Meeting hours by type\"\n />\n </ChartCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Action Item Follow-through\"\n description=\"Completed vs. outstanding across all meetings\"\n >\n <DonutChart\n data={actionSlices}\n centerValue={actionItemsTotal}\n centerLabel=\"action items\"\n height={240}\n emptyLabel=\"No action items captured yet.\"\n ariaLabel=\"Action item completion\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Average Meeting Rating\"\n description=\"How worthwhile meetings have felt, out of 5\"\n >\n <div className=\"flex items-center justify-center\">\n <RadialGauge\n value={avgRating}\n min={0}\n max={5}\n label=\"out of 5\"\n sublabel={`${formatNumber(stats?.totalMeetings ?? 0)} meetings`}\n height={240}\n format={(v) => v.toFixed(1)}\n ariaLabel=\"Average meeting rating out of 5\"\n />\n </div>\n </ChartCard>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MeetingsPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/meetings/MeetingsPage.tsx"],"names":[],"mappings":"AA2GA,wBAAgB,YAAY,
|
|
1
|
+
{"version":3,"file":"MeetingsPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/meetings/MeetingsPage.tsx"],"names":[],"mappings":"AA2GA,wBAAgB,YAAY,gCAwS3B"}
|
|
@@ -62,8 +62,10 @@ function N() {
|
|
|
62
62
|
let { workspaceId: n = "" } = e(), h = t(), [b, O] = _(D), [A, N] = _(!1), [P, F] = _(null), [I, L] = _(null), { data: R, loading: z, error: B, refetch: V, fetchMore: H } = l({
|
|
63
63
|
variables: {
|
|
64
64
|
workspaceId: n,
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
pagination: {
|
|
66
|
+
limit: b,
|
|
67
|
+
offset: 0
|
|
68
|
+
}
|
|
67
69
|
},
|
|
68
70
|
skip: !n,
|
|
69
71
|
notifyOnNetworkStatusChange: !0
|
|
@@ -82,10 +84,10 @@ function N() {
|
|
|
82
84
|
} }), L(null), await V());
|
|
83
85
|
}, ie = async () => {
|
|
84
86
|
let e = b + D;
|
|
85
|
-
O(e), await H({ variables: {
|
|
87
|
+
O(e), await H({ variables: { pagination: {
|
|
86
88
|
limit: e,
|
|
87
89
|
offset: 0
|
|
88
|
-
} });
|
|
90
|
+
} } });
|
|
89
91
|
}, $ = /* @__PURE__ */ T("button", {
|
|
90
92
|
onClick: () => N(!0),
|
|
91
93
|
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",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MeetingsPage.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingsPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { CalendarDays, Plus, Video, Pencil, Trash2, Star } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n cn,\n formatCurrency,\n formatDate,\n formatTime,\n} from \"../../utils/helpers\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n useListMeetingsQuery,\n useCreateMeetingMutation,\n useUpdateMeetingMutation,\n useDeleteMeetingMutation,\n type MeetingFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport {\n MeetingType,\n type CreateMeetingInput,\n type UpdateMeetingInput,\n} from \"@/generated/wspace-types\";\nimport { MeetingFormModal } from \"./MeetingFormModal\";\n\nconst PAGE_SIZE = 25;\n\nconst MEETING_TYPE_LABELS: Record<MeetingType, string> = {\n [MeetingType.Standup]: \"Standup\",\n [MeetingType.OneOnOne]: \"1-on-1\",\n [MeetingType.Team]: \"Team\",\n [MeetingType.Review]: \"Review\",\n [MeetingType.Planning]: \"Planning\",\n [MeetingType.Retrospective]: \"Retro\",\n [MeetingType.External]: \"External\",\n};\n\nfunction meetingTypeLabel(type: MeetingType): string {\n return MEETING_TYPE_LABELS[type] ?? type;\n}\n\n/** Deterministic 2-char monogram from an opaque participant id. */\nfunction idMonogram(id: string): string {\n return (\n id\n .replace(/[^a-zA-Z0-9]/g, \"\")\n .slice(0, 2)\n .toUpperCase() || \"?\"\n );\n}\n\nfunction ParticipantAvatars({\n participants,\n}: {\n participants: MeetingFieldsFragment[\"participants\"];\n}) {\n const shown = participants.slice(0, 4);\n const overflow = participants.length - shown.length;\n return (\n <div className=\"flex items-center -space-x-2\">\n {shown.map((p) => (\n <span\n key={p.id}\n title={p.userId}\n className=\"flex h-8 w-8 items-center justify-center rounded-full bg-action-primary-bg/10 text-[11px] font-semibold text-primary ring-2 ring-bg-surface\"\n >\n {idMonogram(p.userId)}\n </span>\n ))}\n {overflow > 0 && (\n <span className=\"flex h-8 w-8 items-center justify-center rounded-full bg-bg-sunken text-[11px] font-semibold text-text-secondary ring-2 ring-bg-surface\">\n +{overflow}\n </span>\n )}\n </div>\n );\n}\n\nfunction StarRating({ rating }: { rating: number | null | undefined }) {\n const value = rating ?? 0;\n return (\n <div className=\"flex items-center gap-0.5\">\n {[1, 2, 3, 4, 5].map((v) => (\n <Star\n key={v}\n className={cn(\n \"h-3.5 w-3.5\",\n v <= value\n ? \"fill-status-warning-text text-status-warning-text\"\n : \"text-border-seam\",\n )}\n />\n ))}\n <span className=\"ml-1 text-xs font-medium tabular-nums text-text-secondary\">\n {rating != null ? rating.toFixed(1) : \"—\"}\n </span>\n </div>\n );\n}\n\nexport function MeetingsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n\n const [limit, setLimit] = useState(PAGE_SIZE);\n const [createOpen, setCreateOpen] = useState(false);\n const [editing, setEditing] = useState<MeetingFieldsFragment | null>(null);\n const [deleting, setDeleting] = useState<MeetingFieldsFragment | null>(null);\n\n const { data, loading, error, refetch, fetchMore } = useListMeetingsQuery({\n variables: { workspaceId, limit, offset: 0 },\n skip: !workspaceId,\n notifyOnNetworkStatusChange: true,\n });\n\n const [createMeeting, { loading: creating, error: createError }] =\n useCreateMeetingMutation();\n const [updateMeeting, { loading: updating, error: updateError }] =\n useUpdateMeetingMutation();\n const [deleteMeeting, { loading: deletingLoading }] =\n useDeleteMeetingMutation();\n\n const meetings = data?.meetings ?? [];\n const hasMore = meetings.length >= limit;\n\n const handleCreate = async (input: CreateMeetingInput) => {\n await createMeeting({ variables: { input } });\n await refetch();\n setCreateOpen(false);\n };\n\n const handleUpdate = async (input: UpdateMeetingInput) => {\n if (!editing) return;\n await updateMeeting({\n variables: { id: editing.id, workspaceId, input },\n });\n await refetch();\n setEditing(null);\n };\n\n const handleDelete = async () => {\n if (!deleting) return;\n await deleteMeeting({\n variables: { id: deleting.id, workspaceId },\n });\n setDeleting(null);\n await refetch();\n };\n\n const handleLoadMore = async () => {\n const nextLimit = limit + PAGE_SIZE;\n setLimit(nextLimit);\n await fetchMore({ variables: { limit: nextLimit, offset: 0 } });\n };\n\n const addButton = (\n <button\n onClick={() => setCreateOpen(true)}\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 Schedule Meeting\n </button>\n );\n\n if (loading && meetings.length === 0) {\n return (\n <PageLayout\n title=\"Meetings\"\n description=\"Schedule and track meetings\"\n actions={addButton}\n >\n <LoadingSkeleton variant=\"list\" count={5} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout title=\"Meetings\" description=\"Schedule and track meetings\">\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 meetings. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Meetings\"\n description=\"Schedule and track meetings\"\n purpose=\"Keep every meeting and its recording in one place and see how much of your week goes to them — so you can protect focus time, trim low-value calls, and account for meeting hours in your tracking.\"\n actions={addButton}\n >\n {meetings.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No meetings found\"\n description=\"Schedule your first meeting to start tracking calls and their time impact.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"space-y-3\">\n {meetings.map((meeting: MeetingFieldsFragment) => (\n <GlassCard\n key={meeting.id}\n treatment=\"glass\"\n role=\"button\"\n tabIndex={0}\n onClick={() => navigate(`/meetings/${meeting.id}`)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n navigate(`/meetings/${meeting.id}`);\n }\n }}\n className=\"cursor-pointer p-4 transition-colors hover:bg-bg-sunken/30 sm:p-5\"\n >\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between\">\n {/* Title + meta */}\n <div className=\"flex min-w-0 items-start gap-3\">\n <div className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <CalendarDays className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {meeting.title}\n </p>\n {meeting.recordingUrl && (\n <Video\n className=\"h-3.5 w-3.5 shrink-0 text-primary\"\n aria-label=\"Has recording\"\n />\n )}\n </div>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatDate(meeting.startTime)} ·{\" \"}\n {formatTime(meeting.startTime)}\n {meeting.endTime\n ? `–${formatTime(meeting.endTime)}`\n : \"\"}{\" \"}\n · {meeting.duration ?? 0} min\n </p>\n <div className=\"mt-2 flex flex-wrap items-center gap-1.5\">\n <StatusBadge\n label={meetingTypeLabel(meeting.meetingType)}\n variant=\"default\"\n />\n <StatusBadge\n label={meeting.endTime ? \"Completed\" : \"Scheduled\"}\n variant={meeting.endTime ? \"success\" : \"info\"}\n dot\n />\n </div>\n </div>\n </div>\n\n {/* Metrics + actions */}\n <div className=\"flex flex-wrap items-center gap-x-6 gap-y-3 lg:shrink-0 lg:justify-end\">\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Est. cost\n </p>\n <p className=\"text-sm font-semibold tabular-nums text-primary\">\n {meeting.cost != null\n ? formatCurrency(meeting.cost)\n : \"—\"}\n </p>\n </div>\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Rating\n </p>\n <StarRating rating={meeting.rating} />\n </div>\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n {meeting.participants.length}{\" \"}\n {meeting.participants.length === 1\n ? \"participant\"\n : \"participants\"}\n </p>\n {meeting.participants.length > 0 ? (\n <div className=\"mt-0.5\">\n <ParticipantAvatars\n participants={meeting.participants}\n />\n </div>\n ) : (\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n None yet\n </p>\n )}\n </div>\n <div className=\"flex items-center gap-1\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n setEditing(meeting);\n }}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Edit meeting\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n onClick={(e) => {\n e.stopPropagation();\n setDeleting(meeting);\n }}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Delete meeting\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </div>\n </GlassCard>\n ))}\n </div>\n {hasMore && (\n <div className=\"mt-4 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loading}\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 {loading ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n {createOpen && (\n <MeetingFormModal\n open\n onClose={() => setCreateOpen(false)}\n workspaceId={workspaceId}\n submitting={creating}\n error={Boolean(createError)}\n onCreate={handleCreate}\n />\n )}\n\n {editing && (\n <MeetingFormModal\n open\n onClose={() => setEditing(null)}\n workspaceId={workspaceId}\n meeting={editing}\n submitting={updating}\n error={Boolean(updateError)}\n onUpdate={handleUpdate}\n />\n )}\n\n <Modal\n open={Boolean(deleting)}\n onClose={() => setDeleting(null)}\n title=\"Delete meeting\"\n description=\"This permanently removes the meeting and its analytics.\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleting(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={deletingLoading}\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/90 disabled:opacity-50\"\n >\n {deletingLoading ? \"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 {deleting?.title}\n </span>\n ? This action cannot be undone.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MeetingsPage.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingsPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { CalendarDays, Plus, Video, Pencil, Trash2, Star } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n cn,\n formatCurrency,\n formatDate,\n formatTime,\n} from \"../../utils/helpers\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n useListMeetingsQuery,\n useCreateMeetingMutation,\n useUpdateMeetingMutation,\n useDeleteMeetingMutation,\n type MeetingFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport {\n MeetingType,\n type CreateMeetingInput,\n type UpdateMeetingInput,\n} from \"@/generated/wspace-types\";\nimport { MeetingFormModal } from \"./MeetingFormModal\";\n\nconst PAGE_SIZE = 25;\n\nconst MEETING_TYPE_LABELS: Record<MeetingType, string> = {\n [MeetingType.Standup]: \"Standup\",\n [MeetingType.OneOnOne]: \"1-on-1\",\n [MeetingType.Team]: \"Team\",\n [MeetingType.Review]: \"Review\",\n [MeetingType.Planning]: \"Planning\",\n [MeetingType.Retrospective]: \"Retro\",\n [MeetingType.External]: \"External\",\n};\n\nfunction meetingTypeLabel(type: MeetingType): string {\n return MEETING_TYPE_LABELS[type] ?? type;\n}\n\n/** Deterministic 2-char monogram from an opaque participant id. */\nfunction idMonogram(id: string): string {\n return (\n id\n .replace(/[^a-zA-Z0-9]/g, \"\")\n .slice(0, 2)\n .toUpperCase() || \"?\"\n );\n}\n\nfunction ParticipantAvatars({\n participants,\n}: {\n participants: MeetingFieldsFragment[\"participants\"];\n}) {\n const shown = participants.slice(0, 4);\n const overflow = participants.length - shown.length;\n return (\n <div className=\"flex items-center -space-x-2\">\n {shown.map((p) => (\n <span\n key={p.id}\n title={p.userId}\n className=\"flex h-8 w-8 items-center justify-center rounded-full bg-action-primary-bg/10 text-[11px] font-semibold text-primary ring-2 ring-bg-surface\"\n >\n {idMonogram(p.userId)}\n </span>\n ))}\n {overflow > 0 && (\n <span className=\"flex h-8 w-8 items-center justify-center rounded-full bg-bg-sunken text-[11px] font-semibold text-text-secondary ring-2 ring-bg-surface\">\n +{overflow}\n </span>\n )}\n </div>\n );\n}\n\nfunction StarRating({ rating }: { rating: number | null | undefined }) {\n const value = rating ?? 0;\n return (\n <div className=\"flex items-center gap-0.5\">\n {[1, 2, 3, 4, 5].map((v) => (\n <Star\n key={v}\n className={cn(\n \"h-3.5 w-3.5\",\n v <= value\n ? \"fill-status-warning-text text-status-warning-text\"\n : \"text-border-seam\",\n )}\n />\n ))}\n <span className=\"ml-1 text-xs font-medium tabular-nums text-text-secondary\">\n {rating != null ? rating.toFixed(1) : \"—\"}\n </span>\n </div>\n );\n}\n\nexport function MeetingsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n\n const [limit, setLimit] = useState(PAGE_SIZE);\n const [createOpen, setCreateOpen] = useState(false);\n const [editing, setEditing] = useState<MeetingFieldsFragment | null>(null);\n const [deleting, setDeleting] = useState<MeetingFieldsFragment | null>(null);\n\n const { data, loading, error, refetch, fetchMore } = useListMeetingsQuery({\n variables: { workspaceId, pagination: { limit, offset: 0 } },\n skip: !workspaceId,\n notifyOnNetworkStatusChange: true,\n });\n\n const [createMeeting, { loading: creating, error: createError }] =\n useCreateMeetingMutation();\n const [updateMeeting, { loading: updating, error: updateError }] =\n useUpdateMeetingMutation();\n const [deleteMeeting, { loading: deletingLoading }] =\n useDeleteMeetingMutation();\n\n const meetings = data?.meetings ?? [];\n const hasMore = meetings.length >= limit;\n\n const handleCreate = async (input: CreateMeetingInput) => {\n await createMeeting({ variables: { input } });\n await refetch();\n setCreateOpen(false);\n };\n\n const handleUpdate = async (input: UpdateMeetingInput) => {\n if (!editing) return;\n await updateMeeting({\n variables: { id: editing.id, workspaceId, input },\n });\n await refetch();\n setEditing(null);\n };\n\n const handleDelete = async () => {\n if (!deleting) return;\n await deleteMeeting({\n variables: { id: deleting.id, workspaceId },\n });\n setDeleting(null);\n await refetch();\n };\n\n const handleLoadMore = async () => {\n const nextLimit = limit + PAGE_SIZE;\n setLimit(nextLimit);\n await fetchMore({\n variables: { pagination: { limit: nextLimit, offset: 0 } },\n });\n };\n\n const addButton = (\n <button\n onClick={() => setCreateOpen(true)}\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 Schedule Meeting\n </button>\n );\n\n if (loading && meetings.length === 0) {\n return (\n <PageLayout\n title=\"Meetings\"\n description=\"Schedule and track meetings\"\n actions={addButton}\n >\n <LoadingSkeleton variant=\"list\" count={5} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout title=\"Meetings\" description=\"Schedule and track meetings\">\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 meetings. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Meetings\"\n description=\"Schedule and track meetings\"\n purpose=\"Keep every meeting and its recording in one place and see how much of your week goes to them — so you can protect focus time, trim low-value calls, and account for meeting hours in your tracking.\"\n actions={addButton}\n >\n {meetings.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No meetings found\"\n description=\"Schedule your first meeting to start tracking calls and their time impact.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"space-y-3\">\n {meetings.map((meeting: MeetingFieldsFragment) => (\n <GlassCard\n key={meeting.id}\n treatment=\"glass\"\n role=\"button\"\n tabIndex={0}\n onClick={() => navigate(`/meetings/${meeting.id}`)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n navigate(`/meetings/${meeting.id}`);\n }\n }}\n className=\"cursor-pointer p-4 transition-colors hover:bg-bg-sunken/30 sm:p-5\"\n >\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between\">\n {/* Title + meta */}\n <div className=\"flex min-w-0 items-start gap-3\">\n <div className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <CalendarDays className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {meeting.title}\n </p>\n {meeting.recordingUrl && (\n <Video\n className=\"h-3.5 w-3.5 shrink-0 text-primary\"\n aria-label=\"Has recording\"\n />\n )}\n </div>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatDate(meeting.startTime)} ·{\" \"}\n {formatTime(meeting.startTime)}\n {meeting.endTime\n ? `–${formatTime(meeting.endTime)}`\n : \"\"}{\" \"}\n · {meeting.duration ?? 0} min\n </p>\n <div className=\"mt-2 flex flex-wrap items-center gap-1.5\">\n <StatusBadge\n label={meetingTypeLabel(meeting.meetingType)}\n variant=\"default\"\n />\n <StatusBadge\n label={meeting.endTime ? \"Completed\" : \"Scheduled\"}\n variant={meeting.endTime ? \"success\" : \"info\"}\n dot\n />\n </div>\n </div>\n </div>\n\n {/* Metrics + actions */}\n <div className=\"flex flex-wrap items-center gap-x-6 gap-y-3 lg:shrink-0 lg:justify-end\">\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Est. cost\n </p>\n <p className=\"text-sm font-semibold tabular-nums text-primary\">\n {meeting.cost != null\n ? formatCurrency(meeting.cost)\n : \"—\"}\n </p>\n </div>\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Rating\n </p>\n <StarRating rating={meeting.rating} />\n </div>\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n {meeting.participants.length}{\" \"}\n {meeting.participants.length === 1\n ? \"participant\"\n : \"participants\"}\n </p>\n {meeting.participants.length > 0 ? (\n <div className=\"mt-0.5\">\n <ParticipantAvatars\n participants={meeting.participants}\n />\n </div>\n ) : (\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n None yet\n </p>\n )}\n </div>\n <div className=\"flex items-center gap-1\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n setEditing(meeting);\n }}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Edit meeting\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n onClick={(e) => {\n e.stopPropagation();\n setDeleting(meeting);\n }}\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Delete meeting\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </div>\n </GlassCard>\n ))}\n </div>\n {hasMore && (\n <div className=\"mt-4 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loading}\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 {loading ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n {createOpen && (\n <MeetingFormModal\n open\n onClose={() => setCreateOpen(false)}\n workspaceId={workspaceId}\n submitting={creating}\n error={Boolean(createError)}\n onCreate={handleCreate}\n />\n )}\n\n {editing && (\n <MeetingFormModal\n open\n onClose={() => setEditing(null)}\n workspaceId={workspaceId}\n meeting={editing}\n submitting={updating}\n error={Boolean(updateError)}\n onUpdate={handleUpdate}\n />\n )}\n\n <Modal\n open={Boolean(deleting)}\n onClose={() => setDeleting(null)}\n title=\"Delete meeting\"\n description=\"This permanently removes the meeting and its analytics.\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleting(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={deletingLoading}\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/90 disabled:opacity-50\"\n >\n {deletingLoading ? \"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 {deleting?.title}\n </span>\n ? This action cannot be undone.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgCA,IAAM,IAAY,IAEZ,IAAmD;EACtD,EAAY,UAAU;EACtB,EAAY,WAAW;EACvB,EAAY,OAAO;EACnB,EAAY,SAAS;EACrB,EAAY,WAAW;EACvB,EAAY,gBAAgB;EAC5B,EAAY,WAAW;CACzB;AAED,SAAS,EAAiB,GAA2B;AACnD,QAAO,EAAoB,MAAS;;AAItC,SAAS,EAAW,GAAoB;AACtC,QACE,EACG,QAAQ,iBAAiB,GAAG,CAC5B,MAAM,GAAG,EAAE,CACX,aAAa,IAAI;;AAIxB,SAAS,EAAmB,EAC1B,mBAGC;CACD,IAAM,IAAQ,EAAa,MAAM,GAAG,EAAE,EAChC,IAAW,EAAa,SAAS,EAAM;AAC7C,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAM,KAAK,MACV,kBAAC,QAAD;GAEE,OAAO,EAAE;GACT,WAAU;aAET,EAAW,EAAE,OAAO;GAChB,EALA,EAAE,GAKF,CACP,EACD,IAAW,KACV,kBAAC,QAAD;GAAM,WAAU;aAAhB,CAA0J,KACtJ,EACG;KAEL;;;AAIV,SAAS,EAAW,EAAE,aAAiD;CACrE,IAAM,IAAQ,KAAU;AACxB,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG;GAAC;GAAG;GAAG;GAAG;GAAG;GAAE,CAAC,KAAK,MACpB,kBAAC,GAAD,EAEE,WAAW,EACT,eACA,KAAK,IACD,sDACA,mBACL,EACD,EAPK,EAOL,CACF,EACF,kBAAC,QAAD;GAAM,WAAU;aACb,KAAU,OAA2B,MAApB,EAAO,QAAQ,EAAE;GAC9B,CAAA,CACH;;;AAIV,SAAgB,IAAe;CAC7B,IAAM,EAAE,iBAAc,OAAO,GAAe,EACtC,IAAW,GAAuB,EAElC,CAAC,GAAO,KAAY,EAAS,EAAU,EACvC,CAAC,GAAY,KAAiB,EAAS,GAAM,EAC7C,CAAC,GAAS,KAAc,EAAuC,KAAK,EACpE,CAAC,GAAU,KAAe,EAAuC,KAAK,EAEtE,EAAE,SAAM,YAAS,UAAO,YAAS,iBAAc,EAAqB;EACxE,WAAW;GAAE;GAAa,YAAY;IAAE;IAAO,QAAQ;IAAG;GAAE;EAC5D,MAAM,CAAC;EACP,6BAA6B;EAC9B,CAAC,EAEI,CAAC,GAAe,EAAE,SAAS,GAAU,OAAO,OAChD,GAA0B,EACtB,CAAC,GAAe,EAAE,SAAS,GAAU,OAAO,OAChD,GAA0B,EACtB,CAAC,GAAe,EAAE,SAAS,OAC/B,GAA0B,EAEtB,IAAW,GAAM,YAAY,EAAE,EAC/B,IAAU,EAAS,UAAU,GAE7B,KAAe,OAAO,MAA8B;AAGxD,EAFA,MAAM,EAAc,EAAE,WAAW,EAAE,UAAO,EAAE,CAAC,EAC7C,MAAM,GAAS,EACf,EAAc,GAAM;IAGhB,KAAe,OAAO,MAA8B;AACnD,QACL,MAAM,EAAc,EAClB,WAAW;GAAE,IAAI,EAAQ;GAAI;GAAa;GAAO,EAClD,CAAC,EACF,MAAM,GAAS,EACf,EAAW,KAAK;IAGZ,KAAe,YAAY;AAC1B,QACL,MAAM,EAAc,EAClB,WAAW;GAAE,IAAI,EAAS;GAAI;GAAa,EAC5C,CAAC,EACF,EAAY,KAAK,EACjB,MAAM,GAAS;IAGX,KAAiB,YAAY;EACjC,IAAM,IAAY,IAAQ;AAE1B,EADA,EAAS,EAAU,EACnB,MAAM,EAAU,EACd,WAAW,EAAE,YAAY;GAAE,OAAO;GAAW,QAAQ;GAAG,EAAE,EAC3D,CAAC;IAGE,IACJ,kBAAC,UAAD;EACE,eAAe,EAAc,GAAK;EAClC,WAAU;YAFZ,CAIE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,mBAErB;;AAyBX,QAtBI,KAAW,EAAS,WAAW,IAE/B,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SAAS;YAET,kBAAC,GAAD;GAAiB,SAAQ;GAAO,OAAO;GAAK,CAAA;EACjC,CAAA,GAIb,IAEA,kBAAC,GAAD;EAAY,OAAM;EAAW,aAAY;YACvC,kBAAC,OAAD;GAAK,WAAU;aAAoG;GAE7G,CAAA;EACK,CAAA,GAKf,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SAAQ;EACR,SAAS;YAJX;GAMG,EAAS,WAAW,IACnB,kBAAC,GAAD;IACE,cAAa;IACb,OAAM;IACN,aAAY;IACZ,QAAQ;IACR,CAAA,GAEF,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAS,KAAK,MACb,kBAAC,GAAD;KAEE,WAAU;KACV,MAAK;KACL,UAAU;KACV,eAAe,EAAS,aAAa,EAAQ,KAAK;KAClD,YAAY,MAAM;AAChB,OAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,gBAAgB,EAClB,EAAS,aAAa,EAAQ,KAAK;;KAGvC,WAAU;eAEV,kBAAC,OAAD;MAAK,WAAU;gBAAf,CAEE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,IAAD,EAAc,WAAU,WAAY,CAAA;QAChC,CAAA,EACN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAQ;WACP,CAAA,EACH,EAAQ,gBACP,kBAAC,GAAD;WACE,WAAU;WACV,cAAW;WACX,CAAA,CAEA;;SACN,kBAAC,KAAD;UAAG,WAAU;oBAAb;WACG,EAAW,EAAQ,UAAU;WAAC;WAAU;WACxC,EAAW,EAAQ,UAAU;WAC7B,EAAQ,UACL,IAAI,EAAW,EAAQ,QAAQ,KAC/B;WAAI;WAAI;WACF,EAAQ,YAAY;WAAE;WAC9B;;SACJ,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,GAAD;WACE,OAAO,EAAiB,EAAQ,YAAY;WAC5C,SAAQ;WACR,CAAA,EACF,kBAAC,GAAD;WACE,OAAO,EAAQ,UAAU,cAAc;WACvC,SAAS,EAAQ,UAAU,YAAY;WACvC,KAAA;WACA,CAAA,CACE;;SACF;UACF;UAGN,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAkE;SAE3E,CAAA,EACJ,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAQ,QAAQ,OAEb,MADA,EAAe,EAAQ,KAAK;SAE9B,CAAA,CACA,EAAA,CAAA;QACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAkE;SAE3E,CAAA,EACJ,kBAAC,GAAD,EAAY,QAAQ,EAAQ,QAAU,CAAA,CAClC,EAAA,CAAA;QACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAb;UACG,EAAQ,aAAa;UAAQ;UAC7B,EAAQ,aAAa,WAAW,IAC7B,gBACA;UACF;YACH,EAAQ,aAAa,SAAS,IAC7B,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,GAAD,EACE,cAAc,EAAQ,cACtB,CAAA;SACE,CAAA,GAEN,kBAAC,KAAD;SAAG,WAAU;mBAAqC;SAE9C,CAAA,CAEF,EAAA,CAAA;QACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,UAAD;UACE,UAAU,MAAM;AAEd,WADA,EAAE,iBAAiB,EACnB,EAAW,EAAQ;;UAErB,WAAU;UACV,cAAW;oBAEX,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;UACvB,CAAA,EACT,kBAAC,UAAD;UACE,UAAU,MAAM;AAEd,WADA,EAAE,iBAAiB,EACnB,EAAY,EAAQ;;UAEtB,WAAU;UACV,cAAW;oBAEX,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;UACvB,CAAA,CACL;;QACF;SACF;;KACI,EAlHL,EAAQ,GAkHH,CACZ;IACE,CAAA,EACL,KACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,UAAD;KACE,SAAS;KACT,UAAU;KACV,WAAU;eAET,IAAU,eAAe;KACnB,CAAA;IACL,CAAA,CAEP,EAAA,CAAA;GAGJ,KACC,kBAAC,GAAD;IACE,MAAA;IACA,eAAe,EAAc,GAAM;IACtB;IACb,YAAY;IACZ,OAAO,EAAQ;IACf,UAAU;IACV,CAAA;GAGH,KACC,kBAAC,GAAD;IACE,MAAA;IACA,eAAe,EAAW,KAAK;IAClB;IACb,SAAS;IACT,YAAY;IACZ,OAAO,EAAQ;IACf,UAAU;IACV,CAAA;GAGJ,kBAAC,GAAD;IACE,MAAM,EAAQ;IACd,eAAe,EAAY,KAAK;IAChC,OAAM;IACN,aAAY;IACZ,MAAK;IACL,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,eAAe,EAAY,KAAK;KAChC,WAAU;eACX;KAEQ,CAAA,EACT,kBAAC,UAAD;KACE,SAAS;KACT,UAAU;KACV,WAAU;eAET,IAAkB,gBAAgB;KAC5B,CAAA,CACR,EAAA,CAAA;cAGL,kBAAC,KAAD;KAAG,WAAU;eAAb;MAA2C;MACT;MAChC,kBAAC,QAAD;OAAM,WAAU;iBACb,GAAU;OACN,CAAA;;MAEL;;IACE,CAAA;GACG"}
|