Tahasaif3 commited on
Commit
a3bb5d4
·
1 Parent(s): c71cf2c
src/main.py CHANGED
@@ -2,7 +2,7 @@ from fastapi import FastAPI
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from .config import settings
4
 
5
- from .routers import auth, tasks, projects, chat, audit
6
  from .utils.health_check import kafka_health_checker
7
 
8
  app = FastAPI(
@@ -17,6 +17,7 @@ app.include_router(tasks.router)
17
  app.include_router(projects.router)
18
  app.include_router(chat.router)
19
  app.include_router(audit.router)
 
20
 
21
  # Prepare allowed origins from settings.FRONTEND_URL (comma separated)
22
  _frontend_origins = [o.strip() for o in settings.FRONTEND_URL.split(",")] if settings.FRONTEND_URL else []
 
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from .config import settings
4
 
5
+ from .routers import auth, tasks, projects, chat, audit, stats
6
  from .utils.health_check import kafka_health_checker
7
 
8
  app = FastAPI(
 
17
  app.include_router(projects.router)
18
  app.include_router(chat.router)
19
  app.include_router(audit.router)
20
+ app.include_router(stats.router)
21
 
22
  # Prepare allowed origins from settings.FRONTEND_URL (comma separated)
23
  _frontend_origins = [o.strip() for o in settings.FRONTEND_URL.split(",")] if settings.FRONTEND_URL else []
src/routers/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- from . import auth, tasks, projects, chat, audit
2
 
3
- __all__ = ["auth", "tasks", "projects", "chat", "audit"]
 
1
+ from . import auth, tasks, projects, chat, audit, stats
2
 
3
+ __all__ = ["auth", "tasks", "projects", "chat", "audit", "stats"]
src/routers/audit.py CHANGED
@@ -1,8 +1,10 @@
1
  from fastapi import APIRouter, HTTPException, Depends, status, Body
2
  from sqlmodel import Session, select
3
- from typing import List, Dict, Any
4
  from uuid import UUID
5
  import logging
 
 
6
 
7
  from ..models.audit_log import AuditLog, AuditLogCreate
8
  from ..models.user import User
@@ -76,6 +78,95 @@ async def receive_audit_event(
76
  return {"status": "error", "message": str(e)}
77
 
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  @router.get("/events/{user_id}", response_model=dict)
80
  async def get_user_audit_events(
81
  user_id: UUID,
 
1
  from fastapi import APIRouter, HTTPException, Depends, status, Body
2
  from sqlmodel import Session, select
3
+ from typing import List, Dict, Any, Optional
4
  from uuid import UUID
5
  import logging
6
+ from datetime import datetime
7
+ import uuid
8
 
9
  from ..models.audit_log import AuditLog, AuditLogCreate
10
  from ..models.user import User
 
78
  return {"status": "error", "message": str(e)}
79
 
80
 
81
+ @router.post("/events/{user_id}")
82
+ async def create_audit_event(
83
+ user_id: str,
84
+ data: Dict[str, Any] = Body(...),
85
+ current_user: User = Depends(get_current_user),
86
+ session: Session = Depends(get_session_dep)
87
+ ):
88
+ """
89
+ Frontend API endpoint to create audit events for task operations.
90
+ Called by the frontend when tasks are created, updated, completed, or deleted.
91
+
92
+ Expected request body:
93
+ {
94
+ "event_type": "created|updated|completed|deleted",
95
+ "task_id": 123,
96
+ "event_data": {
97
+ "title": "task title",
98
+ "description": "optional description",
99
+ "completed": false
100
+ }
101
+ }
102
+ """
103
+ try:
104
+ # Verify that the user is creating audit events for themselves
105
+ if str(current_user.id) != user_id:
106
+ raise HTTPException(
107
+ status_code=status.HTTP_403_FORBIDDEN,
108
+ detail="Unauthorized"
109
+ )
110
+
111
+ # Extract fields from request body
112
+ event_type = data.get("event_type")
113
+ task_id = data.get("task_id")
114
+ event_data = data.get("event_data", {})
115
+
116
+ # Validate required fields
117
+ if not event_type or not task_id:
118
+ raise HTTPException(
119
+ status_code=status.HTTP_400_BAD_REQUEST,
120
+ detail="Missing required fields: event_type, task_id"
121
+ )
122
+
123
+ # Validate event type
124
+ valid_event_types = ['created', 'updated', 'completed', 'deleted']
125
+ if event_type not in valid_event_types:
126
+ raise HTTPException(
127
+ status_code=status.HTTP_400_BAD_REQUEST,
128
+ detail=f"Invalid event type. Must be one of: {valid_event_types}"
129
+ )
130
+
131
+ # Generate unique event ID
132
+ event_id = str(uuid.uuid4())
133
+
134
+ # Create audit log entry
135
+ audit_log = AuditLog(
136
+ event_id=event_id,
137
+ event_type=event_type,
138
+ user_id=user_id,
139
+ task_id=task_id,
140
+ event_data={
141
+ "title": event_data.get("title", ""),
142
+ "description": event_data.get("description", ""),
143
+ "completed": event_data.get("completed", False)
144
+ }
145
+ )
146
+
147
+ session.add(audit_log)
148
+ session.commit()
149
+ session.refresh(audit_log)
150
+
151
+ logger.info(f"Audit event {event_id} created successfully for user {user_id}")
152
+ return {
153
+ "status": "success",
154
+ "message": "Audit event created",
155
+ "id": audit_log.id,
156
+ "event_id": audit_log.event_id
157
+ }
158
+
159
+ except HTTPException:
160
+ raise
161
+ except Exception as e:
162
+ logger.error(f"Error creating audit event: {e}", exc_info=True)
163
+ session.rollback()
164
+ raise HTTPException(
165
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
166
+ detail=f"Failed to create audit event: {str(e)}"
167
+ )
168
+
169
+
170
  @router.get("/events/{user_id}", response_model=dict)
171
  async def get_user_audit_events(
172
  user_id: UUID,
src/routers/stats.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Task statistics router
3
+ Provides endpoints for calculating and retrieving task statistics
4
+ """
5
+ from fastapi import APIRouter, Depends, HTTPException
6
+ from sqlalchemy.orm import Session
7
+ from datetime import datetime, timedelta
8
+ from src.database import get_db
9
+ from src.models.task import Task
10
+ from src.models.user import User
11
+ from src.middleware.auth import get_current_user
12
+
13
+ router = APIRouter(prefix="/api", tags=["stats"])
14
+
15
+
16
+ @router.get("/{user_id}/tasks/stats")
17
+ async def get_task_stats(
18
+ user_id: str,
19
+ current_user: User = Depends(get_current_user),
20
+ db: Session = Depends(get_db)
21
+ ):
22
+ """
23
+ Get task statistics for a user.
24
+ Returns: total tasks, completed tasks, pending tasks, completion rate, streak, etc.
25
+ """
26
+ # Verify user is requesting their own stats
27
+ if current_user.id != user_id:
28
+ raise HTTPException(status_code=403, detail="Unauthorized")
29
+
30
+ # Get all tasks for user
31
+ tasks = db.query(Task).filter(Task.user_id == user_id).all()
32
+
33
+ total = len(tasks)
34
+ completed = len([t for t in tasks if t.completed])
35
+ pending = total - completed
36
+ completion_rate = (completed / total * 100) if total > 0 else 0
37
+
38
+ # Calculate streak (consecutive days with completed tasks)
39
+ streak = calculate_streak(tasks)
40
+
41
+ # Get achievements
42
+ achievements = calculate_achievements(total, completed, streak)
43
+
44
+ # Chart data for last 7 days
45
+ chart_data = calculate_chart_data(tasks)
46
+
47
+ return {
48
+ "total": total,
49
+ "completed": completed,
50
+ "pending": pending,
51
+ "completionRate": completion_rate,
52
+ "streak": streak,
53
+ "achievements": achievements,
54
+ "chartData": chart_data
55
+ }
56
+
57
+
58
+ def calculate_streak(tasks: list) -> int:
59
+ """Calculate consecutive days with completed tasks"""
60
+ if not tasks:
61
+ return 0
62
+
63
+ # Sort tasks by completion date (most recent first)
64
+ completed_tasks = sorted(
65
+ [t for t in tasks if t.completed and t.updated_at],
66
+ key=lambda x: x.updated_at,
67
+ reverse=True
68
+ )
69
+
70
+ if not completed_tasks:
71
+ return 0
72
+
73
+ streak = 0
74
+ current_date = datetime.now().date()
75
+
76
+ for task in completed_tasks:
77
+ task_date = task.updated_at.date() if hasattr(task.updated_at, 'date') else task.updated_at
78
+
79
+ # Check if task was completed today or yesterday from current streak
80
+ if task_date == current_date or task_date == current_date - timedelta(days=streak):
81
+ streak += 1
82
+ current_date = task_date
83
+ else:
84
+ break
85
+
86
+ return streak
87
+
88
+
89
+ def calculate_achievements(total: int, completed: int, streak: int) -> list:
90
+ """Calculate unlocked achievements based on stats"""
91
+ achievements = []
92
+
93
+ # First task completed
94
+ if completed >= 1:
95
+ achievements.append({
96
+ "id": "first_task",
97
+ "name": "Getting Started",
98
+ "description": "Completed your first task",
99
+ "icon": "Star",
100
+ "unlocked": True
101
+ })
102
+
103
+ # 5 tasks completed
104
+ if completed >= 5:
105
+ achievements.append({
106
+ "id": "five_tasks",
107
+ "name": "Task Master",
108
+ "description": "Completed 5 tasks",
109
+ "icon": "Trophy",
110
+ "unlocked": True
111
+ })
112
+
113
+ # 10 tasks completed
114
+ if completed >= 10:
115
+ achievements.append({
116
+ "id": "ten_tasks",
117
+ "name": "Productivity Pro",
118
+ "description": "Completed 10 tasks",
119
+ "icon": "Zap",
120
+ "unlocked": True
121
+ })
122
+
123
+ # 3 day streak
124
+ if streak >= 3:
125
+ achievements.append({
126
+ "id": "three_day_streak",
127
+ "name": "On Fire",
128
+ "description": "3 day completion streak",
129
+ "icon": "Flame",
130
+ "unlocked": True
131
+ })
132
+
133
+ # 100% completion rate
134
+ if total > 0 and (completed / total) == 1.0:
135
+ achievements.append({
136
+ "id": "perfect_score",
137
+ "name": "Perfect Score",
138
+ "description": "100% task completion rate",
139
+ "icon": "Award",
140
+ "unlocked": True
141
+ })
142
+
143
+ return achievements
144
+
145
+
146
+ def calculate_chart_data(tasks: list) -> list:
147
+ """Calculate chart data for last 7 days"""
148
+ chart_data = []
149
+ today = datetime.now().date()
150
+
151
+ for i in range(6, -1, -1):
152
+ date = today - timedelta(days=i)
153
+ count = len([
154
+ t for t in tasks
155
+ if t.completed
156
+ and hasattr(t.updated_at, 'date')
157
+ and t.updated_at.date() == date
158
+ ])
159
+
160
+ chart_data.append({
161
+ "date": date.strftime("%a"),
162
+ "count": count,
163
+ "isToday": date == today
164
+ })
165
+
166
+ return chart_data
src/routers/tasks.py CHANGED
@@ -2,7 +2,7 @@ from fastapi import APIRouter, HTTPException, status, Depends
2
  from sqlmodel import Session, select, and_, func
3
  from typing import List
4
  from uuid import UUID
5
- from datetime import datetime, timedelta, date
6
  import logging
7
  import uuid as uuid_lib
8
 
@@ -80,7 +80,7 @@ def get_task_stats(
80
 
81
  streak = 0
82
  if completed_dates:
83
- today = datetime.now(datetime.UTC).date()
84
  yesterday = today - timedelta(days=1)
85
 
86
  # Check if the streak is still active (completed something today or yesterday)
@@ -140,7 +140,7 @@ def get_task_stats(
140
  # Productivity chart data (last 7 days)
141
  chart_data = []
142
  for i in range(6, -1, -1):
143
- day = (datetime.now(datetime.UTC) - timedelta(days=i)).date()
144
  count = len([t for t in completed_tasks if t.updated_at.date() == day])
145
  chart_data.append({
146
  "date": day.strftime("%a"),
@@ -386,7 +386,7 @@ async def update_task(
386
  task.project_id = task_data.project_id
387
 
388
  # Update the timestamp
389
- task.updated_at = datetime.now(datetime.UTC)
390
 
391
  session.add(task)
392
  session.commit()
@@ -484,7 +484,7 @@ async def patch_task(
484
  task.project_id = task_data.project_id
485
 
486
  # Update the timestamp
487
- task.updated_at = datetime.now(datetime.UTC)
488
 
489
  session.add(task)
490
  session.commit()
@@ -599,7 +599,7 @@ async def toggle_task_completion(
599
 
600
  # Toggle the completion status
601
  task.completed = not task.completed
602
- task.updated_at = datetime.now(datetime.UTC)
603
 
604
  session.add(task)
605
  session.commit()
 
2
  from sqlmodel import Session, select, and_, func
3
  from typing import List
4
  from uuid import UUID
5
+ from datetime import datetime, timedelta, date, timezone
6
  import logging
7
  import uuid as uuid_lib
8
 
 
80
 
81
  streak = 0
82
  if completed_dates:
83
+ today = datetime.now(timezone.utc).date()
84
  yesterday = today - timedelta(days=1)
85
 
86
  # Check if the streak is still active (completed something today or yesterday)
 
140
  # Productivity chart data (last 7 days)
141
  chart_data = []
142
  for i in range(6, -1, -1):
143
+ day = (datetime.now(timezone.utc) - timedelta(days=i)).date()
144
  count = len([t for t in completed_tasks if t.updated_at.date() == day])
145
  chart_data.append({
146
  "date": day.strftime("%a"),
 
386
  task.project_id = task_data.project_id
387
 
388
  # Update the timestamp
389
+ task.updated_at = datetime.now(timezone.utc)
390
 
391
  session.add(task)
392
  session.commit()
 
484
  task.project_id = task_data.project_id
485
 
486
  # Update the timestamp
487
+ task.updated_at = datetime.now(timezone.utc)
488
 
489
  session.add(task)
490
  session.commit()
 
599
 
600
  # Toggle the completion status
601
  task.completed = not task.completed
602
+ task.updated_at = datetime.now(timezone.utc)
603
 
604
  session.add(task)
605
  session.commit()