diff --git a/backend/package.json b/backend/package.json index c8cab4a..fb86991 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "hoa-ledgeriq-backend", - "version": "0.2.0", + "version": "2026.3.2-beta", "description": "HOA LedgerIQ - Backend API", "private": true, "scripts": { diff --git a/backend/src/modules/projects/projects.service.ts b/backend/src/modules/projects/projects.service.ts index 392e6c9..11a9faf 100644 --- a/backend/src/modules/projects/projects.service.ts +++ b/backend/src/modules/projects/projects.service.ts @@ -20,7 +20,7 @@ export class ProjectsService { async findForPlanning() { const projects = await this.tenant.query( - 'SELECT * FROM projects WHERE is_active = true AND target_year IS NOT NULL ORDER BY target_year, target_month NULLS LAST, priority', + 'SELECT * FROM projects WHERE is_active = true ORDER BY target_year NULLS LAST, target_month NULLS LAST, priority', ); return this.computeFunding(projects); } diff --git a/frontend/package.json b/frontend/package.json index 6e51efc..a165315 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "hoa-ledgeriq-frontend", - "version": "0.2.0", + "version": "2026.3.2-beta", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/pages/capital-projects/CapitalProjectsPage.tsx b/frontend/src/pages/capital-projects/CapitalProjectsPage.tsx index 4177652..013b1de 100644 --- a/frontend/src/pages/capital-projects/CapitalProjectsPage.tsx +++ b/frontend/src/pages/capital-projects/CapitalProjectsPage.tsx @@ -30,7 +30,7 @@ interface Project { fund_source: string; funded_percentage: string; planned_date: string; - target_year: number; + target_year: number | null; target_month: number; status: string; priority: number; @@ -38,6 +38,7 @@ interface Project { } const FUTURE_YEAR = 9999; +const UNSCHEDULED = -1; // sentinel for projects with no target_year const statusColors: Record = { planned: 'blue', approved: 'green', in_progress: 'yellow', @@ -49,7 +50,8 @@ const priorityColor = (p: number) => (p <= 2 ? 'red' : p <= 3 ? 'yellow' : 'gray const fmt = (v: string | number) => parseFloat(String(v || '0')).toLocaleString('en-US', { style: 'currency', currency: 'USD' }); -const yearLabel = (year: number) => (year === FUTURE_YEAR ? 'Future' : String(year)); +const yearLabel = (year: number) => + year === FUTURE_YEAR ? 'Future' : year === UNSCHEDULED ? 'Unscheduled' : String(year); const formatPlannedDate = (d: string | null | undefined) => { if (!d) return null; @@ -154,7 +156,8 @@ function KanbanColumn({ }: KanbanColumnProps) { const totalEst = projects.reduce((s, p) => s + parseFloat(p.estimated_cost || '0'), 0); const isFuture = year === FUTURE_YEAR; - const useWideLayout = isFuture && projects.length > 3; + const isUnscheduled = year === UNSCHEDULED; + const useWideLayout = (isFuture || isUnscheduled) && projects.length > 3; return ( {yearLabel(year)} + {isUnscheduled && projects.length > 0 && ( + needs scheduling + )} {fmt(totalEst)} @@ -311,10 +321,10 @@ export function CapitalProjectsPage() { }); const moveProjectMutation = useMutation({ - mutationFn: ({ id, target_year, target_month }: { id: string; target_year: number; target_month: number }) => { + mutationFn: ({ id, target_year, target_month }: { id: string; target_year: number | null; target_month: number }) => { const payload: Record = { target_year }; // Derive planned_date based on the new year - if (target_year === FUTURE_YEAR) { + if (target_year === null || target_year === FUTURE_YEAR) { payload.planned_date = null; } else { payload.planned_date = `${target_year}-${String(target_month || 6).padStart(2, '0')}-01`; @@ -353,7 +363,7 @@ export function CapitalProjectsPage() { form.setValues({ status: p.status || 'planned', priority: p.priority || 3, - target_year: p.target_year, + target_year: p.target_year ?? currentYear, target_month: p.target_month || 6, planned_date: p.planned_date || '', notes: p.notes || '', @@ -376,7 +386,7 @@ export function CapitalProjectsPage() { const handleDragStart = useCallback((e: DragEvent, project: Project) => { e.dataTransfer.setData('application/json', JSON.stringify({ id: project.id, - source_year: project.target_year, + source_year: project.target_year ?? UNSCHEDULED, target_month: project.target_month, })); e.dataTransfer.effectAllowed = 'move'; @@ -400,7 +410,7 @@ export function CapitalProjectsPage() { if (payload.source_year !== targetYear) { moveProjectMutation.mutate({ id: payload.id, - target_year: targetYear, + target_year: targetYear === UNSCHEDULED ? null : targetYear, target_month: payload.target_month || 6, }); } @@ -413,15 +423,20 @@ export function CapitalProjectsPage() { // Always show current year through current+4, plus FUTURE_YEAR if any projects have it const baseYears = Array.from({ length: 5 }, (_, i) => currentYear + i); - const projectYears = [...new Set(projects.map((p) => p.target_year))]; + const projectYears = [...new Set(projects.map((p) => p.target_year).filter((y): y is number => y !== null))]; const hasFutureProjects = projectYears.includes(FUTURE_YEAR); + const hasUnscheduledProjects = projects.some((p) => p.target_year === null); // Merge base years with any extra years from projects (excluding FUTURE_YEAR for now) const regularYears = [...new Set([...baseYears, ...projectYears.filter((y) => y !== FUTURE_YEAR)])].sort(); - const years = hasFutureProjects ? [...regularYears, FUTURE_YEAR] : regularYears; + const years = [ + ...(hasUnscheduledProjects ? [UNSCHEDULED] : []), + ...regularYears, + ...(hasFutureProjects ? [FUTURE_YEAR] : []), + ]; - // Kanban columns: always current..current+4 plus Future - const kanbanYears = [...baseYears, FUTURE_YEAR]; + // Kanban columns: Unscheduled + current..current+4 + Future + const kanbanYears = [UNSCHEDULED, ...baseYears, FUTURE_YEAR]; // ---- Loading state ---- @@ -441,12 +456,11 @@ export function CapitalProjectsPage() { - No projects in the capital plan + No projects yet - Capital Planning displays projects that have a target year assigned. Head over to the Projects page to define your reserve and operating - projects, then assign target years to see them here. + projects. They'll appear here for capital planning and scheduling.