Sheet 1 · Tổng quan hệ thống

CTC ERP — Tổng quan & Danh mục yêu cầu

CTC ERP (frontend-next, Next.js 16 + React 19 + shadcn) là hệ thống quản trị nội bộ đa tenant của Smart Tech CTC — hợp nhất Công việc, Đối tác/CRM, Dự án, Tài chính, Hoá đơn, Lịch, Quy trình tự động, Kỹ năng và các module vận hành khác trên 1 codebase, phục vụ đồng thời 4 tenant (ctc, ode, lhbd, journalist) bằng cùng 1 bộ source, mỗi tenant 1 SQLite DB riêng biệt. Sản phẩm chạy tại https://ctc.erp.smarttechctc.io/ trên VPS riêng (SSH ctc-erp, unix user erp, mã tại /home/erp/erp-platform) — chuyển sang từ 03/09/2026; PM2 gồm erp-next (web :4123) và erp-crm-sync (API :4114), cạnh đó là môi trường thử https://staging-erp.smarttechctc.io (erp-staging-web / erp-staging-api). Bộ tài liệu bạn đang đọc đã tách sang site riêng https://erp-docs.smarttechctc.io để vòng đời của nó không dính vào một tenant nào.

25
module trong erp/modules/*
30
route thật dưới (erp)/* (+ 1 trang đăng nhập)
29
bảng SQLite / tenant (20 bảng server riêng + 9 sinh lại từ blob)
84
test tự động trong sync-server/test/ (20 file), pass hết

Cách đếm (03/09/2026): module = số thư mục con cấp 1 dưới frontend-next/src/erp/modules/; route = số file page.tsx dưới frontend-next/src/app/(erp)/ (không tính route demo template (pages)/(blank) chưa gỡ khỏi source); bảng = đếm thật trong sqlite_master của tenant ctc (29). Cách đếm này quan trọng: tenant.js có 21 câu CREATE TABLE IF NOT EXISTS, nhưng 20 trong đó là bảng có tên cố định còn câu thứ 21 nằm trong vòng lặp CREATE TABLE … ${table} sinh ra 9 bảng thực thể — nên "21" là con số của mã nguồn, không phải của cơ sở dữ liệu; test = node --test sync-server/test/*.test.js. Riêng số endpoint không nêu thành thẻ vì cách đếm dễ gây hiểu sai: server.js có 103 chỗ kiểm req.method === '…', nhưng một path thường được kiểm nhiều lần cho nhiều method nên con số đó KHÔNG phải số endpoint riêng biệt.

Một chỗ dễ lẫn tên. Trong 25 phân hệ có một thư mục tên ai (erp/modules/ai/ai.ts) — đó là AI Narrative Engine cho nghiệp vụ duyệt nội dung bất động sản, gọi /api/ai bằng khoá HTTP của tenant. Nó không liên quan tới AI Gateway hay trợ lý ngoài; hai thứ đó xem trang AI Engine & Gateway.

Kiến trúc tóm tắt

Thành phầnMô tả
Đa tenant1 codebase phục vụ 4 khách hàng qua Host header (slugFromHost) → mỗi tenant 1 file SQLite riêng tại sync-server/data/tenants/<slug>/crm.db, cách ly vật lý.
Đồng bộ realtime dạng blobToàn bộ dữ liệu nghiệp vụ (tasks/contacts/projects/events/invoices/transactions/notifications) đóng gói thành 1 JSON "state" lưu trong bảng revisions (cột rev tăng dần). Client pullState() lấy state mới nhất, PUT /api/state ghi đè state mới (server tự diff ra thay đổi + tăng rev), GET /api/events là kênh SSE đẩy thay đổi tới các tab đang mở. Bảng thực thể (tasks/contacts/...) chỉ là materialized view sinh lại từ blob, không phải nguồn sự thật.
Phân quyền theo moduleauth_users.modules (mảng JSON) quyết định user thấy menu/route nào; vai trò admin luôn có toàn bộ ALL_MODULE_IDS. Xem chi tiết ma trận ở trang Vai trò.
Per-user server-sideMột số dữ liệu KHÔNG nằm trong blob sync (tránh phải bump schema toàn cục): Workflow/Skill/DevRequest/Comment lưu bảng riêng; cấu hình cá nhân (thu nhập, tích hợp relay, đã đọc mail...) lưu bảng user_data qua /api/me/data.
Hai tầng lọc dữ liệuBắt buộc ở MỌI đường đọc: scopeStateForUser(filterStateForUser(inner, actor), actor). Tầng 1 cắt theo phân hệ, tầng 2 cắt theo phạm vi từng bản ghi. Thiếu tầng 2 là rò dữ liệu cả công ty — đã xảy ra ba lần và được vá 02/09.
Trợ lý ngoài gọi VÀO ERPChiều duy nhất được phép: trợ lý AI (Claude Code, antigravity, bot Telegram — chạy trên máy khác, unix user khác) gọi vào ERP bằng khoá API của từng người (header X-Agent-Key, bảng bot_api_keys). Khoá gắn chết một tài khoản, chỉ mở đúng phân hệ và phạm vi dữ liệu mà chính người đó có. Có 9 hành động allowlist (viec, tim, help, them, xong, trangthai, han, tao_ky_nang, tao_quy_trinh) — không có hành động xoá nào. Xem chi tiết ở trang Quy trình.
ERP không chạy CLI mô hìnhERP không bao giờ chạy claude -p / agy -p. Sáng 03/09 từng có ngoại lệ (ai-cli.js cho tính năng "Dựng bằng AI") và đã gỡ hẳn cùng ngày: endpoint đó cho mọi tài khoản đăng nhập gõ 2000 ký tự tự do rồi nối thẳng vào dòng lệnh chạy dưới user erp — cùng máy với cơ sở dữ liệu của cả 4 khách hàng. Việc sinh nội dung nay do trợ lý bên ngoài làm rồi gọi ngược vào ERP.
Hệ thống ngoàiHubSpot (đồng bộ contact/deal), Zalo OA (relay follower/profile), Gmail qua Google Apps Script Web App (ingest mail + gửi), webhook Google Workspace (script.google.com/googleusercontent/chat.googleapis/zapier). Telegram không còn do ERP long-poll — AI Gateway giữ kênh Telegram và gọi vào ERP qua /api/agent/* (đảo chiều 29/08, gỡ bot-worker.js 30/08).

Danh mục yêu cầu (suy luận từ lịch sử phát triển ghi trong CONTEXT.md — không phải văn bản yêu cầu gốc)

NODESCRIPTION
1Trước đây CTC vận hành CRM/công việc rời rạc qua 4 instance riêng biệt (CTC, ODE, journalist, teamwork) không chia sẻ được code lẫn dữ liệu — cần hợp nhất thành 1 nền tảng đa tenant duy nhất.
2Quản lý phải theo dõi được toàn bộ vòng đời bán hàng: đối tác (contact) → deal trên dự án (dealStage/dealValue) → hoá đơn → thu tiền, thay vì rải rác trên Excel.
3Mỗi nhân sự cần 1 nơi duy nhất quản lý công việc được giao (kanban + bảng), có thể lọc theo dự án/đối tác/giải pháp, sắp xếp theo nhiều cột, sửa nhanh (inline edit) ưu tiên/phụ trách/hạn.
4Ban điều hành cần dashboard tổng quan tỷ trọng doanh thu/khối lượng việc theo giải pháp (gp:*) và theo ngành khách hàng (nganh:*) để ra quyết định phân bổ nguồn lực.
5Cần phân quyền theo vai trò: admin thấy toàn bộ hệ thống; member (nhân viên) chỉ thấy công việc/dự án/đối tác của chính mình, giới hạn theo module được cấp.
6Cần tự động hoá các quy trình lặp lại (nhắc việc, đổi trạng thái, gửi thông báo relay Telegram/GWS) mà không cần biết lập trình — thông qua builder workflow trực quan (canvas kéo-thả các bước).
7Cần chuẩn hoá và tái sử dụng tri thức vận hành (quy trình thao tác chuẩn) qua module Kỹ năng, có thể nhập từ file Markdown sẵn có, chia sẻ giữa các nhân sự.
8Nhân sự cần kênh đề xuất/yêu cầu phát triển tính năng (workflow/skill mới) có quy trình duyệt rõ ràng (new → reviewing → approved/rejected → released) thay vì báo miệng.
9Cần theo dõi tài chính (thu/chi, giao dịch định kỳ), ngân sách truyền thông theo chiến dịch (NCC, nghiệm thu, quy đổi Overtime→ngày nghỉ/tiền OT) và xuất hoá đơn GTGT có OCR quét ảnh hoá đơn đầu vào.
10Từng nhân sự cần công cụ cá nhân tính thu nhập/thuế TNCN theo lương + % hoa hồng dự án, không phải làm thủ công ngoài hệ thống.
11Cần tích hợp email (Gmail cá nhân qua Apps Script) và Zalo OA ngay trong ERP để không phải chuyển đổi qua lại nhiều công cụ khi chăm sóc khách hàng.
12Dữ liệu khách hàng đã có sẵn trên HubSpot (9.949 contact) cần đồng bộ một chiều vào ERP thay vì nhập tay lại.
13Cần cơ chế review/bình luận (comment ghim theo trường dữ liệu, @mention) để nhiều người cùng góp ý một bản ghi mà không cần rời khỏi màn hình đang xem.
14Cần tìm kiếm toàn hệ thống (⌘K) xuyên suốt các loại bản ghi (task/contact/project/invoice...), có bỏ dấu tiếng Việt, tôn trọng phân quyền của người tìm.
15Toàn bộ lịch sử thay đổi dữ liệu phải truy vết được (revision tăng dần + nhật ký hệ thống) để phục vụ audit và khôi phục khi cần.
Ghi chú: Danh sách trên là suy luận nghiệp vụ từ nhật ký triển khai (mục GIAI ĐOẠN trong CONTEXT.md gốc của erp-platform), không phải văn bản yêu cầu chính thức do khách hàng cung cấp — CTC vừa là đơn vị phát triển vừa là người dùng chính (nội bộ) của hệ thống này.

Giải thích thuật ngữ

Thuật ngữGiải nghĩa
dealStage vs statusProject.dealStage (lead→contact→demo→proposal→negotiation→won/lost) đo tiến độ bán hàng (nằm trên phễu); Project.status (planning→active→on_hold→acceptance→completed) đo tiến độ giao hàng/triển khai. Hai trục độc lập, tách khỏi nhau kể từ đợt "Deal → PROJECT" (27/07).
Tag gp:*8 tag Giải pháp (truyen-thong · su-kien · ai · crm-erp · digital-marketing · business · iot · van-hanh) — chiều dữ liệu xuyên suốt Contact/Project/Task/Transaction/Invoice, dùng tính tỷ trọng doanh thu/khối lượng theo giải pháp. n tag trên 1 bản ghi → mỗi tag trọng số 1/n.
Tag nganh:*10 tag Ngành khách hàng, gắn ở cấp Đối tác (Contact) — dùng tính tỷ trọng doanh thu/số lượng theo ngành trên Dashboard.
Tag viec:*8 tag Loại lao động ở cấp Công việc (noi-dung · thiet-ke-media · ky-thuat · su-kien-hien-truong · sales-deal · quan-he · nghien-cuu · van-hanh-noi-bo) — đo khối lượng việc theo loại.
Tag tc:*8 tag Bản chất dòng tiền, dùng chung cho Giao dịch/Định kỳ/Hoá đơn (thu-du-an · thu-dinh-ky · chi-media-booking · chi-san-xuat · chi-luong · chi-cong-cu · chi-van-hanh · khac).
Rollup giá trị đối tácContact.dealValue (field nhập tay, còn tồn tại trong schema nhưng KHÔNG còn hiển thị) đã được thay bằng giá trị tính lại (rollup) = tổng dealValue của mọi Project gắn contactId đó (trừ dự án lost). UI hiển thị dưới tên "Giá trị hợp đồng", đọc-only, phụ đề "Tổng từ N dự án".
Blob state & revToàn bộ dữ liệu nghiệp vụ của 1 tenant đóng gói thành 1 JSON lớn ("state"), mỗi lần ghi tăng số rev (revision) và lưu 1 bản snapshot mới vào bảng revisions — vừa là cơ chế đồng bộ realtime giữa các tab/thiết bị, vừa là lịch sử thay đổi.
Tenant1 khách hàng/tổ chức độc lập dùng chung source code nhưng dữ liệu tách vật lý theo file SQLite riêng, resolve theo tên miền (Host header) lúc request tới.