Nest projects under a master project and roll their figures up (#1264)

Projects were flat. The parent_id column and the sub-project list existed
but nothing could set a parent outside the API, and a master project's
stats only ever covered its own prints.

The project dialog gets a parent picker, and a project with sub-projects
gets a second card covering the whole tree -- jobs, parts, time, filament,
cost, and progress against every target in the tree added together. That
card is separate from the project's own stats, which keep their existing
meaning; widening them would have restated the figures of anyone who had
already nested projects over the API. Each listed sub-project carries its
own branch's roll-up, so the rows add up to the card above them.

On the Projects page a sub-project is drawn inside its parent's group
rather than as another card in the grid -- two cards columns apart cannot
show that they belong together, whatever the caption says. A sub-project
whose parent the status filter has hidden stays put and names its parent
instead.

compute_project_stats now goes through the same grouped aggregation as the
roll-up rather than its own copy of the SQL, since the two must agree.

Three things the interface made reachable:

- PATCH refused only a project as its own direct parent, so A -> B -> A
  was two calls away. A cycle has no root to roll up to, and the walk
  keeps its seen-set for databases that already contain one.
- A sub-project's percentage was completed quantities against the plate
  target, disagreeing with the page it linked to.
- Deleting a mid-tree project orphaned its children at top level; they
  now move up to its own parent.
This commit is contained in:
maziggy
2026-08-06 10:00:45 +02:00
parent 312f09a4ac
commit afa0ba0dc0
26 changed files with 1575 additions and 308 deletions
+15
View File
@@ -908,12 +908,21 @@ export interface ProjectStats {
bom_cost: number;
}
// A sub-project as listed on its parent's page. The figures cover the child's
// own subtree, so the listed rows add up to the parent's roll-up minus the
// parent's own prints (#1264).
export interface ProjectChildPreview {
id: number;
name: string;
color: string | null;
status: string;
progress_percent: number | null;
descendant_count: number;
total_archives: number;
completed_prints: number;
total_print_time_hours: number;
total_filament_grams: number;
total_cost: number; // Filament + energy + BOM, matching the parent's cost card
}
export interface Project {
@@ -936,9 +945,13 @@ export interface Project {
parent_id: number | null;
parent_name: string | null;
children: ProjectChildPreview[];
descendant_count: number; // Sub-projects at any depth beneath this one (#1264)
created_at: string;
updated_at: string;
stats?: ProjectStats;
// This project's numbers combined with every sub-project's. Null when there
// are none, since it would only repeat `stats` (#1264).
rollup_stats?: ProjectStats | null;
url: string | null; // External link rendered next to project name on the card (#1155)
cover_image_filename: string | null; // Filename within project attachments dir (#1155)
}
@@ -985,6 +998,8 @@ export interface ProjectListItem {
failed_count: number; // Sum of quantities for failed prints
queue_count: number;
progress_percent: number | null; // Plates progress
parent_id: number | null; // #1264 — set when this is a sub-project
child_count: number; // #1264 — direct sub-projects only
archives: ArchivePreview[];
url: string | null; // #1155
cover_image_filename: string | null; // #1155