Spaces:
Running
Running
| 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 <span style={{ fontSize: size, color, lineHeight: 1 }}>{icons[name] || 'β'}</span>; | |
| }; | |
| // --- CUSTOM HEADER COMPONENTS --- | |
| // A header with a wave-like SVG background. | |
| const WaveHeader = ({ title }) => ( | |
| <div style={styles.headerContainer}> | |
| <svg height="100%" width="100%" style={{ position: 'absolute', top: 0, left: 0 }} viewBox="0 0 400 180" preserveAspectRatio="none"> | |
| <path | |
| d="M0,0 L0,130 Q100,180 200,130 T400,140 L400,0 Z" | |
| fill="#8e44ad" | |
| /> | |
| </svg> | |
| <p style={styles.headerTitle}>{title}</p> | |
| </div> | |
| ); | |
| // A simple back button component. | |
| const BackButton = ({ onClick }) => ( | |
| <button onClick={onClick} style={styles.backButton}> | |
| <Icon name="arrow-left" size={24} color="#fff" /> | |
| </button> | |
| ); | |
| // A header component that includes a back button. | |
| const HeaderWithBack = ({ title, onBackPress }) => ( | |
| <div style={{position: 'relative'}}> | |
| <WaveHeader title={title} /> | |
| {onBackPress && <BackButton onClick={onBackPress} />} | |
| </div> | |
| ); | |
| // --- CUSTOM MESSAGE BOX COMPONENT --- | |
| const MessageBox = ({ message, onClose }) => { | |
| if (!message) return null; | |
| return ( | |
| <div style={styles.messageBoxOverlay}> | |
| <div style={styles.messageBox}> | |
| <p style={styles.messageText}>{message}</p> | |
| <button onClick={onClose} style={styles.messageButton}>OK</button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // --- 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 ( | |
| <div style={styles.authContainer}> | |
| <div style={styles.authTopBg} /> | |
| <div style={styles.authContent}> | |
| <div style={styles.authCard}> | |
| <p style={styles.authTitle}>{isLogin ? 'Login' : 'Register'}</p> | |
| {error && <p style={styles.errorText}>{error}</p>} | |
| <input style={styles.input} placeholder="Username" value={formData.username} onChange={e => handleInputChange('username', e.target.value)} /> | |
| {!isLogin && ( | |
| <> | |
| <input style={styles.input} placeholder="Full Name" value={formData.fullName} onChange={e => handleInputChange('fullName', e.target.value)} /> | |
| <input style={styles.input} placeholder="Employee ID" value={formData.employeeId} onChange={e => handleInputChange('employeeId', e.target.value)} /> | |
| <select style={styles.input} value={formData.department} onChange={e => handleInputChange('department', e.target.value)}> | |
| {departments.map(dept => <option key={dept} value={dept}>{dept}</option>)} | |
| </select> | |
| <select style={styles.input} value={formData.role} onChange={e => handleInputChange('role', e.target.value)}> | |
| <option value="Staff">Staff</option> | |
| <option value="HOD">HOD</option> | |
| <option value="Class Advisor">Class Advisor</option> | |
| <option value="Admin">Admin</option> | |
| </select> | |
| </> | |
| )} | |
| <input style={styles.input} placeholder="Password" type="password" value={formData.password} onChange={e => handleInputChange('password', e.target.value)} /> | |
| {!isLogin && <input style={styles.input} placeholder="Retype Password" type="password" value={formData.confirmPassword} onChange={e => handleInputChange('confirmPassword', e.target.value)} />} | |
| <button style={styles.mainButton} onClick={handleAuthAction}> | |
| <span style={styles.mainButtonText}>{isLogin ? 'Login' : 'Register'}</span> | |
| </button> | |
| <button style={styles.secondaryButton} onClick={() => { setIsLogin(!isLogin); setError(''); }}> | |
| <span style={styles.secondaryButtonText}>{isLogin ? 'Switch to Register' : 'Switch to Login'}</span> | |
| </button> | |
| </div> | |
| <div style={styles.logoContainer}> | |
| <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT2R7bHsdVlhJda1oUOZ1NPc7lvacTuGPvHgg&s" style={styles.logoImage} alt="College Logo" /> | |
| <div style={styles.logoTextContainer}> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // 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 ( | |
| <div style={styles.screenContainer}> | |
| <WaveHeader title="Attendance Management Application" /> | |
| {/* Dashboard chart above department list */} | |
| <div style={styles.pieChartContainer}> | |
| <div style={{ width: '60%' }}> | |
| <PieChart data={chartData} /> | |
| </div> | |
| <div style={styles.pieChartLegend}> | |
| {chartData.length > 0 ? chartData.map(d => ( | |
| <div key={d.label} style={styles.pieChartLegendItem}> | |
| <span style={{ backgroundColor: d.color, ...styles.pieChartLegendColor }}></span> | |
| <p>{d.label}: {d.value.toFixed(1)}%</p> | |
| </div> | |
| )) : ( | |
| <p style={styles.emptyListMessage}>No attendance data for this class to display a chart.</p> | |
| )} | |
| </div> | |
| </div> | |
| {/* Class selection for chart */} | |
| <div style={{ display: 'flex', gap: 10, margin: '0 20px 20px 20px' }}> | |
| <select style={styles.input} value={selectedDept} onChange={e => setSelectedDept(e.target.value)}> | |
| {departments.map(dept => <option key={dept} value={dept}>{dept}</option>)} | |
| </select> | |
| <select style={styles.input} value={selectedYear} onChange={e => setSelectedYear(e.target.value)}> | |
| {years.map(year => <option key={year} value={year}>{year}</option>)} | |
| </select> | |
| <select style={styles.input} value={selectedSection} onChange={e => setSelectedSection(e.target.value)}> | |
| {sections.map(section => <option key={section} value={section}>{section}</option>)} | |
| </select> | |
| </div> | |
| {/* Department list */} | |
| <p style={styles.sectionTitle}>Department</p> | |
| <div style={{ flex: 1, overflowY: 'auto', paddingBottom: 20 }}> | |
| {departmentData.map((dept, index) => ( | |
| <button key={index} style={styles.deptButton} onClick={() => navigate('Year', { department: dept.name })}> | |
| <Icon name={dept.icon} size={28} color="#8e44ad" /> | |
| <span style={styles.deptButtonText}>{dept.name}</span> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // 3. Year Selection Screen | |
| const YearScreen = ({ navigate, params }) => { | |
| const years = ['First Year', 'Second Year', 'Third Year', 'Fourth Year']; | |
| return ( | |
| <div style={styles.screenContainer}> | |
| <HeaderWithBack | |
| title="Select Year" | |
| onBackPress={() => navigate('MainApp', { screen: 'Home' })} | |
| /> | |
| <p style={styles.sectionTitle}>{params.department}</p> | |
| <div style={styles.yearGrid}> | |
| {years.map((year, index) => ( | |
| <button key={index} style={styles.yearCard} onClick={() => navigate('Section', { ...params, year: year })}> | |
| <span style={styles.yearCardNumber}>{index + 1}</span> | |
| <span style={styles.yearCardText}>{year}</span> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // 4. Section Selection Screen | |
| const SectionScreen = ({ navigate, params }) => { | |
| const sections = ['Section A', 'Section B']; | |
| const { year, department } = params; | |
| return ( | |
| <div style={styles.screenContainer}> | |
| <HeaderWithBack | |
| title="Select Section" | |
| onBackPress={() => navigate('Year', { department })} | |
| /> | |
| <p style={styles.sectionTitle}>{`${department} - ${year}`}</p> | |
| <div style={styles.yearGrid}> | |
| {sections.map((section, index) => ( | |
| <button key={index} style={styles.yearCard} onClick={() => navigate('MainApp', { ...params, screen: 'Attendance', year, section })}> | |
| <span style={styles.yearCardText}>{section}</span> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // 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 ( | |
| <div style={styles.screenContainer}> | |
| <HeaderWithBack | |
| title={`${department}\n${year} - ${section}`} | |
| onBackPress={() => navigate('Section', { department, year })} | |
| /> | |
| <div style={{ padding: 20, overflowY: 'auto' }}> | |
| <div style={styles.attendanceForm}> | |
| <div style={styles.attendanceRow}> | |
| <p>Total Number of Students:</p> | |
| <input type="number" style={styles.attendanceInput} value={formData.total} onChange={(e) => handleInputChange('total', e.target.value)} /> | |
| </div> | |
| <div style={styles.attendanceRow}> | |
| <p>Total Number of Students Present:</p> | |
| <input type="number" style={styles.attendanceInput} value={formData.present} onChange={(e) => handleInputChange('present', e.target.value)} /> | |
| </div> | |
| <div style={styles.attendanceRow}> | |
| <p>Total Number of Students Absents:</p> | |
| <input type="number" style={styles.attendanceInput} value={formData.absent} onChange={(e) => handleInputChange('absent', e.target.value)} /> | |
| </div> | |
| <div style={styles.attendanceRow}> | |
| <p>Total Number of Students on OD:</p> | |
| <input type="number" style={styles.attendanceInput} value={formData.onDuty} onChange={(e) => handleInputChange('onDuty', e.target.value)} /> | |
| </div> | |
| <p style={{marginTop: 20, marginBottom: 10}}>Enter the Absent Students Register Numbers (comma separated):</p> | |
| <textarea | |
| style={styles.largeInput} | |
| rows={4} | |
| value={formData.absentNumbers} | |
| onChange={(e) => setFormData({...formData, absentNumbers: e.target.value})} | |
| /> | |
| {/* Show valid register numbers for this class */} | |
| <div style={{marginTop: 10, marginBottom: 10, fontSize: 13, color: '#555'}}> | |
| <strong>Valid Register Numbers for this class:</strong> | |
| <div style={{marginTop: 5, wordBreak: 'break-all'}}> | |
| {currentRoster.length > 0 ? ( | |
| <ul style={{paddingLeft: 18, margin: 0}}> | |
| {currentRoster.map((student, idx) => ( | |
| <li key={idx}> | |
| <span style={{fontWeight: 500}}>{student.regNo}</span> - {student.name} | |
| </li> | |
| ))} | |
| </ul> | |
| ) : ( | |
| <span style={{color: 'red'}}>No roster uploaded for this class.</span> | |
| )} | |
| </div> | |
| </div> | |
| <button style={styles.submitButton} onClick={handleSubmit}> | |
| <span style={styles.submitButtonText}>Submit</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // 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 ( | |
| <div style={styles.screenContainer}> | |
| <WaveHeader title="Student Roster" /> | |
| <div style={styles.rosterContent}> | |
| <p style={styles.sectionTitle}>Select Class for Roster</p> | |
| <div style={{...styles.filterContainer, marginBottom: 20}}> | |
| <select style={{...styles.input, flex: 1}} value={selectedDept} onChange={e => setSelectedDept(e.target.value)}> | |
| {departments.map(dept => <option key={dept} value={dept}>{dept}</option>)} | |
| </select> | |
| <select style={{...styles.input, flex: 1}} value={selectedYear} onChange={e => setSelectedYear(e.target.value)}> | |
| {years.map(year => <option key={year} value={year}>{year}</option>)} | |
| </select> | |
| <select style={{...styles.input, flex: 1}} value={selectedSection} onChange={e => setSelectedSection(e.target.value)}> | |
| {sections.map(section => <option key={section} value={section}>{section}</option>)} | |
| </select> | |
| </div> | |
| <input | |
| style={{ ...styles.input, marginBottom: 10 }} | |
| placeholder="Filter by Name or Register Number" | |
| value={filter} | |
| onChange={e => setFilter(e.target.value)} | |
| /> | |
| <p style={{...styles.sectionTitle, marginTop: 30}}>Current Roster ({filteredRoster.length})</p> | |
| <div style={styles.rosterList}> | |
| {filteredRoster.length > 0 ? ( | |
| filteredRoster.map((student, index) => ( | |
| <div key={index} style={styles.rosterItem}> | |
| <span>{student.name}</span> | |
| <span style={{color: 'grey'}}>{student.regNo}</span> | |
| <span style={{color: '#2ecc71', marginLeft: 10, fontSize: 13}}> | |
| Weekly: {calcStudentPercentPeriod(attendanceRecords, student.regNo, weekAgo)}% | |
| </span> | |
| <span style={{color: '#8e44ad', marginLeft: 10, fontSize: 13}}> | |
| Monthly: {calcStudentPercentPeriod(attendanceRecords, student.regNo, monthAgo)}% | |
| </span> | |
| </div> | |
| )) | |
| ) : ( | |
| <p style={styles.emptyListMessage}>No students match your filter.</p> | |
| )} | |
| </div> | |
| <p style={{...styles.sectionTitle, marginTop: 30}}>Upload Students via CSV/Excel</p> | |
| <div style={styles.rosterUploadSection}> | |
| <p>Format should be: RegNo,Name</p> | |
| <button style={{...styles.secondaryButton, marginBottom: 10}} onClick={handleDownloadTemplate}> | |
| <span style={styles.secondaryButtonText}>Download Template CSV</span> | |
| </button> | |
| <label htmlFor="csv-upload" style={styles.uploadButton}> | |
| <Icon name="upload" color="#fff" size={20} /> | |
| <span style={styles.uploadButtonText}>Upload CSV File</span> | |
| <input | |
| id="csv-upload" | |
| type="file" | |
| accept=".csv, .xlsx" | |
| style={{ display: 'none' }} | |
| onChange={handleFileUpload} | |
| /> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // 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 ( | |
| <path | |
| key={index} | |
| d={pathData} | |
| fill={color} | |
| /> | |
| ); | |
| }); | |
| return ( | |
| <svg | |
| width="100%" | |
| height="100%" | |
| viewBox="-50 -50 100 100" | |
| style={{ transform: 'rotate(-90deg)' }} | |
| > | |
| {sectors} | |
| </svg> | |
| ); | |
| }; | |
| // Bar Chart Component | |
| const BarChart = ({ data, labels }) => { | |
| if (!data || data.length === 0) { | |
| return <p style={styles.emptyListMessage}>No data available for the bar chart.</p>; | |
| } | |
| 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 ( | |
| <div style={styles.chartContainer}> | |
| <p style={styles.chartTitle}>Daily Attendance Count</p> | |
| <div style={styles.barChartInnerContainer}> | |
| <svg width="100%" height={svgHeight} viewBox={`0 0 ${totalWidth + 50} ${svgHeight}`} preserveAspectRatio="xMinYMid meet"> | |
| {/* Y-axis */} | |
| <line x1="30" y1="15" x2="30" y2={svgHeight - 15} stroke="#ddd" /> | |
| {/* Y-axis labels */} | |
| <text x="25" y="15" fontSize="10" fill="#555" textAnchor="end">{Math.ceil(maxVal)}</text> | |
| <text x="25" y={svgHeight / 2} fontSize="10" fill="#555" textAnchor="end">{Math.ceil(maxVal / 2)}</text> | |
| <text x="25" y={svgHeight - 15} fontSize="10" fill="#555" textAnchor="end">0</text> | |
| {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 ( | |
| <g key={i} transform={`translate(${x}, 0)`}> | |
| {/* Present bar */} | |
| <rect | |
| x="0" | |
| y={svgHeight - presentHeight - 15} | |
| width={barWidth} | |
| height={presentHeight} | |
| fill="#2ecc71" | |
| rx="2" ry="2" | |
| /> | |
| {/* Absent bar */} | |
| <rect | |
| x={barWidth + padding/2} | |
| y={svgHeight - absentHeight - 15} | |
| width={barWidth} | |
| height={absentHeight} | |
| fill="#e74c3c" | |
| rx="2" ry="2" | |
| /> | |
| {/* On Duty bar */} | |
| <rect | |
| x={barWidth * 2 + padding} | |
| y={svgHeight - onDutyHeight - 15} | |
| width={barWidth} | |
| height={onDutyHeight} | |
| fill="#f39c12" | |
| rx="2" ry="2" | |
| /> | |
| {/* X-axis label (date) */} | |
| <text | |
| x={groupWidth/2} | |
| y={svgHeight + 5} | |
| fontSize="10" | |
| fill="#555" | |
| textAnchor="middle" | |
| transform={`rotate(45, ${x + groupWidth/2}, ${svgHeight + 5})`} | |
| >{labels[i]}</text> | |
| </g> | |
| ); | |
| })} | |
| </svg> | |
| </div> | |
| <div style={styles.chartLegend}> | |
| <div style={styles.pieChartLegendItem}> | |
| <span style={{ backgroundColor: '#2ecc71', ...styles.pieChartLegendColor }}></span> | |
| <p>Present</p> | |
| </div> | |
| <div style={styles.pieChartLegendItem}> | |
| <span style={{ backgroundColor: '#e74c3c', ...styles.pieChartLegendColor }}></span> | |
| <p>Absent</p> | |
| </div> | |
| <div style={styles.pieChartLegendItem}> | |
| <span style={{ backgroundColor: '#f39c12', ...styles.pieChartLegendColor }}></span> | |
| <p>On Duty</p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // 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 ( | |
| <div style={styles.screenContainer}> | |
| <WaveHeader title="Dashboard & Reports" /> | |
| <div style={styles.dashboardContent}> | |
| <p style={styles.sectionTitle}>Attendance Breakdown</p> | |
| <div style={styles.chartSelectionForm}> | |
| <p>Select Class to View Charts</p> | |
| <select style={styles.input} value={selectedDept} onChange={e => setSelectedDept(e.target.value)}> | |
| {departments.map(dept => <option key={dept} value={dept}>{dept}</option>)} | |
| </select> | |
| <select style={styles.input} value={selectedYear} onChange={e => setSelectedYear(e.target.value)}> | |
| {years.map(year => <option key={year} value={year}>{year}</option>)} | |
| </select> | |
| <select style={styles.input} value={selectedSection} onChange={e => setSelectedSection(e.target.value)}> | |
| {sections.map(section => <option key={section} value={section}>{section}</option>)} | |
| </select> | |
| </div> | |
| <div style={styles.chartContainer}> | |
| <div style={styles.pieChartContainer}> | |
| <PieChart data={chartData} /> | |
| </div> | |
| <div style={styles.chartLegend}> | |
| {chartData.map((d, i) => ( | |
| <div key={i} style={styles.pieChartLegendItem}> | |
| <span style={{ backgroundColor: d.color, ...styles.pieChartLegendColor }}></span> | |
| <p>{d.label}: {d.value.toFixed(1)}%</p> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <div style={styles.barChartContainer}> | |
| <BarChart data={barChartData} labels={barChartLabels} /> | |
| </div> | |
| <button style={styles.exportButton} onClick={handleExportCSV}> | |
| <Icon name="download" size={20} color="#fff" /> | |
| <span style={styles.exportButtonText}>Export CSV Report</span> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // --- 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 ( | |
| <div style={styles.screenContainer}> | |
| <WaveHeader title="Share Attendance" /> | |
| <div style={{ padding: 20 }}> | |
| <p style={styles.sectionTitle}>Select Class to Share</p> | |
| <div style={{ display: 'flex', gap: 10, marginBottom: 20 }}> | |
| <select style={styles.input} value={selectedDept} onChange={e => setSelectedDept(e.target.value)}> | |
| {departments.map(dept => <option key={dept} value={dept}>{dept}</option>)} | |
| </select> | |
| <select style={styles.input} value={selectedYear} onChange={e => setSelectedYear(e.target.value)}> | |
| {years.map(year => <option key={year} value={year}>{year}</option>)} | |
| </select> | |
| <select style={styles.input} value={selectedSection} onChange={e => setSelectedSection(e.target.value)}> | |
| {sections.map(section => <option key={section} value={section}>{section}</option>)} | |
| </select> | |
| </div> | |
| <div ref={summaryRef} style={{ background: '#fff', borderRadius: 10, padding: 15, marginBottom: 20, boxShadow: '0 2px 8px rgba(0,0,0,0.08)' }}> | |
| <p style={{ fontWeight: 'bold', marginBottom: 10 }}>Attendance Summary:</p> | |
| <p><strong>Weekly Attendance:</strong> {weeklyPercent}%</p> | |
| <p><strong>Monthly Attendance:</strong> {monthlyPercent}%</p> | |
| <textarea | |
| style={{ width: '100%', height: 120, borderRadius: 8, border: '1px solid #eee', padding: 10, fontFamily: 'monospace', fontSize: 14, resize: 'vertical' }} | |
| value={summaryText} | |
| readOnly | |
| /> | |
| </div> | |
| <div style={{ display: 'flex', gap: 10 }}> | |
| <button style={{ ...styles.submitButton, flex: 1 }} onClick={handleCopy}> | |
| <Icon name="share" size={20} color="#fff" /> | |
| <span style={{ marginLeft: 8 }}>{copied ? 'Copied!' : 'Copy Summary'}</span> | |
| </button> | |
| <button style={{ ...styles.submitButton, flex: 1, backgroundColor: '#2ecc71' }} onClick={handleDownloadCSV}> | |
| <Icon name="download" size={20} color="#fff" /> | |
| <span style={{ marginLeft: 8 }}>Download CSV</span> | |
| </button> | |
| <button style={{ ...styles.submitButton, flex: 1, backgroundColor: '#25D366' }} onClick={handleWhatsAppShare}> | |
| <span style={{ marginRight: 8 }}>π’</span> | |
| <span>Share via WhatsApp (Text)</span> | |
| </button> | |
| <button style={{ ...styles.submitButton, flex: 1, backgroundColor: '#25D366' }} onClick={handleCopyCSV}> | |
| <Icon name="share" size={20} color="#fff" /> | |
| <span style={{ marginLeft: 8 }}>{csvCopied ? 'CSV Copied!' : 'Copy CSV for WhatsApp'}</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // 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 }) => ( | |
| <div style={styles.profileDetailRow}> | |
| <Icon name={icon} size={24} color="#555" /> | |
| <div style={styles.profileDetailTextContainer}> | |
| <p style={styles.profileDetailLabel}>{label}</p> | |
| <p style={styles.profileDetailValue}>{value || 'Not Set'}</p> | |
| </div> | |
| </div> | |
| ); | |
| return ( | |
| <div style={styles.screenContainer}> | |
| <WaveHeader title="Faculty Profile" /> | |
| <div style={styles.settingsContent}> | |
| <div style={styles.profileCard}> | |
| <div style={styles.profileCardHeader}> | |
| <div style={styles.avatar}> | |
| <p style={styles.avatarLetter}>{initial}</p> | |
| </div> | |
| <div style={{marginLeft: 15}}> | |
| <p style={styles.profileName}>{name || 'User Name'}</p> | |
| <p style={styles.profilePosition}>{position || 'Position'}</p> | |
| </div> | |
| </div> | |
| <div style={styles.profileDetailsContainer}> | |
| <ProfileDetail icon="id" label="Employee ID" value={employeeId} /> | |
| <ProfileDetail icon="briefcase" label="Department" value={department} /> | |
| <ProfileDetail icon="user" label="Role" value={userProfile.role} /> | |
| </div> | |
| </div> | |
| <button style={styles.logoutButton} onClick={() => navigate('Auth')}> | |
| <span style={styles.logoutButtonText}>Logout</span> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // --- 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 <DepartmentScreen | |
| navigate={navigate} | |
| attendanceRecords={attendanceRecords} | |
| userProfile={userProfile || { department: departments[0] }} | |
| departments={departments} | |
| />; | |
| case 'Attendance': | |
| return <AttendanceScreen | |
| addAttendanceRecord={addAttendanceRecord} | |
| year={currentYear || params.year} | |
| section={currentSection || params.section} | |
| department={currentDepartment || params.department} | |
| navigate={navigate} | |
| defaultStudentCounts={defaultStudentCounts} | |
| showMessage={showMessage} | |
| studentList={studentList} // <-- Add this prop | |
| />; | |
| case 'Dashboard': | |
| return <DashboardScreen | |
| attendanceRecords={attendanceRecords} | |
| userProfile={userProfile || { department: departments[0] }} | |
| departments={departments} | |
| showMessage={showMessage} | |
| />; | |
| case 'Roster': | |
| return <RosterScreen | |
| studentList={studentList} | |
| setStudentList={setStudentList} | |
| showMessage={showMessage} | |
| departments={departments} | |
| attendanceRecords={attendanceRecords} | |
| />; | |
| case 'Share': | |
| return <ShareScreen | |
| attendanceRecords={attendanceRecords} | |
| userProfile={userProfile || { department: departments[0] }} | |
| departments={departments} | |
| showMessage={showMessage} | |
| />; | |
| case 'Settings': | |
| return <SettingsScreen navigate={navigate} userProfile={userProfile} />; | |
| default: | |
| return <DepartmentScreen | |
| navigate={navigate} | |
| attendanceRecords={attendanceRecords} | |
| userProfile={userProfile || { department: departments[0] }} | |
| departments={departments} | |
| />; | |
| } | |
| } | |
| const tabs = ['Home', 'Dashboard', 'Roster', 'Share', 'Settings']; | |
| const tabIcons = { Home: 'home', Dashboard: 'chart', Roster: 'people', Share: 'share', Settings: 'settings' }; | |
| return ( | |
| <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}> | |
| <div style={{ flex: 1, overflow: 'hidden' }}> | |
| {renderScreen()} | |
| </div> | |
| <div style={styles.tabBar}> | |
| {tabs.map(tab => ( | |
| <button key={tab} style={styles.tabButton} onClick={() => setActiveTab(tab)}> | |
| <Icon name={tabIcons[tab]} size={24} color={activeTab === tab ? '#8e44ad' : 'gray'} /> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| 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 <AuthScreen navigate={navigate} setUserProfile={setUserProfile} showMessage={showMessage} />; | |
| } | |
| switch (page) { | |
| case 'Auth': | |
| return <AuthScreen navigate={navigate} setUserProfile={setUserProfile} showMessage={showMessage} />; | |
| case 'MainApp': | |
| return <MainApp | |
| navigate={navigate} | |
| params={params} | |
| attendanceRecords={attendanceRecords} | |
| addAttendanceRecord={addAttendanceRecord} | |
| userProfile={userProfile} | |
| currentYear={currentYear} | |
| currentSection={currentSection} | |
| currentDepartment={currentDepartment} | |
| defaultStudentCounts={defaultStudentCounts} | |
| studentList={studentList} | |
| setStudentList={setStudentList} | |
| showMessage={showMessage} | |
| departments={departments} | |
| />; | |
| case 'Year': | |
| return <YearScreen navigate={navigate} params={params} />; | |
| case 'Section': | |
| return <SectionScreen navigate={navigate} params={params} />; | |
| default: | |
| return <DepartmentScreen | |
| navigate={navigate} | |
| attendanceRecords={attendanceRecords} | |
| userProfile={userProfile} | |
| departments={departments} | |
| />; | |
| } | |
| }; | |
| return ( | |
| <> | |
| <div style={{fontFamily: 'sans-serif'}}> | |
| {renderPage()} | |
| <MessageBox message={message} onClose={hideMessage} /> | |
| </div> | |
| </> | |
| ); | |
| } | |
| // --- 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%' } | |
| }; | |