from fastapi import FastAPI, HTTPException, UploadFile, File, Query, Response from fastapi.responses import JSONResponse from typing import Dict, List, Optional import itertools import math app = FastAPI( title="Fake Evaluation Management API", version="0.1.0", openapi_url="/api/v1/openapi.json", docs_url="/api/v1/docs", ) # In-memory seed data evaluation_objects: List[Dict] = [ {"id": 1, "code": "CNTDG", "name": "Ca nhan tu danh gia", "status": "active"}, {"id": 2, "code": "QLTT", "name": "Quan ly truc tiep", "status": "active"}, {"id": 3, "code": "DN", "name": "Doanh nghiep", "status": "inactive"}, ] evaluation_object_counter = itertools.count(start=len(evaluation_objects) + 1) organization_tree: List[Dict] = [ { "id": 1, "name": "Ban Giam doc", "isExpanded": True, "children": [ { "id": 2, "name": "Phong Phat trien", "isExpanded": True, "children": [], "individuals": [ {"id": 101, "code": "EMP001", "name": "Tran Van Long", "selectedObjectId": 1}, {"id": 102, "code": "EMP002", "name": "Le Thi Mai", "selectedObjectId": 2}, ], } ], "individuals": [], } ] criteria_sets: List[Dict] = [ { "id": 1, "name": "Bo tieu chi nang luc chung", "applicableObjects": ["Ca nhan tu danh gia", "Quan ly truc tiep"], "applicationPeriods": [{"month": 1, "year": 2024}, {"month": 6, "year": 2024}], "criteriaTree": [ {"id": 1, "code": "NL01", "name": "Nang luc chuyen mon", "maxScore": 50, "weight": 0.5}, {"id": 2, "code": "NL02", "name": "Hieu biet san pham", "maxScore": 50, "weight": 0.5}, ], "classificationLevels": [ {"id": 1, "name": "Xuat sac", "fromScore": 90, "toScore": 100, "color": "green"}, {"id": 2, "name": "Hoan thanh tot", "fromScore": 75, "toScore": 89, "color": "blue"}, ], }, { "id": 2, "name": "Bo tieu chi ABC", "applicableObjects": ["Doanh nghiep"], "applicationPeriods": [{"month": 7, "year": 2024}], "criteriaTree": [], "classificationLevels": [], }, ] criteria_set_counter = itertools.count(start=len(criteria_sets) + 1) evaluation_flows: List[Dict] = [ { "id": 1, "code": "LDG-NVCT", "name": "Luong danh gia nhan vien chinh thuc", "applicableDepartments": ["Khoi Kinh doanh"], "status": "active", "roleHierarchy": [ { "id": 1, "name": "Nhan vien", "isExpanded": True, "children": [ {"id": 2, "name": "Truong phong", "isExpanded": False, "children": []}, ], } ], }, { "id": 2, "code": "LDG-TV", "name": "Luong danh gia thu viec", "applicableDepartments": ["Toan cong ty"], "status": "active", "roleHierarchy": [], }, ] evaluation_flow_counter = itertools.count(start=len(evaluation_flows) + 1) evaluations: List[Dict] = [ { "id": 1, "criteriaSetId": 1, "fullName": "Nguyen Van A", "evaluationPeriod": "Thang 6/2024", "department": "Phong Phat trien", "selfScore": None, "managerScore": None, "status": "Draft", "statusColor": "gray", "period": "Thang 6", "year": 2024, "tab": "self", "scores": {}, "managerScores": {}, }, { "id": 2, "criteriaSetId": 1, "fullName": "Tran Thi B", "evaluationPeriod": "Thang 6/2024", "department": "Khoi Kinh doanh", "selfScore": 90, "managerScore": 92, "status": "Approved", "statusColor": "green", "period": "Thang 6", "year": 2024, "tab": "manager", "scores": {"2": 20}, "managerScores": {"2": 21}, }, { "id": 3, "criteriaSetId": 2, "fullName": "Doanh nghiep X", "evaluationPeriod": "Thang 7/2024", "department": "Phong Bao tri", "selfScore": None, "managerScore": None, "status": "Draft", "statusColor": "gray", "period": "Thang 7", "year": 2024, "tab": "results", "scores": {}, "managerScores": {}, }, ] evaluation_counter = itertools.count(start=len(evaluations) + 1) evaluation_chats: Dict[int, List[Dict]] = { 1: [ { "id": 1, "sender": "Lieu Huu Hoang - Pho Truong phong", "avatar": "person", "message": "Em xem lai muc tieu nang luc chuyen mon.", "timestamp": "1 ngay truoc", "replyToMessageId": None, } ] } report_types: List[Dict] = [ {"id": 1, "code": "RPT001", "name": "Bao cao thang", "status": "active"}, {"id": 2, "code": "RPT002", "name": "Bao cao phong ban", "status": "inactive"}, ] report_type_counter = itertools.count(start=len(report_types) + 1) reports_payload: List[Dict] = [ { "stt": 1, "canBo": "Pham Van Tien", "donVi": "Phong 5", "diemCaNhan": 100, "diemDonVi": 100, "phanLoaiCaNhan": "Hoan thanh xuat sac nhiem vu", "phanLoaiDonVi": "Hoan thanh xuat sac nhiem vu", } ] reports_info = { "title": "DANH GIA, CHAM DIEM VA PHAN LOAI", "subtitle": "Ap dung doi voi can bo, chien si", "date": "Thang 07 Nam 2025", } dashboard_stats = {"totalEvaluations": 125, "pendingEvaluations": 12} managers = [ {"id": 1, "name": "Lieu Huu Hoang", "title": "Pho Truong phong"}, {"id": 2, "name": "Tran Minh Tuan", "title": "Truong phong"}, ] departments = [ { "id": 1, "code": "CTY", "name": "Toan cong ty", "isExpanded": True, "selection": "unchecked", "children": [ {"id": 2, "code": "KD", "name": "Khoi Kinh doanh", "isExpanded": False, "selection": "partial", "children": []}, {"id": 3, "code": "PT", "name": "Phong Phat trien", "isExpanded": False, "selection": "checked", "children": []}, ], } ] def paginate_list(items: List[Dict], page: int, limit: int) -> Dict: start = max(page - 1, 0) * limit end = start + limit total_items = len(items) total_pages = max(1, math.ceil(total_items / limit)) if total_items else 0 return { "data": items[start:end], "pagination": { "currentPage": page, "totalPages": total_pages, "totalItems": total_items, "itemsPerPage": limit, }, } def find_by_id(items: List[Dict], item_id: int) -> Optional[Dict]: return next((item for item in items if item.get("id") == item_id), None) @app.get("/") def root() -> Dict: return {"message": "Fake Evaluation Management API. Explore /api/v1/docs"} # Authentication @app.post("/api/v1/auth/login") def login(credentials: Dict) -> JSONResponse: username = credentials.get("username") password = credentials.get("password") if username == "admin" and password == "admin": return JSONResponse( { "token": "fake-jwt-token", "user": {"id": 1, "username": "admin", "fullName": "Administrator"}, } ) return JSONResponse(status_code=401, content={"error": "Invalid username or password"}) @app.post("/api/v1/auth/logout") def logout() -> Response: return Response(status_code=204) # Evaluation Objects @app.get("/api/v1/evaluation-objects") def list_evaluation_objects( page: int = 1, limit: int = 5, search: Optional[str] = None, ): filtered = evaluation_objects if search: term = search.lower() filtered = [ item for item in evaluation_objects if term in item["code"].lower() or term in item["name"].lower() ] return paginate_list(filtered, page, limit) @app.post("/api/v1/evaluation-objects") def create_evaluation_object(payload: Dict): existing_code = next((o for o in evaluation_objects if o["code"] == payload.get("code")), None) if existing_code: raise HTTPException(status_code=400, detail="Code must be unique") new_obj = { "id": next(evaluation_object_counter), "code": payload.get("code"), "name": payload.get("name"), "status": payload.get("status", "inactive"), } evaluation_objects.append(new_obj) return new_obj @app.get("/api/v1/evaluation-objects/{item_id}") def get_evaluation_object(item_id: int): obj = find_by_id(evaluation_objects, item_id) if not obj: raise HTTPException(status_code=404, detail="Evaluation object not found") return obj @app.put("/api/v1/evaluation-objects/{item_id}") def update_evaluation_object(item_id: int, payload: Dict): obj = find_by_id(evaluation_objects, item_id) if not obj: raise HTTPException(status_code=404, detail="Evaluation object not found") obj.update({"name": payload.get("name", obj["name"]), "status": payload.get("status", obj["status"])}) return obj @app.delete("/api/v1/evaluation-objects/{item_id}") def delete_evaluation_object(item_id: int) -> Response: obj = find_by_id(evaluation_objects, item_id) if not obj: raise HTTPException(status_code=404, detail="Evaluation object not found") evaluation_objects.remove(obj) return Response(status_code=204) @app.get("/api/v1/organization-tree") def get_organization_tree(): return organization_tree @app.put("/api/v1/organization-tree/roles") def save_organization_roles(payload: List[Dict]) -> Response: return Response(status_code=204) # Criteria Sets @app.get("/api/v1/criteria-sets") def list_criteria_sets( page: int = 1, limit: int = 5, search: Optional[str] = None, object: Optional[str] = Query(None, alias="object"), year: Optional[int] = None, month: Optional[int] = None, ): filtered = criteria_sets if search: term = search.lower() filtered = [c for c in filtered if term in c["name"].lower()] if object: term = object.lower() filtered = [c for c in filtered if any(term in obj.lower() for obj in c.get("applicableObjects", []))] if year: filtered = [ c for c in filtered if any(period.get("year") == year for period in c.get("applicationPeriods", [])) ] if month: filtered = [ c for c in filtered if any(period.get("month") == month for period in c.get("applicationPeriods", [])) ] return paginate_list(filtered, page, limit) @app.post("/api/v1/criteria-sets") def create_criteria_set(payload: Dict): new_set = payload.copy() new_set["id"] = next(criteria_set_counter) new_set.setdefault("criteriaTree", []) new_set.setdefault("classificationLevels", []) new_set.setdefault("applicationPeriods", []) new_set.setdefault("applicableObjects", []) criteria_sets.append(new_set) return new_set @app.get("/api/v1/criteria-sets/{item_id}") def get_criteria_set(item_id: int): result = find_by_id(criteria_sets, item_id) if not result: raise HTTPException(status_code=404, detail="Criteria set not found") return result @app.put("/api/v1/criteria-sets/{item_id}") def update_criteria_set(item_id: int, payload: Dict): result = find_by_id(criteria_sets, item_id) if not result: raise HTTPException(status_code=404, detail="Criteria set not found") result.update(payload) return result @app.delete("/api/v1/criteria-sets") def delete_criteria_sets(payload: Dict) -> Response: ids = payload.get("ids", []) for item_id in ids: existing = find_by_id(criteria_sets, item_id) if existing: criteria_sets.remove(existing) return Response(status_code=204) @app.get("/api/v1/criteria-sets/export") def export_criteria_sets(format: str = Query(..., pattern="^(pdf|xlsx)$")): return {"message": f"Generated {format} export for criteria sets"} @app.get("/api/v1/criteria-sets/template") def download_criteria_template(): return {"template": "criteria-template.xlsx", "message": "Template generated"} @app.post("/api/v1/criteria-sets/import") def import_criteria_set(file: UploadFile = File(...)): return {"message": "Import successful", "filename": file.filename} # Evaluation Flows @app.get("/api/v1/evaluation-flows") def list_evaluation_flows( page: int = 1, limit: int = 5, search: Optional[str] = None, ): filtered = evaluation_flows if search: term = search.lower() filtered = [ f for f in evaluation_flows if term in f["code"].lower() or term in f["name"].lower() or any(term in dep.lower() for dep in f.get("applicableDepartments", [])) ] return paginate_list(filtered, page, limit) @app.post("/api/v1/evaluation-flows") def create_evaluation_flow(payload: Dict): existing_code = next((f for f in evaluation_flows if f["code"] == payload.get("code")), None) if existing_code: raise HTTPException(status_code=400, detail="Code must be unique") new_flow = { "id": next(evaluation_flow_counter), "code": payload.get("code"), "name": payload.get("name"), "applicableDepartments": payload.get("applicableDepartments", []), "status": payload.get("status", "inactive"), "roleHierarchy": payload.get("roleHierarchy", []), } evaluation_flows.append(new_flow) return new_flow @app.get("/api/v1/evaluation-flows/{item_id}") def get_evaluation_flow(item_id: int): flow = find_by_id(evaluation_flows, item_id) if not flow: raise HTTPException(status_code=404, detail="Evaluation flow not found") return flow @app.put("/api/v1/evaluation-flows/{item_id}") def update_evaluation_flow(item_id: int, payload: Dict): flow = find_by_id(evaluation_flows, item_id) if not flow: raise HTTPException(status_code=404, detail="Evaluation flow not found") if "code" in payload and payload["code"] != flow["code"]: raise HTTPException(status_code=400, detail="Code is immutable") flow.update({ "name": payload.get("name", flow["name"]), "applicableDepartments": payload.get("applicableDepartments", flow.get("applicableDepartments", [])), "status": payload.get("status", flow["status"]), "roleHierarchy": payload.get("roleHierarchy", flow.get("roleHierarchy", [])), }) return flow @app.delete("/api/v1/evaluation-flows") def delete_evaluation_flows(payload: Dict) -> Response: ids = payload.get("ids", []) for item_id in ids: existing = find_by_id(evaluation_flows, item_id) if existing: evaluation_flows.remove(existing) return Response(status_code=204) # Evaluations @app.get("/api/v1/evaluations") def list_evaluations( tab: str = Query(..., description="self | manager | results"), page: int = 1, limit: int = 5, search: Optional[str] = None, status: Optional[str] = None, period: Optional[str] = None, year: Optional[int] = None, ): filtered = [ev for ev in evaluations if ev.get("tab") == tab] if search: term = search.lower() filtered = [ev for ev in filtered if term in ev["fullName"].lower() or term in ev["department"].lower()] if status: filtered = [ev for ev in filtered if ev.get("status", "").lower() == status.lower()] if period: filtered = [ev for ev in filtered if ev.get("period") == period] if year: filtered = [ev for ev in filtered if ev.get("year") == year] return paginate_list(filtered, page, limit) @app.post("/api/v1/evaluations") def create_evaluation(payload: Dict): criteria_set_id = payload.get("criteriaSetId") period = payload.get("period") year = payload.get("year") new_eval = { "id": next(evaluation_counter), "criteriaSetId": criteria_set_id, "fullName": payload.get("fullName", "Nguoi dung moi"), "evaluationPeriod": f"{period}/{year}", "department": payload.get("department", "Phong moi"), "selfScore": None, "managerScore": None, "status": "Draft", "statusColor": "gray", "period": period, "year": year, "tab": payload.get("tab", "self"), "scores": payload.get("scores", {}), "managerScores": payload.get("managerScores", {}), } evaluations.append(new_eval) return new_eval @app.get("/api/v1/evaluations/{item_id}") def get_evaluation(item_id: int): evaluation = find_by_id(evaluations, item_id) if not evaluation: raise HTTPException(status_code=404, detail="Evaluation not found") return evaluation @app.put("/api/v1/evaluations/{item_id}") def update_evaluation(item_id: int, payload: Dict): evaluation = find_by_id(evaluations, item_id) if not evaluation: raise HTTPException(status_code=404, detail="Evaluation not found") evaluation.update({ "evaluationPeriod": payload.get("evaluationPeriod", evaluation["evaluationPeriod"]), "scores": payload.get("scores", evaluation.get("scores", {})), "managerScores": payload.get("managerScores", evaluation.get("managerScores", {})), "status": payload.get("status", evaluation["status"]), }) return evaluation @app.delete("/api/v1/evaluations") def delete_evaluations(payload: Dict) -> Response: ids = payload.get("ids", []) for item_id in ids: existing = find_by_id(evaluations, item_id) if existing: evaluations.remove(existing) return Response(status_code=204) @app.post("/api/v1/evaluations/{item_id}/submit") def submit_evaluation(item_id: int, payload: Dict): evaluation = find_by_id(evaluations, item_id) if not evaluation: raise HTTPException(status_code=404, detail="Evaluation not found") evaluation["status"] = "Submitted" evaluation["managerId"] = payload.get("managerId") evaluation["comments"] = payload.get("comments") return evaluation @app.post("/api/v1/evaluations/{item_id}/withdraw") def withdraw_evaluation(item_id: int): evaluation = find_by_id(evaluations, item_id) if not evaluation: raise HTTPException(status_code=404, detail="Evaluation not found") evaluation["status"] = "Draft" return evaluation @app.post("/api/v1/evaluations/{item_id}/return") def return_evaluation(item_id: int): evaluation = find_by_id(evaluations, item_id) if not evaluation: raise HTTPException(status_code=404, detail="Evaluation not found") evaluation["status"] = "Draft" return evaluation @app.get("/api/v1/evaluations/{item_id}/chat") def list_evaluation_chat(item_id: int): return evaluation_chats.get(item_id, []) @app.post("/api/v1/evaluations/{item_id}/chat") def post_evaluation_chat(item_id: int, payload: Dict): chat_thread = evaluation_chats.setdefault(item_id, []) new_message = { "id": len(chat_thread) + 1, "sender": payload.get("sender", "Nguoi dung"), "avatar": payload.get("avatar", "person"), "message": payload.get("message"), "timestamp": payload.get("timestamp", "vua xong"), "replyToMessageId": payload.get("replyToMessageId"), "attachments": payload.get("attachments", []), } chat_thread.append(new_message) return new_message @app.get("/api/v1/evaluations/export") def export_evaluations(format: str = Query(..., pattern="^(pdf|xlsx)$")): return {"message": f"Generated {format} export for evaluations"} # Reports @app.get("/api/v1/reports") def get_reports( reportType: str = Query(..., alias="reportType"), period: int = Query(..., ge=1, le=12), year: int = Query(..., ge=2000), search: Optional[str] = None, ): filtered = reports_payload if search: term = search.lower() filtered = [row for row in reports_payload if term in row["canBo"].lower() or term in row["donVi"].lower()] return {"data": filtered, "reportInfo": reports_info} @app.get("/api/v1/reports/export") def export_reports( reportType: str = Query(..., alias="reportType"), period: int = Query(..., ge=1, le=12), year: int = Query(..., ge=2000), format: str = Query(..., pattern="^(pdf|xlsx|docx)$"), ): return {"message": f"Generated {format} export for {reportType} {period}/{year}"} # Report Types @app.get("/api/v1/report-types") def list_report_types(page: int = 1, limit: int = 5, search: Optional[str] = None): filtered = report_types if search: term = search.lower() filtered = [r for r in report_types if term in r["code"].lower() or term in r["name"].lower()] return paginate_list(filtered, page, limit) @app.post("/api/v1/report-types") def create_report_type(payload: Dict): new_type = { "id": next(report_type_counter), "code": payload.get("code"), "name": payload.get("name"), "status": payload.get("status", "inactive"), } report_types.append(new_type) return new_type @app.put("/api/v1/report-types/{item_id}") def update_report_type(item_id: int, payload: Dict): report_type = find_by_id(report_types, item_id) if not report_type: raise HTTPException(status_code=404, detail="Report type not found") report_type.update({ "name": payload.get("name", report_type["name"]), "status": payload.get("status", report_type["status"]), }) return report_type @app.delete("/api/v1/report-types/{item_id}") def delete_report_type(item_id: int) -> Response: report_type = find_by_id(report_types, item_id) if not report_type: raise HTTPException(status_code=404, detail="Report type not found") report_types.remove(report_type) return Response(status_code=204) # Dashboard @app.get("/api/v1/dashboard/stats") def get_dashboard_stats(): return dashboard_stats @app.get("/api/v1/dashboard/charts") def get_dashboard_charts(year: int = Query(..., ge=2000), criteria: str = Query(...)): monthly_breakdown = [{"month": month, "score": 80 + (month % 5)} for month in range(1, 13)] pie_data = [ {"label": "Xuat sac", "value": 40}, {"label": "Tot", "value": 35}, {"label": "Trung binh", "value": 20}, {"label": "Can cai thien", "value": 5}, ] return {"monthlyBreakdown": monthly_breakdown, "pieData": pie_data} @app.get("/api/v1/dashboard/filters") def get_dashboard_filters(): return {"years": [2024, 2023], "criteria": ["Bo tieu chi nang luc chung", "Bo tieu chi ABC"]} # Users and Departments @app.get("/api/v1/users/managers") def get_managers(): return managers @app.get("/api/v1/departments") def get_departments(): return departments @app.get("/api/v1/dashboard") def dashboard_root(): return {"message": "Dashboard data ready"}