import { useEffect, useMemo, useState } from "react"; import { FaChevronLeft, FaChevronRight } from "react-icons/fa6"; import { getWeekSchedule } from "@api/scheduleApi"; import { PageHeader } from "@components/PageHeader"; import type { BackendPreference } from "@/types/backend"; import type { Schedule } from "@/types/schedule"; import styles from "./SchedulePage.module.scss"; import { useCompaniesStore } from "../store/companiesStore"; type SchedulePageProps = { backendPreference: BackendPreference; }; const businessTimeZone = "America/New_York"; const weekdayLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const companyColorPalette = [ "#2563eb", "#dc2626", "#16a34a", "#9333ea", "#ea580c", "#0891b2", "#be123c", "#4f46e5", "#65a30d", "#c026d3", "#0e7490", "#9d174d", ]; function getCompanyColor(company: string | undefined): string { if (!company) { return "#64748b"; } const hash = [...company].reduce((total, char) => total + char.charCodeAt(0), 0); return companyColorPalette[hash % companyColorPalette.length]; } function normalizeBackendTimestamp(value: string): string { const hasTimezone = /[Zz]$/.test(value) || /[+-]\d{2}:\d{2}$/.test(value); const baseValue = hasTimezone ? value : `${value}Z`; return baseValue.replace(/(\.\d{3})\d+/, "$1"); } function parseBackendDateAsUTC(value: string): Date { return new Date(normalizeBackendTimestamp(value)); } function getBusinessDateKey(date: Date): string { return date.toLocaleDateString("en-CA", { timeZone: businessTimeZone }); } function getBusinessWeekdayIndex(date: Date): number { const weekday = new Intl.DateTimeFormat("en-US", { timeZone: businessTimeZone, weekday: "short", }).format(date); const mapping: Record = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, }; return mapping[weekday] ?? 0; } function addDaysToDateKey(dateKey: string, days: number): string { const [year, month, day] = dateKey.split("-").map(Number); const date = new Date(Date.UTC(year, month - 1, day)); date.setUTCDate(date.getUTCDate() + days); return date.toISOString().slice(0, 10); } function getBusinessMonthStartKey(date: Date): string { const businessKey = getBusinessDateKey(date); return `${businessKey.slice(0, 7)}-01`; } function getBusinessWeekdayIndexForKey(dateKey: string): number { const [year, month, day] = dateKey.split("-").map(Number); const date = new Date(Date.UTC(year, month - 1, day, 12, 0, 0)); return getBusinessWeekdayIndex(date); } function getCalendarGridStartKey(monthStartKey: string): string { const weekday = getBusinessWeekdayIndexForKey(monthStartKey); return addDaysToDateKey(monthStartKey, -weekday); } function formatMonthLabel(monthStartKey: string): string { const [year, month] = monthStartKey.split("-").map(Number); const monthNames = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; return `${monthNames[month - 1]} ${year}`; } function formatTime(value: string | null): string { if (!value) { return "Not registered"; } return parseBackendDateAsUTC(value).toLocaleTimeString("en-US", { timeZone: businessTimeZone, hour: "2-digit", minute: "2-digit", }); } export function SchedulePage({ backendPreference }: SchedulePageProps) { const [schedules, setSchedules] = useState([]); const [error, setError] = useState(""); const [isLoading, setIsLoading] = useState(true); const [selectedCompany, setSelectedCompany] = useState(""); const [selectedMonthStartKey, setSelectedMonthStartKey] = useState(() => getBusinessMonthStartKey(new Date()), ); const companyNames = useCompaniesStore((state) => state.companyNames); const companiesError = useCompaniesStore((state) => state.error); const loadCompanies = useCompaniesStore((state) => state.loadCompanies); const todayKey = useMemo(() => getBusinessDateKey(new Date()), []); const calendarStartKey = useMemo( () => getCalendarGridStartKey(selectedMonthStartKey), [selectedMonthStartKey], ); const calendarDays = useMemo(() => { return Array.from({ length: 42 }, (_, index) => { const dateKey = addDaysToDateKey(calendarStartKey, index); const isCurrentMonth = dateKey.slice(0, 7) === selectedMonthStartKey.slice(0, 7); const isToday = dateKey === todayKey; const dayNumber = Number(dateKey.slice(8, 10)); return { dateKey, isCurrentMonth, isToday, dayNumber }; }); }, [calendarStartKey, selectedMonthStartKey, todayKey]); const schedulesByDateKey = useMemo(() => { return schedules.reduce>((accumulator, schedule) => { if (selectedCompany && schedule.company !== selectedCompany) { return accumulator; } if (!schedule.enter_time) { return accumulator; } const enterDate = parseBackendDateAsUTC(schedule.enter_time); if (Number.isNaN(enterDate.getTime())) { return accumulator; } const businessDateKey = getBusinessDateKey(enterDate); accumulator[businessDateKey] = [...(accumulator[businessDateKey] ?? []), schedule]; return accumulator; }, {}); }, [schedules, selectedCompany]); function showPreviousMonth() { setSelectedMonthStartKey((currentKey) => { const [y, m] = currentKey.split("-").map(Number); const d = new Date(Date.UTC(y, m - 1, 1)); d.setUTCMonth(d.getUTCMonth() - 1); return d.toISOString().slice(0, 10); }); } function showNextMonth() { setSelectedMonthStartKey((currentKey) => { const [y, m] = currentKey.split("-").map(Number); const d = new Date(Date.UTC(y, m - 1, 1)); d.setUTCMonth(d.getUTCMonth() + 1); return d.toISOString().slice(0, 10); }); } function showToday() { setSelectedMonthStartKey(getBusinessMonthStartKey(new Date())); } useEffect(() => { let isMounted = true; async function loadSchedule() { setError(""); setIsLoading(true); try { const scheduleResponse = await getWeekSchedule(backendPreference); if (isMounted) { setSchedules(scheduleResponse.schedules); } } catch (loadError) { if (isMounted) { setError(loadError instanceof Error ? loadError.message : "Unable to load schedule"); } } finally { if (isMounted) { setIsLoading(false); } } } void loadSchedule(); return () => { isMounted = false; }; }, [backendPreference]); useEffect(() => { void loadCompanies(backendPreference); }, [backendPreference, loadCompanies]); useEffect(() => { if (selectedCompany && !companyNames.includes(selectedCompany)) { setSelectedCompany(""); } }, [companyNames, selectedCompany]); return (
{error ?

{error}

: null} {companiesError ?

{companiesError}

: null} {isLoading ?

Loading schedule...

: null}

{formatMonthLabel(selectedMonthStartKey)}

{calendarDays.map(({ dateKey, isCurrentMonth, isToday, dayNumber }) => { const daySchedules = schedulesByDateKey[dateKey] ?? []; return (
{dayNumber}
{daySchedules.length > 0 ? ( daySchedules.map((schedule) => (
{schedule.user ? `${schedule.user.name} ${schedule.user.last_name}` : "Unknown user"} {schedule.company ?? "No company"} ยท {formatTime(schedule.enter_time)} - {formatTime(schedule.out_time)}
)) ) : ( )}
); })}
); }