Autosave: 20260617-141704

This commit is contained in:
Flatlogic Bot 2026-06-17 14:16:50 +00:00
parent d1a08e4c3d
commit 191f79c21c
2 changed files with 669 additions and 0 deletions

View File

@ -0,0 +1,204 @@
const express = require('express');
const db = require('../db/models');
const wrapAsync = require('../helpers').wrapAsync;
const { checkPermissions } = require('../middlewares/check-permissions');
const router = express.Router();
const ATTENDANCE_STATUSES = ['present', 'absent', 'late', 'excused'];
const SESSION_TYPES = ['homeroom', 'subject', 'exam', 'event'];
const getUserOrganizationId = (currentUser) => (
currentUser?.organization?.id ||
currentUser?.organizations?.id ||
currentUser?.organizationId ||
currentUser?.organizationsId ||
null
);
const isGlobalAccess = (currentUser) => Boolean(currentUser?.app_role?.globalAccess);
const scopedWhere = (currentUser) => {
if (isGlobalAccess(currentUser)) {
return {};
}
const organizationId = getUserOrganizationId(currentUser);
return organizationId ? { organizationId } : { id: null };
};
const studentName = (student) => [student?.first_name, student?.last_name].filter(Boolean).join(' ') || 'Unnamed student';
router.get(
'/attendance-context',
checkPermissions('READ_CLASSES'),
checkPermissions('READ_ATTENDANCE_SESSIONS'),
wrapAsync(async (req, res) => {
const classWhere = scopedWhere(req.currentUser);
const sessionWhere = scopedWhere(req.currentUser);
const [classes, recentSessions] = await Promise.all([
db.classes.findAll({
where: classWhere,
include: [
{ model: db.campuses, as: 'campus' },
{ model: db.grades, as: 'grade' },
{ model: db.academic_years, as: 'academic_year' },
],
order: [['name', 'asc']],
limit: 100,
}),
db.attendance_sessions.findAll({
where: sessionWhere,
include: [
{ model: db.campuses, as: 'campus' },
{ model: db.classes, as: 'class' },
{ model: db.staff, as: 'taken_by' },
{ model: db.attendance_records, as: 'attendance_records_attendance_session', required: false },
],
order: [['session_date', 'desc'], ['createdAt', 'desc']],
limit: 8,
}),
]);
res.status(200).send({
classes: classes.map((item) => item.get({ plain: true })),
recentSessions: recentSessions.map((item) => item.get({ plain: true })),
});
}),
);
router.get(
'/attendance-roster',
checkPermissions('READ_CLASS_ENROLLMENTS'),
wrapAsync(async (req, res) => {
const { classId } = req.query;
if (!classId) {
return res.status(400).send({ message: 'classId is required' });
}
const selectedClass = await db.classes.findByPk(classId);
if (!selectedClass) {
return res.status(404).send({ message: 'Class not found' });
}
const userOrganizationId = getUserOrganizationId(req.currentUser);
if (!isGlobalAccess(req.currentUser) && selectedClass.organizationId !== userOrganizationId) {
return res.status(403).send({ message: 'You can only view rosters inside your organization.' });
}
const enrollments = await db.class_enrollments.findAll({
where: {
classId,
status: 'active',
},
include: [{ model: db.students, as: 'student' }],
order: [['createdAt', 'asc']],
limit: 300,
});
res.status(200).send({
class: selectedClass.get({ plain: true }),
students: enrollments
.map((enrollment) => enrollment.get({ plain: true }).student)
.filter(Boolean)
.map((student) => ({
id: student.id,
student_number: student.student_number,
first_name: student.first_name,
last_name: student.last_name,
status: student.status,
displayName: studentName(student),
})),
});
}),
);
router.post(
'/attendance-runs',
checkPermissions('CREATE_ATTENDANCE_SESSIONS'),
checkPermissions('CREATE_ATTENDANCE_RECORDS'),
wrapAsync(async (req, res) => {
const { classId, sessionDate, sessionType, notes, records } = req.body || {};
if (!classId) {
return res.status(400).send({ message: 'Please select a class.' });
}
if (!sessionDate || Number.isNaN(Date.parse(sessionDate))) {
return res.status(400).send({ message: 'Please choose a valid attendance date.' });
}
if (!SESSION_TYPES.includes(sessionType)) {
return res.status(400).send({ message: 'Please choose a valid session type.' });
}
if (!Array.isArray(records) || records.length === 0) {
return res.status(400).send({ message: 'Add at least one student attendance record.' });
}
const invalidRecord = records.find((record) => !record.studentId || !ATTENDANCE_STATUSES.includes(record.status));
if (invalidRecord) {
return res.status(400).send({ message: 'Each record needs a student and a valid attendance status.' });
}
const selectedClass = await db.classes.findByPk(classId);
if (!selectedClass) {
return res.status(404).send({ message: 'Class not found' });
}
const userOrganizationId = getUserOrganizationId(req.currentUser);
if (!isGlobalAccess(req.currentUser) && selectedClass.organizationId !== userOrganizationId) {
return res.status(403).send({ message: 'You can only record attendance inside your organization.' });
}
const transaction = await db.sequelize.transaction();
try {
const attendanceSession = await db.attendance_sessions.create(
{
session_date: sessionDate,
session_type: sessionType,
notes: notes || null,
organizationId: selectedClass.organizationId,
campusId: selectedClass.campusId,
classId: selectedClass.id,
createdById: req.currentUser.id,
updatedById: req.currentUser.id,
},
{ transaction },
);
const attendanceRecords = await db.attendance_records.bulkCreate(
records.map((record) => ({
status: record.status,
minutes_late: record.status === 'late' ? Number(record.minutes_late || 0) : null,
remarks: record.remarks || null,
organizationId: selectedClass.organizationId,
attendance_sessionId: attendanceSession.id,
studentId: record.studentId,
createdById: req.currentUser.id,
updatedById: req.currentUser.id,
})),
{ transaction },
);
await transaction.commit();
res.status(201).send({
session: attendanceSession.get({ plain: true }),
recordsCreated: attendanceRecords.length,
summary: ATTENDANCE_STATUSES.reduce((acc, status) => {
acc[status] = records.filter((record) => record.status === status).length;
return acc;
}, {}),
});
} catch (error) {
await transaction.rollback();
throw error;
}
}),
);
module.exports = router;

View File

@ -0,0 +1,465 @@
import { mdiAccountCheckOutline, mdiChartTimelineVariant, mdiCheckCircleOutline, mdiClipboardListOutline, mdiRefresh, mdiSchoolOutline } from '@mdi/js';
import axios from 'axios';
import Head from 'next/head';
import Link from 'next/link';
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import BaseButton from '../components/BaseButton';
import BaseIcon from '../components/BaseIcon';
import CardBox from '../components/CardBox';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import LayoutAuthenticated from '../layouts/Authenticated';
type SchoolClass = {
id?: string;
name?: string;
section?: string;
capacity?: number;
campus?: { id: string; name?: string };
grade?: { id: string; name?: string };
academic_year?: { id: string; name?: string };
};
type RosterStudent = {
id: string;
student_number?: string;
first_name?: string;
last_name?: string;
displayName?: string;
};
type AttendanceStatus = 'present' | 'absent' | 'late' | 'excused';
type DraftRecord = {
studentId: string;
studentName: string;
studentNumber?: string;
status: AttendanceStatus;
minutes_late: number;
remarks: string;
};
type RecentSession = {
id: string;
session_date?: string;
session_type?: string;
notes?: string;
class?: { name?: string; section?: string };
campus?: { name?: string };
attendance_records_attendance_session?: Array<{ status?: AttendanceStatus }>;
};
const statusOptions: AttendanceStatus[] = ['present', 'absent', 'late', 'excused'];
const statusClasses: Record<AttendanceStatus, string> = {
present: 'bg-emerald-50 text-emerald-700 ring-emerald-200',
absent: 'bg-rose-50 text-rose-700 ring-rose-200',
late: 'bg-amber-50 text-amber-700 ring-amber-200',
excused: 'bg-sky-50 text-sky-700 ring-sky-200',
};
const classLabel = (schoolClass?: SchoolClass) => {
if (!schoolClass) return 'Select class';
return [schoolClass.name, schoolClass.section].filter(Boolean).join(' • ') || 'Unnamed class';
};
const studentLabel = (student: RosterStudent) => student.displayName || [student.first_name, student.last_name].filter(Boolean).join(' ') || 'Unnamed student';
const DailyAttendance = () => {
const [classes, setClasses] = useState<SchoolClass[]>([]);
const [recentSessions, setRecentSessions] = useState<RecentSession[]>([]);
const [selectedClassId, setSelectedClassId] = useState('');
const [sessionDate, setSessionDate] = useState(() => new Date().toISOString().slice(0, 10));
const [sessionType, setSessionType] = useState('homeroom');
const [notes, setNotes] = useState('');
const [records, setRecords] = useState<DraftRecord[]>([]);
const [loading, setLoading] = useState(true);
const [rosterLoading, setRosterLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const selectedClass = useMemo(
() => classes.find((item) => item.id === selectedClassId),
[classes, selectedClassId],
);
const totals = useMemo(
() => statusOptions.reduce((acc, status) => {
acc[status] = records.filter((record) => record.status === status).length;
return acc;
}, {} as Record<AttendanceStatus, number>),
[records],
);
const loadContext = async () => {
setError('');
setLoading(true);
try {
const response = await axios.get('school-ops/attendance-context');
setClasses(response.data.classes || []);
setRecentSessions(response.data.recentSessions || []);
} catch (err: any) {
console.error('Daily attendance context failed:', err);
setError(err?.response?.data?.message || 'Could not load attendance workspace.');
} finally {
setLoading(false);
}
};
const loadRoster = async (classId: string) => {
setSelectedClassId(classId);
setSuccess('');
setError('');
setRecords([]);
if (!classId) return;
setRosterLoading(true);
try {
const response = await axios.get(`school-ops/attendance-roster?classId=${classId}`);
const students: RosterStudent[] = response.data.students || [];
setRecords(students.map((student) => ({
studentId: student.id,
studentName: studentLabel(student),
studentNumber: student.student_number,
status: 'present',
minutes_late: 0,
remarks: '',
})));
} catch (err: any) {
console.error('Daily attendance roster failed:', err);
setError(err?.response?.data?.message || 'Could not load the class roster.');
} finally {
setRosterLoading(false);
}
};
useEffect(() => {
loadContext();
}, []);
const updateRecord = (studentId: string, patch: Partial<DraftRecord>) => {
setRecords((current) => current.map((record) => (
record.studentId === studentId ? { ...record, ...patch } : record
)));
};
const markAll = (status: AttendanceStatus) => {
setRecords((current) => current.map((record) => ({
...record,
status,
minutes_late: status === 'late' ? record.minutes_late : 0,
})));
};
const submitAttendance = async () => {
setError('');
setSuccess('');
if (!selectedClassId) {
setError('Please select a class before saving attendance.');
return;
}
if (!sessionDate) {
setError('Please choose an attendance date.');
return;
}
if (!records.length) {
setError('This class has no active students to mark yet. Add enrollments first.');
return;
}
setSaving(true);
try {
const response = await axios.post('school-ops/attendance-runs', {
classId: selectedClassId,
sessionDate,
sessionType,
notes,
records: records.map((record) => ({
studentId: record.studentId,
status: record.status,
minutes_late: record.minutes_late,
remarks: record.remarks,
})),
});
setSuccess(`Saved ${response.data.recordsCreated} attendance records for ${classLabel(selectedClass)}.`);
await loadContext();
} catch (err: any) {
console.error('Daily attendance save failed:', err);
setError(err?.response?.data?.message || 'Attendance could not be saved. Please review the roster and try again.');
} finally {
setSaving(false);
}
};
return (
<>
<Head>
<title>{getPageTitle('Daily Attendance')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiClipboardListOutline} title="Daily Attendance" main>
<BaseButton href="/attendance_sessions/attendance_sessions-list" label="All sessions" color="whiteDark" small />
</SectionTitleLineWithButton>
<div className="mb-6 overflow-hidden rounded-3xl bg-gradient-to-br from-slate-950 via-slate-900 to-emerald-900 p-6 text-white shadow-xl">
<div className="grid gap-6 lg:grid-cols-[1.2fr_0.8fr] lg:items-end">
<div>
<div className="mb-4 inline-flex items-center rounded-full border border-white/15 bg-white/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.25em] text-emerald-100">
Multi-tenant operations
</div>
<h2 className="max-w-3xl text-3xl font-black tracking-tight md:text-5xl">
Record a clean daily attendance run in one guided workspace.
</h2>
<p className="mt-4 max-w-2xl text-sm leading-6 text-slate-200 md:text-base">
Pick a class, load its active roster, mark exceptions, and save a complete auditable attendance session for the current organization.
</p>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="rounded-2xl border border-white/10 bg-white/10 p-4 backdrop-blur">
<div className="text-3xl font-black">{classes.length}</div>
<div className="text-emerald-100">classes available</div>
</div>
<div className="rounded-2xl border border-white/10 bg-white/10 p-4 backdrop-blur">
<div className="text-3xl font-black">{recentSessions.length}</div>
<div className="text-emerald-100">recent sessions</div>
</div>
</div>
</div>
</div>
{error && (
<div className="mb-6 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-medium text-rose-700">
{error}
</div>
)}
{success && (
<div className="mb-6 flex items-center rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-700">
<BaseIcon path={mdiCheckCircleOutline} className="mr-2" /> {success}
</div>
)}
<div className="grid gap-6 xl:grid-cols-[1fr_380px]">
<CardBox className="overflow-hidden border-0 shadow-sm">
<div className="grid gap-4 lg:grid-cols-4">
<label className="block lg:col-span-2">
<span className="mb-2 block text-sm font-bold text-slate-700 dark:text-slate-200">Class roster</span>
<select
className="h-12 w-full rounded-2xl border border-slate-200 bg-white px-4 text-sm outline-none transition focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 dark:border-dark-700 dark:bg-dark-800"
value={selectedClassId}
onChange={(event) => loadRoster(event.target.value)}
disabled={loading}
>
<option value="">Choose a class</option>
{classes.map((item) => (
<option key={item.id} value={item.id}>{classLabel(item)}{item.campus?.name ? `${item.campus.name}` : ''}</option>
))}
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-bold text-slate-700 dark:text-slate-200">Date</span>
<input
className="h-12 w-full rounded-2xl border border-slate-200 bg-white px-4 text-sm outline-none transition focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 dark:border-dark-700 dark:bg-dark-800"
type="date"
value={sessionDate}
onChange={(event) => setSessionDate(event.target.value)}
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-bold text-slate-700 dark:text-slate-200">Session</span>
<select
className="h-12 w-full rounded-2xl border border-slate-200 bg-white px-4 text-sm capitalize outline-none transition focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 dark:border-dark-700 dark:bg-dark-800"
value={sessionType}
onChange={(event) => setSessionType(event.target.value)}
>
<option value="homeroom">Homeroom</option>
<option value="subject">Subject</option>
<option value="exam">Exam</option>
<option value="event">Event</option>
</select>
</label>
</div>
{selectedClass && (
<div className="mt-4 flex flex-wrap gap-2 text-xs font-semibold text-slate-500">
<span className="rounded-full bg-slate-100 px-3 py-1 dark:bg-dark-800"><BaseIcon path={mdiSchoolOutline} className="mr-1" size={14} />{selectedClass.campus?.name || 'No campus set'}</span>
<span className="rounded-full bg-slate-100 px-3 py-1 dark:bg-dark-800">{selectedClass.grade?.name || 'Grade not set'}</span>
<span className="rounded-full bg-slate-100 px-3 py-1 dark:bg-dark-800">Capacity {selectedClass.capacity || '—'}</span>
</div>
)}
<label className="mt-5 block">
<span className="mb-2 block text-sm font-bold text-slate-700 dark:text-slate-200">Session notes</span>
<textarea
className="min-h-24 w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm outline-none transition focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 dark:border-dark-700 dark:bg-dark-800"
placeholder="Optional context: substitute teacher, assembly day, weather disruption..."
value={notes}
onChange={(event) => setNotes(event.target.value)}
/>
</label>
<div className="mt-6 flex flex-wrap items-center justify-between gap-3 border-t border-slate-100 pt-5 dark:border-dark-700">
<div className="flex flex-wrap gap-2">
{statusOptions.map((status) => (
<button
key={status}
type="button"
className={`rounded-full px-3 py-1.5 text-xs font-bold capitalize ring-1 ${statusClasses[status]}`}
onClick={() => markAll(status)}
disabled={!records.length}
>
Mark all {status}
</button>
))}
</div>
<BaseButton icon={mdiRefresh} label="Reload" color="whiteDark" small onClick={() => selectedClassId ? loadRoster(selectedClassId) : loadContext()} />
</div>
<div className="mt-5 overflow-hidden rounded-3xl border border-slate-100 dark:border-dark-700">
<div className="grid grid-cols-12 bg-slate-50 px-4 py-3 text-xs font-black uppercase tracking-wide text-slate-400 dark:bg-dark-800">
<div className="col-span-5">Student</div>
<div className="col-span-3">Status</div>
<div className="col-span-2">Late min</div>
<div className="col-span-2">Remark</div>
</div>
{loading || rosterLoading ? (
<div className="p-8 text-center text-sm text-slate-500">Loading attendance workspace...</div>
) : !selectedClassId ? (
<div className="p-8 text-center">
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-50 text-emerald-600">
<BaseIcon path={mdiAccountCheckOutline} size={24} />
</div>
<p className="font-bold text-slate-700 dark:text-slate-200">Start by choosing a class.</p>
<p className="text-sm text-slate-500">The roster will appear here with everyone marked present by default.</p>
</div>
) : !records.length ? (
<div className="p-8 text-center">
<p className="font-bold text-slate-700 dark:text-slate-200">No active enrollments found.</p>
<p className="mt-1 text-sm text-slate-500">Use Class enrollments to add students before taking attendance.</p>
<BaseButton href="/class_enrollments/class_enrollments-new" label="Add enrollment" color="info" small className="mt-4" />
</div>
) : (
<div className="divide-y divide-slate-100 dark:divide-dark-700">
{records.map((record) => (
<div key={record.studentId} className="grid grid-cols-12 items-center gap-3 px-4 py-3 text-sm">
<div className="col-span-12 sm:col-span-5">
<div className="font-bold text-slate-800 dark:text-slate-100">{record.studentName}</div>
<div className="text-xs text-slate-400">{record.studentNumber || 'No student number'}</div>
</div>
<div className="col-span-6 sm:col-span-3">
<select
className="h-10 w-full rounded-xl border border-slate-200 bg-white px-3 text-sm capitalize outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 dark:border-dark-700 dark:bg-dark-800"
value={record.status}
onChange={(event) => updateRecord(record.studentId, {
status: event.target.value as AttendanceStatus,
minutes_late: event.target.value === 'late' ? record.minutes_late : 0,
})}
>
{statusOptions.map((status) => <option key={status} value={status}>{status}</option>)}
</select>
</div>
<div className="col-span-3 sm:col-span-2">
<input
className="h-10 w-full rounded-xl border border-slate-200 bg-white px-3 text-sm outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 disabled:bg-slate-50 dark:border-dark-700 dark:bg-dark-800"
type="number"
min="0"
value={record.minutes_late}
disabled={record.status !== 'late'}
onChange={(event) => updateRecord(record.studentId, { minutes_late: Number(event.target.value) })}
/>
</div>
<div className="col-span-3 sm:col-span-2">
<input
className="h-10 w-full rounded-xl border border-slate-200 bg-white px-3 text-sm outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 dark:border-dark-700 dark:bg-dark-800"
placeholder="Optional"
value={record.remarks}
onChange={(event) => updateRecord(record.studentId, { remarks: event.target.value })}
/>
</div>
</div>
))}
</div>
)}
</div>
<div className="mt-6 flex flex-wrap items-center justify-between gap-4">
<div className="flex flex-wrap gap-2">
{statusOptions.map((status) => (
<span key={status} className={`rounded-full px-3 py-1 text-xs font-black capitalize ring-1 ${statusClasses[status]}`}>
{status}: {totals[status] || 0}
</span>
))}
</div>
<BaseButton
label={saving ? 'Saving...' : 'Save attendance run'}
color="success"
icon={mdiCheckCircleOutline}
disabled={saving || rosterLoading || !records.length}
onClick={submitAttendance}
/>
</div>
</CardBox>
<div className="space-y-6">
<CardBox className="border-0 shadow-sm">
<div className="mb-4 flex items-center justify-between">
<div>
<h3 className="text-lg font-black text-slate-800 dark:text-slate-100">Recent sessions</h3>
<p className="text-sm text-slate-500">Latest attendance activity in your accessible schools.</p>
</div>
<BaseIcon path={mdiChartTimelineVariant} className="text-emerald-600" size={26} />
</div>
<div className="space-y-3">
{recentSessions.length ? recentSessions.map((session) => {
const sessionRecords = session.attendance_records_attendance_session || [];
return (
<Link key={session.id} href={`/attendance_sessions/${session.id}`} className="block rounded-2xl border border-slate-100 p-4 transition hover:border-emerald-200 hover:bg-emerald-50/40 dark:border-dark-700 dark:hover:bg-dark-800">
<div className="flex items-start justify-between gap-3">
<div>
<div className="font-black text-slate-800 dark:text-slate-100">{classLabel(session.class as SchoolClass)}</div>
<div className="text-xs text-slate-500">{session.campus?.name || 'Campus not set'} {session.session_type || 'session'}</div>
</div>
<div className="rounded-full bg-slate-100 px-2 py-1 text-xs font-bold text-slate-500 dark:bg-dark-800">
{sessionRecords.length} records
</div>
</div>
<div className="mt-3 text-xs font-semibold text-slate-400">
{session.session_date ? new Date(session.session_date).toLocaleDateString() : 'No date'}
</div>
</Link>
);
}) : (
<div className="rounded-2xl border border-dashed border-slate-200 p-6 text-center text-sm text-slate-500 dark:border-dark-700">
No sessions yet. Your first saved attendance run will appear here.
</div>
)}
</div>
</CardBox>
<CardBox className="border-0 bg-gradient-to-br from-emerald-50 to-sky-50 shadow-sm dark:from-dark-900 dark:to-dark-800">
<h3 className="text-lg font-black text-slate-800 dark:text-slate-100">Next operational links</h3>
<div className="mt-4 grid gap-3 text-sm font-semibold">
<Link href="/classes/classes-list" className="rounded-2xl bg-white/80 px-4 py-3 text-slate-700 shadow-sm transition hover:text-emerald-700 dark:bg-dark-800 dark:text-slate-200">Manage classes</Link>
<Link href="/students/students-list" className="rounded-2xl bg-white/80 px-4 py-3 text-slate-700 shadow-sm transition hover:text-emerald-700 dark:bg-dark-800 dark:text-slate-200">Review students</Link>
<Link href="/attendance_records/attendance_records-list" className="rounded-2xl bg-white/80 px-4 py-3 text-slate-700 shadow-sm transition hover:text-emerald-700 dark:bg-dark-800 dark:text-slate-200">Open records table</Link>
</div>
</CardBox>
</div>
</div>
</SectionMain>
</>
);
};
DailyAttendance.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission="READ_ATTENDANCE_SESSIONS">{page}</LayoutAuthenticated>;
};
export default DailyAttendance;