| from fastapi import APIRouter, Depends, HTTPException, status |
| from fastapi.responses import StreamingResponse |
| from sqlalchemy.orm import Session |
| from sqlalchemy import select, func, and_, desc |
| from datetime import date, datetime, timedelta |
| from typing import Dict, Any, List |
| import asyncio |
| import json |
|
|
| from app.core.database import get_db |
| from app.core import security |
| from app.core.security import RoleCheckerSSE |
| from app.core import event_bus |
| from app.crud import crud |
| from app.schemas import schemas |
| from app.models import models |
|
|
| router = APIRouter() |
|
|
| checker_view = security.RoleChecker(["Super Admin", "Admin", "HR"]) |
|
|
| @router.get("/dashboard-summary") |
| def get_dashboard_summary( |
| db: Session = Depends(get_db), |
| current_user: models.User = Depends(checker_view) |
| ): |
| today = date.today() |
| |
| |
| total_employees = db.query(func.count(models.Employee.id)).filter(models.Employee.status == "Active").scalar() or 0 |
| |
| |
| present_today = db.query(func.count(models.Attendance.id)).filter( |
| and_( |
| models.Attendance.date == today, |
| models.Attendance.status.in_(["Present", "Late", "Half Day"]) |
| ) |
| ).scalar() or 0 |
| |
| |
| late_today = db.query(func.count(models.Attendance.id)).filter( |
| and_( |
| models.Attendance.date == today, |
| models.Attendance.status == "Late" |
| ) |
| ).scalar() or 0 |
| |
| |
| checked_out_today = db.query(func.count(models.Attendance.id)).filter( |
| and_( |
| models.Attendance.date == today, |
| models.Attendance.check_out.isnot(None) |
| ) |
| ).scalar() or 0 |
| |
| |
| |
| absent_today = max(0, total_employees - present_today) |
| |
| |
| attendance_percentage = (present_today / total_employees * 100) if total_employees > 0 else 0.0 |
| |
| return { |
| "total_employees": total_employees, |
| "present_today": present_today, |
| "absent_today": absent_today, |
| "late_today": late_today, |
| "checked_out_today": checked_out_today, |
| "attendance_percentage": round(attendance_percentage, 1) |
| } |
|
|
| @router.get("/attendance-trends") |
| def get_attendance_trends( |
| days: int = 7, |
| db: Session = Depends(get_db), |
| current_user: models.User = Depends(checker_view) |
| ): |
| """ |
| Returns daily stats for the last N days (date, present, late, absent) for ECharts. |
| """ |
| end_date = date.today() |
| start_date = end_date - timedelta(days=days - 1) |
| |
| |
| date_list = [start_date + timedelta(days=i) for i in range(days)] |
| |
| |
| total_active = db.query(func.count(models.Employee.id)).filter(models.Employee.status == "Active").scalar() or 0 |
| |
| trends = [] |
| for d in date_list: |
| present = db.query(func.count(models.Attendance.id)).filter( |
| and_( |
| models.Attendance.date == d, |
| models.Attendance.status.in_(["Present", "Late", "Half Day"]) |
| ) |
| ).scalar() or 0 |
| |
| late = db.query(func.count(models.Attendance.id)).filter( |
| and_( |
| models.Attendance.date == d, |
| models.Attendance.status == "Late" |
| ) |
| ).scalar() or 0 |
| |
| absent = max(0, total_active - present) |
| |
| trends.append({ |
| "date": d.isoformat(), |
| "present": present, |
| "late": late, |
| "absent": absent |
| }) |
| |
| return trends |
|
|
| @router.get("/department-distribution") |
| def get_department_distribution( |
| db: Session = Depends(get_db), |
| current_user: models.User = Depends(checker_view) |
| ): |
| """ |
| Returns employee and attendance counts by department for ECharts. |
| """ |
| today = date.today() |
| departments = db.query(models.Department).all() |
| |
| dist = [] |
| for dept in departments: |
| |
| total_in_dept = db.query(func.count(models.Employee.id)).filter( |
| and_( |
| models.Employee.department_id == dept.id, |
| models.Employee.status == "Active" |
| ) |
| ).scalar() or 0 |
| |
| |
| present_in_dept = db.query(func.count(models.Attendance.id)).join(models.Employee).filter( |
| and_( |
| models.Employee.department_id == dept.id, |
| models.Attendance.date == today, |
| models.Attendance.status.in_(["Present", "Late", "Half Day"]) |
| ) |
| ).scalar() or 0 |
| |
| dist.append({ |
| "id": dept.id, |
| "department": dept.name, |
| "code": dept.code, |
| "total_employees": total_in_dept, |
| "present_today": present_in_dept |
| }) |
| |
| return dist |
|
|
| @router.get("/recent-activity", response_model=List[schemas.AttendanceLogOut]) |
| def get_recent_activity( |
| limit: int = 10, |
| db: Session = Depends(get_db), |
| current_user: models.User = Depends(checker_view) |
| ): |
| """ |
| Returns latest kiosk scan logs for the dashboard tickfeed. |
| """ |
| cutoff = datetime.utcnow() - timedelta(hours=24) |
| return db.query(models.AttendanceLog).filter(models.AttendanceLog.timestamp >= cutoff).order_by(desc(models.AttendanceLog.timestamp)).limit(limit).all() |
|
|
|
|
| @router.get("/live-stream") |
| async def live_stream( |
| current_user: models.User = Depends(RoleCheckerSSE(["Super Admin", "Admin", "HR"])) |
| ): |
| """ |
| Real-time Server-Sent Events (SSE) feed of kiosk scans. |
| """ |
| async def event_generator(): |
| queue = event_bus.subscribe() |
| try: |
| while True: |
| |
| event_data = await queue.get() |
| |
| yield f"data: {json.dumps(event_data)}\n\n" |
| except asyncio.CancelledError: |
| |
| pass |
| finally: |
| event_bus.unsubscribe(queue) |
|
|
| return StreamingResponse(event_generator(), media_type="text/event-stream") |
|
|