Vấn đề
ERP nội bộ của TTMI được dùng ở 50 cửa hàng, ~500 nhân viên và 4 thương hiệu. Nhân viên tài chính nhập doanh thu hằng ngày vào một lưới hoạt động như bảng tính: dán cả khối, kéo chọn, di chuyển bằng bàn phím. Tôi xây phần frontend cho khu vực này và cùng backend thống nhất API contract.
Có bốn chỗ dễ sai.
Tổng phải chính xác. Số trong JavaScript là số thực dấu phẩy động, nên nhiều số tiền không biểu diễn đúng được và các tổng rất lớn bị mất độ chính xác.
Ô trống phải có nghĩa là “chưa nhập”. Nếu giao diện coi nó là 0, dashboard sẽ hiện doanh thu mà chẳng ai nhập.
Nhiều người có thể sửa cùng một kỳ. Một tab mở từ một tiếng trước không được phép ghi đè dữ liệu mới hơn trong im lặng.
Xuất Excel cũng có vấn đề riêng. Một quy tắc định dạng áp cho cả cột khiến số tiền nguyên bị lòi dấu thập phân thừa, và có nguy cơ biến mã định danh như mã nhân viên thành số. Báo cáo tháng toàn công ty còn cần chi tiết từng nhân viên, mà gửi một request cho mỗi người thì chậm.
Tôi đã làm gì
Tiền là số nguyên, cộng bằng BigInt
Số tiền được truyền và giữ trên client dưới dạng chuỗi số nguyên, và tổng được cộng bằng BigInt. Dấu phân cách hàng nghìn chỉ tồn tại trên màn hình. Nhờ vậy tổng vẫn chính xác ở mức mà số thực đã sai. Đổi lại, mọi ranh giới đều cần code chuyển đổi: nhập, dán, hiển thị và payload.
Ô trống là một trạng thái riêng
“Chưa nhập” luôn tách khỏi số 0 qua hiển thị, thao tác dán, payload của request và dữ liệu API lưu. Dashboard hiện số ngày đã nhập so với số ngày còn thiếu, nên một ngày bị bỏ trống không trông như một ngày bán ế. Mọi phép tổng hợp cần một nhánh riêng cho ô trống, và product owner phải thống nhất ý nghĩa của ô trống trước khi tôi viết bất kỳ phép tính nào.
Revision check với conflict hiện rõ
Mỗi lần lưu gửi kèm revision mà nó dựa vào, và API từ chối lượt ghi đã cũ. Trên client, thao tác lưu hiện tổng trước và sau để xác nhận, chặn bấm lưu hai lần, và giữ bản nháp local nếu lưu lỗi hoặc bị conflict. Thỉnh thoảng người dùng phải tải lại và nhập lại thay đổi. Tôi chọn cách đó thay cho “ai lưu sau thì thắng”, nơi số liệu của một người biến mất mà không ai để ý.
Một helper định dạng số cho mọi file xuất
Tôi viết một helper xuất file dùng chung, chọn định dạng ô theo giá trị thật (nguyên hay có phần lẻ) và chỉ áp cho các trường đo lường được, để mã định danh vẫn là text. Mọi module xuất file giờ đều đi qua nó. Cách này kém khai báo hơn so với cấu hình từng cột, và chỉ hiệu quả khi mọi exporter đều dùng nó, nên tôi chuyển các file xuất hiện có sang dùng helper này.
Xuất theo batch, hoặc đủ hết hoặc không gì cả
Với báo cáo toàn công ty, tôi thay request theo từng nhân viên bằng các batch request gọn, gửi theo từng đợt song song nhỏ. Nếu có batch nào lỗi, việc xuất dừng lại và báo lỗi. Một batch lỗi sẽ chặn cả file. Tôi chấp nhận điều đó vì thiếu một file vẫn an toàn hơn một bảng lương có số 0 ở những chỗ dữ liệu tải hỏng.
Kết quả
Các màn hình và file xuất tài chính đã chạy production và được đội tài chính sử dụng. Tổng luôn chính xác, ô trống không bao giờ bị tính là 0, và hai người sửa cùng một kỳ sẽ thấy conflict rõ ràng.
Tôi kiểm tra các workbook xuất ra bằng cách đọc lại bằng code và so với lưới trên màn hình, còn Vitest phủ các helper về tiền và xuất file. Giờ chỉ một quy tắc định dạng chi phối mọi file xuất, và báo cáo toàn công ty chỉ cần vài lượt batch. Tôi chưa đo thời gian, nên không đưa ra con số về tốc độ.
Nếu làm lại
Tôi sẽ chốt ý nghĩa của ô trống với đội tài chính ngay ngày đầu, trước khi có dòng code nào cho lưới, vì đổi về sau phải sửa mọi phép tổng hợp. Tôi cũng sẽ đo thời gian xuất file trước và sau khi chia batch, để có con số cụ thể cho mức cải thiện.