diff --git a/src/App.tsx b/src/App.tsx index dce999f..a33ccd2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,7 +47,7 @@ function App() { } /> } /> } /> - } /> + } /> } /> diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index d45b415..63ca69c 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -37,11 +37,11 @@ const contentNavItems = [ { to: "/app/languages", label: "Languages", icon: Languages }, { to: "/app/organizations", label: "Organizations", icon: Building2 }, { to: "/app/projects", label: "Projects", icon: FolderOpen }, - { to: "/app/phases", label: "Phases", icon: GitBranch }, { to: "/app/map", label: "Map", icon: Globe }, ] const adminNavItems = [ + { to: "/app/phases", label: "Phases", icon: GitBranch }, { to: "/app/users", label: "Users", icon: Users }, { to: "/app/apps", label: "Manage Apps", icon: AppWindow }, ] diff --git a/src/components/pages/MapPage.tsx b/src/components/pages/MapPage.tsx index b0e8dac..0287866 100644 --- a/src/components/pages/MapPage.tsx +++ b/src/components/pages/MapPage.tsx @@ -5,7 +5,7 @@ import L from "leaflet" import "leaflet/dist/leaflet.css" import { Globe, MapPin, ChevronLeft, ChevronRight, Search } from "lucide-react" import { projectsAPI } from "@/services/api" -import type { ProjectResponse, ProjectPhaseResponse } from "@/types" +import type { PhaseStatus, ProjectResponse, ProjectPhaseResponse } from "@/types" import { useLanguagesStore } from "@/stores/languagesStore" import { LoadingSpinner } from "@/components/common/LoadingSpinner" import { Input } from "@/components/ui/input" @@ -42,7 +42,7 @@ function FlyToProject({ lat, lng }: { lat: number; lng: number }) { return null } -const PHASE_STATUS_COLORS: Record = { +const PHASE_STATUS_COLORS: Record = { not_started: { bg: `${AREIA}30`, text: VERDE, dot: `${AREIA}` }, in_progress: { bg: `${AZUL}30`, text: AZUL, dot: AZUL }, completed: { bg: "#777D4530", text: "#777D45", dot: "#777D45" }, @@ -136,7 +136,7 @@ function ProjectPopupContent({
{phases.map((p) => { - const colors = PHASE_STATUS_COLORS[p.status] ?? PHASE_STATUS_COLORS.not_started + const colors = PHASE_STATUS_COLORS[p.status] return ( void - onDetach: (node: LayoutNode) => void }) { const config = STATUS_CONFIG[node.status] const StatusIcon = config.icon @@ -191,7 +179,7 @@ function PhaseNode({
onStatusClick(node)} @@ -210,16 +198,6 @@ function PhaseNode({ {config.label}
-
@@ -263,7 +241,7 @@ function StatusPopover({

Update Status

- {ALL_STATUSES.map((status) => { + {PHASE_STATUSES.map((status) => { const config = STATUS_CONFIG[status] const StatusIcon = config.icon const isActive = node.status === status @@ -291,112 +269,11 @@ function StatusPopover({ ) } -function AttachPhaseDialog({ - open, - onOpenChange, - projectId, - attachedPhaseIds, - onAttached, -}: { - open: boolean - onOpenChange: (open: boolean) => void - projectId: string - attachedPhaseIds: Set - onAttached: () => void -}) { - const { phases: allPhases, loading, fetch: fetchGlobalPhases } = usePhasesStore() - const [attaching, setAttaching] = useState(null) - - useEffect(() => { - if (!open) return - fetchGlobalPhases() - }, [open, fetchGlobalPhases]) - - const available = useMemo( - () => allPhases.filter((p) => !attachedPhaseIds.has(p.id)), - [allPhases, attachedPhaseIds], - ) - - async function handleAttach(phaseId: string) { - setAttaching(phaseId) - try { - await projectsAPI.attachPhase(projectId, phaseId) - toast.success("Phase attached to project") - onAttached() - } catch { - toast.error("Failed to attach phase") - } finally { - setAttaching(null) - } - } - - return ( - - - - Attach Phase - - Select a global phase to attach to this project. Attached phases can be tracked with status updates. - - -
- {loading ? ( - - ) : available.length === 0 ? ( -

- {allPhases.length === 0 - ? "No global phases have been created yet." - : "All available phases are already attached."} -

- ) : ( -
- {available.map((phase) => ( -
-
-

{phase.name}

- {phase.description && ( -

{phase.description}

- )} -
- -
- ))} -
- )} -
- - - -
-
- ) -} - export function ProjectPhasesTab({ projectId }: { projectId: string }) { const [phases, setPhases] = useState([]) const [deps, setDeps] = useState>(new Map()) const [loading, setLoading] = useState(true) - const [attachOpen, setAttachOpen] = useState(false) const [statusNode, setStatusNode] = useState(null) - const [detachTarget, setDetachTarget] = useState(null) const statusAnchorRef = useRef(null) const fetchPhases = useCallback(async () => { @@ -420,7 +297,6 @@ export function ProjectPhasesTab({ projectId }: { projectId: string }) { }, [fetchPhases]) const layout = useMemo(() => buildLayout(phases, deps), [phases, deps]) - const attachedIds = useMemo(() => new Set(phases.map((p) => p.phase_id)), [phases]) async function handleStatusUpdate(phaseId: string, status: PhaseStatus) { setStatusNode(null) @@ -433,22 +309,6 @@ export function ProjectPhasesTab({ projectId }: { projectId: string }) { } } - async function handleDetach() { - if (!detachTarget) return - try { - await projectsAPI.detachPhase(projectId, detachTarget.phaseId) - toast.success("Phase detached from project") - setDetachTarget(null) - await fetchPhases() - } catch { - toast.error("Failed to detach phase") - } - } - - function handleAttached() { - fetchPhases() - } - if (loading) { return } @@ -458,21 +318,15 @@ export function ProjectPhasesTab({ projectId }: { projectId: string }) {

Phases - +

-
{phases.length === 0 ? ( setAttachOpen(true)} + title="No phases defined yet" + description="Phases and their dependencies are defined by a platform admin and apply to every project. Once created, they appear here and you can track this project's progress by setting each phase's status." /> ) : (
@@ -505,7 +359,6 @@ export function ProjectPhasesTab({ projectId }: { projectId: string }) { key={node.phaseId} node={node} onStatusClick={setStatusNode} - onDetach={setDetachTarget} /> ))} @@ -518,28 +371,10 @@ export function ProjectPhasesTab({ projectId }: { projectId: string }) { />
-

Click a phase to update its status. Hover and click the x to detach.

+

Click a phase to update its status.

)} - - - - { if (!open) setDetachTarget(null) }} - title="Detach Phase" - description={`Are you sure you want to detach "${detachTarget?.name ?? "this phase"}" from the project? The phase status for this project will be removed.`} - confirmLabel="Detach" - variant="destructive" - onConfirm={handleDetach} - /> ) } diff --git a/src/services/api.ts b/src/services/api.ts index d0f3a8c..f040d29 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -43,6 +43,7 @@ import type { PhaseUpdate, PhaseDependencyResponse, ProjectPhaseResponse, + PhaseStatus, } from "@/types" const api = axios.create({ @@ -235,11 +236,7 @@ export const projectsAPI = { api.delete(`/projects/${projectId}/access/organizations/${orgId}`), listPhases: (projectId: string) => api.get(`/projects/${projectId}/phases`), - attachPhase: (projectId: string, phaseId: string) => - api.post(`/projects/${projectId}/phases`, { phase_id: phaseId }), - detachPhase: (projectId: string, phaseId: string) => - api.delete(`/projects/${projectId}/phases/${phaseId}`), - updatePhaseStatus: (projectId: string, phaseId: string, status: string) => + updatePhaseStatus: (projectId: string, phaseId: string, status: PhaseStatus) => api.patch(`/projects/${projectId}/phases/${phaseId}`, { status }), listPhasesWithDeps: (projectId: string) => api.get<{ phases: ProjectPhaseResponse[]; dependencies: Record }>(`/projects/${projectId}/phases-with-deps`), diff --git a/src/types/index.ts b/src/types/index.ts index e343aa0..40a3243 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -67,4 +67,7 @@ export type { PhaseUpdate, PhaseDependencyResponse, ProjectPhaseResponse, + PhaseStatus, } from "./phase" + +export { PHASE_STATUSES } from "./phase" diff --git a/src/types/phase.ts b/src/types/phase.ts index 1578eaa..b542732 100644 --- a/src/types/phase.ts +++ b/src/types/phase.ts @@ -23,10 +23,19 @@ export interface PhaseDependencyResponse { depends_on_id: string } +export type PhaseStatus = "not_started" | "in_progress" | "completed" | "blocked" + +export const PHASE_STATUSES: PhaseStatus[] = [ + "not_started", + "in_progress", + "completed", + "blocked", +] + export interface ProjectPhaseResponse { id: string phase_id: string phase_name: string phase_description: string | null - status: string + status: PhaseStatus }