malik-AI commited on
Commit
bf8ba08
Β·
verified Β·
1 Parent(s): 67ba9e4

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 CHANGED
@@ -1,3 +1,416 @@
1
- ---
2
- license: llama2
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
+
__pycache__/attendance_tracker.cpython-311.pyc ADDED
Binary file (20.3 kB). View file
 
__pycache__/attendance_tracker.cpython-313.pyc ADDED
Binary file (18.6 kB). View file
 
__pycache__/auth.cpython-311.pyc ADDED
Binary file (5.22 kB). View file
 
__pycache__/auth.cpython-313.pyc ADDED
Binary file (4.76 kB). View file
 
__pycache__/database.cpython-311.pyc ADDED
Binary file (33.2 kB). View file
 
__pycache__/database.cpython-313.pyc ADDED
Binary file (28.4 kB). View file
 
attendance.db ADDED
Binary file (53.2 kB). View file
 
attendance_tracker.py ADDED
@@ -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
+
auth.py ADDED
@@ -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
+
config.json ADDED
@@ -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
+
database.py ADDED
@@ -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
+
employees.json ADDED
@@ -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
+ ]
logs/attendance_summary_2025-07-28.csv ADDED
@@ -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
main.py ADDED
@@ -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
+
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ flask>=2.0.0
2
+ flask-cors>=3.0.0
3
+ bcrypt>=4.0.0
4
+
run.bat ADDED
@@ -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
+
setup.bat ADDED
@@ -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
+
static/css/style.css ADDED
@@ -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
+
static/js/script.js ADDED
@@ -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
+
templates/index.html ADDED
@@ -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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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
+
templates/login.html ADDED
@@ -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
+
web_interface.py ADDED
@@ -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
+