Season view: filter the editor to a year (#54) (#66)
Build image / build-and-push (push) Successful in 17s

Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #66.
This commit is contained in:
2026-07-21 05:47:34 +00:00
committed by steve
parent b96ca1ec0a
commit 8c5ddb21ec
17 changed files with 556 additions and 36 deletions
+62
View File
@@ -62,6 +62,68 @@ func queryPlantings(ctx context.Context, q queryer, query string, args ...any) (
return plantings, nil
}
// ListPlantingsForGardenYear returns every plop in a garden whose time in the
// ground overlaps the given calendar year — the season view (#54).
//
// The interval is [planted_at, removed_at]: planted before the year ended, and
// either still in the ground or removed after the year began. Garlic planted in
// October and pulled the following July therefore appears in BOTH years, which
// is what actually happened.
//
// Undated plantings are always included. Every planting that predates this
// feature has a null planted_at, and a rule that excluded them would empty every
// existing garden the moment a year was selected — an unhelpful way to be
// technically correct.
func (d *DB) ListPlantingsForGardenYear(ctx context.Context, gardenID int64, year int) ([]domain.Planting, error) {
start := fmt.Sprintf("%04d-01-01", year)
end := fmt.Sprintf("%04d-12-31", year)
return queryPlantings(ctx, d.sql,
`SELECT `+qualifyColumns("pl", plantingColumns)+` FROM plantings pl
JOIN garden_objects o ON o.id = pl.object_id
WHERE o.garden_id = ?
AND (pl.planted_at IS NULL OR pl.planted_at <= ?)
AND (pl.removed_at IS NULL OR pl.removed_at >= ?)
ORDER BY pl.id`,
gardenID, end, start)
}
// GardenPlantingYears lists the calendar years a garden has planting data for,
// newest first — every year touched by a [planted_at, removed_at] interval.
//
// Undated plantings contribute no year, deliberately: they belong to every year
// the selector offers, so adding one here would be noise.
func (d *DB) GardenPlantingYears(ctx context.Context, gardenID int64) ([]int, error) {
rows, err := d.sql.QueryContext(ctx,
`SELECT DISTINCT CAST(substr(y, 1, 4) AS INTEGER) AS year FROM (
SELECT pl.planted_at AS y FROM plantings pl
JOIN garden_objects o ON o.id = pl.object_id
WHERE o.garden_id = ? AND pl.planted_at IS NOT NULL
UNION
SELECT pl.removed_at AS y FROM plantings pl
JOIN garden_objects o ON o.id = pl.object_id
WHERE o.garden_id = ? AND pl.removed_at IS NOT NULL
)
ORDER BY year DESC`,
gardenID, gardenID)
if err != nil {
return nil, fmt.Errorf("store: garden planting years: %w", err)
}
defer rows.Close()
years := []int{}
for rows.Next() {
var y int
if err := rows.Scan(&y); err != nil {
return nil, fmt.Errorf("store: scan planting year: %w", err)
}
years = append(years, y)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("store: iterate planting years: %w", err)
}
return years, nil
}
// ListActivePlantingsForObject returns an object's currently-planted plops
// (removed_at IS NULL). Always a non-nil slice. Used by FillRegion to avoid
// stacking new plops inside existing ones.