Add first day of week setting and fix calendar header alignment
- Add first_day_of_week column to settings (0=Sunday, 1=Monday) - Add Calendar section in Settings with toggle button - Pass firstDay to FullCalendar from settings - Align calendar toolbar and sidebar header to h-16 (matches UMBRA header) - Remove border/padding wrapper from calendar grid for full-width layout Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5701e067dd
commit
c62f8bc2a2
23
backend/alembic/versions/008_add_first_day_of_week.py
Normal file
23
backend/alembic/versions/008_add_first_day_of_week.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"""Add first_day_of_week to settings
|
||||||
|
|
||||||
|
Revision ID: 008
|
||||||
|
Revises: 007
|
||||||
|
Create Date: 2026-02-22
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers
|
||||||
|
revision: str = '008'
|
||||||
|
down_revision: Union[str, None] = '007'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('settings', sa.Column('first_day_of_week', sa.Integer(), server_default='0', nullable=False))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('settings', 'first_day_of_week')
|
||||||
@ -15,5 +15,6 @@ class Settings(Base):
|
|||||||
weather_city: Mapped[str | None] = mapped_column(String(100), nullable=True, default=None)
|
weather_city: Mapped[str | None] = mapped_column(String(100), nullable=True, default=None)
|
||||||
weather_lat: Mapped[float | None] = mapped_column(Float, nullable=True, default=None)
|
weather_lat: Mapped[float | None] = mapped_column(Float, nullable=True, default=None)
|
||||||
weather_lon: Mapped[float | None] = mapped_column(Float, nullable=True, default=None)
|
weather_lon: Mapped[float | None] = mapped_column(Float, nullable=True, default=None)
|
||||||
|
first_day_of_week: Mapped[int] = mapped_column(Integer, default=0) # 0=Sunday, 1=Monday
|
||||||
created_at: Mapped[datetime] = mapped_column(default=func.now())
|
created_at: Mapped[datetime] = mapped_column(default=func.now())
|
||||||
updated_at: Mapped[datetime] = mapped_column(default=func.now(), onupdate=func.now())
|
updated_at: Mapped[datetime] = mapped_column(default=func.now(), onupdate=func.now())
|
||||||
|
|||||||
@ -29,6 +29,14 @@ class SettingsUpdate(BaseModel):
|
|||||||
weather_city: str | None = None
|
weather_city: str | None = None
|
||||||
weather_lat: float | None = None
|
weather_lat: float | None = None
|
||||||
weather_lon: float | None = None
|
weather_lon: float | None = None
|
||||||
|
first_day_of_week: int | None = None
|
||||||
|
|
||||||
|
@field_validator('first_day_of_week')
|
||||||
|
@classmethod
|
||||||
|
def validate_first_day(cls, v: int | None) -> int | None:
|
||||||
|
if v is not None and v not in (0, 1):
|
||||||
|
raise ValueError('first_day_of_week must be 0 (Sunday) or 1 (Monday)')
|
||||||
|
return v
|
||||||
|
|
||||||
@field_validator('weather_lat')
|
@field_validator('weather_lat')
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -53,6 +61,7 @@ class SettingsResponse(BaseModel):
|
|||||||
weather_city: str | None = None
|
weather_city: str | None = None
|
||||||
weather_lat: float | None = None
|
weather_lat: float | None = None
|
||||||
weather_lon: float | None = None
|
weather_lon: float | None = None
|
||||||
|
first_day_of_week: int = 0
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|||||||
import api, { getErrorMessage } from '@/lib/api';
|
import api, { getErrorMessage } from '@/lib/api';
|
||||||
import type { CalendarEvent } from '@/types';
|
import type { CalendarEvent } from '@/types';
|
||||||
import { useCalendars } from '@/hooks/useCalendars';
|
import { useCalendars } from '@/hooks/useCalendars';
|
||||||
|
import { useSettings } from '@/hooks/useSettings';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -48,6 +49,7 @@ export default function CalendarPage() {
|
|||||||
const [scopeEvent, setScopeEvent] = useState<CalendarEvent | null>(null);
|
const [scopeEvent, setScopeEvent] = useState<CalendarEvent | null>(null);
|
||||||
const [activeEditScope, setActiveEditScope] = useState<'this' | 'this_and_future' | null>(null);
|
const [activeEditScope, setActiveEditScope] = useState<'this' | 'this_and_future' | null>(null);
|
||||||
|
|
||||||
|
const { settings } = useSettings();
|
||||||
const { data: calendars = [] } = useCalendars();
|
const { data: calendars = [] } = useCalendars();
|
||||||
|
|
||||||
const { data: events = [] } = useQuery({
|
const { data: events = [] } = useQuery({
|
||||||
@ -269,8 +271,8 @@ export default function CalendarPage() {
|
|||||||
<CalendarSidebar />
|
<CalendarSidebar />
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Custom toolbar */}
|
{/* Custom toolbar — h-16 matches sidebar header */}
|
||||||
<div className="border-b bg-card px-6 py-3 flex items-center gap-4">
|
<div className="border-b bg-card px-6 h-16 flex items-center gap-4 shrink-0">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={navigatePrev}>
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={navigatePrev}>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
@ -305,13 +307,14 @@ export default function CalendarPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Calendar grid */}
|
{/* Calendar grid */}
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="bg-card rounded-lg border p-3 h-full">
|
<div className="h-full">
|
||||||
<FullCalendar
|
<FullCalendar
|
||||||
ref={calendarRef}
|
ref={calendarRef}
|
||||||
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||||
initialView="dayGridMonth"
|
initialView="dayGridMonth"
|
||||||
headerToolbar={false}
|
headerToolbar={false}
|
||||||
|
firstDay={settings?.first_day_of_week ?? 0}
|
||||||
events={calendarEvents}
|
events={calendarEvents}
|
||||||
editable={true}
|
editable={true}
|
||||||
selectable={true}
|
selectable={true}
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export default function CalendarSidebar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-56 shrink-0 border-r bg-card flex flex-col">
|
<div className="w-56 shrink-0 border-r bg-card flex flex-col">
|
||||||
<div className="p-4 border-b flex items-center justify-between">
|
<div className="h-16 px-4 border-b flex items-center justify-between shrink-0">
|
||||||
<span className="text-sm font-semibold font-heading text-foreground">Calendars</span>
|
<span className="text-sm font-semibold font-heading text-foreground">Calendars</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@ -32,6 +32,8 @@ export default function SettingsPage() {
|
|||||||
const [showDropdown, setShowDropdown] = useState(false);
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
const searchRef = useRef<HTMLDivElement>(null);
|
const searchRef = useRef<HTMLDivElement>(null);
|
||||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const [firstDayOfWeek, setFirstDayOfWeek] = useState(settings?.first_day_of_week ?? 0);
|
||||||
|
|
||||||
const [pinForm, setPinForm] = useState({
|
const [pinForm, setPinForm] = useState({
|
||||||
oldPin: '',
|
oldPin: '',
|
||||||
newPin: '',
|
newPin: '',
|
||||||
@ -136,6 +138,17 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFirstDayChange = async (value: number) => {
|
||||||
|
setFirstDayOfWeek(value);
|
||||||
|
try {
|
||||||
|
await updateSettings({ first_day_of_week: value });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
|
||||||
|
toast.success(value === 0 ? 'Week starts on Sunday' : 'Week starts on Monday');
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to update first day of week');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpcomingDaysSubmit = async (e: FormEvent) => {
|
const handleUpcomingDaysSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
@ -235,6 +248,55 @@ export default function SettingsPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Calendar</CardTitle>
|
||||||
|
<CardDescription>Configure your calendar preferences</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>First Day of Week</Label>
|
||||||
|
<div className="flex items-center rounded-md border border-border overflow-hidden w-fit">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleFirstDayChange(0)}
|
||||||
|
className={cn(
|
||||||
|
'px-4 py-2 text-sm font-medium transition-colors duration-150',
|
||||||
|
firstDayOfWeek === 0
|
||||||
|
? 'text-accent'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-card-elevated'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: firstDayOfWeek === 0 ? 'hsl(var(--accent-color) / 0.15)' : undefined,
|
||||||
|
color: firstDayOfWeek === 0 ? 'hsl(var(--accent-color))' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sunday
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleFirstDayChange(1)}
|
||||||
|
className={cn(
|
||||||
|
'px-4 py-2 text-sm font-medium transition-colors duration-150',
|
||||||
|
firstDayOfWeek === 1
|
||||||
|
? 'text-accent'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-card-elevated'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: firstDayOfWeek === 1 ? 'hsl(var(--accent-color) / 0.15)' : undefined,
|
||||||
|
color: firstDayOfWeek === 1 ? 'hsl(var(--accent-color))' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Monday
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Sets which day the calendar week starts on
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Dashboard</CardTitle>
|
<CardTitle>Dashboard</CardTitle>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export interface Settings {
|
|||||||
weather_city?: string | null;
|
weather_city?: string | null;
|
||||||
weather_lat?: number | null;
|
weather_lat?: number | null;
|
||||||
weather_lon?: number | null;
|
weather_lon?: number | null;
|
||||||
|
first_day_of_week: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user