import { useQuery } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import { listCampusAttendanceConfigs, listCampusAttendanceSummaries, saveCampusAttendanceConfig, saveCampusAttendanceSummary, } from '@/shared/api/campusAttendance'; import { useCampusCatalog } from '@/business/campuses/hooks'; import { CAMPUS_ATTENDANCE_QUERY_KEYS } from '@/shared/constants/campusAttendance'; import { findCampusByNameOrCode } from '@/shared/constants/campusDisplay'; import { UI_FEEDBACK_CLEAR_DELAY_MS } from '@/shared/constants/ui'; import type { CampusAttendanceCampusKey, CampusAttendanceListFilter, } from '@/shared/types/campusAttendance'; import { toCampusAttendanceConfigViewModel, toCampusAttendanceSummaryMutationDto, toCampusAttendanceSummaryViewModel, } from '@/business/campus-attendance/mappers'; import { buildAttendanceEntryInput, buildCampusAttendanceScopeModel, buildCampusAttendanceStats, buildCombinedAttendanceStats, buildOverallAttendanceStats, getToday, getTodayPercentage, getWeeklyAverage, getWeekEnd, getWeekStart, } from '@/business/campus-attendance/selectors'; import { useStaffAttendanceRecords, useStaffAttendanceSummary, } from '@/business/staff-attendance/hooks'; import { saveStaffAttendanceRecord } from '@/shared/api/staffAttendance'; import { CAMPUS_ATTENDANCE_PRINT_POPUP_BLOCKED_MESSAGE, openCampusAttendancePrintReport, } from '@/business/campus-attendance/printReport'; import type { CampusAttendanceChildStats, CampusAttendanceEntryDraft, CampusAttendanceEntryInput, CampusAttendanceRollupDraft, AttendanceRosterStatus, StaffAttendanceEntryDraft, } from '@/business/campus-attendance/types'; import type { CampusId, UserRole } from '@/shared/types/app'; import { getOptionalErrorMessage } from '@/shared/errors/errorMessages'; import { mapApiListRows } from '@/shared/business/apiListRows'; import { useInvalidatingMutation } from '@/shared/business/queryMutations'; import { usePermissions } from '@/shared/app/usePermissions'; import { useScopeContext } from '@/shared/app/scope-context'; import { getScopeChildren } from '@/shared/api/scope'; import type { CampusInfo } from '@/shared/types/app'; import type { TenantChild, TenantLevel } from '@/shared/types/scope'; import { listUsers, type AdminUserRow } from '@/shared/api/users'; import { STAFF_ATTENDANCE_QUERY_KEYS } from '@/shared/constants/staffAttendance'; import { getClass } from '@/shared/api/classes'; import type { StaffAttendanceRecordViewModel } from '@/business/staff-attendance/types'; const EMPTY_CONFIGS: ReturnType[] = []; const EMPTY_SUMMARIES: ReturnType[] = []; const EMPTY_STAFF_RECORDS: readonly StaffAttendanceRecordViewModel[] = []; const EMPTY_CAMPUSES: CampusInfo[] = []; const EMPTY_TENANT_CHILDREN: TenantChild[] = []; const EMPTY_CAMPUS_CHILDREN_BY_PARENT: Readonly> = {}; const SCHOOL_TILE_GRADIENTS = [ 'from-emerald-500 to-green-500', 'from-orange-600 to-amber-500', 'from-rose-500 to-pink-500', 'from-violet-500 to-purple-500', 'from-blue-500 to-cyan-500', ] as const; export function useCampusAttendanceConfigs(campusKey?: CampusAttendanceCampusKey, enabled = true) { return useQuery({ queryKey: [CAMPUS_ATTENDANCE_QUERY_KEYS.configs, campusKey], enabled, queryFn: () => mapApiListRows( listCampusAttendanceConfigs(campusKey ? { campusKey } : undefined), toCampusAttendanceConfigViewModel, ), }); } export function useSaveCampusAttendanceConfig() { return useInvalidatingMutation({ mutationFn: (input: { campusKey: CampusAttendanceCampusKey; attendanceLink: string | null }) => ( saveCampusAttendanceConfig(input.campusKey, { attendance_link: input.attendanceLink }) ), invalidateQueryKey: [CAMPUS_ATTENDANCE_QUERY_KEYS.configs], }); } export function useCampusAttendanceSummaries(filter?: CampusAttendanceListFilter, enabled = true) { return useQuery({ queryKey: [CAMPUS_ATTENDANCE_QUERY_KEYS.summaries, filter], enabled, queryFn: () => mapApiListRows( listCampusAttendanceSummaries(filter), toCampusAttendanceSummaryViewModel, ), }); } export function useSaveCampusAttendanceSummary() { return useInvalidatingMutation({ mutationFn: (input: CampusAttendanceEntryInput) => saveCampusAttendanceSummary( input.campusId, input.date, toCampusAttendanceSummaryMutationDto(input), ), invalidateQueryKey: [CAMPUS_ATTENDANCE_QUERY_KEYS.summaries], }); } export function useAttendanceDetailsChildCampuses( level: TenantLevel | undefined, tenantId: string | undefined, ) { return useQuery({ queryKey: ['attendance-details-child-campuses', level, tenantId], enabled: level === 'school' && Boolean(tenantId), queryFn: () => getScopeChildren('school', tenantId ?? '', { limit: 500 }), }); } export function useSaveStaffAttendanceRecord() { return useInvalidatingMutation({ mutationFn: (input: StaffAttendanceEntryDraft) => saveStaffAttendanceRecord( input.userId, input.date, { status: input.status, note: input.note.trim() || null }, ), invalidateQueryKeys: [ [STAFF_ATTENDANCE_QUERY_KEYS.records], [STAFF_ATTENDANCE_QUERY_KEYS.summary], ], }); } type UseCampusAttendancePageInput = { readonly userRole: UserRole; readonly userCampus: string; readonly userName: string; }; const emptyEntryDraft = (date: string, campusId: CampusId = ''): CampusAttendanceEntryDraft => ({ date, campusId, enrolled: '', present: '', absent: '', tardy: '', notes: '', }); const emptyStaffEntryDraft = (date: string): StaffAttendanceEntryDraft => ({ date, userId: '', status: 'present', note: '', }); type AttendanceStatusMap = Record; type StudentRollupOverrideMap = Record>>; function userDisplayName(user: AdminUserRow): string { const fullName = `${user.firstName ?? ''} ${user.lastName ?? ''}`.trim(); return fullName || user.email; } function reconcileAttendanceStatuses( current: AttendanceStatusMap, userIds: readonly string[], ): AttendanceStatusMap { const next: AttendanceStatusMap = {}; for (const userId of userIds) { next[userId] = current[userId] ?? 'present'; } return next; } function isBlankRollupRow(row: CampusAttendanceRollupDraft): boolean { return !row.enrolled && !row.present && !row.absent && !row.tardy && !row.notes.trim(); } function toRollupEntryInput( row: CampusAttendanceRollupDraft, date: string, ): CampusAttendanceEntryInput | null { return buildAttendanceEntryInput( { date, campusId: row.campusId, enrolled: row.enrolled, present: row.present, absent: row.absent, tardy: row.tardy, notes: row.notes, }, row.campusId, ); } function isOfficeStaffUser(user: AdminUserRow, mode: 'organization' | 'school' | 'campus'): boolean { const role = user.app_role?.name; if (role === 'student' || role === 'guardian') { return false; } if (mode === 'organization') { return !user.schoolId && !user.campusId; } if (mode === 'school') { return Boolean(user.schoolId) && !user.campusId; } return Boolean(user.campusId || user.classId); } function isStaffRosterUser(user: AdminUserRow): boolean { const role = user.app_role?.name; return role !== 'student' && role !== 'guardian'; } function mapAttendanceRosterUser(user: AdminUserRow) { return { id: user.id, name: userDisplayName(user), role: user.app_role?.name ?? null, }; } interface ScopedAttendanceChildren { readonly directChildren: readonly TenantChild[]; readonly campusChildrenByParentId: Readonly>; readonly campusChildren: readonly TenantChild[]; } async function listScopedAttendanceChildren( level: TenantLevel, tenantId: string, ): Promise { if (level === 'campus') { return { directChildren: [], campusChildrenByParentId: {}, campusChildren: [], }; } if (level === 'school') { const response = await getScopeChildren(level, tenantId, { limit: 500 }); const campusChildren = response.rows.filter((child) => child.level === 'campus'); return { directChildren: campusChildren, campusChildrenByParentId: Object.fromEntries(campusChildren.map((campus) => [campus.id, [campus]])), campusChildren, }; } if (level === 'organization') { const schoolsResponse = await getScopeChildren(level, tenantId, { limit: 500 }); const schools = schoolsResponse.rows.filter((child) => child.level === 'school'); const campusGroups = await Promise.all( schools.map((school) => getScopeChildren('school', school.id, { limit: 500 })), ); const campusChildrenByParentId = Object.fromEntries( schools.map((school, index) => [ school.id, campusGroups[index]?.rows.filter((child) => child.level === 'campus') ?? [], ]), ); return { directChildren: schools, campusChildrenByParentId, campusChildren: Object.values(campusChildrenByParentId).flat(), }; } return { directChildren: [], campusChildrenByParentId: {}, campusChildren: [], }; } function percentageFromRecords( records: readonly ReturnType[], ): number | null { const enrolled = records.reduce((sum, record) => sum + record.total_enrolled, 0); const present = records.reduce((sum, record) => sum + record.total_present, 0); return enrolled > 0 ? Number(((present / enrolled) * 100).toFixed(2)) : null; } function percentageFromStaffDailySummaries( records: readonly { readonly total_staff: number; readonly total_present: number }[], ): number | null { const staff = records.reduce((sum, record) => sum + record.total_staff, 0); const present = records.reduce((sum, record) => sum + record.total_present, 0); return staff > 0 ? Number(((present / staff) * 100).toFixed(2)) : null; } export function useCampusAttendancePage({ userRole, userCampus, userName, }: UseCampusAttendancePageInput) { const permissions = usePermissions(); const { tier, effectiveTenant, selectedTenant } = useScopeContext(); const effectiveTier = selectedTenant ? selectedTenant.level : tier; const selectedCampusId = effectiveTier === 'campus' ? effectiveTenant?.id ?? null : null; const selectedClassId = effectiveTier === 'class' ? effectiveTenant?.id ?? null : null; const roleAccess = { isSuperintendent: userRole === 'superintendent', isDirector: userRole === 'director', isOfficeManager: userRole === 'office_manager', canSeeAllCampuses: effectiveTier === 'organization' || effectiveTier === 'school', canEnterData: permissions.has('FILL_ATTENDANCE') && ( effectiveTier === 'organization' || effectiveTier === 'school' || effectiveTier === 'campus' || effectiveTier === 'class' ), canPrint: userRole === 'superintendent' || userRole === 'director' || userRole === 'office_manager' || permissions.has('READ_STAFF_ATTENDANCE_REPORTS'), canReadStaffReports: permissions.has('READ_STAFF_ATTENDANCE_REPORTS'), }; const campusCatalog = useCampusCatalog(); const classQuery = useQuery({ queryKey: ['attendance-class-scope', selectedClassId], enabled: Boolean(selectedClassId), queryFn: () => getClass(selectedClassId ?? ''), }); const classCampusTenantId = classQuery.data?.campusId ?? null; const scopedCampusTenantId = selectedCampusId ?? classCampusTenantId; const campusInfo = scopedCampusTenantId ? campusCatalog.campuses.find((campus) => campus.tenantId === scopedCampusTenantId || campus.id === scopedCampusTenantId) : findCampusByNameOrCode(campusCatalog.campuses, userCampus); const campusId = campusInfo?.id ?? null; const today = getToday(); const weekStart = getWeekStart(new Date()); const weekEnd = getWeekEnd(new Date()); const scopeModel = buildCampusAttendanceScopeModel( effectiveTier, effectiveTenant, campusId, campusInfo?.fullName || userCampus, ); const attendanceCampusId = scopeModel.campusId; const attendanceSummaryFilter = attendanceCampusId ? { campusKey: attendanceCampusId } : undefined; const scopedAttendanceChildrenQuery = useQuery({ queryKey: ['attendance-scoped-children', effectiveTier, effectiveTenant?.id ?? null], enabled: Boolean( effectiveTenant?.id && (effectiveTier === 'organization' || effectiveTier === 'school'), ), queryFn: () => listScopedAttendanceChildren(effectiveTier as TenantLevel, effectiveTenant?.id ?? ''), }); const hasAttendanceScope = scopeModel.mode !== 'campus' || Boolean(attendanceCampusId); const configsQuery = useCampusAttendanceConfigs( attendanceCampusId ?? undefined, hasAttendanceScope, ); const summariesQuery = useCampusAttendanceSummaries( attendanceSummaryFilter, hasAttendanceScope, ); const staffSummaryQuery = useStaffAttendanceSummary( { startDate: today, endDate: today }, roleAccess.canReadStaffReports && hasAttendanceScope, ); const staffRecordsQuery = useStaffAttendanceRecords( { limit: 500 }, roleAccess.canReadStaffReports && hasAttendanceScope, ); const officeStaffQuery = useQuery({ queryKey: ['attendance-office-staff-users', effectiveTier, effectiveTenant?.id ?? null], enabled: roleAccess.canEnterData && hasAttendanceScope, queryFn: ({ signal }) => listUsers({ limit: 500, field: 'name', sort: 'asc' }, { signal }), }); const saveConfigMutation = useSaveCampusAttendanceConfig(); const saveSummaryMutation = useSaveCampusAttendanceSummary(); const saveStaffAttendanceMutation = useSaveStaffAttendanceRecord(); const configs = configsQuery.data ?? EMPTY_CONFIGS; const attendanceData = summariesQuery.data ?? EMPTY_SUMMARIES; const staffSummary = staffSummaryQuery.data ?? null; const staffRecords = staffRecordsQuery.data ?? EMPTY_STAFF_RECORDS; const officeStaffUsers = useMemo(() => ( (officeStaffQuery.data?.rows ?? []) .filter((user) => isOfficeStaffUser(user, scopeModel.mode) && isStaffRosterUser(user)) .map(mapAttendanceRosterUser) ), [officeStaffQuery.data?.rows, scopeModel.mode]); const scopedAttendanceChildren = scopedAttendanceChildrenQuery.data; const scopedCampusTenants = scopedAttendanceChildren?.campusChildren; const scopedDirectChildren = scopedAttendanceChildren?.directChildren ?? EMPTY_TENANT_CHILDREN; const campusChildrenByParentId = scopedAttendanceChildren?.campusChildrenByParentId ?? EMPTY_CAMPUS_CHILDREN_BY_PARENT; const scopedCampusOptions = useMemo(() => { if (scopeModel.mode === 'campus') { return campusInfo ? [campusInfo] : EMPTY_CAMPUSES; } const scopedTenantIds = new Set((scopedCampusTenants ?? EMPTY_TENANT_CHILDREN).map((campus) => campus.id)); return campusCatalog.campuses.filter((campus) => ( campus.tenantId ? scopedTenantIds.has(campus.tenantId) : false )); }, [campusCatalog.campuses, campusInfo, scopeModel.mode, scopedCampusTenants]); const loading = campusCatalog.isLoading || (effectiveTier === 'class' && classQuery.isLoading) || configsQuery.isLoading || summariesQuery.isLoading || (roleAccess.canReadStaffReports && staffSummaryQuery.isLoading) || (roleAccess.canReadStaffReports && staffRecordsQuery.isLoading) || (roleAccess.canEnterData && officeStaffQuery.isLoading) || (roleAccess.canSeeAllCampuses && scopedAttendanceChildrenQuery.isLoading); const saving = saveConfigMutation.isPending || saveSummaryMutation.isPending || saveStaffAttendanceMutation.isPending; const loadError = campusCatalog.error ?? (effectiveTier === 'class' ? classQuery.error : null) ?? configsQuery.error ?? summariesQuery.error ?? (roleAccess.canReadStaffReports ? staffSummaryQuery.error : null) ?? (roleAccess.canReadStaffReports ? staffRecordsQuery.error : null) ?? (roleAccess.canEnterData ? officeStaffQuery.error : null) ?? (roleAccess.canSeeAllCampuses ? scopedAttendanceChildrenQuery.error : null); const saveError = saveConfigMutation.error ?? saveSummaryMutation.error ?? saveStaffAttendanceMutation.error; const [successMessage, setSuccessMessage] = useState(''); const [printError, setPrintError] = useState(null); const errorMessage = printError ?? getOptionalErrorMessage(loadError ?? saveError); const [editingLink, setEditingLink] = useState(null); const [linkValue, setLinkValue] = useState(''); const [showEntryForm, setShowEntryForm] = useState(false); const [expandedCampus, setExpandedCampus] = useState(null); const [entryDraft, setEntryDraft] = useState(() => emptyEntryDraft(today, attendanceCampusId ?? '')); const [staffEntryDraft, setStaffEntryDraft] = useState(() => emptyStaffEntryDraft(today)); const [entryError, setEntryError] = useState(null); const [staffEntryError, setStaffEntryError] = useState(null); const [studentAttendanceStatusOverrides, setStudentAttendanceStatusOverrides] = useState({}); const [staffAttendanceStatusOverrides, setStaffAttendanceStatusOverrides] = useState({}); const [studentRollupOverrides, setStudentRollupOverrides] = useState({}); const campusStats = useMemo( () => buildCampusAttendanceStats(campusCatalog.campuses, configs, attendanceData, today, weekStart, weekEnd, staffRecords), [attendanceData, campusCatalog.campuses, configs, staffRecords, today, weekEnd, weekStart], ); const visibleCampusStats = useMemo(() => { if (scopeModel.mode === 'campus') { return attendanceCampusId ? campusStats.filter((campus) => campus.id === attendanceCampusId) : []; } const scopedCampusIds = new Set([ ...scopedCampusOptions.map((campus) => campus.id), ...configs.map((config) => config.campus_id), ...attendanceData.map((record) => record.campus_id), ]); return campusStats.filter((campus) => scopedCampusIds.has(campus.id)); }, [attendanceCampusId, attendanceData, campusStats, configs, scopeModel.mode, scopedCampusOptions]); const attendanceChildStats = useMemo((): readonly CampusAttendanceChildStats[] => { if (scopeModel.mode === 'campus') { return visibleCampusStats.map((campus) => ({ id: campus.tenantId ?? campus.id, level: 'campus', mascot: campus.mascot, fullName: campus.fullName, bgGradient: campus.bgGradient, isOnline: campus.isOnline, todayPct: campus.todayPct, weekAvg: campus.weekAvg, recentData: campus.recentData, todayRecord: campus.todayRecord, childCampusIds: [campus.id], recentStaffData: campus.recentStaffData, todayStaffRecord: campus.todayStaffRecord, })); } if (scopeModel.mode === 'school') { return scopedDirectChildren .filter((child) => child.level === 'campus') .map((child, index) => { const campus = visibleCampusStats.find((item) => item.tenantId === child.id) ?? campusStats.find((item) => item.tenantId === child.id); return { id: child.id, level: child.level, mascot: campus?.mascot ?? child.name ?? 'Campus', fullName: campus?.fullName ?? child.name ?? 'Campus', bgGradient: campus?.bgGradient ?? SCHOOL_TILE_GRADIENTS[index % SCHOOL_TILE_GRADIENTS.length], isOnline: campus?.isOnline, todayPct: campus?.todayPct ?? null, weekAvg: campus?.weekAvg ?? null, recentData: campus?.recentData ?? [], todayRecord: campus?.todayRecord ?? null, childCampusIds: campus ? [campus.id] : [], recentStaffData: campus?.recentStaffData ?? [], todayStaffRecord: campus?.todayStaffRecord ?? null, }; }); } return scopedDirectChildren .filter((child) => child.level === 'school') .map((child, index) => { const childCampuses = (campusChildrenByParentId[child.id] ?? []) .map((campusChild) => campusCatalog.campuses.find((campus) => campus.tenantId === campusChild.id)) .filter((campus): campus is CampusInfo => Boolean(campus)); const childCampusIds = childCampuses.map((campus) => campus.id); const childTodayRecords = attendanceData.filter((record) => ( childCampusIds.includes(record.campus_id) && record.date === today )); const childWeekRecords = attendanceData.filter((record) => ( childCampusIds.includes(record.campus_id) && record.date >= weekStart && record.date <= weekEnd )); const childStaffRecords = campusStats .filter((campus) => childCampusIds.includes(campus.id)) .flatMap((campus) => campus.recentStaffData); const childTodayStaffRecords = childStaffRecords.filter((record) => record.date === today); const todayStaffRecord = childTodayStaffRecords.length > 0 ? { id: `school-staff:${child.id}:${today}`, campus_id: null, date: today, total_staff: childTodayStaffRecords.reduce((sum, record) => sum + record.total_staff, 0), total_present: childTodayStaffRecords.reduce((sum, record) => sum + record.total_present, 0), total_absent: childTodayStaffRecords.reduce((sum, record) => sum + record.total_absent, 0), total_late: childTodayStaffRecords.reduce((sum, record) => sum + record.total_late, 0), attendance_percentage: percentageFromStaffDailySummaries(childTodayStaffRecords) ?? 0, notes: null, } : null; const weekDays = Array.from(new Set(childWeekRecords.map((record) => record.date))); const weekAvg = weekDays.length > 0 ? Number((weekDays.reduce((sum, day) => ( sum + (percentageFromRecords(childWeekRecords.filter((record) => record.date === day)) ?? 0) ), 0) / weekDays.length).toFixed(2)) : null; return { id: child.id, level: child.level, mascot: child.name ?? 'School', fullName: child.name ?? 'School', bgGradient: SCHOOL_TILE_GRADIENTS[index % SCHOOL_TILE_GRADIENTS.length], todayPct: percentageFromRecords(childTodayRecords), weekAvg, recentData: childWeekRecords.slice(0, 10), todayRecord: null, childCampusIds, recentStaffData: childStaffRecords.slice(0, 10), todayStaffRecord, }; }); }, [ attendanceData, campusCatalog.campuses, campusChildrenByParentId, campusStats, scopeModel.mode, scopedDirectChildren, today, visibleCampusStats, weekEnd, weekStart, ]); const overallStats = useMemo( () => buildOverallAttendanceStats(attendanceData, today, weekStart, weekEnd), [attendanceData, today, weekEnd, weekStart], ); const combinedStats = useMemo( () => buildCombinedAttendanceStats( overallStats, staffSummary, scopeModel.mode === 'campus' ? undefined : attendanceChildStats, ), [attendanceChildStats, overallStats, scopeModel.mode, staffSummary], ); const myCampusConfig = attendanceCampusId ? configs.find((config) => config.campus_id === attendanceCampusId) : undefined; const myCampusData = attendanceCampusId ? attendanceData.filter((record) => record.campus_id === attendanceCampusId) : []; const myCampusStats = attendanceCampusId ? visibleCampusStats.find((campus) => campus.id === attendanceCampusId) : undefined; const myStaffData = myCampusStats?.recentStaffData ?? []; const myTodayPct = attendanceCampusId ? getTodayPercentage(attendanceData, attendanceCampusId, today) : null; const myWeekAvg = attendanceCampusId ? getWeeklyAverage(attendanceData, attendanceCampusId, weekStart, weekEnd) : null; const selectedEntryCampus = scopedCampusOptions.find((campus) => campus.id === entryDraft.campusId); const selectedEntryCampusTenantId = scopeModel.mode === 'campus' ? scopedCampusTenantId : selectedEntryCampus?.tenantId ?? null; const selectedEntryClassId = scopeModel.mode === 'campus' ? selectedClassId : null; const attendanceStudentsQuery = useQuery({ queryKey: ['attendance-student-users', selectedEntryCampusTenantId, selectedEntryClassId], enabled: roleAccess.canEnterData && showEntryForm && Boolean(selectedEntryClassId || selectedEntryCampusTenantId), queryFn: ({ signal }) => listUsers({ app_role: 'student', classId: selectedEntryClassId ?? undefined, campusId: selectedEntryClassId ? undefined : selectedEntryCampusTenantId ?? undefined, limit: 1000, field: 'name', sort: 'asc', }, { signal }), }); const attendanceStudents = useMemo(() => ( (attendanceStudentsQuery.data?.rows ?? []).map((user) => ({ id: user.id, name: userDisplayName(user), role: user.app_role?.name ?? null, })) ), [attendanceStudentsQuery.data?.rows]); const studentAttendanceStatuses = useMemo( () => reconcileAttendanceStatuses( studentAttendanceStatusOverrides, attendanceStudents.map((student) => student.id), ), [attendanceStudents, studentAttendanceStatusOverrides], ); const staffAttendanceStatuses = useMemo( () => reconcileAttendanceStatuses( staffAttendanceStatusOverrides, officeStaffUsers.map((staff) => staff.id), ), [officeStaffUsers, staffAttendanceStatusOverrides], ); const studentRollupRows = useMemo((): readonly CampusAttendanceRollupDraft[] => { if (scopeModel.mode === 'campus') { return []; } return scopedCampusOptions.map((campus) => { const todayRecord = attendanceData.find((record) => ( record.campus_id === campus.id && record.date === entryDraft.date )); const override = studentRollupOverrides[campus.id] ?? {}; return { campusId: campus.id, campusName: campus.fullName, enrolled: override.enrolled ?? (todayRecord ? String(todayRecord.total_enrolled) : ''), present: override.present ?? (todayRecord ? String(todayRecord.total_present) : ''), absent: override.absent ?? (todayRecord ? String(todayRecord.total_absent) : ''), tardy: override.tardy ?? (todayRecord ? String(todayRecord.total_tardy) : ''), notes: override.notes ?? todayRecord?.notes ?? '', hasRecordedData: Boolean(todayRecord), }; }); }, [attendanceData, entryDraft.date, scopeModel.mode, scopedCampusOptions, studentRollupOverrides]); const showSuccess = (message: string) => { setPrintError(null); setSuccessMessage(message); window.setTimeout(() => setSuccessMessage(''), UI_FEEDBACK_CLEAR_DELAY_MS); }; const updateEntryDraft = (patch: Partial) => { setEntryDraft((currentDraft) => ({ ...currentDraft, ...patch })); setEntryError(null); }; const updateStaffEntryDraft = (patch: Partial) => { setStaffEntryDraft((currentDraft) => ({ ...currentDraft, ...patch })); setStaffEntryError(null); }; const updateStudentAttendanceStatus = (userId: string, status: AttendanceRosterStatus) => { setStudentAttendanceStatusOverrides((currentStatuses) => ({ ...currentStatuses, [userId]: status })); setEntryError(null); }; const updateStudentRollupDraft = ( campusIdToUpdate: CampusId, patch: Partial>, ) => { setStudentRollupOverrides((currentOverrides) => ({ ...currentOverrides, [campusIdToUpdate]: { ...currentOverrides[campusIdToUpdate], ...patch, }, })); setEntryError(null); }; const updateStaffAttendanceStatus = (userId: string, status: AttendanceRosterStatus) => { setStaffAttendanceStatusOverrides((currentStatuses) => ({ ...currentStatuses, [userId]: status })); setStaffEntryError(null); }; const setEntryFormVisibility = (nextShowEntryForm: boolean) => { if (nextShowEntryForm) { setEntryDraft((currentDraft) => ({ ...currentDraft, campusId: attendanceCampusId ?? currentDraft.campusId, })); } setEntryError(null); setStaffEntryError(null); setShowEntryForm(nextShowEntryForm); }; const handleSaveLink = async (targetCampusId: CampusId) => { setPrintError(null); await saveConfigMutation.mutateAsync({ campusKey: targetCampusId, attendanceLink: linkValue.trim() || null, }); showSuccess('Link saved successfully!'); setEditingLink(null); }; const saveStudentAttendance = async (): Promise => { if (scopeModel.mode !== 'campus') { const rowsToSave: CampusAttendanceEntryInput[] = []; let hasInvalidRow = false; for (const row of studentRollupRows) { if (isBlankRollupRow(row)) { continue; } const input = toRollupEntryInput(row, entryDraft.date); if (!input) { hasInvalidRow = true; break; } rowsToSave.push(input); } if (hasInvalidRow) { setEntryError('Each campus row needs enrolled, present, and absent counts before saving.'); return false; } if (rowsToSave.length === 0) { setEntryError('Enter at least one campus attendance row before saving.'); return false; } for (const input of rowsToSave) { await saveSummaryMutation.mutateAsync(input); } setStudentRollupOverrides({}); return true; } const targetCampusId = attendanceCampusId ?? entryDraft.campusId; if (!targetCampusId) { setEntryError('Select a campus before saving attendance.'); return false; } const input = attendanceStudents.length > 0 ? { campusId: targetCampusId, date: entryDraft.date, totalEnrolled: attendanceStudents.length, totalPresent: attendanceStudents.filter((student) => ( (studentAttendanceStatuses[student.id] ?? 'present') !== 'absent' )).length, totalAbsent: attendanceStudents.filter((student) => ( (studentAttendanceStatuses[student.id] ?? 'present') === 'absent' )).length, totalTardy: attendanceStudents.filter((student) => ( (studentAttendanceStatuses[student.id] ?? 'present') === 'late' )).length, notes: entryDraft.notes.trim() || null, } : buildAttendanceEntryInput(entryDraft, targetCampusId); if (!input) { setEntryError('Enter enrolled, present, and absent counts before saving.'); return false; } await saveSummaryMutation.mutateAsync(input); setEntryDraft(emptyEntryDraft(today, attendanceCampusId ?? targetCampusId)); return true; }; const saveStaffBatchAttendance = async ( requireStaff: boolean, input: Pick = staffEntryDraft, ): Promise => { if (officeStaffUsers.length === 0) { if (requireStaff) { setStaffEntryError('No office staff are available for this scope.'); } return !requireStaff; } for (const staffUser of officeStaffUsers) { await saveStaffAttendanceMutation.mutateAsync({ date: input.date, userId: staffUser.id, status: staffAttendanceStatuses[staffUser.id] ?? 'present', note: input.note, }); } setStaffEntryDraft(emptyStaffEntryDraft(today)); return true; }; const handleSubmitEntry = async () => { setPrintError(null); const saved = await saveStudentAttendance(); if (!saved) { return; } showSuccess('Student attendance saved!'); setShowEntryForm(false); }; const handleSubmitStaffEntry = async () => { setPrintError(null); if (!staffEntryDraft.userId) { setStaffEntryError('Select an office staff member before saving attendance.'); return; } await saveStaffAttendanceMutation.mutateAsync(staffEntryDraft); showSuccess('Office staff attendance saved!'); setStaffEntryDraft(emptyStaffEntryDraft(today)); }; const handleSubmitStaffBatch = async () => { setPrintError(null); const saved = await saveStaffBatchAttendance(true); if (!saved) { return; } showSuccess('Staff attendance saved!'); }; const handleSubmitAttendanceForm = async () => { setPrintError(null); const studentSaved = await saveStudentAttendance(); if (!studentSaved) { return; } const staffSaved = await saveStaffBatchAttendance(false, { date: entryDraft.date, note: staffEntryDraft.note, }); if (!staffSaved) { return; } showSuccess('Attendance saved!'); setShowEntryForm(false); }; const handlePrint = () => { const campusesToPrint = roleAccess.canSeeAllCampuses ? visibleCampusStats : campusStats.filter((campus) => campus.id === attendanceCampusId); const reportTitle = roleAccess.canSeeAllCampuses ? `${scopeModel.reportLabel} Attendance Report` : `${campusInfo?.fullName || userCampus} Attendance Report`; const printTodayRecords = roleAccess.canSeeAllCampuses ? overallStats.todayRecords : attendanceData.filter((record) => record.campus_id === attendanceCampusId && record.date === today); const printWeekRecords = roleAccess.canSeeAllCampuses ? overallStats.weekRecords : attendanceData.filter((record) => record.campus_id === attendanceCampusId && record.date >= weekStart && record.date <= weekEnd); const printResult = openCampusAttendancePrintReport({ input: { reportTitle, generatedByName: userName, generatedByRole: `${userRole.charAt(0).toUpperCase()}${userRole.slice(1)}`, today, weekStart, campusesToPrint, printTodayRecords, printWeekRecords, staffSummary, }, }); if (!printResult.ok) { setPrintError(CAMPUS_ATTENDANCE_PRINT_POPUP_BLOCKED_MESSAGE); return; } setPrintError(null); }; return { state: { roleAccess, scopeModel, campusInfo, campusId: attendanceCampusId, today, weekStart, weekEnd, configs, attendanceData, staffRecords, loading, saving, errorMessage, successMessage, editingLink, linkValue, showEntryForm, expandedCampus, entryDraft, entryError, staffEntryDraft, staffEntryError, officeStaffUsers, staffAttendanceStatuses, studentRollupRows, attendanceStudents, studentAttendanceStatuses, attendanceStudentsLoading: attendanceStudentsQuery.isLoading, scopedCampusOptions, campusStats: visibleCampusStats, attendanceChildStats, overallStats, combinedStats, staffSummary: { summary: staffSummary, loading: staffSummaryQuery.isLoading, error: staffSummaryQuery.error, }, myCampusConfig, myCampusData, myStaffData, myTodayPct, myWeekAvg, userCampus, }, actions: { setEditingLink, setLinkValue, setShowEntryForm: setEntryFormVisibility, setExpandedCampus, updateEntryDraft, updateStaffEntryDraft, updateStudentAttendanceStatus, updateStudentRollupDraft, updateStaffAttendanceStatus, handleSaveLink, handleSubmitEntry, handleSubmitStaffEntry, handleSubmitStaffBatch, handleSubmitAttendanceForm, handlePrint, }, }; }