From feb0851e312daeea3bb8a912b7dd6b69781c5609 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 20:11:10 +0000 Subject: [PATCH 1/2] Add project filter to report page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a project dropdown to the weekly report page so users can scope the week preview, AI prompt, and generated summary to a single project. Also introduces filterWeekByProject utility and a full test suite for reportUtils. - reportUtils: add filterWeekByProject(week, project) — filters days/tasks by project name, excludes empty days, recomputes totalDuration - reportUtils.test.ts: new test file covering filterWeekByProject, formatDuration, groupByCalendarWeek, and getMostRecentCompleteWeek - Report.tsx: add selectedProject state + filteredWeek memo; render a project Select below week navigation when the week has ≥1 project; pass filteredWeek to WeekPreview, generate(), and auto-save; include project in localStorage key so filtered summaries don't collide with full-week summaries; reset project on week change https://claude.ai/code/session_014FCyGZxPpxqy1z1Stg4X9u --- src/pages/Report.tsx | 91 ++++++++++--- src/utils/reportUtils.test.ts | 245 ++++++++++++++++++++++++++++++++++ src/utils/reportUtils.ts | 28 ++++ 3 files changed, 346 insertions(+), 18 deletions(-) create mode 100644 src/utils/reportUtils.test.ts diff --git a/src/pages/Report.tsx b/src/pages/Report.tsx index 06a23be..081fc2d 100644 --- a/src/pages/Report.tsx +++ b/src/pages/Report.tsx @@ -19,6 +19,13 @@ import { PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; @@ -27,6 +34,7 @@ import { groupByCalendarWeek, groupByDateRange, getMostRecentCompleteWeek, + filterWeekByProject, formatDuration, serializeWeekForPrompt, weekKey, @@ -368,6 +376,8 @@ function SavedSummaryBanner({ // Main Page // --------------------------------------------------------------------------- +const ALL_PROJECTS_VALUE = "all-projects"; + export default function Report() { const { archivedDays: rawArchivedDays, todoItems } = useTimeTracking(); const archivedDays = useMemo( @@ -383,14 +393,23 @@ export default function Report() { const [isCustomRange, setIsCustomRange] = useState(false); const [calendarIndex, setCalendarIndex] = useState(0); const [tone, setTone] = useState('standup'); + const [selectedProject, setSelectedProject] = useState(''); + + const filteredWeek = useMemo( + () => selectedWeek ? filterWeekByProject(selectedWeek, selectedProject) : null, + [selectedWeek, selectedProject] + ); const { summary, state, error, generate, load, updateSummary, reset } = useReportSummary(); - // Derive the localStorage key for the current week+tone + // Derive the localStorage key for the current week+tone+project. // Empty string when no week is selected yet; useReportStorage will return null - // for this key and save() is gated behind the generate button (requires selectedWeek) - const currentWeekKey = selectedWeek ? weekKey(selectedWeek.weekStart) : ""; + // for this key and save() is gated behind the generate button (requires selectedWeek). + // Including the project keeps project-filtered summaries separate from full-week summaries. + const currentWeekKey = selectedWeek + ? `${weekKey(selectedWeek.weekStart)}${selectedProject ? `_${selectedProject}` : ""}` + : ""; const { saved: savedSummary, save: saveSummary, @@ -399,13 +418,13 @@ export default function Report() { // Auto-save whenever generation succeeds or the user edits the summary useEffect(() => { - if (state !== "success" || !summary || !selectedWeek) return; + if (state !== "success" || !summary || !filteredWeek) return; // Debounce saves to avoid write-on-every-keystroke (important for iOS WebView) const timer = setTimeout(() => { - saveSummary(summary, selectedWeek.label); + saveSummary(summary, filteredWeek.label); }, 300); return () => clearTimeout(timer); - }, [state, summary, selectedWeek, saveSummary]); + }, [state, summary, filteredWeek, saveSummary]); useEffect(() => { const initial = getMostRecentCompleteWeek(calendarWeeks); @@ -417,6 +436,7 @@ export default function Report() { if (next < calendarWeeks.length) { setCalendarIndex(next); setSelectedWeek(calendarWeeks[next]); + setSelectedProject(''); setIsCustomRange(false); reset(); } @@ -427,6 +447,7 @@ export default function Report() { if (next >= 0) { setCalendarIndex(next); setSelectedWeek(calendarWeeks[next]); + setSelectedProject(''); setIsCustomRange(false); reset(); } @@ -435,6 +456,7 @@ export default function Report() { function handleCustomRange(from: Date, to: Date) { const group = groupByDateRange(archivedDays, from, to); setSelectedWeek(group); + setSelectedProject(''); setIsCustomRange(true); reset(); } @@ -444,9 +466,14 @@ export default function Report() { reset(); } + function handleProjectChange(v: string) { + setSelectedProject(v); + reset(); + } + function handleGenerate() { - if (!selectedWeek) return; - generate(selectedWeek, tone, todoItems); + if (!filteredWeek) return; + generate(filteredWeek, tone, todoItems); } function handleSummaryUpdate(v: string) { @@ -463,7 +490,7 @@ export default function Report() { const canGoPrev = !isCustomRange && calendarIndex < calendarWeeks.length - 1; const canGoNext = !isCustomRange && calendarIndex > 0; - const hasNoData = selectedWeek?.days.length === 0; + const hasNoData = filteredWeek?.days.length === 0; // Empty state — no archived data at all if (archivedDays.length === 0) { @@ -546,6 +573,7 @@ export default function Report() { onClick={() => { setIsCustomRange(false); setSelectedWeek(calendarWeeks[calendarIndex]); + setSelectedProject(''); reset(); }} > @@ -555,16 +583,43 @@ export default function Report() { )} + {/* Project filter */} + {selectedWeek && selectedWeek.projects.length > 0 && ( +
+ + +
+ )} + {/* Week preview */} - {selectedWeek && } + {filteredWeek && } {/* Tone selector */} - {selectedWeek && !hasNoData && ( + {filteredWeek && !hasNoData && ( )} {/* Generate button */} - {selectedWeek && !hasNoData && state !== 'loading' && ( + {filteredWeek && !hasNoData && state !== 'loading' && (