Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions client/src/components/ChoreWidget.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
DialogTitle,
DialogContent,
DialogActions,
Divider,
CircularProgress,
Menu,
ListItemText,
Expand Down Expand Up @@ -58,7 +59,7 @@ import { householdNow, isExpired } from '../utils/choreExpiry.js';
import useRoutinesToday from '../utils/useRoutinesToday.js';
import { shouldPersistSettings } from '../utils/widgetSettingsPersist';
import { parseVacationModeSetting } from '../utils/interfaceSettings.js';
import { addDays, choreDays } from '../utils/choreDays.js';
import { addDays, choreDays, displayDays } from '../utils/choreDays.js';
import { getServerTimezoneSync } from '../utils/timezone.js';
import ChoreDayMark from './ChoreDayMark.jsx';
import ChoreHistoryDialog from './ChoreHistoryDialog.jsx';
Expand All @@ -71,6 +72,9 @@ const DEFAULT_SHOW_BONUS_CHORES = true;
const DEFAULT_SOUND_ENABLED = true;
// Off by default: the 7-day row under each chore is opt-in per display.
const DEFAULT_SHOW_WEEK = false;
// On by default: the 7-day row shows missed days as red crosses. Turn it off
// per display to draw missed days as plain "nothing logged" instead.
const DEFAULT_SHOW_MISSED_MARKS = true;
const WEEK_DAYS = 7;
// Frozen: it is shared by every instance's initial state and by the
// no-stored-settings snapshot, so an in-place mutation would corrupt both.
Expand Down Expand Up @@ -156,6 +160,7 @@ const ChoreWidget = ({ refreshNonce = 0, hiddenControls = [] }) => {
});
const [soundEnabled, setSoundEnabled] = useState(DEFAULT_SOUND_ENABLED);
const [showWeek, setShowWeek] = useState(DEFAULT_SHOW_WEEK);
const [showMissedMarks, setShowMissedMarks] = useState(DEFAULT_SHOW_MISSED_MARKS);
// Per-device: which real users this display hides. Bonus pseudo-user (id 0)
// is not a real user and is not offered in the selector — the "Bonus Chores"
// column already has its own toggle.
Expand Down Expand Up @@ -258,11 +263,15 @@ const ChoreWidget = ({ refreshNonce = 0, hiddenControls = [] }) => {
showWeek: choreSettings && typeof choreSettings.showWeek === 'boolean'
? choreSettings.showWeek
: DEFAULT_SHOW_WEEK,
showMissedMarks: choreSettings && typeof choreSettings.showMissedMarks === 'boolean'
? choreSettings.showMissedMarks
: DEFAULT_SHOW_MISSED_MARKS,
};
setShowBonusChores(adopted.showBonusChores);
setSoundEnabled(adopted.soundEnabled);
setHiddenUserIds(adopted.hiddenUserIds);
setShowWeek(adopted.showWeek);
setShowMissedMarks(adopted.showMissedMarks);
} catch (error) {
console.error('Error loading chore widget settings:', error);
} finally {
Expand Down Expand Up @@ -291,7 +300,7 @@ const ChoreWidget = ({ refreshNonce = 0, hiddenControls = [] }) => {
return;
}

const current = { showBonusChores, soundEnabled, hiddenUserIds, showWeek };
const current = { showBonusChores, soundEnabled, hiddenUserIds, showWeek, showMissedMarks };
// The load itself is not an edit. Without this the effect writes back what
// it just read on every mount — and on every tab change that remounts the
// widget, which on a rotating display is a write a minute.
Expand All @@ -311,7 +320,7 @@ const ChoreWidget = ({ refreshNonce = 0, hiddenControls = [] }) => {
}, 250);

return () => clearTimeout(timeoutId);
}, [API_DEVICE_URL, deviceSettingsLoaded, showBonusChores, soundEnabled, hiddenUserIds, showWeek]);
}, [API_DEVICE_URL, deviceSettingsLoaded, showBonusChores, soundEnabled, hiddenUserIds, showWeek, showMissedMarks]);

useEffect(() => {
const onUsersUpdated = () => {
Expand Down Expand Up @@ -1461,9 +1470,13 @@ const ChoreWidget = ({ refreshNonce = 0, hiddenControls = [] }) => {
)}
{/* Its own line under the title and the button: beside the button,
seven marks don't fit a narrow column and push the button out. */}
{/* The 7-day row. displayDays hides the red missed crosses when
the display's "show missed days" setting is off — missed days
draw as plain "nothing logged" instead. The History dialog
still shows the real record. */}
{showWeek && schedule.user_id ? (
<Box sx={{ flexBasis: '100%', display: 'flex', gap: '2px', mt: 0.5 }} aria-label={t('chores:history.weekAria', { title: schedule.title })}>
{choreDays({ rows: weekHistory, schedule, userId: schedule.user_id, today: getTodayDateString(), count: WEEK_DAYS, vacation, timeZone: getServerTimezoneSync() })
{displayDays(choreDays({ rows: weekHistory, schedule, userId: schedule.user_id, today: getTodayDateString(), count: WEEK_DAYS, vacation, timeZone: getServerTimezoneSync() }), { showMissed: showMissedMarks })
.map((day) => (
<ChoreDayMark key={day.date} state={day.state} size={14} label={`${day.date}: ${t(`chores:history.state.${day.state}`)}`} />
))}
Expand Down Expand Up @@ -2243,6 +2256,24 @@ const ChoreWidget = ({ refreshNonce = 0, hiddenControls = [] }) => {
})}
</FormGroup>
)}
<Divider sx={{ my: 1.5 }} />
<Typography variant="subtitle2" sx={{ mb: 0.5 }}>
{t('chores:history.weekHeading')}
</Typography>
<FormControlLabel
control={
<Switch
size="small"
checked={showMissedMarks}
disabled={!showWeek}
onChange={(event) => setShowMissedMarks(event.target.checked)}
/>
}
label={t('chores:history.showMissedMarks')}
/>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
{t('chores:history.showMissedMarksHelper')}
</Typography>
</Box>
</Popover>

Expand Down
3 changes: 3 additions & 0 deletions client/src/i18n/locales/en/chores.json
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,9 @@
"menuItem": "History",
"showWeek": "Show the last 7 days",
"hideWeek": "Hide the last 7 days",
"weekHeading": "Last 7 days",
"showMissedMarks": "Show missed days",
"showMissedMarksHelper": "When off, a missed day looks like any other day with nothing logged. The record in History is unchanged.",
"weekAria": "Last 7 days of {{title}}",
"lastDays": "Last {{count}} days",
"streak_one": "{{count}} day in a row",
Expand Down
3 changes: 3 additions & 0 deletions client/src/i18n/locales/es/chores.json
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,9 @@
"menuItem": "Historial",
"showWeek": "Mostrar los últimos 7 días",
"hideWeek": "Ocultar los últimos 7 días",
"weekHeading": "Últimos 7 días",
"showMissedMarks": "Mostrar los días no hechos",
"showMissedMarksHelper": "Cuando está apagado, un día no hecho se ve como cualquier otro día sin registro. El historial no cambia.",
"weekAria": "Últimos 7 días de {{title}}",
"lastDays": "Últimos {{count}} días",
"streak_one": "{{count}} día seguido",
Expand Down
11 changes: 11 additions & 0 deletions client/src/utils/choreDays.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,17 @@ export function choreDays({ rows, schedule, userId, today, count = 7, vacation =
return days;
}

/**
* The 7-day row's display states. With missed marks hidden, a missed day
* draws as 'open' (due, nothing logged) instead of the red cross — no
* negative mark for a day that didn't get done. The underlying days are
* unchanged, so the History dialog and the streak still see the miss.
*/
export function displayDays(days, { showMissed = true } = {}) {
if (showMissed) return days;
return days.map((day) => (day.state === 'missed' ? { ...day, state: 'open' } : day));
}

/**
* Consecutive due days done, counting back from today. Today counts once done
* and never breaks the run while still open; days off and vacation days are
Expand Down
23 changes: 22 additions & 1 deletion client/src/utils/choreDays.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { addDays, cronFiresOn, missesTracked, dueOn, choreDays, streakOf, summarize, rowsFor } from './choreDays.js';
import { addDays, cronFiresOn, missesTracked, dueOn, choreDays, displayDays, streakOf, summarize, rowsFor } from './choreDays.js';

// 2026-10-20 is a Tuesday.
const TODAY = '2026-10-20';
Expand Down Expand Up @@ -120,6 +120,27 @@ describe('choreDays', () => {
});
});

describe('displayDays', () => {
const d = (...s) => s.map((state, i) => ({ date: `2026-10-1${i}`, state }));
it('hides missed days as open when missed marks are off', () => {
expect(displayDays(d('done', 'missed', 'open', 'missed'), { showMissed: false }).map((day) => day.state))
.toEqual(['done', 'open', 'open', 'open']);
});
it('keeps missed days when missed marks are on', () => {
expect(displayDays(d('done', 'missed'), { showMissed: true }).map((day) => day.state))
.toEqual(['done', 'missed']);
});
it('defaults to showing missed days and leaves other states alone', () => {
expect(displayDays(d('missed', 'today', 'vacation', 'off')).map((day) => day.state))
.toEqual(['missed', 'today', 'vacation', 'off']);
});
it('does not mutate the input days', () => {
const days = d('missed');
displayDays(days, { showMissed: false });
expect(days[0].state).toBe('missed');
});
});

describe('streakOf', () => {
const d = (...s) => s.map((state) => ({ state }));
it('skips days off and an open today', () => {
Expand Down
Loading