@burdenoff/microfe-timecampus 2026.719.2 → 2026.719.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-operations.d.ts +193 -76
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-operations.js +859 -788
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.d.ts +56 -38
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/index.js +5 -5
- 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 +19 -13
- 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/TeamAnalyticsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/team/TeamAnalyticsPage.js +2 -2
- package/dist/timecampus/pages/team/TeamAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.d.ts.map +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js +49 -45
- 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/timeline/TimelinePage.d.ts.map +1 -1
- package/dist/timecampus/pages/timeline/TimelinePage.js +224 -162
- package/dist/timecampus/pages/timeline/TimelinePage.js.map +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
- package/package.json +2 -1
|
@@ -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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PomodoroAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/pomodoro/PomodoroAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, Target, Flame, Timer, Coffee } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n BarsChart,\n DonutChart,\n ProgressMeter,\n StatusBadge,\n LoadingSkeleton,\n formatNumber,\n type DonutSlice,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetPomodoroStatsQuery,\n useListPomodoroSessionsQuery,\n type PomodoroSessionFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { PomodoroSessionType } from \"@/generated/wspace-types\";\n\nfunction formatMinutes(min: number): string {\n const total = Math.max(0, Math.round(min));\n const h = Math.floor(total / 60);\n const m = total % 60;\n return h > 0 ? `${h}h ${m}m` : `${m}m`;\n}\n\nfunction shortDay(ts: number): string {\n return new Date(ts).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n });\n}\n\nfunction shortTime(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\nconst isBreak = (t: PomodoroSessionType): boolean =>\n t === PomodoroSessionType.ShortBreak || t === PomodoroSessionType.LongBreak;\n\nexport function PomodoroAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetPomodoroStatsQuery({\n variables: { workspaceId },\n skip: !workspaceId,\n });\n\n const { data: sessionsData, loading: sessionsLoading } =\n useListPomodoroSessionsQuery({\n variables: { workspaceId, limit: 50 },\n skip: !workspaceId,\n });\n\n const stats = statsData?.pomodoroStats;\n const sessions = sessionsData?.pomodoroSessions ?? [];\n\n if (statsLoading || sessionsLoading) {\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions 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 pomodoro analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalFocusH = Math.floor((stats?.totalFocusMinutes ?? 0) / 60);\n const totalFocusM = (stats?.totalFocusMinutes ?? 0) % 60;\n const avgSessions = stats?.totalSessions\n ? (stats.totalSessions / 30).toFixed(1)\n : \"0\";\n\n // ---- Daily completed focus sessions (BarsChart) --------------------------\n const dailyMap = new Map<\n string,\n { ts: number; label: string; completed: number }\n >();\n for (const s of sessions) {\n if (s.type !== PomodoroSessionType.Focus) continue;\n const d = new Date(s.startTime);\n if (Number.isNaN(d.getTime())) continue;\n const day = new Date(d.getFullYear(), d.getMonth(), d.getDate());\n const key = String(day.getTime());\n const entry = dailyMap.get(key) ?? {\n ts: day.getTime(),\n label: shortDay(day.getTime()),\n completed: 0,\n };\n if (s.completed) entry.completed += 1;\n dailyMap.set(key, entry);\n }\n const dailyData = [...dailyMap.values()]\n .sort((a, b) => a.ts - b.ts)\n .slice(-14)\n .map((e) => ({ day: e.label, completed: e.completed }));\n\n // ---- Focus vs break split (DonutChart) -----------------------------------\n let focusSeconds = 0;\n let breakSeconds = 0;\n let focusCount = 0;\n let breakCount = 0;\n for (const s of sessions) {\n const dur =\n typeof s.duration === \"number\" && Number.isFinite(s.duration)\n ? s.duration\n : 0;\n if (isBreak(s.type)) {\n breakSeconds += dur;\n breakCount += 1;\n } else {\n focusSeconds += dur;\n focusCount += 1;\n }\n }\n const useDuration = focusSeconds + breakSeconds > 0;\n const focusValue = useDuration ? Math.round(focusSeconds / 60) : focusCount;\n const breakValue = useDuration ? Math.round(breakSeconds / 60) : breakCount;\n const donutSlices: DonutSlice[] = [\n { name: \"Focus\", value: focusValue },\n { name: \"Break\", value: breakValue },\n ];\n const donutFormat = useDuration ? formatMinutes : formatNumber;\n\n // ---- Recent sessions -----------------------------------------------------\n const recentSessions = [...sessions]\n .sort(\n (a, b) =>\n new Date(b.startTime).getTime() - new Date(a.startTime).getTime(),\n )\n .slice(0, 6);\n\n const completionRate = stats?.completionRate ?? 0;\n const completedSessions = stats?.completedSessions ?? 0;\n const totalSessions = stats?.totalSessions ?? 0;\n\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n purpose=\"See whether your focus habit is sticking — completion rate, streaks and focus minutes over time — so you can tell good-focus days from distracted ones and keep your momentum going.\"\n >\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Sessions\"\n value={String(stats?.totalSessions ?? 0)}\n trend={{\n value: stats?.completionRate ?? 0,\n label: \"completion rate\",\n }}\n />\n <StatCard\n title=\"Total Focus Time\"\n value={`${totalFocusH}h ${totalFocusM}m`}\n icon={<Timer className=\"h-5 w-5\" />}\n trend={{\n value: stats?.averageSessionMinutes ?? 0,\n label: \"avg min/session\",\n }}\n />\n <StatCard\n title=\"Avg. Sessions/Day\"\n value={avgSessions}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={`Target: ${stats?.totalSessions ?? 0} total`}\n />\n <StatCard\n title=\"Completion Rate\"\n value={`${stats?.completionRate ?? 0}%`}\n icon={<Target className=\"h-5 w-5\" />}\n trend={{ value: stats?.streakDays ?? 0, label: \"day streak\" }}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <ChartCard\n className=\"lg:col-span-2\"\n title=\"Daily Completed Focus Sessions\"\n description=\"Pomodoros you finished, by day\"\n >\n <BarsChart\n data={dailyData}\n xKey=\"day\"\n series={[{ key: \"completed\", label: \"Completed\" }]}\n height={260}\n emptyLabel=\"No completed focus sessions yet.\"\n ariaLabel=\"Completed focus sessions by day\"\n />\n </ChartCard>\n\n <GlassCard treatment=\"glass\" glow className=\"p-5 sm:p-6\">\n <div className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Flame className=\"h-4 w-4 text-primary\" />\n Current Focus Streak\n </div>\n <p className=\"mt-2 text-4xl font-bold tabular-nums text-text-primary\">\n {stats?.streakDays ?? 0}\n <span className=\"ml-1 text-lg font-medium text-text-secondary\">\n days\n </span>\n </p>\n <div className=\"mt-6 space-y-5\">\n <ProgressMeter\n value={completionRate}\n max={100}\n higherIsWorse={false}\n label=\"Completion rate\"\n format={(v) => `${Math.round(v)}%`}\n />\n <ProgressMeter\n value={completedSessions}\n max={totalSessions > 0 ? totalSessions : 1}\n higherIsWorse={false}\n label=\"Sessions completed\"\n />\n </div>\n </GlassCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Focus vs Break\"\n description={\n useDuration\n ? \"Share of tracked time spent focusing vs. on breaks\"\n : \"Share of sessions by type\"\n }\n >\n <DonutChart\n data={donutSlices}\n centerLabel={useDuration ? \"total tracked\" : \"sessions\"}\n valueFormat={donutFormat}\n height={260}\n emptyLabel=\"No sessions recorded yet.\"\n ariaLabel=\"Focus versus break split\"\n />\n </ChartCard>\n\n <GlassCard treatment=\"glass\" className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Timer className=\"h-4 w-4 text-primary\" />\n Recent Sessions\n </h3>\n {recentSessions.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No focus sessions yet. Start a pomodoro to see it here.\n </p>\n ) : (\n <div className=\"space-y-2.5\">\n {recentSessions.map((s: PomodoroSessionFieldsFragment) => {\n const sessionIsBreak = isBreak(s.type);\n const durationLabel =\n typeof s.duration === \"number\" &&\n Number.isFinite(s.duration)\n ? `${Math.round(s.duration / 60)}m`\n : \"--\";\n return (\n <div\n key={s.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface/40 p-3\"\n >\n <div\n className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${\n sessionIsBreak\n ? \"bg-status-info-bg text-status-info-text\"\n : \"bg-action-primary-bg/10 text-primary\"\n }`}\n >\n {sessionIsBreak ? (\n <Coffee className=\"h-4 w-4\" />\n ) : (\n <Timer className=\"h-4 w-4\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {s.notes?.trim()\n ? s.notes\n : sessionIsBreak\n ? \"Break\"\n : \"Focus session\"}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {durationLabel} · {shortTime(s.startTime)}\n </p>\n </div>\n <StatusBadge\n variant={\n s.completed\n ? \"success\"\n : s.interrupted\n ? \"warning\"\n : \"muted\"\n }\n label={\n s.completed\n ? \"Completed\"\n : s.interrupted\n ? \"Interrupted\"\n : \"In progress\"\n }\n />\n </div>\n );\n })}\n </div>\n )}\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PomodoroAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/pomodoro/PomodoroAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, Target, Flame, Timer, Coffee } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n BarsChart,\n DonutChart,\n ProgressMeter,\n StatusBadge,\n LoadingSkeleton,\n formatNumber,\n type DonutSlice,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetPomodoroStatsQuery,\n useListPomodoroSessionsQuery,\n type PomodoroSessionFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { PomodoroSessionType } from \"@/generated/wspace-types\";\n\nfunction formatMinutes(min: number): string {\n const total = Math.max(0, Math.round(min));\n const h = Math.floor(total / 60);\n const m = total % 60;\n return h > 0 ? `${h}h ${m}m` : `${m}m`;\n}\n\nfunction shortDay(ts: number): string {\n return new Date(ts).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n });\n}\n\nfunction shortTime(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\nconst isBreak = (t: PomodoroSessionType): boolean =>\n t === PomodoroSessionType.ShortBreak || t === PomodoroSessionType.LongBreak;\n\nexport function PomodoroAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetPomodoroStatsQuery({\n variables: { workspaceId },\n skip: !workspaceId,\n });\n\n const { data: sessionsData, loading: sessionsLoading } =\n useListPomodoroSessionsQuery({\n variables: { workspaceId, pagination: { limit: 50 } },\n skip: !workspaceId,\n });\n\n const stats = statsData?.pomodoroStats;\n const sessions = sessionsData?.pomodoroSessions ?? [];\n\n if (statsLoading || sessionsLoading) {\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions 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 pomodoro analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalFocusH = Math.floor((stats?.totalFocusMinutes ?? 0) / 60);\n const totalFocusM = (stats?.totalFocusMinutes ?? 0) % 60;\n const avgSessions = stats?.totalSessions\n ? (stats.totalSessions / 30).toFixed(1)\n : \"0\";\n\n // ---- Daily completed focus sessions (BarsChart) --------------------------\n const dailyMap = new Map<\n string,\n { ts: number; label: string; completed: number }\n >();\n for (const s of sessions) {\n if (s.type !== PomodoroSessionType.Focus) continue;\n const d = new Date(s.startTime);\n if (Number.isNaN(d.getTime())) continue;\n const day = new Date(d.getFullYear(), d.getMonth(), d.getDate());\n const key = String(day.getTime());\n const entry = dailyMap.get(key) ?? {\n ts: day.getTime(),\n label: shortDay(day.getTime()),\n completed: 0,\n };\n if (s.completed) entry.completed += 1;\n dailyMap.set(key, entry);\n }\n const dailyData = [...dailyMap.values()]\n .sort((a, b) => a.ts - b.ts)\n .slice(-14)\n .map((e) => ({ day: e.label, completed: e.completed }));\n\n // ---- Focus vs break split (DonutChart) -----------------------------------\n let focusSeconds = 0;\n let breakSeconds = 0;\n let focusCount = 0;\n let breakCount = 0;\n for (const s of sessions) {\n const dur =\n typeof s.duration === \"number\" && Number.isFinite(s.duration)\n ? s.duration\n : 0;\n if (isBreak(s.type)) {\n breakSeconds += dur;\n breakCount += 1;\n } else {\n focusSeconds += dur;\n focusCount += 1;\n }\n }\n const useDuration = focusSeconds + breakSeconds > 0;\n const focusValue = useDuration ? Math.round(focusSeconds / 60) : focusCount;\n const breakValue = useDuration ? Math.round(breakSeconds / 60) : breakCount;\n const donutSlices: DonutSlice[] = [\n { name: \"Focus\", value: focusValue },\n { name: \"Break\", value: breakValue },\n ];\n const donutFormat = useDuration ? formatMinutes : formatNumber;\n\n // ---- Recent sessions -----------------------------------------------------\n const recentSessions = [...sessions]\n .sort(\n (a, b) =>\n new Date(b.startTime).getTime() - new Date(a.startTime).getTime(),\n )\n .slice(0, 6);\n\n const completionRate = stats?.completionRate ?? 0;\n const completedSessions = stats?.completedSessions ?? 0;\n const totalSessions = stats?.totalSessions ?? 0;\n\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n purpose=\"See whether your focus habit is sticking — completion rate, streaks and focus minutes over time — so you can tell good-focus days from distracted ones and keep your momentum going.\"\n >\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Total Sessions\"\n value={String(stats?.totalSessions ?? 0)}\n trend={{\n value: stats?.completionRate ?? 0,\n label: \"completion rate\",\n }}\n />\n <StatCard\n title=\"Total Focus Time\"\n value={`${totalFocusH}h ${totalFocusM}m`}\n icon={<Timer className=\"h-5 w-5\" />}\n trend={{\n value: stats?.averageSessionMinutes ?? 0,\n label: \"avg min/session\",\n }}\n />\n <StatCard\n title=\"Avg. Sessions/Day\"\n value={avgSessions}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={`Target: ${stats?.totalSessions ?? 0} total`}\n />\n <StatCard\n title=\"Completion Rate\"\n value={`${stats?.completionRate ?? 0}%`}\n icon={<Target className=\"h-5 w-5\" />}\n trend={{ value: stats?.streakDays ?? 0, label: \"day streak\" }}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <ChartCard\n className=\"lg:col-span-2\"\n title=\"Daily Completed Focus Sessions\"\n description=\"Pomodoros you finished, by day\"\n >\n <BarsChart\n data={dailyData}\n xKey=\"day\"\n series={[{ key: \"completed\", label: \"Completed\" }]}\n height={260}\n emptyLabel=\"No completed focus sessions yet.\"\n ariaLabel=\"Completed focus sessions by day\"\n />\n </ChartCard>\n\n <GlassCard treatment=\"glass\" glow className=\"p-5 sm:p-6\">\n <div className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Flame className=\"h-4 w-4 text-primary\" />\n Current Focus Streak\n </div>\n <p className=\"mt-2 text-4xl font-bold tabular-nums text-text-primary\">\n {stats?.streakDays ?? 0}\n <span className=\"ml-1 text-lg font-medium text-text-secondary\">\n days\n </span>\n </p>\n <div className=\"mt-6 space-y-5\">\n <ProgressMeter\n value={completionRate}\n max={100}\n higherIsWorse={false}\n label=\"Completion rate\"\n format={(v) => `${Math.round(v)}%`}\n />\n <ProgressMeter\n value={completedSessions}\n max={totalSessions > 0 ? totalSessions : 1}\n higherIsWorse={false}\n label=\"Sessions completed\"\n />\n </div>\n </GlassCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Focus vs Break\"\n description={\n useDuration\n ? \"Share of tracked time spent focusing vs. on breaks\"\n : \"Share of sessions by type\"\n }\n >\n <DonutChart\n data={donutSlices}\n centerLabel={useDuration ? \"total tracked\" : \"sessions\"}\n valueFormat={donutFormat}\n height={260}\n emptyLabel=\"No sessions recorded yet.\"\n ariaLabel=\"Focus versus break split\"\n />\n </ChartCard>\n\n <GlassCard treatment=\"glass\" className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Timer className=\"h-4 w-4 text-primary\" />\n Recent Sessions\n </h3>\n {recentSessions.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No focus sessions yet. Start a pomodoro to see it here.\n </p>\n ) : (\n <div className=\"space-y-2.5\">\n {recentSessions.map((s: PomodoroSessionFieldsFragment) => {\n const sessionIsBreak = isBreak(s.type);\n const durationLabel =\n typeof s.duration === \"number\" &&\n Number.isFinite(s.duration)\n ? `${Math.round(s.duration / 60)}m`\n : \"--\";\n return (\n <div\n key={s.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface/40 p-3\"\n >\n <div\n className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${\n sessionIsBreak\n ? \"bg-status-info-bg text-status-info-text\"\n : \"bg-action-primary-bg/10 text-primary\"\n }`}\n >\n {sessionIsBreak ? (\n <Coffee className=\"h-4 w-4\" />\n ) : (\n <Timer className=\"h-4 w-4\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {s.notes?.trim()\n ? s.notes\n : sessionIsBreak\n ? \"Break\"\n : \"Focus session\"}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {durationLabel} · {shortTime(s.startTime)}\n </p>\n </div>\n <StatusBadge\n variant={\n s.completed\n ? \"success\"\n : s.interrupted\n ? \"warning\"\n : \"muted\"\n }\n label={\n s.completed\n ? \"Completed\"\n : s.interrupted\n ? \"Interrupted\"\n : \"In progress\"\n }\n />\n </div>\n );\n })}\n </div>\n )}\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -28,10 +28,10 @@ function w() {
|
|
|
28
28
|
let n = w(e);
|
|
29
29
|
return n === e ? t : n;
|
|
30
30
|
}, E = e(), [D, O] = u(y), { data: k, loading: A, error: ne, refetch: j } = o({
|
|
31
|
-
variables: {
|
|
31
|
+
variables: { pagination: {
|
|
32
32
|
limit: D,
|
|
33
33
|
offset: 0
|
|
34
|
-
},
|
|
34
|
+
} },
|
|
35
35
|
notifyOnNetworkStatusChange: !0
|
|
36
36
|
}), M = k?.projects?.items ?? [], N = k?.projects?.hasMore ?? !1, [P, F] = u(!1), [I, L] = u(null), [R, z] = u(null), [B, V] = u(x), [H, { loading: U, error: W }] = i(), [G, { loading: K, error: q }] = s(), [J, { loading: Y }] = r(), [X, { loading: Z, error: re }] = a(), Q = () => {
|
|
37
37
|
V(x), F(!0);
|