Chuyển tới nội dung chính

Đặc tả API

Tài liệu kỹ thuật

Trang này dành cho người lập trình.

Quy ước chung

MụcQuy định
Đường dẫn gốc/api/v1
Định dạngJSON, mã hoá UTF-8
Xác thựcJWT 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 trangTham số page bắt đầu từ 1, limit mặc định 20, tối đa 100
Sắp xếpsort=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ườngGạch dướiassignee_id, due_date. Áp dụng cho cả dữ liệu gửi lên và trả về
Khoá chínhChuỗ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.

Hai quy ước trên là bắt buộc

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ã HTTPMã lỗiKhi nào
400VALIDATION_FAILEDDữ liệu gửi lên không hợp lệ
401UNAUTHENTICATEDChưa đăng nhập hoặc phiên hết hạn
403FORBIDDEN_ROLEVai trò không được phép làm thao tác này
403FORBIDDEN_SCOPEDữ liệu nằm ngoài phạm vi được xem
404NOT_FOUNDKhông tìm thấy
409TASK_INVALID_TRANSITIONChuyển trạng thái không hợp lệ theo BR-05
409TASK_ALREADY_CLOSEDViệc đã Hoàn thành hoặc Đã huỷ
413FILE_TOO_LARGETệp đính kèm vượt 20 MB
413EXPORT_TOO_LARGEXuất Excel quá 5.000 dòng
409COMMENT_EDIT_WINDOW_CLOSEDSửa bình luận sau 15 phút
429TOO_MANY_REQUESTSGọ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ẫnViệcQuyền
POST/auth/loginĐăng nhập bằng email và mật khẩuKhông cần
POST/auth/logoutĐăng xuất, xoá phiênĐã đăng nhập
GET/auth/meLấ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ẫnViệcQuyền
GET/tasksDanh sách, có lọc và phân trangTheo phạm vi
GET/tasks/:idChi tiết một việcTheo phạm vi
POST/tasksTạo việc mớiBGH, Trưởng phòng
PATCH/tasks/:idSửa nội dung, hạn, mức ưu tiênBGH, Trưởng phòng
POST/tasks/:id/transitionChuyển trạng tháiTheo BR-05
POST/tasks/:id/reassignChuyển giao cho người khácTrưởng phòng, BGH
GET/tasks/:id/historyLịch sử thay đổiTheo phạm vi
GET/tasks/myViệc của tôiĐã đăng nhập

Tham số lọc cho GET /tasks:

Tham sốVí dụGhi chú
statusstatus=new,in_progressNhiều giá trị cách nhau bằng dấu phẩy
assignee_idassignee_id=12
department_iddepartment_id=3Gồm cả phòng con
prioritypriority=urgent
due_from, due_todue_from=2026-08-01
overdueoverdue=trueChỉ lấy việc trễ hạn
qq=công vănTì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ự:

  1. Việc có tồn tại và nằm trong phạm vi không
  2. Bước chuyển có hợp lệ theo BR-05 không
  3. Vai trò này có được phép chuyển bước đó không
  4. Ghi vào task_status_history, cập nhật submitted_at hoặc completed_at
  5. 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ẫnViệcQuyền
GET/tasks/:id/commentsDanh sách bình luậnTheo phạm vi
POST/tasks/:id/commentsThêm bình luậnTheo phạm vi
PATCH/comments/:idSửa bình luận của chính mình, chỉ trong 15 phút đầuTác giả
DELETE/comments/:idXoá bình luận của chính mình (xoá mềm)Tác giả
GET/tasks/:id/attachmentsDanh sách tệpTheo phạm vi
POST/tasks/:id/attachmentsTải tệp lên, tối đa 20 MBTheo phạm vi
GET/attachments/:id/downloadTải tệp vềTheo phạm vi
DELETE/attachments/:idXoá 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ẫnViệcQuyền
GET/usersDanh sách người dùngBGH, Trưởng phòng
POST/usersTạo tài khoảnBGH
PATCH/users/:idSửa thông tinBGH
POST/users/:id/disableKhoá tài khoảnBGH
POST/users/:id/reset-passwordĐặt lại mật khẩuBGH
GET/departmentsCây phòng banĐã đăng nhập
POST/departmentsThêm phòng banBGH
GET/rolesDanh 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ẫnViệcQuyền
GET/dashboard/summaryBốn ô đếm theo trạng tháiBGH, Trưởng phòng
GET/dashboard/overdueDanh sách việc trễ hạn của phạm vi quản lýBGH, Trưởng phòng
GET/reports/tasks.xlsxXuất Excel, nhận cùng tham số lọc như /tasks. Quá 5.000 dòng thì trả lỗi EXPORT_TOO_LARGEBGH, Trưởng phòng
Màn hình trễ hạn của nhân viên

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ẫnViệc
GET/notificationsDanh sách thông báo của mình
GET/notifications/unread-countSố 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ườngVì sao máy chủ phải tính
overdue.daysTí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.kindNhậ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, creatorNhú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
}
}
}
Vì sao máy chủ trả cả danh sách bước chuyển và quyền

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",
"email": "[email protected]",
"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_idstoà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ườngBắt buộcGhi chú
titleTối đa 500 ký tự
assignee_idĐúng một người, theo BR-02
department_idTuỳ 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_dateKhôngYYYY-MM-DD, theo BR-04
priorityKhôngMặc định normal
confirm_past_dueKhô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.

Mọi thao tác ghi trả về cùng một hình dạ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_transitionspermissions.

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

{ "email": "[email protected]", "password": "…", "organization_code": "thpt-x" }

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",
"email": "[email protected]",
"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ã HTTPMã lỗiKhi nào
401ACCOUNT_DISABLEDTài khoản bị khoá, lúc đăng nhập
409EMAIL_ALREADY_USEDĐịa chỉ thư đã có người dùng
409ASSIGNEE_DISABLEDGiao việc cho tài khoản đã khoá
409TASK_CONCURRENT_UPDATENgườ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ẫnViệcQuyền
GET/tasks/countĐếm số việc thoả bộ lọc, dùng trước khi xuất ExcelBGH, Trưởng phòng
PATCH/departments/:idSửa tên, đổi phòng cha, khoá phòng banBGH
PUT/users/:id/assignmentGán hoặc đổi phòng ban và vai tròBGH
POST/users/:id/enableMở khoá tài khoảnBGH

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 idfull_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ạiCách hiệnVí dụ
Ngày trong năm hiện tạidd/MM05/08
Ngày khác nămdd/MM/yyyy05/08/2025
Ngày giờdd/MM HH:mm05/08 14:20
Dung lượng tệpBội số 1024, dấu phẩy thập phân, một chữ số với MB1,2 MB · 340 KB
Số ngày trễSố ngày trọn vẹn sau khi hết ngày hạntrễ 2 ngày

Quy ước tải dữ liệu và xử lý lỗi ở giao diện

Tình huốngCách xử lý
Đang tải lần đầuKhung xám mờ đúng hình dạng nội dung, không hiện số 0
Đang tải lạiGiữ dữ liệu cũ, thêm dấu hiệu đang cập nhật
Lỗi mạng khi đọcHiệ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 ghiGiữ 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áiTải lại chi tiết công việc và số đếm liên quan
Chuông thông báoCậ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.