huy00001 commited on
Commit
400be22
·
1 Parent(s): a8c752c

Add application file

Browse files
Files changed (3) hide show
  1. Dockerfile +14 -0
  2. main.py +743 -0
  3. requirements.txt +3 -0
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install deps early for layer caching
6
+ COPY requirements.txt ./
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy source
10
+ COPY main.py ./
11
+
12
+ EXPOSE 8000
13
+
14
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
main.py ADDED
@@ -0,0 +1,743 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, UploadFile, File, Query, Response
2
+ from fastapi.responses import JSONResponse
3
+ from typing import Dict, List, Optional
4
+ import itertools
5
+ import math
6
+
7
+ app = FastAPI(
8
+ title="Fake Evaluation Management API",
9
+ version="0.1.0",
10
+ openapi_url="/api/v1/openapi.json",
11
+ docs_url="/api/v1/docs",
12
+ )
13
+
14
+ # In-memory seed data
15
+
16
+
17
+ evaluation_objects: List[Dict] = [
18
+ {"id": 1, "code": "CNTDG", "name": "Ca nhan tu danh gia", "status": "active"},
19
+ {"id": 2, "code": "QLTT", "name": "Quan ly truc tiep", "status": "active"},
20
+ {"id": 3, "code": "DN", "name": "Doanh nghiep", "status": "inactive"},
21
+ ]
22
+
23
+ evaluation_object_counter = itertools.count(start=len(evaluation_objects) + 1)
24
+
25
+ organization_tree: List[Dict] = [
26
+ {
27
+ "id": 1,
28
+ "name": "Ban Giam doc",
29
+ "isExpanded": True,
30
+ "children": [
31
+ {
32
+ "id": 2,
33
+ "name": "Phong Phat trien",
34
+ "isExpanded": True,
35
+ "children": [],
36
+ "individuals": [
37
+ {"id": 101, "code": "EMP001", "name": "Tran Van Long", "selectedObjectId": 1},
38
+ {"id": 102, "code": "EMP002", "name": "Le Thi Mai", "selectedObjectId": 2},
39
+ ],
40
+ }
41
+ ],
42
+ "individuals": [],
43
+ }
44
+ ]
45
+
46
+ criteria_sets: List[Dict] = [
47
+ {
48
+ "id": 1,
49
+ "name": "Bo tieu chi nang luc chung",
50
+ "applicableObjects": ["Ca nhan tu danh gia", "Quan ly truc tiep"],
51
+ "applicationPeriods": [{"month": 1, "year": 2024}, {"month": 6, "year": 2024}],
52
+ "criteriaTree": [
53
+ {"id": 1, "code": "NL01", "name": "Nang luc chuyen mon", "maxScore": 50, "weight": 0.5},
54
+ {"id": 2, "code": "NL02", "name": "Hieu biet san pham", "maxScore": 50, "weight": 0.5},
55
+ ],
56
+ "classificationLevels": [
57
+ {"id": 1, "name": "Xuat sac", "fromScore": 90, "toScore": 100, "color": "green"},
58
+ {"id": 2, "name": "Hoan thanh tot", "fromScore": 75, "toScore": 89, "color": "blue"},
59
+ ],
60
+ },
61
+ {
62
+ "id": 2,
63
+ "name": "Bo tieu chi ABC",
64
+ "applicableObjects": ["Doanh nghiep"],
65
+ "applicationPeriods": [{"month": 7, "year": 2024}],
66
+ "criteriaTree": [],
67
+ "classificationLevels": [],
68
+ },
69
+ ]
70
+
71
+ criteria_set_counter = itertools.count(start=len(criteria_sets) + 1)
72
+
73
+ evaluation_flows: List[Dict] = [
74
+ {
75
+ "id": 1,
76
+ "code": "LDG-NVCT",
77
+ "name": "Luong danh gia nhan vien chinh thuc",
78
+ "applicableDepartments": ["Khoi Kinh doanh"],
79
+ "status": "active",
80
+ "roleHierarchy": [
81
+ {
82
+ "id": 1,
83
+ "name": "Nhan vien",
84
+ "isExpanded": True,
85
+ "children": [
86
+ {"id": 2, "name": "Truong phong", "isExpanded": False, "children": []},
87
+ ],
88
+ }
89
+ ],
90
+ },
91
+ {
92
+ "id": 2,
93
+ "code": "LDG-TV",
94
+ "name": "Luong danh gia thu viec",
95
+ "applicableDepartments": ["Toan cong ty"],
96
+ "status": "active",
97
+ "roleHierarchy": [],
98
+ },
99
+ ]
100
+
101
+ evaluation_flow_counter = itertools.count(start=len(evaluation_flows) + 1)
102
+
103
+ evaluations: List[Dict] = [
104
+ {
105
+ "id": 1,
106
+ "criteriaSetId": 1,
107
+ "fullName": "Nguyen Van A",
108
+ "evaluationPeriod": "Thang 6/2024",
109
+ "department": "Phong Phat trien",
110
+ "selfScore": None,
111
+ "managerScore": None,
112
+ "status": "Draft",
113
+ "statusColor": "gray",
114
+ "period": "Thang 6",
115
+ "year": 2024,
116
+ "tab": "self",
117
+ "scores": {},
118
+ "managerScores": {},
119
+ },
120
+ {
121
+ "id": 2,
122
+ "criteriaSetId": 1,
123
+ "fullName": "Tran Thi B",
124
+ "evaluationPeriod": "Thang 6/2024",
125
+ "department": "Khoi Kinh doanh",
126
+ "selfScore": 90,
127
+ "managerScore": 92,
128
+ "status": "Approved",
129
+ "statusColor": "green",
130
+ "period": "Thang 6",
131
+ "year": 2024,
132
+ "tab": "manager",
133
+ "scores": {"2": 20},
134
+ "managerScores": {"2": 21},
135
+ },
136
+ {
137
+ "id": 3,
138
+ "criteriaSetId": 2,
139
+ "fullName": "Doanh nghiep X",
140
+ "evaluationPeriod": "Thang 7/2024",
141
+ "department": "Phong Bao tri",
142
+ "selfScore": None,
143
+ "managerScore": None,
144
+ "status": "Draft",
145
+ "statusColor": "gray",
146
+ "period": "Thang 7",
147
+ "year": 2024,
148
+ "tab": "results",
149
+ "scores": {},
150
+ "managerScores": {},
151
+ },
152
+ ]
153
+
154
+ evaluation_counter = itertools.count(start=len(evaluations) + 1)
155
+
156
+ evaluation_chats: Dict[int, List[Dict]] = {
157
+ 1: [
158
+ {
159
+ "id": 1,
160
+ "sender": "Lieu Huu Hoang - Pho Truong phong",
161
+ "avatar": "person",
162
+ "message": "Em xem lai muc tieu nang luc chuyen mon.",
163
+ "timestamp": "1 ngay truoc",
164
+ "replyToMessageId": None,
165
+ }
166
+ ]
167
+ }
168
+
169
+ report_types: List[Dict] = [
170
+ {"id": 1, "code": "RPT001", "name": "Bao cao thang", "status": "active"},
171
+ {"id": 2, "code": "RPT002", "name": "Bao cao phong ban", "status": "inactive"},
172
+ ]
173
+
174
+ report_type_counter = itertools.count(start=len(report_types) + 1)
175
+
176
+ reports_payload: List[Dict] = [
177
+ {
178
+ "stt": 1,
179
+ "canBo": "Pham Van Tien",
180
+ "donVi": "Phong 5",
181
+ "diemCaNhan": 100,
182
+ "diemDonVi": 100,
183
+ "phanLoaiCaNhan": "Hoan thanh xuat sac nhiem vu",
184
+ "phanLoaiDonVi": "Hoan thanh xuat sac nhiem vu",
185
+ }
186
+ ]
187
+
188
+ reports_info = {
189
+ "title": "DANH GIA, CHAM DIEM VA PHAN LOAI",
190
+ "subtitle": "Ap dung doi voi can bo, chien si",
191
+ "date": "Thang 07 Nam 2025",
192
+ }
193
+
194
+ dashboard_stats = {"totalEvaluations": 125, "pendingEvaluations": 12}
195
+
196
+ managers = [
197
+ {"id": 1, "name": "Lieu Huu Hoang", "title": "Pho Truong phong"},
198
+ {"id": 2, "name": "Tran Minh Tuan", "title": "Truong phong"},
199
+ ]
200
+
201
+ departments = [
202
+ {
203
+ "id": 1,
204
+ "code": "CTY",
205
+ "name": "Toan cong ty",
206
+ "isExpanded": True,
207
+ "selection": "unchecked",
208
+ "children": [
209
+ {"id": 2, "code": "KD", "name": "Khoi Kinh doanh", "isExpanded": False, "selection": "partial", "children": []},
210
+ {"id": 3, "code": "PT", "name": "Phong Phat trien", "isExpanded": False, "selection": "checked", "children": []},
211
+ ],
212
+ }
213
+ ]
214
+
215
+
216
+ def paginate_list(items: List[Dict], page: int, limit: int) -> Dict:
217
+ start = max(page - 1, 0) * limit
218
+ end = start + limit
219
+ total_items = len(items)
220
+ total_pages = max(1, math.ceil(total_items / limit)) if total_items else 0
221
+ return {
222
+ "data": items[start:end],
223
+ "pagination": {
224
+ "currentPage": page,
225
+ "totalPages": total_pages,
226
+ "totalItems": total_items,
227
+ "itemsPerPage": limit,
228
+ },
229
+ }
230
+
231
+
232
+ def find_by_id(items: List[Dict], item_id: int) -> Optional[Dict]:
233
+ return next((item for item in items if item.get("id") == item_id), None)
234
+
235
+
236
+ @app.get("/")
237
+ def root() -> Dict:
238
+ return {"message": "Fake Evaluation Management API. Explore /api/v1/docs"}
239
+
240
+
241
+ # Authentication
242
+
243
+
244
+ @app.post("/api/v1/auth/login")
245
+ def login(credentials: Dict) -> JSONResponse:
246
+ username = credentials.get("username")
247
+ password = credentials.get("password")
248
+ if username == "admin" and password == "admin":
249
+ return JSONResponse(
250
+ {
251
+ "token": "fake-jwt-token",
252
+ "user": {"id": 1, "username": "admin", "fullName": "Administrator"},
253
+ }
254
+ )
255
+ return JSONResponse(status_code=401, content={"error": "Invalid username or password"})
256
+
257
+
258
+ @app.post("/api/v1/auth/logout")
259
+ def logout() -> Response:
260
+ return Response(status_code=204)
261
+
262
+
263
+ # Evaluation Objects
264
+
265
+
266
+ @app.get("/api/v1/evaluation-objects")
267
+ def list_evaluation_objects(
268
+ page: int = 1,
269
+ limit: int = 5,
270
+ search: Optional[str] = None,
271
+ ):
272
+ filtered = evaluation_objects
273
+ if search:
274
+ term = search.lower()
275
+ filtered = [
276
+ item
277
+ for item in evaluation_objects
278
+ if term in item["code"].lower() or term in item["name"].lower()
279
+ ]
280
+ return paginate_list(filtered, page, limit)
281
+
282
+
283
+ @app.post("/api/v1/evaluation-objects")
284
+ def create_evaluation_object(payload: Dict):
285
+ existing_code = next((o for o in evaluation_objects if o["code"] == payload.get("code")), None)
286
+ if existing_code:
287
+ raise HTTPException(status_code=400, detail="Code must be unique")
288
+ new_obj = {
289
+ "id": next(evaluation_object_counter),
290
+ "code": payload.get("code"),
291
+ "name": payload.get("name"),
292
+ "status": payload.get("status", "inactive"),
293
+ }
294
+ evaluation_objects.append(new_obj)
295
+ return new_obj
296
+
297
+
298
+ @app.get("/api/v1/evaluation-objects/{item_id}")
299
+ def get_evaluation_object(item_id: int):
300
+ obj = find_by_id(evaluation_objects, item_id)
301
+ if not obj:
302
+ raise HTTPException(status_code=404, detail="Evaluation object not found")
303
+ return obj
304
+
305
+
306
+ @app.put("/api/v1/evaluation-objects/{item_id}")
307
+ def update_evaluation_object(item_id: int, payload: Dict):
308
+ obj = find_by_id(evaluation_objects, item_id)
309
+ if not obj:
310
+ raise HTTPException(status_code=404, detail="Evaluation object not found")
311
+ obj.update({"name": payload.get("name", obj["name"]), "status": payload.get("status", obj["status"])})
312
+ return obj
313
+
314
+
315
+ @app.delete("/api/v1/evaluation-objects/{item_id}")
316
+ def delete_evaluation_object(item_id: int) -> Response:
317
+ obj = find_by_id(evaluation_objects, item_id)
318
+ if not obj:
319
+ raise HTTPException(status_code=404, detail="Evaluation object not found")
320
+ evaluation_objects.remove(obj)
321
+ return Response(status_code=204)
322
+
323
+
324
+ @app.get("/api/v1/organization-tree")
325
+ def get_organization_tree():
326
+ return organization_tree
327
+
328
+
329
+ @app.put("/api/v1/organization-tree/roles")
330
+ def save_organization_roles(payload: List[Dict]) -> Response:
331
+ return Response(status_code=204)
332
+
333
+
334
+ # Criteria Sets
335
+
336
+
337
+ @app.get("/api/v1/criteria-sets")
338
+ def list_criteria_sets(
339
+ page: int = 1,
340
+ limit: int = 5,
341
+ search: Optional[str] = None,
342
+ object: Optional[str] = Query(None, alias="object"),
343
+ year: Optional[int] = None,
344
+ month: Optional[int] = None,
345
+ ):
346
+ filtered = criteria_sets
347
+ if search:
348
+ term = search.lower()
349
+ filtered = [c for c in filtered if term in c["name"].lower()]
350
+ if object:
351
+ term = object.lower()
352
+ filtered = [c for c in filtered if any(term in obj.lower() for obj in c.get("applicableObjects", []))]
353
+ if year:
354
+ filtered = [
355
+ c
356
+ for c in filtered
357
+ if any(period.get("year") == year for period in c.get("applicationPeriods", []))
358
+ ]
359
+ if month:
360
+ filtered = [
361
+ c
362
+ for c in filtered
363
+ if any(period.get("month") == month for period in c.get("applicationPeriods", []))
364
+ ]
365
+ return paginate_list(filtered, page, limit)
366
+
367
+
368
+ @app.post("/api/v1/criteria-sets")
369
+ def create_criteria_set(payload: Dict):
370
+ new_set = payload.copy()
371
+ new_set["id"] = next(criteria_set_counter)
372
+ new_set.setdefault("criteriaTree", [])
373
+ new_set.setdefault("classificationLevels", [])
374
+ new_set.setdefault("applicationPeriods", [])
375
+ new_set.setdefault("applicableObjects", [])
376
+ criteria_sets.append(new_set)
377
+ return new_set
378
+
379
+
380
+ @app.get("/api/v1/criteria-sets/{item_id}")
381
+ def get_criteria_set(item_id: int):
382
+ result = find_by_id(criteria_sets, item_id)
383
+ if not result:
384
+ raise HTTPException(status_code=404, detail="Criteria set not found")
385
+ return result
386
+
387
+
388
+ @app.put("/api/v1/criteria-sets/{item_id}")
389
+ def update_criteria_set(item_id: int, payload: Dict):
390
+ result = find_by_id(criteria_sets, item_id)
391
+ if not result:
392
+ raise HTTPException(status_code=404, detail="Criteria set not found")
393
+ result.update(payload)
394
+ return result
395
+
396
+
397
+ @app.delete("/api/v1/criteria-sets")
398
+ def delete_criteria_sets(payload: Dict) -> Response:
399
+ ids = payload.get("ids", [])
400
+ for item_id in ids:
401
+ existing = find_by_id(criteria_sets, item_id)
402
+ if existing:
403
+ criteria_sets.remove(existing)
404
+ return Response(status_code=204)
405
+
406
+
407
+ @app.get("/api/v1/criteria-sets/export")
408
+ def export_criteria_sets(format: str = Query(..., regex="^(pdf|xlsx)$")):
409
+ return {"message": f"Generated {format} export for criteria sets"}
410
+
411
+
412
+ @app.get("/api/v1/criteria-sets/template")
413
+ def download_criteria_template():
414
+ return {"template": "criteria-template.xlsx", "message": "Template generated"}
415
+
416
+
417
+ @app.post("/api/v1/criteria-sets/import")
418
+ def import_criteria_set(file: UploadFile = File(...)):
419
+ return {"message": "Import successful", "filename": file.filename}
420
+
421
+
422
+ # Evaluation Flows
423
+
424
+
425
+ @app.get("/api/v1/evaluation-flows")
426
+ def list_evaluation_flows(
427
+ page: int = 1,
428
+ limit: int = 5,
429
+ search: Optional[str] = None,
430
+ ):
431
+ filtered = evaluation_flows
432
+ if search:
433
+ term = search.lower()
434
+ filtered = [
435
+ f
436
+ for f in evaluation_flows
437
+ if term in f["code"].lower() or term in f["name"].lower() or any(term in dep.lower() for dep in f.get("applicableDepartments", []))
438
+ ]
439
+ return paginate_list(filtered, page, limit)
440
+
441
+
442
+ @app.post("/api/v1/evaluation-flows")
443
+ def create_evaluation_flow(payload: Dict):
444
+ existing_code = next((f for f in evaluation_flows if f["code"] == payload.get("code")), None)
445
+ if existing_code:
446
+ raise HTTPException(status_code=400, detail="Code must be unique")
447
+ new_flow = {
448
+ "id": next(evaluation_flow_counter),
449
+ "code": payload.get("code"),
450
+ "name": payload.get("name"),
451
+ "applicableDepartments": payload.get("applicableDepartments", []),
452
+ "status": payload.get("status", "inactive"),
453
+ "roleHierarchy": payload.get("roleHierarchy", []),
454
+ }
455
+ evaluation_flows.append(new_flow)
456
+ return new_flow
457
+
458
+
459
+ @app.get("/api/v1/evaluation-flows/{item_id}")
460
+ def get_evaluation_flow(item_id: int):
461
+ flow = find_by_id(evaluation_flows, item_id)
462
+ if not flow:
463
+ raise HTTPException(status_code=404, detail="Evaluation flow not found")
464
+ return flow
465
+
466
+
467
+ @app.put("/api/v1/evaluation-flows/{item_id}")
468
+ def update_evaluation_flow(item_id: int, payload: Dict):
469
+ flow = find_by_id(evaluation_flows, item_id)
470
+ if not flow:
471
+ raise HTTPException(status_code=404, detail="Evaluation flow not found")
472
+ if "code" in payload and payload["code"] != flow["code"]:
473
+ raise HTTPException(status_code=400, detail="Code is immutable")
474
+ flow.update({
475
+ "name": payload.get("name", flow["name"]),
476
+ "applicableDepartments": payload.get("applicableDepartments", flow.get("applicableDepartments", [])),
477
+ "status": payload.get("status", flow["status"]),
478
+ "roleHierarchy": payload.get("roleHierarchy", flow.get("roleHierarchy", [])),
479
+ })
480
+ return flow
481
+
482
+
483
+ @app.delete("/api/v1/evaluation-flows")
484
+ def delete_evaluation_flows(payload: Dict) -> Response:
485
+ ids = payload.get("ids", [])
486
+ for item_id in ids:
487
+ existing = find_by_id(evaluation_flows, item_id)
488
+ if existing:
489
+ evaluation_flows.remove(existing)
490
+ return Response(status_code=204)
491
+
492
+
493
+ # Evaluations
494
+
495
+
496
+ @app.get("/api/v1/evaluations")
497
+ def list_evaluations(
498
+ tab: str = Query(..., description="self | manager | results"),
499
+ page: int = 1,
500
+ limit: int = 5,
501
+ search: Optional[str] = None,
502
+ status: Optional[str] = None,
503
+ period: Optional[str] = None,
504
+ year: Optional[int] = None,
505
+ ):
506
+ filtered = [ev for ev in evaluations if ev.get("tab") == tab]
507
+ if search:
508
+ term = search.lower()
509
+ filtered = [ev for ev in filtered if term in ev["fullName"].lower() or term in ev["department"].lower()]
510
+ if status:
511
+ filtered = [ev for ev in filtered if ev.get("status", "").lower() == status.lower()]
512
+ if period:
513
+ filtered = [ev for ev in filtered if ev.get("period") == period]
514
+ if year:
515
+ filtered = [ev for ev in filtered if ev.get("year") == year]
516
+ return paginate_list(filtered, page, limit)
517
+
518
+
519
+ @app.post("/api/v1/evaluations")
520
+ def create_evaluation(payload: Dict):
521
+ criteria_set_id = payload.get("criteriaSetId")
522
+ period = payload.get("period")
523
+ year = payload.get("year")
524
+ new_eval = {
525
+ "id": next(evaluation_counter),
526
+ "criteriaSetId": criteria_set_id,
527
+ "fullName": payload.get("fullName", "Nguoi dung moi"),
528
+ "evaluationPeriod": f"{period}/{year}",
529
+ "department": payload.get("department", "Phong moi"),
530
+ "selfScore": None,
531
+ "managerScore": None,
532
+ "status": "Draft",
533
+ "statusColor": "gray",
534
+ "period": period,
535
+ "year": year,
536
+ "tab": payload.get("tab", "self"),
537
+ "scores": payload.get("scores", {}),
538
+ "managerScores": payload.get("managerScores", {}),
539
+ }
540
+ evaluations.append(new_eval)
541
+ return new_eval
542
+
543
+
544
+ @app.get("/api/v1/evaluations/{item_id}")
545
+ def get_evaluation(item_id: int):
546
+ evaluation = find_by_id(evaluations, item_id)
547
+ if not evaluation:
548
+ raise HTTPException(status_code=404, detail="Evaluation not found")
549
+ return evaluation
550
+
551
+
552
+ @app.put("/api/v1/evaluations/{item_id}")
553
+ def update_evaluation(item_id: int, payload: Dict):
554
+ evaluation = find_by_id(evaluations, item_id)
555
+ if not evaluation:
556
+ raise HTTPException(status_code=404, detail="Evaluation not found")
557
+ evaluation.update({
558
+ "evaluationPeriod": payload.get("evaluationPeriod", evaluation["evaluationPeriod"]),
559
+ "scores": payload.get("scores", evaluation.get("scores", {})),
560
+ "managerScores": payload.get("managerScores", evaluation.get("managerScores", {})),
561
+ "status": payload.get("status", evaluation["status"]),
562
+ })
563
+ return evaluation
564
+
565
+
566
+ @app.delete("/api/v1/evaluations")
567
+ def delete_evaluations(payload: Dict) -> Response:
568
+ ids = payload.get("ids", [])
569
+ for item_id in ids:
570
+ existing = find_by_id(evaluations, item_id)
571
+ if existing:
572
+ evaluations.remove(existing)
573
+ return Response(status_code=204)
574
+
575
+
576
+ @app.post("/api/v1/evaluations/{item_id}/submit")
577
+ def submit_evaluation(item_id: int, payload: Dict):
578
+ evaluation = find_by_id(evaluations, item_id)
579
+ if not evaluation:
580
+ raise HTTPException(status_code=404, detail="Evaluation not found")
581
+ evaluation["status"] = "Submitted"
582
+ evaluation["managerId"] = payload.get("managerId")
583
+ evaluation["comments"] = payload.get("comments")
584
+ return evaluation
585
+
586
+
587
+ @app.post("/api/v1/evaluations/{item_id}/withdraw")
588
+ def withdraw_evaluation(item_id: int):
589
+ evaluation = find_by_id(evaluations, item_id)
590
+ if not evaluation:
591
+ raise HTTPException(status_code=404, detail="Evaluation not found")
592
+ evaluation["status"] = "Draft"
593
+ return evaluation
594
+
595
+
596
+ @app.post("/api/v1/evaluations/{item_id}/return")
597
+ def return_evaluation(item_id: int):
598
+ evaluation = find_by_id(evaluations, item_id)
599
+ if not evaluation:
600
+ raise HTTPException(status_code=404, detail="Evaluation not found")
601
+ evaluation["status"] = "Draft"
602
+ return evaluation
603
+
604
+
605
+ @app.get("/api/v1/evaluations/{item_id}/chat")
606
+ def list_evaluation_chat(item_id: int):
607
+ return evaluation_chats.get(item_id, [])
608
+
609
+
610
+ @app.post("/api/v1/evaluations/{item_id}/chat")
611
+ def post_evaluation_chat(item_id: int, payload: Dict):
612
+ chat_thread = evaluation_chats.setdefault(item_id, [])
613
+ new_message = {
614
+ "id": len(chat_thread) + 1,
615
+ "sender": payload.get("sender", "Nguoi dung"),
616
+ "avatar": payload.get("avatar", "person"),
617
+ "message": payload.get("message"),
618
+ "timestamp": payload.get("timestamp", "vua xong"),
619
+ "replyToMessageId": payload.get("replyToMessageId"),
620
+ "attachments": payload.get("attachments", []),
621
+ }
622
+ chat_thread.append(new_message)
623
+ return new_message
624
+
625
+
626
+ @app.get("/api/v1/evaluations/export")
627
+ def export_evaluations(format: str = Query(..., regex="^(pdf|xlsx)$")):
628
+ return {"message": f"Generated {format} export for evaluations"}
629
+
630
+
631
+ # Reports
632
+
633
+
634
+ @app.get("/api/v1/reports")
635
+ def get_reports(
636
+ reportType: str = Query(..., alias="reportType"),
637
+ period: int = Query(..., ge=1, le=12),
638
+ year: int = Query(..., ge=2000),
639
+ search: Optional[str] = None,
640
+ ):
641
+ filtered = reports_payload
642
+ if search:
643
+ term = search.lower()
644
+ filtered = [row for row in reports_payload if term in row["canBo"].lower() or term in row["donVi"].lower()]
645
+ return {"data": filtered, "reportInfo": reports_info}
646
+
647
+
648
+ @app.get("/api/v1/reports/export")
649
+ def export_reports(
650
+ reportType: str = Query(..., alias="reportType"),
651
+ period: int = Query(..., ge=1, le=12),
652
+ year: int = Query(..., ge=2000),
653
+ format: str = Query(..., regex="^(pdf|xlsx|docx)$"),
654
+ ):
655
+ return {"message": f"Generated {format} export for {reportType} {period}/{year}"}
656
+
657
+
658
+ # Report Types
659
+
660
+
661
+ @app.get("/api/v1/report-types")
662
+ def list_report_types(page: int = 1, limit: int = 5, search: Optional[str] = None):
663
+ filtered = report_types
664
+ if search:
665
+ term = search.lower()
666
+ filtered = [r for r in report_types if term in r["code"].lower() or term in r["name"].lower()]
667
+ return paginate_list(filtered, page, limit)
668
+
669
+
670
+ @app.post("/api/v1/report-types")
671
+ def create_report_type(payload: Dict):
672
+ new_type = {
673
+ "id": next(report_type_counter),
674
+ "code": payload.get("code"),
675
+ "name": payload.get("name"),
676
+ "status": payload.get("status", "inactive"),
677
+ }
678
+ report_types.append(new_type)
679
+ return new_type
680
+
681
+
682
+ @app.put("/api/v1/report-types/{item_id}")
683
+ def update_report_type(item_id: int, payload: Dict):
684
+ report_type = find_by_id(report_types, item_id)
685
+ if not report_type:
686
+ raise HTTPException(status_code=404, detail="Report type not found")
687
+ report_type.update({
688
+ "name": payload.get("name", report_type["name"]),
689
+ "status": payload.get("status", report_type["status"]),
690
+ })
691
+ return report_type
692
+
693
+
694
+ @app.delete("/api/v1/report-types/{item_id}")
695
+ def delete_report_type(item_id: int) -> Response:
696
+ report_type = find_by_id(report_types, item_id)
697
+ if not report_type:
698
+ raise HTTPException(status_code=404, detail="Report type not found")
699
+ report_types.remove(report_type)
700
+ return Response(status_code=204)
701
+
702
+
703
+ # Dashboard
704
+
705
+
706
+ @app.get("/api/v1/dashboard/stats")
707
+ def get_dashboard_stats():
708
+ return dashboard_stats
709
+
710
+
711
+ @app.get("/api/v1/dashboard/charts")
712
+ def get_dashboard_charts(year: int = Query(..., ge=2000), criteria: str = Query(...)):
713
+ monthly_breakdown = [{"month": month, "score": 80 + (month % 5)} for month in range(1, 13)]
714
+ pie_data = [
715
+ {"label": "Xuat sac", "value": 40},
716
+ {"label": "Tot", "value": 35},
717
+ {"label": "Trung binh", "value": 20},
718
+ {"label": "Can cai thien", "value": 5},
719
+ ]
720
+ return {"monthlyBreakdown": monthly_breakdown, "pieData": pie_data}
721
+
722
+
723
+ @app.get("/api/v1/dashboard/filters")
724
+ def get_dashboard_filters():
725
+ return {"years": [2024, 2023], "criteria": ["Bo tieu chi nang luc chung", "Bo tieu chi ABC"]}
726
+
727
+
728
+ # Users and Departments
729
+
730
+
731
+ @app.get("/api/v1/users/managers")
732
+ def get_managers():
733
+ return managers
734
+
735
+
736
+ @app.get("/api/v1/departments")
737
+ def get_departments():
738
+ return departments
739
+
740
+
741
+ @app.get("/api/v1/dashboard")
742
+ def dashboard_root():
743
+ return {"message": "Dashboard data ready"}
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart