@burdenoff/microfe-planmagnet 2026.928.4 → 2026.928.6
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/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +130 -97
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +256 -156
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/automation/AutomationExecutionsPanel.d.ts +5 -0
- package/dist/planmagnet/automation/AutomationExecutionsPanel.d.ts.map +1 -0
- package/dist/planmagnet/automation/AutomationExecutionsPanel.js +107 -0
- package/dist/planmagnet/automation/AutomationExecutionsPanel.js.map +1 -0
- package/dist/planmagnet/automation/AutomationRuleFields.d.ts +29 -0
- package/dist/planmagnet/automation/AutomationRuleFields.d.ts.map +1 -0
- package/dist/planmagnet/automation/AutomationRuleFields.js +308 -0
- package/dist/planmagnet/automation/AutomationRuleFields.js.map +1 -0
- package/dist/planmagnet/automation/automationRuleTypes.d.ts +60 -0
- package/dist/planmagnet/automation/automationRuleTypes.d.ts.map +1 -0
- package/dist/planmagnet/automation/automationRuleTypes.js +113 -0
- package/dist/planmagnet/automation/automationRuleTypes.js.map +1 -0
- package/dist/planmagnet/board/BulkActions.d.ts +11 -0
- package/dist/planmagnet/board/BulkActions.d.ts.map +1 -0
- package/dist/planmagnet/board/BulkActions.js +182 -0
- package/dist/planmagnet/board/BulkActions.js.map +1 -0
- package/dist/planmagnet/board/ProjectBoard.d.ts.map +1 -1
- package/dist/planmagnet/board/ProjectBoard.js +289 -254
- package/dist/planmagnet/board/ProjectBoard.js.map +1 -1
- package/dist/planmagnet/budgets/BudgetControls.d.ts +30 -0
- package/dist/planmagnet/budgets/BudgetControls.d.ts.map +1 -0
- package/dist/planmagnet/budgets/BudgetControls.js +470 -0
- package/dist/planmagnet/budgets/BudgetControls.js.map +1 -0
- package/dist/planmagnet/budgets/ExpenseControls.d.ts +27 -0
- package/dist/planmagnet/budgets/ExpenseControls.d.ts.map +1 -0
- package/dist/planmagnet/budgets/ExpenseControls.js +366 -0
- package/dist/planmagnet/budgets/ExpenseControls.js.map +1 -0
- package/dist/planmagnet/budgets/ProjectBudgetsSummary.d.ts +5 -0
- package/dist/planmagnet/budgets/ProjectBudgetsSummary.d.ts.map +1 -0
- package/dist/planmagnet/budgets/ProjectBudgetsSummary.js +112 -0
- package/dist/planmagnet/budgets/ProjectBudgetsSummary.js.map +1 -0
- package/dist/planmagnet/budgets/budgetMath.d.ts +26 -0
- package/dist/planmagnet/budgets/budgetMath.d.ts.map +1 -0
- package/dist/planmagnet/budgets/budgetMath.js +49 -0
- package/dist/planmagnet/budgets/budgetMath.js.map +1 -0
- package/dist/planmagnet/controls/AutomationRuleControls.d.ts +23 -0
- package/dist/planmagnet/controls/AutomationRuleControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/AutomationRuleControls.js +172 -0
- package/dist/planmagnet/controls/AutomationRuleControls.js.map +1 -0
- package/dist/planmagnet/controls/CompetitorControls.js +15 -15
- package/dist/planmagnet/controls/CustomFieldControls.d.ts +42 -0
- package/dist/planmagnet/controls/CustomFieldControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/CustomFieldControls.js +84 -0
- package/dist/planmagnet/controls/CustomFieldControls.js.map +1 -0
- package/dist/planmagnet/controls/CustomFieldValuesPanel.d.ts +8 -0
- package/dist/planmagnet/controls/CustomFieldValuesPanel.d.ts.map +1 -0
- package/dist/planmagnet/controls/CustomFieldValuesPanel.js +315 -0
- package/dist/planmagnet/controls/CustomFieldValuesPanel.js.map +1 -0
- package/dist/planmagnet/controls/FeatureControls.js +54 -54
- package/dist/planmagnet/controls/ObjectiveControls.js +15 -15
- package/dist/planmagnet/controls/Pickers.d.ts +7 -0
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +58 -20
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/controls/PortfolioControls.js +7 -7
- package/dist/planmagnet/controls/ProgramControls.js +19 -19
- package/dist/planmagnet/controls/ProjectControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/ProjectControls.js +54 -47
- package/dist/planmagnet/controls/ProjectControls.js.map +1 -1
- package/dist/planmagnet/controls/ReleaseControls.js +7 -7
- package/dist/planmagnet/controls/RiskControls.d.ts +27 -0
- package/dist/planmagnet/controls/RiskControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/RiskControls.js +346 -0
- package/dist/planmagnet/controls/RiskControls.js.map +1 -0
- package/dist/planmagnet/controls/TemplateControls.d.ts +15 -0
- package/dist/planmagnet/controls/TemplateControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/TemplateControls.js +55 -0
- package/dist/planmagnet/controls/TemplateControls.js.map +1 -0
- package/dist/planmagnet/controls/WorkItemControls.d.ts +6 -2
- package/dist/planmagnet/controls/WorkItemControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/WorkItemControls.js +9 -9
- package/dist/planmagnet/controls/WorkItemControls.js.map +1 -1
- package/dist/planmagnet/create/CreateDialogs.d.ts +20 -0
- package/dist/planmagnet/create/CreateDialogs.d.ts.map +1 -1
- package/dist/planmagnet/create/CreateDialogs.js +455 -116
- package/dist/planmagnet/create/CreateDialogs.js.map +1 -1
- package/dist/planmagnet/dependency/DependencyPanel.d.ts +6 -0
- package/dist/planmagnet/dependency/DependencyPanel.d.ts.map +1 -0
- package/dist/planmagnet/dependency/DependencyPanel.js +310 -0
- package/dist/planmagnet/dependency/DependencyPanel.js.map +1 -0
- package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.js +171 -0
- package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.js +232 -0
- package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.js.map +1 -0
- package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.d.ts +2 -0
- package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.js +93 -0
- package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.js.map +1 -0
- package/dist/planmagnet/pages/DashboardDashboardPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js +11 -3
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/planmagnet/pages/PortfoliosEditPortfolioPagePage.js +5 -5
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +9 -9
- package/dist/planmagnet/pages/PortfoliosPortfoliosPagePage.js +6 -6
- package/dist/planmagnet/pages/ProgramsCreateProgramPagePage.js +4 -4
- package/dist/planmagnet/pages/ProgramsEditProgramPagePage.js +6 -6
- package/dist/planmagnet/pages/ProgramsProgramsPagePage.js +5 -5
- package/dist/planmagnet/pages/ProjectsBudgetDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProjectsBudgetDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProjectsBudgetDetailPagePage.js +315 -0
- package/dist/planmagnet/pages/ProjectsBudgetDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProjectsBudgetsPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProjectsBudgetsPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js +186 -0
- package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js +164 -0
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProjectsCustomFieldsPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProjectsCustomFieldsPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProjectsCustomFieldsPagePage.js +348 -0
- package/dist/planmagnet/pages/ProjectsCustomFieldsPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +10 -10
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js +4 -4
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +20 -10
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js +13 -8
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.js +76 -56
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +372 -203
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +81 -72
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/RisksRiskDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/RisksRiskDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/RisksRiskDetailPagePage.js +278 -0
- package/dist/planmagnet/pages/RisksRiskDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/RisksRiskRegisterPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/RisksRiskRegisterPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/RisksRiskRegisterPagePage.js +191 -0
- package/dist/planmagnet/pages/RisksRiskRegisterPagePage.js.map +1 -0
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +168 -122
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +5 -5
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js +4 -4
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +161 -137
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTasksPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTasksPagePage.js +124 -113
- package/dist/planmagnet/pages/TasksTasksPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTimeEntriesPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TasksTimeEntriesPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js +263 -0
- package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TasksTimesheetDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TasksTimesheetDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TasksTimesheetDetailPagePage.js +254 -0
- package/dist/planmagnet/pages/TasksTimesheetDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TasksTimesheetsPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TasksTimesheetsPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TasksTimesheetsPagePage.js +165 -0
- package/dist/planmagnet/pages/TasksTimesheetsPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TeamCreateTeamPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamCreateTeamPagePage.js +288 -91
- package/dist/planmagnet/pages/TeamCreateTeamPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.js +217 -0
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TeamTeamEditPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TeamTeamEditPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TeamTeamEditPagePage.js +211 -0
- package/dist/planmagnet/pages/TeamTeamEditPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TeamTeamsListPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TeamTeamsListPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TeamTeamsListPagePage.js +118 -0
- package/dist/planmagnet/pages/TeamTeamsListPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TemplatesEditTemplatePagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TemplatesEditTemplatePagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TemplatesEditTemplatePagePage.js +316 -0
- package/dist/planmagnet/pages/TemplatesEditTemplatePagePage.js.map +1 -0
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.js +208 -0
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.js.map +1 -0
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts +0 -5
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +47 -89
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapControls.js +24 -24
- package/dist/planmagnet/sprints/SprintControls.d.ts +15 -3
- package/dist/planmagnet/sprints/SprintControls.d.ts.map +1 -1
- package/dist/planmagnet/sprints/SprintControls.js +293 -190
- package/dist/planmagnet/sprints/SprintControls.js.map +1 -1
- package/dist/planmagnet/team/TeamControls.d.ts +31 -0
- package/dist/planmagnet/team/TeamControls.d.ts.map +1 -0
- package/dist/planmagnet/team/TeamControls.js +407 -0
- package/dist/planmagnet/team/TeamControls.js.map +1 -0
- package/dist/planmagnet/time/TaskTimePanel.d.ts +7 -0
- package/dist/planmagnet/time/TaskTimePanel.d.ts.map +1 -0
- package/dist/planmagnet/time/TaskTimePanel.js +104 -0
- package/dist/planmagnet/time/TaskTimePanel.js.map +1 -0
- package/dist/planmagnet/time/TimeEntryControls.d.ts +37 -0
- package/dist/planmagnet/time/TimeEntryControls.d.ts.map +1 -0
- package/dist/planmagnet/time/TimeEntryControls.js +338 -0
- package/dist/planmagnet/time/TimeEntryControls.js.map +1 -0
- package/dist/planmagnet/time/TimesheetControls.d.ts +18 -0
- package/dist/planmagnet/time/TimesheetControls.d.ts.map +1 -0
- package/dist/planmagnet/time/TimesheetControls.js +244 -0
- package/dist/planmagnet/time/TimesheetControls.js.map +1 -0
- package/dist/planmagnet/time/timeShared.d.ts +29 -0
- package/dist/planmagnet/time/timeShared.d.ts.map +1 -0
- package/dist/planmagnet/time/timeShared.js +34 -0
- package/dist/planmagnet/time/timeShared.js.map +1 -0
- package/dist/planmagnet/utils/rowNavigation.js +5 -2
- package/dist/planmagnet/utils/rowNavigation.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TeamControls.js","names":[],"sources":["../../../src/planmagnet/team/TeamControls.tsx"],"sourcesContent":["import { useDeferredValue, useEffect, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Button,\n Checkbox,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { useWorkspaceDirectory } from '@burdenoff/fe-libs/sharing';\nimport { Pencil, Plus, Trash2, X } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { PersonAvatar, PersonName } from '../people/People';\n\n/**\n * Team controls (BOFF-8044). A Team is a first-class, cross-project entity: members, a lead, capacity, and the\n * projects it can draw team-sprint work from. Adding a member or a project here writes ONLY `team_members` /\n * `team_projects` rows — it never touches RBAC, never assigns a platform role, and grants no project access\n * (the owner decision this ticket is built to). Access to a project's own data stays governed purely by each\n * actor's own PlanMagnet workspace-role permissions, exactly as before a team existed.\n */\n\nexport interface TeamMemberRef {\n id: string;\n actorId: string;\n}\n\nexport interface TeamProjectRef {\n id: string;\n name: string;\n key: string;\n}\n\n/** Every query that lists teams or shows a team's membership/projects. */\nexport const TEAM_QUERIES = ['GetTeams', 'GetTeamDetail'];\n\n// ─── Header actions (edit / delete) ────────────────────────────────────────\n\nconst DELETE_TEAM = gql`\n mutation TeamControlsDelete($id: ID!) {\n planmagnetDeleteTeam(id: $id)\n }\n`;\n\nexport function TeamHeaderActions({ teamId, name }: { teamId: string; name: string }) {\n const navigate = usePlanMagnetNavigate();\n const [remove, removeState] = useMutation(DELETE_TEAM, {\n refetchQueries: TEAM_QUERIES,\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: teamId } }));\n navigate('/team/teams-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The team could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"team-edit\"\n onClick={() => navigate(detailPath('/team/team-edit-page', teamId))}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete ${name}?`}\n description=\"The team is deleted. Its team-scoped sprints disappear from every list — the same way a deleted project's sprints do. Its projects and members are unaffected; nothing about them is deleted. This cannot be undone here.\"\n confirmLabel=\"Delete team\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n// ─── Members ────────────────────────────────────────────────────────────────\n\nconst ADD_TEAM_MEMBERS = gql`\n mutation TeamControlsAddMembers($id: ID!, $input: PlanmagnetTeamMembersInput!) {\n planmagnetAddTeamMembers(id: $id, input: $input) {\n id\n }\n }\n`;\nconst REMOVE_TEAM_MEMBERS = gql`\n mutation TeamControlsRemoveMembers($id: ID!, $input: PlanmagnetTeamMembersInput!) {\n planmagnetRemoveTeamMembers(id: $id, input: $input) {\n id\n }\n }\n`;\n\nexport function TeamMembersPanel({\n teamId,\n members,\n}: {\n teamId: string;\n members: TeamMemberRef[];\n}) {\n const [remove, removeState] = useMutation(REMOVE_TEAM_MEMBERS, {\n refetchQueries: TEAM_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (actorId: string) => {\n setError(null);\n setRemovingId(actorId);\n try {\n await throwOnError(remove({ variables: { id: teamId, input: { actorIds: [actorId] } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The member could not be removed.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"team-members\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {members.length} member{members.length === 1 ? '' : 's'}\n </p>\n <AddTeamMembersDialog teamId={teamId} existingActorIds={members.map((m) => m.actorId)} />\n </div>\n {error && <ErrorLine message={error} />}\n {members.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No members on this team yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {members.map((member) => (\n <li\n key={member.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <span className=\"flex items-center gap-2 min-w-0\">\n <PersonAvatar\n id={member.actorId}\n className=\"h-7 w-7 shrink-0 rounded-full bg-primary/10 flex items-center justify-center text-[10px] font-medium text-primary\"\n />\n <PersonName id={member.actorId} className=\"truncate\" />\n </span>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label=\"Remove member\"\n disabled={removeState.loading}\n onClick={() => handleRemove(member.actorId)}\n >\n {removingId === member.actorId ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddTeamMembersDialog({\n teamId,\n existingActorIds,\n}: {\n teamId: string;\n existingActorIds: string[];\n}) {\n const { workspaceId, authToken, apiGatewayUrl } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const deferredSearch = useDeferredValue(search.trim());\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const directory = useWorkspaceDirectory({\n workspaceId: workspaceId ?? '',\n authToken: authToken ?? undefined,\n apiGatewayUrl: apiGatewayUrl ?? undefined,\n });\n const { search: setDirectoryFilter } = directory;\n // The directory's own filter, applied on the loaded page; fetchMore below widens the page.\n useEffect(() => {\n setDirectoryFilter(deferredSearch);\n }, [deferredSearch, setDirectoryFilter]);\n const candidates = directory.members.filter((m) => !existingActorIds.includes(m.id));\n\n const [add, addState] = useMutation(ADD_TEAM_MEMBERS, {\n refetchQueries: TEAM_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setSearch('');\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await throwOnError(add({ variables: { id: teamId, input: { actorIds: [...selected] } } }));\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The members could not be added.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"team-add-members\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add members\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add members</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Adding someone to the team does not grant them access to the team's projects —\n project access is still controlled by each person's own workspace role.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <Input\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search workspace members\"\n aria-label=\"Search workspace members\"\n />\n <div className=\"max-h-80 overflow-y-auto space-y-1\" data-testid=\"team-add-candidates\">\n {directory.loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>\n )}\n {!directory.loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n {deferredSearch\n ? 'No matching members.'\n : 'Everyone in the workspace is already on this team.'}\n </p>\n )}\n {candidates.map((member) => {\n const inputId = `team-add-member-${member.id}`;\n return (\n <label\n key={member.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(member.id)}\n onCheckedChange={(checked) => toggle(member.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">\n {member.name}\n {member.email && (\n <span className=\"text-muted-foreground\"> · {member.email}</span>\n )}\n </span>\n </label>\n );\n })}\n {directory.hasMore && (\n <button\n type=\"button\"\n className=\"w-full py-1 text-xs text-primary hover:underline\"\n onClick={directory.fetchMore}\n >\n Show more members\n </button>\n )}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={handleAdd}\n disabled={selected.size === 0 || addState.loading}\n data-testid=\"team-add-members-confirm\"\n >\n {addState.loading\n ? 'Adding…'\n : `Add ${selected.size} member${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── Projects ───────────────────────────────────────────────────────────────\n\nconst ADD_TEAM_PROJECTS = gql`\n mutation TeamControlsAddProjects($id: ID!, $input: PlanmagnetTeamProjectsInput!) {\n planmagnetAddTeamProjects(id: $id, input: $input) {\n id\n }\n }\n`;\nconst REMOVE_TEAM_PROJECTS = gql`\n mutation TeamControlsRemoveProjects($id: ID!, $input: PlanmagnetTeamProjectsInput!) {\n planmagnetRemoveTeamProjects(id: $id, input: $input) {\n id\n }\n }\n`;\nconst TEAM_CANDIDATE_PROJECTS = gql`\n query TeamControlsCandidateProjects($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n key\n }\n totalCount\n }\n }\n`;\n\nexport function TeamProjectsPanel({\n teamId,\n projects,\n}: {\n teamId: string;\n projects: TeamProjectRef[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [remove, removeState] = useMutation(REMOVE_TEAM_PROJECTS, {\n refetchQueries: TEAM_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (projectId: string) => {\n setError(null);\n setRemovingId(projectId);\n try {\n await throwOnError(remove({ variables: { id: teamId, input: { projectIds: [projectId] } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The project could not be removed.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"team-projects\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {projects.length} project{projects.length === 1 ? '' : 's'}\n </p>\n <AddTeamProjectsDialog teamId={teamId} existingProjectIds={projects.map((p) => p.id)} />\n </div>\n {/* Removing a project here does not touch work items already in a team sprint from it (no retroactive\n eviction, BOFF-8044 §4.2) — only new adds from that project stop being eligible from this point on. */}\n {error && <ErrorLine message={error} />}\n {projects.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No projects on this team yet. Team sprints can only draw work items from these.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {projects.map((project) => (\n <li\n key={project.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => navigate(detailPath('/projects/project-detail-page', project.id))}\n >\n <span className=\"text-muted-foreground mr-2\">{project.key}</span>\n {project.name}\n </button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove ${project.name} from team`}\n disabled={removeState.loading}\n onClick={() => handleRemove(project.id)}\n >\n {removingId === project.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddTeamProjectsDialog({\n teamId,\n existingProjectIds,\n}: {\n teamId: string;\n existingProjectIds: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetProjects: { items: TeamProjectRef[]; totalCount: number };\n }>(TEAM_CANDIDATE_PROJECTS, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const candidates = (data?.planmagnetProjects.items ?? []).filter(\n (p) => !existingProjectIds.includes(p.id)\n );\n\n const [add, addState] = useMutation(ADD_TEAM_PROJECTS, {\n refetchQueries: TEAM_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await throwOnError(add({ variables: { id: teamId, input: { projectIds: [...selected] } } }));\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The projects could not be added.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"team-add-projects\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add projects\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add projects</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n A team sprint can only hold work items from a project on this list.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div\n className=\"max-h-80 overflow-y-auto space-y-1\"\n data-testid=\"team-add-project-candidates\"\n >\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n Every project in this workspace is already on this team.\n </p>\n )}\n {candidates.map((project) => {\n const inputId = `team-add-project-${project.id}`;\n return (\n <label\n key={project.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(project.id)}\n onCheckedChange={(checked) => toggle(project.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">\n <span className=\"text-muted-foreground mr-2\">{project.key}</span>\n {project.name}\n </span>\n </label>\n );\n })}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={handleAdd}\n disabled={selected.size === 0 || addState.loading}\n data-testid=\"team-add-projects-confirm\"\n >\n {addState.loading\n ? 'Adding…'\n : `Add ${selected.size} project${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function TaskTimePanel({ workspaceId, workItemId, projectId, taskLabel, }: {
|
|
2
|
+
workspaceId?: string | null;
|
|
3
|
+
workItemId: string;
|
|
4
|
+
projectId: string;
|
|
5
|
+
taskLabel: string;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=TaskTimePanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaskTimePanel.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/time/TaskTimePanel.tsx"],"names":[],"mappings":"AA8CA,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,UAAU,EACV,SAAS,EACT,SAAS,GACV,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,2CA0DA"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { PersonName as e } from "../people/People.js";
|
|
2
|
+
import { formatMinutes as t } from "./timeShared.js";
|
|
3
|
+
import { LogTimeDialog as n } from "./TimeEntryControls.js";
|
|
4
|
+
import { Clock as r } from "lucide-react";
|
|
5
|
+
import { gql as i } from "@apollo/client";
|
|
6
|
+
import { useQuery as a } from "@apollo/client/react";
|
|
7
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
8
|
+
import { Card as c, CardContent as l, CardHeader as u, CardTitle as d } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
//#region src/planmagnet/time/TaskTimePanel.tsx
|
|
10
|
+
var f = i`
|
|
11
|
+
query TaskTimeEntries($workspaceId: ID!, $workItemId: String!) {
|
|
12
|
+
planmagnetTimeEntries(
|
|
13
|
+
workspaceId: $workspaceId
|
|
14
|
+
filter: { workItemId: $workItemId }
|
|
15
|
+
pagination: { first: 50 }
|
|
16
|
+
) {
|
|
17
|
+
items {
|
|
18
|
+
id
|
|
19
|
+
userId
|
|
20
|
+
description
|
|
21
|
+
durationMinutes
|
|
22
|
+
date
|
|
23
|
+
billingType
|
|
24
|
+
status
|
|
25
|
+
}
|
|
26
|
+
totalCount
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
function p({ workspaceId: i, workItemId: p, projectId: m, taskLabel: h }) {
|
|
31
|
+
let { data: g, loading: _, error: v } = a(f, {
|
|
32
|
+
variables: {
|
|
33
|
+
workspaceId: i,
|
|
34
|
+
workItemId: p
|
|
35
|
+
},
|
|
36
|
+
skip: !i || !p
|
|
37
|
+
}), y = g?.planmagnetTimeEntries.items ?? [], b = y.reduce((e, t) => e + t.durationMinutes, 0);
|
|
38
|
+
return /* @__PURE__ */ s(c, { children: [/* @__PURE__ */ s(u, {
|
|
39
|
+
className: "flex flex-row items-center justify-between space-y-0",
|
|
40
|
+
children: [/* @__PURE__ */ s(d, {
|
|
41
|
+
className: "flex items-center gap-2",
|
|
42
|
+
children: [
|
|
43
|
+
/* @__PURE__ */ o(r, { className: "h-4 w-4" }),
|
|
44
|
+
"Time logged ",
|
|
45
|
+
g ? `(${t(b)})` : ""
|
|
46
|
+
]
|
|
47
|
+
}), /* @__PURE__ */ o(n, {
|
|
48
|
+
lock: {
|
|
49
|
+
workItemId: p,
|
|
50
|
+
projectId: m,
|
|
51
|
+
label: h
|
|
52
|
+
},
|
|
53
|
+
refetchQueries: ["TaskTimeEntries"],
|
|
54
|
+
triggerLabel: "Log time"
|
|
55
|
+
})]
|
|
56
|
+
}), /* @__PURE__ */ s(l, {
|
|
57
|
+
className: "space-y-2",
|
|
58
|
+
children: [
|
|
59
|
+
_ && !g && /* @__PURE__ */ o("p", {
|
|
60
|
+
className: "text-sm text-muted-foreground",
|
|
61
|
+
children: "Loading…"
|
|
62
|
+
}),
|
|
63
|
+
v && /* @__PURE__ */ s("p", {
|
|
64
|
+
role: "alert",
|
|
65
|
+
className: "text-sm text-destructive",
|
|
66
|
+
children: ["Time entries could not be loaded: ", v.message]
|
|
67
|
+
}),
|
|
68
|
+
g && y.length === 0 && /* @__PURE__ */ o("p", {
|
|
69
|
+
className: "text-sm text-muted-foreground",
|
|
70
|
+
children: "No time logged on this task yet."
|
|
71
|
+
}),
|
|
72
|
+
y.map((n) => /* @__PURE__ */ s("div", {
|
|
73
|
+
className: "flex items-start justify-between gap-3 rounded-md border p-2",
|
|
74
|
+
children: [/* @__PURE__ */ s("div", {
|
|
75
|
+
className: "min-w-0",
|
|
76
|
+
children: [/* @__PURE__ */ s("p", {
|
|
77
|
+
className: "text-sm",
|
|
78
|
+
children: [
|
|
79
|
+
/* @__PURE__ */ o(e, {
|
|
80
|
+
id: n.userId,
|
|
81
|
+
className: "font-medium"
|
|
82
|
+
}),
|
|
83
|
+
" ",
|
|
84
|
+
/* @__PURE__ */ o("span", {
|
|
85
|
+
className: "text-muted-foreground",
|
|
86
|
+
children: new Date(n.date).toLocaleDateString()
|
|
87
|
+
})
|
|
88
|
+
]
|
|
89
|
+
}), n.description && /* @__PURE__ */ o("p", {
|
|
90
|
+
className: "text-xs text-muted-foreground truncate",
|
|
91
|
+
children: n.description
|
|
92
|
+
})]
|
|
93
|
+
}), /* @__PURE__ */ o("span", {
|
|
94
|
+
className: "shrink-0 text-sm font-medium tabular-nums",
|
|
95
|
+
children: t(n.durationMinutes)
|
|
96
|
+
})]
|
|
97
|
+
}, n.id))
|
|
98
|
+
]
|
|
99
|
+
})] });
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { p as TaskTimePanel };
|
|
103
|
+
|
|
104
|
+
//# sourceMappingURL=TaskTimePanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaskTimePanel.js","names":[],"sources":["../../../src/planmagnet/time/TaskTimePanel.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { Clock } from 'lucide-react';\nimport { LogTimeDialog } from './TimeEntryControls';\nimport { formatMinutes } from './timeShared';\nimport { PersonName } from '../people/People';\n\n/**\n * The Task Detail entry point into time tracking (BOFF-8047, rule 2): \"log time on a task\" is meaningless\n * without a way to do it from the task itself. Read-only here on purpose — full edit/delete of an entry\n * lives on the Time Entries page (`pages/TasksTimeEntriesPagePage.tsx`), the single place that page needs to\n * reason about whether an entry is still editable (its owning timesheet may already be frozen).\n */\n\nconst TASK_TIME_ENTRIES = gql`\n query TaskTimeEntries($workspaceId: ID!, $workItemId: String!) {\n planmagnetTimeEntries(\n workspaceId: $workspaceId\n filter: { workItemId: $workItemId }\n pagination: { first: 50 }\n ) {\n items {\n id\n userId\n description\n durationMinutes\n date\n billingType\n status\n }\n totalCount\n }\n }\n`;\n\ninterface EntryRow {\n id: string;\n userId: string;\n description: string | null;\n durationMinutes: number;\n date: string;\n billingType: string;\n status: string;\n}\n\nexport function TaskTimePanel({\n workspaceId,\n workItemId,\n projectId,\n taskLabel,\n}: {\n workspaceId?: string | null;\n workItemId: string;\n projectId: string;\n taskLabel: string;\n}) {\n const { data, loading, error } = useQuery<{\n planmagnetTimeEntries: { items: EntryRow[]; totalCount: number };\n }>(TASK_TIME_ENTRIES, {\n variables: { workspaceId, workItemId },\n skip: !workspaceId || !workItemId,\n });\n\n const entries = data?.planmagnetTimeEntries.items ?? [];\n const totalMinutes = entries.reduce((sum, e) => sum + e.durationMinutes, 0);\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle className=\"flex items-center gap-2\">\n <Clock className=\"h-4 w-4\" />\n Time logged {data ? `(${formatMinutes(totalMinutes)})` : ''}\n </CardTitle>\n <LogTimeDialog\n lock={{ workItemId, projectId, label: taskLabel }}\n refetchQueries={['TaskTimeEntries']}\n triggerLabel=\"Log time\"\n />\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading…</p>}\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n Time entries could not be loaded: {error.message}\n </p>\n )}\n {data && entries.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">No time logged on this task yet.</p>\n )}\n {entries.map((entry) => (\n <div\n key={entry.id}\n className=\"flex items-start justify-between gap-3 rounded-md border p-2\"\n >\n <div className=\"min-w-0\">\n <p className=\"text-sm\">\n <PersonName id={entry.userId} className=\"font-medium\" />{' '}\n <span className=\"text-muted-foreground\">\n {new Date(entry.date).toLocaleDateString()}\n </span>\n </p>\n {entry.description && (\n <p className=\"text-xs text-muted-foreground truncate\">{entry.description}</p>\n )}\n </div>\n <span className=\"shrink-0 text-sm font-medium tabular-nums\">\n {formatMinutes(entry.durationMinutes)}\n </span>\n </div>\n ))}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;AAeA,IAAM,IAAoB,CAAG;;;;;;;;;;;;;;;;;;;;AA+B7B,SAAgB,EAAc,EAC5B,gBACA,eACA,cACA,gBAMC;CACD,IAAM,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAmB;EACpB,WAAW;GAAE;GAAa;GAAY;EACtC,MAAM,CAAC,KAAe,CAAC;EACxB,CAAC,EAEI,IAAU,GAAM,sBAAsB,SAAS,EAAE,EACjD,IAAe,EAAQ,QAAQ,GAAK,MAAM,IAAM,EAAE,iBAAiB,EAAE;AAE3E,QACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;EAAY,WAAU;YAAtB,CACE,kBAAC,GAAD;GAAW,WAAU;aAArB;IACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;;IAChB,IAAO,IAAI,EAAc,EAAa,CAAC,KAAK;IAC/C;MACZ,kBAAC,GAAD;GACE,MAAM;IAAE;IAAY;IAAW,OAAO;IAAW;GACjD,gBAAgB,CAAC,kBAAkB;GACnC,cAAa;GACb,CAAA,CACS;KACb,kBAAC,GAAD;EAAa,WAAU;YAAvB;GACG,KAAW,CAAC,KAAQ,kBAAC,KAAD;IAAG,WAAU;cAAgC;IAAY,CAAA;GAC7E,KACC,kBAAC,KAAD;IAAG,MAAK;IAAQ,WAAU;cAA1B,CAAqD,sCAChB,EAAM,QACvC;;GAEL,KAAQ,EAAQ,WAAW,KAC1B,kBAAC,KAAD;IAAG,WAAU;cAAgC;IAAoC,CAAA;GAElF,EAAQ,KAAK,MACZ,kBAAC,OAAD;IAEE,WAAU;cAFZ,CAIE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAb;OACE,kBAAC,GAAD;QAAY,IAAI,EAAM;QAAQ,WAAU;QAAgB,CAAA;OAAC;OACzD,kBAAC,QAAD;QAAM,WAAU;kBACb,IAAI,KAAK,EAAM,KAAK,CAAC,oBAAoB;QACrC,CAAA;OACL;SACH,EAAM,eACL,kBAAC,KAAD;MAAG,WAAU;gBAA0C,EAAM;MAAgB,CAAA,CAE3E;QACN,kBAAC,QAAD;KAAM,WAAU;eACb,EAAc,EAAM,gBAAgB;KAChC,CAAA,CACH;MAjBC,EAAM,GAiBP,CACN;GACU;IACT,EAAA,CAAA"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** The work item a "Log time" dialog logs against, when the caller already knows it (e.g. a task's own page). */
|
|
2
|
+
export interface TimeEntryLock {
|
|
3
|
+
workItemId: string;
|
|
4
|
+
projectId: string;
|
|
5
|
+
/** Shown in the dialog instead of the project/work-item pickers, e.g. "PM-42 — Ship the thing". */
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Log time against a work item. With `lock`, the work item is fixed (the task-detail entry point); otherwise
|
|
10
|
+
* the dialog offers a project picker and a work-item search, for logging time from the Time Entries page.
|
|
11
|
+
*/
|
|
12
|
+
export declare function LogTimeDialog({ lock, refetchQueries, onLogged, triggerLabel, triggerSize, }: {
|
|
13
|
+
lock?: TimeEntryLock;
|
|
14
|
+
/** Every query that must re-run after a successful log, e.g. ['TimeEntriesList'] or ['TaskTimeEntries']. */
|
|
15
|
+
refetchQueries: string[];
|
|
16
|
+
onLogged?: (id: string) => void;
|
|
17
|
+
triggerLabel?: string;
|
|
18
|
+
triggerSize?: 'sm' | 'default';
|
|
19
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export interface TimeEntryValues {
|
|
21
|
+
id: string;
|
|
22
|
+
description: string | null;
|
|
23
|
+
durationMinutes: number;
|
|
24
|
+
date: string;
|
|
25
|
+
billingType: string;
|
|
26
|
+
}
|
|
27
|
+
/** Edit a time entry's description, duration, date and billing type. Only changed fields are sent. */
|
|
28
|
+
export declare function EditTimeEntryDialog({ entry, refetchQueries, }: {
|
|
29
|
+
entry: TimeEntryValues;
|
|
30
|
+
refetchQueries: string[];
|
|
31
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
/** Delete a time entry, with confirmation. */
|
|
33
|
+
export declare function DeleteTimeEntryButton({ id, refetchQueries, }: {
|
|
34
|
+
id: string;
|
|
35
|
+
refetchQueries: string[];
|
|
36
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
37
|
+
//# sourceMappingURL=TimeEntryControls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimeEntryControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/time/TimeEntryControls.tsx"],"names":[],"mappings":"AAiFA,iHAAiH;AACjH,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,mGAAmG;IACnG,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,cAAc,EACd,QAAQ,EACR,YAAyB,EACzB,WAAkB,GACnB,EAAE;IACD,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,4GAA4G;IAC5G,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;CAChC,2CAyLA;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,sGAAsG;AACtG,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,cAAc,GACf,EAAE;IACD,KAAK,EAAE,eAAe,CAAC;IACvB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B,2CAuIA;AAED,8CAA8C;AAC9C,wBAAgB,qBAAqB,CAAC,EACpC,EAAE,EACF,cAAc,GACf,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B,2CAiCA"}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { throwOnError as e } from "../utils/mutationResult.js";
|
|
2
|
+
import { ConfirmButton as t, ErrorLine as n } from "../controls/ConfirmButton.js";
|
|
3
|
+
import { ProjectSelect as r, WorkItemSearchSelect as i } from "../controls/Pickers.js";
|
|
4
|
+
import { enumLabel as a } from "../controls/WorkItemControls.js";
|
|
5
|
+
import { BILLING_TYPES as o, hoursInputToMinutes as s, minutesToHoursInput as c, toDateInputValue as l, todayInputValue as u } from "./timeShared.js";
|
|
6
|
+
import { useState as d } from "react";
|
|
7
|
+
import { Clock as f, Pencil as p, Trash2 as m } from "lucide-react";
|
|
8
|
+
import { gql as h } from "@apollo/client";
|
|
9
|
+
import { useMutation as g } from "@apollo/client/react";
|
|
10
|
+
import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
|
|
11
|
+
import { Button as b, Input as x, ResponsiveDialog as S, ResponsiveDialogContent as C, ResponsiveDialogDescription as w, ResponsiveDialogFooter as T, ResponsiveDialogHeader as E, ResponsiveDialogTitle as D, ResponsiveDialogTrigger as O } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
//#region src/planmagnet/time/TimeEntryControls.tsx
|
|
13
|
+
var k = h`
|
|
14
|
+
mutation TimeEntryControlsLogTime($input: PlanmagnetLogTimeInput!) {
|
|
15
|
+
planmagnetLogTime(input: $input) {
|
|
16
|
+
id
|
|
17
|
+
workItemId
|
|
18
|
+
projectId
|
|
19
|
+
timesheetId
|
|
20
|
+
description
|
|
21
|
+
durationMinutes
|
|
22
|
+
date
|
|
23
|
+
status
|
|
24
|
+
billingType
|
|
25
|
+
createdAt
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
`, A = h`
|
|
29
|
+
mutation TimeEntryControlsUpdate($id: ID!, $input: PlanmagnetUpdateTimeEntryInput!) {
|
|
30
|
+
planmagnetUpdateTimeEntry(id: $id, input: $input) {
|
|
31
|
+
id
|
|
32
|
+
description
|
|
33
|
+
durationMinutes
|
|
34
|
+
date
|
|
35
|
+
billingType
|
|
36
|
+
updatedAt
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`, j = h`
|
|
40
|
+
mutation TimeEntryControlsDelete($id: ID!) {
|
|
41
|
+
planmagnetDeleteTimeEntry(id: $id)
|
|
42
|
+
}
|
|
43
|
+
`, M = "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60", N = (e, t) => e instanceof Error ? e.message : t;
|
|
44
|
+
function P({ lock: t, refetchQueries: c, onLogged: l, triggerLabel: p = "Log time", triggerSize: m = "sm" }) {
|
|
45
|
+
let [h, A] = d(!1), [j, P] = d(""), [F, I] = d(""), [L, R] = d(""), [z, B] = d(u()), [V, H] = d("NON_BILLABLE"), [U, W] = d(""), [G, K] = d(null), [q, { loading: J }] = g(k, {
|
|
46
|
+
refetchQueries: c,
|
|
47
|
+
awaitRefetchQueries: !0
|
|
48
|
+
}), Y = () => {
|
|
49
|
+
P(""), I(""), R(""), B(u()), H("NON_BILLABLE"), W(""), K(null);
|
|
50
|
+
}, X = t?.workItemId ?? F, Z = t?.projectId ?? j, Q = s(L), $ = !J && !!X && !!Z && Q > 0 && !!z, ee = async () => {
|
|
51
|
+
K(null);
|
|
52
|
+
try {
|
|
53
|
+
let t = (await e(q({ variables: { input: {
|
|
54
|
+
workItemId: X,
|
|
55
|
+
projectId: Z,
|
|
56
|
+
durationMinutes: Q,
|
|
57
|
+
date: z,
|
|
58
|
+
billingType: V,
|
|
59
|
+
...U.trim() ? { description: U.trim() } : {}
|
|
60
|
+
} } }))).data?.planmagnetLogTime.id;
|
|
61
|
+
A(!1), Y(), t && l?.(t);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
K(N(e, "The time could not be logged."));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
return /* @__PURE__ */ y(S, {
|
|
67
|
+
open: h,
|
|
68
|
+
onOpenChange: (e) => {
|
|
69
|
+
A(e), e ? Y() : K(null);
|
|
70
|
+
},
|
|
71
|
+
children: [/* @__PURE__ */ v(O, {
|
|
72
|
+
asChild: !0,
|
|
73
|
+
children: /* @__PURE__ */ y(b, {
|
|
74
|
+
size: m,
|
|
75
|
+
"data-testid": "log-time-open",
|
|
76
|
+
children: [/* @__PURE__ */ v(f, { className: "mr-1 h-4 w-4" }), p]
|
|
77
|
+
})
|
|
78
|
+
}), /* @__PURE__ */ y(C, {
|
|
79
|
+
className: "max-w-lg",
|
|
80
|
+
children: [
|
|
81
|
+
/* @__PURE__ */ y(E, { children: [/* @__PURE__ */ v(D, { children: "Log time" }), /* @__PURE__ */ v(w, { children: t ? `Against ${t.label}.` : "Pick a project and a work item, then the time spent." })] }),
|
|
82
|
+
/* @__PURE__ */ y("div", {
|
|
83
|
+
className: "space-y-3",
|
|
84
|
+
children: [
|
|
85
|
+
!t && /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
86
|
+
htmlFor: "log-time-project",
|
|
87
|
+
className: "text-sm font-medium",
|
|
88
|
+
children: "Project"
|
|
89
|
+
}), /* @__PURE__ */ v("div", {
|
|
90
|
+
className: "mt-1",
|
|
91
|
+
children: /* @__PURE__ */ v(r, {
|
|
92
|
+
id: "log-time-project",
|
|
93
|
+
value: j,
|
|
94
|
+
onChange: (e) => {
|
|
95
|
+
P(e), I("");
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
})] }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
99
|
+
htmlFor: "log-time-work-item",
|
|
100
|
+
className: "text-sm font-medium",
|
|
101
|
+
children: "Work item"
|
|
102
|
+
}), /* @__PURE__ */ v("div", {
|
|
103
|
+
className: "mt-1",
|
|
104
|
+
children: /* @__PURE__ */ v(i, {
|
|
105
|
+
id: "log-time-work-item",
|
|
106
|
+
projectId: j || void 0,
|
|
107
|
+
value: F,
|
|
108
|
+
onChange: I
|
|
109
|
+
})
|
|
110
|
+
})] })] }),
|
|
111
|
+
/* @__PURE__ */ y("div", {
|
|
112
|
+
className: "grid grid-cols-2 gap-3",
|
|
113
|
+
children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
114
|
+
htmlFor: "log-time-hours",
|
|
115
|
+
className: "text-sm font-medium",
|
|
116
|
+
children: "Hours"
|
|
117
|
+
}), /* @__PURE__ */ v(x, {
|
|
118
|
+
id: "log-time-hours",
|
|
119
|
+
className: "mt-1",
|
|
120
|
+
type: "number",
|
|
121
|
+
min: "0",
|
|
122
|
+
step: "0.25",
|
|
123
|
+
inputMode: "decimal",
|
|
124
|
+
placeholder: "e.g. 1.5",
|
|
125
|
+
value: L,
|
|
126
|
+
onChange: (e) => R(e.target.value)
|
|
127
|
+
})] }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
128
|
+
htmlFor: "log-time-date",
|
|
129
|
+
className: "text-sm font-medium",
|
|
130
|
+
children: "Date"
|
|
131
|
+
}), /* @__PURE__ */ v(x, {
|
|
132
|
+
id: "log-time-date",
|
|
133
|
+
className: "mt-1",
|
|
134
|
+
type: "date",
|
|
135
|
+
value: z,
|
|
136
|
+
onChange: (e) => B(e.target.value)
|
|
137
|
+
})] })]
|
|
138
|
+
}),
|
|
139
|
+
/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
140
|
+
htmlFor: "log-time-billing",
|
|
141
|
+
className: "text-sm font-medium",
|
|
142
|
+
children: "Billing type"
|
|
143
|
+
}), /* @__PURE__ */ v("select", {
|
|
144
|
+
id: "log-time-billing",
|
|
145
|
+
className: M,
|
|
146
|
+
value: V,
|
|
147
|
+
onChange: (e) => H(e.target.value),
|
|
148
|
+
children: o.map((e) => /* @__PURE__ */ v("option", {
|
|
149
|
+
value: e,
|
|
150
|
+
children: a(e)
|
|
151
|
+
}, e))
|
|
152
|
+
})] }),
|
|
153
|
+
/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
154
|
+
htmlFor: "log-time-description",
|
|
155
|
+
className: "text-sm font-medium",
|
|
156
|
+
children: "Description"
|
|
157
|
+
}), /* @__PURE__ */ v("textarea", {
|
|
158
|
+
id: "log-time-description",
|
|
159
|
+
className: `${M} min-h-[64px]`,
|
|
160
|
+
placeholder: "What did you work on? (optional)",
|
|
161
|
+
value: U,
|
|
162
|
+
onChange: (e) => W(e.target.value)
|
|
163
|
+
})] }),
|
|
164
|
+
G && /* @__PURE__ */ v(n, { message: G })
|
|
165
|
+
]
|
|
166
|
+
}),
|
|
167
|
+
/* @__PURE__ */ y(T, { children: [/* @__PURE__ */ v(b, {
|
|
168
|
+
variant: "outline",
|
|
169
|
+
onClick: () => A(!1),
|
|
170
|
+
children: "Cancel"
|
|
171
|
+
}), /* @__PURE__ */ v(b, {
|
|
172
|
+
onClick: () => void ee(),
|
|
173
|
+
disabled: !$,
|
|
174
|
+
"data-testid": "log-time-submit",
|
|
175
|
+
children: J ? "Logging…" : "Log time"
|
|
176
|
+
})] })
|
|
177
|
+
]
|
|
178
|
+
})]
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function F({ entry: t, refetchQueries: r }) {
|
|
182
|
+
let i = {
|
|
183
|
+
description: t.description ?? "",
|
|
184
|
+
hours: c(t.durationMinutes),
|
|
185
|
+
date: l(t.date),
|
|
186
|
+
billingType: t.billingType
|
|
187
|
+
}, [u, f] = d(!1), [m, h] = d(i), [_, k] = d(null), [j, { loading: P }] = g(A, {
|
|
188
|
+
refetchQueries: r,
|
|
189
|
+
awaitRefetchQueries: !0
|
|
190
|
+
}), F = s(m.hours), I = !P && F > 0 && !!m.date, L = async () => {
|
|
191
|
+
k(null);
|
|
192
|
+
let n = {};
|
|
193
|
+
if (m.description.trim() !== i.description.trim() && (n.description = m.description.trim() || null), F !== t.durationMinutes && (n.durationMinutes = F), m.date !== i.date && (n.date = m.date), m.billingType !== i.billingType && (n.billingType = m.billingType), Object.keys(n).length === 0) {
|
|
194
|
+
f(!1);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
await e(j({ variables: {
|
|
199
|
+
id: t.id,
|
|
200
|
+
input: n
|
|
201
|
+
} })), f(!1);
|
|
202
|
+
} catch (e) {
|
|
203
|
+
k(N(e, "The time entry could not be saved."));
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
return /* @__PURE__ */ y(S, {
|
|
207
|
+
open: u,
|
|
208
|
+
onOpenChange: (e) => {
|
|
209
|
+
f(e), e ? h(i) : k(null);
|
|
210
|
+
},
|
|
211
|
+
children: [/* @__PURE__ */ v(O, {
|
|
212
|
+
asChild: !0,
|
|
213
|
+
children: /* @__PURE__ */ v(b, {
|
|
214
|
+
size: "sm",
|
|
215
|
+
variant: "outline",
|
|
216
|
+
"aria-label": "Edit time entry",
|
|
217
|
+
"data-testid": "time-entry-edit",
|
|
218
|
+
children: /* @__PURE__ */ v(p, { className: "h-4 w-4" })
|
|
219
|
+
})
|
|
220
|
+
}), /* @__PURE__ */ y(C, {
|
|
221
|
+
className: "max-w-lg",
|
|
222
|
+
children: [
|
|
223
|
+
/* @__PURE__ */ y(E, { children: [/* @__PURE__ */ v(D, { children: "Edit time entry" }), /* @__PURE__ */ v(w, { children: "Change the hours, date, billing type or description." })] }),
|
|
224
|
+
/* @__PURE__ */ y("div", {
|
|
225
|
+
className: "space-y-3",
|
|
226
|
+
children: [
|
|
227
|
+
/* @__PURE__ */ y("div", {
|
|
228
|
+
className: "grid grid-cols-2 gap-3",
|
|
229
|
+
children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
230
|
+
htmlFor: "edit-time-hours",
|
|
231
|
+
className: "text-sm font-medium",
|
|
232
|
+
children: "Hours"
|
|
233
|
+
}), /* @__PURE__ */ v(x, {
|
|
234
|
+
id: "edit-time-hours",
|
|
235
|
+
className: "mt-1",
|
|
236
|
+
type: "number",
|
|
237
|
+
min: "0",
|
|
238
|
+
step: "0.25",
|
|
239
|
+
inputMode: "decimal",
|
|
240
|
+
value: m.hours,
|
|
241
|
+
onChange: (e) => h({
|
|
242
|
+
...m,
|
|
243
|
+
hours: e.target.value
|
|
244
|
+
})
|
|
245
|
+
})] }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
246
|
+
htmlFor: "edit-time-date",
|
|
247
|
+
className: "text-sm font-medium",
|
|
248
|
+
children: "Date"
|
|
249
|
+
}), /* @__PURE__ */ v(x, {
|
|
250
|
+
id: "edit-time-date",
|
|
251
|
+
className: "mt-1",
|
|
252
|
+
type: "date",
|
|
253
|
+
value: m.date,
|
|
254
|
+
onChange: (e) => h({
|
|
255
|
+
...m,
|
|
256
|
+
date: e.target.value
|
|
257
|
+
})
|
|
258
|
+
})] })]
|
|
259
|
+
}),
|
|
260
|
+
/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
261
|
+
htmlFor: "edit-time-billing",
|
|
262
|
+
className: "text-sm font-medium",
|
|
263
|
+
children: "Billing type"
|
|
264
|
+
}), /* @__PURE__ */ v("select", {
|
|
265
|
+
id: "edit-time-billing",
|
|
266
|
+
className: M,
|
|
267
|
+
value: m.billingType,
|
|
268
|
+
onChange: (e) => h({
|
|
269
|
+
...m,
|
|
270
|
+
billingType: e.target.value
|
|
271
|
+
}),
|
|
272
|
+
children: o.map((e) => /* @__PURE__ */ v("option", {
|
|
273
|
+
value: e,
|
|
274
|
+
children: a(e)
|
|
275
|
+
}, e))
|
|
276
|
+
})] }),
|
|
277
|
+
/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
|
|
278
|
+
htmlFor: "edit-time-description",
|
|
279
|
+
className: "text-sm font-medium",
|
|
280
|
+
children: "Description"
|
|
281
|
+
}), /* @__PURE__ */ v("textarea", {
|
|
282
|
+
id: "edit-time-description",
|
|
283
|
+
className: `${M} min-h-[64px]`,
|
|
284
|
+
value: m.description,
|
|
285
|
+
onChange: (e) => h({
|
|
286
|
+
...m,
|
|
287
|
+
description: e.target.value
|
|
288
|
+
})
|
|
289
|
+
})] }),
|
|
290
|
+
_ && /* @__PURE__ */ v(n, { message: _ })
|
|
291
|
+
]
|
|
292
|
+
}),
|
|
293
|
+
/* @__PURE__ */ y(T, { children: [/* @__PURE__ */ v(b, {
|
|
294
|
+
variant: "outline",
|
|
295
|
+
onClick: () => f(!1),
|
|
296
|
+
children: "Cancel"
|
|
297
|
+
}), /* @__PURE__ */ v(b, {
|
|
298
|
+
onClick: () => void L(),
|
|
299
|
+
disabled: !I,
|
|
300
|
+
"data-testid": "edit-time-save",
|
|
301
|
+
children: P ? "Saving…" : "Save"
|
|
302
|
+
})] })
|
|
303
|
+
]
|
|
304
|
+
})]
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function I({ id: r, refetchQueries: i }) {
|
|
308
|
+
let [a, { loading: o }] = g(j, {
|
|
309
|
+
refetchQueries: i,
|
|
310
|
+
awaitRefetchQueries: !0
|
|
311
|
+
}), [s, c] = d(null), l = async () => {
|
|
312
|
+
c(null);
|
|
313
|
+
try {
|
|
314
|
+
await e(a({ variables: { id: r } }));
|
|
315
|
+
} catch (e) {
|
|
316
|
+
c(N(e, "The time entry could not be deleted."));
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
return /* @__PURE__ */ y("div", {
|
|
320
|
+
className: "flex flex-col items-end gap-1",
|
|
321
|
+
children: [/* @__PURE__ */ v(t, {
|
|
322
|
+
label: "",
|
|
323
|
+
ariaLabel: "Delete time entry",
|
|
324
|
+
variant: "outline",
|
|
325
|
+
icon: /* @__PURE__ */ v(m, { className: "h-4 w-4" }),
|
|
326
|
+
title: "Delete this time entry?",
|
|
327
|
+
description: "The logged time is removed. This cannot be undone here.",
|
|
328
|
+
confirmLabel: "Delete entry",
|
|
329
|
+
destructive: !0,
|
|
330
|
+
disabled: o,
|
|
331
|
+
onConfirm: () => void l()
|
|
332
|
+
}), s && /* @__PURE__ */ v(n, { message: s })]
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
//#endregion
|
|
336
|
+
export { I as DeleteTimeEntryButton, F as EditTimeEntryDialog, P as LogTimeDialog };
|
|
337
|
+
|
|
338
|
+
//# sourceMappingURL=TimeEntryControls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimeEntryControls.js","names":[],"sources":["../../../src/planmagnet/time/TimeEntryControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport {\n Button,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { Clock, Pencil, Trash2 } from 'lucide-react';\nimport { ProjectSelect, WorkItemSearchSelect } from '../controls/Pickers';\nimport { throwOnError } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport {\n BILLING_TYPES,\n type BillingType,\n enumLabel,\n hoursInputToMinutes,\n minutesToHoursInput,\n todayInputValue,\n toDateInputValue,\n} from './timeShared';\n\n/**\n * Time entry controls (BOFF-8047): logging time, editing an entry, deleting one.\n *\n * `planmagnetLogTime` (not the lower-level `planmagnetCreateTimeEntry`) is the write path here — it is the\n * \"quick log time\" mutation the schema documents for this exact purpose, and it does not accept a billable\n * rate. That is deliberate, not a missing feature: \"A Delivery Contributor with EDITOR can read every time\n * entry in the workspace, including colleagues' billable rate and currency\" is a known, still-open RBAC gap\n * (planmagnet-specs/CORRECTIONS.md §B) — the resolver does not scope `billingRate` by role. Keeping rate\n * capture and display out of this UI entirely means this screen cannot become a new way to see it; fixing\n * the gap itself is a backend RBAC change, out of scope here (recorded as a backend gap).\n */\n\nconst LOG_TIME = gql`\n mutation TimeEntryControlsLogTime($input: PlanmagnetLogTimeInput!) {\n planmagnetLogTime(input: $input) {\n id\n workItemId\n projectId\n timesheetId\n description\n durationMinutes\n date\n status\n billingType\n createdAt\n }\n }\n`;\n\nconst UPDATE_TIME_ENTRY = gql`\n mutation TimeEntryControlsUpdate($id: ID!, $input: PlanmagnetUpdateTimeEntryInput!) {\n planmagnetUpdateTimeEntry(id: $id, input: $input) {\n id\n description\n durationMinutes\n date\n billingType\n updatedAt\n }\n }\n`;\n\nconst DELETE_TIME_ENTRY = gql`\n mutation TimeEntryControlsDelete($id: ID!) {\n planmagnetDeleteTimeEntry(id: $id)\n }\n`;\n\nconst FIELD =\n 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\nconst messageOf = (e: unknown, fallback: string) => (e instanceof Error ? e.message : fallback);\n\n/** The work item a \"Log time\" dialog logs against, when the caller already knows it (e.g. a task's own page). */\nexport interface TimeEntryLock {\n workItemId: string;\n projectId: string;\n /** Shown in the dialog instead of the project/work-item pickers, e.g. \"PM-42 — Ship the thing\". */\n label: string;\n}\n\n/**\n * Log time against a work item. With `lock`, the work item is fixed (the task-detail entry point); otherwise\n * the dialog offers a project picker and a work-item search, for logging time from the Time Entries page.\n */\nexport function LogTimeDialog({\n lock,\n refetchQueries,\n onLogged,\n triggerLabel = 'Log time',\n triggerSize = 'sm',\n}: {\n lock?: TimeEntryLock;\n /** Every query that must re-run after a successful log, e.g. ['TimeEntriesList'] or ['TaskTimeEntries']. */\n refetchQueries: string[];\n onLogged?: (id: string) => void;\n triggerLabel?: string;\n triggerSize?: 'sm' | 'default';\n}) {\n const [open, setOpen] = useState(false);\n const [projectId, setProjectId] = useState('');\n const [workItemId, setWorkItemId] = useState('');\n const [hours, setHours] = useState('');\n const [date, setDate] = useState(todayInputValue());\n const [billingType, setBillingType] = useState<BillingType>('NON_BILLABLE');\n const [description, setDescription] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [log, { loading }] = useMutation<{ planmagnetLogTime: { id: string } }>(LOG_TIME, {\n refetchQueries,\n awaitRefetchQueries: true,\n });\n\n const reset = () => {\n setProjectId('');\n setWorkItemId('');\n setHours('');\n setDate(todayInputValue());\n setBillingType('NON_BILLABLE');\n setDescription('');\n setError(null);\n };\n\n const effectiveWorkItemId = lock?.workItemId ?? workItemId;\n const effectiveProjectId = lock?.projectId ?? projectId;\n const minutes = hoursInputToMinutes(hours);\n const canSubmit =\n !loading && !!effectiveWorkItemId && !!effectiveProjectId && minutes > 0 && !!date;\n\n const submit = async () => {\n setError(null);\n try {\n const result = await throwOnError(\n log({\n variables: {\n input: {\n workItemId: effectiveWorkItemId,\n projectId: effectiveProjectId,\n durationMinutes: minutes,\n date,\n billingType,\n ...(description.trim() ? { description: description.trim() } : {}),\n },\n },\n })\n );\n const id = result.data?.planmagnetLogTime.id;\n setOpen(false);\n reset();\n if (id) onLogged?.(id);\n } catch (e) {\n setError(messageOf(e, 'The time could not be logged.'));\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) reset();\n else setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size={triggerSize} data-testid=\"log-time-open\">\n <Clock className=\"mr-1 h-4 w-4\" />\n {triggerLabel}\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Log time</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {lock\n ? `Against ${lock.label}.`\n : 'Pick a project and a work item, then the time spent.'}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n {!lock && (\n <>\n <div>\n <label htmlFor=\"log-time-project\" className=\"text-sm font-medium\">\n Project\n </label>\n <div className=\"mt-1\">\n <ProjectSelect\n id=\"log-time-project\"\n value={projectId}\n onChange={(next) => {\n setProjectId(next);\n setWorkItemId('');\n }}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"log-time-work-item\" className=\"text-sm font-medium\">\n Work item\n </label>\n <div className=\"mt-1\">\n <WorkItemSearchSelect\n id=\"log-time-work-item\"\n projectId={projectId || undefined}\n value={workItemId}\n onChange={setWorkItemId}\n />\n </div>\n </div>\n </>\n )}\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"log-time-hours\" className=\"text-sm font-medium\">\n Hours\n </label>\n <Input\n id=\"log-time-hours\"\n className=\"mt-1\"\n type=\"number\"\n min=\"0\"\n step=\"0.25\"\n inputMode=\"decimal\"\n placeholder=\"e.g. 1.5\"\n value={hours}\n onChange={(e) => setHours(e.target.value)}\n />\n </div>\n <div>\n <label htmlFor=\"log-time-date\" className=\"text-sm font-medium\">\n Date\n </label>\n <Input\n id=\"log-time-date\"\n className=\"mt-1\"\n type=\"date\"\n value={date}\n onChange={(e) => setDate(e.target.value)}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"log-time-billing\" className=\"text-sm font-medium\">\n Billing type\n </label>\n <select\n id=\"log-time-billing\"\n className={FIELD}\n value={billingType}\n onChange={(e) => setBillingType(e.target.value as BillingType)}\n >\n {BILLING_TYPES.map((b) => (\n <option key={b} value={b}>\n {enumLabel(b)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"log-time-description\" className=\"text-sm font-medium\">\n Description\n </label>\n <textarea\n id=\"log-time-description\"\n className={`${FIELD} min-h-[64px]`}\n placeholder=\"What did you work on? (optional)\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>\n Cancel\n </Button>\n <Button onClick={() => void submit()} disabled={!canSubmit} data-testid=\"log-time-submit\">\n {loading ? 'Logging…' : 'Log time'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nexport interface TimeEntryValues {\n id: string;\n description: string | null;\n durationMinutes: number;\n date: string;\n billingType: string;\n}\n\n/** Edit a time entry's description, duration, date and billing type. Only changed fields are sent. */\nexport function EditTimeEntryDialog({\n entry,\n refetchQueries,\n}: {\n entry: TimeEntryValues;\n refetchQueries: string[];\n}) {\n const initial = {\n description: entry.description ?? '',\n hours: minutesToHoursInput(entry.durationMinutes),\n date: toDateInputValue(entry.date),\n billingType: entry.billingType,\n };\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(initial);\n const [error, setError] = useState<string | null>(null);\n const [update, { loading }] = useMutation(UPDATE_TIME_ENTRY, {\n refetchQueries,\n awaitRefetchQueries: true,\n });\n\n const minutes = hoursInputToMinutes(form.hours);\n const canSubmit = !loading && minutes > 0 && !!form.date;\n\n const save = async () => {\n setError(null);\n const input: Record<string, unknown> = {};\n if (form.description.trim() !== initial.description.trim())\n input.description = form.description.trim() || null;\n if (minutes !== entry.durationMinutes) input.durationMinutes = minutes;\n if (form.date !== initial.date) input.date = form.date;\n if (form.billingType !== initial.billingType) input.billingType = form.billingType;\n if (Object.keys(input).length === 0) {\n setOpen(false);\n return;\n }\n try {\n await throwOnError(update({ variables: { id: entry.id, input } }));\n setOpen(false);\n } catch (e) {\n setError(messageOf(e, 'The time entry could not be saved.'));\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) setForm(initial);\n else setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button\n size=\"sm\"\n variant=\"outline\"\n aria-label=\"Edit time entry\"\n data-testid=\"time-entry-edit\"\n >\n <Pencil className=\"h-4 w-4\" />\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit time entry</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Change the hours, date, billing type or description.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"edit-time-hours\" className=\"text-sm font-medium\">\n Hours\n </label>\n <Input\n id=\"edit-time-hours\"\n className=\"mt-1\"\n type=\"number\"\n min=\"0\"\n step=\"0.25\"\n inputMode=\"decimal\"\n value={form.hours}\n onChange={(e) => setForm({ ...form, hours: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"edit-time-date\" className=\"text-sm font-medium\">\n Date\n </label>\n <Input\n id=\"edit-time-date\"\n className=\"mt-1\"\n type=\"date\"\n value={form.date}\n onChange={(e) => setForm({ ...form, date: e.target.value })}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"edit-time-billing\" className=\"text-sm font-medium\">\n Billing type\n </label>\n <select\n id=\"edit-time-billing\"\n className={FIELD}\n value={form.billingType}\n onChange={(e) => setForm({ ...form, billingType: e.target.value })}\n >\n {BILLING_TYPES.map((b) => (\n <option key={b} value={b}>\n {enumLabel(b)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"edit-time-description\" className=\"text-sm font-medium\">\n Description\n </label>\n <textarea\n id=\"edit-time-description\"\n className={`${FIELD} min-h-[64px]`}\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>\n Cancel\n </Button>\n <Button onClick={() => void save()} disabled={!canSubmit} data-testid=\"edit-time-save\">\n {loading ? 'Saving…' : 'Save'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n/** Delete a time entry, with confirmation. */\nexport function DeleteTimeEntryButton({\n id,\n refetchQueries,\n}: {\n id: string;\n refetchQueries: string[];\n}) {\n const [remove, { loading }] = useMutation(DELETE_TIME_ENTRY, {\n refetchQueries,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id } }));\n } catch (e) {\n setError(messageOf(e, 'The time entry could not be deleted.'));\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <ConfirmButton\n label=\"\"\n ariaLabel=\"Delete time entry\"\n variant=\"outline\"\n icon={<Trash2 className=\"h-4 w-4\" />}\n title=\"Delete this time entry?\"\n description=\"The logged time is removed. This cannot be undone here.\"\n confirmLabel=\"Delete entry\"\n destructive\n disabled={loading}\n onConfirm={() => void onDelete()}\n />\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
|