Upload 23 files
Browse files# WiFi Attendance Tracker - Employee Management Version
## Overview
The WiFi-Based Attendance & Break Tracker is an advanced employee time tracking system that monitors attendance by detecting MAC addresses of devices connected to the local WiFi network. This enhanced version includes comprehensive employee management features, password protection, and a modern web interface.
## Key Features
### π **Employee Management**
- **Add New Employees**: Password-protected employee addition with admin authentication
- **Search Functionality**: Search employees by name or MAC address
- **Employee Pictures**: Support for employee profile pictures
- **Password Protection**: Secure admin access with customizable passwords (default: 1122)
### β° **Advanced Time Tracking**
- **Time-In/Time-Out**: Automatic detection of first arrival and final departure
- **Break Monitoring**: Tracks when employees go on break and return
- **5:00 PM Auto-Timeout**: Employees automatically marked as timed out at office closing
- **Duration Calculations**: Precise tracking of total work time and break time
### π **Comprehensive Reporting**
- **Real-time Dashboard**: Live employee status updates every 10 seconds
- **Daily Attendance Sheets**: Detailed CSV reports with formatted durations
- **Event History**: Complete audit trail of all attendance events
- **Summary Statistics**: Daily overview with present/absent/break/timeout counts
### π **Modern Web Interface**
- **Responsive Design**: Works on desktop and mobile devices
- **Real-time Updates**: Live status changes and notifications
- **Interactive Dashboard**: Modern UI with statistics cards and employee cards
- **Modal Dialogs**: Professional forms for employee management
### π§ **System Features**
- **Non-Admin Operation**: Attempts to run without administrator privileges
- **Offline Operation**: Works completely without internet connection
- **Database Storage**: SQLite database for persistent data storage
- **JSON Configuration**: Separate employee data file for easy management
- **Integrated Application**: Single entry point for all functionality
## System Requirements
- **Operating System**: Windows 10/11, Linux, or macOS
- **Python**: 3.7 or higher
- **Network**: Local WiFi network access
- **Permissions**: Network scanning capabilities (admin/root recommended but not required)
## Installation
### Quick Setup (Windows)
1. **Extract the Project**
```
Extract wifi_attendance_tracker_advanced.zip to your desired location
```
2. **Run Setup Script**
```
Double-click setup.bat
```
3. **Configure Employees**
```
Edit employees.json with your employee data
```
4. **Start the Application**
```
Double-click run.bat or run: python main.py
```
### Manual Installation
1. **Install Python Dependencies**
```bash
pip install flask flask-cors bcrypt
```
2. **Configure System**
```bash
# Edit config.json for system settings
# Edit employees.json for employee data
```
3. **Run Application**
```bash
python main.py
```
## Configuration
### config.json
```json
{
"scan_interval_seconds": 60,
"web_port": 5000,
"office_timeout_hour": 17,
"office_timeout_minute": 0
}
```
### employees.json
```json
[
{
"name": "John Doe",
"mac_address": "aa-bb-cc-dd-ee-ff",
"picture": "static/img/john_doe.jpg"
},
{
"name": "Jane Smith",
"mac_address": "11-22-33-44-55-66",
"picture": "static/img/jane_smith.jpg"
}
]
```
## Usage
### Starting the Application
#### Web Interface Mode (Default)
```bash
python main.py
```
Access the web interface at: http://localhost:5000
#### Console Mode
```bash
python main.py --console
```
#### Custom Port
```bash
python main.py --port 8080
```
#### System Status
```bash
python main.py --status
```
### Web Interface Features
#### Dashboard Overview
- **System Status**: Current monitoring state and employee count
- **Statistics Cards**: Real-time counts of present, absent, on break, and timed out employees
- **Employee Status**: Live employee cards with status badges and time information
- **Recent Events**: Timeline of latest attendance events
#### Employee Management
1. **Adding Employees**:
- Click "Add Employee" button
- Fill in employee name and MAC address
- Optionally add picture path
- Enter admin password (default: 1122)
- Click "Add Employee"
2. **Searching Employees**:
- Use the search box to find employees by name or MAC address
- Results update in real-time
3. **Changing Admin Password**:
- Click "Settings" button
- Enter current password and new password
- Click "Change Password"
#### Monitoring Controls
- **Start Monitoring**: Begin attendance tracking
- **Stop Monitoring**: Pause attendance tracking
- **Refresh Data**: Manually update all data
- **Export CSV**: Download daily attendance summary
### Employee Pictures
To add employee pictures:
1. **Create Image Directory**:
```
Create: static/img/ directory
```
2. **Add Picture Files**:
```
Place images in: static/img/employee_name.jpg
```
3. **Update Configuration**:
```json
{
"name": "John Doe",
"mac_address": "aa-bb-cc-dd-ee-ff",
"picture": "static/img/john_doe.jpg"
}
```
## How It Works
### MAC Address Detection
The system uses the `arp -a` command to scan for devices on the local network. When an employee's device (identified by MAC address) is detected:
1. **First Detection**: Marked as "Time In"
2. **Continuous Presence**: Status remains "Present"
3. **Temporary Absence**: Marked as "On Break" if gone for short period
4. **Extended Absence**: Marked as "Time Out" if gone for extended period
5. **5:00 PM Timeout**: Automatically marked as "Timed Out" at office closing
### Status Logic
- **Present**: Device currently detected on network
- **Absent**: Device not detected and no previous activity today
- **On Break**: Device temporarily not detected but was present earlier
- **Timed Out**: Device not detected after 5:00 PM or extended absence
### Data Storage
- **SQLite Database**: Stores all attendance events and employee data
- **CSV Logs**: Daily attendance summaries exported to `logs/` directory
- **JSON Files**: Configuration and employee data in human-readable format
## Security Features
### Password Protection
- **Admin Authentication**: Required for adding new employees
- **Password Hashing**: Secure bcrypt hashing for stored passwords
- **Default Password**: 1122 (changeable through web interface)
- **Session Security**: No persistent login sessions for security
### Data Privacy
- **Local Storage**: All data stored locally, no cloud transmission
- **Offline Operation**: No internet connection required
- **Encrypted Passwords**: Admin passwords securely hashed
- **Access Control**: Employee management requires authentication
## Troubleshooting
### Common Issues
#### "Permission Denied" Errors
- **Windows**: Run Command Prompt as Administrator
- **Linux/macOS**: Use `sudo python main.py`
- **Alternative**: Use non-admin mode (limited network scanning)
#### No Employees Detected
1. Verify MAC addresses in `employees.json`
2. Check if devices are connected to same network
3. Ensure WiFi is enabled on employee devices
4. Run `python main.py --status` to check configuration
#### Web Interface Not Loading
1. Check if port 5000 is available
2. Try different port: `python main.py --port 8080`
3. Verify firewall settings
4. Check console for error messages
#### Database Errors
1. Delete `attendance.db` to reset database
2. Restart application to recreate tables
3. Check file permissions in project directory
### Network Scanning Limitations
#### Non-Admin Mode
When running without administrator privileges:
- Limited network scanning capabilities
- May not detect all devices
- Reduced accuracy in some network configurations
- Employee management features work normally
#### Admin Mode (Recommended)
- Full network scanning capabilities
- Accurate device detection
- Complete attendance tracking
- All features fully functional
## File Structure
```
wifi_attendance_tracker/
βββ main.py # Main application entry point
βββ attendance_tracker.py # Core attendance tracking logic
βββ database.py # Database operations
βββ auth.py # Authentication management
βββ web_interface.py # Flask web interface (legacy)
βββ config.json # System configuration
βββ employees.json # Employee data
βββ requirements.txt # Python dependencies
βββ setup.bat # Windows setup script
βββ run.bat # Windows run script
βββ templates/
β βββ index.html # Web interface template
βββ static/
β βββ css/
β β βββ style.css # Web interface styles
β βββ js/
β β βββ script.js # Web interface JavaScript
β βββ img/ # Employee pictures directory
βββ logs/ # CSV export directory
βββ attendance.db # SQLite database
```
## API Endpoints
The web interface provides REST API endpoints:
### System Status
- `GET /api/status` - Get system status
- `POST /api/start_monitoring` - Start monitoring
- `POST /api/stop_monitoring` - Stop monitoring
### Employee Management
- `GET /api/employees` - Get all employees
- `POST /api/add_employee` - Add new employee (requires password)
- `GET /api/search_employees?q=query` - Search employees
### Attendance Data
- `GET /api/attendance_events` - Get attendance events
- `GET /api/daily_summary?date=YYYY-MM-DD` - Get daily summary
- `GET /api/summary_stats?date=YYYY-MM-DD` - Get summary statistics
- `GET /api/export_csv?date=YYYY-MM-DD` - Export CSV
### Authentication
- `POST /api/change_password` - Change admin password
## Advanced Configuration
### Custom Office Hours
Edit `config.json`:
```json
{
"office_timeout_hour": 18,
"office_timeout_minute": 30
}
```
### Scan Interval
Adjust monitoring frequency:
```json
{
"scan_interval_seconds": 30
}
```
### Web Port
Change web interface port:
```json
{
"web_port": 8080
}
`
- README.md +416 -3
- __pycache__/attendance_tracker.cpython-311.pyc +0 -0
- __pycache__/attendance_tracker.cpython-313.pyc +0 -0
- __pycache__/auth.cpython-311.pyc +0 -0
- __pycache__/auth.cpython-313.pyc +0 -0
- __pycache__/database.cpython-311.pyc +0 -0
- __pycache__/database.cpython-313.pyc +0 -0
- attendance.db +0 -0
- attendance_tracker.py +350 -0
- auth.py +78 -0
- config.json +15 -0
- database.py +573 -0
- employees.json +12 -0
- logs/attendance_summary_2025-07-28.csv +8 -0
- main.py +865 -0
- requirements.txt +4 -0
- run.bat +30 -0
- setup.bat +36 -0
- static/css/style.css +899 -0
- static/js/script.js +712 -0
- templates/index.html +370 -0
- templates/login.html +282 -0
- web_interface.py +236 -0
|
@@ -1,3 +1,416 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# WiFi Attendance Tracker - Employee Management Version
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The WiFi-Based Attendance & Break Tracker is an advanced employee time tracking system that monitors attendance by detecting MAC addresses of devices connected to the local WiFi network. This enhanced version includes comprehensive employee management features, password protection, and a modern web interface.
|
| 6 |
+
|
| 7 |
+
## Key Features
|
| 8 |
+
|
| 9 |
+
### π **Employee Management**
|
| 10 |
+
- **Add New Employees**: Password-protected employee addition with admin authentication
|
| 11 |
+
- **Search Functionality**: Search employees by name or MAC address
|
| 12 |
+
- **Employee Pictures**: Support for employee profile pictures
|
| 13 |
+
- **Password Protection**: Secure admin access with customizable passwords (default: 1122)
|
| 14 |
+
|
| 15 |
+
### β° **Advanced Time Tracking**
|
| 16 |
+
- **Time-In/Time-Out**: Automatic detection of first arrival and final departure
|
| 17 |
+
- **Break Monitoring**: Tracks when employees go on break and return
|
| 18 |
+
- **5:00 PM Auto-Timeout**: Employees automatically marked as timed out at office closing
|
| 19 |
+
- **Duration Calculations**: Precise tracking of total work time and break time
|
| 20 |
+
|
| 21 |
+
### π **Comprehensive Reporting**
|
| 22 |
+
- **Real-time Dashboard**: Live employee status updates every 10 seconds
|
| 23 |
+
- **Daily Attendance Sheets**: Detailed CSV reports with formatted durations
|
| 24 |
+
- **Event History**: Complete audit trail of all attendance events
|
| 25 |
+
- **Summary Statistics**: Daily overview with present/absent/break/timeout counts
|
| 26 |
+
|
| 27 |
+
### π **Modern Web Interface**
|
| 28 |
+
- **Responsive Design**: Works on desktop and mobile devices
|
| 29 |
+
- **Real-time Updates**: Live status changes and notifications
|
| 30 |
+
- **Interactive Dashboard**: Modern UI with statistics cards and employee cards
|
| 31 |
+
- **Modal Dialogs**: Professional forms for employee management
|
| 32 |
+
|
| 33 |
+
### π§ **System Features**
|
| 34 |
+
- **Non-Admin Operation**: Attempts to run without administrator privileges
|
| 35 |
+
- **Offline Operation**: Works completely without internet connection
|
| 36 |
+
- **Database Storage**: SQLite database for persistent data storage
|
| 37 |
+
- **JSON Configuration**: Separate employee data file for easy management
|
| 38 |
+
- **Integrated Application**: Single entry point for all functionality
|
| 39 |
+
|
| 40 |
+
## System Requirements
|
| 41 |
+
|
| 42 |
+
- **Operating System**: Windows 10/11, Linux, or macOS
|
| 43 |
+
- **Python**: 3.7 or higher
|
| 44 |
+
- **Network**: Local WiFi network access
|
| 45 |
+
- **Permissions**: Network scanning capabilities (admin/root recommended but not required)
|
| 46 |
+
|
| 47 |
+
## Installation
|
| 48 |
+
|
| 49 |
+
### Quick Setup (Windows)
|
| 50 |
+
|
| 51 |
+
1. **Extract the Project**
|
| 52 |
+
```
|
| 53 |
+
Extract wifi_attendance_tracker_advanced.zip to your desired location
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
2. **Run Setup Script**
|
| 57 |
+
```
|
| 58 |
+
Double-click setup.bat
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
3. **Configure Employees**
|
| 62 |
+
```
|
| 63 |
+
Edit employees.json with your employee data
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
4. **Start the Application**
|
| 67 |
+
```
|
| 68 |
+
Double-click run.bat or run: python main.py
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
### Manual Installation
|
| 72 |
+
|
| 73 |
+
1. **Install Python Dependencies**
|
| 74 |
+
```bash
|
| 75 |
+
pip install flask flask-cors bcrypt
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
2. **Configure System**
|
| 79 |
+
```bash
|
| 80 |
+
# Edit config.json for system settings
|
| 81 |
+
# Edit employees.json for employee data
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
3. **Run Application**
|
| 85 |
+
```bash
|
| 86 |
+
python main.py
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## Configuration
|
| 90 |
+
|
| 91 |
+
### config.json
|
| 92 |
+
```json
|
| 93 |
+
{
|
| 94 |
+
"scan_interval_seconds": 60,
|
| 95 |
+
"web_port": 5000,
|
| 96 |
+
"office_timeout_hour": 17,
|
| 97 |
+
"office_timeout_minute": 0
|
| 98 |
+
}
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
### employees.json
|
| 102 |
+
```json
|
| 103 |
+
[
|
| 104 |
+
{
|
| 105 |
+
"name": "John Doe",
|
| 106 |
+
"mac_address": "aa-bb-cc-dd-ee-ff",
|
| 107 |
+
"picture": "static/img/john_doe.jpg"
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"name": "Jane Smith",
|
| 111 |
+
"mac_address": "11-22-33-44-55-66",
|
| 112 |
+
"picture": "static/img/jane_smith.jpg"
|
| 113 |
+
}
|
| 114 |
+
]
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
## Usage
|
| 118 |
+
|
| 119 |
+
### Starting the Application
|
| 120 |
+
|
| 121 |
+
#### Web Interface Mode (Default)
|
| 122 |
+
```bash
|
| 123 |
+
python main.py
|
| 124 |
+
```
|
| 125 |
+
Access the web interface at: http://localhost:5000
|
| 126 |
+
|
| 127 |
+
#### Console Mode
|
| 128 |
+
```bash
|
| 129 |
+
python main.py --console
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
#### Custom Port
|
| 133 |
+
```bash
|
| 134 |
+
python main.py --port 8080
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
#### System Status
|
| 138 |
+
```bash
|
| 139 |
+
python main.py --status
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
### Web Interface Features
|
| 143 |
+
|
| 144 |
+
#### Dashboard Overview
|
| 145 |
+
- **System Status**: Current monitoring state and employee count
|
| 146 |
+
- **Statistics Cards**: Real-time counts of present, absent, on break, and timed out employees
|
| 147 |
+
- **Employee Status**: Live employee cards with status badges and time information
|
| 148 |
+
- **Recent Events**: Timeline of latest attendance events
|
| 149 |
+
|
| 150 |
+
#### Employee Management
|
| 151 |
+
1. **Adding Employees**:
|
| 152 |
+
- Click "Add Employee" button
|
| 153 |
+
- Fill in employee name and MAC address
|
| 154 |
+
- Optionally add picture path
|
| 155 |
+
- Enter admin password (default: 1122)
|
| 156 |
+
- Click "Add Employee"
|
| 157 |
+
|
| 158 |
+
2. **Searching Employees**:
|
| 159 |
+
- Use the search box to find employees by name or MAC address
|
| 160 |
+
- Results update in real-time
|
| 161 |
+
|
| 162 |
+
3. **Changing Admin Password**:
|
| 163 |
+
- Click "Settings" button
|
| 164 |
+
- Enter current password and new password
|
| 165 |
+
- Click "Change Password"
|
| 166 |
+
|
| 167 |
+
#### Monitoring Controls
|
| 168 |
+
- **Start Monitoring**: Begin attendance tracking
|
| 169 |
+
- **Stop Monitoring**: Pause attendance tracking
|
| 170 |
+
- **Refresh Data**: Manually update all data
|
| 171 |
+
- **Export CSV**: Download daily attendance summary
|
| 172 |
+
|
| 173 |
+
### Employee Pictures
|
| 174 |
+
|
| 175 |
+
To add employee pictures:
|
| 176 |
+
|
| 177 |
+
1. **Create Image Directory**:
|
| 178 |
+
```
|
| 179 |
+
Create: static/img/ directory
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
2. **Add Picture Files**:
|
| 183 |
+
```
|
| 184 |
+
Place images in: static/img/employee_name.jpg
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
3. **Update Configuration**:
|
| 188 |
+
```json
|
| 189 |
+
{
|
| 190 |
+
"name": "John Doe",
|
| 191 |
+
"mac_address": "aa-bb-cc-dd-ee-ff",
|
| 192 |
+
"picture": "static/img/john_doe.jpg"
|
| 193 |
+
}
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
## How It Works
|
| 197 |
+
|
| 198 |
+
### MAC Address Detection
|
| 199 |
+
The system uses the `arp -a` command to scan for devices on the local network. When an employee's device (identified by MAC address) is detected:
|
| 200 |
+
|
| 201 |
+
1. **First Detection**: Marked as "Time In"
|
| 202 |
+
2. **Continuous Presence**: Status remains "Present"
|
| 203 |
+
3. **Temporary Absence**: Marked as "On Break" if gone for short period
|
| 204 |
+
4. **Extended Absence**: Marked as "Time Out" if gone for extended period
|
| 205 |
+
5. **5:00 PM Timeout**: Automatically marked as "Timed Out" at office closing
|
| 206 |
+
|
| 207 |
+
### Status Logic
|
| 208 |
+
- **Present**: Device currently detected on network
|
| 209 |
+
- **Absent**: Device not detected and no previous activity today
|
| 210 |
+
- **On Break**: Device temporarily not detected but was present earlier
|
| 211 |
+
- **Timed Out**: Device not detected after 5:00 PM or extended absence
|
| 212 |
+
|
| 213 |
+
### Data Storage
|
| 214 |
+
- **SQLite Database**: Stores all attendance events and employee data
|
| 215 |
+
- **CSV Logs**: Daily attendance summaries exported to `logs/` directory
|
| 216 |
+
- **JSON Files**: Configuration and employee data in human-readable format
|
| 217 |
+
|
| 218 |
+
## Security Features
|
| 219 |
+
|
| 220 |
+
### Password Protection
|
| 221 |
+
- **Admin Authentication**: Required for adding new employees
|
| 222 |
+
- **Password Hashing**: Secure bcrypt hashing for stored passwords
|
| 223 |
+
- **Default Password**: 1122 (changeable through web interface)
|
| 224 |
+
- **Session Security**: No persistent login sessions for security
|
| 225 |
+
|
| 226 |
+
### Data Privacy
|
| 227 |
+
- **Local Storage**: All data stored locally, no cloud transmission
|
| 228 |
+
- **Offline Operation**: No internet connection required
|
| 229 |
+
- **Encrypted Passwords**: Admin passwords securely hashed
|
| 230 |
+
- **Access Control**: Employee management requires authentication
|
| 231 |
+
|
| 232 |
+
## Troubleshooting
|
| 233 |
+
|
| 234 |
+
### Common Issues
|
| 235 |
+
|
| 236 |
+
#### "Permission Denied" Errors
|
| 237 |
+
- **Windows**: Run Command Prompt as Administrator
|
| 238 |
+
- **Linux/macOS**: Use `sudo python main.py`
|
| 239 |
+
- **Alternative**: Use non-admin mode (limited network scanning)
|
| 240 |
+
|
| 241 |
+
#### No Employees Detected
|
| 242 |
+
1. Verify MAC addresses in `employees.json`
|
| 243 |
+
2. Check if devices are connected to same network
|
| 244 |
+
3. Ensure WiFi is enabled on employee devices
|
| 245 |
+
4. Run `python main.py --status` to check configuration
|
| 246 |
+
|
| 247 |
+
#### Web Interface Not Loading
|
| 248 |
+
1. Check if port 5000 is available
|
| 249 |
+
2. Try different port: `python main.py --port 8080`
|
| 250 |
+
3. Verify firewall settings
|
| 251 |
+
4. Check console for error messages
|
| 252 |
+
|
| 253 |
+
#### Database Errors
|
| 254 |
+
1. Delete `attendance.db` to reset database
|
| 255 |
+
2. Restart application to recreate tables
|
| 256 |
+
3. Check file permissions in project directory
|
| 257 |
+
|
| 258 |
+
### Network Scanning Limitations
|
| 259 |
+
|
| 260 |
+
#### Non-Admin Mode
|
| 261 |
+
When running without administrator privileges:
|
| 262 |
+
- Limited network scanning capabilities
|
| 263 |
+
- May not detect all devices
|
| 264 |
+
- Reduced accuracy in some network configurations
|
| 265 |
+
- Employee management features work normally
|
| 266 |
+
|
| 267 |
+
#### Admin Mode (Recommended)
|
| 268 |
+
- Full network scanning capabilities
|
| 269 |
+
- Accurate device detection
|
| 270 |
+
- Complete attendance tracking
|
| 271 |
+
- All features fully functional
|
| 272 |
+
|
| 273 |
+
## File Structure
|
| 274 |
+
|
| 275 |
+
```
|
| 276 |
+
wifi_attendance_tracker/
|
| 277 |
+
βββ main.py # Main application entry point
|
| 278 |
+
βββ attendance_tracker.py # Core attendance tracking logic
|
| 279 |
+
βββ database.py # Database operations
|
| 280 |
+
βββ auth.py # Authentication management
|
| 281 |
+
βββ web_interface.py # Flask web interface (legacy)
|
| 282 |
+
βββ config.json # System configuration
|
| 283 |
+
βββ employees.json # Employee data
|
| 284 |
+
βββ requirements.txt # Python dependencies
|
| 285 |
+
βββ setup.bat # Windows setup script
|
| 286 |
+
βββ run.bat # Windows run script
|
| 287 |
+
βββ templates/
|
| 288 |
+
β βββ index.html # Web interface template
|
| 289 |
+
βββ static/
|
| 290 |
+
β βββ css/
|
| 291 |
+
β β βββ style.css # Web interface styles
|
| 292 |
+
β βββ js/
|
| 293 |
+
β β βββ script.js # Web interface JavaScript
|
| 294 |
+
β βββ img/ # Employee pictures directory
|
| 295 |
+
βββ logs/ # CSV export directory
|
| 296 |
+
βββ attendance.db # SQLite database
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
## API Endpoints
|
| 300 |
+
|
| 301 |
+
The web interface provides REST API endpoints:
|
| 302 |
+
|
| 303 |
+
### System Status
|
| 304 |
+
- `GET /api/status` - Get system status
|
| 305 |
+
- `POST /api/start_monitoring` - Start monitoring
|
| 306 |
+
- `POST /api/stop_monitoring` - Stop monitoring
|
| 307 |
+
|
| 308 |
+
### Employee Management
|
| 309 |
+
- `GET /api/employees` - Get all employees
|
| 310 |
+
- `POST /api/add_employee` - Add new employee (requires password)
|
| 311 |
+
- `GET /api/search_employees?q=query` - Search employees
|
| 312 |
+
|
| 313 |
+
### Attendance Data
|
| 314 |
+
- `GET /api/attendance_events` - Get attendance events
|
| 315 |
+
- `GET /api/daily_summary?date=YYYY-MM-DD` - Get daily summary
|
| 316 |
+
- `GET /api/summary_stats?date=YYYY-MM-DD` - Get summary statistics
|
| 317 |
+
- `GET /api/export_csv?date=YYYY-MM-DD` - Export CSV
|
| 318 |
+
|
| 319 |
+
### Authentication
|
| 320 |
+
- `POST /api/change_password` - Change admin password
|
| 321 |
+
|
| 322 |
+
## Advanced Configuration
|
| 323 |
+
|
| 324 |
+
### Custom Office Hours
|
| 325 |
+
Edit `config.json`:
|
| 326 |
+
```json
|
| 327 |
+
{
|
| 328 |
+
"office_timeout_hour": 18,
|
| 329 |
+
"office_timeout_minute": 30
|
| 330 |
+
}
|
| 331 |
+
```
|
| 332 |
+
|
| 333 |
+
### Scan Interval
|
| 334 |
+
Adjust monitoring frequency:
|
| 335 |
+
```json
|
| 336 |
+
{
|
| 337 |
+
"scan_interval_seconds": 30
|
| 338 |
+
}
|
| 339 |
+
```
|
| 340 |
+
|
| 341 |
+
### Web Port
|
| 342 |
+
Change web interface port:
|
| 343 |
+
```json
|
| 344 |
+
{
|
| 345 |
+
"web_port": 8080
|
| 346 |
+
}
|
| 347 |
+
```
|
| 348 |
+
|
| 349 |
+
## Development
|
| 350 |
+
|
| 351 |
+
### Adding New Features
|
| 352 |
+
1. **Backend**: Modify `main.py` or create new modules
|
| 353 |
+
2. **Frontend**: Update `templates/index.html` and `static/` files
|
| 354 |
+
3. **Database**: Add new tables/columns in `database.py`
|
| 355 |
+
4. **API**: Add new endpoints in `main.py`
|
| 356 |
+
|
| 357 |
+
### Testing
|
| 358 |
+
```bash
|
| 359 |
+
# Test database functionality
|
| 360 |
+
python database.py
|
| 361 |
+
|
| 362 |
+
# Test authentication
|
| 363 |
+
python auth.py
|
| 364 |
+
|
| 365 |
+
# Test attendance tracking
|
| 366 |
+
python attendance_tracker.py
|
| 367 |
+
|
| 368 |
+
# Check system status
|
| 369 |
+
python main.py --status
|
| 370 |
+
```
|
| 371 |
+
|
| 372 |
+
## Support
|
| 373 |
+
|
| 374 |
+
### Getting Help
|
| 375 |
+
1. Check this README for common solutions
|
| 376 |
+
2. Run `python main.py --status` for system diagnostics
|
| 377 |
+
3. Check log files in `logs/` directory
|
| 378 |
+
4. Verify configuration files are properly formatted
|
| 379 |
+
|
| 380 |
+
### Reporting Issues
|
| 381 |
+
When reporting issues, include:
|
| 382 |
+
- Operating system and Python version
|
| 383 |
+
- Error messages from console
|
| 384 |
+
- Configuration files (remove sensitive data)
|
| 385 |
+
- Steps to reproduce the issue
|
| 386 |
+
|
| 387 |
+
## License
|
| 388 |
+
|
| 389 |
+
This project is provided as-is for educational and internal business use. Please ensure compliance with local privacy and employment laws when monitoring employee attendance.
|
| 390 |
+
|
| 391 |
+
## Version History
|
| 392 |
+
|
| 393 |
+
### Version 3.0 (Current)
|
| 394 |
+
- Employee management with password protection
|
| 395 |
+
- Enhanced web interface with modern design
|
| 396 |
+
- Non-admin operation support
|
| 397 |
+
- Employee pictures and search functionality
|
| 398 |
+
- Integrated single-file application
|
| 399 |
+
|
| 400 |
+
### Version 2.0
|
| 401 |
+
- Advanced time tracking with breaks
|
| 402 |
+
- 5:00 PM automatic timeout
|
| 403 |
+
- Daily attendance summaries
|
| 404 |
+
- Web interface with real-time updates
|
| 405 |
+
|
| 406 |
+
### Version 1.0
|
| 407 |
+
- Basic MAC address detection
|
| 408 |
+
- Simple console interface
|
| 409 |
+
- CSV logging
|
| 410 |
+
|
| 411 |
+
---
|
| 412 |
+
|
| 413 |
+
**WiFi Attendance Tracker - Employee Management Version**
|
| 414 |
+
*Advanced MAC Detection with Employee Management Features*
|
| 415 |
+
*Version 3.0 - 2025*
|
| 416 |
+
|
|
Binary file (20.3 kB). View file
|
|
|
|
Binary file (18.6 kB). View file
|
|
|
|
Binary file (5.22 kB). View file
|
|
|
|
Binary file (4.76 kB). View file
|
|
|
|
Binary file (33.2 kB). View file
|
|
|
|
Binary file (28.4 kB). View file
|
|
|
|
Binary file (53.2 kB). View file
|
|
|
|
@@ -0,0 +1,350 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
import platform
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
from typing import Dict, Set, List, Tuple
|
| 7 |
+
import os
|
| 8 |
+
from database import AttendanceDatabase
|
| 9 |
+
|
| 10 |
+
class AttendanceTracker:
|
| 11 |
+
def __init__(self, config_path: str = "config.json", employees_path: str = "employees.json", db_path: str = "attendance.db"):
|
| 12 |
+
"""Initialize the attendance tracker with configuration and database."""
|
| 13 |
+
self.config_path = config_path
|
| 14 |
+
self.employees_path = employees_path
|
| 15 |
+
self.config = self.load_config()
|
| 16 |
+
self.employees = self.load_employees()
|
| 17 |
+
self.scan_interval = self.config.get("scan_interval_seconds", 60)
|
| 18 |
+
self.office_timeout_hour = self.config.get("office_timeout_hour", 17)
|
| 19 |
+
self.office_timeout_minute = self.config.get("office_timeout_minute", 0)
|
| 20 |
+
|
| 21 |
+
self.db = AttendanceDatabase(db_path)
|
| 22 |
+
|
| 23 |
+
# State tracking for each employee
|
| 24 |
+
# {mac: {"is_present": bool, "last_seen": datetime, "time_in": datetime, "on_break": bool, "break_start_time": datetime}}
|
| 25 |
+
self.employee_states = {}
|
| 26 |
+
self._initialize_employee_states()
|
| 27 |
+
|
| 28 |
+
# Ensure logs directory exists
|
| 29 |
+
os.makedirs("logs", exist_ok=True)
|
| 30 |
+
|
| 31 |
+
def _initialize_employee_states(self):
|
| 32 |
+
"""Initialize employee states from the database for the current day."""
|
| 33 |
+
today_str = datetime.now().strftime("%Y-%m-%d")
|
| 34 |
+
for mac, name in self.employees.items():
|
| 35 |
+
employee_info = self.db.get_employee_by_mac(mac)
|
| 36 |
+
if employee_info:
|
| 37 |
+
summary = self.db.get_daily_summary_for_employee(employee_info["id"], today_str)
|
| 38 |
+
if summary and summary["status"] == "Present":
|
| 39 |
+
# If employee was marked present today and not timed out
|
| 40 |
+
self.employee_states[mac] = {
|
| 41 |
+
"is_present": True,
|
| 42 |
+
"last_seen": datetime.now(), # Assume still present if last status was Present
|
| 43 |
+
"time_in": datetime.strptime(f"{today_str} {summary['time_in']}",
|
| 44 |
+
"%Y-%m-%d %H:%M:%S") if summary["time_in"] else None,
|
| 45 |
+
"on_break": False, # Cannot determine break status from summary, assume not on break
|
| 46 |
+
"break_start_time": None
|
| 47 |
+
}
|
| 48 |
+
else:
|
| 49 |
+
self.employee_states[mac] = {
|
| 50 |
+
"is_present": False,
|
| 51 |
+
"last_seen": None,
|
| 52 |
+
"time_in": None,
|
| 53 |
+
"on_break": False,
|
| 54 |
+
"break_start_time": None
|
| 55 |
+
}
|
| 56 |
+
else:
|
| 57 |
+
self.employee_states[mac] = {
|
| 58 |
+
"is_present": False,
|
| 59 |
+
"last_seen": None,
|
| 60 |
+
"time_in": None,
|
| 61 |
+
"on_break": False,
|
| 62 |
+
"break_start_time": None
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
def load_config(self) -> dict:
|
| 66 |
+
"""Load configuration from JSON file."""
|
| 67 |
+
try:
|
| 68 |
+
with open(self.config_path, 'r') as f:
|
| 69 |
+
return json.load(f)
|
| 70 |
+
except FileNotFoundError:
|
| 71 |
+
print(f"Config file {self.config_path} not found. Using default settings.")
|
| 72 |
+
return {"scan_interval_seconds": 60, "office_timeout_hour": 17, "office_timeout_minute": 0}
|
| 73 |
+
except json.JSONDecodeError:
|
| 74 |
+
print(f"Invalid JSON in {self.config_path}. Using default settings.")
|
| 75 |
+
return {"scan_interval_seconds": 60, "office_timeout_hour": 17, "office_timeout_minute": 0}
|
| 76 |
+
|
| 77 |
+
def load_employees(self) -> dict:
|
| 78 |
+
"""Load employees from employees.json file."""
|
| 79 |
+
try:
|
| 80 |
+
with open(self.employees_path, 'r') as f:
|
| 81 |
+
employees_list = json.load(f)
|
| 82 |
+
|
| 83 |
+
# Convert list to dict for compatibility
|
| 84 |
+
employees_dict = {}
|
| 85 |
+
for emp in employees_list:
|
| 86 |
+
employees_dict[emp['mac_address']] = emp['name']
|
| 87 |
+
|
| 88 |
+
return employees_dict
|
| 89 |
+
except FileNotFoundError:
|
| 90 |
+
print(f"Employees file {self.employees_path} not found. Using empty employee list.")
|
| 91 |
+
return {}
|
| 92 |
+
except json.JSONDecodeError:
|
| 93 |
+
print(f"Invalid JSON in {self.employees_path}. Using empty employee list.")
|
| 94 |
+
return {}
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print(f"Error loading employees: {e}")
|
| 97 |
+
return {}
|
| 98 |
+
|
| 99 |
+
def get_connected_devices(self) -> Set[str]:
|
| 100 |
+
"""Get MAC addresses of devices connected to the local network."""
|
| 101 |
+
connected_macs = set()
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
if platform.system() == "Windows":
|
| 105 |
+
result = subprocess.run(['arp', '-a'], capture_output=True, text=True, timeout=30)
|
| 106 |
+
if result.returncode == 0:
|
| 107 |
+
lines = result.stdout.split('\n')
|
| 108 |
+
for line in lines:
|
| 109 |
+
parts = line.strip().split()
|
| 110 |
+
if len(parts) >= 2:
|
| 111 |
+
potential_mac = parts[1]
|
| 112 |
+
if self.is_valid_mac(potential_mac):
|
| 113 |
+
normalized_mac = self.normalize_mac(potential_mac)
|
| 114 |
+
connected_macs.add(normalized_mac)
|
| 115 |
+
else:
|
| 116 |
+
result = subprocess.run(['arp', '-a'], capture_output=True, text=True, timeout=30)
|
| 117 |
+
if result.returncode == 0:
|
| 118 |
+
lines = result.stdout.split('\n')
|
| 119 |
+
for line in lines:
|
| 120 |
+
if '(' in line and ')' in line and 'at' in line:
|
| 121 |
+
parts = line.split(' at ')
|
| 122 |
+
if len(parts) >= 2:
|
| 123 |
+
mac_part = parts[1].split(' ')[0]
|
| 124 |
+
if self.is_valid_mac(mac_part):
|
| 125 |
+
normalized_mac = self.normalize_mac(mac_part)
|
| 126 |
+
connected_macs.add(normalized_mac)
|
| 127 |
+
|
| 128 |
+
except subprocess.TimeoutExpired:
|
| 129 |
+
print("ARP command timed out")
|
| 130 |
+
except Exception as e:
|
| 131 |
+
print(f"Error getting connected devices: {e}")
|
| 132 |
+
|
| 133 |
+
return connected_macs
|
| 134 |
+
|
| 135 |
+
def is_valid_mac(self, mac: str) -> bool:
|
| 136 |
+
"""Check if a string is a valid MAC address."""
|
| 137 |
+
if not mac:
|
| 138 |
+
return False
|
| 139 |
+
clean_mac = mac.replace('-', '').replace(':', '').replace('.', '')
|
| 140 |
+
if len(clean_mac) != 12:
|
| 141 |
+
return False
|
| 142 |
+
try:
|
| 143 |
+
int(clean_mac, 16)
|
| 144 |
+
return True
|
| 145 |
+
except ValueError:
|
| 146 |
+
return False
|
| 147 |
+
|
| 148 |
+
def normalize_mac(self, mac: str) -> str:
|
| 149 |
+
"""Normalize MAC address to lowercase with dashes."""
|
| 150 |
+
clean_mac = mac.replace('-', '').replace(':', '').replace('.', '').lower()
|
| 151 |
+
return '-'.join([clean_mac[i:i+2] for i in range(0, 12, 2)])
|
| 152 |
+
|
| 153 |
+
def get_employee_name(self, mac: str) -> str:
|
| 154 |
+
"""Get employee name from MAC address."""
|
| 155 |
+
return self.employees.get(mac, f"Unknown ({mac})")
|
| 156 |
+
|
| 157 |
+
def process_scan_results(self, detected_macs: Set[str]) -> List[Tuple[str, str, datetime]]:
|
| 158 |
+
"""Process scan results and generate attendance events."""
|
| 159 |
+
current_time = datetime.now()
|
| 160 |
+
today_str = current_time.strftime("%Y-%m-%d")
|
| 161 |
+
events = []
|
| 162 |
+
|
| 163 |
+
known_detected = {mac for mac in detected_macs if mac in self.employees}
|
| 164 |
+
|
| 165 |
+
for mac in self.employees:
|
| 166 |
+
employee_info = self.db.get_employee_by_mac(mac)
|
| 167 |
+
if not employee_info: # Should not happen if sync_employees_from_json is called
|
| 168 |
+
continue
|
| 169 |
+
employee_id = employee_info["id"]
|
| 170 |
+
|
| 171 |
+
current_state = self.employee_states.get(mac, {})
|
| 172 |
+
was_present = current_state.get("is_present", False)
|
| 173 |
+
on_break = current_state.get("on_break", False)
|
| 174 |
+
time_in = current_state.get("time_in")
|
| 175 |
+
|
| 176 |
+
is_currently_present = mac in known_detected
|
| 177 |
+
|
| 178 |
+
# Handle Time-In
|
| 179 |
+
if is_currently_present and not was_present:
|
| 180 |
+
event_type = "time_in"
|
| 181 |
+
self.db.log_attendance_event(mac, event_type, current_time)
|
| 182 |
+
events.append((mac, event_type, current_time))
|
| 183 |
+
self.employee_states[mac]["is_present"] = True
|
| 184 |
+
self.employee_states[mac]["last_seen"] = current_time
|
| 185 |
+
self.employee_states[mac]["time_in"] = current_time
|
| 186 |
+
self.employee_states[mac]["on_break"] = False
|
| 187 |
+
self.employee_states[mac]["break_start_time"] = None
|
| 188 |
+
|
| 189 |
+
# Update daily summary: set time_in and status to Present
|
| 190 |
+
self.db.update_daily_summary(employee_id, today_str,
|
| 191 |
+
time_in=current_time.strftime("%H:%M:%S"), status="Present")
|
| 192 |
+
|
| 193 |
+
# Handle Break Start
|
| 194 |
+
elif was_present and not is_currently_present and not on_break:
|
| 195 |
+
event_type = "break_start"
|
| 196 |
+
self.db.log_attendance_event(mac, event_type, current_time)
|
| 197 |
+
events.append((mac, event_type, current_time))
|
| 198 |
+
self.employee_states[mac]["is_present"] = False
|
| 199 |
+
self.employee_states[mac]["last_seen"] = current_time
|
| 200 |
+
self.employee_states[mac]["on_break"] = True
|
| 201 |
+
self.employee_states[mac]["break_start_time"] = current_time
|
| 202 |
+
|
| 203 |
+
# Update daily summary: status to On Break
|
| 204 |
+
self.db.update_daily_summary(employee_id, today_str, status="On Break")
|
| 205 |
+
|
| 206 |
+
# Handle Break End
|
| 207 |
+
elif not was_present and is_currently_present and on_break:
|
| 208 |
+
event_type = "break_end"
|
| 209 |
+
self.db.log_attendance_event(mac, event_type, current_time)
|
| 210 |
+
events.append((mac, event_type, current_time))
|
| 211 |
+
self.employee_states[mac]["is_present"] = True
|
| 212 |
+
self.employee_states[mac]["last_seen"] = current_time
|
| 213 |
+
self.employee_states[mac]["on_break"] = False
|
| 214 |
+
self.employee_states[mac]["break_start_time"] = None
|
| 215 |
+
|
| 216 |
+
# Update daily summary: status to Present, recalculate break duration
|
| 217 |
+
durations = self.db.calculate_durations(employee_id, today_str)
|
| 218 |
+
self.db.update_daily_summary(employee_id, today_str, status="Present",
|
| 219 |
+
total_break_duration=durations["total_break_duration"])
|
| 220 |
+
|
| 221 |
+
# Handle Time-Out (if employee was present and is now absent, and not on break)
|
| 222 |
+
elif was_present and not is_currently_present and not on_break and time_in:
|
| 223 |
+
# This is a final time_out for the day, not just a break
|
| 224 |
+
event_type = "time_out"
|
| 225 |
+
self.db.log_attendance_event(mac, event_type, current_time)
|
| 226 |
+
events.append((mac, event_type, current_time))
|
| 227 |
+
self.employee_states[mac]["is_present"] = False
|
| 228 |
+
self.employee_states[mac]["last_seen"] = current_time
|
| 229 |
+
self.employee_states[mac]["on_break"] = False
|
| 230 |
+
|
| 231 |
+
# Update daily summary: set time_out and status to Absent, recalculate work duration
|
| 232 |
+
durations = self.db.calculate_durations(employee_id, today_str)
|
| 233 |
+
self.db.update_daily_summary(employee_id, today_str,
|
| 234 |
+
time_out=current_time.strftime("%H:%M:%S"), status="Absent",
|
| 235 |
+
total_work_duration=durations["total_work_duration"])
|
| 236 |
+
|
| 237 |
+
# Update last seen for currently present employees
|
| 238 |
+
if is_currently_present:
|
| 239 |
+
self.employee_states[mac]["last_seen"] = current_time
|
| 240 |
+
|
| 241 |
+
# Automatic 5:00 PM timeout
|
| 242 |
+
timeout_time = current_time.replace(hour=self.office_timeout_hour, minute=self.office_timeout_minute, second=0, microsecond=0)
|
| 243 |
+
if current_time >= timeout_time and was_present and time_in and not on_break:
|
| 244 |
+
# Check if already timed out today
|
| 245 |
+
summary = self.db.get_daily_summary_for_employee(employee_id, today_str)
|
| 246 |
+
if summary and summary["status"] != "Timed Out":
|
| 247 |
+
event_type = "timeout_5pm"
|
| 248 |
+
self.db.log_attendance_event(mac, event_type, timeout_time)
|
| 249 |
+
events.append((mac, event_type, timeout_time))
|
| 250 |
+
self.employee_states[mac]["is_present"] = False
|
| 251 |
+
self.employee_states[mac]["last_seen"] = timeout_time
|
| 252 |
+
|
| 253 |
+
# Update daily summary: set time_out to 5 PM and status to Timed Out
|
| 254 |
+
durations = self.db.calculate_durations(employee_id, today_str)
|
| 255 |
+
self.db.update_daily_summary(employee_id, today_str,
|
| 256 |
+
time_out=timeout_time.strftime("%H:%M:%S"), status="Timed Out",
|
| 257 |
+
total_work_duration=durations["total_work_duration"])
|
| 258 |
+
|
| 259 |
+
# Export daily summary to CSV at the end of the day or on significant event
|
| 260 |
+
# For simplicity, let's export it every scan for now, or when a time_out/timeout_5pm event occurs
|
| 261 |
+
self.db.export_daily_summary_to_csv(today_str)
|
| 262 |
+
|
| 263 |
+
return events
|
| 264 |
+
|
| 265 |
+
def scan_once(self) -> List[Tuple[str, str, datetime]]:
|
| 266 |
+
"""Perform one scan and return any events."""
|
| 267 |
+
print(f"Scanning network at {datetime.now().strftime('%H:%M:%S')}...")
|
| 268 |
+
detected_macs = self.get_connected_devices()
|
| 269 |
+
print(f"Detected {len(detected_macs)} devices")
|
| 270 |
+
|
| 271 |
+
events = self.process_scan_results(detected_macs)
|
| 272 |
+
return events
|
| 273 |
+
|
| 274 |
+
def start_monitoring(self):
|
| 275 |
+
"""Start continuous monitoring loop."""
|
| 276 |
+
print("Starting WiFi Attendance Tracker...")
|
| 277 |
+
print(f"Monitoring {len(self.employees)} employees")
|
| 278 |
+
print(f"Scan interval: {self.scan_interval} seconds")
|
| 279 |
+
print("Press Ctrl+C to stop")
|
| 280 |
+
|
| 281 |
+
try:
|
| 282 |
+
while True:
|
| 283 |
+
self.scan_once()
|
| 284 |
+
time.sleep(self.scan_interval)
|
| 285 |
+
except KeyboardInterrupt:
|
| 286 |
+
print("\nStopping attendance tracker...")
|
| 287 |
+
|
| 288 |
+
def get_current_status(self) -> Dict[str, dict]:
|
| 289 |
+
"""Get current attendance status of all employees."""
|
| 290 |
+
status = {}
|
| 291 |
+
current_time = datetime.now()
|
| 292 |
+
|
| 293 |
+
for mac, name in self.employees.items():
|
| 294 |
+
state = self.employee_states.get(mac, {})
|
| 295 |
+
is_present = state.get("is_present", False)
|
| 296 |
+
last_seen_time = state.get("last_seen")
|
| 297 |
+
on_break = state.get("on_break", False)
|
| 298 |
+
|
| 299 |
+
display_status = "Present" if is_present else ("On Break" if on_break else "Absent")
|
| 300 |
+
|
| 301 |
+
status[mac] = {
|
| 302 |
+
'name': name,
|
| 303 |
+
'mac': mac,
|
| 304 |
+
'is_present': is_present,
|
| 305 |
+
'status': display_status,
|
| 306 |
+
'last_seen': last_seen_time.strftime('%Y-%m-%d %H:%M:%S') if last_seen_time else 'Never',
|
| 307 |
+
'time_in': state.get('time_in').strftime('%H:%M:%S') if state.get('time_in') else 'N/A'
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
return status
|
| 311 |
+
|
| 312 |
+
def sync_employees_from_json(self):
|
| 313 |
+
"""Sync employees from employees.json to database."""
|
| 314 |
+
try:
|
| 315 |
+
with open(self.employees_path, 'r') as f:
|
| 316 |
+
employees_list = json.load(f)
|
| 317 |
+
|
| 318 |
+
synced_count = 0
|
| 319 |
+
for emp in employees_list:
|
| 320 |
+
name = emp.get('name')
|
| 321 |
+
mac_address = emp.get('mac_address')
|
| 322 |
+
picture_path = emp.get('picture')
|
| 323 |
+
|
| 324 |
+
if name and mac_address:
|
| 325 |
+
if self.db.add_employee(name, mac_address, picture_path=picture_path):
|
| 326 |
+
synced_count += 1
|
| 327 |
+
# Also initialize daily summary for today if not exists
|
| 328 |
+
today_str = datetime.now().strftime('%Y-%m-%d')
|
| 329 |
+
employee_info = self.db.get_employee_by_mac(mac_address)
|
| 330 |
+
if employee_info:
|
| 331 |
+
self.db.update_daily_summary(employee_info['id'], today_str, status='Absent')
|
| 332 |
+
|
| 333 |
+
print(f"Synced {synced_count} new employees from {self.employees_path}")
|
| 334 |
+
return synced_count
|
| 335 |
+
|
| 336 |
+
except FileNotFoundError:
|
| 337 |
+
print(f"Employees file {self.employees_path} not found")
|
| 338 |
+
return 0
|
| 339 |
+
except json.JSONDecodeError:
|
| 340 |
+
print(f"Invalid JSON in {self.employees_path}")
|
| 341 |
+
return 0
|
| 342 |
+
except Exception as e:
|
| 343 |
+
print(f"Error syncing employees: {e}")
|
| 344 |
+
return 0
|
| 345 |
+
|
| 346 |
+
if __name__ == "__main__":
|
| 347 |
+
tracker = AttendanceTracker()
|
| 348 |
+
tracker.sync_employees_from_json()
|
| 349 |
+
tracker.start_monitoring()
|
| 350 |
+
|
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import bcrypt
|
| 2 |
+
from database import AttendanceDatabase
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
class AuthManager:
|
| 6 |
+
def __init__(self, db: AttendanceDatabase):
|
| 7 |
+
"""Initialize the authentication manager with database connection."""
|
| 8 |
+
self.db = db
|
| 9 |
+
self.default_password = "1122"
|
| 10 |
+
self._ensure_default_password()
|
| 11 |
+
|
| 12 |
+
def _ensure_default_password(self):
|
| 13 |
+
"""Ensure the default password is set in the database."""
|
| 14 |
+
stored_password_hash = self.db.get_setting("admin_password_hash")
|
| 15 |
+
if not stored_password_hash:
|
| 16 |
+
# Set default password hash
|
| 17 |
+
default_hash = self.hash_password(self.default_password)
|
| 18 |
+
self.db.set_setting("admin_password_hash", default_hash)
|
| 19 |
+
|
| 20 |
+
def hash_password(self, password: str) -> str:
|
| 21 |
+
"""Hash a password using bcrypt."""
|
| 22 |
+
salt = bcrypt.gensalt()
|
| 23 |
+
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
|
| 24 |
+
return hashed.decode('utf-8')
|
| 25 |
+
|
| 26 |
+
def verify_password(self, password: str, hashed_password: str) -> bool:
|
| 27 |
+
"""Verify a password against its hash."""
|
| 28 |
+
try:
|
| 29 |
+
return bcrypt.checkpw(password.encode('utf-8'), hashed_password.encode('utf-8'))
|
| 30 |
+
except Exception as e:
|
| 31 |
+
print(f"Error verifying password: {e}")
|
| 32 |
+
return False
|
| 33 |
+
|
| 34 |
+
def authenticate_admin(self, password: str) -> bool:
|
| 35 |
+
"""Authenticate admin password for employee management operations."""
|
| 36 |
+
stored_password_hash = self.db.get_setting("admin_password_hash")
|
| 37 |
+
if not stored_password_hash:
|
| 38 |
+
# If no password is set, use default
|
| 39 |
+
return password == self.default_password
|
| 40 |
+
|
| 41 |
+
return self.verify_password(password, stored_password_hash)
|
| 42 |
+
|
| 43 |
+
def change_admin_password(self, current_password: str, new_password: str) -> bool:
|
| 44 |
+
"""Change the admin password."""
|
| 45 |
+
if not self.authenticate_admin(current_password):
|
| 46 |
+
return False
|
| 47 |
+
|
| 48 |
+
new_hash = self.hash_password(new_password)
|
| 49 |
+
return self.db.set_setting("admin_password_hash", new_hash)
|
| 50 |
+
|
| 51 |
+
def get_current_admin_password_hint(self) -> str:
|
| 52 |
+
"""Get a hint about the current admin password (for development/testing)."""
|
| 53 |
+
stored_password_hash = self.db.get_setting("admin_password_hash")
|
| 54 |
+
if not stored_password_hash:
|
| 55 |
+
return "Default password: 1122"
|
| 56 |
+
else:
|
| 57 |
+
return "Custom password set"
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
# Test the authentication functionality
|
| 61 |
+
from database import AttendanceDatabase
|
| 62 |
+
|
| 63 |
+
db = AttendanceDatabase()
|
| 64 |
+
auth = AuthManager(db)
|
| 65 |
+
|
| 66 |
+
# Test default password
|
| 67 |
+
print("Testing default password '1122':", auth.authenticate_admin("1122"))
|
| 68 |
+
print("Testing wrong password 'wrong':", auth.authenticate_admin("wrong"))
|
| 69 |
+
|
| 70 |
+
# Test changing password
|
| 71 |
+
print("Changing password from '1122' to 'newpass':", auth.change_admin_password("1122", "newpass"))
|
| 72 |
+
print("Testing old password '1122':", auth.authenticate_admin("1122"))
|
| 73 |
+
print("Testing new password 'newpass':", auth.authenticate_admin("newpass"))
|
| 74 |
+
|
| 75 |
+
# Change back to default for testing
|
| 76 |
+
print("Changing back to default:", auth.change_admin_password("newpass", "1122"))
|
| 77 |
+
print("Testing default password again:", auth.authenticate_admin("1122"))
|
| 78 |
+
|
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"employees": {
|
| 3 |
+
"f8-98-b9-7f-fe-0d": "Ahsan",
|
| 4 |
+
"ff-ff-ff-ff-ff-ff": "Broadcast Device",
|
| 5 |
+
"01-00-5e-00-00-16": "Multicast Device 1",
|
| 6 |
+
"01-00-5e-00-00-fb": "Multicast Device 2",
|
| 7 |
+
"01-00-5e-00-00-fc": "Multicast Device 3",
|
| 8 |
+
"01-00-5e-7f-ff-fa": "Multicast Device 4"
|
| 9 |
+
},
|
| 10 |
+
"scan_interval_seconds": 60,
|
| 11 |
+
"web_port": 5000,
|
| 12 |
+
"office_timeout_hour": 17,
|
| 13 |
+
"office_timeout_minute": 0
|
| 14 |
+
}
|
| 15 |
+
|
|
@@ -0,0 +1,573 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import json
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from typing import List, Dict, Optional, Tuple
|
| 5 |
+
import os
|
| 6 |
+
import csv
|
| 7 |
+
|
| 8 |
+
class AttendanceDatabase:
|
| 9 |
+
def __init__(self, db_path: str = "attendance.db"):
|
| 10 |
+
"""Initialize the database connection and create tables if they don't exist."""
|
| 11 |
+
self.db_path = db_path
|
| 12 |
+
self.init_database()
|
| 13 |
+
|
| 14 |
+
def init_database(self):
|
| 15 |
+
"""Create database tables if they don't exist."""
|
| 16 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 17 |
+
cursor = conn.cursor()
|
| 18 |
+
|
| 19 |
+
# Create employees table with password_hash and picture_path
|
| 20 |
+
cursor.execute("""
|
| 21 |
+
CREATE TABLE IF NOT EXISTS employees (
|
| 22 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 23 |
+
name TEXT NOT NULL,
|
| 24 |
+
mac_address TEXT UNIQUE NOT NULL,
|
| 25 |
+
password_hash TEXT, -- New: for employee management
|
| 26 |
+
picture_path TEXT, -- New: path to employee picture
|
| 27 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 28 |
+
)
|
| 29 |
+
""")
|
| 30 |
+
|
| 31 |
+
# Create attendance_events table (replaces attendance_logs)
|
| 32 |
+
cursor.execute("""
|
| 33 |
+
CREATE TABLE IF NOT EXISTS attendance_events (
|
| 34 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 35 |
+
employee_id INTEGER,
|
| 36 |
+
mac_address TEXT NOT NULL,
|
| 37 |
+
event_type TEXT NOT NULL, -- 'time_in', 'time_out', 'break_start', 'break_end', 'timeout_5pm'
|
| 38 |
+
timestamp TIMESTAMP NOT NULL,
|
| 39 |
+
date TEXT NOT NULL,
|
| 40 |
+
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
| 41 |
+
)
|
| 42 |
+
""")
|
| 43 |
+
|
| 44 |
+
# Create daily_attendance_summary table
|
| 45 |
+
cursor.execute("""
|
| 46 |
+
CREATE TABLE IF NOT EXISTS daily_attendance_summary (
|
| 47 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 48 |
+
employee_id INTEGER NOT NULL,
|
| 49 |
+
date TEXT NOT NULL,
|
| 50 |
+
time_in TEXT,
|
| 51 |
+
time_out TEXT,
|
| 52 |
+
total_break_duration INTEGER DEFAULT 0, -- in seconds
|
| 53 |
+
total_work_duration INTEGER DEFAULT 0, -- in seconds
|
| 54 |
+
status TEXT NOT NULL, -- 'Present', 'Absent', 'Timed Out'
|
| 55 |
+
UNIQUE(employee_id, date),
|
| 56 |
+
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
| 57 |
+
)
|
| 58 |
+
""")
|
| 59 |
+
|
| 60 |
+
# Create settings table for general application settings like admin password
|
| 61 |
+
cursor.execute("""
|
| 62 |
+
CREATE TABLE IF NOT EXISTS settings (
|
| 63 |
+
key TEXT PRIMARY KEY,
|
| 64 |
+
value TEXT
|
| 65 |
+
)
|
| 66 |
+
""")
|
| 67 |
+
|
| 68 |
+
# Create indexes for better performance
|
| 69 |
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_mac_address ON employees (mac_address)")
|
| 70 |
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_event_date ON attendance_events (date)")
|
| 71 |
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_event_timestamp ON attendance_events (timestamp)")
|
| 72 |
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_summary_employee_date ON daily_attendance_summary (employee_id, date)")
|
| 73 |
+
|
| 74 |
+
conn.commit()
|
| 75 |
+
|
| 76 |
+
def add_employee(self, name: str, mac_address: str, password_hash: Optional[str] = None, picture_path: Optional[str] = None) -> bool:
|
| 77 |
+
"""Add a new employee to the database."""
|
| 78 |
+
try:
|
| 79 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 80 |
+
cursor = conn.cursor()
|
| 81 |
+
cursor.execute(
|
| 82 |
+
"INSERT INTO employees (name, mac_address, password_hash, picture_path) VALUES (?, ?, ?, ?)",
|
| 83 |
+
(name, mac_address.lower(), password_hash, picture_path)
|
| 84 |
+
)
|
| 85 |
+
conn.commit()
|
| 86 |
+
return True
|
| 87 |
+
except sqlite3.IntegrityError:
|
| 88 |
+
# print(f"Employee with MAC address {mac_address} already exists")
|
| 89 |
+
return False
|
| 90 |
+
except Exception as e:
|
| 91 |
+
print(f"Error adding employee: {e}")
|
| 92 |
+
return False
|
| 93 |
+
|
| 94 |
+
def update_employee(self, employee_id: int, name: Optional[str] = None, mac_address: Optional[str] = None, password_hash: Optional[str] = None, picture_path: Optional[str] = None) -> bool:
|
| 95 |
+
"""Update an existing employee's information."""
|
| 96 |
+
try:
|
| 97 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 98 |
+
cursor = conn.cursor()
|
| 99 |
+
update_fields = []
|
| 100 |
+
update_values = []
|
| 101 |
+
if name is not None: update_fields.append("name = ?"); update_values.append(name)
|
| 102 |
+
if mac_address is not None: update_fields.append("mac_address = ?"); update_values.append(mac_address.lower())
|
| 103 |
+
if password_hash is not None: update_fields.append("password_hash = ?"); update_values.append(password_hash)
|
| 104 |
+
if picture_path is not None: update_fields.append("picture_path = ?"); update_values.append(picture_path)
|
| 105 |
+
|
| 106 |
+
if not update_fields:
|
| 107 |
+
return False # Nothing to update
|
| 108 |
+
|
| 109 |
+
query = f"UPDATE employees SET {', '.join(update_fields)} WHERE id = ?"
|
| 110 |
+
cursor.execute(query, (*update_values, employee_id))
|
| 111 |
+
conn.commit()
|
| 112 |
+
return True
|
| 113 |
+
except Exception as e:
|
| 114 |
+
print(f"Error updating employee: {e}")
|
| 115 |
+
return False
|
| 116 |
+
|
| 117 |
+
def delete_employee(self, employee_id: int) -> bool:
|
| 118 |
+
"""Delete an employee from the database."""
|
| 119 |
+
try:
|
| 120 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 121 |
+
cursor = conn.cursor()
|
| 122 |
+
cursor.execute("DELETE FROM employees WHERE id = ?", (employee_id,))
|
| 123 |
+
conn.commit()
|
| 124 |
+
return True
|
| 125 |
+
except Exception as e:
|
| 126 |
+
print(f"Error deleting employee: {e}")
|
| 127 |
+
return False
|
| 128 |
+
|
| 129 |
+
def get_employee_by_mac(self, mac_address: str) -> Optional[Dict]:
|
| 130 |
+
"""Get employee information by MAC address."""
|
| 131 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 132 |
+
cursor = conn.cursor()
|
| 133 |
+
cursor.execute(
|
| 134 |
+
"SELECT id, name, mac_address, password_hash, picture_path, created_at FROM employees WHERE mac_address = ?",
|
| 135 |
+
(mac_address.lower(),)
|
| 136 |
+
)
|
| 137 |
+
row = cursor.fetchone()
|
| 138 |
+
if row:
|
| 139 |
+
return {
|
| 140 |
+
'id': row[0],
|
| 141 |
+
'name': row[1],
|
| 142 |
+
'mac_address': row[2],
|
| 143 |
+
'password_hash': row[3],
|
| 144 |
+
'picture_path': row[4],
|
| 145 |
+
'created_at': row[5]
|
| 146 |
+
}
|
| 147 |
+
return None
|
| 148 |
+
|
| 149 |
+
def get_employee_by_id(self, employee_id: int) -> Optional[Dict]:
|
| 150 |
+
"""Get employee information by ID."""
|
| 151 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 152 |
+
cursor = conn.cursor()
|
| 153 |
+
cursor.execute(
|
| 154 |
+
"SELECT id, name, mac_address, password_hash, picture_path, created_at FROM employees WHERE id = ?",
|
| 155 |
+
(employee_id,)
|
| 156 |
+
)
|
| 157 |
+
row = cursor.fetchone()
|
| 158 |
+
if row:
|
| 159 |
+
return {
|
| 160 |
+
'id': row[0],
|
| 161 |
+
'name': row[1],
|
| 162 |
+
'mac_address': row[2],
|
| 163 |
+
'password_hash': row[3],
|
| 164 |
+
'picture_path': row[4],
|
| 165 |
+
'created_at': row[5]
|
| 166 |
+
}
|
| 167 |
+
return None
|
| 168 |
+
|
| 169 |
+
def get_all_employees(self, search_query: Optional[str] = None) -> List[Dict]:
|
| 170 |
+
"""Get all employees from the database, optionally filtered by search query."""
|
| 171 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 172 |
+
cursor = conn.cursor()
|
| 173 |
+
if search_query:
|
| 174 |
+
search_query = f'%{search_query.lower()}%'
|
| 175 |
+
cursor.execute(
|
| 176 |
+
"SELECT id, name, mac_address, password_hash, picture_path, created_at FROM employees WHERE LOWER(name) LIKE ? OR LOWER(mac_address) LIKE ? ORDER BY name",
|
| 177 |
+
(search_query, search_query)
|
| 178 |
+
)
|
| 179 |
+
else:
|
| 180 |
+
cursor.execute('SELECT id, name, mac_address, password_hash, picture_path, created_at FROM employees ORDER BY name')
|
| 181 |
+
rows = cursor.fetchall()
|
| 182 |
+
return [
|
| 183 |
+
{
|
| 184 |
+
'id': row[0],
|
| 185 |
+
'name': row[1],
|
| 186 |
+
'mac_address': row[2],
|
| 187 |
+
'password_hash': row[3],
|
| 188 |
+
'picture_path': row[4],
|
| 189 |
+
'created_at': row[5]
|
| 190 |
+
}
|
| 191 |
+
for row in rows
|
| 192 |
+
]
|
| 193 |
+
|
| 194 |
+
def log_attendance_event(self, mac_address: str, event_type: str, timestamp: datetime = None) -> bool:
|
| 195 |
+
"""Log an attendance event to the attendance_events table."""
|
| 196 |
+
if timestamp is None:
|
| 197 |
+
timestamp = datetime.now()
|
| 198 |
+
|
| 199 |
+
date_str = timestamp.strftime('%Y-%m-%d')
|
| 200 |
+
|
| 201 |
+
try:
|
| 202 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 203 |
+
cursor = conn.cursor()
|
| 204 |
+
|
| 205 |
+
employee = self.get_employee_by_mac(mac_address)
|
| 206 |
+
employee_id = employee['id'] if employee else None
|
| 207 |
+
|
| 208 |
+
cursor.execute("""
|
| 209 |
+
INSERT INTO attendance_events (employee_id, mac_address, event_type, timestamp, date)
|
| 210 |
+
VALUES (?, ?, ?, ?, ?)
|
| 211 |
+
""", (employee_id, mac_address.lower(), event_type, timestamp.strftime('%Y-%m-%d %H:%M:%S'), date_str))
|
| 212 |
+
|
| 213 |
+
conn.commit()
|
| 214 |
+
return True
|
| 215 |
+
except Exception as e:
|
| 216 |
+
print(f"Error logging attendance event: {e}")
|
| 217 |
+
return False
|
| 218 |
+
|
| 219 |
+
def get_attendance_events(self, date: str = None, limit: int = 100) -> List[Dict]:
|
| 220 |
+
"""Get attendance events, optionally filtered by date."""
|
| 221 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 222 |
+
cursor = conn.cursor()
|
| 223 |
+
|
| 224 |
+
if date:
|
| 225 |
+
cursor.execute("""
|
| 226 |
+
SELECT ae.id, ae.mac_address, ae.event_type, ae.timestamp, ae.date,
|
| 227 |
+
e.name as employee_name
|
| 228 |
+
FROM attendance_events ae
|
| 229 |
+
LEFT JOIN employees e ON ae.employee_id = e.id
|
| 230 |
+
WHERE ae.date = ?
|
| 231 |
+
ORDER BY ae.timestamp DESC
|
| 232 |
+
LIMIT ?
|
| 233 |
+
""", (date, limit))
|
| 234 |
+
else:
|
| 235 |
+
cursor.execute("""
|
| 236 |
+
SELECT ae.id, ae.mac_address, ae.event_type, ae.timestamp, ae.date,
|
| 237 |
+
e.name as employee_name
|
| 238 |
+
FROM attendance_events ae
|
| 239 |
+
LEFT JOIN employees e ON ae.employee_id = e.id
|
| 240 |
+
ORDER BY ae.timestamp DESC
|
| 241 |
+
LIMIT ?
|
| 242 |
+
""", (limit,))
|
| 243 |
+
|
| 244 |
+
rows = cursor.fetchall()
|
| 245 |
+
return [
|
| 246 |
+
{
|
| 247 |
+
'id': row[0],
|
| 248 |
+
'mac_address': row[1],
|
| 249 |
+
'event_type': row[2],
|
| 250 |
+
'timestamp': row[3],
|
| 251 |
+
'date': row[4],
|
| 252 |
+
'employee_name': row[5] or f"Unknown ({row[1]})"
|
| 253 |
+
}
|
| 254 |
+
for row in rows
|
| 255 |
+
]
|
| 256 |
+
|
| 257 |
+
def update_daily_summary(self, employee_id: int, date_str: str, time_in: Optional[str] = None,
|
| 258 |
+
time_out: Optional[str] = None, total_break_duration: Optional[int] = None,
|
| 259 |
+
total_work_duration: Optional[int] = None, status: Optional[str] = None):
|
| 260 |
+
"""Update or insert a daily attendance summary record."""
|
| 261 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 262 |
+
cursor = conn.cursor()
|
| 263 |
+
|
| 264 |
+
# Check if record exists
|
| 265 |
+
cursor.execute("SELECT * FROM daily_attendance_summary WHERE employee_id = ? AND date = ?",
|
| 266 |
+
(employee_id, date_str))
|
| 267 |
+
existing_record = cursor.fetchone()
|
| 268 |
+
|
| 269 |
+
if existing_record:
|
| 270 |
+
# Update existing record
|
| 271 |
+
update_fields = []
|
| 272 |
+
update_values = []
|
| 273 |
+
if time_in is not None: update_fields.append("time_in = ?"); update_values.append(time_in)
|
| 274 |
+
if time_out is not None: update_fields.append("time_out = ?"); update_values.append(time_out)
|
| 275 |
+
if total_break_duration is not None: update_fields.append("total_break_duration = ?"); update_values.append(total_break_duration)
|
| 276 |
+
if total_work_duration is not None: update_fields.append("total_work_duration = ?"); update_values.append(total_work_duration)
|
| 277 |
+
if status is not None: update_fields.append("status = ?"); update_values.append(status)
|
| 278 |
+
|
| 279 |
+
if update_fields:
|
| 280 |
+
query = f"UPDATE daily_attendance_summary SET {', '.join(update_fields)} WHERE employee_id = ? AND date = ?"
|
| 281 |
+
cursor.execute(query, (*update_values, employee_id, date_str))
|
| 282 |
+
else:
|
| 283 |
+
# Insert new record
|
| 284 |
+
cursor.execute("""
|
| 285 |
+
INSERT INTO daily_attendance_summary (employee_id, date, time_in, time_out, total_break_duration, total_work_duration, status)
|
| 286 |
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 287 |
+
""", (employee_id, date_str, time_in, time_out, total_break_duration, total_work_duration, status))
|
| 288 |
+
|
| 289 |
+
conn.commit()
|
| 290 |
+
|
| 291 |
+
def get_daily_summary_for_employee(self, employee_id: int, date_str: str) -> Optional[Dict]:
|
| 292 |
+
"""Get daily attendance summary for a specific employee on a specific date."""
|
| 293 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 294 |
+
cursor = conn.cursor()
|
| 295 |
+
|
| 296 |
+
cursor.execute("""
|
| 297 |
+
SELECT das.id, e.name, e.mac_address, das.date, das.time_in, das.time_out,
|
| 298 |
+
das.total_break_duration, das.total_work_duration, das.status
|
| 299 |
+
FROM daily_attendance_summary das
|
| 300 |
+
JOIN employees e ON das.employee_id = e.id
|
| 301 |
+
WHERE das.employee_id = ? AND das.date = ?
|
| 302 |
+
""", (employee_id, date_str))
|
| 303 |
+
|
| 304 |
+
row = cursor.fetchone()
|
| 305 |
+
if row:
|
| 306 |
+
return {
|
| 307 |
+
'id': row[0],
|
| 308 |
+
'name': row[1],
|
| 309 |
+
'mac_address': row[2],
|
| 310 |
+
'date': row[3],
|
| 311 |
+
'time_in': row[4],
|
| 312 |
+
'time_out': row[5],
|
| 313 |
+
'total_break_duration': row[6],
|
| 314 |
+
'total_work_duration': row[7],
|
| 315 |
+
'status': row[8]
|
| 316 |
+
}
|
| 317 |
+
return None
|
| 318 |
+
|
| 319 |
+
def get_daily_summary(self, date_str: str = None) -> List[Dict]:
|
| 320 |
+
"""Get daily attendance summary for all employees, optionally filtered by date."""
|
| 321 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 322 |
+
cursor = conn.cursor()
|
| 323 |
+
|
| 324 |
+
if date_str is None:
|
| 325 |
+
date_str = datetime.now().strftime('%Y-%m-%d')
|
| 326 |
+
|
| 327 |
+
cursor.execute("""
|
| 328 |
+
SELECT das.id, e.name, e.mac_address, das.date, das.time_in, das.time_out,
|
| 329 |
+
das.total_break_duration, das.total_work_duration, das.status
|
| 330 |
+
FROM daily_attendance_summary das
|
| 331 |
+
JOIN employees e ON das.employee_id = e.id
|
| 332 |
+
WHERE das.date = ?
|
| 333 |
+
ORDER BY e.name
|
| 334 |
+
""", (date_str,))
|
| 335 |
+
|
| 336 |
+
rows = cursor.fetchall()
|
| 337 |
+
summary_list = []
|
| 338 |
+
for row in rows:
|
| 339 |
+
summary_list.append({
|
| 340 |
+
'id': row[0],
|
| 341 |
+
'name': row[1],
|
| 342 |
+
'mac_address': row[2],
|
| 343 |
+
'date': row[3],
|
| 344 |
+
'time_in': row[4],
|
| 345 |
+
'time_out': row[5],
|
| 346 |
+
'total_break_duration': row[6],
|
| 347 |
+
'total_work_duration': row[7],
|
| 348 |
+
'status': row[8]
|
| 349 |
+
})
|
| 350 |
+
return summary_list
|
| 351 |
+
|
| 352 |
+
def calculate_durations(self, employee_id: int, date_str: str):
|
| 353 |
+
"""Calculate total break and work durations for an employee on a given day."""
|
| 354 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 355 |
+
cursor = conn.cursor()
|
| 356 |
+
|
| 357 |
+
cursor.execute("""
|
| 358 |
+
SELECT event_type, timestamp FROM attendance_events
|
| 359 |
+
WHERE employee_id = ? AND date = ?
|
| 360 |
+
ORDER BY timestamp
|
| 361 |
+
""", (employee_id, date_str))
|
| 362 |
+
|
| 363 |
+
events = cursor.fetchall()
|
| 364 |
+
|
| 365 |
+
time_in = None
|
| 366 |
+
time_out = None
|
| 367 |
+
total_break_duration = timedelta(0)
|
| 368 |
+
total_work_duration = timedelta(0)
|
| 369 |
+
|
| 370 |
+
last_event_time = None
|
| 371 |
+
on_break = False
|
| 372 |
+
|
| 373 |
+
for event_type, timestamp_str in events:
|
| 374 |
+
current_time = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
|
| 375 |
+
|
| 376 |
+
if event_type == 'time_in':
|
| 377 |
+
if time_in is None: # Only set time_in once for the day
|
| 378 |
+
time_in = current_time
|
| 379 |
+
last_event_time = current_time
|
| 380 |
+
on_break = False
|
| 381 |
+
elif event_type == 'time_out':
|
| 382 |
+
if time_in and last_event_time and not on_break:
|
| 383 |
+
total_work_duration += (current_time - last_event_time)
|
| 384 |
+
time_out = current_time
|
| 385 |
+
last_event_time = current_time
|
| 386 |
+
on_break = False
|
| 387 |
+
elif event_type == 'break_start':
|
| 388 |
+
if time_in and last_event_time and not on_break:
|
| 389 |
+
total_work_duration += (current_time - last_event_time)
|
| 390 |
+
last_event_time = current_time
|
| 391 |
+
on_break = True
|
| 392 |
+
elif event_type == 'break_end':
|
| 393 |
+
if time_in and last_event_time and on_break:
|
| 394 |
+
total_break_duration += (current_time - last_event_time)
|
| 395 |
+
last_event_time = current_time
|
| 396 |
+
on_break = False
|
| 397 |
+
elif event_type == 'timeout_5pm':
|
| 398 |
+
if time_in and last_event_time and not on_break:
|
| 399 |
+
total_work_duration += (current_time - last_event_time)
|
| 400 |
+
time_out = current_time # Set time_out to 5 PM
|
| 401 |
+
last_event_time = current_time
|
| 402 |
+
on_break = False
|
| 403 |
+
|
| 404 |
+
# If still present at the end of the day (no explicit time_out or 5pm timeout)
|
| 405 |
+
# and there was a time_in event, calculate work duration up to now
|
| 406 |
+
if time_in and time_out is None and last_event_time and not on_break:
|
| 407 |
+
total_work_duration += (datetime.now() - last_event_time)
|
| 408 |
+
|
| 409 |
+
return {
|
| 410 |
+
'time_in': time_in.strftime('%H:%M:%S') if time_in else None,
|
| 411 |
+
'time_out': time_out.strftime('%H:%M:%S') if time_out else None,
|
| 412 |
+
'total_break_duration': int(total_break_duration.total_seconds()),
|
| 413 |
+
'total_work_duration': int(total_work_duration.total_seconds())
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
def export_daily_summary_to_csv(self, date_str: str):
|
| 417 |
+
"""Export daily attendance summary to a CSV file."""
|
| 418 |
+
summary_data = self.get_daily_summary(date_str)
|
| 419 |
+
|
| 420 |
+
if not summary_data:
|
| 421 |
+
print(f"No summary data for {date_str} to export.")
|
| 422 |
+
return
|
| 423 |
+
|
| 424 |
+
log_file = f"logs/attendance_summary_{date_str}.csv"
|
| 425 |
+
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
| 426 |
+
|
| 427 |
+
fieldnames = ['Name', 'MAC Address', 'Date', 'Time In', 'Time Out', 'Total Break (HH:MM:SS)', 'Total Work (HH:MM:SS)', 'Status']
|
| 428 |
+
|
| 429 |
+
with open(log_file, 'w', newline='', encoding='utf-8') as csvfile:
|
| 430 |
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
| 431 |
+
writer.writeheader()
|
| 432 |
+
|
| 433 |
+
for row in summary_data:
|
| 434 |
+
# Convert seconds to HH:MM:SS format
|
| 435 |
+
total_break_duration = row['total_break_duration'] or 0
|
| 436 |
+
total_work_duration = row['total_work_duration'] or 0
|
| 437 |
+
total_break_formatted = str(timedelta(seconds=total_break_duration))
|
| 438 |
+
total_work_formatted = str(timedelta(seconds=total_work_duration))
|
| 439 |
+
|
| 440 |
+
writer.writerow({
|
| 441 |
+
'Name': row['name'],
|
| 442 |
+
'MAC Address': row['mac_address'],
|
| 443 |
+
'Date': row['date'],
|
| 444 |
+
'Time In': row['time_in'] if row['time_in'] else 'N/A',
|
| 445 |
+
'Time Out': row['time_out'] if row['time_out'] else 'N/A',
|
| 446 |
+
'Total Break (HH:MM:SS)': total_break_formatted,
|
| 447 |
+
'Total Work (HH:MM:SS)': total_work_formatted,
|
| 448 |
+
'Status': row['status']
|
| 449 |
+
})
|
| 450 |
+
print(f"Daily summary for {date_str} exported to {log_file}")
|
| 451 |
+
|
| 452 |
+
def sync_employees_from_config(self, config_path: str = "config.json"):
|
| 453 |
+
"""Sync employees from config file to database."""
|
| 454 |
+
try:
|
| 455 |
+
with open(config_path, 'r') as f:
|
| 456 |
+
config = json.load(f)
|
| 457 |
+
|
| 458 |
+
employees = config.get('employees', {})
|
| 459 |
+
synced_count = 0
|
| 460 |
+
|
| 461 |
+
for mac_address, name in employees.items():
|
| 462 |
+
if self.add_employee(name, mac_address):
|
| 463 |
+
synced_count += 1
|
| 464 |
+
# Also initialize daily summary for today if not exists
|
| 465 |
+
today_str = datetime.now().strftime('%Y-%m-%d')
|
| 466 |
+
employee_info = self.get_employee_by_mac(mac_address)
|
| 467 |
+
if employee_info:
|
| 468 |
+
self.update_daily_summary(employee_info['id'], today_str, status='Absent')
|
| 469 |
+
|
| 470 |
+
print(f"Synced {synced_count} new employees from config")
|
| 471 |
+
return synced_count
|
| 472 |
+
|
| 473 |
+
except FileNotFoundError:
|
| 474 |
+
print(f"Config file {config_path} not found")
|
| 475 |
+
return 0
|
| 476 |
+
except json.JSONDecodeError:
|
| 477 |
+
print(f"Invalid JSON in {config_path}")
|
| 478 |
+
return 0
|
| 479 |
+
except Exception as e:
|
| 480 |
+
print(f"Error syncing employees: {e}")
|
| 481 |
+
return 0
|
| 482 |
+
|
| 483 |
+
def get_setting(self, key: str) -> Optional[str]:
|
| 484 |
+
"""Get a setting value from the settings table."""
|
| 485 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 486 |
+
cursor = conn.cursor()
|
| 487 |
+
cursor.execute("SELECT value FROM settings WHERE key = ?", (key,))
|
| 488 |
+
row = cursor.fetchone()
|
| 489 |
+
return row[0] if row else None
|
| 490 |
+
|
| 491 |
+
def set_setting(self, key: str, value: str) -> bool:
|
| 492 |
+
"""Set a setting value in the settings table."""
|
| 493 |
+
try:
|
| 494 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 495 |
+
cursor = conn.cursor()
|
| 496 |
+
cursor.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", (key, value))
|
| 497 |
+
conn.commit()
|
| 498 |
+
return True
|
| 499 |
+
except Exception as e:
|
| 500 |
+
print(f"Error setting setting {key}: {e}")
|
| 501 |
+
return False
|
| 502 |
+
|
| 503 |
+
def cleanup_old_logs(self, days_to_keep: int = 30):
|
| 504 |
+
"""Remove attendance events and summaries older than specified days."""
|
| 505 |
+
cutoff_date = datetime.now() - timedelta(days=days_to_keep)
|
| 506 |
+
cutoff_date_str = cutoff_date.strftime('%Y-%m-%d %H:%M:%S')
|
| 507 |
+
|
| 508 |
+
try:
|
| 509 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 510 |
+
cursor = conn.cursor()
|
| 511 |
+
|
| 512 |
+
cursor.execute(
|
| 513 |
+
"DELETE FROM attendance_events WHERE timestamp < ?",
|
| 514 |
+
(cutoff_date_str,)
|
| 515 |
+
)
|
| 516 |
+
deleted_events = cursor.rowcount
|
| 517 |
+
|
| 518 |
+
cursor.execute(
|
| 519 |
+
"DELETE FROM daily_attendance_summary WHERE date < ?",
|
| 520 |
+
(cutoff_date.strftime('%Y-%m-%d'),)
|
| 521 |
+
)
|
| 522 |
+
deleted_summaries = cursor.rowcount
|
| 523 |
+
|
| 524 |
+
conn.commit()
|
| 525 |
+
print(f"Cleaned up {deleted_events} old attendance events and {deleted_summaries} old summaries.")
|
| 526 |
+
return deleted_events + deleted_summaries
|
| 527 |
+
except Exception as e:
|
| 528 |
+
print(f"Error cleaning up old logs: {e}")
|
| 529 |
+
return 0
|
| 530 |
+
|
| 531 |
+
if __name__ == "__main__":
|
| 532 |
+
# Test the database functionality
|
| 533 |
+
db = AttendanceDatabase()
|
| 534 |
+
|
| 535 |
+
# Sync employees from config
|
| 536 |
+
db.sync_employees_from_config()
|
| 537 |
+
|
| 538 |
+
# Display all employees
|
| 539 |
+
employees = db.get_all_employees()
|
| 540 |
+
print(f"\nTotal employees in database: {len(employees)}")
|
| 541 |
+
for emp in employees:
|
| 542 |
+
print(f"- {emp['name']} ({emp['mac_address']})")
|
| 543 |
+
|
| 544 |
+
# Display recent events
|
| 545 |
+
recent_events = db.get_attendance_events(limit=10)
|
| 546 |
+
print(f"\nRecent attendance events: {len(recent_events)}")
|
| 547 |
+
for event in recent_events:
|
| 548 |
+
print(f"- {event['timestamp']}: {event['employee_name']} - {event['event_type']}")
|
| 549 |
+
|
| 550 |
+
# Test daily summary export
|
| 551 |
+
today = datetime.now().strftime('%Y-%m-%d')
|
| 552 |
+
db.export_daily_summary_to_csv(today)
|
| 553 |
+
|
| 554 |
+
# Example of logging events and updating summary
|
| 555 |
+
# employee_mac = "f8-98-b9-7f-fe-0d"
|
| 556 |
+
# employee_info = db.get_employee_by_mac(employee_mac)
|
| 557 |
+
# if employee_info:
|
| 558 |
+
# db.log_attendance_event(employee_mac, 'time_in')
|
| 559 |
+
# db.update_daily_summary(employee_info['id'], today, time_in=datetime.now().strftime('%H:%M:%S'), status='Present')
|
| 560 |
+
# time.sleep(5)
|
| 561 |
+
# db.log_attendance_event(employee_mac, 'break_start')
|
| 562 |
+
# db.update_daily_summary(employee_info['id'], today, total_break_duration=db.calculate_durations(employee_info['id'], today)['total_break_duration'])
|
| 563 |
+
# time.sleep(5)
|
| 564 |
+
# db.log_attendance_event(employee_mac, 'break_end')
|
| 565 |
+
# db.update_daily_summary(employee_info['id'], today, total_break_duration=db.calculate_durations(employee_info['id'], today)['total_break_duration'])
|
| 566 |
+
# time.sleep(5)
|
| 567 |
+
# db.log_attendance_event(employee_mac, 'time_out')
|
| 568 |
+
# db.update_daily_summary(employee_info['id'], today, time_out=datetime.now().strftime('%H:%M:%S'), status='Absent', total_work_duration=db.calculate_durations(employee_info['id'], today)['total_work_duration'])
|
| 569 |
+
|
| 570 |
+
# db.export_daily_summary_to_csv(today)
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
|
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"name": "Ahsan",
|
| 4 |
+
"mac_address": "f8-98-b9-7f-fe-0d",
|
| 5 |
+
"picture": "static/img/ahsan.jpg"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"name": "John Smith Updated",
|
| 9 |
+
"mac_address": "aa-bb-cc-dd-ee-ff",
|
| 10 |
+
"picture": "static/img/john_doe.jpg"
|
| 11 |
+
}
|
| 12 |
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Name,MAC Address,Date,Time In,Time Out,Total Break (HH:MM:SS),Total Work (HH:MM:SS),Status
|
| 2 |
+
Ahsan,f8-98-b9-7f-fe-0d,2025-07-28,16:13:57,N/A,0:00:00,0:00:00,Present
|
| 3 |
+
Broadcast Device,ff-ff-ff-ff-ff-ff,2025-07-28,N/A,N/A,0:00:00,0:00:00,Absent
|
| 4 |
+
John Smith Updated,aa-bb-cc-dd-ee-ff,2025-07-28,N/A,N/A,0:00:00,0:00:00,Absent
|
| 5 |
+
Multicast Device 1,01-00-5e-00-00-16,2025-07-28,N/A,N/A,0:00:00,0:00:00,Absent
|
| 6 |
+
Multicast Device 2,01-00-5e-00-00-fb,2025-07-28,N/A,N/A,0:00:00,0:00:00,Absent
|
| 7 |
+
Multicast Device 3,01-00-5e-00-00-fc,2025-07-28,N/A,N/A,0:00:00,0:00:00,Absent
|
| 8 |
+
Multicast Device 4,01-00-5e-7f-ff-fa,2025-07-28,N/A,N/A,0:00:00,0:00:00,Absent
|
|
@@ -0,0 +1,865 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
WiFi-Based Attendance & Break Tracker - Advanced Employee Management Version
|
| 4 |
+
Integrated main application entry point
|
| 5 |
+
|
| 6 |
+
This application tracks employee attendance by detecting MAC addresses
|
| 7 |
+
of devices connected to the local WiFi network with advanced features:
|
| 8 |
+
- Employee management (add, search, password protection)
|
| 9 |
+
- Time-in/time-out tracking
|
| 10 |
+
- Break start/end monitoring
|
| 11 |
+
- Automatic 5:00 PM timeout
|
| 12 |
+
- Detailed attendance sheets
|
| 13 |
+
- Real-time web interface
|
| 14 |
+
- Non-admin operation (where possible)
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import sys
|
| 18 |
+
import os
|
| 19 |
+
import argparse
|
| 20 |
+
import threading
|
| 21 |
+
import time
|
| 22 |
+
import json
|
| 23 |
+
from datetime import datetime
|
| 24 |
+
from flask import Flask, render_template, jsonify, request, session, redirect, url_for
|
| 25 |
+
from flask_cors import CORS
|
| 26 |
+
from functools import wraps
|
| 27 |
+
|
| 28 |
+
# Import our modules
|
| 29 |
+
from attendance_tracker import AttendanceTracker
|
| 30 |
+
from database import AttendanceDatabase
|
| 31 |
+
from auth import AuthManager
|
| 32 |
+
|
| 33 |
+
# Flask app setup
|
| 34 |
+
app = Flask(__name__)
|
| 35 |
+
app.secret_key = 'wifi_attendance_tracker_secret_key_2025' # Change this in production
|
| 36 |
+
CORS(app)
|
| 37 |
+
|
| 38 |
+
# Global variables
|
| 39 |
+
tracker = None
|
| 40 |
+
db = None
|
| 41 |
+
auth = None
|
| 42 |
+
latest_events = []
|
| 43 |
+
is_monitoring = False
|
| 44 |
+
|
| 45 |
+
# Dashboard password (can be changed via settings)
|
| 46 |
+
DASHBOARD_PASSWORD = "admin123"
|
| 47 |
+
|
| 48 |
+
def login_required(f):
|
| 49 |
+
"""Decorator to require login for protected routes."""
|
| 50 |
+
@wraps(f)
|
| 51 |
+
def decorated_function(*args, **kwargs):
|
| 52 |
+
if 'logged_in' not in session or not session['logged_in']:
|
| 53 |
+
return redirect(url_for('login_page'))
|
| 54 |
+
return f(*args, **kwargs)
|
| 55 |
+
return decorated_function
|
| 56 |
+
|
| 57 |
+
def check_dashboard_password(password):
|
| 58 |
+
"""Check if the provided password matches the dashboard password."""
|
| 59 |
+
return password == DASHBOARD_PASSWORD
|
| 60 |
+
|
| 61 |
+
def print_banner():
|
| 62 |
+
"""Print application banner."""
|
| 63 |
+
banner = """
|
| 64 |
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 65 |
+
β WiFi Attendance Tracker - Employee Management β
|
| 66 |
+
β β
|
| 67 |
+
β Advanced MAC Detection with Employee Management Features β
|
| 68 |
+
β Version 3.0 - 2025 β
|
| 69 |
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 70 |
+
"""
|
| 71 |
+
print(banner)
|
| 72 |
+
|
| 73 |
+
def check_requirements():
|
| 74 |
+
"""Check if the system meets requirements."""
|
| 75 |
+
issues = []
|
| 76 |
+
|
| 77 |
+
# Check if config file exists
|
| 78 |
+
if not os.path.exists('config.json'):
|
| 79 |
+
issues.append("config.json file not found")
|
| 80 |
+
|
| 81 |
+
# Check if employees file exists
|
| 82 |
+
if not os.path.exists('employees.json'):
|
| 83 |
+
issues.append("employees.json file not found")
|
| 84 |
+
|
| 85 |
+
# Note: We're removing the admin privilege check as requested
|
| 86 |
+
# The application will attempt to run without admin privileges
|
| 87 |
+
# and provide clear error messages if network scanning fails
|
| 88 |
+
|
| 89 |
+
return issues
|
| 90 |
+
|
| 91 |
+
def initialize_system():
|
| 92 |
+
"""Initialize the attendance tracking system."""
|
| 93 |
+
global tracker, db, auth
|
| 94 |
+
|
| 95 |
+
# Initialize database first
|
| 96 |
+
db = AttendanceDatabase()
|
| 97 |
+
|
| 98 |
+
# Initialize authentication manager
|
| 99 |
+
auth = AuthManager(db)
|
| 100 |
+
|
| 101 |
+
# Initialize tracker
|
| 102 |
+
tracker = AttendanceTracker()
|
| 103 |
+
|
| 104 |
+
# Sync employees from JSON to database
|
| 105 |
+
tracker.sync_employees_from_json()
|
| 106 |
+
|
| 107 |
+
def monitoring_loop():
|
| 108 |
+
"""Background monitoring loop."""
|
| 109 |
+
global latest_events, is_monitoring
|
| 110 |
+
|
| 111 |
+
while is_monitoring:
|
| 112 |
+
try:
|
| 113 |
+
events = tracker.scan_once()
|
| 114 |
+
|
| 115 |
+
# Keep only the latest 50 events for the web interface
|
| 116 |
+
latest_events.extend(events)
|
| 117 |
+
latest_events = latest_events[-50:] # Keep only last 50 events
|
| 118 |
+
|
| 119 |
+
time.sleep(tracker.scan_interval)
|
| 120 |
+
except Exception as e:
|
| 121 |
+
print(f"Error in monitoring loop: {e}")
|
| 122 |
+
time.sleep(5) # Wait before retrying
|
| 123 |
+
|
| 124 |
+
# Web Interface Routes
|
| 125 |
+
@app.route('/')
|
| 126 |
+
def index():
|
| 127 |
+
"""Redirect to login page."""
|
| 128 |
+
return redirect(url_for('login_page'))
|
| 129 |
+
|
| 130 |
+
@app.route('/login')
|
| 131 |
+
def login_page():
|
| 132 |
+
"""Serve the login page."""
|
| 133 |
+
if 'logged_in' in session and session['logged_in']:
|
| 134 |
+
return redirect(url_for('dashboard'))
|
| 135 |
+
return render_template('login.html')
|
| 136 |
+
|
| 137 |
+
@login_required
|
| 138 |
+
@app.route('/api/login', methods=['POST'])
|
| 139 |
+
def login():
|
| 140 |
+
"""Handle login authentication."""
|
| 141 |
+
try:
|
| 142 |
+
data = request.get_json()
|
| 143 |
+
password = data.get('password', '')
|
| 144 |
+
|
| 145 |
+
if check_dashboard_password(password):
|
| 146 |
+
session['logged_in'] = True
|
| 147 |
+
return jsonify({'success': True, 'message': 'Login successful'})
|
| 148 |
+
else:
|
| 149 |
+
return jsonify({'success': False, 'message': 'Invalid password'})
|
| 150 |
+
except Exception as e:
|
| 151 |
+
return jsonify({'success': False, 'message': 'Login error occurred'})
|
| 152 |
+
|
| 153 |
+
@app.route('/logout')
|
| 154 |
+
def logout():
|
| 155 |
+
"""Handle logout."""
|
| 156 |
+
session.pop('logged_in', None)
|
| 157 |
+
return redirect(url_for('login_page'))
|
| 158 |
+
|
| 159 |
+
@app.route('/dashboard')
|
| 160 |
+
@login_required
|
| 161 |
+
def dashboard():
|
| 162 |
+
"""Serve the main dashboard page."""
|
| 163 |
+
return render_template('index.html')
|
| 164 |
+
|
| 165 |
+
@login_required
|
| 166 |
+
@app.route('/api/status')
|
| 167 |
+
@login_required
|
| 168 |
+
def get_status():
|
| 169 |
+
"""Get current system status."""
|
| 170 |
+
return jsonify({
|
| 171 |
+
'is_monitoring': is_monitoring,
|
| 172 |
+
'employee_count': len(tracker.employees) if tracker else 0,
|
| 173 |
+
'scan_interval': tracker.scan_interval if tracker else 60,
|
| 174 |
+
'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 175 |
+
'office_timeout': f"{tracker.office_timeout_hour:02d}:{tracker.office_timeout_minute:02d}" if tracker else "17:00"
|
| 176 |
+
})
|
| 177 |
+
|
| 178 |
+
@login_required
|
| 179 |
+
@app.route('/api/employees')
|
| 180 |
+
@login_required
|
| 181 |
+
def get_employees():
|
| 182 |
+
"""Get all employees and their current status."""
|
| 183 |
+
if not tracker:
|
| 184 |
+
return jsonify([])
|
| 185 |
+
|
| 186 |
+
search_query = request.args.get('search', '').strip()
|
| 187 |
+
|
| 188 |
+
status = tracker.get_current_status()
|
| 189 |
+
employees = []
|
| 190 |
+
|
| 191 |
+
for mac, info in status.items():
|
| 192 |
+
# Get employee picture from database
|
| 193 |
+
employee_info = db.get_employee_by_mac(mac)
|
| 194 |
+
picture_path = employee_info.get('picture_path') if employee_info else None
|
| 195 |
+
|
| 196 |
+
employee_data = {
|
| 197 |
+
'name': info['name'],
|
| 198 |
+
'mac': info['mac'],
|
| 199 |
+
'is_present': info['is_present'],
|
| 200 |
+
'status': info['status'],
|
| 201 |
+
'last_seen': info['last_seen'],
|
| 202 |
+
'time_in': info['time_in'],
|
| 203 |
+
'picture': picture_path
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
# Apply search filter if provided
|
| 207 |
+
if search_query:
|
| 208 |
+
if (search_query.lower() in info['name'].lower() or
|
| 209 |
+
search_query.lower() in info['mac'].lower()):
|
| 210 |
+
employees.append(employee_data)
|
| 211 |
+
else:
|
| 212 |
+
employees.append(employee_data)
|
| 213 |
+
|
| 214 |
+
return jsonify(employees)
|
| 215 |
+
|
| 216 |
+
@app.route('/api/events')
|
| 217 |
+
@login_required
|
| 218 |
+
def get_recent_events():
|
| 219 |
+
"""Get recent attendance events."""
|
| 220 |
+
global latest_events
|
| 221 |
+
|
| 222 |
+
# Convert events to JSON-serializable format
|
| 223 |
+
events_data = []
|
| 224 |
+
for mac, event_type, timestamp in latest_events:
|
| 225 |
+
employee_name = tracker.get_employee_name(mac) if tracker else f"Unknown ({mac})"
|
| 226 |
+
events_data.append({
|
| 227 |
+
'name': employee_name,
|
| 228 |
+
'mac': mac,
|
| 229 |
+
'event_type': event_type,
|
| 230 |
+
'timestamp': timestamp.strftime('%Y-%m-%d %H:%M:%S'),
|
| 231 |
+
'time_ago': get_time_ago(timestamp)
|
| 232 |
+
})
|
| 233 |
+
|
| 234 |
+
# Sort by timestamp (most recent first)
|
| 235 |
+
events_data.sort(key=lambda x: x['timestamp'], reverse=True)
|
| 236 |
+
|
| 237 |
+
return jsonify(events_data)
|
| 238 |
+
|
| 239 |
+
@login_required
|
| 240 |
+
@app.route('/api/attendance_events')
|
| 241 |
+
def get_attendance_events():
|
| 242 |
+
"""Get attendance events from database."""
|
| 243 |
+
if not db:
|
| 244 |
+
return jsonify([])
|
| 245 |
+
|
| 246 |
+
date_filter = request.args.get('date')
|
| 247 |
+
limit = int(request.args.get('limit', 50))
|
| 248 |
+
|
| 249 |
+
events = db.get_attendance_events(date=date_filter, limit=limit)
|
| 250 |
+
|
| 251 |
+
# Add time_ago field
|
| 252 |
+
for event in events:
|
| 253 |
+
timestamp = datetime.fromisoformat(event['timestamp'])
|
| 254 |
+
event['time_ago'] = get_time_ago(timestamp)
|
| 255 |
+
|
| 256 |
+
return jsonify(events)
|
| 257 |
+
|
| 258 |
+
@login_required
|
| 259 |
+
@app.route('/api/daily_summary')
|
| 260 |
+
def get_daily_summary():
|
| 261 |
+
"""Get daily attendance summary."""
|
| 262 |
+
if not db:
|
| 263 |
+
return jsonify([])
|
| 264 |
+
|
| 265 |
+
date_str = request.args.get('date', datetime.now().strftime('%Y-%m-%d'))
|
| 266 |
+
summary = db.get_daily_summary(date_str)
|
| 267 |
+
|
| 268 |
+
# Format durations for display
|
| 269 |
+
for employee in summary:
|
| 270 |
+
employee['total_break_formatted'] = format_duration(employee['total_break_duration'])
|
| 271 |
+
employee['total_work_formatted'] = format_duration(employee['total_work_duration'])
|
| 272 |
+
|
| 273 |
+
return jsonify(summary)
|
| 274 |
+
|
| 275 |
+
@login_required
|
| 276 |
+
@app.route('/api/summary_stats')
|
| 277 |
+
def get_summary_stats():
|
| 278 |
+
"""Get summary statistics for the dashboard."""
|
| 279 |
+
if not db:
|
| 280 |
+
return jsonify({})
|
| 281 |
+
|
| 282 |
+
date_str = request.args.get('date', datetime.now().strftime('%Y-%m-%d'))
|
| 283 |
+
summary = db.get_daily_summary(date_str)
|
| 284 |
+
|
| 285 |
+
stats = {
|
| 286 |
+
'total_employees': len(summary),
|
| 287 |
+
'present_count': len([emp for emp in summary if emp['status'] == 'Present']),
|
| 288 |
+
'absent_count': len([emp for emp in summary if emp['status'] == 'Absent']),
|
| 289 |
+
'on_break_count': len([emp for emp in summary if emp['status'] == 'On Break']),
|
| 290 |
+
'timed_out_count': len([emp for emp in summary if emp['status'] == 'Timed Out']),
|
| 291 |
+
'total_events': len(db.get_attendance_events(date=date_str, limit=1000))
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
return jsonify(stats)
|
| 295 |
+
|
| 296 |
+
@login_required
|
| 297 |
+
@app.route('/api/start_monitoring', methods=['POST'])
|
| 298 |
+
def start_monitoring():
|
| 299 |
+
"""Start the attendance monitoring."""
|
| 300 |
+
global is_monitoring
|
| 301 |
+
|
| 302 |
+
if not is_monitoring:
|
| 303 |
+
is_monitoring = True
|
| 304 |
+
monitoring_thread = threading.Thread(target=monitoring_loop, daemon=True)
|
| 305 |
+
monitoring_thread.start()
|
| 306 |
+
return jsonify({'success': True, 'message': 'Monitoring started'})
|
| 307 |
+
else:
|
| 308 |
+
return jsonify({'success': False, 'message': 'Monitoring already running'})
|
| 309 |
+
|
| 310 |
+
@login_required
|
| 311 |
+
@app.route('/api/stop_monitoring', methods=['POST'])
|
| 312 |
+
def stop_monitoring():
|
| 313 |
+
"""Stop the attendance monitoring."""
|
| 314 |
+
global is_monitoring
|
| 315 |
+
is_monitoring = False
|
| 316 |
+
return jsonify({'success': True, 'message': 'Monitoring stopped'})
|
| 317 |
+
|
| 318 |
+
@login_required
|
| 319 |
+
@app.route('/api/export_csv')
|
| 320 |
+
def export_csv():
|
| 321 |
+
"""Export daily summary to CSV."""
|
| 322 |
+
if not db:
|
| 323 |
+
return jsonify({'success': False, 'message': 'Database not available'})
|
| 324 |
+
|
| 325 |
+
date_str = request.args.get('date', datetime.now().strftime('%Y-%m-%d'))
|
| 326 |
+
|
| 327 |
+
try:
|
| 328 |
+
db.export_daily_summary_to_csv(date_str)
|
| 329 |
+
return jsonify({'success': True, 'message': f'CSV exported for {date_str}'})
|
| 330 |
+
except Exception as e:
|
| 331 |
+
return jsonify({'success': False, 'message': f'Export failed: {str(e)}'})
|
| 332 |
+
|
| 333 |
+
@login_required
|
| 334 |
+
@app.route('/api/add_employee', methods=['POST'])
|
| 335 |
+
def add_employee():
|
| 336 |
+
"""Add a new employee with password authentication."""
|
| 337 |
+
if not auth or not db:
|
| 338 |
+
return jsonify({'success': False, 'message': 'Authentication system not available'})
|
| 339 |
+
|
| 340 |
+
try:
|
| 341 |
+
data = request.get_json()
|
| 342 |
+
|
| 343 |
+
# Validate required fields
|
| 344 |
+
if not all(key in data for key in ['name', 'mac', 'password']):
|
| 345 |
+
return jsonify({'success': False, 'message': 'Missing required fields'})
|
| 346 |
+
|
| 347 |
+
# Authenticate admin password
|
| 348 |
+
if not auth.authenticate_admin(data['password']):
|
| 349 |
+
return jsonify({'success': False, 'message': 'Invalid admin password'})
|
| 350 |
+
|
| 351 |
+
# Validate MAC address format
|
| 352 |
+
mac_address = data['mac'].lower().strip()
|
| 353 |
+
if not is_valid_mac_format(mac_address):
|
| 354 |
+
return jsonify({'success': False, 'message': 'Invalid MAC address format. Use: aa-bb-cc-dd-ee-ff'})
|
| 355 |
+
|
| 356 |
+
# Add employee to database
|
| 357 |
+
success = db.add_employee(
|
| 358 |
+
name=data['name'].strip(),
|
| 359 |
+
mac_address=mac_address,
|
| 360 |
+
picture_path=data.get('picture', '').strip() or None
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
if success:
|
| 364 |
+
# Also add to employees.json for persistence
|
| 365 |
+
update_employees_json(data['name'].strip(), mac_address, data.get('picture', '').strip())
|
| 366 |
+
|
| 367 |
+
# Reload tracker employees
|
| 368 |
+
tracker.employees = tracker.load_employees()
|
| 369 |
+
tracker._initialize_employee_states()
|
| 370 |
+
|
| 371 |
+
return jsonify({'success': True, 'message': 'Employee added successfully'})
|
| 372 |
+
else:
|
| 373 |
+
return jsonify({'success': False, 'message': 'Employee with this MAC address already exists'})
|
| 374 |
+
|
| 375 |
+
except Exception as e:
|
| 376 |
+
print(f"Error adding employee: {e}")
|
| 377 |
+
return jsonify({'success': False, 'message': f'Error adding employee: {str(e)}'})
|
| 378 |
+
|
| 379 |
+
@login_required
|
| 380 |
+
@app.route('/api/search_employees')
|
| 381 |
+
def search_employees():
|
| 382 |
+
"""Search employees by name or MAC address."""
|
| 383 |
+
if not db:
|
| 384 |
+
return jsonify([])
|
| 385 |
+
|
| 386 |
+
search_query = request.args.get('q', '').strip()
|
| 387 |
+
|
| 388 |
+
if not search_query:
|
| 389 |
+
return jsonify([])
|
| 390 |
+
|
| 391 |
+
employees = db.get_all_employees(search_query=search_query)
|
| 392 |
+
|
| 393 |
+
# Format response
|
| 394 |
+
result = []
|
| 395 |
+
for emp in employees:
|
| 396 |
+
result.append({
|
| 397 |
+
'id': emp['id'],
|
| 398 |
+
'name': emp['name'],
|
| 399 |
+
'mac_address': emp['mac_address'],
|
| 400 |
+
'picture_path': emp['picture_path'],
|
| 401 |
+
'created_at': emp['created_at']
|
| 402 |
+
})
|
| 403 |
+
|
| 404 |
+
return jsonify(result)
|
| 405 |
+
|
| 406 |
+
@app.route('/api/delete_employee', methods=['POST'])
|
| 407 |
+
@login_required
|
| 408 |
+
def delete_employee():
|
| 409 |
+
"""Delete an employee with password authentication."""
|
| 410 |
+
if not auth or not db:
|
| 411 |
+
return jsonify({'success': False, 'message': 'Authentication system not available'})
|
| 412 |
+
|
| 413 |
+
try:
|
| 414 |
+
data = request.get_json()
|
| 415 |
+
|
| 416 |
+
# Validate required fields - now accepting either employee_id or mac_address
|
| 417 |
+
if not all(key in data for key in ['password']) or not any(key in data for key in ['employee_id', 'mac_address']):
|
| 418 |
+
return jsonify({'success': False, 'message': 'Missing required fields'})
|
| 419 |
+
|
| 420 |
+
# Authenticate admin password
|
| 421 |
+
if not auth.authenticate_admin(data['password']):
|
| 422 |
+
return jsonify({'success': False, 'message': 'Invalid admin password'})
|
| 423 |
+
|
| 424 |
+
# Get employee info - handle both employee_id and mac_address
|
| 425 |
+
if 'employee_id' in data and isinstance(data['employee_id'], int):
|
| 426 |
+
employee = db.get_employee_by_id(data['employee_id'])
|
| 427 |
+
else:
|
| 428 |
+
# If employee_id is actually a MAC address or mac_address is provided
|
| 429 |
+
mac_address = data.get('employee_id', data.get('mac_address'))
|
| 430 |
+
employee = db.get_employee_by_mac(mac_address)
|
| 431 |
+
|
| 432 |
+
if not employee:
|
| 433 |
+
return jsonify({'success': False, 'message': 'Employee not found'})
|
| 434 |
+
|
| 435 |
+
# Delete employee from database
|
| 436 |
+
success = db.delete_employee(employee['id'])
|
| 437 |
+
|
| 438 |
+
if success:
|
| 439 |
+
# Remove from employees.json for persistence
|
| 440 |
+
remove_employee_from_json(employee['mac_address'])
|
| 441 |
+
|
| 442 |
+
# Reload tracker employees
|
| 443 |
+
tracker.employees = tracker.load_employees()
|
| 444 |
+
tracker._initialize_employee_states()
|
| 445 |
+
|
| 446 |
+
return jsonify({'success': True, 'message': f'Employee {employee["name"]} deleted successfully'})
|
| 447 |
+
else:
|
| 448 |
+
return jsonify({'success': False, 'message': 'Failed to delete employee'})
|
| 449 |
+
|
| 450 |
+
except Exception as e:
|
| 451 |
+
print(f"Error deleting employee: {e}")
|
| 452 |
+
return jsonify({'success': False, 'message': f'Error deleting employee: {str(e)}'})
|
| 453 |
+
|
| 454 |
+
@app.route('/api/modify_employee', methods=['POST'])
|
| 455 |
+
@login_required
|
| 456 |
+
def modify_employee():
|
| 457 |
+
"""Modify employee information with password authentication."""
|
| 458 |
+
if not auth or not db:
|
| 459 |
+
return jsonify({'success': False, 'message': 'Authentication system not available'})
|
| 460 |
+
|
| 461 |
+
try:
|
| 462 |
+
data = request.get_json()
|
| 463 |
+
|
| 464 |
+
# Validate required fields - now accepting either employee_id or mac_address
|
| 465 |
+
if not all(key in data for key in ['password']) or not any(key in data for key in ['employee_id', 'mac_address']):
|
| 466 |
+
return jsonify({'success': False, 'message': 'Missing required fields'})
|
| 467 |
+
|
| 468 |
+
# Authenticate admin password
|
| 469 |
+
if not auth.authenticate_admin(data['password']):
|
| 470 |
+
return jsonify({'success': False, 'message': 'Invalid admin password'})
|
| 471 |
+
|
| 472 |
+
# Get current employee info - handle both employee_id and mac_address
|
| 473 |
+
if 'employee_id' in data and isinstance(data['employee_id'], int):
|
| 474 |
+
employee = db.get_employee_by_id(data['employee_id'])
|
| 475 |
+
else:
|
| 476 |
+
# If employee_id is actually a MAC address or mac_address is provided
|
| 477 |
+
mac_address = data.get('employee_id', data.get('mac_address'))
|
| 478 |
+
employee = db.get_employee_by_mac(mac_address)
|
| 479 |
+
|
| 480 |
+
if not employee:
|
| 481 |
+
return jsonify({'success': False, 'message': 'Employee not found'})
|
| 482 |
+
|
| 483 |
+
# Prepare update data
|
| 484 |
+
update_data = {}
|
| 485 |
+
if 'name' in data and data['name'].strip():
|
| 486 |
+
update_data['name'] = data['name'].strip()
|
| 487 |
+
if 'mac_address' in data and data['mac_address'].strip():
|
| 488 |
+
new_mac = data['mac_address'].lower().strip()
|
| 489 |
+
if not is_valid_mac_format(new_mac):
|
| 490 |
+
return jsonify({'success': False, 'message': 'Invalid MAC address format. Use: aa-bb-cc-dd-ee-ff'})
|
| 491 |
+
update_data['mac_address'] = new_mac
|
| 492 |
+
if 'picture_path' in data:
|
| 493 |
+
update_data['picture_path'] = data['picture_path'].strip() or None
|
| 494 |
+
|
| 495 |
+
if not update_data:
|
| 496 |
+
return jsonify({'success': False, 'message': 'No valid fields to update'})
|
| 497 |
+
|
| 498 |
+
# Update employee in database
|
| 499 |
+
success = db.update_employee(employee['id'], **update_data)
|
| 500 |
+
|
| 501 |
+
if success:
|
| 502 |
+
# Update employees.json for persistence
|
| 503 |
+
update_employee_in_json(employee['mac_address'], update_data)
|
| 504 |
+
|
| 505 |
+
# Reload tracker employees
|
| 506 |
+
tracker.employees = tracker.load_employees()
|
| 507 |
+
tracker._initialize_employee_states()
|
| 508 |
+
|
| 509 |
+
return jsonify({'success': True, 'message': 'Employee updated successfully'})
|
| 510 |
+
else:
|
| 511 |
+
return jsonify({'success': False, 'message': 'Failed to update employee'})
|
| 512 |
+
|
| 513 |
+
except Exception as e:
|
| 514 |
+
print(f"Error modifying employee: {e}")
|
| 515 |
+
return jsonify({'success': False, 'message': f'Error modifying employee: {str(e)}'})
|
| 516 |
+
|
| 517 |
+
@app.route('/api/change_password', methods=['POST'])
|
| 518 |
+
@login_required
|
| 519 |
+
def change_password():
|
| 520 |
+
"""Change the admin password."""
|
| 521 |
+
if not auth:
|
| 522 |
+
return jsonify({'success': False, 'message': 'Authentication system not available'})
|
| 523 |
+
|
| 524 |
+
try:
|
| 525 |
+
data = request.get_json()
|
| 526 |
+
|
| 527 |
+
if not all(key in data for key in ['currentPassword', 'newPassword']):
|
| 528 |
+
return jsonify({'success': False, 'message': 'Missing required fields'})
|
| 529 |
+
|
| 530 |
+
success = auth.change_admin_password(data['currentPassword'], data['newPassword'])
|
| 531 |
+
|
| 532 |
+
if success:
|
| 533 |
+
return jsonify({'success': True, 'message': 'Password changed successfully'})
|
| 534 |
+
else:
|
| 535 |
+
return jsonify({'success': False, 'message': 'Current password is incorrect'})
|
| 536 |
+
|
| 537 |
+
except Exception as e:
|
| 538 |
+
print(f"Error changing password: {e}")
|
| 539 |
+
return jsonify({'success': False, 'message': f'Error changing password: {str(e)}'})
|
| 540 |
+
|
| 541 |
+
def is_valid_mac_format(mac):
|
| 542 |
+
"""Validate MAC address format."""
|
| 543 |
+
if not mac:
|
| 544 |
+
return False
|
| 545 |
+
|
| 546 |
+
# Check for correct format: aa-bb-cc-dd-ee-ff
|
| 547 |
+
parts = mac.split('-')
|
| 548 |
+
if len(parts) != 6:
|
| 549 |
+
return False
|
| 550 |
+
|
| 551 |
+
for part in parts:
|
| 552 |
+
if len(part) != 2:
|
| 553 |
+
return False
|
| 554 |
+
try:
|
| 555 |
+
int(part, 16)
|
| 556 |
+
except ValueError:
|
| 557 |
+
return False
|
| 558 |
+
|
| 559 |
+
return True
|
| 560 |
+
|
| 561 |
+
def update_employees_json(name, mac_address, picture_path):
|
| 562 |
+
"""Update the employees.json file with new employee."""
|
| 563 |
+
try:
|
| 564 |
+
# Read existing employees
|
| 565 |
+
employees = []
|
| 566 |
+
if os.path.exists('employees.json'):
|
| 567 |
+
with open('employees.json', 'r') as f:
|
| 568 |
+
employees = json.load(f)
|
| 569 |
+
|
| 570 |
+
# Check if employee already exists
|
| 571 |
+
for emp in employees:
|
| 572 |
+
if emp.get('mac_address') == mac_address:
|
| 573 |
+
return # Already exists
|
| 574 |
+
|
| 575 |
+
# Add new employee
|
| 576 |
+
new_employee = {
|
| 577 |
+
'name': name,
|
| 578 |
+
'mac_address': mac_address,
|
| 579 |
+
'picture': picture_path if picture_path else f"static/img/{name.lower().replace(' ', '_')}.jpg"
|
| 580 |
+
}
|
| 581 |
+
employees.append(new_employee)
|
| 582 |
+
|
| 583 |
+
# Write back to file
|
| 584 |
+
with open('employees.json', 'w') as f:
|
| 585 |
+
json.dump(employees, f, indent=2)
|
| 586 |
+
|
| 587 |
+
except Exception as e:
|
| 588 |
+
print(f"Error updating employees.json: {e}")
|
| 589 |
+
|
| 590 |
+
def remove_employee_from_json(mac_address):
|
| 591 |
+
"""Remove an employee from employees.json file."""
|
| 592 |
+
try:
|
| 593 |
+
if not os.path.exists('employees.json'):
|
| 594 |
+
return
|
| 595 |
+
|
| 596 |
+
# Read existing employees
|
| 597 |
+
with open('employees.json', 'r') as f:
|
| 598 |
+
employees = json.load(f)
|
| 599 |
+
|
| 600 |
+
# Remove employee with matching MAC address
|
| 601 |
+
employees = [emp for emp in employees if emp.get('mac_address') != mac_address]
|
| 602 |
+
|
| 603 |
+
# Write back to file
|
| 604 |
+
with open('employees.json', 'w') as f:
|
| 605 |
+
json.dump(employees, f, indent=2)
|
| 606 |
+
|
| 607 |
+
except Exception as e:
|
| 608 |
+
print(f"Error removing employee from employees.json: {e}")
|
| 609 |
+
|
| 610 |
+
def update_employee_in_json(old_mac_address, update_data):
|
| 611 |
+
"""Update an employee in employees.json file."""
|
| 612 |
+
try:
|
| 613 |
+
if not os.path.exists('employees.json'):
|
| 614 |
+
return
|
| 615 |
+
|
| 616 |
+
# Read existing employees
|
| 617 |
+
with open('employees.json', 'r') as f:
|
| 618 |
+
employees = json.load(f)
|
| 619 |
+
|
| 620 |
+
# Find and update employee
|
| 621 |
+
for emp in employees:
|
| 622 |
+
if emp.get('mac_address') == old_mac_address:
|
| 623 |
+
if 'name' in update_data:
|
| 624 |
+
emp['name'] = update_data['name']
|
| 625 |
+
if 'mac_address' in update_data:
|
| 626 |
+
emp['mac_address'] = update_data['mac_address']
|
| 627 |
+
if 'picture_path' in update_data:
|
| 628 |
+
emp['picture'] = update_data['picture_path'] or f"static/img/{emp['name'].lower().replace(' ', '_')}.jpg"
|
| 629 |
+
break
|
| 630 |
+
|
| 631 |
+
# Write back to file
|
| 632 |
+
with open('employees.json', 'w') as f:
|
| 633 |
+
json.dump(employees, f, indent=2)
|
| 634 |
+
|
| 635 |
+
except Exception as e:
|
| 636 |
+
print(f"Error updating employee in employees.json: {e}")
|
| 637 |
+
|
| 638 |
+
def get_time_ago(timestamp):
|
| 639 |
+
"""Get human-readable time difference."""
|
| 640 |
+
now = datetime.now()
|
| 641 |
+
diff = now - timestamp
|
| 642 |
+
|
| 643 |
+
if diff.days > 0:
|
| 644 |
+
return f"{diff.days} day{'s' if diff.days > 1 else ''} ago"
|
| 645 |
+
elif diff.seconds > 3600:
|
| 646 |
+
hours = diff.seconds // 3600
|
| 647 |
+
return f"{hours} hour{'s' if hours > 1 else ''} ago"
|
| 648 |
+
elif diff.seconds > 60:
|
| 649 |
+
minutes = diff.seconds // 60
|
| 650 |
+
return f"{minutes} minute{'s' if minutes > 1 else ''} ago"
|
| 651 |
+
else:
|
| 652 |
+
return "Just now"
|
| 653 |
+
|
| 654 |
+
def format_duration(seconds):
|
| 655 |
+
"""Format duration in seconds to HH:MM:SS."""
|
| 656 |
+
if seconds is None:
|
| 657 |
+
return "00:00:00"
|
| 658 |
+
|
| 659 |
+
hours = seconds // 3600
|
| 660 |
+
minutes = (seconds % 3600) // 60
|
| 661 |
+
seconds = seconds % 60
|
| 662 |
+
|
| 663 |
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
| 664 |
+
|
| 665 |
+
def run_console_mode():
|
| 666 |
+
"""Run the application in console mode (no web interface)."""
|
| 667 |
+
print("Starting in console mode...")
|
| 668 |
+
print("Press Ctrl+C to stop\n")
|
| 669 |
+
|
| 670 |
+
initialize_system()
|
| 671 |
+
|
| 672 |
+
try:
|
| 673 |
+
tracker.start_monitoring()
|
| 674 |
+
except KeyboardInterrupt:
|
| 675 |
+
print("\nShutting down...")
|
| 676 |
+
sys.exit(0)
|
| 677 |
+
|
| 678 |
+
def run_web_mode(port=5000):
|
| 679 |
+
"""Run the application with web interface."""
|
| 680 |
+
print("Starting integrated web interface mode...")
|
| 681 |
+
|
| 682 |
+
# Initialize the system
|
| 683 |
+
initialize_system()
|
| 684 |
+
|
| 685 |
+
print(f"\nWeb interface will be available at:")
|
| 686 |
+
print(f" Local: http://localhost:{port}")
|
| 687 |
+
print(f" Network: http://0.0.0.0:{port}")
|
| 688 |
+
print("\nPress Ctrl+C to stop\n")
|
| 689 |
+
|
| 690 |
+
# Note about admin privileges
|
| 691 |
+
print("Note: This application attempts to run without administrator privileges.")
|
| 692 |
+
print("If network scanning fails, you may need to run as administrator/root.")
|
| 693 |
+
print("Employee management features work regardless of privilege level.\n")
|
| 694 |
+
|
| 695 |
+
# Start monitoring automatically
|
| 696 |
+
global is_monitoring
|
| 697 |
+
is_monitoring = True
|
| 698 |
+
monitoring_thread = threading.Thread(target=monitoring_loop, daemon=True)
|
| 699 |
+
monitoring_thread.start()
|
| 700 |
+
|
| 701 |
+
try:
|
| 702 |
+
app.run(host='0.0.0.0', port=port, debug=False)
|
| 703 |
+
except KeyboardInterrupt:
|
| 704 |
+
print("\nShutting down...")
|
| 705 |
+
is_monitoring = False
|
| 706 |
+
sys.exit(0)
|
| 707 |
+
|
| 708 |
+
def show_status():
|
| 709 |
+
"""Show current system status."""
|
| 710 |
+
print("System Status:")
|
| 711 |
+
print("=" * 50)
|
| 712 |
+
|
| 713 |
+
# Initialize system for status check
|
| 714 |
+
initialize_system()
|
| 715 |
+
|
| 716 |
+
# Check database
|
| 717 |
+
try:
|
| 718 |
+
employees = db.get_all_employees()
|
| 719 |
+
print(f"Database: Connected")
|
| 720 |
+
print(f"Employees: {len(employees)}")
|
| 721 |
+
|
| 722 |
+
for emp in employees:
|
| 723 |
+
picture_info = f" (Picture: {emp['picture_path']})" if emp['picture_path'] else ""
|
| 724 |
+
print(f" - {emp['name']} ({emp['mac_address']}){picture_info}")
|
| 725 |
+
|
| 726 |
+
# Show recent events
|
| 727 |
+
recent_events = db.get_attendance_events(limit=5)
|
| 728 |
+
print(f"\nRecent Events: {len(recent_events)}")
|
| 729 |
+
for event in recent_events[-5:]:
|
| 730 |
+
print(f" {event['timestamp']}: {event['employee_name']} - {event['event_type']}")
|
| 731 |
+
|
| 732 |
+
# Show today's summary
|
| 733 |
+
today = datetime.now().strftime('%Y-%m-%d')
|
| 734 |
+
summary = db.get_daily_summary(today)
|
| 735 |
+
print(f"\nToday's Summary ({today}):")
|
| 736 |
+
for emp in summary:
|
| 737 |
+
print(f" {emp['name']}: {emp['status']} | In: {emp['time_in'] or 'N/A'} | Out: {emp['time_out'] or 'N/A'}")
|
| 738 |
+
|
| 739 |
+
except Exception as e:
|
| 740 |
+
print(f"Database: Error - {e}")
|
| 741 |
+
|
| 742 |
+
# Check config
|
| 743 |
+
try:
|
| 744 |
+
import json
|
| 745 |
+
with open('config.json', 'r') as f:
|
| 746 |
+
config = json.load(f)
|
| 747 |
+
print(f"\nConfiguration:")
|
| 748 |
+
print(f" Scan Interval: {config.get('scan_interval_seconds', 60)} seconds")
|
| 749 |
+
print(f" Web Port: {config.get('web_port', 5000)}")
|
| 750 |
+
print(f" Office Timeout: {config.get('office_timeout_hour', 17)}:{config.get('office_timeout_minute', 0):02d}")
|
| 751 |
+
except Exception as e:
|
| 752 |
+
print(f"\nConfiguration: Error - {e}")
|
| 753 |
+
|
| 754 |
+
# Check employees.json
|
| 755 |
+
try:
|
| 756 |
+
with open('employees.json', 'r') as f:
|
| 757 |
+
employees_json = json.load(f)
|
| 758 |
+
print(f"\nEmployees JSON File:")
|
| 759 |
+
print(f" Configured Employees: {len(employees_json)}")
|
| 760 |
+
for emp in employees_json:
|
| 761 |
+
print(f" - {emp.get('name', 'Unknown')} ({emp.get('mac_address', 'Unknown')})")
|
| 762 |
+
except Exception as e:
|
| 763 |
+
print(f"\nEmployees JSON: Error - {e}")
|
| 764 |
+
|
| 765 |
+
# Check authentication
|
| 766 |
+
try:
|
| 767 |
+
print(f"\nAuthentication:")
|
| 768 |
+
print(f" Admin Password: {auth.get_current_admin_password_hint()}")
|
| 769 |
+
except Exception as e:
|
| 770 |
+
print(f"\nAuthentication: Error - {e}")
|
| 771 |
+
|
| 772 |
+
def main():
|
| 773 |
+
"""Main application entry point."""
|
| 774 |
+
parser = argparse.ArgumentParser(
|
| 775 |
+
description='WiFi-Based Attendance & Break Tracker - Employee Management Version',
|
| 776 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 777 |
+
epilog="""
|
| 778 |
+
Examples:
|
| 779 |
+
python main.py # Run with web interface (default)
|
| 780 |
+
python main.py --console # Run in console mode only
|
| 781 |
+
python main.py --status # Show system status
|
| 782 |
+
python main.py --port 8080 # Run web interface on port 8080
|
| 783 |
+
|
| 784 |
+
New Features:
|
| 785 |
+
- Employee management (add, search, password protection)
|
| 786 |
+
- Non-admin operation (where possible)
|
| 787 |
+
- Enhanced web interface with employee pictures
|
| 788 |
+
- Separate employees.json for better data management
|
| 789 |
+
- Password-protected employee addition (default: 1122)
|
| 790 |
+
|
| 791 |
+
For more information, see README.md
|
| 792 |
+
"""
|
| 793 |
+
)
|
| 794 |
+
|
| 795 |
+
parser.add_argument(
|
| 796 |
+
'--console',
|
| 797 |
+
action='store_true',
|
| 798 |
+
help='Run in console mode without web interface'
|
| 799 |
+
)
|
| 800 |
+
|
| 801 |
+
parser.add_argument(
|
| 802 |
+
'--status',
|
| 803 |
+
action='store_true',
|
| 804 |
+
help='Show system status and exit'
|
| 805 |
+
)
|
| 806 |
+
|
| 807 |
+
parser.add_argument(
|
| 808 |
+
'--port',
|
| 809 |
+
type=int,
|
| 810 |
+
default=5000,
|
| 811 |
+
help='Port for web interface (default: 5000)'
|
| 812 |
+
)
|
| 813 |
+
|
| 814 |
+
args = parser.parse_args()
|
| 815 |
+
|
| 816 |
+
# Print banner
|
| 817 |
+
print_banner()
|
| 818 |
+
|
| 819 |
+
# Check requirements
|
| 820 |
+
issues = check_requirements()
|
| 821 |
+
if issues:
|
| 822 |
+
print("β οΈ System Requirements Issues:")
|
| 823 |
+
for issue in issues:
|
| 824 |
+
print(f" - {issue}")
|
| 825 |
+
print()
|
| 826 |
+
|
| 827 |
+
# Create missing files with defaults
|
| 828 |
+
if "config.json file not found" in str(issues):
|
| 829 |
+
print("Creating default config.json...")
|
| 830 |
+
default_config = {
|
| 831 |
+
"scan_interval_seconds": 60,
|
| 832 |
+
"web_port": 5000,
|
| 833 |
+
"office_timeout_hour": 17,
|
| 834 |
+
"office_timeout_minute": 0
|
| 835 |
+
}
|
| 836 |
+
with open('config.json', 'w') as f:
|
| 837 |
+
json.dump(default_config, f, indent=2)
|
| 838 |
+
|
| 839 |
+
if "employees.json file not found" in str(issues):
|
| 840 |
+
print("Creating default employees.json...")
|
| 841 |
+
default_employees = [
|
| 842 |
+
{
|
| 843 |
+
"name": "Sample Employee",
|
| 844 |
+
"mac_address": "aa-bb-cc-dd-ee-ff",
|
| 845 |
+
"picture": "static/img/sample.jpg"
|
| 846 |
+
}
|
| 847 |
+
]
|
| 848 |
+
with open('employees.json', 'w') as f:
|
| 849 |
+
json.dump(default_employees, f, indent=2)
|
| 850 |
+
|
| 851 |
+
print("Default files created. Please edit them with your actual data.\n")
|
| 852 |
+
|
| 853 |
+
# Handle different modes
|
| 854 |
+
if args.status:
|
| 855 |
+
show_status()
|
| 856 |
+
return
|
| 857 |
+
|
| 858 |
+
if args.console:
|
| 859 |
+
run_console_mode()
|
| 860 |
+
else:
|
| 861 |
+
run_web_mode(args.port)
|
| 862 |
+
|
| 863 |
+
if __name__ == "__main__":
|
| 864 |
+
main()
|
| 865 |
+
|
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask>=2.0.0
|
| 2 |
+
flask-cors>=3.0.0
|
| 3 |
+
bcrypt>=4.0.0
|
| 4 |
+
|
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
echo ========================================
|
| 3 |
+
echo WiFi Attendance Tracker
|
| 4 |
+
echo ========================================
|
| 5 |
+
echo.
|
| 6 |
+
|
| 7 |
+
REM Check if running as administrator
|
| 8 |
+
net session >nul 2>&1
|
| 9 |
+
if errorlevel 1 (
|
| 10 |
+
echo ERROR: This application must be run as Administrator
|
| 11 |
+
echo.
|
| 12 |
+
echo Please:
|
| 13 |
+
echo 1. Right-click Command Prompt
|
| 14 |
+
echo 2. Select "Run as Administrator"
|
| 15 |
+
echo 3. Navigate to this folder
|
| 16 |
+
echo 4. Run this script again
|
| 17 |
+
echo.
|
| 18 |
+
pause
|
| 19 |
+
exit /b 1
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
echo Starting WiFi Attendance Tracker...
|
| 23 |
+
echo Web interface will be available at: http://localhost:5000
|
| 24 |
+
echo Press Ctrl+C to stop
|
| 25 |
+
echo.
|
| 26 |
+
|
| 27 |
+
python main.py
|
| 28 |
+
|
| 29 |
+
pause
|
| 30 |
+
|
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
echo ========================================
|
| 3 |
+
echo WiFi Attendance Tracker Setup
|
| 4 |
+
echo ========================================
|
| 5 |
+
echo.
|
| 6 |
+
|
| 7 |
+
REM Check if Python is installed
|
| 8 |
+
python --version >nul 2>&1
|
| 9 |
+
if errorlevel 1 (
|
| 10 |
+
echo ERROR: Python is not installed or not in PATH
|
| 11 |
+
echo Please install Python 3.6+ from https://python.org
|
| 12 |
+
pause
|
| 13 |
+
exit /b 1
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
echo Python found. Installing dependencies...
|
| 17 |
+
pip install -r requirements.txt
|
| 18 |
+
|
| 19 |
+
if errorlevel 1 (
|
| 20 |
+
echo ERROR: Failed to install dependencies
|
| 21 |
+
pause
|
| 22 |
+
exit /b 1
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
echo.
|
| 26 |
+
echo Setup completed successfully!
|
| 27 |
+
echo.
|
| 28 |
+
echo To run the application:
|
| 29 |
+
echo 1. Right-click Command Prompt and select "Run as Administrator"
|
| 30 |
+
echo 2. Navigate to this folder
|
| 31 |
+
echo 3. Run: python main.py
|
| 32 |
+
echo.
|
| 33 |
+
echo The web interface will be available at: http://localhost:5000
|
| 34 |
+
echo.
|
| 35 |
+
pause
|
| 36 |
+
|
|
@@ -0,0 +1,899 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* WiFi Attendance Tracker - Advanced Styles */
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--primary-color: #2563eb;
|
| 5 |
+
--primary-hover: #1d4ed8;
|
| 6 |
+
--secondary-color: #64748b;
|
| 7 |
+
--success-color: #059669;
|
| 8 |
+
--success-hover: #047857;
|
| 9 |
+
--danger-color: #dc2626;
|
| 10 |
+
--danger-hover: #b91c1c;
|
| 11 |
+
--warning-color: #d97706;
|
| 12 |
+
--info-color: #0891b2;
|
| 13 |
+
--light-bg: #f8fafc;
|
| 14 |
+
--card-bg: #ffffff;
|
| 15 |
+
--border-color: #e2e8f0;
|
| 16 |
+
--text-primary: #1e293b;
|
| 17 |
+
--text-secondary: #64748b;
|
| 18 |
+
--text-muted: #94a3b8;
|
| 19 |
+
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
| 20 |
+
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
| 21 |
+
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
| 22 |
+
--radius: 8px;
|
| 23 |
+
--radius-lg: 12px;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
* {
|
| 27 |
+
margin: 0;
|
| 28 |
+
padding: 0;
|
| 29 |
+
box-sizing: border-box;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
body {
|
| 33 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 34 |
+
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
| 35 |
+
min-height: 100vh;
|
| 36 |
+
color: var(--text-primary);
|
| 37 |
+
line-height: 1.6;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.container {
|
| 41 |
+
max-width: 1400px;
|
| 42 |
+
margin: 0 auto;
|
| 43 |
+
padding: 20px;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/* Header */
|
| 47 |
+
.header {
|
| 48 |
+
background: var(--card-bg);
|
| 49 |
+
border-radius: var(--radius-lg);
|
| 50 |
+
padding: 24px;
|
| 51 |
+
margin-bottom: 24px;
|
| 52 |
+
box-shadow: var(--shadow-lg);
|
| 53 |
+
display: flex;
|
| 54 |
+
justify-content: space-between;
|
| 55 |
+
align-items: center;
|
| 56 |
+
flex-wrap: wrap;
|
| 57 |
+
gap: 16px;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.header-content h1 {
|
| 61 |
+
font-size: 2rem;
|
| 62 |
+
font-weight: 700;
|
| 63 |
+
color: var(--text-primary);
|
| 64 |
+
margin-bottom: 4px;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.header-content h1 i {
|
| 68 |
+
color: var(--primary-color);
|
| 69 |
+
margin-right: 12px;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.subtitle {
|
| 73 |
+
color: var(--text-secondary);
|
| 74 |
+
font-size: 1rem;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.header-actions {
|
| 78 |
+
display: flex;
|
| 79 |
+
gap: 12px;
|
| 80 |
+
flex-wrap: wrap;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/* Status Bar */
|
| 84 |
+
.status-bar {
|
| 85 |
+
background: var(--card-bg);
|
| 86 |
+
border-radius: var(--radius);
|
| 87 |
+
padding: 16px 24px;
|
| 88 |
+
margin-bottom: 24px;
|
| 89 |
+
box-shadow: var(--shadow-md);
|
| 90 |
+
display: flex;
|
| 91 |
+
justify-content: space-between;
|
| 92 |
+
align-items: center;
|
| 93 |
+
flex-wrap: wrap;
|
| 94 |
+
gap: 16px;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.status-item {
|
| 98 |
+
display: flex;
|
| 99 |
+
align-items: center;
|
| 100 |
+
gap: 8px;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.status-label {
|
| 104 |
+
font-weight: 600;
|
| 105 |
+
color: var(--text-secondary);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.status-value {
|
| 109 |
+
color: var(--text-primary);
|
| 110 |
+
font-weight: 500;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/* Control Panel */
|
| 114 |
+
.control-panel {
|
| 115 |
+
background: var(--card-bg);
|
| 116 |
+
border-radius: var(--radius);
|
| 117 |
+
padding: 20px;
|
| 118 |
+
margin-bottom: 24px;
|
| 119 |
+
box-shadow: var(--shadow-md);
|
| 120 |
+
display: flex;
|
| 121 |
+
justify-content: space-between;
|
| 122 |
+
align-items: center;
|
| 123 |
+
flex-wrap: wrap;
|
| 124 |
+
gap: 16px;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.control-group {
|
| 128 |
+
display: flex;
|
| 129 |
+
gap: 12px;
|
| 130 |
+
align-items: center;
|
| 131 |
+
flex-wrap: wrap;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.search-container {
|
| 135 |
+
display: flex;
|
| 136 |
+
gap: 8px;
|
| 137 |
+
align-items: center;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.search-input {
|
| 141 |
+
padding: 10px 16px;
|
| 142 |
+
border: 2px solid var(--border-color);
|
| 143 |
+
border-radius: var(--radius);
|
| 144 |
+
font-size: 14px;
|
| 145 |
+
width: 250px;
|
| 146 |
+
transition: all 0.2s ease;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.search-input:focus {
|
| 150 |
+
outline: none;
|
| 151 |
+
border-color: var(--primary-color);
|
| 152 |
+
box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/* Buttons */
|
| 156 |
+
.btn {
|
| 157 |
+
padding: 10px 20px;
|
| 158 |
+
border: none;
|
| 159 |
+
border-radius: var(--radius);
|
| 160 |
+
font-size: 14px;
|
| 161 |
+
font-weight: 600;
|
| 162 |
+
cursor: pointer;
|
| 163 |
+
transition: all 0.2s ease;
|
| 164 |
+
display: inline-flex;
|
| 165 |
+
align-items: center;
|
| 166 |
+
gap: 8px;
|
| 167 |
+
text-decoration: none;
|
| 168 |
+
white-space: nowrap;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.btn:disabled {
|
| 172 |
+
opacity: 0.5;
|
| 173 |
+
cursor: not-allowed;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.btn-primary {
|
| 177 |
+
background: var(--primary-color);
|
| 178 |
+
color: white;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.btn-primary:hover:not(:disabled) {
|
| 182 |
+
background: var(--primary-hover);
|
| 183 |
+
transform: translateY(-1px);
|
| 184 |
+
box-shadow: var(--shadow-md);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.btn-secondary {
|
| 188 |
+
background: var(--secondary-color);
|
| 189 |
+
color: white;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.btn-secondary:hover:not(:disabled) {
|
| 193 |
+
background: #475569;
|
| 194 |
+
transform: translateY(-1px);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.btn-success {
|
| 198 |
+
background: var(--success-color);
|
| 199 |
+
color: white;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.btn-success:hover:not(:disabled) {
|
| 203 |
+
background: var(--success-hover);
|
| 204 |
+
transform: translateY(-1px);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.btn-danger {
|
| 208 |
+
background: var(--danger-color);
|
| 209 |
+
color: white;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.btn-danger:hover:not(:disabled) {
|
| 213 |
+
background: var(--danger-hover);
|
| 214 |
+
transform: translateY(-1px);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.btn-info {
|
| 218 |
+
background: var(--info-color);
|
| 219 |
+
color: white;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.btn-info:hover:not(:disabled) {
|
| 223 |
+
background: #0e7490;
|
| 224 |
+
transform: translateY(-1px);
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.btn-outline {
|
| 228 |
+
background: transparent;
|
| 229 |
+
color: var(--text-primary);
|
| 230 |
+
border: 2px solid var(--border-color);
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.btn-outline:hover:not(:disabled) {
|
| 234 |
+
background: var(--light-bg);
|
| 235 |
+
border-color: var(--primary-color);
|
| 236 |
+
color: var(--primary-color);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/* Statistics Cards */
|
| 240 |
+
.stats-grid {
|
| 241 |
+
display: grid;
|
| 242 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 243 |
+
gap: 20px;
|
| 244 |
+
margin-bottom: 24px;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.stat-card {
|
| 248 |
+
background: var(--card-bg);
|
| 249 |
+
border-radius: var(--radius-lg);
|
| 250 |
+
padding: 24px;
|
| 251 |
+
box-shadow: var(--shadow-md);
|
| 252 |
+
display: flex;
|
| 253 |
+
align-items: center;
|
| 254 |
+
gap: 16px;
|
| 255 |
+
transition: all 0.2s ease;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.stat-card:hover {
|
| 259 |
+
transform: translateY(-2px);
|
| 260 |
+
box-shadow: var(--shadow-lg);
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.stat-icon {
|
| 264 |
+
width: 48px;
|
| 265 |
+
height: 48px;
|
| 266 |
+
border-radius: 50%;
|
| 267 |
+
display: flex;
|
| 268 |
+
align-items: center;
|
| 269 |
+
justify-content: center;
|
| 270 |
+
font-size: 20px;
|
| 271 |
+
color: white;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.stat-card.present .stat-icon {
|
| 275 |
+
background: var(--success-color);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.stat-card.absent .stat-icon {
|
| 279 |
+
background: var(--danger-color);
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.stat-card.break .stat-icon {
|
| 283 |
+
background: var(--warning-color);
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.stat-card.timeout .stat-icon {
|
| 287 |
+
background: var(--info-color);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.stat-content h3 {
|
| 291 |
+
font-size: 2rem;
|
| 292 |
+
font-weight: 700;
|
| 293 |
+
color: var(--text-primary);
|
| 294 |
+
margin-bottom: 4px;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.stat-content p {
|
| 298 |
+
color: var(--text-secondary);
|
| 299 |
+
font-weight: 500;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
/* Main Grid */
|
| 303 |
+
.main-grid {
|
| 304 |
+
display: grid;
|
| 305 |
+
grid-template-columns: 1fr 1fr;
|
| 306 |
+
gap: 24px;
|
| 307 |
+
margin-bottom: 24px;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
/* Panels */
|
| 311 |
+
.panel {
|
| 312 |
+
background: var(--card-bg);
|
| 313 |
+
border-radius: var(--radius-lg);
|
| 314 |
+
box-shadow: var(--shadow-md);
|
| 315 |
+
overflow: hidden;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.panel-header {
|
| 319 |
+
padding: 20px 24px;
|
| 320 |
+
border-bottom: 1px solid var(--border-color);
|
| 321 |
+
display: flex;
|
| 322 |
+
justify-content: space-between;
|
| 323 |
+
align-items: center;
|
| 324 |
+
flex-wrap: wrap;
|
| 325 |
+
gap: 12px;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.panel-header h2 {
|
| 329 |
+
font-size: 1.25rem;
|
| 330 |
+
font-weight: 600;
|
| 331 |
+
color: var(--text-primary);
|
| 332 |
+
display: flex;
|
| 333 |
+
align-items: center;
|
| 334 |
+
gap: 8px;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.panel-header h2 i {
|
| 338 |
+
color: var(--primary-color);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.panel-actions {
|
| 342 |
+
display: flex;
|
| 343 |
+
gap: 8px;
|
| 344 |
+
align-items: center;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.panel-content {
|
| 348 |
+
padding: 24px;
|
| 349 |
+
max-height: 400px;
|
| 350 |
+
overflow-y: auto;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
/* Employee List */
|
| 354 |
+
.employee-list {
|
| 355 |
+
display: flex;
|
| 356 |
+
flex-direction: column;
|
| 357 |
+
gap: 12px;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
.employee-card {
|
| 361 |
+
display: flex;
|
| 362 |
+
align-items: center;
|
| 363 |
+
gap: 16px;
|
| 364 |
+
padding: 16px;
|
| 365 |
+
border: 1px solid var(--border-color);
|
| 366 |
+
border-radius: var(--radius);
|
| 367 |
+
transition: all 0.2s ease;
|
| 368 |
+
cursor: pointer;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.employee-card:hover {
|
| 372 |
+
border-color: var(--primary-color);
|
| 373 |
+
box-shadow: var(--shadow-sm);
|
| 374 |
+
transform: translateY(-1px);
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.employee-avatar {
|
| 378 |
+
width: 48px;
|
| 379 |
+
height: 48px;
|
| 380 |
+
border-radius: 50%;
|
| 381 |
+
background: var(--light-bg);
|
| 382 |
+
display: flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
justify-content: center;
|
| 385 |
+
font-size: 18px;
|
| 386 |
+
font-weight: 600;
|
| 387 |
+
color: var(--text-secondary);
|
| 388 |
+
overflow: hidden;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.employee-avatar img {
|
| 392 |
+
width: 100%;
|
| 393 |
+
height: 100%;
|
| 394 |
+
object-fit: cover;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.employee-info {
|
| 398 |
+
flex: 1;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.employee-name {
|
| 402 |
+
font-weight: 600;
|
| 403 |
+
color: var(--text-primary);
|
| 404 |
+
margin-bottom: 4px;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
.employee-mac {
|
| 408 |
+
font-size: 12px;
|
| 409 |
+
color: var(--text-muted);
|
| 410 |
+
font-family: 'Courier New', monospace;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
.employee-status {
|
| 414 |
+
display: flex;
|
| 415 |
+
align-items: center;
|
| 416 |
+
gap: 8px;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.status-badge {
|
| 420 |
+
padding: 4px 12px;
|
| 421 |
+
border-radius: 20px;
|
| 422 |
+
font-size: 12px;
|
| 423 |
+
font-weight: 600;
|
| 424 |
+
text-transform: uppercase;
|
| 425 |
+
letter-spacing: 0.5px;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.status-badge.present {
|
| 429 |
+
background: #dcfce7;
|
| 430 |
+
color: var(--success-color);
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.status-badge.absent {
|
| 434 |
+
background: #fef2f2;
|
| 435 |
+
color: var(--danger-color);
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
.status-badge.break {
|
| 439 |
+
background: #fef3c7;
|
| 440 |
+
color: var(--warning-color);
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.status-badge.timeout {
|
| 444 |
+
background: #e0f2fe;
|
| 445 |
+
color: var(--info-color);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.employee-time {
|
| 449 |
+
font-size: 12px;
|
| 450 |
+
color: var(--text-muted);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
/* Events List */
|
| 454 |
+
.events-list {
|
| 455 |
+
display: flex;
|
| 456 |
+
flex-direction: column;
|
| 457 |
+
gap: 8px;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.event-item {
|
| 461 |
+
display: flex;
|
| 462 |
+
align-items: center;
|
| 463 |
+
gap: 12px;
|
| 464 |
+
padding: 12px;
|
| 465 |
+
border-left: 4px solid var(--border-color);
|
| 466 |
+
background: var(--light-bg);
|
| 467 |
+
border-radius: 0 var(--radius) var(--radius) 0;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.event-item.time_in {
|
| 471 |
+
border-left-color: var(--success-color);
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.event-item.time_out {
|
| 475 |
+
border-left-color: var(--danger-color);
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.event-item.break_start {
|
| 479 |
+
border-left-color: var(--warning-color);
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
.event-item.break_end {
|
| 483 |
+
border-left-color: var(--info-color);
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
.event-item.timeout_5pm {
|
| 487 |
+
border-left-color: var(--secondary-color);
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.event-icon {
|
| 491 |
+
width: 32px;
|
| 492 |
+
height: 32px;
|
| 493 |
+
border-radius: 50%;
|
| 494 |
+
display: flex;
|
| 495 |
+
align-items: center;
|
| 496 |
+
justify-content: center;
|
| 497 |
+
font-size: 14px;
|
| 498 |
+
color: white;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
.event-item.time_in .event-icon {
|
| 502 |
+
background: var(--success-color);
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
.event-item.time_out .event-icon {
|
| 506 |
+
background: var(--danger-color);
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
.event-item.break_start .event-icon {
|
| 510 |
+
background: var(--warning-color);
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
.event-item.break_end .event-icon {
|
| 514 |
+
background: var(--info-color);
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
.event-item.timeout_5pm .event-icon {
|
| 518 |
+
background: var(--secondary-color);
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
.event-content {
|
| 522 |
+
flex: 1;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
.event-name {
|
| 526 |
+
font-weight: 600;
|
| 527 |
+
color: var(--text-primary);
|
| 528 |
+
margin-bottom: 2px;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
.event-type {
|
| 532 |
+
font-size: 12px;
|
| 533 |
+
color: var(--text-secondary);
|
| 534 |
+
text-transform: capitalize;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
.event-time {
|
| 538 |
+
font-size: 12px;
|
| 539 |
+
color: var(--text-muted);
|
| 540 |
+
text-align: right;
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
/* Summary Table */
|
| 544 |
+
.summary-table {
|
| 545 |
+
overflow-x: auto;
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
.summary-table table {
|
| 549 |
+
width: 100%;
|
| 550 |
+
border-collapse: collapse;
|
| 551 |
+
font-size: 14px;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
.summary-table th,
|
| 555 |
+
.summary-table td {
|
| 556 |
+
padding: 12px;
|
| 557 |
+
text-align: left;
|
| 558 |
+
border-bottom: 1px solid var(--border-color);
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.summary-table th {
|
| 562 |
+
background: var(--light-bg);
|
| 563 |
+
font-weight: 600;
|
| 564 |
+
color: var(--text-secondary);
|
| 565 |
+
text-transform: uppercase;
|
| 566 |
+
font-size: 12px;
|
| 567 |
+
letter-spacing: 0.5px;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.summary-table tr:hover {
|
| 571 |
+
background: var(--light-bg);
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
/* Form Elements */
|
| 575 |
+
.date-filter,
|
| 576 |
+
.date-input {
|
| 577 |
+
padding: 8px 12px;
|
| 578 |
+
border: 1px solid var(--border-color);
|
| 579 |
+
border-radius: var(--radius);
|
| 580 |
+
font-size: 14px;
|
| 581 |
+
background: white;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
.date-filter:focus,
|
| 585 |
+
.date-input:focus {
|
| 586 |
+
outline: none;
|
| 587 |
+
border-color: var(--primary-color);
|
| 588 |
+
box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
/* Modals */
|
| 592 |
+
.modal {
|
| 593 |
+
display: none;
|
| 594 |
+
position: fixed;
|
| 595 |
+
top: 0;
|
| 596 |
+
left: 0;
|
| 597 |
+
width: 100%;
|
| 598 |
+
height: 100%;
|
| 599 |
+
background: rgba(0, 0, 0, 0.5);
|
| 600 |
+
z-index: 1000;
|
| 601 |
+
backdrop-filter: blur(4px);
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
.modal.show {
|
| 605 |
+
display: flex;
|
| 606 |
+
align-items: center;
|
| 607 |
+
justify-content: center;
|
| 608 |
+
animation: fadeIn 0.2s ease;
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
.modal-content {
|
| 612 |
+
background: var(--card-bg);
|
| 613 |
+
border-radius: var(--radius-lg);
|
| 614 |
+
box-shadow: var(--shadow-lg);
|
| 615 |
+
width: 90%;
|
| 616 |
+
max-width: 500px;
|
| 617 |
+
max-height: 90vh;
|
| 618 |
+
overflow-y: auto;
|
| 619 |
+
animation: slideIn 0.3s ease;
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
.modal-header {
|
| 623 |
+
padding: 20px 24px;
|
| 624 |
+
border-bottom: 1px solid var(--border-color);
|
| 625 |
+
display: flex;
|
| 626 |
+
justify-content: space-between;
|
| 627 |
+
align-items: center;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.modal-header h3 {
|
| 631 |
+
font-size: 1.25rem;
|
| 632 |
+
font-weight: 600;
|
| 633 |
+
color: var(--text-primary);
|
| 634 |
+
display: flex;
|
| 635 |
+
align-items: center;
|
| 636 |
+
gap: 8px;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
.modal-close {
|
| 640 |
+
background: none;
|
| 641 |
+
border: none;
|
| 642 |
+
font-size: 24px;
|
| 643 |
+
cursor: pointer;
|
| 644 |
+
color: var(--text-muted);
|
| 645 |
+
padding: 4px;
|
| 646 |
+
border-radius: 4px;
|
| 647 |
+
transition: all 0.2s ease;
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
.modal-close:hover {
|
| 651 |
+
background: var(--light-bg);
|
| 652 |
+
color: var(--text-primary);
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
.modal-body {
|
| 656 |
+
padding: 24px;
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
.modal-footer {
|
| 660 |
+
padding: 16px 24px;
|
| 661 |
+
border-top: 1px solid var(--border-color);
|
| 662 |
+
display: flex;
|
| 663 |
+
justify-content: flex-end;
|
| 664 |
+
gap: 12px;
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
/* Form Styles */
|
| 668 |
+
.form-group {
|
| 669 |
+
margin-bottom: 20px;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
.form-group label {
|
| 673 |
+
display: block;
|
| 674 |
+
margin-bottom: 6px;
|
| 675 |
+
font-weight: 600;
|
| 676 |
+
color: var(--text-primary);
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.form-group input {
|
| 680 |
+
width: 100%;
|
| 681 |
+
padding: 12px 16px;
|
| 682 |
+
border: 2px solid var(--border-color);
|
| 683 |
+
border-radius: var(--radius);
|
| 684 |
+
font-size: 14px;
|
| 685 |
+
transition: all 0.2s ease;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.form-group input:focus {
|
| 689 |
+
outline: none;
|
| 690 |
+
border-color: var(--primary-color);
|
| 691 |
+
box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
.form-group small {
|
| 695 |
+
display: block;
|
| 696 |
+
margin-top: 4px;
|
| 697 |
+
color: var(--text-muted);
|
| 698 |
+
font-size: 12px;
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
/* Settings */
|
| 702 |
+
.settings-section {
|
| 703 |
+
margin-bottom: 32px;
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
.settings-section:last-child {
|
| 707 |
+
margin-bottom: 0;
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
.settings-section h4 {
|
| 711 |
+
font-size: 1.1rem;
|
| 712 |
+
font-weight: 600;
|
| 713 |
+
color: var(--text-primary);
|
| 714 |
+
margin-bottom: 16px;
|
| 715 |
+
padding-bottom: 8px;
|
| 716 |
+
border-bottom: 1px solid var(--border-color);
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.info-grid {
|
| 720 |
+
display: grid;
|
| 721 |
+
gap: 12px;
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
.info-item {
|
| 725 |
+
display: flex;
|
| 726 |
+
justify-content: space-between;
|
| 727 |
+
align-items: center;
|
| 728 |
+
padding: 12px;
|
| 729 |
+
background: var(--light-bg);
|
| 730 |
+
border-radius: var(--radius);
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
.info-label {
|
| 734 |
+
font-weight: 600;
|
| 735 |
+
color: var(--text-secondary);
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
.info-value {
|
| 739 |
+
color: var(--text-primary);
|
| 740 |
+
font-weight: 500;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
/* Notifications */
|
| 744 |
+
.notifications {
|
| 745 |
+
position: fixed;
|
| 746 |
+
top: 20px;
|
| 747 |
+
right: 20px;
|
| 748 |
+
z-index: 1100;
|
| 749 |
+
display: flex;
|
| 750 |
+
flex-direction: column;
|
| 751 |
+
gap: 12px;
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
.notification {
|
| 755 |
+
background: var(--card-bg);
|
| 756 |
+
border-radius: var(--radius);
|
| 757 |
+
padding: 16px 20px;
|
| 758 |
+
box-shadow: var(--shadow-lg);
|
| 759 |
+
border-left: 4px solid var(--primary-color);
|
| 760 |
+
min-width: 300px;
|
| 761 |
+
animation: slideInRight 0.3s ease;
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
.notification.success {
|
| 765 |
+
border-left-color: var(--success-color);
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
.notification.error {
|
| 769 |
+
border-left-color: var(--danger-color);
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
.notification.warning {
|
| 773 |
+
border-left-color: var(--warning-color);
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
.notification-title {
|
| 777 |
+
font-weight: 600;
|
| 778 |
+
color: var(--text-primary);
|
| 779 |
+
margin-bottom: 4px;
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
.notification-message {
|
| 783 |
+
color: var(--text-secondary);
|
| 784 |
+
font-size: 14px;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
/* Loading States */
|
| 788 |
+
.loading {
|
| 789 |
+
display: flex;
|
| 790 |
+
align-items: center;
|
| 791 |
+
justify-content: center;
|
| 792 |
+
padding: 40px;
|
| 793 |
+
color: var(--text-muted);
|
| 794 |
+
font-style: italic;
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
/* Animations */
|
| 798 |
+
@keyframes fadeIn {
|
| 799 |
+
from { opacity: 0; }
|
| 800 |
+
to { opacity: 1; }
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
@keyframes slideIn {
|
| 804 |
+
from {
|
| 805 |
+
opacity: 0;
|
| 806 |
+
transform: translateY(-20px);
|
| 807 |
+
}
|
| 808 |
+
to {
|
| 809 |
+
opacity: 1;
|
| 810 |
+
transform: translateY(0);
|
| 811 |
+
}
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
@keyframes slideInRight {
|
| 815 |
+
from {
|
| 816 |
+
opacity: 0;
|
| 817 |
+
transform: translateX(100%);
|
| 818 |
+
}
|
| 819 |
+
to {
|
| 820 |
+
opacity: 1;
|
| 821 |
+
transform: translateX(0);
|
| 822 |
+
}
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
/* Responsive Design */
|
| 826 |
+
@media (max-width: 768px) {
|
| 827 |
+
.container {
|
| 828 |
+
padding: 12px;
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
.header {
|
| 832 |
+
flex-direction: column;
|
| 833 |
+
text-align: center;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
.header-actions {
|
| 837 |
+
justify-content: center;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.status-bar {
|
| 841 |
+
flex-direction: column;
|
| 842 |
+
gap: 8px;
|
| 843 |
+
}
|
| 844 |
+
|
| 845 |
+
.control-panel {
|
| 846 |
+
flex-direction: column;
|
| 847 |
+
align-items: stretch;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.control-group {
|
| 851 |
+
justify-content: center;
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
.search-container {
|
| 855 |
+
flex-direction: column;
|
| 856 |
+
}
|
| 857 |
+
|
| 858 |
+
.search-input {
|
| 859 |
+
width: 100%;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
.main-grid {
|
| 863 |
+
grid-template-columns: 1fr;
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
.stats-grid {
|
| 867 |
+
grid-template-columns: repeat(2, 1fr);
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
.modal-content {
|
| 871 |
+
width: 95%;
|
| 872 |
+
margin: 20px;
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
.employee-card {
|
| 876 |
+
flex-direction: column;
|
| 877 |
+
text-align: center;
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
.employee-status {
|
| 881 |
+
justify-content: center;
|
| 882 |
+
}
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
@media (max-width: 480px) {
|
| 886 |
+
.stats-grid {
|
| 887 |
+
grid-template-columns: 1fr;
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
.panel-header {
|
| 891 |
+
flex-direction: column;
|
| 892 |
+
align-items: stretch;
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
.panel-actions {
|
| 896 |
+
justify-content: center;
|
| 897 |
+
}
|
| 898 |
+
}
|
| 899 |
+
|
|
@@ -0,0 +1,712 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// WiFi Attendance Tracker - Advanced JavaScript
|
| 2 |
+
|
| 3 |
+
class AttendanceTracker {
|
| 4 |
+
constructor() {
|
| 5 |
+
this.isMonitoring = false;
|
| 6 |
+
this.updateInterval = null;
|
| 7 |
+
this.employees = [];
|
| 8 |
+
this.events = [];
|
| 9 |
+
this.summary = [];
|
| 10 |
+
this.searchQuery = '';
|
| 11 |
+
|
| 12 |
+
this.init();
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
init() {
|
| 16 |
+
this.setupEventListeners();
|
| 17 |
+
this.loadInitialData();
|
| 18 |
+
this.startAutoUpdate();
|
| 19 |
+
this.updateCurrentTime();
|
| 20 |
+
|
| 21 |
+
// Set today's date in summary date input
|
| 22 |
+
const today = new Date().toISOString().split('T')[0];
|
| 23 |
+
document.getElementById('summaryDate').value = today;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
setupEventListeners() {
|
| 27 |
+
// Control buttons
|
| 28 |
+
document.getElementById('startBtn').addEventListener('click', () => this.startMonitoring());
|
| 29 |
+
document.getElementById('stopBtn').addEventListener('click', () => this.stopMonitoring());
|
| 30 |
+
document.getElementById('refreshBtn').addEventListener('click', () => this.refreshData());
|
| 31 |
+
document.getElementById('exportBtn').addEventListener('click', () => this.exportCSV());
|
| 32 |
+
|
| 33 |
+
// Search functionality
|
| 34 |
+
document.getElementById('searchBtn').addEventListener('click', () => this.performSearch());
|
| 35 |
+
document.getElementById('clearSearchBtn').addEventListener('click', () => this.clearSearch());
|
| 36 |
+
document.getElementById('searchInput').addEventListener('keypress', (e) => {
|
| 37 |
+
if (e.key === 'Enter') this.performSearch();
|
| 38 |
+
});
|
| 39 |
+
document.getElementById('searchInput').addEventListener('input', (e) => {
|
| 40 |
+
if (e.target.value === '') this.clearSearch();
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
// Modal buttons
|
| 44 |
+
document.getElementById('addEmployeeBtn').addEventListener('click', () => this.openModal('addEmployeeModal'));
|
| 45 |
+
document.getElementById('settingsBtn').addEventListener('click', () => this.openModal('settingsModal'));
|
| 46 |
+
|
| 47 |
+
// Form submissions
|
| 48 |
+
document.getElementById('addEmployeeForm').addEventListener('submit', (e) => this.handleAddEmployee(e));
|
| 49 |
+
document.getElementById('changePasswordForm').addEventListener('submit', (e) => this.handleChangePassword(e));
|
| 50 |
+
document.getElementById('deleteEmployeeForm').addEventListener('submit', (e) => this.handleDeleteEmployee(e));
|
| 51 |
+
document.getElementById('modifyEmployeeForm').addEventListener('submit', (e) => this.handleModifyEmployee(e));
|
| 52 |
+
|
| 53 |
+
// Summary date change
|
| 54 |
+
document.getElementById('summaryDate').addEventListener('change', () => this.loadDailySummary());
|
| 55 |
+
document.getElementById('summaryRefreshBtn').addEventListener('click', () => this.loadDailySummary());
|
| 56 |
+
|
| 57 |
+
// Event date filter
|
| 58 |
+
document.getElementById('eventDateFilter').addEventListener('change', () => this.loadEvents());
|
| 59 |
+
|
| 60 |
+
// Modal close buttons
|
| 61 |
+
document.querySelectorAll('.modal-close').forEach(btn => {
|
| 62 |
+
btn.addEventListener('click', (e) => {
|
| 63 |
+
const modal = e.target.closest('.modal');
|
| 64 |
+
this.closeModal(modal.id);
|
| 65 |
+
});
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
// Close modal when clicking outside
|
| 69 |
+
document.querySelectorAll('.modal').forEach(modal => {
|
| 70 |
+
modal.addEventListener('click', (e) => {
|
| 71 |
+
if (e.target === modal) {
|
| 72 |
+
this.closeModal(modal.id);
|
| 73 |
+
}
|
| 74 |
+
});
|
| 75 |
+
});
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
async loadInitialData() {
|
| 79 |
+
await Promise.all([
|
| 80 |
+
this.loadSystemStatus(),
|
| 81 |
+
this.loadEmployees(),
|
| 82 |
+
this.loadEvents(),
|
| 83 |
+
this.loadDailySummary(),
|
| 84 |
+
this.loadSummaryStats()
|
| 85 |
+
]);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
async loadSystemStatus() {
|
| 89 |
+
try {
|
| 90 |
+
const response = await fetch('/api/status');
|
| 91 |
+
const status = await response.json();
|
| 92 |
+
|
| 93 |
+
document.getElementById('systemStatus').textContent = status.is_monitoring ? 'Monitoring' : 'Stopped';
|
| 94 |
+
document.getElementById('systemStatus').className = `status-value ${status.is_monitoring ? 'monitoring' : 'stopped'}`;
|
| 95 |
+
document.getElementById('employeeCount').textContent = status.employee_count;
|
| 96 |
+
document.getElementById('scanInterval').textContent = `${status.scan_interval} seconds`;
|
| 97 |
+
document.getElementById('officeTimeout').textContent = status.office_timeout;
|
| 98 |
+
|
| 99 |
+
this.isMonitoring = status.is_monitoring;
|
| 100 |
+
this.updateControlButtons();
|
| 101 |
+
|
| 102 |
+
} catch (error) {
|
| 103 |
+
console.error('Error loading system status:', error);
|
| 104 |
+
this.showNotification('Error loading system status', 'error');
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
async loadEmployees() {
|
| 109 |
+
try {
|
| 110 |
+
const response = await fetch('/api/employees');
|
| 111 |
+
this.employees = await response.json();
|
| 112 |
+
this.renderEmployees();
|
| 113 |
+
} catch (error) {
|
| 114 |
+
console.error('Error loading employees:', error);
|
| 115 |
+
this.showNotification('Error loading employees', 'error');
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
async loadEvents() {
|
| 120 |
+
try {
|
| 121 |
+
const dateFilter = document.getElementById('eventDateFilter').value;
|
| 122 |
+
const url = dateFilter ? `/api/attendance_events?date=${dateFilter}&limit=50` : '/api/attendance_events?limit=50';
|
| 123 |
+
const response = await fetch(url);
|
| 124 |
+
this.events = await response.json();
|
| 125 |
+
this.renderEvents();
|
| 126 |
+
} catch (error) {
|
| 127 |
+
console.error('Error loading events:', error);
|
| 128 |
+
this.showNotification('Error loading events', 'error');
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
async loadDailySummary() {
|
| 133 |
+
try {
|
| 134 |
+
const date = document.getElementById('summaryDate').value;
|
| 135 |
+
const response = await fetch(`/api/daily_summary?date=${date}`);
|
| 136 |
+
this.summary = await response.json();
|
| 137 |
+
this.renderDailySummary();
|
| 138 |
+
} catch (error) {
|
| 139 |
+
console.error('Error loading daily summary:', error);
|
| 140 |
+
this.showNotification('Error loading daily summary', 'error');
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
async loadSummaryStats() {
|
| 145 |
+
try {
|
| 146 |
+
const date = document.getElementById('summaryDate').value;
|
| 147 |
+
const response = await fetch(`/api/summary_stats?date=${date}`);
|
| 148 |
+
const stats = await response.json();
|
| 149 |
+
|
| 150 |
+
document.getElementById('presentCount').textContent = stats.present_count || 0;
|
| 151 |
+
document.getElementById('absentCount').textContent = stats.absent_count || 0;
|
| 152 |
+
document.getElementById('breakCount').textContent = stats.on_break_count || 0;
|
| 153 |
+
document.getElementById('timeoutCount').textContent = stats.timed_out_count || 0;
|
| 154 |
+
|
| 155 |
+
} catch (error) {
|
| 156 |
+
console.error('Error loading summary stats:', error);
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
renderEmployees() {
|
| 161 |
+
const container = document.getElementById('employeeList');
|
| 162 |
+
|
| 163 |
+
if (this.employees.length === 0) {
|
| 164 |
+
container.innerHTML = '<div class="loading">No employees found</div>';
|
| 165 |
+
return;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
// Filter employees based on search query
|
| 169 |
+
const filteredEmployees = this.searchQuery
|
| 170 |
+
? this.employees.filter(emp =>
|
| 171 |
+
emp.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
|
| 172 |
+
emp.mac.toLowerCase().includes(this.searchQuery.toLowerCase())
|
| 173 |
+
)
|
| 174 |
+
: this.employees;
|
| 175 |
+
|
| 176 |
+
if (filteredEmployees.length === 0) {
|
| 177 |
+
container.innerHTML = '<div class="loading">No employees match your search</div>';
|
| 178 |
+
return;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
container.innerHTML = filteredEmployees.map(employee => `
|
| 182 |
+
<div class="employee-card" onclick="attendanceTracker.showEmployeeDetails('${employee.mac}')">
|
| 183 |
+
<div class="employee-avatar">
|
| 184 |
+
${employee.picture ?
|
| 185 |
+
`<img src="${employee.picture}" alt="${employee.name}" onerror="this.style.display='none'; this.parentNode.textContent='${employee.name.charAt(0).toUpperCase()}'">` :
|
| 186 |
+
employee.name.charAt(0).toUpperCase()
|
| 187 |
+
}
|
| 188 |
+
</div>
|
| 189 |
+
<div class="employee-info">
|
| 190 |
+
<div class="employee-name">${employee.name}</div>
|
| 191 |
+
<div class="employee-mac">${employee.mac}</div>
|
| 192 |
+
</div>
|
| 193 |
+
<div class="employee-status">
|
| 194 |
+
<span class="status-badge ${employee.status.toLowerCase().replace(' ', '')}">${employee.status}</span>
|
| 195 |
+
<div class="employee-time">
|
| 196 |
+
${employee.time_in !== 'N/A' ? `In: ${employee.time_in}` : 'Not checked in'}
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
`).join('');
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
renderEvents() {
|
| 204 |
+
const container = document.getElementById('eventsList');
|
| 205 |
+
|
| 206 |
+
if (this.events.length === 0) {
|
| 207 |
+
container.innerHTML = '<div class="loading">No recent events</div>';
|
| 208 |
+
return;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
container.innerHTML = this.events.map(event => `
|
| 212 |
+
<div class="event-item ${event.event_type}">
|
| 213 |
+
<div class="event-icon">
|
| 214 |
+
<i class="fas ${this.getEventIcon(event.event_type)}"></i>
|
| 215 |
+
</div>
|
| 216 |
+
<div class="event-content">
|
| 217 |
+
<div class="event-name">${event.employee_name}</div>
|
| 218 |
+
<div class="event-type">${this.formatEventType(event.event_type)}</div>
|
| 219 |
+
</div>
|
| 220 |
+
<div class="event-time">${event.time_ago}</div>
|
| 221 |
+
</div>
|
| 222 |
+
`).join('');
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
renderDailySummary() {
|
| 226 |
+
const container = document.getElementById('summaryTable');
|
| 227 |
+
|
| 228 |
+
if (this.summary.length === 0) {
|
| 229 |
+
container.innerHTML = '<div class="loading">No summary data for selected date</div>';
|
| 230 |
+
return;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
container.innerHTML = `
|
| 234 |
+
<table>
|
| 235 |
+
<thead>
|
| 236 |
+
<tr>
|
| 237 |
+
<th>Employee</th>
|
| 238 |
+
<th>Time In</th>
|
| 239 |
+
<th>Time Out</th>
|
| 240 |
+
<th>Work Duration</th>
|
| 241 |
+
<th>Break Duration</th>
|
| 242 |
+
<th>Status</th>
|
| 243 |
+
</tr>
|
| 244 |
+
</thead>
|
| 245 |
+
<tbody>
|
| 246 |
+
${this.summary.map(emp => `
|
| 247 |
+
<tr>
|
| 248 |
+
<td>
|
| 249 |
+
<div style="display: flex; align-items: center; gap: 8px;">
|
| 250 |
+
<div class="employee-avatar" style="width: 32px; height: 32px; font-size: 12px;">
|
| 251 |
+
${emp.name.charAt(0).toUpperCase()}
|
| 252 |
+
</div>
|
| 253 |
+
<div>
|
| 254 |
+
<div style="font-weight: 600;">${emp.name}</div>
|
| 255 |
+
<div style="font-size: 11px; color: var(--text-muted);">${emp.mac_address}</div>
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
</td>
|
| 259 |
+
<td>${emp.time_in || 'N/A'}</td>
|
| 260 |
+
<td>${emp.time_out || 'N/A'}</td>
|
| 261 |
+
<td>${emp.total_work_formatted}</td>
|
| 262 |
+
<td>${emp.total_break_formatted}</td>
|
| 263 |
+
<td><span class="status-badge ${emp.status.toLowerCase().replace(' ', '')}">${emp.status}</span></td>
|
| 264 |
+
</tr>
|
| 265 |
+
`).join('')}
|
| 266 |
+
</tbody>
|
| 267 |
+
</table>
|
| 268 |
+
`;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
getEventIcon(eventType) {
|
| 272 |
+
const icons = {
|
| 273 |
+
'time_in': 'fa-sign-in-alt',
|
| 274 |
+
'time_out': 'fa-sign-out-alt',
|
| 275 |
+
'break_start': 'fa-coffee',
|
| 276 |
+
'break_end': 'fa-play',
|
| 277 |
+
'timeout_5pm': 'fa-clock'
|
| 278 |
+
};
|
| 279 |
+
return icons[eventType] || 'fa-circle';
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
formatEventType(eventType) {
|
| 283 |
+
const formats = {
|
| 284 |
+
'time_in': 'Time In',
|
| 285 |
+
'time_out': 'Time Out',
|
| 286 |
+
'break_start': 'Break Start',
|
| 287 |
+
'break_end': 'Break End',
|
| 288 |
+
'timeout_5pm': '5 PM Timeout'
|
| 289 |
+
};
|
| 290 |
+
return formats[eventType] || eventType;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
async startMonitoring() {
|
| 294 |
+
try {
|
| 295 |
+
const response = await fetch('/api/start_monitoring', { method: 'POST' });
|
| 296 |
+
const result = await response.json();
|
| 297 |
+
|
| 298 |
+
if (result.success) {
|
| 299 |
+
this.isMonitoring = true;
|
| 300 |
+
this.updateControlButtons();
|
| 301 |
+
this.showNotification('Monitoring started successfully', 'success');
|
| 302 |
+
await this.loadSystemStatus();
|
| 303 |
+
} else {
|
| 304 |
+
this.showNotification(result.message, 'error');
|
| 305 |
+
}
|
| 306 |
+
} catch (error) {
|
| 307 |
+
console.error('Error starting monitoring:', error);
|
| 308 |
+
this.showNotification('Error starting monitoring', 'error');
|
| 309 |
+
}
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
async stopMonitoring() {
|
| 313 |
+
try {
|
| 314 |
+
const response = await fetch('/api/stop_monitoring', { method: 'POST' });
|
| 315 |
+
const result = await response.json();
|
| 316 |
+
|
| 317 |
+
if (result.success) {
|
| 318 |
+
this.isMonitoring = false;
|
| 319 |
+
this.updateControlButtons();
|
| 320 |
+
this.showNotification('Monitoring stopped', 'warning');
|
| 321 |
+
await this.loadSystemStatus();
|
| 322 |
+
} else {
|
| 323 |
+
this.showNotification(result.message, 'error');
|
| 324 |
+
}
|
| 325 |
+
} catch (error) {
|
| 326 |
+
console.error('Error stopping monitoring:', error);
|
| 327 |
+
this.showNotification('Error stopping monitoring', 'error');
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
async refreshData() {
|
| 332 |
+
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
|
| 333 |
+
await this.loadInitialData();
|
| 334 |
+
this.showNotification('Data refreshed', 'success');
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
async exportCSV() {
|
| 338 |
+
try {
|
| 339 |
+
const date = document.getElementById('summaryDate').value;
|
| 340 |
+
const response = await fetch(`/api/export_csv?date=${date}`);
|
| 341 |
+
const result = await response.json();
|
| 342 |
+
|
| 343 |
+
if (result.success) {
|
| 344 |
+
this.showNotification('CSV exported successfully', 'success');
|
| 345 |
+
} else {
|
| 346 |
+
this.showNotification(result.message, 'error');
|
| 347 |
+
}
|
| 348 |
+
} catch (error) {
|
| 349 |
+
console.error('Error exporting CSV:', error);
|
| 350 |
+
this.showNotification('Error exporting CSV', 'error');
|
| 351 |
+
}
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
performSearch() {
|
| 355 |
+
this.searchQuery = document.getElementById('searchInput').value.trim();
|
| 356 |
+
this.renderEmployees();
|
| 357 |
+
|
| 358 |
+
if (this.searchQuery) {
|
| 359 |
+
this.showNotification(`Searching for: ${this.searchQuery}`, 'info');
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
clearSearch() {
|
| 364 |
+
this.searchQuery = '';
|
| 365 |
+
document.getElementById('searchInput').value = '';
|
| 366 |
+
this.renderEmployees();
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
updateControlButtons() {
|
| 370 |
+
const startBtn = document.getElementById('startBtn');
|
| 371 |
+
const stopBtn = document.getElementById('stopBtn');
|
| 372 |
+
|
| 373 |
+
startBtn.disabled = this.isMonitoring;
|
| 374 |
+
stopBtn.disabled = !this.isMonitoring;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
startAutoUpdate() {
|
| 378 |
+
// Update data every 10 seconds
|
| 379 |
+
this.updateInterval = setInterval(() => {
|
| 380 |
+
if (this.isMonitoring) {
|
| 381 |
+
this.loadEmployees();
|
| 382 |
+
this.loadEvents();
|
| 383 |
+
this.loadSummaryStats();
|
| 384 |
+
}
|
| 385 |
+
}, 10000);
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
updateCurrentTime() {
|
| 389 |
+
const updateTime = () => {
|
| 390 |
+
const now = new Date();
|
| 391 |
+
document.getElementById('currentTime').textContent = now.toLocaleTimeString();
|
| 392 |
+
};
|
| 393 |
+
|
| 394 |
+
updateTime();
|
| 395 |
+
setInterval(updateTime, 1000);
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
openModal(modalId) {
|
| 399 |
+
const modal = document.getElementById(modalId);
|
| 400 |
+
modal.classList.add('show');
|
| 401 |
+
|
| 402 |
+
// Load settings data if opening settings modal
|
| 403 |
+
if (modalId === 'settingsModal') {
|
| 404 |
+
this.loadSystemStatus();
|
| 405 |
+
}
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
closeModal(modalId) {
|
| 409 |
+
const modal = document.getElementById(modalId);
|
| 410 |
+
modal.classList.remove('show');
|
| 411 |
+
|
| 412 |
+
// Reset forms
|
| 413 |
+
const forms = modal.querySelectorAll('form');
|
| 414 |
+
forms.forEach(form => form.reset());
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
async handleAddEmployee(e) {
|
| 418 |
+
e.preventDefault();
|
| 419 |
+
|
| 420 |
+
const formData = new FormData(e.target);
|
| 421 |
+
const employeeData = {
|
| 422 |
+
name: formData.get('name'),
|
| 423 |
+
mac: formData.get('mac').toLowerCase(),
|
| 424 |
+
picture: formData.get('picture'),
|
| 425 |
+
password: formData.get('password')
|
| 426 |
+
};
|
| 427 |
+
|
| 428 |
+
try {
|
| 429 |
+
const response = await fetch('/api/add_employee', {
|
| 430 |
+
method: 'POST',
|
| 431 |
+
headers: {
|
| 432 |
+
'Content-Type': 'application/json',
|
| 433 |
+
},
|
| 434 |
+
body: JSON.stringify(employeeData)
|
| 435 |
+
});
|
| 436 |
+
|
| 437 |
+
const result = await response.json();
|
| 438 |
+
|
| 439 |
+
if (result.success) {
|
| 440 |
+
this.showNotification('Employee added successfully', 'success');
|
| 441 |
+
this.closeModal('addEmployeeModal');
|
| 442 |
+
await this.loadEmployees();
|
| 443 |
+
await this.loadSystemStatus();
|
| 444 |
+
} else {
|
| 445 |
+
this.showNotification(result.message, 'error');
|
| 446 |
+
}
|
| 447 |
+
} catch (error) {
|
| 448 |
+
console.error('Error adding employee:', error);
|
| 449 |
+
this.showNotification('Error adding employee', 'error');
|
| 450 |
+
}
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
async handleChangePassword(e) {
|
| 454 |
+
e.preventDefault();
|
| 455 |
+
|
| 456 |
+
const formData = new FormData(e.target);
|
| 457 |
+
const newPassword = formData.get('newPassword');
|
| 458 |
+
const confirmPassword = formData.get('confirmPassword');
|
| 459 |
+
|
| 460 |
+
if (newPassword !== confirmPassword) {
|
| 461 |
+
this.showNotification('Passwords do not match', 'error');
|
| 462 |
+
return;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
const passwordData = {
|
| 466 |
+
currentPassword: formData.get('currentPassword'),
|
| 467 |
+
newPassword: newPassword
|
| 468 |
+
};
|
| 469 |
+
|
| 470 |
+
try {
|
| 471 |
+
const response = await fetch('/api/change_password', {
|
| 472 |
+
method: 'POST',
|
| 473 |
+
headers: {
|
| 474 |
+
'Content-Type': 'application/json',
|
| 475 |
+
},
|
| 476 |
+
body: JSON.stringify(passwordData)
|
| 477 |
+
});
|
| 478 |
+
|
| 479 |
+
const result = await response.json();
|
| 480 |
+
|
| 481 |
+
if (result.success) {
|
| 482 |
+
this.showNotification('Password changed successfully', 'success');
|
| 483 |
+
this.closeModal('settingsModal');
|
| 484 |
+
} else {
|
| 485 |
+
this.showNotification(result.message, 'error');
|
| 486 |
+
}
|
| 487 |
+
} catch (error) {
|
| 488 |
+
console.error('Error changing password:', error);
|
| 489 |
+
this.showNotification('Error changing password', 'error');
|
| 490 |
+
}
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
showEmployeeDetails(mac) {
|
| 494 |
+
const employee = this.employees.find(emp => emp.mac === mac);
|
| 495 |
+
if (!employee) return;
|
| 496 |
+
|
| 497 |
+
const modal = document.getElementById('employeeDetailsModal');
|
| 498 |
+
const content = document.getElementById('employeeDetailsContent');
|
| 499 |
+
|
| 500 |
+
content.innerHTML = `
|
| 501 |
+
<div style="text-align: center; margin-bottom: 20px;">
|
| 502 |
+
<div class="employee-avatar" style="width: 80px; height: 80px; font-size: 32px; margin: 0 auto 12px;">
|
| 503 |
+
${employee.picture ?
|
| 504 |
+
`<img src="${employee.picture}" alt="${employee.name}" style="width: 100%; height: 100%; object-fit: cover;">` :
|
| 505 |
+
employee.name.charAt(0).toUpperCase()
|
| 506 |
+
}
|
| 507 |
+
</div>
|
| 508 |
+
<h3>${employee.name}</h3>
|
| 509 |
+
<p style="color: var(--text-muted); font-family: monospace;">${employee.mac}</p>
|
| 510 |
+
</div>
|
| 511 |
+
|
| 512 |
+
<div class="info-grid">
|
| 513 |
+
<div class="info-item">
|
| 514 |
+
<span class="info-label">Current Status:</span>
|
| 515 |
+
<span class="status-badge ${employee.status.toLowerCase().replace(' ', '')}">${employee.status}</span>
|
| 516 |
+
</div>
|
| 517 |
+
<div class="info-item">
|
| 518 |
+
<span class="info-label">Time In:</span>
|
| 519 |
+
<span class="info-value">${employee.time_in}</span>
|
| 520 |
+
</div>
|
| 521 |
+
<div class="info-item">
|
| 522 |
+
<span class="info-label">Last Seen:</span>
|
| 523 |
+
<span class="info-value">${employee.last_seen}</span>
|
| 524 |
+
</div>
|
| 525 |
+
<div class="info-item">
|
| 526 |
+
<span class="info-label">Is Present:</span>
|
| 527 |
+
<span class="info-value">${employee.is_present ? 'Yes' : 'No'}</span>
|
| 528 |
+
</div>
|
| 529 |
+
</div>
|
| 530 |
+
`;
|
| 531 |
+
|
| 532 |
+
this.openModal('employeeDetailsModal');
|
| 533 |
+
|
| 534 |
+
// Store current employee for delete/modify operations
|
| 535 |
+
this.currentEmployee = employee;
|
| 536 |
+
|
| 537 |
+
// Add event listeners for delete and modify buttons
|
| 538 |
+
document.getElementById('deleteEmployeeBtn').onclick = () => this.openDeleteEmployeeModal();
|
| 539 |
+
document.getElementById('modifyEmployeeBtn').onclick = () => this.openModifyEmployeeModal();
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
openDeleteEmployeeModal() {
|
| 543 |
+
if (!this.currentEmployee) return;
|
| 544 |
+
|
| 545 |
+
// Populate delete employee info
|
| 546 |
+
document.getElementById('deleteEmployeeInfo').innerHTML = `
|
| 547 |
+
<div style="text-align: center; padding: 15px; background: #fee; border: 1px solid #fcc; border-radius: 8px;">
|
| 548 |
+
<h4>${this.currentEmployee.name}</h4>
|
| 549 |
+
<p style="font-family: monospace; color: #666;">${this.currentEmployee.mac}</p>
|
| 550 |
+
<p style="color: #999;">Status: ${this.currentEmployee.status}</p>
|
| 551 |
+
</div>
|
| 552 |
+
`;
|
| 553 |
+
|
| 554 |
+
// Clear password field
|
| 555 |
+
document.getElementById('deleteAdminPassword').value = '';
|
| 556 |
+
|
| 557 |
+
this.closeModal('employeeDetailsModal');
|
| 558 |
+
this.openModal('deleteEmployeeModal');
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
openModifyEmployeeModal() {
|
| 562 |
+
if (!this.currentEmployee) return;
|
| 563 |
+
|
| 564 |
+
// Pre-fill the form with current employee data
|
| 565 |
+
document.getElementById('modifyEmployeeName').value = this.currentEmployee.name;
|
| 566 |
+
document.getElementById('modifyEmployeeMac').value = this.currentEmployee.mac;
|
| 567 |
+
document.getElementById('modifyEmployeePicture').value = this.currentEmployee.picture || '';
|
| 568 |
+
document.getElementById('modifyAdminPassword').value = '';
|
| 569 |
+
|
| 570 |
+
this.closeModal('employeeDetailsModal');
|
| 571 |
+
this.openModal('modifyEmployeeModal');
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
async handleDeleteEmployee(e) {
|
| 575 |
+
e.preventDefault();
|
| 576 |
+
|
| 577 |
+
if (!this.currentEmployee) {
|
| 578 |
+
this.showNotification('No employee selected', 'error');
|
| 579 |
+
return;
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
const formData = new FormData(e.target);
|
| 583 |
+
const password = formData.get('password');
|
| 584 |
+
|
| 585 |
+
if (!password) {
|
| 586 |
+
this.showNotification('Please enter admin password', 'error');
|
| 587 |
+
return;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
try {
|
| 591 |
+
const response = await fetch('/api/delete_employee', {
|
| 592 |
+
method: 'POST',
|
| 593 |
+
headers: {
|
| 594 |
+
'Content-Type': 'application/json',
|
| 595 |
+
},
|
| 596 |
+
body: JSON.stringify({
|
| 597 |
+
employee_id: this.getEmployeeIdByMac(this.currentEmployee.mac),
|
| 598 |
+
password: password
|
| 599 |
+
})
|
| 600 |
+
});
|
| 601 |
+
|
| 602 |
+
const result = await response.json();
|
| 603 |
+
|
| 604 |
+
if (result.success) {
|
| 605 |
+
this.showNotification(result.message, 'success');
|
| 606 |
+
this.closeModal('deleteEmployeeModal');
|
| 607 |
+
await this.loadEmployees();
|
| 608 |
+
await this.loadSummaryStats();
|
| 609 |
+
} else {
|
| 610 |
+
this.showNotification(result.message, 'error');
|
| 611 |
+
}
|
| 612 |
+
} catch (error) {
|
| 613 |
+
console.error('Error deleting employee:', error);
|
| 614 |
+
this.showNotification('Error deleting employee', 'error');
|
| 615 |
+
}
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
async handleModifyEmployee(e) {
|
| 619 |
+
e.preventDefault();
|
| 620 |
+
|
| 621 |
+
if (!this.currentEmployee) {
|
| 622 |
+
this.showNotification('No employee selected', 'error');
|
| 623 |
+
return;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
const formData = new FormData(e.target);
|
| 627 |
+
const name = formData.get('name');
|
| 628 |
+
const mac_address = formData.get('mac_address');
|
| 629 |
+
const picture_path = formData.get('picture_path');
|
| 630 |
+
const password = formData.get('password');
|
| 631 |
+
|
| 632 |
+
if (!password) {
|
| 633 |
+
this.showNotification('Please enter admin password', 'error');
|
| 634 |
+
return;
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
if (!name || !mac_address) {
|
| 638 |
+
this.showNotification('Please fill in all required fields', 'error');
|
| 639 |
+
return;
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
try {
|
| 643 |
+
const response = await fetch('/api/modify_employee', {
|
| 644 |
+
method: 'POST',
|
| 645 |
+
headers: {
|
| 646 |
+
'Content-Type': 'application/json',
|
| 647 |
+
},
|
| 648 |
+
body: JSON.stringify({
|
| 649 |
+
employee_id: this.getEmployeeIdByMac(this.currentEmployee.mac),
|
| 650 |
+
name: name,
|
| 651 |
+
mac_address: mac_address,
|
| 652 |
+
picture_path: picture_path,
|
| 653 |
+
password: password
|
| 654 |
+
})
|
| 655 |
+
});
|
| 656 |
+
|
| 657 |
+
const result = await response.json();
|
| 658 |
+
|
| 659 |
+
if (result.success) {
|
| 660 |
+
this.showNotification(result.message, 'success');
|
| 661 |
+
this.closeModal('modifyEmployeeModal');
|
| 662 |
+
await this.loadEmployees();
|
| 663 |
+
await this.loadSummaryStats();
|
| 664 |
+
} else {
|
| 665 |
+
this.showNotification(result.message, 'error');
|
| 666 |
+
}
|
| 667 |
+
} catch (error) {
|
| 668 |
+
console.error('Error modifying employee:', error);
|
| 669 |
+
this.showNotification('Error modifying employee', 'error');
|
| 670 |
+
}
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
getEmployeeIdByMac(mac) {
|
| 674 |
+
// Since we don't have employee ID in the frontend data, we'll need to get it from the API
|
| 675 |
+
// For now, we'll use the MAC address as identifier and let the backend handle the ID lookup
|
| 676 |
+
return mac;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
showNotification(message, type = 'info') {
|
| 680 |
+
const container = document.getElementById('notifications');
|
| 681 |
+
const notification = document.createElement('div');
|
| 682 |
+
notification.className = `notification ${type}`;
|
| 683 |
+
|
| 684 |
+
const title = type.charAt(0).toUpperCase() + type.slice(1);
|
| 685 |
+
notification.innerHTML = `
|
| 686 |
+
<div class="notification-title">${title}</div>
|
| 687 |
+
<div class="notification-message">${message}</div>
|
| 688 |
+
`;
|
| 689 |
+
|
| 690 |
+
container.appendChild(notification);
|
| 691 |
+
|
| 692 |
+
// Auto remove after 5 seconds
|
| 693 |
+
setTimeout(() => {
|
| 694 |
+
if (notification.parentNode) {
|
| 695 |
+
notification.parentNode.removeChild(notification);
|
| 696 |
+
}
|
| 697 |
+
}, 5000);
|
| 698 |
+
}
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
// Global functions for modal management
|
| 702 |
+
function closeModal(modalId) {
|
| 703 |
+
if (window.attendanceTracker) {
|
| 704 |
+
window.attendanceTracker.closeModal(modalId);
|
| 705 |
+
}
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
// Initialize the application when DOM is loaded
|
| 709 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 710 |
+
window.attendanceTracker = new AttendanceTracker();
|
| 711 |
+
});
|
| 712 |
+
|
|
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>WiFi Attendance Tracker - Advanced</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 8 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div class="container">
|
| 12 |
+
<!-- Header -->
|
| 13 |
+
<header class="header">
|
| 14 |
+
<div class="header-content">
|
| 15 |
+
<h1><i class="fas fa-wifi"></i> WiFi Attendance Tracker</h1>
|
| 16 |
+
<p class="subtitle">Advanced Employee Management & Time Tracking</p>
|
| 17 |
+
</div>
|
| 18 |
+
<div class="header-actions">
|
| 19 |
+
<button id="settingsBtn" class="btn btn-secondary">
|
| 20 |
+
<i class="fas fa-cog"></i> Settings
|
| 21 |
+
</button>
|
| 22 |
+
<button id="addEmployeeBtn" class="btn btn-primary">
|
| 23 |
+
<i class="fas fa-user-plus"></i> Add Employee
|
| 24 |
+
</button>
|
| 25 |
+
</div>
|
| 26 |
+
</header>
|
| 27 |
+
|
| 28 |
+
<!-- System Status -->
|
| 29 |
+
<div class="status-bar">
|
| 30 |
+
<div class="status-item">
|
| 31 |
+
<span class="status-label">System Status:</span>
|
| 32 |
+
<span id="systemStatus" class="status-value">Loading...</span>
|
| 33 |
+
</div>
|
| 34 |
+
<div class="status-item">
|
| 35 |
+
<span class="status-label">Last Update:</span>
|
| 36 |
+
<span id="lastUpdate" class="status-value">Never</span>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="status-item">
|
| 39 |
+
<span class="status-label">Employees:</span>
|
| 40 |
+
<span id="employeeCount" class="status-value">0</span>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<!-- Control Panel -->
|
| 45 |
+
<div class="control-panel">
|
| 46 |
+
<div class="control-group">
|
| 47 |
+
<button id="startBtn" class="btn btn-success">
|
| 48 |
+
<i class="fas fa-play"></i> Start Monitoring
|
| 49 |
+
</button>
|
| 50 |
+
<button id="stopBtn" class="btn btn-danger" disabled>
|
| 51 |
+
<i class="fas fa-stop"></i> Stop Monitoring
|
| 52 |
+
</button>
|
| 53 |
+
<button id="refreshBtn" class="btn btn-info">
|
| 54 |
+
<i class="fas fa-sync-alt"></i> Refresh Data
|
| 55 |
+
</button>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="control-group">
|
| 58 |
+
<div class="search-container">
|
| 59 |
+
<input type="text" id="searchInput" placeholder="Search employees..." class="search-input">
|
| 60 |
+
<button id="searchBtn" class="btn btn-outline">
|
| 61 |
+
<i class="fas fa-search"></i>
|
| 62 |
+
</button>
|
| 63 |
+
<button id="clearSearchBtn" class="btn btn-outline">
|
| 64 |
+
<i class="fas fa-times"></i>
|
| 65 |
+
</button>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<!-- Statistics Cards -->
|
| 71 |
+
<div class="stats-grid">
|
| 72 |
+
<div class="stat-card present">
|
| 73 |
+
<div class="stat-icon">
|
| 74 |
+
<i class="fas fa-user-check"></i>
|
| 75 |
+
</div>
|
| 76 |
+
<div class="stat-content">
|
| 77 |
+
<h3 id="presentCount">0</h3>
|
| 78 |
+
<p>Present</p>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
<div class="stat-card absent">
|
| 82 |
+
<div class="stat-icon">
|
| 83 |
+
<i class="fas fa-user-times"></i>
|
| 84 |
+
</div>
|
| 85 |
+
<div class="stat-content">
|
| 86 |
+
<h3 id="absentCount">0</h3>
|
| 87 |
+
<p>Absent</p>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="stat-card break">
|
| 91 |
+
<div class="stat-icon">
|
| 92 |
+
<i class="fas fa-coffee"></i>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="stat-content">
|
| 95 |
+
<h3 id="breakCount">0</h3>
|
| 96 |
+
<p>On Break</p>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
<div class="stat-card timeout">
|
| 100 |
+
<div class="stat-icon">
|
| 101 |
+
<i class="fas fa-clock"></i>
|
| 102 |
+
</div>
|
| 103 |
+
<div class="stat-content">
|
| 104 |
+
<h3 id="timeoutCount">0</h3>
|
| 105 |
+
<p>Timed Out</p>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
<!-- Main Content Grid -->
|
| 111 |
+
<div class="main-grid">
|
| 112 |
+
<!-- Employee Status -->
|
| 113 |
+
<div class="panel">
|
| 114 |
+
<div class="panel-header">
|
| 115 |
+
<h2><i class="fas fa-users"></i> Employee Status</h2>
|
| 116 |
+
<div class="panel-actions">
|
| 117 |
+
<button id="exportBtn" class="btn btn-outline">
|
| 118 |
+
<i class="fas fa-download"></i> Export CSV
|
| 119 |
+
</button>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
<div class="panel-content">
|
| 123 |
+
<div id="employeeList" class="employee-list">
|
| 124 |
+
<div class="loading">Loading employees...</div>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
<!-- Recent Events -->
|
| 130 |
+
<div class="panel">
|
| 131 |
+
<div class="panel-header">
|
| 132 |
+
<h2><i class="fas fa-history"></i> Recent Events</h2>
|
| 133 |
+
<div class="panel-actions">
|
| 134 |
+
<select id="eventDateFilter" class="date-filter">
|
| 135 |
+
<option value="">All Events</option>
|
| 136 |
+
</select>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
<div class="panel-content">
|
| 140 |
+
<div id="eventsList" class="events-list">
|
| 141 |
+
<div class="loading">Loading events...</div>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<!-- Daily Summary -->
|
| 148 |
+
<div class="panel">
|
| 149 |
+
<div class="panel-header">
|
| 150 |
+
<h2><i class="fas fa-calendar-day"></i> Daily Summary</h2>
|
| 151 |
+
<div class="panel-actions">
|
| 152 |
+
<input type="date" id="summaryDate" class="date-input">
|
| 153 |
+
<button id="summaryRefreshBtn" class="btn btn-outline">
|
| 154 |
+
<i class="fas fa-sync-alt"></i> Refresh
|
| 155 |
+
</button>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
<div class="panel-content">
|
| 159 |
+
<div id="summaryTable" class="summary-table">
|
| 160 |
+
<div class="loading">Loading daily summary...</div>
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
<!-- Add Employee Modal -->
|
| 167 |
+
<div id="addEmployeeModal" class="modal">
|
| 168 |
+
<div class="modal-content">
|
| 169 |
+
<div class="modal-header">
|
| 170 |
+
<h3><i class="fas fa-user-plus"></i> Add New Employee</h3>
|
| 171 |
+
<button class="modal-close">×</button>
|
| 172 |
+
</div>
|
| 173 |
+
<div class="modal-body">
|
| 174 |
+
<form id="addEmployeeForm">
|
| 175 |
+
<div class="form-group">
|
| 176 |
+
<label for="employeeName">Employee Name</label>
|
| 177 |
+
<input type="text" id="employeeName" name="name" required placeholder="Enter employee name">
|
| 178 |
+
</div>
|
| 179 |
+
<div class="form-group">
|
| 180 |
+
<label for="employeeMac">MAC Address</label>
|
| 181 |
+
<input type="text" id="employeeMac" name="mac" required placeholder="aa-bb-cc-dd-ee-ff" pattern="[a-fA-F0-9]{2}-[a-fA-F0-9]{2}-[a-fA-F0-9]{2}-[a-fA-F0-9]{2}-[a-fA-F0-9]{2}-[a-fA-F0-9]{2}">
|
| 182 |
+
<small>Format: aa-bb-cc-dd-ee-ff (lowercase with dashes)</small>
|
| 183 |
+
</div>
|
| 184 |
+
<div class="form-group">
|
| 185 |
+
<label for="employeePicture">Picture Path (Optional)</label>
|
| 186 |
+
<input type="text" id="employeePicture" name="picture" placeholder="static/img/employee.jpg">
|
| 187 |
+
<small>Path to employee picture file</small>
|
| 188 |
+
</div>
|
| 189 |
+
<div class="form-group">
|
| 190 |
+
<label for="adminPassword">Admin Password</label>
|
| 191 |
+
<input type="password" id="adminPassword" name="password" required placeholder="Enter admin password">
|
| 192 |
+
<small>Default password: 1122</small>
|
| 193 |
+
</div>
|
| 194 |
+
</form>
|
| 195 |
+
</div>
|
| 196 |
+
<div class="modal-footer">
|
| 197 |
+
<button type="button" class="btn btn-secondary" onclick="closeModal('addEmployeeModal')">Cancel</button>
|
| 198 |
+
<button type="submit" form="addEmployeeForm" class="btn btn-primary">Add Employee</button>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
<!-- Settings Modal -->
|
| 204 |
+
<div id="settingsModal" class="modal">
|
| 205 |
+
<div class="modal-content">
|
| 206 |
+
<div class="modal-header">
|
| 207 |
+
<h3><i class="fas fa-cog"></i> Settings</h3>
|
| 208 |
+
<button class="modal-close">×</button>
|
| 209 |
+
</div>
|
| 210 |
+
<div class="modal-body">
|
| 211 |
+
<div class="settings-section">
|
| 212 |
+
<h4>Change Admin Password</h4>
|
| 213 |
+
<form id="changePasswordForm">
|
| 214 |
+
<div class="form-group">
|
| 215 |
+
<label for="currentPassword">Current Password</label>
|
| 216 |
+
<input type="password" id="currentPassword" name="currentPassword" required>
|
| 217 |
+
</div>
|
| 218 |
+
<div class="form-group">
|
| 219 |
+
<label for="newPassword">New Password</label>
|
| 220 |
+
<input type="password" id="newPassword" name="newPassword" required>
|
| 221 |
+
</div>
|
| 222 |
+
<div class="form-group">
|
| 223 |
+
<label for="confirmPassword">Confirm New Password</label>
|
| 224 |
+
<input type="password" id="confirmPassword" name="confirmPassword" required>
|
| 225 |
+
</div>
|
| 226 |
+
<button type="submit" class="btn btn-primary">Change Password</button>
|
| 227 |
+
</form>
|
| 228 |
+
</div>
|
| 229 |
+
<div class="settings-section">
|
| 230 |
+
<h4>System Information</h4>
|
| 231 |
+
<div class="info-grid">
|
| 232 |
+
<div class="info-item">
|
| 233 |
+
<span class="info-label">Scan Interval:</span>
|
| 234 |
+
<span id="scanInterval" class="info-value">60 seconds</span>
|
| 235 |
+
</div>
|
| 236 |
+
<div class="info-item">
|
| 237 |
+
<span class="info-label">Office Timeout:</span>
|
| 238 |
+
<span id="officeTimeout" class="info-value">17:00</span>
|
| 239 |
+
</div>
|
| 240 |
+
<div class="info-item">
|
| 241 |
+
<span class="info-label">Current Time:</span>
|
| 242 |
+
<span id="currentTime" class="info-value">--:--:--</span>
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
<div class="modal-footer">
|
| 248 |
+
<button type="button" class="btn btn-secondary" onclick="closeModal('settingsModal')">Close</button>
|
| 249 |
+
</div>
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
|
| 253 |
+
<!-- Employee Details Modal -->
|
| 254 |
+
<div id="employeeDetailsModal" class="modal">
|
| 255 |
+
<div class="modal-content">
|
| 256 |
+
<div class="modal-header">
|
| 257 |
+
<h3><i class="fas fa-user"></i> Employee Details</h3>
|
| 258 |
+
<button class="modal-close">×</button>
|
| 259 |
+
</div>
|
| 260 |
+
<div class="modal-body">
|
| 261 |
+
<div id="employeeDetailsContent">
|
| 262 |
+
<!-- Employee details will be loaded here -->
|
| 263 |
+
</div>
|
| 264 |
+
</div>
|
| 265 |
+
<div class="modal-footer">
|
| 266 |
+
<button type="button" class="btn btn-danger" id="deleteEmployeeBtn">
|
| 267 |
+
<i class="fas fa-trash"></i> Delete Employee
|
| 268 |
+
</button>
|
| 269 |
+
<button type="button" class="btn btn-warning" id="modifyEmployeeBtn">
|
| 270 |
+
<i class="fas fa-edit"></i> Modify Employee
|
| 271 |
+
</button>
|
| 272 |
+
<button type="button" class="btn btn-secondary" onclick="closeModal('employeeDetailsModal')">Close</button>
|
| 273 |
+
</div>
|
| 274 |
+
</div>
|
| 275 |
+
</div>
|
| 276 |
+
|
| 277 |
+
<!-- Delete Employee Modal -->
|
| 278 |
+
<div id="deleteEmployeeModal" class="modal">
|
| 279 |
+
<div class="modal-content">
|
| 280 |
+
<div class="modal-header">
|
| 281 |
+
<h3><i class="fas fa-trash"></i> Delete Employee</h3>
|
| 282 |
+
<button class="modal-close">×</button>
|
| 283 |
+
</div>
|
| 284 |
+
<div class="modal-body">
|
| 285 |
+
<div class="warning-message">
|
| 286 |
+
<i class="fas fa-exclamation-triangle"></i>
|
| 287 |
+
<p>Are you sure you want to delete this employee? This action cannot be undone.</p>
|
| 288 |
+
<div id="deleteEmployeeInfo" class="employee-info">
|
| 289 |
+
<!-- Employee info will be loaded here -->
|
| 290 |
+
</div>
|
| 291 |
+
</div>
|
| 292 |
+
<form id="deleteEmployeeForm">
|
| 293 |
+
<div class="form-group">
|
| 294 |
+
<label for="deleteAdminPassword">Admin Password</label>
|
| 295 |
+
<input type="password" id="deleteAdminPassword" name="password" required placeholder="Enter admin password">
|
| 296 |
+
<small>Default password: 1122</small>
|
| 297 |
+
</div>
|
| 298 |
+
</form>
|
| 299 |
+
</div>
|
| 300 |
+
<div class="modal-footer">
|
| 301 |
+
<button type="button" class="btn btn-secondary" onclick="closeModal('deleteEmployeeModal')">Cancel</button>
|
| 302 |
+
<button type="submit" form="deleteEmployeeForm" class="btn btn-danger">
|
| 303 |
+
<i class="fas fa-trash"></i> Delete Employee
|
| 304 |
+
</button>
|
| 305 |
+
</div>
|
| 306 |
+
</div>
|
| 307 |
+
</div>
|
| 308 |
+
|
| 309 |
+
<!-- Modify Employee Modal -->
|
| 310 |
+
<div id="modifyEmployeeModal" class="modal">
|
| 311 |
+
<div class="modal-content">
|
| 312 |
+
<div class="modal-header">
|
| 313 |
+
<h3><i class="fas fa-edit"></i> Modify Employee Information</h3>
|
| 314 |
+
<button class="modal-close">×</button>
|
| 315 |
+
</div>
|
| 316 |
+
<div class="modal-body">
|
| 317 |
+
<div class="info-message" style="background: #e3f2fd; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #2196f3;">
|
| 318 |
+
<i class="fas fa-info-circle" style="color: #2196f3; margin-right: 8px;"></i>
|
| 319 |
+
<strong>You can modify:</strong> Employee name, MAC address, or picture path. All fields are editable.
|
| 320 |
+
</div>
|
| 321 |
+
<form id="modifyEmployeeForm">
|
| 322 |
+
<div class="form-group">
|
| 323 |
+
<label for="modifyEmployeeName">
|
| 324 |
+
<i class="fas fa-user"></i> Employee Name
|
| 325 |
+
<small style="font-weight: normal; color: #666;">(Change the employee's display name)</small>
|
| 326 |
+
</label>
|
| 327 |
+
<input type="text" id="modifyEmployeeName" name="name" required placeholder="Enter new employee name">
|
| 328 |
+
</div>
|
| 329 |
+
<div class="form-group">
|
| 330 |
+
<label for="modifyEmployeeMac">
|
| 331 |
+
<i class="fas fa-network-wired"></i> MAC Address
|
| 332 |
+
<small style="font-weight: normal; color: #666;">(Update the device MAC address for tracking)</small>
|
| 333 |
+
</label>
|
| 334 |
+
<input type="text" id="modifyEmployeeMac" name="mac_address" required placeholder="aa-bb-cc-dd-ee-ff" pattern="[a-fA-F0-9]{2}-[a-fA-F0-9]{2}-[a-fA-F0-9]{2}-[a-fA-F0-9]{2}-[a-fA-F0-9]{2}-[a-fA-F0-9]{2}">
|
| 335 |
+
<small>Format: aa-bb-cc-dd-ee-ff (lowercase with dashes)</small>
|
| 336 |
+
</div>
|
| 337 |
+
<div class="form-group">
|
| 338 |
+
<label for="modifyEmployeePicture">
|
| 339 |
+
<i class="fas fa-image"></i> Picture Path (Optional)
|
| 340 |
+
<small style="font-weight: normal; color: #666;">(Update employee photo path)</small>
|
| 341 |
+
</label>
|
| 342 |
+
<input type="text" id="modifyEmployeePicture" name="picture_path" placeholder="static/img/employee.jpg">
|
| 343 |
+
<small>Path to employee picture file</small>
|
| 344 |
+
</div>
|
| 345 |
+
<div class="form-group">
|
| 346 |
+
<label for="modifyAdminPassword">
|
| 347 |
+
<i class="fas fa-lock"></i> Admin Password
|
| 348 |
+
<small style="font-weight: normal; color: #666;">(Required for security verification)</small>
|
| 349 |
+
</label>
|
| 350 |
+
<input type="password" id="modifyAdminPassword" name="password" required placeholder="Enter admin password">
|
| 351 |
+
<small>Default password: 1122</small>
|
| 352 |
+
</div>
|
| 353 |
+
</form>
|
| 354 |
+
</div>
|
| 355 |
+
<div class="modal-footer">
|
| 356 |
+
<button type="button" class="btn btn-secondary" onclick="closeModal('modifyEmployeeModal')">Cancel</button>
|
| 357 |
+
<button type="submit" form="modifyEmployeeForm" class="btn btn-primary">
|
| 358 |
+
<i class="fas fa-save"></i> Save Changes
|
| 359 |
+
</button>
|
| 360 |
+
</div>
|
| 361 |
+
</div>
|
| 362 |
+
</div>
|
| 363 |
+
|
| 364 |
+
<!-- Notification Container -->
|
| 365 |
+
<div id="notifications" class="notifications"></div>
|
| 366 |
+
|
| 367 |
+
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
| 368 |
+
</body>
|
| 369 |
+
</html>
|
| 370 |
+
|
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>WiFi Attendance Tracker - Login</title>
|
| 7 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
* {
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
box-sizing: border-box;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
body {
|
| 16 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 17 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 18 |
+
min-height: 100vh;
|
| 19 |
+
display: flex;
|
| 20 |
+
align-items: center;
|
| 21 |
+
justify-content: center;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.login-container {
|
| 25 |
+
background: white;
|
| 26 |
+
padding: 2rem;
|
| 27 |
+
border-radius: 15px;
|
| 28 |
+
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
| 29 |
+
width: 100%;
|
| 30 |
+
max-width: 400px;
|
| 31 |
+
text-align: center;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.login-header {
|
| 35 |
+
margin-bottom: 2rem;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.login-header i {
|
| 39 |
+
font-size: 3rem;
|
| 40 |
+
color: #667eea;
|
| 41 |
+
margin-bottom: 1rem;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.login-header h1 {
|
| 45 |
+
color: #333;
|
| 46 |
+
font-size: 1.8rem;
|
| 47 |
+
margin-bottom: 0.5rem;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.login-header p {
|
| 51 |
+
color: #666;
|
| 52 |
+
font-size: 0.9rem;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.form-group {
|
| 56 |
+
margin-bottom: 1.5rem;
|
| 57 |
+
text-align: left;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.form-group label {
|
| 61 |
+
display: block;
|
| 62 |
+
margin-bottom: 0.5rem;
|
| 63 |
+
color: #333;
|
| 64 |
+
font-weight: 500;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.password-input-container {
|
| 68 |
+
position: relative;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.form-group input {
|
| 72 |
+
width: 100%;
|
| 73 |
+
padding: 0.75rem;
|
| 74 |
+
border: 2px solid #e1e5e9;
|
| 75 |
+
border-radius: 8px;
|
| 76 |
+
font-size: 1rem;
|
| 77 |
+
transition: border-color 0.3s ease;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.form-group input:focus {
|
| 81 |
+
outline: none;
|
| 82 |
+
border-color: #667eea;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.toggle-password {
|
| 86 |
+
position: absolute;
|
| 87 |
+
right: 10px;
|
| 88 |
+
top: 50%;
|
| 89 |
+
transform: translateY(-50%);
|
| 90 |
+
background: none;
|
| 91 |
+
border: none;
|
| 92 |
+
color: #666;
|
| 93 |
+
cursor: pointer;
|
| 94 |
+
font-size: 1rem;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.login-btn {
|
| 98 |
+
width: 100%;
|
| 99 |
+
padding: 0.75rem;
|
| 100 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 101 |
+
color: white;
|
| 102 |
+
border: none;
|
| 103 |
+
border-radius: 8px;
|
| 104 |
+
font-size: 1rem;
|
| 105 |
+
font-weight: 600;
|
| 106 |
+
cursor: pointer;
|
| 107 |
+
transition: transform 0.2s ease;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.login-btn:hover {
|
| 111 |
+
transform: translateY(-2px);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.login-btn:disabled {
|
| 115 |
+
opacity: 0.6;
|
| 116 |
+
cursor: not-allowed;
|
| 117 |
+
transform: none;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.error-message {
|
| 121 |
+
background: #fee;
|
| 122 |
+
color: #c33;
|
| 123 |
+
padding: 0.75rem;
|
| 124 |
+
border-radius: 8px;
|
| 125 |
+
margin-bottom: 1rem;
|
| 126 |
+
border-left: 4px solid #c33;
|
| 127 |
+
display: none;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.loading {
|
| 131 |
+
display: none;
|
| 132 |
+
margin-top: 1rem;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.loading i {
|
| 136 |
+
animation: spin 1s linear infinite;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
@keyframes spin {
|
| 140 |
+
0% { transform: rotate(0deg); }
|
| 141 |
+
100% { transform: rotate(360deg); }
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.footer {
|
| 145 |
+
margin-top: 2rem;
|
| 146 |
+
padding-top: 1rem;
|
| 147 |
+
border-top: 1px solid #eee;
|
| 148 |
+
color: #666;
|
| 149 |
+
font-size: 0.8rem;
|
| 150 |
+
}
|
| 151 |
+
</style>
|
| 152 |
+
</head>
|
| 153 |
+
<body>
|
| 154 |
+
<div class="login-container">
|
| 155 |
+
<div class="login-header">
|
| 156 |
+
<i class="fas fa-wifi"></i>
|
| 157 |
+
<h1>WiFi Attendance Tracker</h1>
|
| 158 |
+
<p>Please enter your password to access the dashboard</p>
|
| 159 |
+
</div>
|
| 160 |
+
|
| 161 |
+
<div id="errorMessage" class="error-message"></div>
|
| 162 |
+
|
| 163 |
+
<form id="loginForm">
|
| 164 |
+
<div class="form-group">
|
| 165 |
+
<label for="password">Dashboard Password</label>
|
| 166 |
+
<div class="password-input-container">
|
| 167 |
+
<input type="password" id="password" name="password" required placeholder="Enter dashboard password" autocomplete="current-password">
|
| 168 |
+
<button type="button" class="toggle-password" onclick="togglePassword()">
|
| 169 |
+
<i class="fas fa-eye" id="toggleIcon"></i>
|
| 170 |
+
</button>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
<button type="submit" class="login-btn" id="loginBtn">
|
| 175 |
+
<i class="fas fa-sign-in-alt"></i> Access Dashboard
|
| 176 |
+
</button>
|
| 177 |
+
</form>
|
| 178 |
+
|
| 179 |
+
<div class="loading" id="loading">
|
| 180 |
+
<i class="fas fa-spinner"></i> Authenticating...
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
<div class="footer">
|
| 184 |
+
<p>Default password: <strong>admin123</strong></p>
|
| 185 |
+
<p>Advanced Employee Management System</p>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<script>
|
| 190 |
+
function togglePassword() {
|
| 191 |
+
const passwordInput = document.getElementById('password');
|
| 192 |
+
const toggleIcon = document.getElementById('toggleIcon');
|
| 193 |
+
|
| 194 |
+
if (passwordInput.type === 'password') {
|
| 195 |
+
passwordInput.type = 'text';
|
| 196 |
+
toggleIcon.className = 'fas fa-eye-slash';
|
| 197 |
+
} else {
|
| 198 |
+
passwordInput.type = 'password';
|
| 199 |
+
toggleIcon.className = 'fas fa-eye';
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
function showError(message) {
|
| 204 |
+
const errorDiv = document.getElementById('errorMessage');
|
| 205 |
+
errorDiv.textContent = message;
|
| 206 |
+
errorDiv.style.display = 'block';
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
function hideError() {
|
| 210 |
+
const errorDiv = document.getElementById('errorMessage');
|
| 211 |
+
errorDiv.style.display = 'none';
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
function setLoading(loading) {
|
| 215 |
+
const loginBtn = document.getElementById('loginBtn');
|
| 216 |
+
const loadingDiv = document.getElementById('loading');
|
| 217 |
+
const form = document.getElementById('loginForm');
|
| 218 |
+
|
| 219 |
+
if (loading) {
|
| 220 |
+
loginBtn.disabled = true;
|
| 221 |
+
loadingDiv.style.display = 'block';
|
| 222 |
+
form.style.opacity = '0.6';
|
| 223 |
+
} else {
|
| 224 |
+
loginBtn.disabled = false;
|
| 225 |
+
loadingDiv.style.display = 'none';
|
| 226 |
+
form.style.opacity = '1';
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
document.getElementById('loginForm').addEventListener('submit', async function(e) {
|
| 231 |
+
e.preventDefault();
|
| 232 |
+
|
| 233 |
+
const password = document.getElementById('password').value;
|
| 234 |
+
|
| 235 |
+
if (!password) {
|
| 236 |
+
showError('Please enter a password');
|
| 237 |
+
return;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
hideError();
|
| 241 |
+
setLoading(true);
|
| 242 |
+
|
| 243 |
+
try {
|
| 244 |
+
const response = await fetch('/api/login', {
|
| 245 |
+
method: 'POST',
|
| 246 |
+
headers: {
|
| 247 |
+
'Content-Type': 'application/json',
|
| 248 |
+
},
|
| 249 |
+
body: JSON.stringify({ password: password })
|
| 250 |
+
});
|
| 251 |
+
|
| 252 |
+
const result = await response.json();
|
| 253 |
+
|
| 254 |
+
if (result.success) {
|
| 255 |
+
// Redirect to dashboard
|
| 256 |
+
window.location.href = '/dashboard';
|
| 257 |
+
} else {
|
| 258 |
+
showError(result.message || 'Invalid password');
|
| 259 |
+
}
|
| 260 |
+
} catch (error) {
|
| 261 |
+
console.error('Login error:', error);
|
| 262 |
+
showError('Connection error. Please try again.');
|
| 263 |
+
} finally {
|
| 264 |
+
setLoading(false);
|
| 265 |
+
}
|
| 266 |
+
});
|
| 267 |
+
|
| 268 |
+
// Focus on password input when page loads
|
| 269 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 270 |
+
document.getElementById('password').focus();
|
| 271 |
+
});
|
| 272 |
+
|
| 273 |
+
// Handle Enter key
|
| 274 |
+
document.getElementById('password').addEventListener('keypress', function(e) {
|
| 275 |
+
if (e.key === 'Enter') {
|
| 276 |
+
document.getElementById('loginForm').dispatchEvent(new Event('submit'));
|
| 277 |
+
}
|
| 278 |
+
});
|
| 279 |
+
</script>
|
| 280 |
+
</body>
|
| 281 |
+
</html>
|
| 282 |
+
|
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, jsonify, request
|
| 2 |
+
from flask_cors import CORS
|
| 3 |
+
import json
|
| 4 |
+
import threading
|
| 5 |
+
import time
|
| 6 |
+
from datetime import datetime, date, timedelta
|
| 7 |
+
from attendance_tracker import AttendanceTracker
|
| 8 |
+
from database import AttendanceDatabase
|
| 9 |
+
|
| 10 |
+
app = Flask(__name__)
|
| 11 |
+
CORS(app) # Enable CORS for all routes
|
| 12 |
+
|
| 13 |
+
# Global variables
|
| 14 |
+
tracker = None
|
| 15 |
+
db = None
|
| 16 |
+
latest_events = []
|
| 17 |
+
is_monitoring = False
|
| 18 |
+
|
| 19 |
+
def initialize_system():
|
| 20 |
+
"""Initialize the attendance tracking system."""
|
| 21 |
+
global tracker, db
|
| 22 |
+
tracker = AttendanceTracker()
|
| 23 |
+
db = AttendanceDatabase()
|
| 24 |
+
|
| 25 |
+
# Sync employees from config to database
|
| 26 |
+
db.sync_employees_from_config()
|
| 27 |
+
|
| 28 |
+
def monitoring_loop():
|
| 29 |
+
"""Background monitoring loop."""
|
| 30 |
+
global latest_events, is_monitoring
|
| 31 |
+
|
| 32 |
+
while is_monitoring:
|
| 33 |
+
try:
|
| 34 |
+
events = tracker.scan_once()
|
| 35 |
+
|
| 36 |
+
# Keep only the latest 50 events for the web interface
|
| 37 |
+
latest_events.extend(events)
|
| 38 |
+
latest_events = latest_events[-50:] # Keep only last 50 events
|
| 39 |
+
|
| 40 |
+
time.sleep(tracker.scan_interval)
|
| 41 |
+
except Exception as e:
|
| 42 |
+
print(f"Error in monitoring loop: {e}")
|
| 43 |
+
time.sleep(5) # Wait before retrying
|
| 44 |
+
|
| 45 |
+
@app.route('/')
|
| 46 |
+
def index():
|
| 47 |
+
"""Serve the main dashboard page."""
|
| 48 |
+
return render_template('index.html')
|
| 49 |
+
|
| 50 |
+
@app.route('/api/status')
|
| 51 |
+
def get_status():
|
| 52 |
+
"""Get current system status."""
|
| 53 |
+
return jsonify({
|
| 54 |
+
'is_monitoring': is_monitoring,
|
| 55 |
+
'employee_count': len(tracker.employees) if tracker else 0,
|
| 56 |
+
'scan_interval': tracker.scan_interval if tracker else 60,
|
| 57 |
+
'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 58 |
+
'office_timeout': f"{tracker.office_timeout_hour:02d}:{tracker.office_timeout_minute:02d}" if tracker else "17:00"
|
| 59 |
+
})
|
| 60 |
+
|
| 61 |
+
@app.route('/api/employees')
|
| 62 |
+
def get_employees():
|
| 63 |
+
"""Get all employees and their current status."""
|
| 64 |
+
if not tracker:
|
| 65 |
+
return jsonify([])
|
| 66 |
+
|
| 67 |
+
status = tracker.get_current_status()
|
| 68 |
+
employees = []
|
| 69 |
+
|
| 70 |
+
for mac, info in status.items():
|
| 71 |
+
employees.append({
|
| 72 |
+
'name': info['name'],
|
| 73 |
+
'mac': info['mac'],
|
| 74 |
+
'is_present': info['is_present'],
|
| 75 |
+
'status': info['status'],
|
| 76 |
+
'last_seen': info['last_seen'],
|
| 77 |
+
'time_in': info['time_in']
|
| 78 |
+
})
|
| 79 |
+
|
| 80 |
+
return jsonify(employees)
|
| 81 |
+
|
| 82 |
+
@app.route('/api/events')
|
| 83 |
+
def get_recent_events():
|
| 84 |
+
"""Get recent attendance events."""
|
| 85 |
+
global latest_events
|
| 86 |
+
|
| 87 |
+
# Convert events to JSON-serializable format
|
| 88 |
+
events_data = []
|
| 89 |
+
for mac, event_type, timestamp in latest_events:
|
| 90 |
+
employee_name = tracker.get_employee_name(mac) if tracker else f"Unknown ({mac})"
|
| 91 |
+
events_data.append({
|
| 92 |
+
'name': employee_name,
|
| 93 |
+
'mac': mac,
|
| 94 |
+
'event_type': event_type,
|
| 95 |
+
'timestamp': timestamp.strftime('%Y-%m-%d %H:%M:%S'),
|
| 96 |
+
'time_ago': get_time_ago(timestamp)
|
| 97 |
+
})
|
| 98 |
+
|
| 99 |
+
# Sort by timestamp (most recent first)
|
| 100 |
+
events_data.sort(key=lambda x: x['timestamp'], reverse=True)
|
| 101 |
+
|
| 102 |
+
return jsonify(events_data)
|
| 103 |
+
|
| 104 |
+
@app.route('/api/attendance_events')
|
| 105 |
+
def get_attendance_events():
|
| 106 |
+
"""Get attendance events from database."""
|
| 107 |
+
if not db:
|
| 108 |
+
return jsonify([])
|
| 109 |
+
|
| 110 |
+
date_filter = request.args.get('date')
|
| 111 |
+
limit = int(request.args.get('limit', 50))
|
| 112 |
+
|
| 113 |
+
events = db.get_attendance_events(date=date_filter, limit=limit)
|
| 114 |
+
|
| 115 |
+
# Add time_ago field
|
| 116 |
+
for event in events:
|
| 117 |
+
timestamp = datetime.fromisoformat(event['timestamp'])
|
| 118 |
+
event['time_ago'] = get_time_ago(timestamp)
|
| 119 |
+
|
| 120 |
+
return jsonify(events)
|
| 121 |
+
|
| 122 |
+
@app.route('/api/daily_summary')
|
| 123 |
+
def get_daily_summary():
|
| 124 |
+
"""Get daily attendance summary."""
|
| 125 |
+
if not db:
|
| 126 |
+
return jsonify([])
|
| 127 |
+
|
| 128 |
+
date_str = request.args.get('date', date.today().strftime('%Y-%m-%d'))
|
| 129 |
+
summary = db.get_daily_summary(date_str)
|
| 130 |
+
|
| 131 |
+
# Format durations for display
|
| 132 |
+
for employee in summary:
|
| 133 |
+
employee['total_break_formatted'] = format_duration(employee['total_break_duration'])
|
| 134 |
+
employee['total_work_formatted'] = format_duration(employee['total_work_duration'])
|
| 135 |
+
|
| 136 |
+
return jsonify(summary)
|
| 137 |
+
|
| 138 |
+
@app.route('/api/summary_stats')
|
| 139 |
+
def get_summary_stats():
|
| 140 |
+
"""Get summary statistics for the dashboard."""
|
| 141 |
+
if not db:
|
| 142 |
+
return jsonify({})
|
| 143 |
+
|
| 144 |
+
date_str = request.args.get('date', date.today().strftime('%Y-%m-%d'))
|
| 145 |
+
summary = db.get_daily_summary(date_str)
|
| 146 |
+
|
| 147 |
+
stats = {
|
| 148 |
+
'total_employees': len(summary),
|
| 149 |
+
'present_count': len([emp for emp in summary if emp['status'] == 'Present']),
|
| 150 |
+
'absent_count': len([emp for emp in summary if emp['status'] == 'Absent']),
|
| 151 |
+
'on_break_count': len([emp for emp in summary if emp['status'] == 'On Break']),
|
| 152 |
+
'timed_out_count': len([emp for emp in summary if emp['status'] == 'Timed Out']),
|
| 153 |
+
'total_events': len(db.get_attendance_events(date=date_str, limit=1000))
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
return jsonify(stats)
|
| 157 |
+
|
| 158 |
+
@app.route('/api/start_monitoring', methods=['POST'])
|
| 159 |
+
def start_monitoring():
|
| 160 |
+
"""Start the attendance monitoring."""
|
| 161 |
+
global is_monitoring
|
| 162 |
+
|
| 163 |
+
if not is_monitoring:
|
| 164 |
+
is_monitoring = True
|
| 165 |
+
monitoring_thread = threading.Thread(target=monitoring_loop, daemon=True)
|
| 166 |
+
monitoring_thread.start()
|
| 167 |
+
return jsonify({'success': True, 'message': 'Monitoring started'})
|
| 168 |
+
else:
|
| 169 |
+
return jsonify({'success': False, 'message': 'Monitoring already running'})
|
| 170 |
+
|
| 171 |
+
@app.route('/api/stop_monitoring', methods=['POST'])
|
| 172 |
+
def stop_monitoring():
|
| 173 |
+
"""Stop the attendance monitoring."""
|
| 174 |
+
global is_monitoring
|
| 175 |
+
is_monitoring = False
|
| 176 |
+
return jsonify({'success': True, 'message': 'Monitoring stopped'})
|
| 177 |
+
|
| 178 |
+
@app.route('/api/export_csv')
|
| 179 |
+
def export_csv():
|
| 180 |
+
"""Export daily summary to CSV."""
|
| 181 |
+
if not db:
|
| 182 |
+
return jsonify({'success': False, 'message': 'Database not available'})
|
| 183 |
+
|
| 184 |
+
date_str = request.args.get('date', date.today().strftime('%Y-%m-%d'))
|
| 185 |
+
|
| 186 |
+
try:
|
| 187 |
+
db.export_daily_summary_to_csv(date_str)
|
| 188 |
+
return jsonify({'success': True, 'message': f'CSV exported for {date_str}'})
|
| 189 |
+
except Exception as e:
|
| 190 |
+
return jsonify({'success': False, 'message': f'Export failed: {str(e)}'})
|
| 191 |
+
|
| 192 |
+
def get_time_ago(timestamp):
|
| 193 |
+
"""Get human-readable time difference."""
|
| 194 |
+
now = datetime.now()
|
| 195 |
+
diff = now - timestamp
|
| 196 |
+
|
| 197 |
+
if diff.days > 0:
|
| 198 |
+
return f"{diff.days} day{'s' if diff.days > 1 else ''} ago"
|
| 199 |
+
elif diff.seconds > 3600:
|
| 200 |
+
hours = diff.seconds // 3600
|
| 201 |
+
return f"{hours} hour{'s' if hours > 1 else ''} ago"
|
| 202 |
+
elif diff.seconds > 60:
|
| 203 |
+
minutes = diff.seconds // 60
|
| 204 |
+
return f"{minutes} minute{'s' if minutes > 1 else ''} ago"
|
| 205 |
+
else:
|
| 206 |
+
return "Just now"
|
| 207 |
+
|
| 208 |
+
def format_duration(seconds):
|
| 209 |
+
"""Format duration in seconds to HH:MM:SS."""
|
| 210 |
+
if seconds is None:
|
| 211 |
+
return "00:00:00"
|
| 212 |
+
|
| 213 |
+
hours = seconds // 3600
|
| 214 |
+
minutes = (seconds % 3600) // 60
|
| 215 |
+
seconds = seconds % 60
|
| 216 |
+
|
| 217 |
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
| 218 |
+
|
| 219 |
+
if __name__ == '__main__':
|
| 220 |
+
# Initialize the system
|
| 221 |
+
initialize_system()
|
| 222 |
+
|
| 223 |
+
# Get port from config
|
| 224 |
+
port = tracker.config.get('web_port', 5000) if tracker else 5000
|
| 225 |
+
|
| 226 |
+
print(f"Starting WiFi Attendance Tracker Web Interface on port {port}")
|
| 227 |
+
print(f"Open your browser and go to: http://localhost:{port}")
|
| 228 |
+
|
| 229 |
+
# Start monitoring automatically
|
| 230 |
+
is_monitoring = True
|
| 231 |
+
monitoring_thread = threading.Thread(target=monitoring_loop, daemon=True)
|
| 232 |
+
monitoring_thread.start()
|
| 233 |
+
|
| 234 |
+
# Start Flask app
|
| 235 |
+
app.run(host='0.0.0.0', port=port, debug=False)
|
| 236 |
+
|