feat(settings): first day of week (plan Tier 3)

- New per-user 'week_start' setting (0=Sun default / 1=Mon), registered in
  USER_SETTING_KEYS + USER_SETTING_DEFAULTS.
- CalendarPage reads it and offsets the month grid: leading-blank count
  (getDay()-weekStart+7)%7 and a rotated weekday header. Purely presentational.
- Settings: Sun/Mon Select in the new "Regional" section. Also aligns the
  grace_period_days client default (3→5) with the server seed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-05 15:57:35 -05:00
parent 0fa80a77ef
commit 0f24016bcf
3 changed files with 29 additions and 4 deletions

View File

@ -18,6 +18,7 @@ import { api } from '@/api';
import { cn, fmtDate, todayStr, errMessage } from '@/lib/utils';
import { formatUSD, asDollars } from '@/lib/money';
import { isPaidStatus } from '@/lib/trackerUtils';
import { useSettings } from '@/hooks/useQueries';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
@ -395,13 +396,17 @@ function DayIndicators({ day, moneyMarker }: { day: CalDay; moneyMarker: MoneyMa
);
}
function CalendarGrid({ data, selectedDate, onSelectDay, moneyMarkers }: {
function CalendarGrid({ data, selectedDate, onSelectDay, moneyMarkers, weekStart = 0 }: {
data: CalendarData;
selectedDate?: string;
onSelectDay: (day: CalDay) => void;
moneyMarkers?: Record<string, MoneyMarker>;
weekStart?: number;
}) {
const firstWeekday = new Date(data.year, data.month - 1, 1).getDay();
// Offset the leading blanks + rotate the header so the week can start on
// Sunday (0) or Monday (1) per the user's setting.
const firstWeekday = (new Date(data.year, data.month - 1, 1).getDay() - weekStart + 7) % 7;
const orderedWeekdays = [...WEEKDAYS.slice(weekStart), ...WEEKDAYS.slice(0, weekStart)];
const cells: ({ type: 'blank'; key: string } | { type: 'day'; key: string; day: CalDay })[] = [
...Array.from({ length: firstWeekday }, (_, index) => ({ type: 'blank' as const, key: `blank-${index}` })),
...data.days.map(day => ({ type: 'day' as const, key: day.date, day })),
@ -411,7 +416,7 @@ function CalendarGrid({ data, selectedDate, onSelectDay, moneyMarkers }: {
return (
<Card className="overflow-hidden border-border/80 bg-card/95">
<div className="grid grid-cols-7 border-b border-border/70 bg-muted/45">
{WEEKDAYS.map(day => (
{orderedWeekdays.map(day => (
<div key={day} className="px-1 py-2 text-center text-[11px] font-semibold uppercase tracking-wide text-foreground/70 sm:text-xs">
{day}
</div>
@ -911,6 +916,8 @@ export default function CalendarPage() {
})();
const [year, setYear] = useState(initial.year);
const [month, setMonth] = useState(initial.month);
const { data: settingsData } = useSettings();
const weekStart = Number((settingsData as Record<string, unknown> | undefined)?.week_start) === 1 ? 1 : 0;
const [data, setData] = useState<CalendarData | null>(null);
const [summaryData, setSummaryData] = useState<SummaryDataShape | null>(null);
const [snowballProjection, setSnowballProjection] = useState<SnowballProjection | null>(null);
@ -1063,6 +1070,7 @@ export default function CalendarPage() {
<>
<CalendarGrid
data={data}
weekStart={weekStart}
selectedDate={selectedDay?.date}
moneyMarkers={moneyMarkers}
onSelectDay={day => {

View File

@ -314,6 +314,7 @@ function LinkImportToggle() {
interface SettingsState {
currency: string;
date_format: string;
week_start: string;
grace_period_days: number | string;
drift_threshold_pct: string;
tracker_show_bank_projection_banner: string;
@ -332,7 +333,8 @@ export default function SettingsPage() {
const DEFAULTS: SettingsState = {
currency: 'USD',
date_format: 'MM/DD/YYYY',
grace_period_days: 3,
week_start: '0',
grace_period_days: 5,
drift_threshold_pct: '5',
tracker_show_bank_projection_banner: 'true',
tracker_bank_projection_banner_snoozed_until: '',
@ -363,6 +365,7 @@ export default function SettingsPage() {
const buildPayload = (s: SettingsState) => ({
currency: s.currency,
date_format: s.date_format,
week_start: s.week_start,
grace_period_days: s.grace_period_days,
drift_threshold_pct: s.drift_threshold_pct,
tracker_show_bank_projection_banner: s.tracker_show_bank_projection_banner,
@ -476,6 +479,18 @@ export default function SettingsPage() {
</SelectContent>
</Select>
</SettingRow>
<SettingRow label="First day of week" description="The starting column of the calendar month grid.">
<Select value={settings.week_start} onValueChange={(v) => set('week_start', v)}>
<SelectTrigger className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">Sunday</SelectItem>
<SelectItem value="1">Monday</SelectItem>
</SelectContent>
</Select>
</SettingRow>
</SectionCard>
{/* Tracker Layout */}

View File

@ -5,6 +5,7 @@ const { getDb, getSetting } = require('../db/database');
const USER_SETTING_KEYS = [
'currency',
'date_format',
'week_start',
'grace_period_days',
'notify_days_before',
'drift_threshold_pct',
@ -29,6 +30,7 @@ const USER_SETTING_KEYS = [
];
const USER_SETTING_DEFAULTS = {
week_start: '0',
search_bars_collapsed: 'false',
bank_auto_categorize_merchants: 'true',
tracker_show_bank_projection_banner: 'true',