import React, { useState, useEffect, useCallback } from 'react';
// --- MOCK ICONS (using emojis for web) ---
// This component provides a simple way to use icons with emojis.
const Icon = ({ name, size = 24, color = '#000' }) => {
const icons = {
home: '๐ ',
people: '๐ฅ',
cloud: 'โ๏ธ',
settings: 'โ๏ธ',
airplane: 'โ๏ธ',
ai: '๐ง ',
camera: '๐ท',
chart: '๐',
edit: 'โ๏ธ',
computer: '๐ป',
wrench: '๐ง',
building: '๐๏ธ',
bolt: 'โก',
satellite: '๐ก',
flask: '๐งช',
heart: 'โค๏ธ',
car: '๐',
user: '๐ค',
id: '๐',
briefcase: '๐ผ',
download: '๐ฅ',
'arrow-left': 'โฌ
๏ธ',
share: '๐ค',
upload: 'โฌ๏ธ',
};
return {icons[name] || 'โ'} ;
};
// --- CUSTOM HEADER COMPONENTS ---
// A header with a wave-like SVG background.
const WaveHeader = ({ title }) => (
);
// A simple back button component.
const BackButton = ({ onClick }) => (
);
// A header component that includes a back button.
const HeaderWithBack = ({ title, onBackPress }) => (
{onBackPress && }
);
// --- CUSTOM MESSAGE BOX COMPONENT ---
const MessageBox = ({ message, onClose }) => {
if (!message) return null;
return (
);
};
// --- SCREENS ---
// 1. Login/Register Screen
const AuthScreen = ({ navigate, setUserProfile, showMessage }) => {
const [isLogin, setIsLogin] = useState(true);
const [formData, setFormData] = useState({
username: '',
password: '',
confirmPassword: '',
fullName: '',
employeeId: '',
department: 'Aeronautical Engineering',
role: 'Staff'
});
const [error, setError] = useState('');
const getUsers = () => {
const users = localStorage.getItem('attendance_app_users');
return users ? JSON.parse(users) : [];
};
const saveUsers = (users) => {
localStorage.setItem('attendance_app_users', JSON.stringify(users));
};
// Initialize an admin user if no users exist.
useEffect(() => {
const users = getUsers();
if (users.length === 0) {
const adminUser = {
username: 'admin',
password: 'admin',
fullName: 'Admin User',
employeeId: '001',
department: 'Administration',
role: 'Admin',
};
saveUsers([adminUser]);
}
}, []);
const departments = [
'Aeronautical Engineering', 'Artificial Intelligence & Data Science', 'Computer Science & Engineering',
'Mechanical Engineering', 'Civil Engineering', 'Electrical & Electronics Engineering',
'Electronics & Communication Engineering', 'Information Technology', 'Chemical Engineering',
'Mechatronics','Computer Science and Bussiness Science'
];
const handleInputChange = (field, value) => {
setFormData(prev => ({...prev, [field]: value}));
setError('');
};
const handleLogin = () => {
const users = getUsers();
const foundUser = users.find(user => user.username === formData.username && user.password === formData.password);
if (foundUser) {
setUserProfile({
name: foundUser.fullName,
position: foundUser.role,
employeeId: foundUser.employeeId,
department: foundUser.department,
role: foundUser.role,
});
navigate('MainApp');
} else {
setError('Invalid username or password.');
}
};
const handleRegister = () => {
if (!formData.username || !formData.fullName || !formData.employeeId || !formData.password) {
setError('Please fill all the fields.');
return;
}
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match.');
return;
}
const users = getUsers();
const existingUser = users.find(user => user.username === formData.username);
if (existingUser) {
setError('Username already exists.');
return;
}
const newUser = {
username: formData.username,
password: formData.password,
fullName: formData.fullName,
employeeId: formData.employeeId,
department: formData.department,
role: formData.role,
};
const updatedUsers = [...users, newUser];
saveUsers(updatedUsers);
setUserProfile({
name: newUser.fullName,
position: newUser.role,
employeeId: newUser.employeeId,
department: newUser.department,
role: newUser.role,
});
navigate('MainApp');
};
const handleAuthAction = () => {
if (isLogin) {
handleLogin();
} else {
handleRegister();
}
};
return (
);
};
// 2. Department/Home Screen
const DepartmentScreen = ({ navigate, attendanceRecords, userProfile, departments }) => {
const [selectedDept, setSelectedDept] = useState(userProfile?.department || departments[0]);
const [selectedYear, setSelectedYear] = useState('First Year');
const [selectedSection, setSelectedSection] = useState('Section A');
const years = ['First Year', 'Second Year', 'Third Year', 'Fourth Year'];
const sections = ['Section A', 'Section B'];
const filteredRecords = attendanceRecords
? attendanceRecords.filter(record =>
record.department === selectedDept &&
record.year === selectedYear &&
record.section === selectedSection
)
: [];
const total = filteredRecords.reduce((sum, rec) => sum + (rec.total || 0), 0);
const present = filteredRecords.reduce((sum, rec) => sum + (rec.present || 0), 0);
const absent = filteredRecords.reduce((sum, rec) => sum + (rec.absent || 0), 0);
const onDuty = filteredRecords.reduce((sum, rec) => sum + (rec.onDuty || 0), 0);
const chartData = total === 0 ? [] : [
{ label: 'Present', value: (present / total) * 100, color: '#2ecc71' },
{ label: 'Absent', value: (absent / total) * 100, color: '#e74c3c' },
{ label: 'On Duty', value: (onDuty / total) * 100, color: '#f39c12' },
];
const departmentData = [
{ name: 'Aeronautical Engineering', icon: 'airplane' },
{ name: 'Artificial Intelligence & Data Science', icon: 'ai' },
{ name: 'Computer Science & Engineering', icon: 'computer' },
{ name: 'Mechanical Engineering', icon: 'wrench' },
{ name: 'Civil Engineering', icon: 'building' },
{ name: 'Electrical & Electronics Engineering', icon: 'bolt' },
{ name: 'Electronics & Communication Engineering', icon: 'satellite' },
{ name: 'Information Technology', icon: 'computer' },
{ name: 'Chemical Engineering', icon: 'flask' },
{ name: 'Mechatronics', icon: 'wrench' },
{ name: 'Computer Science and Bussiness Science', icon: 'computer' }
];
return (
{/* Dashboard chart above department list */}
{chartData.length > 0 ? chartData.map(d => (
{d.label}: {d.value.toFixed(1)}%
)) : (
No attendance data for this class to display a chart.
)}
{/* Class selection for chart */}
setSelectedDept(e.target.value)}>
{departments.map(dept => {dept} )}
setSelectedYear(e.target.value)}>
{years.map(year => {year} )}
setSelectedSection(e.target.value)}>
{sections.map(section => {section} )}
{/* Department list */}
Department
{departmentData.map((dept, index) => (
navigate('Year', { department: dept.name })}>
{dept.name}
))}
);
};
// 3. Year Selection Screen
const YearScreen = ({ navigate, params }) => {
const years = ['First Year', 'Second Year', 'Third Year', 'Fourth Year'];
return (
navigate('MainApp', { screen: 'Home' })}
/>
{params.department}
{years.map((year, index) => (
navigate('Section', { ...params, year: year })}>
{index + 1}
{year}
))}
);
};
// 4. Section Selection Screen
const SectionScreen = ({ navigate, params }) => {
const sections = ['Section A', 'Section B'];
const { year, department } = params;
return (
navigate('Year', { department })}
/>
{`${department} - ${year}`}
{sections.map((section, index) => (
navigate('MainApp', { ...params, screen: 'Attendance', year, section })}>
{section}
))}
);
};
// 5. Attendance Entry Screen
const AttendanceScreen = ({
addAttendanceRecord,
year,
section,
department,
navigate,
defaultStudentCounts,
showMessage,
studentList
}) => {
const [formData, setFormData] = useState({
total: '', present: '', absent: '', onDuty: '', absentNumbers: ''
});
useEffect(() => {
const classKey = `${department}-${year}-${section}`;
const totalStudents = defaultStudentCounts[classKey];
if (totalStudents) {
setFormData(prevData => ({ ...prevData, total: totalStudents }));
} else {
setFormData(prevData => ({ ...prevData, total: '' }));
}
}, [department, year, section, defaultStudentCounts]);
// Get current roster for validation
const rosterKey = `${department}-${year}-${section}`;
const currentRoster = studentList[rosterKey] || [];
const validRegNos = currentRoster.map(s => s.regNo.trim());
const handleInputChange = (field, value) => {
const numericValue = value === '' ? '' : parseInt(value, 10);
setFormData(prevData => ({
...prevData,
[field]: isNaN(numericValue) ? '' : numericValue,
}));
};
const handleSubmit = () => {
// Validate total = present + absent + onDuty
const total = Number(formData.total);
const present = Number(formData.present);
const absent = Number(formData.absent);
const onDuty = Number(formData.onDuty);
if (!total) {
showMessage("Please enter the total number of students.");
return;
}
if (present + absent + onDuty !== total) {
showMessage("Sum of Present, Absent, and On Duty must equal Total students.");
return;
}
// Validate absentNumbers
const absentNumbersArr = (formData.absentNumbers || '')
.split(',')
.map(s => s.trim())
.filter(s => s);
const invalidRegNos = absentNumbersArr.filter(regNo => !validRegNos.includes(regNo));
if (absentNumbersArr.length !== absent) {
showMessage(`Number of absent register numbers (${absentNumbersArr.length}) does not match Absent count (${absent}).`);
return;
}
if (invalidRegNos.length > 0) {
showMessage(`Invalid register numbers: ${invalidRegNos.join(', ')}. Please check the roster for valid register numbers.`);
return;
}
const newRecord = { ...formData, date: new Date(), year, section, department };
addAttendanceRecord(newRecord);
showMessage("Report Submitted!");
setFormData({ total: formData.total, present: '', absent: '', onDuty: '', absentNumbers: '' });
};
return (
navigate('Section', { department, year })}
/>
Enter the Absent Students Register Numbers (comma separated):
);
};
// 6. Roster Upload Screen
const RosterScreen = ({ studentList, setStudentList, showMessage, departments, attendanceRecords = [] }) => {
const [selectedDept, setSelectedDept] = useState(departments[0]);
const [selectedYear, setSelectedYear] = useState('First Year');
const [selectedSection, setSelectedSection] = useState('Section A');
const [newStudent, setNewStudent] = useState({ name: '', regNo: '' });
const [error, setError] = useState('');
const [filter, setFilter] = useState('');
const years = ['First Year', 'Second Year', 'Third Year', 'Fourth Year'];
const sections = ['Section A', 'Section B'];
const getRosterKey = () => `${selectedDept}-${selectedYear}-${selectedSection}`;
const currentRoster = studentList[getRosterKey()] || [];
// Filter students by name or regNo
const filteredRoster = currentRoster.filter(student =>
student.name.toLowerCase().includes(filter.toLowerCase()) ||
student.regNo.toLowerCase().includes(filter.toLowerCase())
);
// Calculate weekly/monthly percentage for each student
const now = new Date();
const weekAgo = new Date(now); weekAgo.setDate(now.getDate() - 7);
const monthAgo = new Date(now); monthAgo.setMonth(now.getMonth() - 1);
function calcStudentPercent(records, regNo) {
const studentRecords = records.filter(rec =>
rec.department === selectedDept &&
rec.year === selectedYear &&
rec.section === selectedSection &&
rec.absentNumbers &&
!rec.absentNumbers.split(',').map(s => s.trim()).includes(regNo)
);
const totalRecords = records.filter(rec =>
rec.department === selectedDept &&
rec.year === selectedYear &&
rec.section === selectedSection
);
const percent = totalRecords.length ? (studentRecords.length / totalRecords.length) * 100 : 0;
return percent.toFixed(1);
}
function calcStudentPercentPeriod(records, regNo, fromDate) {
const studentRecords = records.filter(rec =>
rec.department === selectedDept &&
rec.year === selectedYear &&
rec.section === selectedSection &&
new Date(rec.date) >= fromDate &&
rec.absentNumbers &&
!rec.absentNumbers.split(',').map(s => s.trim()).includes(regNo)
);
const totalRecords = records.filter(rec =>
rec.department === selectedDept &&
rec.year === selectedYear &&
rec.section === selectedSection &&
new Date(rec.date) >= fromDate
);
const percent = totalRecords.length ? (studentRecords.length / totalRecords.length) * 100 : 0;
return percent.toFixed(1);
}
const handleInputChange = (field, value) => {
setNewStudent(prev => ({ ...prev, [field]: value }));
};
const handleAddStudent = () => {
if (!newStudent.name || !newStudent.regNo) {
setError('Both name and register number are required.');
return;
}
setStudentList(prev => {
const key = getRosterKey();
const currentRoster = prev[key] || [];
const newRoster = [...currentRoster, newStudent];
return { ...prev, [key]: newRoster };
});
setNewStudent({ name: '', regNo: '' });
setError('');
showMessage(`Student ${newStudent.name} added to ${getRosterKey()}.`);
};
const handleFileUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result;
const lines = text.split('\n').filter(line => line.trim() !== '');
const newStudents = lines.map(line => {
const [regNo, name] = line.split(',');
return { regNo: regNo?.trim(), name: name?.trim() };
}).filter(s => s.name && s.regNo);
setStudentList(prev => {
const key = getRosterKey();
const currentRoster = prev[key] || [];
const newRoster = [...currentRoster, ...newStudents];
return { ...prev, [key]: newRoster };
});
showMessage(`${newStudents.length} students added to ${getRosterKey()} from file.`);
};
reader.readAsText(file);
}
};
const handleDownloadTemplate = () => {
const csvContent = "RegNo,Name\n12345,John Doe\n67890,Jane Smith";
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', 'student_roster_template.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
Select Class for Roster
setSelectedDept(e.target.value)}>
{departments.map(dept => {dept} )}
setSelectedYear(e.target.value)}>
{years.map(year => {year} )}
setSelectedSection(e.target.value)}>
{sections.map(section => {section} )}
setFilter(e.target.value)}
/>
Current Roster ({filteredRoster.length})
{filteredRoster.length > 0 ? (
filteredRoster.map((student, index) => (
{student.name}
{student.regNo}
Weekly: {calcStudentPercentPeriod(attendanceRecords, student.regNo, weekAgo)}%
Monthly: {calcStudentPercentPeriod(attendanceRecords, student.regNo, monthAgo)}%
))
) : (
No students match your filter.
)}
Upload Students via CSV/Excel
);
};
// Pie Chart Component
const PieChart = ({ data, radius = 50 }) => {
let total = data.reduce((sum, d) => sum + (d.value || 0), 0);
let cumulativeAngle = 0;
const sectors = data.map((d, index) => {
const value = d.value || 0;
const color = d.color || '#ccc';
const angle = total === 0 ? 0 : (value / total) * 360;
const startAngle = cumulativeAngle;
const endAngle = cumulativeAngle + angle;
cumulativeAngle = endAngle;
const x1 = radius * Math.cos(startAngle * Math.PI / 180);
const y1 = radius * Math.sin(startAngle * Math.PI / 180);
const x2 = radius * Math.cos(endAngle * Math.PI / 180);
const y2 = radius * Math.sin(endAngle * Math.PI / 180);
const largeArcFlag = angle > 180 ? 1 : 0;
const pathData = `
M 0 0
L ${x1} ${y1}
A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}
Z
`;
return (
);
});
return (
{sectors}
);
};
// Bar Chart Component
const BarChart = ({ data, labels }) => {
if (!data || data.length === 0) {
return No data available for the bar chart.
;
}
const maxVal = Math.max(...data.flatMap(d => [d.present, d.absent, d.onDuty]));
const svgHeight = 250;
const barWidth = 10;
const padding = 5;
const groupWidth = barWidth * 3 + padding;
const totalWidth = groupWidth * data.length + padding * (data.length - 1);
return (
Daily Attendance Count
{/* Y-axis */}
{/* Y-axis labels */}
{Math.ceil(maxVal)}
{Math.ceil(maxVal / 2)}
0
{data.map((d, i) => {
const x = i * groupWidth + padding + 30;
const presentHeight = (d.present / maxVal) * (svgHeight - 30);
const absentHeight = (d.absent / maxVal) * (svgHeight - 30);
const onDutyHeight = (d.onDuty / maxVal) * (svgHeight - 30);
return (
{/* Present bar */}
{/* Absent bar */}
{/* On Duty bar */}
{/* X-axis label (date) */}
{labels[i]}
);
})}
);
};
// 8. Dashboard Screen (replaces old UploadsScreen)
const DashboardScreen = ({ attendanceRecords = [], userProfile = {}, departments, showMessage }) => {
const [selectedDept, setSelectedDept] = useState(userProfile.department || departments[0]);
const [selectedYear, setSelectedYear] = useState('First Year');
const [selectedSection, setSelectedSection] = useState('Section A');
const years = ['First Year', 'Second Year', 'Third Year', 'Fourth Year'];
const sections = ['Section A', 'Section B'];
// Helper function to get aggregated data for charts
const getChartData = useCallback((records) => {
const total = records.reduce((sum, rec) => sum + (rec.total || 0), 0);
const present = records.reduce((sum, rec) => sum + (rec.present || 0), 0);
const absent = records.reduce((sum, rec) => sum + (rec.absent || 0), 0);
const onDuty = records.reduce((sum, rec) => sum + (rec.onDuty || 0), 0);
if (total === 0) return [];
return [
{ label: 'Present', value: (present / total) * 100, color: '#2ecc71' },
{ label: 'Absent', value: (absent / total) * 100, color: '#e74c3c' },
{ label: 'On Duty', value: (onDuty / total) * 100, color: '#f39c12' },
];
}, []);
// Helper function to get daily data for the bar chart
const getDailyData = useCallback((records) => {
const dailyData = records.reduce((acc, record) => {
const dateStr = new Date(record.date).toLocaleDateString();
if (!acc[dateStr]) {
acc[dateStr] = { present: 0, absent: 0, onDuty: 0 };
}
acc[dateStr].present += record.present || 0;
acc[dateStr].absent += record.absent || 0;
acc[dateStr].onDuty += record.onDuty || 0;
return acc;
}, {});
const labels = Object.keys(dailyData).sort((a, b) => new Date(a) - new Date(b));
const dataPoints = labels.map(label => ({
...dailyData[label],
date: label
}));
return { labels, dataPoints };
}, []);
// Helper: Get student list for the selected class
const getRosterKey = () => `${selectedDept}-${selectedYear}-${selectedSection}`;
const studentRoster = JSON.parse(localStorage.getItem('attendance_app_students') || '{}')[getRosterKey()] || [];
// Helper: Calculate attendance percentage for a student
function calcStudentPercent(records, regNo, fromDate = null) {
const filtered = records.filter(rec =>
rec.department === selectedDept &&
rec.year === selectedYear &&
rec.section === selectedSection &&
(!fromDate || new Date(rec.date) >= fromDate)
);
const totalDays = filtered.length;
if (totalDays === 0) return { present: '0.0', absent: '0.0' };
let presentDays = 0;
let absentDays = 0;
filtered.forEach(rec => {
const absents = (rec.absentNumbers || '').split(',').map(s => s.trim());
if (absents.includes(regNo)) {
absentDays += 1;
} else {
presentDays += 1;
}
});
return {
present: ((presentDays / totalDays) * 100).toFixed(1),
absent: ((absentDays / totalDays) * 100).toFixed(1)
};
}
// Date ranges
const now = new Date();
const weekAgo = new Date(now); weekAgo.setDate(now.getDate() - 7);
const monthAgo = new Date(now); monthAgo.setMonth(now.getMonth() - 1);
const semesterAgo = new Date(now); semesterAgo.setMonth(now.getMonth() - 6);
const filteredRecords = attendanceRecords.filter(record =>
record.department === selectedDept &&
record.year === selectedYear &&
record.section === selectedSection
);
const chartData = getChartData(filteredRecords);
const { labels: barChartLabels, dataPoints: barChartData } = getDailyData(filteredRecords);
const handleExportCSV = () => {
if (filteredRecords.length === 0) {
showMessage(`No data available to export for ${selectedDept} - ${selectedYear} ${selectedSection}.`);
return;
}
// Student-wise attendance summary
const studentHeaders = [
'RegNo', 'Name',
'Weekly Present (%)', 'Weekly Absent (%)',
'Monthly Present (%)', 'Monthly Absent (%)',
'Semester Present (%)', 'Semester Absent (%)'
];
const studentRows = studentRoster.map(student => {
const weekly = calcStudentPercent(filteredRecords, student.regNo, weekAgo);
const monthly = calcStudentPercent(filteredRecords, student.regNo, monthAgo);
const semester = calcStudentPercent(filteredRecords, student.regNo, semesterAgo);
return [
`"${student.regNo}"`,
`"${student.name}"`,
weekly.present,
weekly.absent,
monthly.present,
monthly.absent,
semester.present,
semester.absent
].join(',');
});
// Daily records (existing)
const headers = ['Date', 'Total Students', 'Present', 'Present (%)', 'Absent', 'Absent (%)', 'On Duty', 'On Duty (%)', 'Absentee Numbers'];
const csvRows = filteredRecords.map((record, index) => {
const rowIndex = index + 2;
return [
new Date(record.date).toLocaleDateString(),
record.total || 0,
record.present || 0,
`=C${rowIndex}/B${rowIndex}*100`,
record.absent || 0,
`=E${rowIndex}/B${rowIndex}*100`,
record.onDuty || 0,
`=G${rowIndex}/B${rowIndex}*100`,
(record.absentNumbers || '').replace(/"/g, '""')
].map(field => `"${field}"`).join(',');
});
// Combine sections
const csvContent = [
'Student-wise Attendance Summary',
studentHeaders.join(','),
...studentRows,
'',
'Daily Attendance Records',
headers.map(h => `"${h}"`).join(','),
...csvRows
].join('\n');
const filename = `${selectedDept}-${selectedYear}-${selectedSection}_detailed_report.csv`;
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
return (
Attendance Breakdown
Select Class to View Charts
setSelectedDept(e.target.value)}>
{departments.map(dept => {dept} )}
setSelectedYear(e.target.value)}>
{years.map(year => {year} )}
setSelectedSection(e.target.value)}>
{sections.map(section => {section} )}
{chartData.map((d, i) => (
{d.label}: {d.value.toFixed(1)}%
))}
Export CSV Report
);
};
// --- PLACEHOLDER SHARE SCREEN ---
const ShareScreen = ({ attendanceRecords, userProfile, departments, showMessage }) => {
const [selectedDept, setSelectedDept] = useState(userProfile?.department || departments[0]);
const [selectedYear, setSelectedYear] = useState('First Year');
const [selectedSection, setSelectedSection] = useState('Section A');
const [copied, setCopied] = useState(false);
const [csvCopied, setCsvCopied] = useState(false);
const summaryRef = React.useRef();
const years = ['First Year', 'Second Year', 'Third Year', 'Fourth Year'];
const sections = ['Section A', 'Section B'];
// Filter records for selected class
const filteredRecords = attendanceRecords.filter(record =>
record.department === selectedDept &&
record.year === selectedYear &&
record.section === selectedSection
);
// Calculate weekly/monthly percentages
const now = new Date();
const weekAgo = new Date(now); weekAgo.setDate(now.getDate() - 7);
const monthAgo = new Date(now); monthAgo.setMonth(now.getMonth() - 1);
const weeklyRecords = filteredRecords.filter(rec => new Date(rec.date) >= weekAgo);
const monthlyRecords = filteredRecords.filter(rec => new Date(rec.date) >= monthAgo);
function calcPercent(records) {
const total = records.reduce((sum, r) => sum + (r.total || 0), 0);
const present = records.reduce((sum, r) => sum + (r.present || 0), 0);
return total ? ((present / total) * 100).toFixed(1) : '0.0';
}
const weeklyPercent = calcPercent(weeklyRecords);
const monthlyPercent = calcPercent(monthlyRecords);
// Prepare summary text
const summaryText = filteredRecords.length > 0
? `Weekly Attendance: ${weeklyPercent}%\nMonthly Attendance: ${monthlyPercent}%\n\n` +
filteredRecords.map(rec =>
`Date: ${new Date(rec.date).toLocaleDateString()}\n` +
`Total: ${rec.total}\nPresent: ${rec.present}\nAbsent: ${rec.absent}\nOn Duty: ${rec.onDuty}\nAbsent Numbers: ${rec.absentNumbers}\n`
).join('\n---\n')
: 'No attendance records for this class.';
// Prepare CSV content
const headers = ['Date', 'Total', 'Present', 'Absent', 'On Duty', 'Absent Numbers', 'Weekly %', 'Monthly %'];
const csvRows = filteredRecords.map(rec => [
new Date(rec.date).toLocaleDateString(),
rec.total,
rec.present,
rec.absent,
rec.onDuty,
`"${(rec.absentNumbers || '').replace(/"/g, '""')}"`,
weeklyPercent,
monthlyPercent
].join(','));
const csvContent = [headers.join(','), ...csvRows].join('\n');
// Copy summary to clipboard
const handleCopy = () => {
navigator.clipboard.writeText(summaryText);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
showMessage('Attendance summary copied to clipboard!');
};
// Download CSV
const handleDownloadCSV = () => {
const filename = `${selectedDept}-${selectedYear}-${selectedSection}_attendance_share.csv`;
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
showMessage('CSV file downloaded!');
};
// WhatsApp share handler (CSV)
const handleWhatsAppShare = () => {
const text = encodeURIComponent(summaryText);
window.open(`https://wa.me/?text=${text}`, '_blank');
showMessage('Opening WhatsApp to share!');
};
const handleCopyCSV = () => {
navigator.clipboard.writeText(csvContent);
setCsvCopied(true);
setTimeout(() => setCsvCopied(false), 1500);
showMessage('CSV copied! Paste it in WhatsApp chat.');
};
return (
Select Class to Share
setSelectedDept(e.target.value)}>
{departments.map(dept => {dept} )}
setSelectedYear(e.target.value)}>
{years.map(year => {year} )}
setSelectedSection(e.target.value)}>
{sections.map(section => {section} )}
Attendance Summary:
Weekly Attendance: {weeklyPercent}%
Monthly Attendance: {monthlyPercent}%
{copied ? 'Copied!' : 'Copy Summary'}
Download CSV
๐ข
Share via WhatsApp (Text)
{csvCopied ? 'CSV Copied!' : 'Copy CSV for WhatsApp'}
);
};
// 9. Settings Screen
const SettingsScreen = ({ navigate, userProfile }) => {
if (!userProfile) return null; // Defensive check
const { name, position, employeeId, department } = userProfile;
const initial = name ? name.charAt(0).toUpperCase() : 'U';
const ProfileDetail = ({ icon, label, value }) => (
{label}
{value || 'Not Set'}
);
return (
{name || 'User Name'}
{position || 'Position'}
navigate('Auth')}>
Logout
);
};
// --- NAVIGATION SETUP ---
const MainApp = ({ navigate, params, attendanceRecords, addAttendanceRecord, userProfile, currentYear, currentSection, currentDepartment, defaultStudentCounts, studentList, setStudentList, showMessage, departments }) => {
const [activeTab, setActiveTab] = useState('Home');
useEffect(() => {
if (params && params.screen) {
setActiveTab(params.screen);
}
}, [params]);
const renderScreen = () => {
switch(activeTab) {
case 'Home':
return ;
case 'Attendance':
return ;
case 'Dashboard':
return ;
case 'Roster':
return ;
case 'Share':
return ;
case 'Settings':
return ;
default:
return ;
}
}
const tabs = ['Home', 'Dashboard', 'Roster', 'Share', 'Settings'];
const tabIcons = { Home: 'home', Dashboard: 'chart', Roster: 'people', Share: 'share', Settings: 'settings' };
return (
{renderScreen()}
{tabs.map(tab => (
setActiveTab(tab)}>
))}
);
};
const defaultStudentCounts = {
'Aeronautical Engineering-First Year-Section A': 65,
'Aeronautical Engineering-First Year-Section B': 63,
'Computer Science & Engineering-Second Year-Section A': 70,
'Computer Science & Engineering-Second Year-Section B': 68,
'Mechanical Engineering-Third Year-Section A': 62,
'Mechanical Engineering-Third Year-Section B': 60,
'Electrical & Electronics Engineering-Fourth Year-Section A': 55,
'Artificial Intelligence & Data Science-First Year-Section A': 72,
};
export default function App() {
const [page, setPage] = useState('Auth');
const [params, setParams] = useState({});
const [attendanceRecords, setAttendanceRecords] = useState([]);
const [studentList, setStudentList] = useState({});
const [userProfile, setUserProfile] = useState(null);
const [currentDepartment, setCurrentDepartment] = useState('');
const [currentYear, setCurrentYear] = useState('');
const [currentSection, setCurrentSection] = useState('');
const [message, setMessage] = useState('');
const showMessage = (msg) => {
setMessage(msg);
};
const hideMessage = () => {
setMessage('');
};
// Load data from local storage on initial app load
useEffect(() => {
const savedRecordsJSON = localStorage.getItem('attendance_app_records');
if (savedRecordsJSON) {
try {
const savedRecords = JSON.parse(savedRecordsJSON);
const recordsWithDates = savedRecords.map(rec => ({ ...rec, date: new Date(rec.date) }));
setAttendanceRecords(recordsWithDates);
} catch (e) {
console.error("Error parsing attendance records from localStorage", e);
setAttendanceRecords([]);
}
}
const savedStudentsJSON = localStorage.getItem('attendance_app_students');
if (savedStudentsJSON) {
try {
const savedStudents = JSON.parse(savedStudentsJSON);
setStudentList(savedStudents);
} catch (e) {
console.error("Error parsing student list from localStorage", e);
setStudentList({});
}
}
}, []);
// Save attendance records to local storage whenever they are updated
useEffect(() => {
try {
localStorage.setItem('attendance_app_records', JSON.stringify(attendanceRecords));
} catch (e) {
console.error("Error saving attendance records to localStorage", e);
}
}, [attendanceRecords]);
// Save student list to local storage whenever it's updated
useEffect(() => {
try {
localStorage.setItem('attendance_app_students', JSON.stringify(studentList));
} catch (e) {
console.error("Error saving student list to localStorage", e);
}
}, [studentList]);
const addAttendanceRecord = (newRecord) => {
setAttendanceRecords(prevRecords => [...prevRecords, newRecord]);
};
const navigate = (newPage, newParams = {}) => {
if (newParams.department) setCurrentDepartment(newParams.department);
if (newParams.year) setCurrentYear(newParams.year);
if (newParams.section) setCurrentSection(newParams.section);
setPage(newPage);
setParams(newParams);
};
const departments = [
'Aeronautical Engineering', 'Artificial Intelligence & Data Science', 'Computer Science & Engineering',
'Mechanical Engineering', 'Civil Engineering', 'Electrical & Electronics Engineering',
'Electronics & Communication Engineering', 'Information Technology', 'Chemical Engineering',
'Mechatronics','Computer Science and Bussiness Science'
];
const renderPage = () => {
if (!userProfile && page !== 'Auth') {
return ;
}
switch (page) {
case 'Auth':
return ;
case 'MainApp':
return ;
case 'Year':
return ;
case 'Section':
return ;
default:
return ;
}
};
return (
<>
{renderPage()}
>
);
}
// --- STYLES ---
const styles = {
authContainer: { height: '100vh', backgroundColor: '#fff', position: 'relative' },
authTopBg: { height: '50%', backgroundColor: '#8e44ad' },
authContent: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', },
authCard: { width: '85%', maxWidth: 400, backgroundColor: '#fff', borderRadius: 20, padding: 25, display: 'flex', flexDirection: 'column', alignItems: 'center', boxShadow: '0 5px 15px rgba(0,0,0,0.3)', },
authTitle: { fontSize: 24, fontWeight: 'bold', marginBottom: 20, margin: 0 },
input: { width: '100%', boxSizing: 'border-box', backgroundColor: '#f0f0f0', borderRadius: 15, padding: 15, marginBottom: 15, fontSize: 16, border: '1px solid #eee', appearance: 'none', },
mainButton: { width: '100%', backgroundColor: '#fff', borderRadius: 15, padding: 15, alignItems: 'center', marginTop: 10, border: '1px solid #ddd', boxShadow: '0 2px 5px rgba(0,0,0,0.2)', cursor: 'pointer', },
mainButtonText: { fontSize: 18, fontWeight: 'bold', color: '#333' },
secondaryButton: { width: '100%', backgroundColor: '#8e44ad', borderRadius: 15, padding: 15, alignItems: 'center', border: 'none', cursor: 'pointer', marginTop: 10, },
secondaryButtonText: { fontSize: 18, fontWeight: 'bold', color: '#fff' },
logoContainer: { display: 'flex', alignItems: 'center', marginTop: 20, },
logoImage: { width: 150, height: 70, marginRight: 15, borderRadius: 10, },
logoTextContainer: { display: 'flex', flexDirection: 'column' },
errorText: { color: 'red', marginBottom: 10, },
screenContainer: { display: 'flex', flexDirection: 'column', height: '100%', backgroundColor: '#f5f5f5' },
headerContainer: { height: 180, position: 'relative', display: 'flex', justifyContent: 'center', alignItems: 'center', flexShrink: 0, },
headerTitle: { fontSize: 22, color: '#fff', fontWeight: 'bold', textAlign: 'center', marginTop: 30, lineHeight: 1.4, position: 'relative', padding: '0 20px', margin: 0, whiteSpace: 'pre-wrap', },
backButton: { position: 'absolute', top: 45, left: 15, background: 'transparent', border: 'none', padding: '10px', cursor: 'pointer', zIndex: 10, },
sectionTitle: { fontSize: 20, fontWeight: 'bold', margin: '20px 20px 10px 20px', },
deptButton: { backgroundColor: '#fff', display: 'flex', flexDirection: 'row', alignItems: 'center', padding: '15px 20px', borderRadius: 15, margin: '8px 20px', boxShadow: '0 1px 5px rgba(0,0,0,0.1)', border: 'none', width: 'calc(100% - 40px)', textAlign: 'left', cursor: 'pointer', },
deptButtonText: { fontSize: 16, marginLeft: 15, whiteSpace: 'normal', flex: 1, },
yearGrid: { display: 'flex', flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-around', padding: '0 10px', },
yearCard: { width: '45%', height: 150, backgroundColor: '#8e44ad', borderRadius: 20, display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', marginBottom: 20, border: 'none', color: '#fff', cursor: 'pointer', },
yearCardNumber: { fontSize: 28, fontWeight: 'bold', },
yearCardText: { fontSize: 18, },
attendanceForm: { backgroundColor: '#fff', borderRadius: 15, padding: 20, },
attendanceRow: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15, },
attendanceInput: { width: 80, height: 40, backgroundColor: '#f0f0f0', borderRadius: 8, border: '1px solid #ddd', textAlign: 'center', fontSize: 16, },
largeInput: { backgroundColor: '#f0f0f0', borderRadius: 8, height: 100, width: '100%', boxSizing: 'border-box', border: '1px solid #ddd', padding: 10, fontFamily: 'sans-serif', fontSize: 14, },
submitButton: { backgroundColor: '#8e44ad', borderRadius: 15, padding: 15, display: 'flex', justifyContent: 'center', alignItems: 'center', marginTop: 20, border: 'none', color: '#fff', width: '100%', cursor: 'pointer', },
submitButtonText: { fontSize: 16, fontWeight: 'bold', },
dashboardContent: { flex: 1, padding: 20, display: 'flex', flexDirection: 'column', alignItems: 'center', overflowY: 'auto' },
filterContainer: { display: 'flex', justifyContent: 'space-between', width: '100%', marginBottom: 20, },
filterButton: { flex: 1, padding: '10px 5px', margin: '0 5px', borderRadius: 10, border: '1px solid #8e44ad', backgroundColor: '#fff', color: '#8e44ad', cursor: 'pointer', fontSize: 14, },
filterButtonActive: { flex: 1, padding: '10px 5px', margin: '0 5px', borderRadius: 10, border: '1px solid #8e44ad', backgroundColor: '#8e44ad', color: '#fff', cursor: 'pointer', fontSize: 14, },
progressCard: { backgroundColor: '#fff', borderRadius: 20, padding: 20, width: '100%', boxSizing: 'border-box', display: 'flex', alignItems: 'center', marginBottom: 20, flexDirection: 'row', justifyContent: 'space-around', },
progressCircleContainer: { position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', },
progressText: { position: 'absolute', fontSize: 24, fontWeight: 'bold', margin: 0, },
progressLabel: { fontSize: 16, fontWeight: '500', marginTop: 10, margin: 0, textAlign: 'center' },
shareButtonsContainer: { display: 'flex', justifyContent: 'space-between', width: '100%', marginTop: 10, gap: '10px' },
shareButton: { borderRadius: 15, padding: 15, display: 'flex', justifyContent: 'center', alignItems: 'center', border: 'none', color: '#fff', cursor: 'pointer', flex: 1, fontSize: '14px' },
settingsContent: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '20px', },
profileCard: { width: '100%', backgroundColor: '#fff', borderRadius: 20, padding: 20, boxSizing: 'border-box', boxShadow: '0 2px 10px rgba(0,0,0,0.1)', },
profileCardHeader: { display: 'flex', alignItems: 'center', borderBottom: '1px solid #eee', paddingBottom: 20, },
avatar: { width: 70, height: 70, borderRadius: 35, backgroundColor: '#8e44ad', display: 'flex', justifyContent: 'center', alignItems: 'center', flexShrink: 0, },
avatarLetter: { color: '#fff', fontSize: 32, fontWeight: 'bold', margin: 0, },
profileName: { fontSize: 22, fontWeight: 'bold', margin: 0, },
profilePosition: { color: 'grey', margin: '0', fontSize: 16, },
profileDetailsContainer: { paddingTop: 20, },
profileDetailRow: { display: 'flex', alignItems: 'center', marginBottom: 15, },
profileDetailTextContainer: { marginLeft: 15, },
profileDetailLabel: { margin: 0, color: 'grey', fontSize: 14, },
profileDetailValue: { margin: 0, fontSize: 16, fontWeight: '500', },
logoutButton: { backgroundColor: '#8e44ad', borderRadius: 15, padding: 15, display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', marginTop: 'auto', border: 'none', cursor: 'pointer', },
logoutButtonText: { color: '#fff', fontSize: 16, fontWeight: 'bold', },
tabBar: { display: 'flex', height: 60, borderTopLeftRadius: 20, borderTopRightRadius: 20, backgroundColor: '#fff', boxShadow: '0 -2px 10px rgba(0,0,0,0.1)', flexShrink: 0, },
tabButton: { flex: 1, background: 'none', border: 'none', cursor: 'pointer', },
chartSelectionForm: { backgroundColor: '#fff', borderRadius: 15, padding: 20, marginBottom: 20, boxShadow: '0 2px 10px rgba(0,0,0,0.1)' },
rosterContent: { flex: 1, padding: 20, overflowY: 'auto' },
rosterForm: { backgroundColor: '#fff', padding: 20, borderRadius: 15, marginBottom: 20, boxShadow: '0 2px 10px rgba(0,0,0,0.1)' },
rosterUploadSection: { backgroundColor: '#fff', padding: 20, borderRadius: 15, marginBottom: 20, boxShadow: '0 2px 10px rgba(0,0,0,0.1)' },
uploadButton: { display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 15, backgroundColor: '#8e44ad', color: '#fff', borderRadius: 15, cursor: 'pointer', fontWeight: 'bold', },
uploadButtonText: { marginLeft: 10, fontSize: 16 },
rosterList: { backgroundColor: '#fff', borderRadius: 15, padding: 20, boxShadow: '0 2px 10px rgba(0,0,0,0.1)' },
rosterItem: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 0', borderBottom: '1px solid #eee' },
emptyListMessage: { textAlign: 'center', color: '#777', fontStyle: 'italic', padding: 20 },
messageBoxOverlay: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 1000 },
messageBox: { backgroundColor: '#fff', padding: 20, borderRadius: 15, boxShadow: '0 5px 15px rgba(0,0,0.3)', maxWidth: 300, textAlign: 'center' },
messageText: { fontSize: 16, marginBottom: 20 },
messageButton: { backgroundColor: '#8e44ad', color: '#fff', border: 'none', padding: '10px 20px', borderRadius: 10, cursor: 'pointer' },
pieChartContainer: { display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', maxWidth: 400, margin: '20px auto', position: 'relative', padding: 20, backgroundColor: '#fff', borderRadius: 15, boxShadow: '0 2px 10px rgba(0,0,0,0.1)' },
pieChart: { width: '60%', height: '100%' },
pieChartLegend: { width: '40%', paddingLeft: 20 },
pieChartLegendItem: { display: 'flex', alignItems: 'center', marginBottom: 5 },
pieChartLegendColor: { width: 15, height: 15, borderRadius: '50%', marginRight: 10 },
barChartContainer: { backgroundColor: '#fff', borderRadius: 15, padding: 20, width: '100%', boxSizing: 'border-box', boxShadow: '0 2px 10px rgba(0,0,0,0.1)', marginTop: 20 },
chartTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 10, textAlign: 'center' },
barChartInnerContainer: { overflowX: 'auto', width: '100%' }
};