@burdenoff/microfe-timecampus 2026.719.1 → 2026.719.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-operations.d.ts +40 -76
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-operations.js +40 -50
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.d.ts +21 -38
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/timecampus/pages/HomePage.js +1 -1
- package/dist/timecampus/pages/HomePage.js.map +1 -1
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +6 -3
- package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js +4 -4
- package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +8 -3
- package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +8 -4
- package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js +1 -1
- package/dist/timecampus/pages/focus-score/FocusScorePage.js.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/geofencing/GeofencingPage.js +4 -4
- package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +4 -4
- package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js +4 -2
- package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingDetailPage.d.ts.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingDetailPage.js +231 -226
- package/dist/timecampus/pages/meetings/MeetingDetailPage.js.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.d.ts.map +1 -1
- package/dist/timecampus/pages/meetings/MeetingsPage.js +6 -4
- package/dist/timecampus/pages/meetings/MeetingsPage.js.map +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js +1 -1
- package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/ProjectsPage.js +2 -2
- package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
- package/dist/timecampus/pages/projects/TasksPage.js +4 -2
- package/dist/timecampus/pages/projects/TasksPage.js.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.d.ts.map +1 -1
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +4 -4
- package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js +1 -1
- package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js +1 -1
- package/dist/timecampus/pages/team-activity/TeamActivityPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimeEntriesPage.js +137 -132
- package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimeTrackingPage.js +2 -2
- package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.js +2 -2
- package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +4 -4
- package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +1 -1
- package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MeetingDetailPage.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingDetailPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport {\n Users,\n Clock,\n Pencil,\n Trash2,\n Plus,\n Star,\n Check,\n UserMinus,\n} from \"lucide-react\";\nimport { NextSteps } from \"@burdenoff/fe-libs/ui\";\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 { cn, type BadgeVariant } from \"../../utils/helpers\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n useGetMeetingQuery,\n useUpdateMeetingMutation,\n useDeleteMeetingMutation,\n useRateMeetingMutation,\n useAddParticipantMutation,\n useRemoveParticipantMutation,\n useUpdateParticipantStatusMutation,\n useCreateActionItemMutation,\n useUpdateActionItemMutation,\n useCompleteActionItemMutation,\n useDeleteActionItemMutation,\n type MeetingParticipantFieldsFragment,\n type MeetingActionItemFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport {\n MeetingParticipantStatus,\n MeetingActionItemStatus,\n type UpdateMeetingInput,\n type AddMeetingParticipantInput,\n type CreateActionItemInput,\n type UpdateActionItemInput,\n} from \"@/generated/wspace-types\";\nimport { formatCurrency } from \"../../utils/helpers\";\nimport { MeetingFormModal } from \"./MeetingFormModal\";\nimport { ParticipantFormModal } from \"./ParticipantFormModal\";\nimport { ActionItemFormModal } from \"./ActionItemFormModal\";\n\nconst PARTICIPANT_STATUS_OPTIONS = [\n MeetingParticipantStatus.Pending,\n MeetingParticipantStatus.Accepted,\n MeetingParticipantStatus.Tentative,\n MeetingParticipantStatus.Declined,\n];\n\nconst ACTION_ITEM_VARIANT: Record<MeetingActionItemStatus, BadgeVariant> = {\n [MeetingActionItemStatus.Completed]: \"success\",\n [MeetingActionItemStatus.InProgress]: \"info\",\n [MeetingActionItemStatus.Pending]: \"warning\",\n [MeetingActionItemStatus.Cancelled]: \"muted\",\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 prettyStatus(status: string): string {\n const lower = status.replace(/_/g, \" \").toLowerCase();\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\nexport function MeetingDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { workspaceId = \"\", currentUser } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n const meetingId = id ?? \"\";\n\n const { data, loading, error, refetch } = useGetMeetingQuery({\n variables: { id: meetingId, workspaceId },\n skip: !workspaceId || !id,\n });\n\n const [updateMeeting, { loading: updating, error: updateError }] =\n useUpdateMeetingMutation();\n const [deleteMeeting, { loading: deletingMeeting }] =\n useDeleteMeetingMutation();\n const [rateMeeting, { loading: rating }] = useRateMeetingMutation();\n const [addParticipant, { loading: addingParticipant, error: addPError }] =\n useAddParticipantMutation();\n const [removeParticipant] = useRemoveParticipantMutation();\n const [updateParticipantStatus] = useUpdateParticipantStatusMutation();\n const [createActionItem, { loading: creatingItem, error: createItemError }] =\n useCreateActionItemMutation();\n const [updateActionItem, { loading: updatingItem, error: updateItemError }] =\n useUpdateActionItemMutation();\n const [completeActionItem] = useCompleteActionItemMutation();\n const [deleteActionItem] = useDeleteActionItemMutation();\n\n const [editOpen, setEditOpen] = useState(false);\n const [deleteOpen, setDeleteOpen] = useState(false);\n const [participantOpen, setParticipantOpen] = useState(false);\n const [removingParticipant, setRemovingParticipant] =\n useState<MeetingParticipantFieldsFragment | null>(null);\n const [actionItemModal, setActionItemModal] = useState<\n | { mode: \"create\" }\n | { mode: \"edit\"; item: MeetingActionItemFieldsFragment }\n | null\n >(null);\n const [deletingItem, setDeletingItem] =\n useState<MeetingActionItemFieldsFragment | null>(null);\n\n const meeting = data?.meeting;\n\n if (loading) {\n return (\n <PageLayout title=\"Meeting Details\" description={`Meeting ID: ${id}`}>\n <LoadingSkeleton variant=\"detail\" />\n </PageLayout>\n );\n }\n\n if (error || !meeting) {\n return (\n <PageLayout title=\"Meeting Details\" description={`Meeting ID: ${id}`}>\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n {error\n ? \"Failed to load meeting details. Please try again later.\"\n : \"Meeting not found.\"}\n </div>\n </PageLayout>\n );\n }\n\n const pendingActions = meeting.actionItems.filter(\n (a: MeetingActionItemFieldsFragment) => a.status !== \"COMPLETED\",\n ).length;\n const currentRating = meeting.rating ?? 0;\n\n const handleUpdateMeeting = async (input: UpdateMeetingInput) => {\n await updateMeeting({ variables: { id: meetingId, workspaceId, input } });\n await refetch();\n setEditOpen(false);\n };\n\n const handleDeleteMeeting = async () => {\n await deleteMeeting({ variables: { id: meetingId, workspaceId } });\n setDeleteOpen(false);\n navigate(\"/meetings\");\n };\n\n const handleRate = async (value: number) => {\n await rateMeeting({\n variables: { id: meetingId, workspaceId, rating: value },\n });\n await refetch();\n };\n\n const handleAddParticipant = async (input: AddMeetingParticipantInput) => {\n await addParticipant({\n variables: { meetingId, workspaceId, input },\n });\n await refetch();\n setParticipantOpen(false);\n };\n\n const handleRemoveParticipant = async () => {\n if (!removingParticipant) return;\n await removeParticipant({\n variables: {\n meetingId,\n participantId: removingParticipant.id,\n workspaceId,\n },\n });\n setRemovingParticipant(null);\n await refetch();\n };\n\n const handleParticipantStatus = async (\n participantId: string,\n status: MeetingParticipantStatus,\n ) => {\n await updateParticipantStatus({\n variables: { meetingId, participantId, workspaceId, status },\n });\n await refetch();\n };\n\n const handleCreateItem = async (input: CreateActionItemInput) => {\n await createActionItem({ variables: { meetingId, workspaceId, input } });\n await refetch();\n setActionItemModal(null);\n };\n\n const handleUpdateItem = async (\n itemId: string,\n input: UpdateActionItemInput,\n ) => {\n await updateActionItem({ variables: { id: itemId, workspaceId, input } });\n await refetch();\n setActionItemModal(null);\n };\n\n const handleCompleteItem = async (itemId: string) => {\n await completeActionItem({ variables: { id: itemId, workspaceId } });\n await refetch();\n };\n\n const handleDeleteItem = async () => {\n if (!deletingItem) return;\n await deleteActionItem({ variables: { id: deletingItem.id, workspaceId } });\n setDeletingItem(null);\n await refetch();\n };\n\n return (\n <PageLayout\n title=\"Meeting Details\"\n description={meeting.title}\n purpose=\"The full record of one meeting — who attended, what it cost in people's time, and the action items it produced — so you can follow up on commitments and judge whether the meeting was worth it.\"\n actions={\n <>\n <button\n onClick={() => setEditOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n <Pencil className=\"h-4 w-4\" />\n Edit\n </button>\n <button\n onClick={() => setDeleteOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/40\"\n >\n <Trash2 className=\"h-4 w-4\" />\n Delete\n </button>\n </>\n }\n >\n <NextSteps\n storageKey=\"timecampus-meeting-detail\"\n className=\"mb-6\"\n steps={[\n {\n id: \"all-meetings\",\n label: \"Back to all meetings\",\n description: \"Browse and schedule other meetings.\",\n onClick: () => navigate(\"/meetings\"),\n },\n {\n id: \"analytics\",\n label: \"See meeting analytics\",\n description: \"Understand your overall meeting load and cost.\",\n onClick: () => navigate(\"/meetings/analytics\"),\n },\n ]}\n />\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Duration\"\n value={`${meeting.duration ?? 0} min`}\n icon={<Clock className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Participants\"\n value={String(meeting.participants.length)}\n icon={<Users className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Cost\"\n value={meeting.cost != null ? formatCurrency(meeting.cost) : \"N/A\"}\n description=\"Based on participant rates\"\n />\n <StatCard\n title=\"Action Items\"\n value={String(meeting.actionItems.length)}\n description={`${pendingActions} pending`}\n />\n </div>\n\n {/* Rating */}\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <h3 className=\"mb-1 text-sm font-semibold text-text-primary\">\n Rate this meeting\n </h3>\n <p className=\"mb-3 text-xs text-text-secondary\">\n Was it worth the time? Your rating feeds meeting analytics.\n </p>\n <div className=\"flex items-center gap-1\">\n {[1, 2, 3, 4, 5].map((value) => (\n <button\n key={value}\n onClick={() => handleRate(value)}\n disabled={rating}\n aria-label={`Rate ${value} star${value > 1 ? \"s\" : \"\"}`}\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Star\n className={cn(\n \"h-6 w-6\",\n value <= currentRating\n ? \"fill-status-warning-text text-status-warning-text\"\n : \"text-text-secondary\",\n )}\n />\n </button>\n ))}\n {currentRating > 0 && (\n <span className=\"ml-2 text-sm text-text-secondary\">\n {currentRating} / 5\n </span>\n )}\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Participants\n </h3>\n <button\n onClick={() => setParticipantOpen(true)}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-2.5 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-3.5 w-3.5\" />\n Add\n </button>\n </div>\n {meeting.participants.length > 0 ? (\n <div className=\"space-y-2\">\n {meeting.participants.map(\n (p: MeetingParticipantFieldsFragment) => (\n <div\n key={p.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-subtle px-3 py-2\"\n >\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-[11px] font-semibold text-primary\">\n {idMonogram(p.userId)}\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-primary\">\n {p.userId}\n </p>\n <p className=\"text-xs text-text-secondary\">{p.role}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={p.status}\n onChange={(e) =>\n handleParticipantStatus(\n p.id,\n e.target.value as MeetingParticipantStatus,\n )\n }\n className=\"rounded-md border border-border-subtle bg-bg-surface px-2 py-1 text-xs text-text-primary\"\n aria-label=\"Participant status\"\n >\n {PARTICIPANT_STATUS_OPTIONS.map((s) => (\n <option key={s} value={s}>\n {s}\n </option>\n ))}\n </select>\n <button\n onClick={() => setRemovingParticipant(p)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Remove participant\"\n >\n <UserMinus className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ),\n )}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No participants added\"\n description=\"Add attendees to capture who was in the room and what it cost.\"\n action={\n <button\n onClick={() => setParticipantOpen(true)}\n className=\"inline-flex items-center gap-1 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 participant\n </button>\n }\n />\n )}\n </div>\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Action Items\n </h3>\n <button\n onClick={() => setActionItemModal({ mode: \"create\" })}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-2.5 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-3.5 w-3.5\" />\n Add\n </button>\n </div>\n {meeting.actionItems.length > 0 ? (\n <div className=\"space-y-2\">\n {meeting.actionItems.map(\n (item: MeetingActionItemFieldsFragment) => (\n <div\n key={item.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-subtle px-3 py-2\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-primary\">\n {item.title}\n </p>\n <div className=\"mt-1\">\n <StatusBadge\n label={prettyStatus(item.status)}\n variant={ACTION_ITEM_VARIANT[item.status]}\n />\n </div>\n </div>\n <div className=\"flex items-center gap-1\">\n {item.status !== MeetingActionItemStatus.Completed && (\n <button\n onClick={() => handleCompleteItem(item.id)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-success-bg hover:text-status-success-text\"\n aria-label=\"Complete action item\"\n >\n <Check className=\"h-4 w-4\" />\n </button>\n )}\n <button\n onClick={() =>\n setActionItemModal({ mode: \"edit\", item })\n }\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Edit action item\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setDeletingItem(item)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Delete action item\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ),\n )}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No action items\"\n description=\"Capture follow-ups here so commitments from the meeting don't get lost.\"\n action={\n <button\n onClick={() => setActionItemModal({ mode: \"create\" })}\n className=\"inline-flex items-center gap-1 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 action item\n </button>\n }\n />\n )}\n </div>\n </div>\n\n {/* Edit meeting */}\n {editOpen && (\n <MeetingFormModal\n open\n onClose={() => setEditOpen(false)}\n workspaceId={workspaceId}\n meeting={meeting}\n submitting={updating}\n error={Boolean(updateError)}\n onUpdate={handleUpdateMeeting}\n />\n )}\n\n {/* Delete meeting */}\n <Modal\n open={deleteOpen}\n onClose={() => setDeleteOpen(false)}\n title=\"Delete meeting\"\n description=\"This permanently removes the meeting and its analytics.\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteOpen(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={handleDeleteMeeting}\n disabled={deletingMeeting}\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 {deletingMeeting ? \"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\">{meeting.title}</span>\n ? This action cannot be undone.\n </p>\n </Modal>\n\n {/* Add participant */}\n {participantOpen && (\n <ParticipantFormModal\n open\n onClose={() => setParticipantOpen(false)}\n defaultUserId={currentUser?.id}\n submitting={addingParticipant}\n error={Boolean(addPError)}\n onSubmit={handleAddParticipant}\n />\n )}\n\n {/* Remove participant confirm */}\n <Modal\n open={Boolean(removingParticipant)}\n onClose={() => setRemovingParticipant(null)}\n title=\"Remove participant\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRemovingParticipant(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={handleRemoveParticipant}\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\"\n >\n Remove\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Remove{\" \"}\n <span className=\"font-medium text-text-primary\">\n {removingParticipant?.userId}\n </span>{\" \"}\n from this meeting?\n </p>\n </Modal>\n\n {/* Create / edit action item */}\n {actionItemModal && (\n <ActionItemFormModal\n open\n onClose={() => setActionItemModal(null)}\n item={\n actionItemModal.mode === \"edit\" ? actionItemModal.item : undefined\n }\n submitting={\n actionItemModal.mode === \"edit\" ? updatingItem : creatingItem\n }\n error={Boolean(\n actionItemModal.mode === \"edit\" ? updateItemError : createItemError,\n )}\n onCreate={handleCreateItem}\n onUpdate={\n actionItemModal.mode === \"edit\"\n ? (input) => handleUpdateItem(actionItemModal.item.id, input)\n : undefined\n }\n />\n )}\n\n {/* Delete action item confirm */}\n <Modal\n open={Boolean(deletingItem)}\n onClose={() => setDeletingItem(null)}\n title=\"Delete action item\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeletingItem(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={handleDeleteItem}\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\"\n >\n Delete\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Delete action item{\" \"}\n <span className=\"font-medium text-text-primary\">\n {deletingItem?.title}\n </span>\n ?\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MeetingDetailPage.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingDetailPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useParams, Link } from \"react-router-dom\";\nimport {\n Users,\n Clock,\n Pencil,\n Trash2,\n Plus,\n Star,\n Check,\n UserMinus,\n CalendarDays,\n} from \"lucide-react\";\nimport { NextSteps } from \"@burdenoff/fe-libs/ui\";\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 { cn, type BadgeVariant } from \"../../utils/helpers\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n useGetMeetingQuery,\n useUpdateMeetingMutation,\n useDeleteMeetingMutation,\n useRateMeetingMutation,\n useAddParticipantMutation,\n useRemoveParticipantMutation,\n useUpdateParticipantStatusMutation,\n useCreateActionItemMutation,\n useUpdateActionItemMutation,\n useCompleteActionItemMutation,\n useDeleteActionItemMutation,\n type MeetingParticipantFieldsFragment,\n type MeetingActionItemFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport {\n MeetingParticipantStatus,\n MeetingActionItemStatus,\n type UpdateMeetingInput,\n type AddMeetingParticipantInput,\n type CreateActionItemInput,\n type UpdateActionItemInput,\n} from \"@/generated/wspace-types\";\nimport { formatCurrency } from \"../../utils/helpers\";\nimport { MeetingFormModal } from \"./MeetingFormModal\";\nimport { ParticipantFormModal } from \"./ParticipantFormModal\";\nimport { ActionItemFormModal } from \"./ActionItemFormModal\";\n\nconst PARTICIPANT_STATUS_OPTIONS = [\n MeetingParticipantStatus.Pending,\n MeetingParticipantStatus.Accepted,\n MeetingParticipantStatus.Tentative,\n MeetingParticipantStatus.Declined,\n];\n\nconst ACTION_ITEM_VARIANT: Record<MeetingActionItemStatus, BadgeVariant> = {\n [MeetingActionItemStatus.Completed]: \"success\",\n [MeetingActionItemStatus.InProgress]: \"info\",\n [MeetingActionItemStatus.Pending]: \"warning\",\n [MeetingActionItemStatus.Cancelled]: \"muted\",\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 prettyStatus(status: string): string {\n const lower = status.replace(/_/g, \" \").toLowerCase();\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\nexport function MeetingDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { workspaceId = \"\", currentUser } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n const meetingId = id ?? \"\";\n\n const { data, loading, error, refetch } = useGetMeetingQuery({\n variables: { id: meetingId, workspaceId },\n skip: !workspaceId || !id,\n });\n\n const [updateMeeting, { loading: updating, error: updateError }] =\n useUpdateMeetingMutation();\n const [deleteMeeting, { loading: deletingMeeting }] =\n useDeleteMeetingMutation();\n const [rateMeeting, { loading: rating }] = useRateMeetingMutation();\n const [addParticipant, { loading: addingParticipant, error: addPError }] =\n useAddParticipantMutation();\n const [removeParticipant] = useRemoveParticipantMutation();\n const [updateParticipantStatus] = useUpdateParticipantStatusMutation();\n const [createActionItem, { loading: creatingItem, error: createItemError }] =\n useCreateActionItemMutation();\n const [updateActionItem, { loading: updatingItem, error: updateItemError }] =\n useUpdateActionItemMutation();\n const [completeActionItem] = useCompleteActionItemMutation();\n const [deleteActionItem] = useDeleteActionItemMutation();\n\n const [editOpen, setEditOpen] = useState(false);\n const [deleteOpen, setDeleteOpen] = useState(false);\n const [participantOpen, setParticipantOpen] = useState(false);\n const [removingParticipant, setRemovingParticipant] =\n useState<MeetingParticipantFieldsFragment | null>(null);\n const [actionItemModal, setActionItemModal] = useState<\n | { mode: \"create\" }\n | { mode: \"edit\"; item: MeetingActionItemFieldsFragment }\n | null\n >(null);\n const [deletingItem, setDeletingItem] =\n useState<MeetingActionItemFieldsFragment | null>(null);\n\n const meeting = data?.meeting;\n\n if (loading) {\n return (\n <PageLayout title=\"Meeting Details\" description={`Meeting ID: ${id}`}>\n <LoadingSkeleton variant=\"detail\" />\n </PageLayout>\n );\n }\n\n if (error || !meeting) {\n return (\n <PageLayout title=\"Meeting Details\" description={`Meeting ID: ${id}`}>\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n {error\n ? \"Failed to load meeting details. Please try again later.\"\n : \"Meeting not found.\"}\n </div>\n </PageLayout>\n );\n }\n\n const pendingActions = meeting.actionItems.filter(\n (a: MeetingActionItemFieldsFragment) => a.status !== \"COMPLETED\",\n ).length;\n const currentRating = meeting.rating ?? 0;\n\n const handleUpdateMeeting = async (input: UpdateMeetingInput) => {\n await updateMeeting({ variables: { id: meetingId, workspaceId, input } });\n await refetch();\n setEditOpen(false);\n };\n\n const handleDeleteMeeting = async () => {\n await deleteMeeting({ variables: { id: meetingId, workspaceId } });\n setDeleteOpen(false);\n navigate(\"/meetings\");\n };\n\n const handleRate = async (value: number) => {\n await rateMeeting({\n variables: { id: meetingId, workspaceId, rating: value },\n });\n await refetch();\n };\n\n const handleAddParticipant = async (input: AddMeetingParticipantInput) => {\n await addParticipant({\n variables: { meetingId, workspaceId, input },\n });\n await refetch();\n setParticipantOpen(false);\n };\n\n const handleRemoveParticipant = async () => {\n if (!removingParticipant) return;\n await removeParticipant({\n variables: {\n meetingId,\n participantId: removingParticipant.id,\n workspaceId,\n },\n });\n setRemovingParticipant(null);\n await refetch();\n };\n\n const handleParticipantStatus = async (\n participantId: string,\n status: MeetingParticipantStatus,\n ) => {\n await updateParticipantStatus({\n variables: { meetingId, participantId, workspaceId, status },\n });\n await refetch();\n };\n\n const handleCreateItem = async (input: CreateActionItemInput) => {\n await createActionItem({ variables: { meetingId, workspaceId, input } });\n await refetch();\n setActionItemModal(null);\n };\n\n const handleUpdateItem = async (\n itemId: string,\n input: UpdateActionItemInput,\n ) => {\n await updateActionItem({ variables: { id: itemId, workspaceId, input } });\n await refetch();\n setActionItemModal(null);\n };\n\n const handleCompleteItem = async (itemId: string) => {\n await completeActionItem({ variables: { id: itemId, workspaceId } });\n await refetch();\n };\n\n const handleDeleteItem = async () => {\n if (!deletingItem) return;\n await deleteActionItem({ variables: { id: deletingItem.id, workspaceId } });\n setDeletingItem(null);\n await refetch();\n };\n\n return (\n <PageLayout\n title=\"Meeting Details\"\n description={meeting.title}\n purpose=\"The full record of one meeting — who attended, what it cost in people's time, and the action items it produced — so you can follow up on commitments and judge whether the meeting was worth it.\"\n actions={\n <>\n <button\n onClick={() => setEditOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n <Pencil className=\"h-4 w-4\" />\n Edit\n </button>\n <button\n onClick={() => setDeleteOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/40\"\n >\n <Trash2 className=\"h-4 w-4\" />\n Delete\n </button>\n </>\n }\n >\n <NextSteps\n storageKey=\"timecampus-meeting-detail\"\n className=\"mb-6\"\n steps={[\n {\n id: \"all-meetings\",\n label: \"Back to all meetings\",\n description: \"Browse and schedule other meetings.\",\n onClick: () => navigate(\"/meetings\"),\n },\n {\n id: \"analytics\",\n label: \"See meeting analytics\",\n description: \"Understand your overall meeting load and cost.\",\n onClick: () => navigate(\"/meetings/analytics\"),\n },\n ]}\n />\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Duration\"\n value={`${meeting.duration ?? 0} min`}\n icon={<Clock className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Participants\"\n value={String(meeting.participants.length)}\n icon={<Users className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Cost\"\n value={meeting.cost != null ? formatCurrency(meeting.cost) : \"N/A\"}\n description=\"Based on participant rates\"\n />\n <StatCard\n title=\"Action Items\"\n value={String(meeting.actionItems.length)}\n description={`${pendingActions} pending`}\n />\n </div>\n\n {/* Calendar link — surfaces the wspace-calendar event auto-linked on\n create (MeetingMeta.calendarEventId). Hidden entirely when the\n meeting has no linked event (e.g. calendar-svc was unreachable at\n creation time), rather than showing a dead/broken state. */}\n {meeting.calendarEventId && (\n <Link\n to={`/calendar/event/${meeting.calendarEventId}`}\n className=\"inline-flex w-fit items-center gap-1.5 rounded-full border border-border-subtle bg-bg-surface px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/50 hover:text-text-primary\"\n >\n <CalendarDays className=\"h-3.5 w-3.5\" />\n View in Calendar\n </Link>\n )}\n\n {/* Rating */}\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <h3 className=\"mb-1 text-sm font-semibold text-text-primary\">\n Rate this meeting\n </h3>\n <p className=\"mb-3 text-xs text-text-secondary\">\n Was it worth the time? Your rating feeds meeting analytics.\n </p>\n <div className=\"flex items-center gap-1\">\n {[1, 2, 3, 4, 5].map((value) => (\n <button\n key={value}\n onClick={() => handleRate(value)}\n disabled={rating}\n aria-label={`Rate ${value} star${value > 1 ? \"s\" : \"\"}`}\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Star\n className={cn(\n \"h-6 w-6\",\n value <= currentRating\n ? \"fill-status-warning-text text-status-warning-text\"\n : \"text-text-secondary\",\n )}\n />\n </button>\n ))}\n {currentRating > 0 && (\n <span className=\"ml-2 text-sm text-text-secondary\">\n {currentRating} / 5\n </span>\n )}\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Participants\n </h3>\n <button\n onClick={() => setParticipantOpen(true)}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-2.5 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-3.5 w-3.5\" />\n Add\n </button>\n </div>\n {meeting.participants.length > 0 ? (\n <div className=\"space-y-2\">\n {meeting.participants.map(\n (p: MeetingParticipantFieldsFragment) => (\n <div\n key={p.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-subtle px-3 py-2\"\n >\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-[11px] font-semibold text-primary\">\n {idMonogram(p.userId)}\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-primary\">\n {p.userId}\n </p>\n <p className=\"text-xs text-text-secondary\">{p.role}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={p.status}\n onChange={(e) =>\n handleParticipantStatus(\n p.id,\n e.target.value as MeetingParticipantStatus,\n )\n }\n className=\"rounded-md border border-border-subtle bg-bg-surface px-2 py-1 text-xs text-text-primary\"\n aria-label=\"Participant status\"\n >\n {PARTICIPANT_STATUS_OPTIONS.map((s) => (\n <option key={s} value={s}>\n {s}\n </option>\n ))}\n </select>\n <button\n onClick={() => setRemovingParticipant(p)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Remove participant\"\n >\n <UserMinus className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ),\n )}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No participants added\"\n description=\"Add attendees to capture who was in the room and what it cost.\"\n action={\n <button\n onClick={() => setParticipantOpen(true)}\n className=\"inline-flex items-center gap-1 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 participant\n </button>\n }\n />\n )}\n </div>\n <div className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Action Items\n </h3>\n <button\n onClick={() => setActionItemModal({ mode: \"create\" })}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-2.5 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-3.5 w-3.5\" />\n Add\n </button>\n </div>\n {meeting.actionItems.length > 0 ? (\n <div className=\"space-y-2\">\n {meeting.actionItems.map(\n (item: MeetingActionItemFieldsFragment) => (\n <div\n key={item.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-subtle px-3 py-2\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-primary\">\n {item.title}\n </p>\n <div className=\"mt-1\">\n <StatusBadge\n label={prettyStatus(item.status)}\n variant={ACTION_ITEM_VARIANT[item.status]}\n />\n </div>\n </div>\n <div className=\"flex items-center gap-1\">\n {item.status !== MeetingActionItemStatus.Completed && (\n <button\n onClick={() => handleCompleteItem(item.id)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-success-bg hover:text-status-success-text\"\n aria-label=\"Complete action item\"\n >\n <Check className=\"h-4 w-4\" />\n </button>\n )}\n <button\n onClick={() =>\n setActionItemModal({ mode: \"edit\", item })\n }\n className=\"rounded-md p-1 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Edit action item\"\n >\n <Pencil className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setDeletingItem(item)}\n className=\"rounded-md p-1 text-text-secondary hover:bg-status-error-bg hover:text-status-error-text\"\n aria-label=\"Delete action item\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ),\n )}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No action items\"\n description=\"Capture follow-ups here so commitments from the meeting don't get lost.\"\n action={\n <button\n onClick={() => setActionItemModal({ mode: \"create\" })}\n className=\"inline-flex items-center gap-1 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 action item\n </button>\n }\n />\n )}\n </div>\n </div>\n\n {/* Edit meeting */}\n {editOpen && (\n <MeetingFormModal\n open\n onClose={() => setEditOpen(false)}\n workspaceId={workspaceId}\n meeting={meeting}\n submitting={updating}\n error={Boolean(updateError)}\n onUpdate={handleUpdateMeeting}\n />\n )}\n\n {/* Delete meeting */}\n <Modal\n open={deleteOpen}\n onClose={() => setDeleteOpen(false)}\n title=\"Delete meeting\"\n description=\"This permanently removes the meeting and its analytics.\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteOpen(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={handleDeleteMeeting}\n disabled={deletingMeeting}\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 {deletingMeeting ? \"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\">{meeting.title}</span>\n ? This action cannot be undone.\n </p>\n </Modal>\n\n {/* Add participant */}\n {participantOpen && (\n <ParticipantFormModal\n open\n onClose={() => setParticipantOpen(false)}\n defaultUserId={currentUser?.id}\n submitting={addingParticipant}\n error={Boolean(addPError)}\n onSubmit={handleAddParticipant}\n />\n )}\n\n {/* Remove participant confirm */}\n <Modal\n open={Boolean(removingParticipant)}\n onClose={() => setRemovingParticipant(null)}\n title=\"Remove participant\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRemovingParticipant(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={handleRemoveParticipant}\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\"\n >\n Remove\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Remove{\" \"}\n <span className=\"font-medium text-text-primary\">\n {removingParticipant?.userId}\n </span>{\" \"}\n from this meeting?\n </p>\n </Modal>\n\n {/* Create / edit action item */}\n {actionItemModal && (\n <ActionItemFormModal\n open\n onClose={() => setActionItemModal(null)}\n item={\n actionItemModal.mode === \"edit\" ? actionItemModal.item : undefined\n }\n submitting={\n actionItemModal.mode === \"edit\" ? updatingItem : creatingItem\n }\n error={Boolean(\n actionItemModal.mode === \"edit\" ? updateItemError : createItemError,\n )}\n onCreate={handleCreateItem}\n onUpdate={\n actionItemModal.mode === \"edit\"\n ? (input) => handleUpdateItem(actionItemModal.item.id, input)\n : undefined\n }\n />\n )}\n\n {/* Delete action item confirm */}\n <Modal\n open={Boolean(deletingItem)}\n onClose={() => setDeletingItem(null)}\n title=\"Delete action item\"\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeletingItem(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={handleDeleteItem}\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\"\n >\n Delete\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Delete action item{\" \"}\n <span className=\"font-medium text-text-primary\">\n {deletingItem?.title}\n </span>\n ?\n </p>\n </Modal>\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);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectsPage.js","names":[],"sources":["../../../../src/timecampus/pages/projects/ProjectsPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { Plus, Pencil, Trash2, Archive } from \"lucide-react\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport { getStatusColor, formatHours } from \"../../utils/helpers\";\nimport {\n useListProjectsQuery,\n useCreateProjectMutation,\n useUpdateProjectMutation,\n useArchiveProjectMutation,\n useDeleteProjectMutation,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\ntype ProjectItem = NonNullable<ListProjectsQuery[\"projects\"]>[\"items\"][number];\n\nconst PAGE_SIZE = 25;\n\nconst PROJECT_STATUSES = [\n \"active\",\n \"on-hold\",\n \"completed\",\n \"archived\",\n] as const;\n\ninterface ProjectFormState {\n name: string;\n clientId: string;\n description: string;\n status: string;\n estimatedHours: string;\n hourlyRate: string;\n}\n\nconst emptyForm: ProjectFormState = {\n name: \"\",\n clientId: \"\",\n description: \"\",\n status: \"active\",\n estimatedHours: \"\",\n hourlyRate: \"\",\n};\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\nconst labelClass = \"block text-sm font-medium text-text-primary\";\n\nexport function ProjectsPage() {\n const { t: translate } = useI18n();\n const t = (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n const navigate = useTimeCampusNavigate();\n\n const [limit, setLimit] = useState(PAGE_SIZE);\n const { data, loading, error, refetch } = useListProjectsQuery({\n variables: { limit, offset: 0 },\n notifyOnNetworkStatusChange: true,\n });\n\n const projects = data?.projects?.items ?? [];\n const hasMore = data?.projects?.hasMore ?? false;\n\n // Modal state\n const [createOpen, setCreateOpen] = useState(false);\n const [editTarget, setEditTarget] = useState<ProjectItem | null>(null);\n const [deleteTarget, setDeleteTarget] = useState<ProjectItem | null>(null);\n const [form, setForm] = useState<ProjectFormState>(emptyForm);\n\n const [createProject, { loading: creating, error: createError }] =\n useCreateProjectMutation();\n const [updateProject, { loading: updating, error: updateError }] =\n useUpdateProjectMutation();\n const [archiveProject, { loading: archiving }] = useArchiveProjectMutation();\n const [deleteProject, { loading: deleting, error: deleteError }] =\n useDeleteProjectMutation();\n\n const openCreate = () => {\n setForm(emptyForm);\n setCreateOpen(true);\n };\n\n const openEdit = (project: ProjectItem) => {\n setForm({\n name: project.name,\n clientId: project.clientId ?? \"\",\n description: project.description ?? \"\",\n status: project.status,\n estimatedHours:\n project.estimatedHours != null ? String(project.estimatedHours) : \"\",\n hourlyRate: project.hourlyRate != null ? String(project.hourlyRate) : \"\",\n });\n setEditTarget(project);\n };\n\n const handleCreate = async () => {\n if (!form.name.trim()) return;\n await createProject({\n variables: {\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n clientId: form.clientId.trim() || undefined,\n status: form.status,\n estimatedHours: form.estimatedHours\n ? Number(form.estimatedHours)\n : undefined,\n hourlyRate: form.hourlyRate ? Number(form.hourlyRate) : undefined,\n },\n },\n });\n setCreateOpen(false);\n await refetch();\n };\n\n const handleUpdate = async () => {\n if (!editTarget || !form.name.trim()) return;\n await updateProject({\n variables: {\n id: editTarget.id,\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n clientId: form.clientId.trim() || undefined,\n status: form.status,\n estimatedHours: form.estimatedHours\n ? Number(form.estimatedHours)\n : undefined,\n hourlyRate: form.hourlyRate ? Number(form.hourlyRate) : undefined,\n },\n },\n });\n setEditTarget(null);\n await refetch();\n };\n\n const handleArchive = async (project: ProjectItem) => {\n await archiveProject({ variables: { id: project.id } });\n await refetch();\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n await deleteProject({ variables: { id: deleteTarget.id } });\n setDeleteTarget(null);\n await refetch();\n };\n\n const formFields = (\n <div className=\"space-y-4\">\n <div>\n <label className={labelClass}>\n {t(\"timecampus.projects.form.name\", \"Project Name\")}\n </label>\n <input\n type=\"text\"\n placeholder=\"Enter project name\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClass}>\n {t(\"timecampus.projects.form.client\", \"Client\")}\n </label>\n <input\n type=\"text\"\n placeholder=\"Client name\"\n value={form.clientId}\n onChange={(e) => setForm({ ...form, clientId: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className={labelClass}>\n {t(\"timecampus.projects.form.status\", \"Status\")}\n </label>\n <select\n value={form.status}\n onChange={(e) => setForm({ ...form, status: e.target.value })}\n className={inputClass}\n >\n {PROJECT_STATUSES.map((s) => (\n <option key={s} value={s}>\n {s}\n </option>\n ))}\n </select>\n </div>\n </div>\n <div>\n <label className={labelClass}>\n {t(\"timecampus.projects.form.description\", \"Description\")}\n </label>\n <textarea\n placeholder=\"Project description\"\n rows={3}\n value={form.description}\n onChange={(e) => setForm({ ...form, description: 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={labelClass}>\n {t(\"timecampus.projects.form.estimatedHours\", \"Estimated Hours\")}\n </label>\n <input\n type=\"number\"\n placeholder=\"0\"\n value={form.estimatedHours}\n onChange={(e) =>\n setForm({ ...form, estimatedHours: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className={labelClass}>\n {t(\"timecampus.projects.form.hourlyRate\", \"Hourly Rate\")}\n </label>\n <input\n type=\"number\"\n placeholder=\"0.00\"\n value={form.hourlyRate}\n onChange={(e) => setForm({ ...form, hourlyRate: e.target.value })}\n className={inputClass}\n />\n </div>\n </div>\n </div>\n );\n\n if (error) {\n return (\n <PageLayout\n title={t(\"timecampus.projects.title\", \"Projects\")}\n description={t(\n \"timecampus.projects.description\",\n \"Manage and track project time\",\n )}\n >\n <div className=\"rounded-xl border border-status-error-bg/40 bg-status-error-bg p-6 text-sm text-status-error-text\">\n {t(\n \"timecampus.projects.error.loadFailed\",\n \"Failed to load projects. Please try again later.\",\n )}\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={t(\"timecampus.projects.title\", \"Projects\")}\n description={t(\n \"timecampus.projects.description\",\n \"Manage and track project time\",\n )}\n purpose={t(\n \"timecampus.projects.purpose\",\n \"Group your work into projects so every hour you track rolls up to the right client and budget — use it to estimate effort, watch progress against the plan, and bill accurately.\",\n )}\n actions={\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 {t(\"timecampus.projects.actions.newProject\", \"New Project\")}\n </button>\n }\n >\n {loading && projects.length === 0 ? (\n <LoadingSkeleton variant=\"card\" count={4} />\n ) : projects.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title={t(\"timecampus.projects.empty.title\", \"No projects yet\")}\n description={t(\n \"timecampus.projects.empty\",\n \"No projects yet. Create your first project to get started.\",\n )}\n action={\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 {t(\"timecampus.projects.actions.newProject\", \"New Project\")}\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {projects.map((project: ProjectItem) => (\n <div\n key={project.id}\n role=\"button\"\n tabIndex={0}\n onClick={() => navigate(`/projects/${project.id}`)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n navigate(`/projects/${project.id}`);\n }\n }}\n className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-5 transition-colors hover:bg-bg-sunken/50 cursor-pointer\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n <div\n className=\"h-3 w-3 rounded-full\"\n style={{\n backgroundColor:\n project.color ?? \"var(--color-accent-blue)\",\n }}\n />\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {project.name}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {project.clientId ??\n t(\"timecampus.projects.card.noClient\", \"No client\")}\n </p>\n </div>\n </div>\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${getStatusColor(project.status)}`}\n >\n {project.status}\n </span>\n </div>\n <div className=\"mt-4\">\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span>\n {t(\"timecampus.projects.card.progress\", \"Progress\")}\n </span>\n <span>\n {project.estimatedHours\n ? Math.round(\n (project.trackedHours / project.estimatedHours) *\n 100,\n )\n : 0}\n %\n </span>\n </div>\n <div className=\"mt-1 h-2 rounded-full bg-bg-sunken\">\n <div\n className=\"h-2 rounded-full bg-action-primary-bg\"\n style={{\n width: `${project.estimatedHours ? Math.min(Math.round((project.trackedHours / project.estimatedHours) * 100), 100) : 0}%`,\n }}\n />\n </div>\n </div>\n <div className=\"mt-3 flex items-center justify-between text-xs text-text-secondary\">\n <span>\n {formatHours(project.trackedHours)} /{\" \"}\n {formatHours(project.estimatedHours ?? 0)}\n </span>\n <span>\n {project.tasks?.length ?? 0}{\" \"}\n {t(\"timecampus.projects.card.tasks\", \"tasks\")}\n </span>\n </div>\n <div className=\"mt-4 flex items-center justify-end gap-1 border-t border-border-subtle pt-3\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n openEdit(project);\n }}\n className=\"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label={t(\"timecampus.projects.actions.edit\", \"Edit\")}\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n {t(\"timecampus.projects.actions.edit\", \"Edit\")}\n </button>\n {project.status !== \"archived\" && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n void handleArchive(project);\n }}\n disabled={archiving}\n className=\"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-text-secondary hover:bg-bg-sunken hover:text-text-primary disabled:opacity-50\"\n aria-label={t(\n \"timecampus.projects.actions.archive\",\n \"Archive\",\n )}\n >\n <Archive className=\"h-3.5 w-3.5\" />\n {t(\"timecampus.projects.actions.archive\", \"Archive\")}\n </button>\n )}\n <button\n onClick={(e) => {\n e.stopPropagation();\n setDeleteTarget(project);\n }}\n className=\"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-status-error-text hover:bg-status-error-bg\"\n aria-label={t(\n \"timecampus.projects.actions.delete\",\n \"Delete\",\n )}\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n {t(\"timecampus.projects.actions.delete\", \"Delete\")}\n </button>\n </div>\n </div>\n ))}\n </div>\n {hasMore && (\n <div className=\"mt-6 flex justify-center\">\n <button\n onClick={() => setLimit((prev) => prev + PAGE_SIZE)}\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\n ? t(\"timecampus.common.loading\", \"Loading…\")\n : t(\"timecampus.common.loadMore\", \"Load more\")}\n </button>\n </div>\n )}\n </>\n )}\n\n {/* Create modal */}\n <Modal\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title={t(\"timecampus.projects.actions.newProject\", \"New Project\")}\n description={t(\n \"timecampus.projects.form.createHint\",\n \"Define a new project for time tracking.\",\n )}\n size=\"xl\"\n footer={\n <>\n <button\n onClick={() => setCreateOpen(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 {t(\"timecampus.common.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleCreate}\n disabled={creating || !form.name.trim()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating\n ? t(\"timecampus.common.creating\", \"Creating…\")\n : t(\"timecampus.projects.actions.create\", \"Create Project\")}\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 {t(\n \"timecampus.projects.error.createFailed\",\n \"Failed to create project. Please try again.\",\n )}\n </div>\n )}\n {formFields}\n </Modal>\n\n {/* Edit modal */}\n <Modal\n open={editTarget !== null}\n onClose={() => setEditTarget(null)}\n title={t(\"timecampus.projects.actions.editProject\", \"Edit Project\")}\n size=\"xl\"\n footer={\n <>\n <button\n onClick={() => setEditTarget(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 {t(\"timecampus.common.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleUpdate}\n disabled={updating || !form.name.trim()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {updating\n ? t(\"timecampus.common.saving\", \"Saving…\")\n : t(\"timecampus.common.saveChanges\", \"Save Changes\")}\n </button>\n </>\n }\n >\n {updateError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n {t(\n \"timecampus.projects.error.updateFailed\",\n \"Failed to update project. Please try again.\",\n )}\n </div>\n )}\n {formFields}\n </Modal>\n\n {/* Delete confirm modal */}\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title={t(\"timecampus.projects.actions.deleteProject\", \"Delete Project\")}\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n {t(\"timecampus.common.cancel\", \"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/90 disabled:opacity-50\"\n >\n {deleting\n ? t(\"timecampus.common.deleting\", \"Deleting…\")\n : t(\"timecampus.common.delete\", \"Delete\")}\n </button>\n </>\n }\n >\n {deleteError && (\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 {t(\n \"timecampus.projects.error.deleteFailed\",\n \"Failed to delete project. Please try again.\",\n )}\n </div>\n )}\n <p className=\"text-sm text-text-secondary\">\n {t(\n \"timecampus.projects.deleteConfirm\",\n \"Are you sure you want to delete\",\n )}{\" \"}\n <span className=\"font-semibold text-text-primary\">\n {deleteTarget?.name}\n </span>\n ? {t(\"timecampus.common.cannotUndo\", \"This action cannot be undone.\")}\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProjectsPage.js","names":[],"sources":["../../../../src/timecampus/pages/projects/ProjectsPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { Plus, Pencil, Trash2, Archive } from \"lucide-react\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { Modal } from \"../../components/Modal\";\nimport { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport { getStatusColor, formatHours } from \"../../utils/helpers\";\nimport {\n useListProjectsQuery,\n useCreateProjectMutation,\n useUpdateProjectMutation,\n useArchiveProjectMutation,\n useDeleteProjectMutation,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\ntype ProjectItem = NonNullable<ListProjectsQuery[\"projects\"]>[\"items\"][number];\n\nconst PAGE_SIZE = 25;\n\nconst PROJECT_STATUSES = [\n \"active\",\n \"on-hold\",\n \"completed\",\n \"archived\",\n] as const;\n\ninterface ProjectFormState {\n name: string;\n clientId: string;\n description: string;\n status: string;\n estimatedHours: string;\n hourlyRate: string;\n}\n\nconst emptyForm: ProjectFormState = {\n name: \"\",\n clientId: \"\",\n description: \"\",\n status: \"active\",\n estimatedHours: \"\",\n hourlyRate: \"\",\n};\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\nconst labelClass = \"block text-sm font-medium text-text-primary\";\n\nexport function ProjectsPage() {\n const { t: translate } = useI18n();\n const t = (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n const navigate = useTimeCampusNavigate();\n\n const [limit, setLimit] = useState(PAGE_SIZE);\n const { data, loading, error, refetch } = useListProjectsQuery({\n variables: { pagination: { limit, offset: 0 } },\n notifyOnNetworkStatusChange: true,\n });\n\n const projects = data?.projects?.items ?? [];\n const hasMore = data?.projects?.hasMore ?? false;\n\n // Modal state\n const [createOpen, setCreateOpen] = useState(false);\n const [editTarget, setEditTarget] = useState<ProjectItem | null>(null);\n const [deleteTarget, setDeleteTarget] = useState<ProjectItem | null>(null);\n const [form, setForm] = useState<ProjectFormState>(emptyForm);\n\n const [createProject, { loading: creating, error: createError }] =\n useCreateProjectMutation();\n const [updateProject, { loading: updating, error: updateError }] =\n useUpdateProjectMutation();\n const [archiveProject, { loading: archiving }] = useArchiveProjectMutation();\n const [deleteProject, { loading: deleting, error: deleteError }] =\n useDeleteProjectMutation();\n\n const openCreate = () => {\n setForm(emptyForm);\n setCreateOpen(true);\n };\n\n const openEdit = (project: ProjectItem) => {\n setForm({\n name: project.name,\n clientId: project.clientId ?? \"\",\n description: project.description ?? \"\",\n status: project.status,\n estimatedHours:\n project.estimatedHours != null ? String(project.estimatedHours) : \"\",\n hourlyRate: project.hourlyRate != null ? String(project.hourlyRate) : \"\",\n });\n setEditTarget(project);\n };\n\n const handleCreate = async () => {\n if (!form.name.trim()) return;\n await createProject({\n variables: {\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n clientId: form.clientId.trim() || undefined,\n status: form.status,\n estimatedHours: form.estimatedHours\n ? Number(form.estimatedHours)\n : undefined,\n hourlyRate: form.hourlyRate ? Number(form.hourlyRate) : undefined,\n },\n },\n });\n setCreateOpen(false);\n await refetch();\n };\n\n const handleUpdate = async () => {\n if (!editTarget || !form.name.trim()) return;\n await updateProject({\n variables: {\n id: editTarget.id,\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n clientId: form.clientId.trim() || undefined,\n status: form.status,\n estimatedHours: form.estimatedHours\n ? Number(form.estimatedHours)\n : undefined,\n hourlyRate: form.hourlyRate ? Number(form.hourlyRate) : undefined,\n },\n },\n });\n setEditTarget(null);\n await refetch();\n };\n\n const handleArchive = async (project: ProjectItem) => {\n await archiveProject({ variables: { id: project.id } });\n await refetch();\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n await deleteProject({ variables: { id: deleteTarget.id } });\n setDeleteTarget(null);\n await refetch();\n };\n\n const formFields = (\n <div className=\"space-y-4\">\n <div>\n <label className={labelClass}>\n {t(\"timecampus.projects.form.name\", \"Project Name\")}\n </label>\n <input\n type=\"text\"\n placeholder=\"Enter project name\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClass}>\n {t(\"timecampus.projects.form.client\", \"Client\")}\n </label>\n <input\n type=\"text\"\n placeholder=\"Client name\"\n value={form.clientId}\n onChange={(e) => setForm({ ...form, clientId: e.target.value })}\n className={inputClass}\n />\n </div>\n <div>\n <label className={labelClass}>\n {t(\"timecampus.projects.form.status\", \"Status\")}\n </label>\n <select\n value={form.status}\n onChange={(e) => setForm({ ...form, status: e.target.value })}\n className={inputClass}\n >\n {PROJECT_STATUSES.map((s) => (\n <option key={s} value={s}>\n {s}\n </option>\n ))}\n </select>\n </div>\n </div>\n <div>\n <label className={labelClass}>\n {t(\"timecampus.projects.form.description\", \"Description\")}\n </label>\n <textarea\n placeholder=\"Project description\"\n rows={3}\n value={form.description}\n onChange={(e) => setForm({ ...form, description: 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={labelClass}>\n {t(\"timecampus.projects.form.estimatedHours\", \"Estimated Hours\")}\n </label>\n <input\n type=\"number\"\n placeholder=\"0\"\n value={form.estimatedHours}\n onChange={(e) =>\n setForm({ ...form, estimatedHours: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className={labelClass}>\n {t(\"timecampus.projects.form.hourlyRate\", \"Hourly Rate\")}\n </label>\n <input\n type=\"number\"\n placeholder=\"0.00\"\n value={form.hourlyRate}\n onChange={(e) => setForm({ ...form, hourlyRate: e.target.value })}\n className={inputClass}\n />\n </div>\n </div>\n </div>\n );\n\n if (error) {\n return (\n <PageLayout\n title={t(\"timecampus.projects.title\", \"Projects\")}\n description={t(\n \"timecampus.projects.description\",\n \"Manage and track project time\",\n )}\n >\n <div className=\"rounded-xl border border-status-error-bg/40 bg-status-error-bg p-6 text-sm text-status-error-text\">\n {t(\n \"timecampus.projects.error.loadFailed\",\n \"Failed to load projects. Please try again later.\",\n )}\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={t(\"timecampus.projects.title\", \"Projects\")}\n description={t(\n \"timecampus.projects.description\",\n \"Manage and track project time\",\n )}\n purpose={t(\n \"timecampus.projects.purpose\",\n \"Group your work into projects so every hour you track rolls up to the right client and budget — use it to estimate effort, watch progress against the plan, and bill accurately.\",\n )}\n actions={\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 {t(\"timecampus.projects.actions.newProject\", \"New Project\")}\n </button>\n }\n >\n {loading && projects.length === 0 ? (\n <LoadingSkeleton variant=\"card\" count={4} />\n ) : projects.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title={t(\"timecampus.projects.empty.title\", \"No projects yet\")}\n description={t(\n \"timecampus.projects.empty\",\n \"No projects yet. Create your first project to get started.\",\n )}\n action={\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 {t(\"timecampus.projects.actions.newProject\", \"New Project\")}\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {projects.map((project: ProjectItem) => (\n <div\n key={project.id}\n role=\"button\"\n tabIndex={0}\n onClick={() => navigate(`/projects/${project.id}`)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n navigate(`/projects/${project.id}`);\n }\n }}\n className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-5 transition-colors hover:bg-bg-sunken/50 cursor-pointer\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n <div\n className=\"h-3 w-3 rounded-full\"\n style={{\n backgroundColor:\n project.color ?? \"var(--color-accent-blue)\",\n }}\n />\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {project.name}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {project.clientId ??\n t(\"timecampus.projects.card.noClient\", \"No client\")}\n </p>\n </div>\n </div>\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${getStatusColor(project.status)}`}\n >\n {project.status}\n </span>\n </div>\n <div className=\"mt-4\">\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span>\n {t(\"timecampus.projects.card.progress\", \"Progress\")}\n </span>\n <span>\n {project.estimatedHours\n ? Math.round(\n (project.trackedHours / project.estimatedHours) *\n 100,\n )\n : 0}\n %\n </span>\n </div>\n <div className=\"mt-1 h-2 rounded-full bg-bg-sunken\">\n <div\n className=\"h-2 rounded-full bg-action-primary-bg\"\n style={{\n width: `${project.estimatedHours ? Math.min(Math.round((project.trackedHours / project.estimatedHours) * 100), 100) : 0}%`,\n }}\n />\n </div>\n </div>\n <div className=\"mt-3 flex items-center justify-between text-xs text-text-secondary\">\n <span>\n {formatHours(project.trackedHours)} /{\" \"}\n {formatHours(project.estimatedHours ?? 0)}\n </span>\n <span>\n {project.tasks?.length ?? 0}{\" \"}\n {t(\"timecampus.projects.card.tasks\", \"tasks\")}\n </span>\n </div>\n <div className=\"mt-4 flex items-center justify-end gap-1 border-t border-border-subtle pt-3\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n openEdit(project);\n }}\n className=\"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label={t(\"timecampus.projects.actions.edit\", \"Edit\")}\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n {t(\"timecampus.projects.actions.edit\", \"Edit\")}\n </button>\n {project.status !== \"archived\" && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n void handleArchive(project);\n }}\n disabled={archiving}\n className=\"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-text-secondary hover:bg-bg-sunken hover:text-text-primary disabled:opacity-50\"\n aria-label={t(\n \"timecampus.projects.actions.archive\",\n \"Archive\",\n )}\n >\n <Archive className=\"h-3.5 w-3.5\" />\n {t(\"timecampus.projects.actions.archive\", \"Archive\")}\n </button>\n )}\n <button\n onClick={(e) => {\n e.stopPropagation();\n setDeleteTarget(project);\n }}\n className=\"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-status-error-text hover:bg-status-error-bg\"\n aria-label={t(\n \"timecampus.projects.actions.delete\",\n \"Delete\",\n )}\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n {t(\"timecampus.projects.actions.delete\", \"Delete\")}\n </button>\n </div>\n </div>\n ))}\n </div>\n {hasMore && (\n <div className=\"mt-6 flex justify-center\">\n <button\n onClick={() => setLimit((prev) => prev + PAGE_SIZE)}\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\n ? t(\"timecampus.common.loading\", \"Loading…\")\n : t(\"timecampus.common.loadMore\", \"Load more\")}\n </button>\n </div>\n )}\n </>\n )}\n\n {/* Create modal */}\n <Modal\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n title={t(\"timecampus.projects.actions.newProject\", \"New Project\")}\n description={t(\n \"timecampus.projects.form.createHint\",\n \"Define a new project for time tracking.\",\n )}\n size=\"xl\"\n footer={\n <>\n <button\n onClick={() => setCreateOpen(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 {t(\"timecampus.common.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleCreate}\n disabled={creating || !form.name.trim()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating\n ? t(\"timecampus.common.creating\", \"Creating…\")\n : t(\"timecampus.projects.actions.create\", \"Create Project\")}\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 {t(\n \"timecampus.projects.error.createFailed\",\n \"Failed to create project. Please try again.\",\n )}\n </div>\n )}\n {formFields}\n </Modal>\n\n {/* Edit modal */}\n <Modal\n open={editTarget !== null}\n onClose={() => setEditTarget(null)}\n title={t(\"timecampus.projects.actions.editProject\", \"Edit Project\")}\n size=\"xl\"\n footer={\n <>\n <button\n onClick={() => setEditTarget(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 {t(\"timecampus.common.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleUpdate}\n disabled={updating || !form.name.trim()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {updating\n ? t(\"timecampus.common.saving\", \"Saving…\")\n : t(\"timecampus.common.saveChanges\", \"Save Changes\")}\n </button>\n </>\n }\n >\n {updateError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n {t(\n \"timecampus.projects.error.updateFailed\",\n \"Failed to update project. Please try again.\",\n )}\n </div>\n )}\n {formFields}\n </Modal>\n\n {/* Delete confirm modal */}\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title={t(\"timecampus.projects.actions.deleteProject\", \"Delete Project\")}\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n {t(\"timecampus.common.cancel\", \"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/90 disabled:opacity-50\"\n >\n {deleting\n ? t(\"timecampus.common.deleting\", \"Deleting…\")\n : t(\"timecampus.common.delete\", \"Delete\")}\n </button>\n </>\n }\n >\n {deleteError && (\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 {t(\n \"timecampus.projects.error.deleteFailed\",\n \"Failed to delete project. Please try again.\",\n )}\n </div>\n )}\n <p className=\"text-sm text-text-secondary\">\n {t(\n \"timecampus.projects.deleteConfirm\",\n \"Are you sure you want to delete\",\n )}{\" \"}\n <span className=\"font-semibold text-text-primary\">\n {deleteTarget?.name}\n </span>\n ? {t(\"timecampus.common.cannotUndo\", \"This action cannot be undone.\")}\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -33,8 +33,10 @@ function E() {
|
|
|
33
33
|
let { projectId: E } = g(), [D, re] = d(x), { data: O, loading: k, error: ie, refetch: A } = i({
|
|
34
34
|
variables: {
|
|
35
35
|
projectId: E,
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
pagination: {
|
|
37
|
+
limit: D,
|
|
38
|
+
offset: 0
|
|
39
|
+
}
|
|
38
40
|
},
|
|
39
41
|
skip: !E,
|
|
40
42
|
notifyOnNetworkStatusChange: !0
|