import { useCallback, useEffect, useMemo, useState } from "react"; import type { FormEvent } from "react"; import { getCompanies } from "@api/companiesApi"; import { clockIn, clockOut, getWeekSchedule } from "@api/scheduleApi"; import { createUser, getUsers, updateUserPreference } from "@api/usersApi"; import { Modal } from "@components/Modal"; import { PageHeader } from "@components/PageHeader"; import type { BackendPreference } from "@/types/backend"; import type { Company } from "@/types/companies"; import type { Schedule } from "@/types/schedule"; import type { Employee, WorkTime } from "@/types/users"; import styles from "./UsersPage.module.scss"; type UsersPageProps = { backendPreference: BackendPreference; }; type UserStatus = "off-duty" | "working" | "worked"; const workTimeOptions: WorkTime[] = ["morning", "afternoon", "evening"]; const statusOptions: UserStatus[] = ["off-duty", "working", "worked"]; const usersPerPage = 10; function isSameLocalDate(firstDate: Date, secondDate: Date): boolean { return ( firstDate.getFullYear() === secondDate.getFullYear() && firstDate.getMonth() === secondDate.getMonth() && firstDate.getDate() === secondDate.getDate() ); } function getWeekStart(date: Date): Date { const weekStart = new Date(date); weekStart.setHours(0, 0, 0, 0); weekStart.setDate(weekStart.getDate() - weekStart.getDay()); return weekStart; } function addDays(date: Date, days: number): Date { const nextDate = new Date(date); nextDate.setDate(nextDate.getDate() + days); return nextDate; } function isScheduleInWeek(schedule: Schedule, weekStart: Date, weekEndExclusive: Date): boolean { if (!schedule.enter_time) { return false; } const enterDate = new Date(schedule.enter_time); return ( !Number.isNaN(enterDate.getTime()) && enterDate >= weekStart && enterDate < weekEndExclusive ); } function getTodaySchedules(schedules: Schedule[], today: Date): Schedule[] { return schedules.filter((schedule) => { if (!schedule.enter_time) { return false; } const enterDate = new Date(schedule.enter_time); return !Number.isNaN(enterDate.getTime()) && isSameLocalDate(enterDate, today); }); } function getLatestSchedule(schedules: Schedule[]): Schedule | undefined { return [...schedules].sort((firstSchedule, secondSchedule) => { const firstTime = firstSchedule.enter_time ? new Date(firstSchedule.enter_time).getTime() : 0; const secondTime = secondSchedule.enter_time ? new Date(secondSchedule.enter_time).getTime() : 0; return firstTime - secondTime; }).at(-1); } function getUserStatus(schedules: Schedule[], today: Date): UserStatus { const todaySchedules = getTodaySchedules(schedules, today); const latestSchedule = getLatestSchedule(todaySchedules); if (!latestSchedule?.enter_time) { return "off-duty"; } if (latestSchedule.out_time) { return "worked"; } return "working"; } function getStatusLabel(status: UserStatus): string { if (status === "working") { return "Working"; } if (status === "worked") { return "Worked"; } return "Off duty"; } function formatDateTime(value: string | null): string { if (!value) { return "--"; } return new Date(value).toLocaleString(); } function getNameScheduleKey(name: string, lastName: string): string { return `name:${name.trim().toLowerCase()}:${lastName.trim().toLowerCase()}`; } function getEmployeeScheduleKeys(user: Employee): string[] { return [`id:${user.id}`, getNameScheduleKey(user.name, user.last_name)]; } function getScheduleUserKey(schedule: Schedule): string | null { if (typeof schedule.user_id === "number") { return `id:${schedule.user_id}`; } if (schedule.user) { return getNameScheduleKey(schedule.user.name, schedule.user.last_name); } return null; } function getSchedulesForUser(user: Employee, schedulesByUser: Record): Schedule[] { return getEmployeeScheduleKeys(user).flatMap((key) => schedulesByUser[key] ?? []); } export function UsersPage({ backendPreference }: UsersPageProps) { const [companies, setCompanies] = useState([]); const [schedules, setSchedules] = useState([]); const [selectedUserId, setSelectedUserId] = useState(null); const [users, setUsers] = useState([]); const [companyId, setCompanyId] = useState(""); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); const [preference, setPreference] = useState("morning"); const [error, setError] = useState(""); const [isAddUserOpen, setIsAddUserOpen] = useState(false); const [isPreferenceOpen, setIsPreferenceOpen] = useState(false); const [checkingInUser, setCheckingInUser] = useState(null); const [checkInDateTime, setCheckInDateTime] = useState(""); const [savingActionUserId, setSavingActionUserId] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [selectedCompanyId, setSelectedCompanyId] = useState(""); const [selectedStatus, setSelectedStatus] = useState(""); const selectedUser = users.find((user) => user.id === selectedUserId) ?? null; const today = new Date(); const currentWeekStart = getWeekStart(today); const currentWeekEndExclusive = addDays(currentWeekStart, 7); const schedulesByUser = useMemo(() => { return schedules.reduce>((accumulator, schedule) => { const userKey = getScheduleUserKey(schedule); if (!userKey) { return accumulator; } accumulator[userKey] = [...(accumulator[userKey] ?? []), schedule]; return accumulator; }, {}); }, [schedules]); const filteredUsers = users.filter((user) => { if (selectedCompanyId && user.company_id !== Number(selectedCompanyId)) { return false; } if (selectedStatus && getUserStatus(getSchedulesForUser(user, schedulesByUser), today) !== selectedStatus) { return false; } return true; }); const totalPages = Math.max(1, Math.ceil(filteredUsers.length / usersPerPage)); const pageStartIndex = (currentPage - 1) * usersPerPage; const paginatedUsers = filteredUsers.slice(pageStartIndex, pageStartIndex + usersPerPage); const pageEndIndex = pageStartIndex + paginatedUsers.length; const loadUsersPage = useCallback(async () => { setError(""); setIsLoading(true); try { const [companiesResponse, usersResponse, scheduleResponse] = await Promise.all([ getCompanies(backendPreference), getUsers(backendPreference), getWeekSchedule(backendPreference), ]); setCompanies(companiesResponse); setUsers(usersResponse); setSchedules(scheduleResponse.schedules); } catch (loadError) { setError(loadError instanceof Error ? loadError.message : "Unable to load users"); } finally { setIsLoading(false); } }, [backendPreference]); useEffect(() => { void loadUsersPage(); }, [loadUsersPage]); useEffect(() => { if (currentPage > totalPages) { setCurrentPage(totalPages); } }, [currentPage, totalPages]); useEffect(() => { setCurrentPage(1); }, [selectedCompanyId, selectedStatus]); function resetUserForm() { setCompanyId(""); setFirstName(""); setLastName(""); setPreference("morning"); } async function handleCreateUser(event: FormEvent) { event.preventDefault(); setError(""); setIsSaving(true); try { await createUser(backendPreference, { company_id: Number(companyId), name: firstName, last_name: lastName, preference, }); resetUserForm(); setIsAddUserOpen(false); await loadUsersPage(); } catch (saveError) { setError(saveError instanceof Error ? saveError.message : "Unable to add user"); } finally { setIsSaving(false); } } async function handleUpdatePreference(event: FormEvent) { event.preventDefault(); if (!selectedUser) { return; } setError(""); setIsSaving(true); try { await updateUserPreference(backendPreference, selectedUser.id, { preference }); setIsPreferenceOpen(false); await loadUsersPage(); } catch (saveError) { setError(saveError instanceof Error ? saveError.message : "Unable to update preference"); } finally { setIsSaving(false); } } function getDayOfWeekFromDateTime(value: string): import("@/types/users").DayOfWeek { const days: import("@/types/users").DayOfWeek[] = [ "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", ]; return days[new Date(value).getDay()]; } function getShiftFromDateTime(value: string): WorkTime { const hour = new Date(value).getHours(); if (hour < 12) { return "morning"; } if (hour < 19) { return "evening"; } return "afternoon"; } function openCheckInModal(user: Employee) { const now = new Date(); const local = new Date(now.getTime() - now.getTimezoneOffset() * 60000) .toISOString() .slice(0, 16); setCheckingInUser(user); setCheckInDateTime(local); } async function handleClockIn(event: FormEvent) { event.preventDefault(); if (!checkingInUser || !checkInDateTime) { return; } setError(""); setSavingActionUserId(checkingInUser.id); try { await clockIn(backendPreference, checkingInUser.id, { day_of_week: getDayOfWeekFromDateTime(checkInDateTime), enter_time: new Date(checkInDateTime).toISOString(), shift: getShiftFromDateTime(checkInDateTime), }); setCheckingInUser(null); await loadUsersPage(); } catch (saveError) { setError(saveError instanceof Error ? saveError.message : "Unable to check in user"); } finally { setSavingActionUserId(null); } } async function handleClockOut(userId: number, activeSchedule: Schedule) { setError(""); setSavingActionUserId(userId); try { await clockOut(backendPreference, userId, { day_of_week: activeSchedule.day_of_week, out_time: new Date().toISOString(), }); await loadUsersPage(); } catch (saveError) { setError(saveError instanceof Error ? saveError.message : "Unable to check out user"); } finally { setSavingActionUserId(null); } } return ( <> {selectedUser ? ( ) : null} } /> {error ?

{error}

: null} {isLoading ?

Loading users...

: null}
{paginatedUsers.map((user) => { const userSchedules = getSchedulesForUser(user, schedulesByUser); const todaySchedules = getTodaySchedules(userSchedules, today); const status = getUserStatus(userSchedules, today); const latestSchedule = getLatestSchedule(todaySchedules); const activeSchedule = latestSchedule?.enter_time && !latestSchedule.out_time ? latestSchedule : undefined; const company = companies.find((item) => item.id === user.company_id); const daysWorked = new Set( userSchedules .filter((schedule) => isScheduleInWeek(schedule, currentWeekStart, currentWeekEndExclusive)) .map((schedule) => new Date(schedule.enter_time as string).getDay()), ).size; return ( setSelectedUserId(user.id)} title="Double click to edit user information" > ); })}
Name Company Preference Status Check in Check out Days worked Actions
{user.name} {user.last_name} {company?.name ?? (user.company_id ? `Company #${user.company_id}` : "Unassigned")} {user.preference} {getStatusLabel(status)} {formatDateTime(latestSchedule?.enter_time ?? null)} {formatDateTime(latestSchedule?.out_time ?? null)} {daysWorked}
Showing {filteredUsers.length === 0 ? 0 : pageStartIndex + 1}-{pageEndIndex} of{" "} {filteredUsers.length} users
Page {currentPage} of {totalPages}
{isAddUserOpen ? ( { setIsAddUserOpen(false); resetUserForm(); }} >
) : null} {isPreferenceOpen && selectedUser ? ( setIsPreferenceOpen(false)}>
) : null} {checkingInUser ? ( setCheckingInUser(null)} >

Day of week will be derived as{" "} {checkInDateTime ? getDayOfWeekFromDateTime(checkInDateTime) : "--"} from the selected date.

) : null} ); }