From 8ede920e5d32a371d3929d6eeef1f358f1a80186 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Thu, 26 Mar 2026 01:28:32 +0300 Subject: [PATCH] feat: monthly view for staff schedule + week/month toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add month view to SchedulePage — toggle between Неделя/Месяц, navigate by month, compact 31-column grid with sticky employee names, weekend highlighting, shift start time or В badge in each cell. --- src/pages/SchedulePage.tsx | 220 +++++++++++++++++++++++++++++++++---- 1 file changed, 196 insertions(+), 24 deletions(-) diff --git a/src/pages/SchedulePage.tsx b/src/pages/SchedulePage.tsx index 068f4e9..69ca462 100644 --- a/src/pages/SchedulePage.tsx +++ b/src/pages/SchedulePage.tsx @@ -4,7 +4,10 @@ import { api, type ScheduleEntry } from '../lib/api' import { useAuth } from '../contexts/AuthContext' import type { User } from '../types' import { cn } from '../lib/utils' -import { format, startOfWeek, addDays, addWeeks, subWeeks, isToday, parseISO } from 'date-fns' +import { + format, startOfWeek, addDays, addWeeks, subWeeks, isToday, parseISO, + startOfMonth, endOfMonth, eachDayOfInterval, addMonths, subMonths, getDay, +} from 'date-fns' import { ru } from 'date-fns/locale' const ROLE_COLORS: Record = { @@ -48,6 +51,12 @@ function fmtTime(t: string | null | undefined) { return t.slice(0, 5) } +/** Returns true if the date is Saturday (6) or Sunday (0) */ +function isWeekend(d: Date): boolean { + const dow = getDay(d) + return dow === 0 || dow === 6 +} + interface CellEditorProps { userId: string date: string @@ -168,18 +177,27 @@ export function SchedulePage() { const slug = user?.hotelSlug ?? '' const isManager = ['hotel_admin', 'manager', 'super_admin'].includes(user?.role ?? '') + const [viewMode, setViewMode] = useState<'week' | 'month'>('week') + const [weekStart, setWeekStart] = useState(() => startOfWeek(new Date(), { weekStartsOn: 1 }) ) + const [monthStart, setMonthStart] = useState(() => + startOfMonth(new Date()) + ) + const [users, setUsers] = useState([]) const [schedule, setSchedule] = useState([]) const [loading, setLoading] = useState(true) const [roleFilter, setRoleFilter] = useState('all') const [editing, setEditing] = useState<{ userId: string; date: string } | null>(null) + // Compute date range depending on view mode const weekDays = getWeekDays(weekStart) - const from = fmtDate(weekDays[0]) - const to = fmtDate(weekDays[6]) + const monthDays = eachDayOfInterval({ start: monthStart, end: endOfMonth(monthStart) }) + + const from = viewMode === 'week' ? fmtDate(weekDays[0]) : fmtDate(monthStart) + const to = viewMode === 'week' ? fmtDate(weekDays[6]) : fmtDate(endOfMonth(monthStart)) const loadData = useCallback(async () => { if (!slug) return @@ -232,12 +250,16 @@ export function SchedulePage() { roleFilter === 'all' || u.role === roleFilter ) - // Count scheduled shifts this week per user - const getWeekCount = (userId: string) => + // Count scheduled shifts per user in current range + const getShiftCount = (userId: string) => schedule.filter(s => s.userId === userId && !s.isDayOff).length const allRoles = [...new Set(users.map(u => u.role))].filter(Boolean).sort() + // Navigation handlers + const goTodayWeek = () => setWeekStart(startOfWeek(new Date(), { weekStartsOn: 1 })) + const goTodayMonth = () => setMonthStart(startOfMonth(new Date())) + return (
{/* Header */} @@ -247,23 +269,65 @@ export function SchedulePage() {

{users.length} сотрудников

- {/* Week navigation */} -
- -
- {format(weekDays[0], 'd MMM', { locale: ru })} — {format(weekDays[6], 'd MMM yyyy', { locale: ru })} +
+ {/* View mode toggle */} +
+ +
- - + + {/* Navigation */} + {viewMode === 'week' ? ( + <> + +
+ {format(weekDays[0], 'd MMM', { locale: ru })} — {format(weekDays[6], 'd MMM yyyy', { locale: ru })} +
+ + + + ) : ( + <> + +
+ {format(monthStart, 'LLLL yyyy', { locale: ru })} +
+ + + + )}
@@ -305,7 +369,8 @@ export function SchedulePage() { ? 'Нет сотрудников. Добавьте их на странице «Сотрудники».' : 'Нет сотрудников с выбранной ролью.'}
- ) : ( + ) : viewMode === 'week' ? ( + /* ── WEEK VIEW ── */
@@ -392,9 +457,116 @@ export function SchedulePage() { ) })} - {/* Week shift count */} + {/* Shift count */} + + ))} + +
- {getWeekCount(u.id)} + {getShiftCount(u.id)} +
+
+ ) : ( + /* ── MONTH VIEW ── */ +
+ + + + {/* Employee column */} + + {monthDays.map(day => ( + + ))} + {/* Shift count column */} + + + + + {filteredUsers.map((u, idx) => ( + + {/* Employee info — sticky */} + + + {/* Day cells — compact */} + {monthDays.map(day => { + const dateStr = fmtDate(day) + const entry = getEntry(u.id, dateStr) + const weekend = isWeekend(day) + return ( + + ) + })} + + {/* Monthly shift count */} + ))}
+ Сотрудник + +
{format(day, 'EEE', { locale: ru }).slice(0, 2)}
+
+ {format(day, 'd')} +
+
+ Смен +
+
+ {u.name} +
+ + {ROLE_LABELS[u.role] ?? u.role} + +
+ + + {getShiftCount(u.id)}