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:
parent
0fa80a77ef
commit
0f24016bcf
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in New Issue