hoangthiencm commited on
Commit
a2d9a28
·
1 Parent(s): e58085c

Upload code backend lan dau

Browse files
.env ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase Configuration
2
+ SUPABASE_URL=https://gtwatjmkyfweohhvpute.supabase.co
3
+ SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd0d2F0am1reWZ3ZW9oaHZwdXRlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjU4ODEyNzIsImV4cCI6MjA4MTQ1NzI3Mn0.vZjoP5dcnYJJGS23rFgg9KMZwpevJDf2Fk76J35RkZM
4
+ SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd0d2F0am1reWZ3ZW9oaHZwdXRlIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2NTg4MTI3MiwiZXhwIjoyMDgxNDU3MjcyfQ.7_aMq3PHNJr_HeiWV6n8Lom2hEcOBBI2zsa7h_nzfRo
5
+
6
+ # JWT Configuration
7
+ SECRET_KEY=hoangtanthiendeptrai1209@
8
+ ALGORITHM=HS256
9
+ ACCESS_TOKEN_EXPIRE_MINUTES=30
10
+
11
+ # CORS Configuration
12
+ CORS_ORIGINS=["http://localhost:3000"]
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ gcc \
8
+ g++ \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements and install Python dependencies
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy application code
16
+ COPY . .
17
+
18
+ # Expose port (Hugging Face Spaces uses port 7860)
19
+ EXPOSE 7860
20
+
21
+ # Run the application
22
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
23
+
README.md CHANGED
@@ -1,11 +1,66 @@
1
- ---
2
- title: Thoikhoabieu
3
- emoji: 📚
4
- colorFrom: pink
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TKB Web Backend - Hugging Face Spaces
2
+
3
+ Backend API cho hệ thống quản lý thời khóa biểu, được deploy trên Hugging Face Spaces.
4
+
5
+ ## Cấu trúc
6
+
7
+ ```
8
+ web-backend/
9
+ ├── app/
10
+ │ ├── __init__.py
11
+ │ ├── main.py # FastAPI application
12
+ │ ├── config.py # Configuration
13
+ │ ├── database.py # Supabase connection
14
+ │ ├── models/ # Pydantic models
15
+ │ ├── routers/ # API routes
16
+ │ │ ├── auth.py
17
+ │ │ ├── units.py
18
+ │ │ ├── teachers.py
19
+ │ │ ├── subjects.py
20
+ │ │ ├── classes.py
21
+ │ │ ├── timetable.py
22
+ │ │ └── solver.py
23
+ │ ├── services/ # Business logic
24
+ │ │ ├── solver_service.py
25
+ │ │ ├── timetable_service.py
26
+ │ │ └── ai_service.py
27
+ │ └── utils/ # Utilities
28
+ ├── requirements.txt
29
+ ├── Dockerfile
30
+ └── README.md
31
+ ```
32
+
33
+ ## Setup
34
+
35
+ 1. Cài đặt dependencies:
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ ```
39
+
40
+ 2. Cấu hình environment variables:
41
+ ```bash
42
+ export SUPABASE_URL=your_supabase_url
43
+ export SUPABASE_KEY=your_supabase_key
44
+ export GEMINI_API_KEY=your_gemini_key
45
+ ```
46
+
47
+ 3. Chạy server:
48
+ ```bash
49
+ uvicorn app.main:app --host 0.0.0.0 --port 7860
50
+ ```
51
+
52
+ ## Deploy lên Hugging Face Spaces
53
+
54
+ 1. Tạo Space mới trên Hugging Face
55
+ 2. Chọn SDK: Docker
56
+ 3. Push code lên repository
57
+ 4. Hugging Face sẽ tự động build và deploy
58
+
59
+ ## API Endpoints
60
+
61
+ - `POST /api/auth/login` - Đăng nhập
62
+ - `GET /api/units` - Lấy danh sách đơn vị
63
+ - `GET /api/teachers` - Lấy danh sách giáo viên
64
+ - `POST /api/timetable/solve` - Xếp thời khóa biểu
65
+ - `GET /api/timetable/{session_id}` - Lấy thời khóa biểu
66
+
app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # TKB Web Backend Application
app/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (143 Bytes). View file
 
app/__pycache__/config.cpython-312.pyc ADDED
Binary file (1.38 kB). View file
 
app/__pycache__/database.cpython-312.pyc ADDED
Binary file (3.98 kB). View file
 
app/__pycache__/main.cpython-312.pyc ADDED
Binary file (3.19 kB). View file
 
app/config.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration settings for the application
3
+ """
4
+ from pydantic_settings import BaseSettings
5
+ from typing import Optional
6
+
7
+
8
+ class Settings(BaseSettings):
9
+ """Application settings loaded from environment variables"""
10
+
11
+ # Supabase Configuration
12
+ supabase_url: str
13
+ supabase_key: str
14
+ supabase_service_key: Optional[str] = None
15
+
16
+ # Gemini AI Configuration
17
+ gemini_api_key: Optional[str] = None
18
+
19
+ # JWT Configuration
20
+ secret_key: str = "your-secret-key-change-in-production"
21
+ algorithm: str = "HS256"
22
+ access_token_expire_minutes: int = 30
23
+
24
+ # CORS Configuration
25
+ cors_origins: list[str] = [
26
+ "http://localhost:3000",
27
+ "https://your-vercel-app.vercel.app"
28
+ ]
29
+
30
+ # Hugging Face Spaces
31
+ space_id: Optional[str] = None
32
+
33
+ class Config:
34
+ env_file = ".env"
35
+ case_sensitive = False
36
+
37
+
38
+ settings = Settings()
39
+
app/database.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Supabase database connection and utilities
3
+ """
4
+ from supabase import create_client, Client
5
+ from app.config import settings
6
+ from typing import Optional
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class DatabaseManager:
12
+ """Manages Supabase database connections"""
13
+
14
+ def __init__(self):
15
+ self.supabase: Optional[Client] = None
16
+ self._initialize_client()
17
+
18
+ def _initialize_client(self):
19
+ """Initialize Supabase client"""
20
+ try:
21
+ self.supabase = create_client(
22
+ settings.supabase_url,
23
+ settings.supabase_key
24
+ )
25
+ logger.info("Supabase client initialized successfully")
26
+ except Exception as e:
27
+ logger.error(f"Failed to initialize Supabase client: {e}")
28
+ raise
29
+
30
+ def get_client(self) -> Client:
31
+ """Get Supabase client instance"""
32
+ if self.supabase is None:
33
+ self._initialize_client()
34
+ return self.supabase
35
+
36
+ # Helper methods for common operations
37
+ def execute_query(self, table: str, operation: str = "select", **kwargs):
38
+ """Execute a query on a table"""
39
+ client = self.get_client()
40
+
41
+ try:
42
+ if operation == "select":
43
+ query = client.table(table).select("*")
44
+ if "filters" in kwargs:
45
+ for filter_item in kwargs["filters"]:
46
+ query = query.eq(filter_item["column"], filter_item["value"])
47
+ if "limit" in kwargs:
48
+ query = query.limit(kwargs["limit"])
49
+ if "order" in kwargs:
50
+ order_col = kwargs["order"]["column"]
51
+ order_desc = kwargs["order"].get("desc", False)
52
+ query = query.order(order_col, desc=order_desc)
53
+ result = query.execute()
54
+ return result
55
+
56
+ elif operation == "insert":
57
+ data = kwargs.get("data", {})
58
+ result = client.table(table).insert(data).execute()
59
+ return result
60
+
61
+ elif operation == "update":
62
+ query = client.table(table).update(kwargs.get("data", {}))
63
+ if "filters" in kwargs:
64
+ for filter_item in kwargs["filters"]:
65
+ query = query.eq(filter_item["column"], filter_item["value"])
66
+ result = query.execute()
67
+ return result
68
+
69
+ elif operation == "delete":
70
+ query = client.table(table).delete()
71
+ if "filters" in kwargs:
72
+ for filter_item in kwargs["filters"]:
73
+ query = query.eq(filter_item["column"], filter_item["value"])
74
+ result = query.execute()
75
+ return result
76
+ except Exception as e:
77
+ logger.error(f"Error executing query on {table}: {e}")
78
+ raise
79
+
80
+
81
+ # Global database manager instance
82
+ db_manager = DatabaseManager()
83
+
app/main.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI main application
3
+ """
4
+ from fastapi import FastAPI, Request
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.responses import JSONResponse
7
+ from app.config import settings
8
+ from app.routers import auth, units, teachers, subjects, classes, timetable, solver
9
+ import logging
10
+
11
+ logging.basicConfig(level=logging.INFO)
12
+ logger = logging.getLogger(__name__)
13
+
14
+ app = FastAPI(
15
+ title="TKB Web API",
16
+ description="API cho hệ thống quản lý thời khóa biểu",
17
+ version="2.0.0"
18
+ )
19
+
20
+ # CORS Middleware
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=settings.cors_origins,
24
+ allow_credentials=True,
25
+ allow_methods=["*"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+ # Include routers
30
+ app.include_router(auth.router, prefix="/api/auth", tags=["Authentication"])
31
+ app.include_router(units.router, prefix="/api/units", tags=["Units"])
32
+ app.include_router(teachers.router, prefix="/api/teachers", tags=["Teachers"])
33
+ app.include_router(subjects.router, prefix="/api/subjects", tags=["Subjects"])
34
+ app.include_router(classes.router, prefix="/api/classes", tags=["Classes"])
35
+ app.include_router(timetable.router, prefix="/api/timetable", tags=["Timetable"])
36
+ app.include_router(solver.router, prefix="/api/solver", tags=["Solver"])
37
+
38
+
39
+ @app.get("/")
40
+ async def root():
41
+ """Root endpoint"""
42
+ return {
43
+ "message": "TKB Web API",
44
+ "version": "2.0.0",
45
+ "status": "running"
46
+ }
47
+
48
+
49
+ @app.get("/health")
50
+ async def health_check():
51
+ """Health check endpoint"""
52
+ return {"status": "healthy"}
53
+
54
+
55
+ @app.exception_handler(Exception)
56
+ async def global_exception_handler(request: Request, exc: Exception):
57
+ """Global exception handler"""
58
+ logger.error(f"Unhandled exception: {exc}", exc_info=True)
59
+ return JSONResponse(
60
+ status_code=500,
61
+ content={"detail": "Internal server error"}
62
+ )
63
+
64
+
65
+ if __name__ == "__main__":
66
+ import uvicorn
67
+ uvicorn.run(app, host="0.0.0.0", port=7860)
68
+
app/models/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Models package
2
+
app/models/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (150 Bytes). View file
 
app/models/__pycache__/schemas.cpython-312.pyc ADDED
Binary file (6.86 kB). View file
 
app/models/schemas.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic models for request/response schemas
3
+ """
4
+ from pydantic import BaseModel, EmailStr
5
+ from typing import Optional, List, Dict, Any
6
+ from datetime import datetime
7
+
8
+
9
+ # Authentication Models
10
+ class LoginRequest(BaseModel):
11
+ username: str
12
+ password: str
13
+
14
+
15
+ class TokenResponse(BaseModel):
16
+ access_token: str
17
+ token_type: str = "bearer"
18
+
19
+
20
+ # Unit Models
21
+ class UnitCreate(BaseModel):
22
+ name: str
23
+
24
+
25
+ class UnitResponse(BaseModel):
26
+ id: int
27
+ name: str
28
+
29
+ class Config:
30
+ from_attributes = True
31
+
32
+
33
+ # Teacher Models
34
+ class TeacherCreate(BaseModel):
35
+ id: str
36
+ name: str
37
+ subjects: List[str] = []
38
+ secondary_subjects: List[str] = []
39
+ busy_slots: List[List[int]] = [] # [[day, period], ...]
40
+ is_locked: bool = False
41
+ work_days_preference: int = 0
42
+ role: Optional[str] = None
43
+ target_periods: Optional[int] = None
44
+ email: Optional[EmailStr] = None
45
+ professional_group_name: Optional[str] = None
46
+
47
+
48
+ class TeacherResponse(BaseModel):
49
+ unit_id: int
50
+ school_year: str
51
+ id: str
52
+ name: str
53
+ subjects: List[str]
54
+ secondary_subjects: List[str]
55
+ busy_slots: List[List[int]]
56
+ is_locked: bool
57
+ work_days_preference: int
58
+ role: Optional[str]
59
+ target_periods: Optional[int]
60
+ email: Optional[str]
61
+ professional_group_name: Optional[str]
62
+
63
+ class Config:
64
+ from_attributes = True
65
+
66
+
67
+ # Subject Models
68
+ class SubjectCreate(BaseModel):
69
+ name: str
70
+ periods_per_week: Dict[str, int] = {} # {grade: periods}
71
+ is_double_period_only: bool = False
72
+
73
+
74
+ class SubjectResponse(BaseModel):
75
+ unit_id: int
76
+ school_year: str
77
+ name: str
78
+ periods_per_week: Dict[str, int]
79
+ is_double_period_only: bool
80
+
81
+ class Config:
82
+ from_attributes = True
83
+
84
+
85
+ # Class Models
86
+ class ClassCreate(BaseModel):
87
+ name: str
88
+ grade: Optional[int] = None
89
+ session: Optional[str] = None
90
+ homeroom_teacher_id: Optional[str] = None
91
+ fixed_off_slots: List[List[int]] = []
92
+ is_locked: bool = False
93
+
94
+
95
+ class ClassResponse(BaseModel):
96
+ unit_id: int
97
+ school_year: str
98
+ name: str
99
+ grade: Optional[int]
100
+ session: Optional[str]
101
+ homeroom_teacher_id: Optional[str]
102
+ fixed_off_slots: List[List[int]]
103
+ is_locked: bool
104
+
105
+ class Config:
106
+ from_attributes = True
107
+
108
+
109
+ # Timetable Models
110
+ class TimetableSessionCreate(BaseModel):
111
+ session_name: str
112
+ effective_date: Optional[str] = None
113
+
114
+
115
+ class TimetableSessionResponse(BaseModel):
116
+ id: int
117
+ unit_id: int
118
+ school_year: str
119
+ session_name: str
120
+ is_locked: bool
121
+ created_at: str
122
+ effective_date: Optional[str]
123
+
124
+ class Config:
125
+ from_attributes = True
126
+
127
+
128
+ class TimetableData(BaseModel):
129
+ timetable: Dict[str, Any] # Flexible structure for timetable data
130
+ unplaced_lessons: List[Dict[str, Any]] = []
131
+
132
+
133
+ # Solver Models
134
+ class SolverRequest(BaseModel):
135
+ unit_id: int
136
+ school_year: str
137
+ mode: str = "ortools_balanced" # ortools_balanced, ortools_days_off
138
+ num_workers: int = 8
139
+ time_limit: float = 300.0
140
+ weights: Optional[Dict[str, float]] = None
141
+ priority_teacher_ids: Optional[List[str]] = None
142
+ phase: Optional[str] = None # phase1, phase2, phase3
143
+
144
+
145
+ class SolverResponse(BaseModel):
146
+ success: bool
147
+ timetable: Optional[Dict[str, Any]] = None
148
+ objective_value: Optional[float] = None
149
+ unplaced_lessons: List[Dict[str, Any]] = []
150
+ message: str
151
+ execution_time: Optional[float] = None
152
+
app/routers/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Routers package
2
+
app/routers/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (151 Bytes). View file
 
app/routers/__pycache__/auth.cpython-312.pyc ADDED
Binary file (7.42 kB). View file
 
app/routers/__pycache__/classes.cpython-312.pyc ADDED
Binary file (4.08 kB). View file
 
app/routers/__pycache__/solver.cpython-312.pyc ADDED
Binary file (1.93 kB). View file
 
app/routers/__pycache__/subjects.cpython-312.pyc ADDED
Binary file (3.98 kB). View file
 
app/routers/__pycache__/teachers.cpython-312.pyc ADDED
Binary file (6.54 kB). View file
 
app/routers/__pycache__/timetable.cpython-312.pyc ADDED
Binary file (5.95 kB). View file
 
app/routers/__pycache__/units.cpython-312.pyc ADDED
Binary file (3.93 kB). View file
 
app/routers/auth.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Authentication routes - Debug Version (Using Logger)
3
+ """
4
+ from fastapi import APIRouter, Depends, HTTPException, status
5
+ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
6
+ from datetime import datetime, timedelta
7
+ from typing import Optional
8
+ from jose import JWTError, jwt
9
+ from app.config import settings
10
+ from app.models.schemas import LoginRequest, TokenResponse
11
+ from app.database import db_manager
12
+ import hashlib
13
+ import logging
14
+
15
+ # Cấu hình logger để đảm bảo hiện ra terminal
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger("uvicorn.error") # Sử dụng logger của uvicorn để chắc chắn hiện
18
+
19
+ router = APIRouter()
20
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
21
+
22
+
23
+ def hash_password(password: str) -> str:
24
+ """Hash password using SHA-256 (compatible with original code)"""
25
+ return hashlib.sha256(password.encode('utf-8')).hexdigest()
26
+
27
+
28
+ def verify_password(stored_hash: str, provided_password: str) -> bool:
29
+ """Verify password against stored hash (SHA-256)"""
30
+ calculated_hash = hash_password(provided_password)
31
+ # Log hash để so sánh
32
+ logger.warning(f"CHECK PASS: Hash trong DB = {stored_hash}")
33
+ logger.warning(f"CHECK PASS: Hash nhập vào = {calculated_hash}")
34
+ return stored_hash == calculated_hash
35
+
36
+
37
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
38
+ """Create JWT access token"""
39
+ to_encode = data.copy()
40
+ if expires_delta:
41
+ expire = datetime.utcnow() + expires_delta
42
+ else:
43
+ expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
44
+ to_encode.update({"exp": expire})
45
+ encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
46
+ return encoded_jwt
47
+
48
+
49
+ async def get_current_user(token: str = Depends(oauth2_scheme)):
50
+ """Get current authenticated user"""
51
+ credentials_exception = HTTPException(
52
+ status_code=status.HTTP_401_UNAUTHORIZED,
53
+ detail="Could not validate credentials",
54
+ headers={"WWW-Authenticate": "Bearer"},
55
+ )
56
+ try:
57
+ payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
58
+ username: str = payload.get("sub")
59
+ if username is None:
60
+ raise credentials_exception
61
+ except JWTError:
62
+ raise credentials_exception
63
+
64
+ # Verify user exists in database
65
+ try:
66
+ result = db_manager.execute_query(
67
+ "users",
68
+ "select",
69
+ filters=[{"column": "username", "value": username}]
70
+ )
71
+ if not result.data:
72
+ raise credentials_exception
73
+ return result.data[0]
74
+ except Exception as e:
75
+ logger.error(f"Error fetching user: {e}")
76
+ raise credentials_exception
77
+
78
+
79
+ @router.post("/login", response_model=TokenResponse)
80
+ async def login(form_data: OAuth2PasswordRequestForm = Depends()):
81
+ """Login endpoint with LOGGER WARNING"""
82
+ try:
83
+ logger.warning("\n" + "="*50)
84
+ logger.warning(f"DEBUG: Bắt đầu đăng nhập cho user: '{form_data.username}'")
85
+
86
+ # 1. Get user from database
87
+ result = db_manager.execute_query(
88
+ "users",
89
+ "select",
90
+ filters=[{"column": "username", "value": form_data.username}]
91
+ )
92
+
93
+ logger.warning(f"DEBUG: Kết quả tìm trong DB (Raw Data): {result.data}")
94
+
95
+ if not result.data:
96
+ logger.warning("DEBUG: LỖI -> Không tìm thấy user hoặc bị RLS chặn. Danh sách trả về rỗng []")
97
+ logger.warning("="*50 + "\n")
98
+ raise HTTPException(
99
+ status_code=status.HTTP_401_UNAUTHORIZED,
100
+ detail="Incorrect username or password"
101
+ )
102
+
103
+ user = result.data[0]
104
+
105
+ # 2. Verify password
106
+ stored_hash = user.get("password_hash", "")
107
+ # Password verify sẽ tự log hash bên trong hàm verify_password
108
+
109
+ is_valid = False
110
+ if not stored_hash:
111
+ logger.warning("DEBUG: LỖI -> User trong DB không có cột password_hash")
112
+ else:
113
+ is_valid = verify_password(stored_hash, form_data.password)
114
+
115
+ if not is_valid:
116
+ logger.warning("DEBUG: KẾT QUẢ -> Mật khẩu KHÔNG khớp!")
117
+ logger.warning("="*50 + "\n")
118
+ raise HTTPException(
119
+ status_code=status.HTTP_401_UNAUTHORIZED,
120
+ detail="Incorrect username or password"
121
+ )
122
+
123
+ logger.warning("DEBUG: KẾT QUẢ -> Đăng nhập THÀNH CÔNG!")
124
+ logger.warning("="*50 + "\n")
125
+
126
+ # 3. Create access token
127
+ access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
128
+ access_token = create_access_token(
129
+ data={"sub": user["username"], "role": user.get("role", "user")},
130
+ expires_delta=access_token_expires
131
+ )
132
+
133
+ return {"access_token": access_token, "token_type": "bearer"}
134
+
135
+ except HTTPException:
136
+ raise
137
+ except Exception as e:
138
+ logger.error(f"Login error: {e}")
139
+ logger.warning(f"DEBUG: LỖI HỆ THỐNG -> {str(e)}")
140
+ raise HTTPException(
141
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
142
+ detail="Internal server error"
143
+ )
144
+
145
+
146
+ @router.get("/me")
147
+ async def get_current_user_info(current_user: dict = Depends(get_current_user)):
148
+ """Get current user information"""
149
+ return {
150
+ "username": current_user.get("username"),
151
+ "role": current_user.get("role", "user")
152
+ }
app/routers/classes.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Classes management routes
3
+ """
4
+ from fastapi import APIRouter, Depends, HTTPException, Query
5
+ from app.models.schemas import ClassCreate, ClassResponse
6
+ from app.routers.auth import get_current_user
7
+ from app.database import db_manager
8
+ import json
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ @router.get("", response_model=list[ClassResponse])
17
+ async def get_classes(
18
+ unit_id: int = Query(...),
19
+ school_year: str = Query(...),
20
+ current_user: dict = Depends(get_current_user)
21
+ ):
22
+ """Get all classes for a unit and school year"""
23
+ try:
24
+ result = db_manager.execute_query(
25
+ "classes",
26
+ "select",
27
+ filters=[
28
+ {"column": "unit_id", "value": unit_id},
29
+ {"column": "school_year", "value": school_year}
30
+ ]
31
+ )
32
+ # Parse JSON fields
33
+ classes = []
34
+ for cls in result.data:
35
+ cls["fixed_off_slots"] = json.loads(cls.get("fixed_off_slots") or "[]")
36
+ classes.append(cls)
37
+ return classes
38
+ except Exception as e:
39
+ logger.error(f"Error fetching classes: {e}")
40
+ raise HTTPException(status_code=500, detail="Error fetching classes")
41
+
42
+
43
+ @router.post("", response_model=ClassResponse)
44
+ async def create_class(
45
+ unit_id: int,
46
+ school_year: str,
47
+ class_data: ClassCreate,
48
+ current_user: dict = Depends(get_current_user)
49
+ ):
50
+ """Create a new class"""
51
+ try:
52
+ data = {
53
+ "unit_id": unit_id,
54
+ "school_year": school_year,
55
+ "name": class_data.name,
56
+ "grade": class_data.grade,
57
+ "session": class_data.session,
58
+ "homeroom_teacher_id": class_data.homeroom_teacher_id,
59
+ "fixed_off_slots": json.dumps(class_data.fixed_off_slots),
60
+ "is_locked": class_data.is_locked
61
+ }
62
+ result = db_manager.execute_query("classes", "insert", data=data)
63
+ if result.data:
64
+ class_result = result.data[0]
65
+ class_result["fixed_off_slots"] = class_data.fixed_off_slots
66
+ return class_result
67
+ raise HTTPException(status_code=500, detail="Error creating class")
68
+ except HTTPException:
69
+ raise
70
+ except Exception as e:
71
+ logger.error(f"Error creating class: {e}")
72
+ raise HTTPException(status_code=500, detail="Error creating class")
73
+
74
+
75
+ @router.delete("/{class_name}")
76
+ async def delete_class(
77
+ unit_id: int,
78
+ school_year: str,
79
+ class_name: str,
80
+ current_user: dict = Depends(get_current_user)
81
+ ):
82
+ """Delete a class"""
83
+ try:
84
+ db_manager.execute_query(
85
+ "classes",
86
+ "delete",
87
+ filters=[
88
+ {"column": "unit_id", "value": unit_id},
89
+ {"column": "school_year", "value": school_year},
90
+ {"column": "name", "value": class_name}
91
+ ]
92
+ )
93
+ return {"message": "Class deleted successfully"}
94
+ except Exception as e:
95
+ logger.error(f"Error deleting class: {e}")
96
+ raise HTTPException(status_code=500, detail="Error deleting class")
97
+
app/routers/solver.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Timetable solver routes
3
+ """
4
+ from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
5
+ from app.models.schemas import SolverRequest, SolverResponse
6
+ from app.routers.auth import get_current_user
7
+ from app.services.solver_service import SolverService
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ @router.post("/solve", response_model=SolverResponse)
16
+ async def solve_timetable(
17
+ request: SolverRequest,
18
+ background_tasks: BackgroundTasks,
19
+ current_user: dict = Depends(get_current_user)
20
+ ):
21
+ """Solve timetable scheduling problem"""
22
+ try:
23
+ solver_service = SolverService()
24
+ result = await solver_service.solve(
25
+ unit_id=request.unit_id,
26
+ school_year=request.school_year,
27
+ mode=request.mode,
28
+ num_workers=request.num_workers,
29
+ time_limit=request.time_limit,
30
+ weights=request.weights,
31
+ priority_teacher_ids=request.priority_teacher_ids,
32
+ phase=request.phase
33
+ )
34
+ return result
35
+ except Exception as e:
36
+ logger.error(f"Error solving timetable: {e}")
37
+ raise HTTPException(status_code=500, detail=f"Error solving timetable: {str(e)}")
38
+
app/routers/subjects.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Subjects management routes
3
+ """
4
+ from fastapi import APIRouter, Depends, HTTPException, Query
5
+ from app.models.schemas import SubjectCreate, SubjectResponse
6
+ from app.routers.auth import get_current_user
7
+ from app.database import db_manager
8
+ import json
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ @router.get("", response_model=list[SubjectResponse])
17
+ async def get_subjects(
18
+ unit_id: int = Query(...),
19
+ school_year: str = Query(...),
20
+ current_user: dict = Depends(get_current_user)
21
+ ):
22
+ """Get all subjects for a unit and school year"""
23
+ try:
24
+ result = db_manager.execute_query(
25
+ "subjects",
26
+ "select",
27
+ filters=[
28
+ {"column": "unit_id", "value": unit_id},
29
+ {"column": "school_year", "value": school_year}
30
+ ]
31
+ )
32
+ # Parse JSON fields
33
+ subjects = []
34
+ for subject in result.data:
35
+ subject["periods_per_week"] = json.loads(subject.get("periods_per_week") or "{}")
36
+ subjects.append(subject)
37
+ return subjects
38
+ except Exception as e:
39
+ logger.error(f"Error fetching subjects: {e}")
40
+ raise HTTPException(status_code=500, detail="Error fetching subjects")
41
+
42
+
43
+ @router.post("", response_model=SubjectResponse)
44
+ async def create_subject(
45
+ unit_id: int,
46
+ school_year: str,
47
+ subject: SubjectCreate,
48
+ current_user: dict = Depends(get_current_user)
49
+ ):
50
+ """Create a new subject"""
51
+ try:
52
+ data = {
53
+ "unit_id": unit_id,
54
+ "school_year": school_year,
55
+ "name": subject.name,
56
+ "periods_per_week": json.dumps(subject.periods_per_week),
57
+ "is_double_period_only": subject.is_double_period_only
58
+ }
59
+ result = db_manager.execute_query("subjects", "insert", data=data)
60
+ if result.data:
61
+ subject_data = result.data[0]
62
+ subject_data["periods_per_week"] = subject.periods_per_week
63
+ return subject_data
64
+ raise HTTPException(status_code=500, detail="Error creating subject")
65
+ except HTTPException:
66
+ raise
67
+ except Exception as e:
68
+ logger.error(f"Error creating subject: {e}")
69
+ raise HTTPException(status_code=500, detail="Error creating subject")
70
+
71
+
72
+ @router.delete("/{subject_name}")
73
+ async def delete_subject(
74
+ unit_id: int,
75
+ school_year: str,
76
+ subject_name: str,
77
+ current_user: dict = Depends(get_current_user)
78
+ ):
79
+ """Delete a subject"""
80
+ try:
81
+ db_manager.execute_query(
82
+ "subjects",
83
+ "delete",
84
+ filters=[
85
+ {"column": "unit_id", "value": unit_id},
86
+ {"column": "school_year", "value": school_year},
87
+ {"column": "name", "value": subject_name}
88
+ ]
89
+ )
90
+ return {"message": "Subject deleted successfully"}
91
+ except Exception as e:
92
+ logger.error(f"Error deleting subject: {e}")
93
+ raise HTTPException(status_code=500, detail="Error deleting subject")
94
+
app/routers/teachers.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Teachers management routes
3
+ """
4
+ from fastapi import APIRouter, Depends, HTTPException, Query
5
+ from app.models.schemas import TeacherCreate, TeacherResponse
6
+ from app.routers.auth import get_current_user
7
+ from app.database import db_manager
8
+ import json
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ @router.get("", response_model=list[TeacherResponse])
17
+ async def get_teachers(
18
+ unit_id: int = Query(...),
19
+ school_year: str = Query(...),
20
+ current_user: dict = Depends(get_current_user)
21
+ ):
22
+ """Get all teachers for a unit and school year"""
23
+ try:
24
+ result = db_manager.execute_query(
25
+ "teachers",
26
+ "select",
27
+ filters=[
28
+ {"column": "unit_id", "value": unit_id},
29
+ {"column": "school_year", "value": school_year}
30
+ ]
31
+ )
32
+ # Parse JSON fields
33
+ teachers = []
34
+ for teacher in result.data:
35
+ teacher["subjects"] = json.loads(teacher.get("subjects") or "[]")
36
+ teacher["secondary_subjects"] = json.loads(teacher.get("secondary_subjects") or "[]")
37
+ teacher["busy_slots"] = json.loads(teacher.get("busy_slots") or "[]")
38
+ teachers.append(teacher)
39
+ return teachers
40
+ except Exception as e:
41
+ logger.error(f"Error fetching teachers: {e}")
42
+ raise HTTPException(status_code=500, detail="Error fetching teachers")
43
+
44
+
45
+ @router.post("", response_model=TeacherResponse)
46
+ async def create_teacher(
47
+ unit_id: int,
48
+ school_year: str,
49
+ teacher: TeacherCreate,
50
+ current_user: dict = Depends(get_current_user)
51
+ ):
52
+ """Create a new teacher"""
53
+ try:
54
+ data = {
55
+ "unit_id": unit_id,
56
+ "school_year": school_year,
57
+ "id": teacher.id,
58
+ "name": teacher.name,
59
+ "subjects": json.dumps(teacher.subjects),
60
+ "secondary_subjects": json.dumps(teacher.secondary_subjects),
61
+ "busy_slots": json.dumps(teacher.busy_slots),
62
+ "is_locked": teacher.is_locked,
63
+ "work_days_preference": teacher.work_days_preference,
64
+ "role": teacher.role,
65
+ "target_periods": teacher.target_periods,
66
+ "email": teacher.email,
67
+ "professional_group_name": teacher.professional_group_name
68
+ }
69
+ result = db_manager.execute_query("teachers", "insert", data=data)
70
+ if result.data:
71
+ teacher_data = result.data[0]
72
+ teacher_data["subjects"] = teacher.subjects
73
+ teacher_data["secondary_subjects"] = teacher.secondary_subjects
74
+ teacher_data["busy_slots"] = teacher.busy_slots
75
+ return teacher_data
76
+ raise HTTPException(status_code=500, detail="Error creating teacher")
77
+ except HTTPException:
78
+ raise
79
+ except Exception as e:
80
+ logger.error(f"Error creating teacher: {e}")
81
+ raise HTTPException(status_code=500, detail="Error creating teacher")
82
+
83
+
84
+ @router.put("/{teacher_id}", response_model=TeacherResponse)
85
+ async def update_teacher(
86
+ unit_id: int,
87
+ school_year: str,
88
+ teacher_id: str,
89
+ teacher: TeacherCreate,
90
+ current_user: dict = Depends(get_current_user)
91
+ ):
92
+ """Update a teacher"""
93
+ try:
94
+ data = {
95
+ "name": teacher.name,
96
+ "subjects": json.dumps(teacher.subjects),
97
+ "secondary_subjects": json.dumps(teacher.secondary_subjects),
98
+ "busy_slots": json.dumps(teacher.busy_slots),
99
+ "is_locked": teacher.is_locked,
100
+ "work_days_preference": teacher.work_days_preference,
101
+ "role": teacher.role,
102
+ "target_periods": teacher.target_periods,
103
+ "email": teacher.email,
104
+ "professional_group_name": teacher.professional_group_name
105
+ }
106
+ result = db_manager.execute_query(
107
+ "teachers",
108
+ "update",
109
+ data=data,
110
+ filters=[
111
+ {"column": "unit_id", "value": unit_id},
112
+ {"column": "school_year", "value": school_year},
113
+ {"column": "id", "value": teacher_id}
114
+ ]
115
+ )
116
+ if not result.data:
117
+ raise HTTPException(status_code=404, detail="Teacher not found")
118
+ teacher_data = result.data[0]
119
+ teacher_data["subjects"] = teacher.subjects
120
+ teacher_data["secondary_subjects"] = teacher.secondary_subjects
121
+ teacher_data["busy_slots"] = teacher.busy_slots
122
+ return teacher_data
123
+ except HTTPException:
124
+ raise
125
+ except Exception as e:
126
+ logger.error(f"Error updating teacher: {e}")
127
+ raise HTTPException(status_code=500, detail="Error updating teacher")
128
+
129
+
130
+ @router.delete("/{teacher_id}")
131
+ async def delete_teacher(
132
+ unit_id: int,
133
+ school_year: str,
134
+ teacher_id: str,
135
+ current_user: dict = Depends(get_current_user)
136
+ ):
137
+ """Delete a teacher"""
138
+ try:
139
+ db_manager.execute_query(
140
+ "teachers",
141
+ "delete",
142
+ filters=[
143
+ {"column": "unit_id", "value": unit_id},
144
+ {"column": "school_year", "value": school_year},
145
+ {"column": "id", "value": teacher_id}
146
+ ]
147
+ )
148
+ return {"message": "Teacher deleted successfully"}
149
+ except Exception as e:
150
+ logger.error(f"Error deleting teacher: {e}")
151
+ raise HTTPException(status_code=500, detail="Error deleting teacher")
152
+
app/routers/timetable.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Timetable management routes
3
+ """
4
+ from fastapi import APIRouter, Depends, HTTPException, Query
5
+ from app.models.schemas import TimetableSessionCreate, TimetableSessionResponse, TimetableData
6
+ from app.routers.auth import get_current_user
7
+ from app.database import db_manager
8
+ import json
9
+ import logging
10
+ from datetime import datetime
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ @router.get("/sessions", response_model=list[TimetableSessionResponse])
18
+ async def get_timetable_sessions(
19
+ unit_id: int = Query(...),
20
+ school_year: str = Query(...),
21
+ current_user: dict = Depends(get_current_user)
22
+ ):
23
+ """Get all timetable sessions for a unit and school year"""
24
+ try:
25
+ result = db_manager.execute_query(
26
+ "timetable_sessions",
27
+ "select",
28
+ filters=[
29
+ {"column": "unit_id", "value": unit_id},
30
+ {"column": "school_year", "value": school_year}
31
+ ],
32
+ order={"column": "created_at", "desc": True}
33
+ )
34
+ return result.data
35
+ except Exception as e:
36
+ logger.error(f"Error fetching timetable sessions: {e}")
37
+ raise HTTPException(status_code=500, detail="Error fetching timetable sessions")
38
+
39
+
40
+ @router.post("/sessions", response_model=TimetableSessionResponse)
41
+ async def create_timetable_session(
42
+ unit_id: int,
43
+ school_year: str,
44
+ session: TimetableSessionCreate,
45
+ timetable_data: TimetableData,
46
+ current_user: dict = Depends(get_current_user)
47
+ ):
48
+ """Create a new timetable session"""
49
+ try:
50
+ data = {
51
+ "unit_id": unit_id,
52
+ "school_year": school_year,
53
+ "session_name": session.session_name,
54
+ "timetable_data": json.dumps(timetable_data.timetable),
55
+ "is_locked": False,
56
+ "created_at": datetime.now().isoformat(),
57
+ "effective_date": session.effective_date
58
+ }
59
+ result = db_manager.execute_query("timetable_sessions", "insert", data=data)
60
+ if result.data:
61
+ return result.data[0]
62
+ raise HTTPException(status_code=500, detail="Error creating timetable session")
63
+ except HTTPException:
64
+ raise
65
+ except Exception as e:
66
+ logger.error(f"Error creating timetable session: {e}")
67
+ raise HTTPException(status_code=500, detail="Error creating timetable session")
68
+
69
+
70
+ @router.get("/sessions/{session_id}", response_model=TimetableData)
71
+ async def get_timetable_data(
72
+ session_id: int,
73
+ current_user: dict = Depends(get_current_user)
74
+ ):
75
+ """Get timetable data for a session"""
76
+ try:
77
+ result = db_manager.execute_query(
78
+ "timetable_sessions",
79
+ "select",
80
+ filters=[{"column": "id", "value": session_id}]
81
+ )
82
+ if not result.data:
83
+ raise HTTPException(status_code=404, detail="Timetable session not found")
84
+
85
+ session = result.data[0]
86
+ timetable_data = json.loads(session.get("timetable_data", "{}"))
87
+ return TimetableData(timetable=timetable_data)
88
+ except HTTPException:
89
+ raise
90
+ except Exception as e:
91
+ logger.error(f"Error fetching timetable data: {e}")
92
+ raise HTTPException(status_code=500, detail="Error fetching timetable data")
93
+
94
+
95
+ @router.put("/sessions/{session_id}/lock")
96
+ async def toggle_session_lock(
97
+ session_id: int,
98
+ is_locked: bool,
99
+ current_user: dict = Depends(get_current_user)
100
+ ):
101
+ """Toggle lock status of a timetable session"""
102
+ try:
103
+ db_manager.execute_query(
104
+ "timetable_sessions",
105
+ "update",
106
+ data={"is_locked": is_locked},
107
+ filters=[{"column": "id", "value": session_id}]
108
+ )
109
+ return {"message": "Session lock status updated"}
110
+ except Exception as e:
111
+ logger.error(f"Error updating session lock: {e}")
112
+ raise HTTPException(status_code=500, detail="Error updating session lock")
113
+
114
+
115
+ @router.delete("/sessions/{session_id}")
116
+ async def delete_timetable_session(
117
+ session_id: int,
118
+ current_user: dict = Depends(get_current_user)
119
+ ):
120
+ """Delete a timetable session"""
121
+ try:
122
+ db_manager.execute_query(
123
+ "timetable_sessions",
124
+ "delete",
125
+ filters=[{"column": "id", "value": session_id}]
126
+ )
127
+ return {"message": "Timetable session deleted successfully"}
128
+ except Exception as e:
129
+ logger.error(f"Error deleting timetable session: {e}")
130
+ raise HTTPException(status_code=500, detail="Error deleting timetable session")
131
+
app/routers/units.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Units management routes
3
+ """
4
+ from fastapi import APIRouter, Depends, HTTPException
5
+ from app.models.schemas import UnitCreate, UnitResponse
6
+ from app.routers.auth import get_current_user
7
+ from app.database import db_manager
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ @router.get("", response_model=list[UnitResponse])
16
+ async def get_all_units(current_user: dict = Depends(get_current_user)):
17
+ """Get all units"""
18
+ try:
19
+ result = db_manager.execute_query("units", "select", order={"column": "name", "desc": False})
20
+ return result.data
21
+ except Exception as e:
22
+ logger.error(f"Error fetching units: {e}")
23
+ raise HTTPException(status_code=500, detail="Error fetching units")
24
+
25
+
26
+ @router.post("", response_model=UnitResponse)
27
+ async def create_unit(unit: UnitCreate, current_user: dict = Depends(get_current_user)):
28
+ """Create a new unit"""
29
+ try:
30
+ result = db_manager.execute_query(
31
+ "units",
32
+ "insert",
33
+ data={"name": unit.name}
34
+ )
35
+ return result.data[0] if result.data else None
36
+ except Exception as e:
37
+ logger.error(f"Error creating unit: {e}")
38
+ raise HTTPException(status_code=500, detail="Error creating unit")
39
+
40
+
41
+ @router.put("/{unit_id}", response_model=UnitResponse)
42
+ async def update_unit(unit_id: int, unit: UnitCreate, current_user: dict = Depends(get_current_user)):
43
+ """Update a unit"""
44
+ try:
45
+ result = db_manager.execute_query(
46
+ "units",
47
+ "update",
48
+ data={"name": unit.name},
49
+ filters=[{"column": "id", "value": unit_id}]
50
+ )
51
+ if not result.data:
52
+ raise HTTPException(status_code=404, detail="Unit not found")
53
+ return result.data[0]
54
+ except HTTPException:
55
+ raise
56
+ except Exception as e:
57
+ logger.error(f"Error updating unit: {e}")
58
+ raise HTTPException(status_code=500, detail="Error updating unit")
59
+
60
+
61
+ @router.delete("/{unit_id}")
62
+ async def delete_unit(unit_id: int, current_user: dict = Depends(get_current_user)):
63
+ """Delete a unit"""
64
+ try:
65
+ db_manager.execute_query(
66
+ "units",
67
+ "delete",
68
+ filters=[{"column": "id", "value": unit_id}]
69
+ )
70
+ return {"message": "Unit deleted successfully"}
71
+ except Exception as e:
72
+ logger.error(f"Error deleting unit: {e}")
73
+ raise HTTPException(status_code=500, detail="Error deleting unit")
74
+
app/services/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Services package
2
+
app/services/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (152 Bytes). View file
 
app/services/__pycache__/solver_service.cpython-312.pyc ADDED
Binary file (4.53 kB). View file
 
app/services/solver_service.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Timetable solver service - adapts the existing solver logic for web API
3
+ """
4
+ import time
5
+ from typing import Optional, Dict, Any, List
6
+ from app.models.schemas import SolverResponse
7
+ from app.database import db_manager
8
+ import json
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Note: This is a simplified version. You'll need to adapt the actual solver
14
+ # from TKB_V1.2/core/solver_ortools.py to work in this async context
15
+ class SolverService:
16
+ """Service for solving timetable scheduling problems"""
17
+
18
+ async def solve(
19
+ self,
20
+ unit_id: int,
21
+ school_year: str,
22
+ mode: str = "ortools_balanced",
23
+ num_workers: int = 8,
24
+ time_limit: float = 300.0,
25
+ weights: Optional[Dict[str, float]] = None,
26
+ priority_teacher_ids: Optional[List[str]] = None,
27
+ phase: Optional[str] = None
28
+ ) -> SolverResponse:
29
+ """
30
+ Solve timetable scheduling problem
31
+
32
+ This is a placeholder that needs to be implemented with the actual
33
+ solver logic from TKB_V1.2/core/solver_ortools.py
34
+ """
35
+ start_time = time.time()
36
+
37
+ try:
38
+ # Load school data
39
+ school_data = await self._load_school_data(unit_id, school_year)
40
+
41
+ # TODO: Implement actual solver logic here
42
+ # This should call the OrToolsSolverWorker logic but adapted for async
43
+
44
+ # Placeholder response
45
+ execution_time = time.time() - start_time
46
+
47
+ return SolverResponse(
48
+ success=True,
49
+ timetable={},
50
+ objective_value=0.0,
51
+ unplaced_lessons=[],
52
+ message="Solver not yet fully implemented",
53
+ execution_time=execution_time
54
+ )
55
+
56
+ except Exception as e:
57
+ logger.error(f"Solver error: {e}")
58
+ execution_time = time.time() - start_time
59
+ return SolverResponse(
60
+ success=False,
61
+ message=f"Error: {str(e)}",
62
+ execution_time=execution_time
63
+ )
64
+
65
+ async def _load_school_data(self, unit_id: int, school_year: str) -> Dict[str, Any]:
66
+ """Load school data from database"""
67
+ try:
68
+ # Load teachers
69
+ teachers_result = db_manager.execute_query(
70
+ "teachers",
71
+ "select",
72
+ filters=[
73
+ {"column": "unit_id", "value": unit_id},
74
+ {"column": "school_year", "value": school_year}
75
+ ]
76
+ )
77
+
78
+ # Load subjects
79
+ subjects_result = db_manager.execute_query(
80
+ "subjects",
81
+ "select",
82
+ filters=[
83
+ {"column": "unit_id", "value": unit_id},
84
+ {"column": "school_year", "value": school_year}
85
+ ]
86
+ )
87
+
88
+ # Load classes
89
+ classes_result = db_manager.execute_query(
90
+ "classes",
91
+ "select",
92
+ filters=[
93
+ {"column": "unit_id", "value": unit_id},
94
+ {"column": "school_year", "value": school_year}
95
+ ]
96
+ )
97
+
98
+ # Parse JSON fields
99
+ teachers = []
100
+ for teacher in teachers_result.data:
101
+ teacher["subjects"] = json.loads(teacher.get("subjects") or "[]")
102
+ teacher["secondary_subjects"] = json.loads(teacher.get("secondary_subjects") or "[]")
103
+ teacher["busy_slots"] = json.loads(teacher.get("busy_slots") or "[]")
104
+ teachers.append(teacher)
105
+
106
+ subjects = []
107
+ for subject in subjects_result.data:
108
+ subject["periods_per_week"] = json.loads(subject.get("periods_per_week") or "{}")
109
+ subjects.append(subject)
110
+
111
+ classes = []
112
+ for cls in classes_result.data:
113
+ cls["fixed_off_slots"] = json.loads(cls.get("fixed_off_slots") or "[]")
114
+ classes.append(cls)
115
+
116
+ return {
117
+ "teachers": teachers,
118
+ "subjects": subjects,
119
+ "classes": classes
120
+ }
121
+ except Exception as e:
122
+ logger.error(f"Error loading school data: {e}")
123
+ raise
124
+
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ python-multipart==0.0.6
4
+ pydantic==2.5.0
5
+ pydantic-settings==2.1.0
6
+ supabase==2.0.3
7
+ postgrest==0.13.0
8
+ python-jose[cryptography]==3.3.0
9
+ passlib[bcrypt]==1.7.4
10
+ python-dotenv==1.0.0
11
+ ortools==9.9.3963
12
+ google-generativeai==0.3.2
13
+ httpx==0.25.2
14
+ python-dateutil==2.8.2
15
+