Đặc tả API
Trang này dành cho người lập trình.
Quy ước chung
| Mục | Quy định |
|---|---|
| Đường dẫn gốc | /api/v1 |
| Định dạng | JSON, mã hoá UTF-8 |
| Xác thực | JWT trong cookie HttpOnly, tên adang_session |
| Ngày giờ | Chuẩn ISO 8601, luôn kèm độ lệch +07:00, không dùng hậu tố Z |
| Phân trang | Tham số page bắt đầu từ 1, limit mặc định 20, tối đa 100 |
| Sắp xếp | sort=field:asc hoặc sort=field:desc. Chỉ nhận các trường trong danh sách cho phép bên dưới |
| Tên trường | Gạch dưới — assignee_id, due_date. Áp dụng cho cả dữ liệu gửi lên và trả về |
| Khoá chính | Chuỗi, không phải số — "id": "42". Vì khoá chính là số nguyên lớn, JavaScript không biểu diễn an toàn bằng kiểu số |
Trường được phép sắp xếp cho GET /tasks: created_at, due_date, priority, title, status. Mặc định due_date:asc, việc không có hạn xếp cuối.
Máy chủ dùng Prisma, mà Prisma sinh tên thuộc tính kiểu assigneeId. Tầng ánh xạ phải đổi sang gạch dưới trước khi trả ra. Trả thẳng đối tượng Prisma là sai hợp đồng và làm hỏng toàn bộ giao diện.
Đưa số hiệu phiên bản vào đường dẫn ngay từ đầu. Khi cần thay đổi phá vỡ tương thích thì mở /api/v2 chạy song song, không buộc mọi thứ chuyển cùng lúc.
Mẫu phản hồi
Thành công:
{ "data": { }, "meta": { "page": 1, "limit": 20, "total": 57 } }
Lỗi:
{
"error": {
"code": "TASK_INVALID_TRANSITION",
"message": "Không thể chuyển việc từ Hoàn thành sang Đang làm.",
"details": { "from": "done", "to": "in_progress" }
}
}
Trường message luôn bằng tiếng Việt và hiển thị thẳng cho người dùng được, theo NFR-USE-04. Trường code dành cho lập trình, không hiện ra.
Bảng mã lỗi
| Mã HTTP | Mã lỗi | Khi nào |
|---|---|---|
| 400 | VALIDATION_FAILED | Dữ liệu gửi lên không hợp lệ |
| 401 | UNAUTHENTICATED | Chưa đăng nhập hoặc phiên hết hạn |
| 403 | FORBIDDEN_ROLE | Vai trò không được phép làm thao tác này |
| 403 | FORBIDDEN_SCOPE | Dữ liệu nằm ngoài phạm vi được xem |
| 404 | NOT_FOUND | Không tìm thấy |
| 409 | TASK_INVALID_TRANSITION | Chuyển trạng thái không hợp lệ theo BR-05 |
| 409 | TASK_ALREADY_CLOSED | Việc đã Hoàn thành hoặc Đã huỷ |
| 413 | FILE_TOO_LARGE | Tệp đính kèm vượt 20 MB |
| 413 | EXPORT_TOO_LARGE | Xuất Excel quá 5.000 dòng |
| 409 | COMMENT_EDIT_WINDOW_CLOSED | Sửa bình luận sau 15 phút |
| 429 | TOO_MANY_REQUESTS | Gọi quá nhiều lần |
Phân biệt FORBIDDEN_ROLE với FORBIDDEN_SCOPE để lỗi ghi lại đủ rõ khi cần truy vết: một bên là sai vai trò, một bên là vượt phạm vi phòng ban.
Đăng nhập
| Phương thức | Đường dẫn | Việc | Quyền |
|---|---|---|---|
| POST | /auth/login | Đăng nhập bằng email và mật khẩu | Không cần |
| POST | /auth/logout | Đăng xuất, xoá phiên | Đã đăng nhập |
| GET | /auth/me | Lấy thông tin người đang đăng nhập kèm vai trò và phòng ban | Đã đăng nhập |
| POST | /auth/change-password | Đổi mật khẩu của chính mình | Đã đăng nhập |
Không có đường dẫn tự đăng ký, theo BR-10.
Công việc
| Phương thức | Đường dẫn | Việc | Quyền |
|---|---|---|---|
| GET | /tasks | Danh sách, có lọc và phân trang | Theo phạm vi |
| GET | /tasks/:id | Chi tiết một việc | Theo phạm vi |
| POST | /tasks | Tạo việc mới | BGH, Trưởng phòng |
| PATCH | /tasks/:id | Sửa nội dung, hạn, mức ưu tiên | BGH, Trưởng phòng |
| POST | /tasks/:id/transition | Chuyển trạng thái | Theo BR-05 |
| POST | /tasks/:id/reassign | Chuyển giao cho người khác | Trưởng phòng, BGH |
| GET | /tasks/:id/history | Lịch sử thay đổi | Theo phạm vi |
| GET | /tasks/my | Việc của tôi | Đã đăng nhập |
Tham số lọc cho GET /tasks:
| Tham số | Ví dụ | Ghi chú |
|---|---|---|
status | status=new,in_progress | Nhiều giá trị cách nhau bằng dấu phẩy |
assignee_id | assignee_id=12 | |
department_id | department_id=3 | Gồm cả phòng con |
priority | priority=urgent | |
due_from, due_to | due_from=2026-08-01 | |
overdue | overdue=true | Chỉ lấy việc trễ hạn |
q | q=công văn | Tìm trong tên và mô tả |
Chuyển trạng thái
Dùng một đường dẫn riêng thay vì cho sửa thẳng trường status qua PATCH. Lý do: mỗi lần chuyển trạng thái đều kéo theo kiểm tra quy tắc, ghi lịch sử và tạo thông báo. Tách riêng thì không thể vô tình bỏ qua các bước đó.
POST /api/v1/tasks/42/transition
{ "to": "pending_review", "note": "Đã gửi công văn, chờ anh duyệt" }
Máy chủ kiểm tra theo thứ tự:
- Việc có tồn tại và nằm trong phạm vi không
- Bước chuyển có hợp lệ theo BR-05 không
- Vai trò này có được phép chuyển bước đó không
- Ghi vào
task_status_history, cập nhậtsubmitted_athoặccompleted_at - Tạo thông báo cho người liên quan
Bình luận và tệp đính kèm
| Phương thức | Đường dẫn | Việc | Quyền |
|---|---|---|---|
| GET | /tasks/:id/comments | Danh sách bình luận | Theo phạm vi |
| POST | /tasks/:id/comments | Thêm bình luận | Theo phạm vi |
| PATCH | /comments/:id | Sửa bình luận của chính mình, chỉ trong 15 phút đầu | Tác giả |
| DELETE | /comments/:id | Xoá bình luận của chính mình (xoá mềm) | Tác giả |
| GET | /tasks/:id/attachments | Danh sách tệp | Theo phạm vi |
| POST | /tasks/:id/attachments | Tải tệp lên, tối đa 20 MB | Theo phạm vi |
| GET | /attachments/:id/download | Tải tệp về | Theo phạm vi |
| DELETE | /attachments/:id | Xoá tệp (xoá mềm) | Người tải lên, hoặc Trưởng phòng/BGH trong phạm vi |
GET /attachments/:id/download phải kiểm tra quyền trước khi trả tệp, theo NFR-SEC-07. Tệp lưu ngoài thư mục web nên không thể truy cập trực tiếp bằng đường dẫn.
Người dùng và phòng ban
| Phương thức | Đường dẫn | Việc | Quyền |
|---|---|---|---|
| GET | /users | Danh sách người dùng | BGH, Trưởng phòng |
| POST | /users | Tạo tài khoản | BGH |
| PATCH | /users/:id | Sửa thông tin | BGH |
| POST | /users/:id/disable | Khoá tài khoản | BGH |
| POST | /users/:id/reset-password | Đặt lại mật khẩu | BGH |
| GET | /departments | Cây phòng ban | Đã đăng nhập |
| POST | /departments | Thêm phòng ban | BGH |
| GET | /roles | Danh sách vai trò | Đã đăng nhập |
Màn hình tổng quan và báo cáo
| Phương thức | Đường dẫn | Việc | Quyền |
|---|---|---|---|
| GET | /dashboard/summary | Bốn ô đếm theo trạng thái | BGH, Trưởng phòng |
| GET | /dashboard/overdue | Danh sách việc trễ hạn của phạm vi quản lý | BGH, Trưởng phòng |
| GET | /reports/tasks.xlsx | Xuất Excel, nhận cùng tham số lọc như /tasks. Quá 5.000 dòng thì trả lỗi EXPORT_TOO_LARGE | BGH, Trưởng phòng |
Nhân viên không gọi /dashboard/overdue. Màn hình trễ hạn của nhân viên dùng GET /tasks?overdue=true, tự giới hạn trong phạm vi việc được giao cho họ.
Thông báo
| Phương thức | Đường dẫn | Việc |
|---|---|---|
| GET | /notifications | Danh sách thông báo của mình |
| GET | /notifications/unread-count | Số thông báo chưa đọc |
| POST | /notifications/:id/read | Đánh dấu đã đọc |
| POST | /notifications/read-all | Đánh dấu đã đọc tất cả |
Hình dạng dữ liệu trả về
Phần này là hợp đồng giữa máy chủ và giao diện. Không có nó thì hai bên không làm song song được.
GET /tasks — danh sách công việc
{
"data": [
{
"id": "42",
"title": "Chuẩn bị hồ sơ thanh tra",
"status": "in_progress",
"priority": "urgent",
"due_date": "2026-08-05",
"department": { "id": "3", "name": "Văn phòng Hành chính" },
"assignee": { "id": "12", "full_name": "Nguyễn Thị B", "status": "active" },
"creator": { "id": "7", "full_name": "Phạm Văn A", "status": "active" },
"overdue": {
"is_overdue": true,
"days": 2,
"kind": "assignee_late"
},
"counts": { "comments": 3, "attachments": 2 },
"created_at": "2026-08-02T09:15:00+07:00",
"updated_at": "2026-08-05T14:20:00+07:00"
}
],
"meta": { "page": 1, "limit": 20, "total": 6 }
}
Ba nhóm trường dưới đây máy chủ bắt buộc tính sẵn, giao diện không tự tính:
| Trường | Vì sao máy chủ phải tính |
|---|---|
overdue.days | Tính bằng đồng hồ máy người dùng sẽ vênh với báo cáo Excel khi máy đó sai ngày |
overdue.kind | Nhận assignee_late hoặc pending_review_overdue, theo BR-08. Phân loại này quyết định hiển thị "Chưa nộp" hay "Chờ tôi duyệt" |
assignee, creator | Nhúng sẵn để không phải gọi thêm 20 lần cho 20 dòng. creator.status cần cho quy tắc "người giao bị khoá thì Ban Giám hiệu duyệt thay" |
counts để hiện số bình luận và số tệp trong danh sách mà không phải mở chi tiết.
GET /tasks/:id — chi tiết công việc
Như một phần tử của danh sách, thêm:
{
"data": {
"id": "42",
"description": "Tập hợp hồ sơ theo danh mục đoàn thanh tra yêu cầu…",
"submitted_at": null,
"completed_at": null,
"cancelled_at": null,
"cancel_reason": null,
"available_transitions": [
{ "to": "pending_review", "label": "Báo hoàn thành", "requires_note": false },
{ "to": "cancelled", "label": "Huỷ việc", "requires_note": true }
],
"permissions": {
"can_edit": true, "can_reassign": true, "can_cancel": true,
"can_confirm": false, "can_comment": true, "can_attach": true
}
}
}
Giao diện không tự suy ai được bấm nút gì. Luật nằm ở BR-05 và ma trận phân quyền — nếu cài lại ở giao diện thì có hai bản luật, sửa một chỗ quên chỗ kia.
Giao diện chỉ vẽ những gì máy chủ nói là được phép. Máy chủ vẫn kiểm lại lần nữa khi nhận yêu cầu, vì ẩn nút không phải là phân quyền.
GET /auth/me
{
"data": {
"id": "7",
"full_name": "Phạm Văn A",
"role": { "code": "truong_phong", "name": "Trưởng phòng" },
"organization": { "id": "1", "name": "Trường THPT X" },
"primary_department": { "id": "3", "name": "Văn phòng Hành chính" },
"scope_department_ids": ["3", "8", "9"],
"unread_notifications": 3
}
}
scope_department_ids là toàn bộ nhánh phòng ban người này xem được, đã duyệt cây sẵn. Thiếu trường này thì trưởng phòng có tổ trực thuộc sẽ bị ẩn nhầm nút trên việc của tổ mình.
GET /dashboard/summary
{
"data": {
"new": 8,
"in_progress": 15,
"done_this_month": 42,
"overdue": { "total": 3, "assignee_late": 2, "pending_review": 1 }
}
}
Ba ô đầu là số hiện tại, không lọc theo tháng. Chỉ ô Hoàn thành tính trong tháng dương lịch hiện tại.
GET /tasks/:id/history
{
"data": [
{
"id": "301",
"kind": "due_date_changed",
"actor": { "id": "7", "full_name": "Phạm Văn A" },
"from": { "due_date": "2026-08-03" },
"to": { "due_date": "2026-08-05" },
"note": null,
"created_at": "2026-08-04T08:00:00+07:00"
}
]
}
kind nhận: created, status_changed, assignee_changed, due_date_changed.
Giao diện dựng câu tiếng Việt từ dữ liệu này, máy chủ không trả câu sẵn. Lý do: câu chữ thuộc về lớp hiển thị, và NFR-MAINT-05 yêu cầu tách chuỗi khỏi mã nguồn.
Dữ liệu gửi lên
POST /tasks
{
"title": "Tổng hợp báo cáo tháng 8",
"description": "Không bắt buộc",
"department_id": "3",
"assignee_id": "12",
"due_date": "2026-08-20",
"priority": "normal",
"confirm_past_due": false
}
| Trường | Bắt buộc | Ghi chú |
|---|---|---|
title | Có | Tối đa 500 ký tự |
assignee_id | Có | Đúng một người, theo BR-02 |
department_id | Tuỳ vai trò | Trưởng phòng bỏ trống thì lấy phòng mình. Ban Giám hiệu bắt buộc điền |
due_date | Không | YYYY-MM-DD, theo BR-04 |
priority | Không | Mặc định normal |
confirm_past_due | Không | Đặt true để xác nhận vẫn lưu khi hạn đã qua |
PATCH /tasks/:id — nhận các trường của POST trừ assignee_id, tất cả đều tuỳ chọn. Đổi người phụ trách dùng đường dẫn reassign riêng.
POST /tasks, PATCH /tasks/:id, POST /tasks/:id/transition, POST /tasks/:id/reassign đều trả về đúng hình dạng chi tiết công việc như GET /tasks/:id, gồm cả available_transitions và permissions.
Trả rút gọn kiểu { "id": "42", "status": "in_progress" } buộc giao diện phải gọi thêm một lần nữa để biết nút nào còn bấm được, và mỗi thao tác lại có hình dạng riêng phải xử lý riêng.
POST /tasks/:id/transition
{ "to": "pending_review", "note": "Đã gửi công văn, chờ anh duyệt", "if_unmodified_since": "2026-08-05T14:20:00+07:00" }
if_unmodified_since truyền lại giá trị updated_at nhận được lúc mở việc. Nếu công việc đã bị người khác đổi trong lúc đó, máy chủ trả lỗi TASK_CONCURRENT_UPDATE. Đây là cách thực hiện yêu cầu "hai người cùng đổi trạng thái" ở FR-TASK-03.
POST /tasks/:id/reassign
{ "assignee_id": "15", "note": "Chị B nghỉ phép tuần này" }
POST /auth/login
organization_code bắt buộc vì địa chỉ thư chỉ duy nhất trong phạm vi một trường. Bản dùng thử chỉ có một trường nên giao diện điền sẵn giá trị mặc định, nhưng trường này phải có ngay từ đầu — thêm sau sẽ phá tương thích.
POST /users
{
"full_name": "Nguyễn Văn X",
"department_id": "3",
"role_code": "nhan_vien",
"phone": null
}
Phản hồi trả kèm mật khẩu tạm, chỉ trả đúng lần này:
{ "data": { "id": "20", "full_name": "Nguyễn Văn X", "temporary_password": "Tam2026!xY" } }
Bổ sung mã lỗi
| Mã HTTP | Mã lỗi | Khi nào |
|---|---|---|
| 401 | ACCOUNT_DISABLED | Tài khoản bị khoá, lúc đăng nhập |
| 409 | EMAIL_ALREADY_USED | Địa chỉ thư đã có người dùng |
| 409 | ASSIGNEE_DISABLED | Giao việc cho tài khoản đã khoá |
| 409 | TASK_CONCURRENT_UPDATE | Người khác vừa cập nhật công việc |
Bổ sung đường dẫn còn thiếu
| Phương thức | Đường dẫn | Việc | Quyền |
|---|---|---|---|
| GET | /tasks/count | Đếm số việc thoả bộ lọc, dùng trước khi xuất Excel | BGH, Trưởng phòng |
| PATCH | /departments/:id | Sửa tên, đổi phòng cha, khoá phòng ban | BGH |
| PUT | /users/:id/assignment | Gán hoặc đổi phòng ban và vai trò | BGH |
| POST | /users/:id/enable | Mở khoá tài khoản | BGH |
GET /users bổ sung tham số department_id, status, q, và phân trang như /tasks. Nhân viên gọi được GET /users?department_id=<phòng mình> chỉ để lấy tên hiển thị, phản hồi rút gọn còn id và full_name.
GET /departments trả mảng phẳng kèm parent_id, giao diện tự dựng cây. Trả cây lồng nhau sẽ khó phân trang và khó lọc.
Định dạng hiển thị
Máy chủ luôn trả theo chuẩn ISO 8601 kèm múi giờ. Giao diện hiển thị theo bảng sau:
| Loại | Cách hiện | Ví dụ |
|---|---|---|
| Ngày trong năm hiện tại | dd/MM | 05/08 |
| Ngày khác năm | dd/MM/yyyy | 05/08/2025 |
| Ngày giờ | dd/MM HH:mm | 05/08 14:20 |
| Dung lượng tệp | Bội số 1024, dấu phẩy thập phân, một chữ số với MB | 1,2 MB · 340 KB |
| Số ngày trễ | Số ngày trọn vẹn sau khi hết ngày hạn | trễ 2 ngày |
Quy ước tải dữ liệu và xử lý lỗi ở giao diện
| Tình huống | Cách xử lý |
|---|---|
| Đang tải lần đầu | Khung xám mờ đúng hình dạng nội dung, không hiện số 0 |
| Đang tải lại | Giữ dữ liệu cũ, thêm dấu hiệu đang cập nhật |
| Lỗi mạng khi đọc | Hiện câu tiếng Việt kèm nút Thử lại. Tự thử lại tối đa một lần |
| Lỗi khi ghi | Giữ nguyên nội dung người dùng đã nhập, không xoá biểu mẫu |
| Hết phiên (401) | Hiện hộp thoại đăng nhập lại đè lên, giữ nguyên trang. Không đá thẳng về trang đăng nhập |
| Sau khi đổi trạng thái | Tải lại chi tiết công việc và số đếm liên quan |
| Chuông thông báo | Cập nhật khi tải trang và sau mỗi thao tác. Không tự hỏi máy chủ theo chu kỳ |
Tài liệu tự sinh
Dùng @nestjs/swagger để sinh tài liệu từ chính mã nguồn, xem tại /api/docs ở môi trường thử nghiệm.
Tài liệu tự sinh là nguồn chính xác nhất về hình dạng dữ liệu. Trang này mô tả ý định thiết kế — vì sao có đường dẫn đó, quyền ra sao, ràng buộc nghiệp vụ nào áp dụng. Hai thứ bổ sung cho nhau, không thay thế nhau.
Trang /api/docs không mở ở môi trường chính thức.