life-manager/codebase/features/projects/frontend/ProjectGoalsPage.tsx
2026-03-17 17:51:06 -07:00

186 lines
4.9 KiB
TypeScript

/** @jsxImportSource react */
import { useState, useEffect, useCallback } from 'react';
import type { ReactElement } from 'react';
import styled from 'styled-components';
import { useTheme } from '@lilith/ui-theme';
import { useProjectContext } from './useProjectContext';
import { useGoalTree } from '@features/goals/frontend/useGoals';
import { GoalLevel, GoalStatus } from '@life-platform/shared';
import type { Goal } from '@life-platform/shared';
import { Page, SectionTitle, Card, EmptyState } from '@features/domains/frontend/domain-styles';
import GoalTreeNode from '@features/domains/frontend/components/GoalTreeNode';
const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
`;
const LevelPills = styled.div`
display: flex;
gap: 6px;
flex-wrap: wrap;
`;
const LevelPill = styled.span<{ $color: string }>`
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
background: ${({ $color }) => `${$color}22`};
color: ${({ $color }) => $color};
`;
const ToggleButtons = styled.div`
display: flex;
gap: 4px;
`;
const ToggleBtn = styled.button`
background: none;
border: 1px solid ${({ theme }) => theme.colors.border.default};
border-radius: 4px;
color: ${({ theme }) => theme.colors.text.muted};
font-size: 10px;
padding: 3px 8px;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 0.5px;
&:hover {
color: ${({ theme }) => theme.colors.text.primary};
border-color: ${({ theme }) => theme.colors.text.muted};
}
`;
const LEVEL_COLORS: Record<GoalLevel, string> = {
[GoalLevel.Yearly]: '#ff6600',
[GoalLevel.Quarterly]: '#aa44ff',
[GoalLevel.Monthly]: '#4488ff',
[GoalLevel.Weekly]: '#00ff9f',
};
function collectAllIds(goals: Goal[]): string[] {
const ids: string[] = [];
function walk(list: Goal[]): void {
for (const g of list) {
ids.push(g.id);
if (g.children?.length) walk(g.children);
}
}
walk(goals);
return ids;
}
function collectActiveIds(goals: Goal[]): string[] {
const ids: string[] = [];
function walk(list: Goal[]): boolean {
let hasActive = false;
for (const g of list) {
const childActive = g.children?.length ? walk(g.children) : false;
if (g.status === GoalStatus.Active || childActive) {
ids.push(g.id);
hasActive = true;
}
}
return hasActive;
}
walk(goals);
return ids;
}
function countByLevel(goals: Goal[]): Record<GoalLevel, number> {
const counts = {
[GoalLevel.Yearly]: 0,
[GoalLevel.Quarterly]: 0,
[GoalLevel.Monthly]: 0,
[GoalLevel.Weekly]: 0,
};
function walk(list: Goal[]): void {
for (const g of list) {
if (counts[g.level] !== undefined) counts[g.level]++;
if (g.children?.length) walk(g.children);
}
}
walk(goals);
return counts;
}
export default function ProjectGoalsPage(): ReactElement | null {
const { project } = useProjectContext();
const { theme } = useTheme();
const { data: goalTree } = useGoalTree({ projectId: project.id });
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const [initialized, setInitialized] = useState(false);
const goals = goalTree ?? [];
useEffect(() => {
if (goals.length > 0 && !initialized) {
setExpandedIds(new Set(collectActiveIds(goals)));
setInitialized(true);
}
}, [goals, initialized]);
const handleToggle = useCallback((id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}, []);
const accent = project.color || theme.colors.primary.main;
const levelCounts = countByLevel(goals);
return (
<Page>
<Header>
<SectionTitle style={{ margin: 0 }}>Goals {project.name}</SectionTitle>
<ToggleButtons>
<ToggleBtn onClick={() => setExpandedIds(new Set(collectAllIds(goals)))}>
Expand all
</ToggleBtn>
<ToggleBtn onClick={() => setExpandedIds(new Set())}>
Collapse all
</ToggleBtn>
</ToggleButtons>
</Header>
<LevelPills>
{(Object.entries(levelCounts) as [GoalLevel, number][])
.filter(([, count]) => count > 0)
.map(([level, count]) => (
<LevelPill key={level} $color={LEVEL_COLORS[level]}>
{level}: {count}
</LevelPill>
))}
</LevelPills>
<Card>
{goals.length > 0 ? (
goals.map((goal: Goal) => (
<GoalTreeNode
key={goal.id}
goal={goal}
depth={0}
accent={accent}
expandedIds={expandedIds}
onToggle={handleToggle}
/>
))
) : (
<EmptyState>No goals</EmptyState>
)}
</Card>
</Page>
);
}