sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
{
|
||||
"TRANG BÌA": {
|
||||
"Tên sáng kiến (Tiếng Việt)": "",
|
||||
"Tác giả/nhóm tác giả sáng kiến": "",
|
||||
"Đơn vị công tác": "",
|
||||
"Thông tin liên hệ (Điện thoại, Email)": "",
|
||||
"Năm": ""
|
||||
},
|
||||
"MẪU SỐ 01 - BÁO CÁO MÔ TẢ SÁNG KIẾN": {
|
||||
"1. Mở đầu": "",
|
||||
"2. Tên sáng kiến (tên quy trình, giải pháp, phương pháp)": "",
|
||||
"3. Lĩnh vực áp dụng của sáng kiến": "",
|
||||
"4. Mô tả sáng kiến": {
|
||||
"4.1 Tình trạng giải pháp đã biết hoặc hiện trạng công tác khi chưa có sáng kiến": "",
|
||||
"4.2 Nội dung giải pháp đề nghị công nhận là sáng kiến": {
|
||||
"Mục đích của sáng kiến": "",
|
||||
"Về nội dung của sáng kiến": {
|
||||
"Các bước thực hiện giải pháp": "",
|
||||
"Các điều kiện cần thiết để áp dụng giải pháp": "",
|
||||
"Lĩnh vực áp dụng": "",
|
||||
"Kết quả thu được": "",
|
||||
"Danh sách đơn vị/cá nhân đã tham gia áp dụng thử hoặc lần đầu": [
|
||||
{
|
||||
"TT": "",
|
||||
"Tên tổ chức/cá nhân": "",
|
||||
"Địa chỉ": "",
|
||||
"Lĩnh vực áp dụng sáng kiến": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"Về tính mới của sáng kiến": "",
|
||||
"Về tính hiệu quả": {
|
||||
"Tạo ra lợi ích kinh tế": "",
|
||||
"Đem lại hiệu quả trong giảng dạy": "",
|
||||
"Tăng năng suất lao động": "",
|
||||
"Nâng cao hiệu quả công việc": "",
|
||||
"Nâng cao chất lượng công việc, dịch vụ": "",
|
||||
"Giảm chi phí": "",
|
||||
"Cải thiện môi trường, điều kiện học tập, làm việc, sống": "",
|
||||
"Bảo vệ sức khỏe": "",
|
||||
"Đảm bảo an toàn lao động, PCCC": "",
|
||||
"Nâng cao khả năng, trình độ, nhận thức, trách nhiệm": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"6. Những thông tin cần được bảo mật (nếu có)": "",
|
||||
"Ngày ký": {
|
||||
"Ngày": "",
|
||||
"Tháng": "",
|
||||
"Năm": ""
|
||||
},
|
||||
"Lãnh đạo đơn vị (Ký, ghi rõ họ tên)": "",
|
||||
"Tác giả sáng kiến (Ký, ghi rõ họ tên)": ""
|
||||
},
|
||||
"MẪU SỐ 02 - ĐƠN ĐỀ NGHỊ CÔNG NHẬN SÁNG KIẾN": {
|
||||
"Đơn vị": "",
|
||||
"Danh sách tác giả": [
|
||||
{
|
||||
"STT": "",
|
||||
"Họ và tên": "",
|
||||
"Ngày tháng năm sinh": "",
|
||||
"Nơi công tác": "",
|
||||
"Chức danh": "",
|
||||
"Trình độ chuyên môn": "",
|
||||
"Tỷ lệ (%) đóng góp vào việc tạo ra sáng kiến": ""
|
||||
}
|
||||
],
|
||||
"Tên sáng kiến đề nghị xét công nhận": "",
|
||||
"Chủ đầu tư tạo ra sáng kiến": "",
|
||||
"Lĩnh vực áp dụng sáng kiến": "",
|
||||
"Ngày sáng kiến được áp dụng": "",
|
||||
"Nội dung của sáng kiến": "",
|
||||
"Phân loại sáng kiến (đánh dấu ☑)": {
|
||||
"Giải pháp kỹ thuật, quản lý, tác nghiệp, ứng dụng tiến bộ kỹ thuật áp dụng cho ĐHYD TP.HCM": false,
|
||||
"Sáng kiến – cải tiến kỹ thuật từ các nghiên cứu khoa học có kết quả được đăng tải trên các tạp chí, hội nghị trong nước và quốc tế": false,
|
||||
"Sáng kiến – cải tiến kỹ thuật từ sách, giáo trình, tài liệu tham khảo": false
|
||||
},
|
||||
"Những thông tin cần được bảo mật (nếu có)": "",
|
||||
"Các điều kiện cần thiết để áp dụng sáng kiến": "",
|
||||
"Đánh giá lợi ích theo ý kiến của tác giả": "",
|
||||
"Đánh giá lợi ích theo ý kiến của tổ chức, cá nhân đã tham gia áp dụng sáng kiến lần đầu": "",
|
||||
"Danh sách những người đã tham gia áp dụng thử hoặc áp dụng sáng kiến lần đầu": [
|
||||
{
|
||||
"Số TT": "",
|
||||
"Họ và tên": "",
|
||||
"Ngày tháng năm sinh": "",
|
||||
"Nơi công tác": "",
|
||||
"Chức danh": "",
|
||||
"Trình độ chuyên môn": "",
|
||||
"Nội dung công việc hỗ trợ": ""
|
||||
}
|
||||
],
|
||||
"Ngày ký": {
|
||||
"Ngày": "",
|
||||
"Tháng": "",
|
||||
"Năm": ""
|
||||
},
|
||||
"Xác nhận của lãnh đạo Đơn vị": "",
|
||||
"Tác giả sáng kiến (Ký, ghi rõ họ tên)": ""
|
||||
},
|
||||
"MẪU SỐ 03 - BẢN XÁC NHẬN TỶ LỆ (%) ĐÓNG GÓP VÀO VIỆC TẠO RA SÁNG KIẾN": {
|
||||
"Ngày ký": {
|
||||
"Ngày": "",
|
||||
"Tháng": "",
|
||||
"Năm": ""
|
||||
},
|
||||
"1. Tên sáng kiến": "",
|
||||
"2. Tác giả chính/Đại diện nhóm tác giả sáng kiến": "",
|
||||
"Chức vụ, đơn vị công tác": "",
|
||||
"Tỷ lệ đóng góp": [
|
||||
{
|
||||
"STT": "",
|
||||
"Họ và tên": "",
|
||||
"Đơn vị công tác": "",
|
||||
"% đóng góp": "",
|
||||
"Chữ ký xác nhận": ""
|
||||
}
|
||||
],
|
||||
"Tổng % đóng góp": "100",
|
||||
"Tác giả chính/Đại diện nhóm tác giả sáng kiến (chữ ký và ghi rõ họ tên)": ""
|
||||
},
|
||||
"MẪU SỐ 04 - PHIẾU ĐÁNH GIÁ SÁNG KIẾN": {
|
||||
"1. Tên sáng kiến": "",
|
||||
"2. Tác giả/đồng tác giả sáng kiến": "",
|
||||
"Chức vụ, đơn vị công tác": "",
|
||||
"3. Nội dung đánh giá": {
|
||||
"Tính mới (Tối đa 40 điểm)": {
|
||||
"Nhận xét": "",
|
||||
"Điểm chấm": ""
|
||||
},
|
||||
"Tính hiệu quả (Tối đa 60 điểm)": {
|
||||
"Nhận xét": "",
|
||||
"Điểm chấm": ""
|
||||
},
|
||||
"Tổng cộng": ""
|
||||
},
|
||||
"Kết luận": "",
|
||||
"Ngày ký": {
|
||||
"Ngày": "",
|
||||
"Tháng": "",
|
||||
"Năm": ""
|
||||
},
|
||||
"Thành viên Hội đồng (Ký, ghi rõ họ tên)": ""
|
||||
},
|
||||
"BẢN CAM KẾT": {
|
||||
"Ngày ký": {
|
||||
"Ngày": "",
|
||||
"Tháng": "",
|
||||
"Năm": ""
|
||||
},
|
||||
"Tiêu đề phụ (Áp dụng đối với cá nhân đăng ký xét công nhận sáng kiến – cải tiến kỹ thuật tại Đại học Y Dược TP. Hồ Chí Minh là tác giả của bài báo khoa học)": "",
|
||||
"I. THÔNG TIN CHỦ THỂ CAM KẾT": {
|
||||
"Tác giả đăng ký sáng kiến": "",
|
||||
"CCCD/Hộ chiếu số": "",
|
||||
"Đơn vị": "",
|
||||
"Tên Bài báo trong nước/quốc tế là sản phẩm của nhiệm vụ NCKH": "",
|
||||
"Năm xét công nhận sáng kiến": "",
|
||||
"Vai trò đối với bài báo (☑ vào ô tương ứng)": {
|
||||
"Tác giả chính Bài báo trong nước/quốc tế là sản phẩm của nhiệm vụ NCKH": false,
|
||||
"Đồng tác giả Bài báo trong nước/quốc tế là sản phẩm của nhiệm vụ NCKH": false
|
||||
}
|
||||
},
|
||||
"II. CAM KẾT NỘI DUNG (☑ vào ô tương ứng)": {
|
||||
"1. Quyền sở hữu đối với bài báo trong nước/quốc tế": {
|
||||
"Tôi là chủ sở hữu hợp pháp của bài báo hoặc được chủ sở hữu/đồng chủ sở hữu đồng ý cho sử dụng bài báo có tên nêu trên làm sản phẩm đăng ký xét công nhận sáng kiến – cải tiến kỹ thuật tại ĐHYD": false,
|
||||
"Trường hợp bài báo là sản phẩm của nhiệm vụ NCKH: chủ sở hữu bài báo (cơ quan) đồng ý cho tác giả/nhóm tác giả sử dụng bài báo có tên nêu trên để đăng ký xét công nhận sáng kiến – cải tiến kỹ thuật tại ĐHYD": false
|
||||
},
|
||||
"2. Đồng thuận của đồng tác giả bài báo trong nước/quốc tế": {
|
||||
"Tất cả đồng tác giả đã biết, đồng ý và ký xác nhận cho phép Tác giả đăng ký sáng kiến được sử dụng bài báo có tên nêu trên để đăng ký xét công nhận sáng kiến – cải tiến kỹ thuật tại ĐHYD": false
|
||||
},
|
||||
"3. Cam kết bài báo trong nước/quốc tế uy tín": {
|
||||
"Cá nhân đăng ký xét công nhận sáng kiến – cải tiến kỹ thuật tại ĐHYD đối với bài báo trong nước/quốc tế cam kết bài báo không thuộc 'Tạp chí săn mồi'. Tôi xin chịu trách nhiệm kiểm tra, đối chiếu và cung cấp bằng chứng khi được yêu cầu": false
|
||||
},
|
||||
"4. Tuân thủ pháp luật sở hữu trí tuệ": {
|
||||
"Tôi cam kết rằng việc sử dụng bài báo đăng ký xét công nhận sáng kiến tại ĐHYD sẽ không gây tranh chấp về: quyền tác giả/quyền liên quan, quyền sở hữu công nghiệp, tiết lộ bí mật kinh doanh, vi phạm bảo mật dữ liệu của bất kỳ bên thứ ba nào. Tôi chịu trách nhiệm trước pháp luật về tính trung thực, hợp pháp của hồ sơ": false
|
||||
}
|
||||
},
|
||||
"III. HẬU QUẢ PHÁP LÝ KHI THÔNG TIN KHÔNG TRUNG THỰC": "Tôi xin cam kết chịu trách nhiệm đối với các thông tin kê khai nêu trên. Nếu thông tin được khai trong bản cam kết này không đúng thì tôi chấp nhận: Hủy kết quả công nhận sáng kiến đã được xét (nếu có); Thu hồi, hủy các danh hiệu thi đua, khen thưởng, hoặc các quyền lợi phát sinh có sử dụng sáng kiến này để xét; Xử lý theo quy định pháp luật hiện hành và theo quy chế/quy định của ĐHYD. Cam kết này có hiệu lực kể từ ngày ký và ràng buộc đối với cá nhân cam kết trong suốt thời gian xét công nhận sáng kiến và sau khi kết thúc 02 năm.",
|
||||
"Người cam kết (Ký tên, ghi rõ họ tên)": ""
|
||||
},
|
||||
"BẢN XÁC NHẬN TÀI LIỆU THAM KHẢO (2.2.2)": {
|
||||
"Ngày ký": {
|
||||
"Ngày": "",
|
||||
"Tháng": "",
|
||||
"Năm": ""
|
||||
},
|
||||
"Tiêu đề phụ (Áp dụng đối với cá nhân đăng ký minh chứng là tài liệu tham khảo nhóm 2.2.2)": "",
|
||||
"I. THÔNG TIN ĐĂNG KÝ": {
|
||||
"Tác giả đăng ký sáng kiến": "",
|
||||
"CCCD/Hộ chiếu số": "",
|
||||
"Đơn vị": "",
|
||||
"Tên tài liệu tham khảo (theo Quyết định xuất bản)": "",
|
||||
"Năm xét công nhận sáng kiến": ""
|
||||
},
|
||||
"II. XÁC NHẬN VÀ CAM KẾT (☑ vào ô tương ứng)": {
|
||||
"1. Trung thực thông tin và minh chứng": {
|
||||
"Tôi cam đoan các thông tin kê khai và minh chứng đính kèm đối với tài liệu tham khảo là trung thực, đúng sự thật và phù hợp với Quyết định xuất bản trong giai đoạn quy định (15/4/2025–15/4/2026).": false
|
||||
},
|
||||
"2. Trách nhiệm pháp luật": {
|
||||
"Tôi hoàn toàn chịu trách nhiệm trước pháp luật và trước nhà trường về tính hợp pháp của tài liệu và nội dung đăng ký.": false
|
||||
},
|
||||
"3. Bổ sung hồ sơ khi được yêu cầu": {
|
||||
"Tôi đồng ý bổ sung hoặc chỉnh sửa hồ sơ khi được yêu cầu.": false
|
||||
}
|
||||
},
|
||||
"Người cam kết (Ký tên, ghi rõ họ tên)": ""
|
||||
},
|
||||
"BẢN XÁC NHẬN BÀI BÁO TRONG NƯỚC (2.1.2)": {
|
||||
"Ngày ký": {
|
||||
"Ngày": "",
|
||||
"Tháng": "",
|
||||
"Năm": ""
|
||||
},
|
||||
"Tiêu đề phụ (Áp dụng đối với cá nhân đăng ký minh chứng là bài báo tạp chí trong nước nhóm 2.1.2)": "",
|
||||
"I. THÔNG TIN ĐĂNG KÝ": {
|
||||
"Tác giả đăng ký sáng kiến": "",
|
||||
"CCCD/Hộ chiếu số": "",
|
||||
"Đơn vị": "",
|
||||
"Tên bài báo (tạp chí trong nước, giai đoạn xuất bản quy định)": "",
|
||||
"Năm xét công nhận sáng kiến": ""
|
||||
},
|
||||
"II. XÁC NHẬN VÀ CAM KẾT (☑ vào ô tương ứng)": {
|
||||
"1. Trung thực thông tin và minh chứng": {
|
||||
"Tôi cam đoan các thông tin kê khai và minh chứng đính kèm đối với bài báo trên tạp chí trong nước là trung thực, đúng sự thật và phù hợp với thời điểm xuất bản trong giai đoạn quy định (15/4/2025–15/4/2026).": false
|
||||
},
|
||||
"2. Trách nhiệm pháp luật": {
|
||||
"Tôi hoàn toàn chịu trách nhiệm trước pháp luật và trước nhà trường về tính hợp pháp của bài báo và nội dung đăng ký.": false
|
||||
},
|
||||
"3. Bổ sung hồ sơ khi được yêu cầu": {
|
||||
"Tôi đồng ý bổ sung hoặc chỉnh sửa hồ sơ khi được yêu cầu.": false
|
||||
}
|
||||
},
|
||||
"Người cam kết (Ký tên, ghi rõ họ tên)": ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"trang_bia": {
|
||||
"ten_sang_kien": "",
|
||||
"tac_gia": "",
|
||||
"don_vi": "",
|
||||
"thong_tin_lien_he": "",
|
||||
"nam": ""
|
||||
},
|
||||
"mau_01": {
|
||||
"mo_dau": "",
|
||||
"ten_sang_kien": "",
|
||||
"linh_vuc_ap_dung": "",
|
||||
"tinh_trang_da_biet": "",
|
||||
"muc_dich": "",
|
||||
"cac_buoc_thuc_hien": "",
|
||||
"dieu_kien_ap_dung": "",
|
||||
"linh_vuc_ap_dung_2": "",
|
||||
"ket_qua_thu_duoc": "",
|
||||
"danh_sach_ap_dung": [
|
||||
{ "tt": "1", "ten_to_chuc": "", "dia_chi": "", "linh_vuc": "" }
|
||||
],
|
||||
"tinh_moi": "",
|
||||
"tinh_hieu_qua": {
|
||||
"loi_ich_kinh_te": "",
|
||||
"hieu_qua_giang_day": "",
|
||||
"tang_nang_suat": "",
|
||||
"nang_cao_hieu_qua": "",
|
||||
"nang_cao_chat_luong": "",
|
||||
"giam_chi_phi": "",
|
||||
"cai_thien_moi_truong": "",
|
||||
"bao_ve_suc_khoe": "",
|
||||
"an_toan_lao_dong": "",
|
||||
"nang_cao_nhan_thuc": ""
|
||||
},
|
||||
"thong_tin_bao_mat": "",
|
||||
"ngay_ky": { "ngay": "", "thang": "", "nam": "" },
|
||||
"lanh_dao_don_vi": "",
|
||||
"tac_gia_sang_kien": ""
|
||||
},
|
||||
"mau_02": {
|
||||
"don_vi": "",
|
||||
"danh_sach_tac_gia": [
|
||||
{
|
||||
"stt": "1",
|
||||
"ho_ten": "",
|
||||
"ngay_sinh": "",
|
||||
"noi_cong_tac": "",
|
||||
"chuc_danh": "",
|
||||
"trinh_do": "",
|
||||
"ty_le": ""
|
||||
}
|
||||
],
|
||||
"ten_sang_kien": "",
|
||||
"chu_dau_tu": "",
|
||||
"linh_vuc_ap_dung": "",
|
||||
"ngay_ap_dung": "",
|
||||
"noi_dung": "",
|
||||
"phan_loai": {
|
||||
"giai_phap_ky_thuat": false,
|
||||
"sang_kien_tu_nckh": false,
|
||||
"sang_kien_tu_sach": false
|
||||
},
|
||||
"thong_tin_bao_mat": "",
|
||||
"dieu_kien_ap_dung": "",
|
||||
"danh_gia_tac_gia": "",
|
||||
"danh_gia_to_chuc": "",
|
||||
"danh_sach_tham_gia": [
|
||||
{
|
||||
"stt": "1",
|
||||
"ho_ten": "",
|
||||
"ngay_sinh": "",
|
||||
"noi_cong_tac": "",
|
||||
"chuc_danh": "",
|
||||
"trinh_do": "",
|
||||
"noi_dung_ho_tro": ""
|
||||
}
|
||||
],
|
||||
"ngay_ky": { "ngay": "", "thang": "", "nam": "" },
|
||||
"lanh_dao_don_vi": "",
|
||||
"tac_gia_sang_kien": ""
|
||||
},
|
||||
"mau_03": {
|
||||
"ngay_ky": { "ngay": "", "thang": "", "nam": "" },
|
||||
"ten_sang_kien": "",
|
||||
"tac_gia_chinh": "",
|
||||
"chuc_vu_don_vi": "",
|
||||
"ty_le_dong_gop": [
|
||||
{ "stt": "1", "ho_ten": "", "don_vi": "", "phan_tram": "", "chu_ky": "" }
|
||||
],
|
||||
"tac_gia_chinh_ky": ""
|
||||
},
|
||||
"mau_04": {
|
||||
"ten_sang_kien": "",
|
||||
"tac_gia": "",
|
||||
"chuc_vu_don_vi": "",
|
||||
"tinh_moi": { "nhan_xet": "", "diem": "" },
|
||||
"tinh_hieu_qua": { "nhan_xet": "", "diem": "" },
|
||||
"tong_cong": "",
|
||||
"ket_luan": "",
|
||||
"ngay_ky": { "ngay": "", "thang": "", "nam": "" },
|
||||
"thanh_vien_hoi_dong": ""
|
||||
},
|
||||
"ban_cam_ket": {
|
||||
"ngay_ky": { "ngay": "", "thang": "", "nam": "" },
|
||||
"tac_gia_dang_ky": "",
|
||||
"cccd": "",
|
||||
"don_vi": "",
|
||||
"ten_bai_bao": "",
|
||||
"nam_xet": "",
|
||||
"vai_tro": {
|
||||
"tac_gia_chinh": false,
|
||||
"dong_tac_gia": false
|
||||
},
|
||||
"cam_ket": {
|
||||
"quyen_so_huu_1": false,
|
||||
"quyen_so_huu_2": false,
|
||||
"dong_thuan": false,
|
||||
"bai_bao_uy_tin": false,
|
||||
"tuan_thu_phap_luat": false
|
||||
},
|
||||
"nguoi_cam_ket": ""
|
||||
},
|
||||
"reference_material_honesty": {
|
||||
"ngay_ky": { "ngay": "", "thang": "", "nam": "" },
|
||||
"tac_gia_dang_ky": "",
|
||||
"cccd": "",
|
||||
"don_vi": "",
|
||||
"ten_tai_lieu": "",
|
||||
"nam_xet": "",
|
||||
"cam_ket": {
|
||||
"thong_tin_trung_thuc": false,
|
||||
"trach_nhiem_phap_luat": false,
|
||||
"bo_sung_khi_yeu_cau": false
|
||||
},
|
||||
"nguoi_cam_ket": ""
|
||||
},
|
||||
"research_domestic_honesty": {
|
||||
"ngay_ky": { "ngay": "", "thang": "", "nam": "" },
|
||||
"tieu_de_phu": "",
|
||||
"tac_gia_dang_ky": "",
|
||||
"cccd": "",
|
||||
"don_vi": "",
|
||||
"ten_bai_bao": "",
|
||||
"nam_xet": "",
|
||||
"cam_ket": {
|
||||
"thong_tin_trung_thuc": false,
|
||||
"trach_nhiem_phap_luat": false,
|
||||
"bo_sung_khi_yeu_cau": false
|
||||
},
|
||||
"nguoi_cam_ket": ""
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,627 @@
|
||||
# Building the Sáng Kiến Application Form — TypeScript Component Guide
|
||||
|
||||
This guide explains how to turn the three artifacts in this folder into a working web form that:
|
||||
|
||||
1. Renders the full UMP "Sáng kiến – Cải tiến kỹ thuật" application (cover + Mẫu 01–04 + Bản Cam Kết).
|
||||
2. Captures user input into a `FormData` object whose shape matches `data_blank.json` exactly.
|
||||
3. POSTs that object to a backend endpoint that uses **`docxtpl`** (or `docx-templates`) to render `template_application_form.docx` and return a downloadable `.docx`.
|
||||
|
||||
---
|
||||
|
||||
## 1. The Three Artifacts and Their Roles
|
||||
|
||||
| File | Role | Used by |
|
||||
|---|---|---|
|
||||
| `template_application_form.docx` | The Word document with `{{ jinja2 }}` placeholders. **Do not edit by hand**; round-trips through Word will preserve placeholders only if they live in single text runs. | Backend renderer |
|
||||
| `data_blank.json` | The canonical, flat, snake_case shape. **Every key here matches a placeholder in the docx 1:1.** This is the contract between the frontend and the renderer. | Frontend `FormData` type, backend context object |
|
||||
| `bieu_mau_sang_kien_template.json` | Vietnamese-labeled mirror of the same structure. Use it as the source of human labels when you want to avoid hard-coding Vietnamese strings inside components. | Frontend label lookups, i18n |
|
||||
|
||||
The form has six top-level sections corresponding to the six top-level keys of `data_blank.json`:
|
||||
|
||||
```
|
||||
trang_bia → Cover page
|
||||
mau_01 → Mẫu số 01: Báo cáo mô tả sáng kiến
|
||||
mau_02 → Mẫu số 02: Đơn đề nghị công nhận sáng kiến
|
||||
mau_03 → Mẫu số 03: Bản xác nhận tỷ lệ (%) đóng góp
|
||||
mau_04 → Mẫu số 04: Phiếu đánh giá sáng kiến (đánh giá của hội đồng)
|
||||
ban_cam_ket → Bản cam kết (tác giả bài báo khoa học)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. TypeScript Types (derive these from `data_blank.json`)
|
||||
|
||||
Place this file at `src/types/sangKien.ts`. The shape mirrors `data_blank.json` exactly — if you add or rename a field in the docx, update this file, the JSON, and the placeholder in the same commit.
|
||||
|
||||
```ts
|
||||
// src/types/sangKien.ts
|
||||
|
||||
export interface NgayKy {
|
||||
ngay: string; // "DD" — keep as string so leading zeros survive
|
||||
thang: string; // "MM"
|
||||
nam: string; // "YYYY"
|
||||
}
|
||||
|
||||
export interface TrangBia {
|
||||
ten_sang_kien: string;
|
||||
tac_gia: string;
|
||||
don_vi: string;
|
||||
thong_tin_lien_he: string;
|
||||
nam: string;
|
||||
}
|
||||
|
||||
export interface DanhSachApDungRow {
|
||||
tt: string;
|
||||
ten_to_chuc: string;
|
||||
dia_chi: string;
|
||||
linh_vuc: string;
|
||||
}
|
||||
|
||||
export interface TinhHieuQua {
|
||||
loi_ich_kinh_te: string;
|
||||
hieu_qua_giang_day: string;
|
||||
tang_nang_suat: string;
|
||||
nang_cao_hieu_qua: string;
|
||||
nang_cao_chat_luong: string;
|
||||
giam_chi_phi: string;
|
||||
cai_thien_moi_truong: string;
|
||||
bao_ve_suc_khoe: string;
|
||||
an_toan_lao_dong: string;
|
||||
nang_cao_nhan_thuc: string;
|
||||
}
|
||||
|
||||
export interface Mau01 {
|
||||
mo_dau: string;
|
||||
ten_sang_kien: string;
|
||||
linh_vuc_ap_dung: string;
|
||||
tinh_trang_da_biet: string;
|
||||
muc_dich: string;
|
||||
cac_buoc_thuc_hien: string;
|
||||
dieu_kien_ap_dung: string;
|
||||
linh_vuc_ap_dung_2: string; // distinct from linh_vuc_ap_dung above — see PDF §4.2
|
||||
ket_qua_thu_duoc: string;
|
||||
danh_sach_ap_dung: DanhSachApDungRow[];
|
||||
tinh_moi: string;
|
||||
tinh_hieu_qua: TinhHieuQua;
|
||||
thong_tin_bao_mat: string;
|
||||
ngay_ky: NgayKy;
|
||||
lanh_dao_don_vi: string;
|
||||
tac_gia_sang_kien: string;
|
||||
}
|
||||
|
||||
export interface TacGiaRow {
|
||||
stt: string;
|
||||
ho_ten: string;
|
||||
ngay_sinh: string;
|
||||
noi_cong_tac: string;
|
||||
chuc_danh: string;
|
||||
trinh_do: string;
|
||||
ty_le: string; // percentage as string, e.g. "60"
|
||||
}
|
||||
|
||||
export interface ThamGiaRow {
|
||||
stt: string;
|
||||
ho_ten: string;
|
||||
ngay_sinh: string;
|
||||
noi_cong_tac: string;
|
||||
chuc_danh: string;
|
||||
trinh_do: string;
|
||||
noi_dung_ho_tro: string;
|
||||
}
|
||||
|
||||
export interface PhanLoai {
|
||||
giai_phap_ky_thuat: boolean;
|
||||
sang_kien_tu_nckh: boolean;
|
||||
sang_kien_tu_sach: boolean;
|
||||
}
|
||||
|
||||
export interface Mau02 {
|
||||
don_vi: string;
|
||||
danh_sach_tac_gia: TacGiaRow[];
|
||||
ten_sang_kien: string;
|
||||
chu_dau_tu: string;
|
||||
linh_vuc_ap_dung: string;
|
||||
ngay_ap_dung: string;
|
||||
noi_dung: string;
|
||||
phan_loai: PhanLoai; // mutually exclusive in practice — radio-style group
|
||||
thong_tin_bao_mat: string;
|
||||
dieu_kien_ap_dung: string;
|
||||
danh_gia_tac_gia: string;
|
||||
danh_gia_to_chuc: string;
|
||||
danh_sach_tham_gia: ThamGiaRow[];
|
||||
ngay_ky: NgayKy;
|
||||
lanh_dao_don_vi: string;
|
||||
tac_gia_sang_kien: string;
|
||||
}
|
||||
|
||||
export interface TyLeDongGopRow {
|
||||
stt: string;
|
||||
ho_ten: string;
|
||||
don_vi: string;
|
||||
phan_tram: string;
|
||||
chu_ky: string;
|
||||
}
|
||||
|
||||
export interface Mau03 {
|
||||
ngay_ky: NgayKy;
|
||||
ten_sang_kien: string;
|
||||
tac_gia_chinh: string;
|
||||
chuc_vu_don_vi: string;
|
||||
ty_le_dong_gop: TyLeDongGopRow[]; // sum of phan_tram MUST equal 100
|
||||
tac_gia_chinh_ky: string;
|
||||
}
|
||||
|
||||
export interface DiemNhanXet {
|
||||
nhan_xet: string;
|
||||
diem: string; // numeric in a string, e.g. "35"
|
||||
}
|
||||
|
||||
export interface Mau04 {
|
||||
ten_sang_kien: string;
|
||||
tac_gia: string;
|
||||
chuc_vu_don_vi: string;
|
||||
tinh_moi: DiemNhanXet; // max 40
|
||||
tinh_hieu_qua: DiemNhanXet; // max 60
|
||||
tong_cong: string; // = tinh_moi.diem + tinh_hieu_qua.diem (compute, do not free-edit)
|
||||
ket_luan: string;
|
||||
ngay_ky: NgayKy;
|
||||
thanh_vien_hoi_dong: string;
|
||||
}
|
||||
|
||||
export interface VaiTroBaiBao {
|
||||
tac_gia_chinh: boolean; // mutually exclusive with dong_tac_gia
|
||||
dong_tac_gia: boolean;
|
||||
}
|
||||
|
||||
export interface CamKetChecks {
|
||||
quyen_so_huu_1: boolean;
|
||||
quyen_so_huu_2: boolean;
|
||||
dong_thuan: boolean;
|
||||
bai_bao_uy_tin: boolean;
|
||||
tuan_thu_phap_luat: boolean;
|
||||
}
|
||||
|
||||
export interface BanCamKet {
|
||||
ngay_ky: NgayKy;
|
||||
tac_gia_dang_ky: string;
|
||||
cccd: string;
|
||||
don_vi: string;
|
||||
ten_bai_bao: string;
|
||||
nam_xet: string; // year, e.g. "2026"
|
||||
vai_tro: VaiTroBaiBao;
|
||||
cam_ket: CamKetChecks;
|
||||
nguoi_cam_ket: string;
|
||||
}
|
||||
|
||||
export interface SangKienFormData {
|
||||
trang_bia: TrangBia;
|
||||
mau_01: Mau01;
|
||||
mau_02: Mau02;
|
||||
mau_03: Mau03;
|
||||
mau_04: Mau04;
|
||||
ban_cam_ket: BanCamKet;
|
||||
}
|
||||
```
|
||||
|
||||
The blank initial value is just the JSON file imported and cast:
|
||||
|
||||
```ts
|
||||
import blank from '../../assets/data_blank.json';
|
||||
import type { SangKienFormData } from '../types/sangKien';
|
||||
|
||||
export const emptyForm: SangKienFormData = blank as SangKienFormData;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Field-by-Field Rendering Reference
|
||||
|
||||
The table below is the **single source of truth** for which UI control to render. Sections that aren't filled by the applicant (Mẫu 04 is filled by the council; signatures are filled at print time) should still be present in the schema so the template can render placeholders.
|
||||
|
||||
### 3.1 `trang_bia` — Cover
|
||||
|
||||
| Path | Control | Notes |
|
||||
|---|---|---|
|
||||
| `ten_sang_kien` | `textarea` (2 rows) | Required, max ~300 chars |
|
||||
| `tac_gia` | `text` | Required |
|
||||
| `don_vi` | `text` | Required |
|
||||
| `thong_tin_lien_he` | `text` | "phone, email" — accept free format |
|
||||
| `nam` | `number` (year picker) | Default = current year |
|
||||
|
||||
### 3.2 `mau_01` — Báo cáo mô tả sáng kiến
|
||||
|
||||
| Path | Control | Notes |
|
||||
|---|---|---|
|
||||
| `mo_dau` | `textarea` (rich, 8+ rows) | Required |
|
||||
| `ten_sang_kien` | `text` | Auto-mirror from `trang_bia.ten_sang_kien` (allow override) |
|
||||
| `linh_vuc_ap_dung` | `text` | E.g. "quản lý giáo dục" |
|
||||
| `tinh_trang_da_biet` | `textarea` | Required |
|
||||
| `muc_dich` | `textarea` | Required |
|
||||
| `cac_buoc_thuc_hien` | `textarea` (rich) | Required |
|
||||
| `dieu_kien_ap_dung` | `textarea` | |
|
||||
| `linh_vuc_ap_dung_2` | `text` | **Different** from `linh_vuc_ap_dung` — this is the field inside §4.2; keep both even if they often duplicate |
|
||||
| `ket_qua_thu_duoc` | `textarea` | Required |
|
||||
| `danh_sach_ap_dung[]` | dynamic table | columns: TT, Tên tổ chức/cá nhân, Địa chỉ, Lĩnh vực. Allow add/remove rows |
|
||||
| `tinh_moi` | `textarea` | Required |
|
||||
| `tinh_hieu_qua.*` (10 fields) | `textarea` (3–4 rows each) | All optional individually but at least one should be filled |
|
||||
| `thong_tin_bao_mat` | `textarea` | Optional |
|
||||
| `ngay_ky.{ngay,thang,nam}` | three `number` inputs OR a single date picker that decomposes on submit | |
|
||||
| `lanh_dao_don_vi` | `text` | Name of unit leader (signature label) |
|
||||
| `tac_gia_sang_kien` | `text` | Auto-mirror from `trang_bia.tac_gia` |
|
||||
|
||||
### 3.3 `mau_02` — Đơn đề nghị công nhận sáng kiến
|
||||
|
||||
| Path | Control | Notes |
|
||||
|---|---|---|
|
||||
| `don_vi` | `text` | Auto-mirror from `trang_bia.don_vi` |
|
||||
| `danh_sach_tac_gia[]` | dynamic table | 7 columns. **Validation: sum of `ty_le` should equal 100.** |
|
||||
| `ten_sang_kien` | `text` | Auto-mirror |
|
||||
| `chu_dau_tu` | `text` | Often the unit name |
|
||||
| `linh_vuc_ap_dung` | `text` | Auto-mirror from `mau_01.linh_vuc_ap_dung` |
|
||||
| `ngay_ap_dung` | `date` | The day the innovation began being used |
|
||||
| `noi_dung` | `textarea` (rich) | Required |
|
||||
| `phan_loai.*` (3 booleans) | `radio` group rendered as 3 checkboxes | Spec says "đánh dấu ☑" — applicant typically picks ONE. Enforce single-selection in UI but keep all three booleans in the data shape because the docx renders three independent ☑/☐. |
|
||||
| `thong_tin_bao_mat` | `textarea` | Optional |
|
||||
| `dieu_kien_ap_dung` | `textarea` | |
|
||||
| `danh_gia_tac_gia` | `textarea` | Required |
|
||||
| `danh_gia_to_chuc` | `textarea` | If §danh_sach_ap_dung has rows, this should be filled |
|
||||
| `danh_sach_tham_gia[]` | dynamic table | 7 columns; people who tested the innovation |
|
||||
| `ngay_ky` | as above | |
|
||||
| `lanh_dao_don_vi`, `tac_gia_sang_kien` | `text` | |
|
||||
|
||||
### 3.4 `mau_03` — Bản xác nhận tỷ lệ đóng góp
|
||||
|
||||
| Path | Control | Notes |
|
||||
|---|---|---|
|
||||
| `ngay_ky` | as above | |
|
||||
| `ten_sang_kien`, `tac_gia_chinh`, `chuc_vu_don_vi` | `text` | Auto-mirror where possible |
|
||||
| `ty_le_dong_gop[]` | dynamic table | columns: STT, Họ và tên, Đơn vị, % đóng góp, Chữ ký. **Validation: sum of `phan_tram` MUST equal 100** (block submit otherwise). The signature column is filled by hand on the printed copy — leave a blank in the rendered docx. |
|
||||
| `tac_gia_chinh_ky` | `text` | Name of main author at signature |
|
||||
|
||||
### 3.5 `mau_04` — Phiếu đánh giá (council use)
|
||||
|
||||
This page is filled by the evaluation council, not the applicant. In the applicant-facing UI, **either hide this section behind a role check or render it read-only** so the data round-trips when the docx is regenerated.
|
||||
|
||||
| Path | Control | Notes |
|
||||
|---|---|---|
|
||||
| `ten_sang_kien`, `tac_gia`, `chuc_vu_don_vi` | `text` | Pre-filled |
|
||||
| `tinh_moi.{nhan_xet,diem}` | textarea + number (0–40) | |
|
||||
| `tinh_hieu_qua.{nhan_xet,diem}` | textarea + number (0–60) | |
|
||||
| `tong_cong` | computed | = `Number(tinh_moi.diem) + Number(tinh_hieu_qua.diem)`; render as readonly |
|
||||
| `ket_luan` | `textarea` | |
|
||||
| `ngay_ky`, `thanh_vien_hoi_dong` | as above | |
|
||||
|
||||
### 3.6 `ban_cam_ket` — Bản cam kết (article-author commitment)
|
||||
|
||||
This is the section described by the PDF you provided. It only applies when the innovation is based on a published article.
|
||||
|
||||
| Path | Control | Notes |
|
||||
|---|---|---|
|
||||
| `ngay_ky` | as above | |
|
||||
| `tac_gia_dang_ky` | `text` | Required, ~120 chars |
|
||||
| `cccd` | `text` | National ID (12 digits) or passport — accept either format, validate length 8–12 |
|
||||
| `don_vi` | `text` | Auto-mirror from `trang_bia.don_vi` |
|
||||
| `ten_bai_bao` | `textarea` (2 rows) | Article title (VN or international) |
|
||||
| `nam_xet` | `number` (year) | The year of submission. Defaults to `trang_bia.nam`. **This year also appears in the form's subtitle** ("...năm {nam_xet}..."). |
|
||||
| `vai_tro.tac_gia_chinh` / `vai_tro.dong_tac_gia` | radio-style pair of checkboxes | Mutually exclusive — selecting one clears the other. |
|
||||
| `cam_ket.quyen_so_huu_1` | checkbox | "Tôi là chủ sở hữu hợp pháp..." |
|
||||
| `cam_ket.quyen_so_huu_2` | checkbox | "Trường hợp bài báo là sản phẩm của nhiệm vụ NCKH..." — at least ONE of `quyen_so_huu_1` / `quyen_so_huu_2` must be checked |
|
||||
| `cam_ket.dong_thuan` | checkbox (required) | "Tất cả đồng tác giả đã biết, đồng ý..." |
|
||||
| `cam_ket.bai_bao_uy_tin` | checkbox (required) | "Cam kết bài báo không thuộc 'Tạp chí săn mồi'" |
|
||||
| `cam_ket.tuan_thu_phap_luat` | checkbox (required) | "Tuân thủ pháp luật sở hữu trí tuệ" |
|
||||
| `nguoi_cam_ket` | `text` | Same person as `tac_gia_dang_ky`, used as signature label |
|
||||
|
||||
---
|
||||
|
||||
## 4. Recommended File Layout
|
||||
|
||||
```
|
||||
src/
|
||||
types/
|
||||
sangKien.ts # types from §2
|
||||
assets/
|
||||
data_blank.json # imported as initial state
|
||||
bieu_mau_sang_kien_template.json # imported for VN labels (optional)
|
||||
hooks/
|
||||
useSangKienForm.ts # wraps react-hook-form, handles auto-mirror + validation
|
||||
components/
|
||||
sang-kien/
|
||||
SangKienForm.tsx # top-level form, tabs/accordions for the 6 sections
|
||||
sections/
|
||||
TrangBiaSection.tsx
|
||||
Mau01Section.tsx
|
||||
Mau02Section.tsx
|
||||
Mau03Section.tsx
|
||||
Mau04Section.tsx # gated behind role
|
||||
BanCamKetSection.tsx
|
||||
controls/
|
||||
DateTriple.tsx # renders ngay/thang/nam as one row
|
||||
DynamicTable.tsx # generic add/remove rows for arrays
|
||||
ExclusiveCheckGroup.tsx # for vai_tro and phan_loai
|
||||
TextArea.tsx
|
||||
api/
|
||||
renderDocx.ts # POST FormData → returns Blob (docx)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Recommended Stack
|
||||
|
||||
- **React 18+** with **TypeScript**
|
||||
- **react-hook-form** for state, with **zod** resolvers for validation
|
||||
- **shadcn/ui** or your existing component library for inputs
|
||||
- **Backend**: any service that runs `python-docx-template` — a 30-line FastAPI/Flask endpoint is enough
|
||||
|
||||
A minimal zod schema for one section (sketch):
|
||||
|
||||
```ts
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ngayKySchema = z.object({
|
||||
ngay: z.string().regex(/^\d{1,2}$/),
|
||||
thang: z.string().regex(/^\d{1,2}$/),
|
||||
nam: z.string().regex(/^\d{4}$/),
|
||||
});
|
||||
|
||||
export const banCamKetSchema = z.object({
|
||||
ngay_ky: ngayKySchema,
|
||||
tac_gia_dang_ky: z.string().min(1, 'Bắt buộc'),
|
||||
cccd: z.string().min(8).max(12),
|
||||
don_vi: z.string().min(1),
|
||||
ten_bai_bao: z.string().min(1),
|
||||
nam_xet: z.string().regex(/^\d{4}$/),
|
||||
vai_tro: z
|
||||
.object({
|
||||
tac_gia_chinh: z.boolean(),
|
||||
dong_tac_gia: z.boolean(),
|
||||
})
|
||||
.refine(
|
||||
v => Number(v.tac_gia_chinh) + Number(v.dong_tac_gia) === 1,
|
||||
{ message: 'Phải chọn đúng một vai trò' }
|
||||
),
|
||||
cam_ket: z
|
||||
.object({
|
||||
quyen_so_huu_1: z.boolean(),
|
||||
quyen_so_huu_2: z.boolean(),
|
||||
dong_thuan: z.boolean(),
|
||||
bai_bao_uy_tin: z.boolean(),
|
||||
tuan_thu_phap_luat: z.boolean(),
|
||||
})
|
||||
.refine(v => v.quyen_so_huu_1 || v.quyen_so_huu_2, {
|
||||
path: ['quyen_so_huu_1'],
|
||||
message: 'Phải chọn ít nhất một mục về quyền sở hữu',
|
||||
})
|
||||
.refine(v => v.dong_thuan && v.bai_bao_uy_tin && v.tuan_thu_phap_luat, {
|
||||
message: 'Phải xác nhận tất cả các cam kết bắt buộc',
|
||||
}),
|
||||
nguoi_cam_ket: z.string().min(1),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Auto-Mirroring Fields
|
||||
|
||||
Several values logically duplicate. Wire these one-way bindings inside `useSangKienForm`:
|
||||
|
||||
```
|
||||
trang_bia.ten_sang_kien → mau_01.ten_sang_kien
|
||||
→ mau_02.ten_sang_kien
|
||||
→ mau_03.ten_sang_kien
|
||||
→ mau_04.ten_sang_kien
|
||||
|
||||
trang_bia.tac_gia → mau_01.tac_gia_sang_kien
|
||||
→ mau_02.tac_gia_sang_kien
|
||||
→ mau_04.tac_gia
|
||||
→ ban_cam_ket.tac_gia_dang_ky
|
||||
→ ban_cam_ket.nguoi_cam_ket
|
||||
→ mau_03.tac_gia_chinh
|
||||
→ mau_03.tac_gia_chinh_ky
|
||||
|
||||
trang_bia.don_vi → mau_02.don_vi
|
||||
→ ban_cam_ket.don_vi
|
||||
|
||||
trang_bia.nam → ban_cam_ket.nam_xet (default only)
|
||||
```
|
||||
|
||||
Use `useWatch` to populate these on first focus of the destination field; allow the user to override (some authors sign Mẫu 03 with a fuller title than they put on the cover).
|
||||
|
||||
---
|
||||
|
||||
## 7. Submitting and Rendering
|
||||
|
||||
The frontend never builds the docx — it only POSTs the form data. Two options:
|
||||
|
||||
### 7a. Python backend (recommended — matches the template author's environment)
|
||||
|
||||
```python
|
||||
# server/render.py
|
||||
from io import BytesIO
|
||||
from docxtpl import DocxTemplate
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
TEMPLATE_PATH = "template_application_form.docx"
|
||||
|
||||
@app.post("/render")
|
||||
def render(payload: dict): # accept full FormData object
|
||||
doc = DocxTemplate(TEMPLATE_PATH)
|
||||
doc.render(payload)
|
||||
buf = BytesIO()
|
||||
doc.save(buf)
|
||||
buf.seek(0)
|
||||
return StreamingResponse(
|
||||
buf,
|
||||
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
headers={"Content-Disposition": 'attachment; filename="sang_kien.docx"'},
|
||||
)
|
||||
```
|
||||
|
||||
### 7b. Node backend
|
||||
|
||||
Use `docxtemplater` with the same template — it understands `{{ }}` and `{% %}` tags via the `nunjucks-docxtemplater-module` or manual angular-parser equivalent. This is more setup; pick 7a if you have the choice.
|
||||
|
||||
### Frontend call
|
||||
|
||||
```ts
|
||||
// src/api/renderDocx.ts
|
||||
import type { SangKienFormData } from '../types/sangKien';
|
||||
|
||||
export async function renderDocx(data: SangKienFormData): Promise<Blob> {
|
||||
const res = await fetch('/api/render', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Render failed: ${res.status}`);
|
||||
return await res.blob();
|
||||
}
|
||||
|
||||
// usage inside component:
|
||||
const onSubmit = async (data: SangKienFormData) => {
|
||||
const blob = await renderDocx(data);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = Object.assign(document.createElement('a'), {
|
||||
href: url, download: 'sang_kien.docx',
|
||||
});
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Validation Rules to Enforce Before Submit
|
||||
|
||||
1. **`trang_bia`**: all five fields non-empty.
|
||||
2. **`mau_01.danh_sach_ap_dung`**: every row has all four columns filled OR remove the row.
|
||||
3. **`mau_02.danh_sach_tac_gia`**: same; **plus** sum of `ty_le` (parsed as int) === 100.
|
||||
4. **`mau_02.phan_loai`**: at least one of the three booleans is `true` (typical case: exactly one).
|
||||
5. **`mau_03.ty_le_dong_gop`**: sum of `phan_tram` === 100.
|
||||
6. **`mau_04.tinh_moi.diem`** ≤ 40, **`mau_04.tinh_hieu_qua.diem`** ≤ 60, `tong_cong = sum`.
|
||||
7. **`ban_cam_ket`**:
|
||||
- exactly one of `vai_tro.tac_gia_chinh` / `vai_tro.dong_tac_gia` is true;
|
||||
- at least one of `cam_ket.quyen_so_huu_1` / `quyen_so_huu_2` is true;
|
||||
- `cam_ket.dong_thuan` AND `cam_ket.bai_bao_uy_tin` AND `cam_ket.tuan_thu_phap_luat` are all true.
|
||||
8. **`ngay_ky.nam`** is a 4-digit year ≥ current year.
|
||||
|
||||
---
|
||||
|
||||
## 9. Common Pitfalls
|
||||
|
||||
- **Don't change keys in `data_blank.json` without updating the docx.** Open the docx, search for `{{ key }}`, and replace there too. The docx uses Jinja2-style `{{ ... }}` tags inside Word text runs; if Word splits the placeholder across runs (which it can do silently), `docxtpl` will fail. Use the included unpack/pack scripts when editing.
|
||||
- **Booleans must stay booleans, not "true"/"false" strings.** The `{% if %}` checks treat strings as truthy.
|
||||
- **Empty arrays render as one empty row** because the table loops `{% tr for item in ... %}` over the array. If you want to suppress empty tables, filter out fully-empty rows on the client before submit.
|
||||
- **Vietnamese diacritics**: send `Content-Type: application/json; charset=utf-8`. Most fetch defaults are correct, but check the backend echoes `nguyễn` not `nguyá»…n`.
|
||||
- **Dates as `string`**: keep them as strings throughout. JavaScript `Date` objects round-trip lossy through JSON and break leading zeros.
|
||||
|
||||
---
|
||||
|
||||
## 10. Quick Component Sketch (for one section)
|
||||
|
||||
A minimal `BanCamKetSection.tsx` to anchor the rest of the implementation:
|
||||
|
||||
```tsx
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import type { SangKienFormData } from '../../../types/sangKien';
|
||||
|
||||
export function BanCamKetSection() {
|
||||
const { register, watch, setValue } = useFormContext<SangKienFormData>();
|
||||
const vaiTro = watch('ban_cam_ket.vai_tro');
|
||||
|
||||
// mutually-exclusive helper
|
||||
const pickRole = (role: 'tac_gia_chinh' | 'dong_tac_gia') => {
|
||||
setValue('ban_cam_ket.vai_tro.tac_gia_chinh', role === 'tac_gia_chinh');
|
||||
setValue('ban_cam_ket.vai_tro.dong_tac_gia', role === 'dong_tac_gia');
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>Bản Cam Kết</h2>
|
||||
|
||||
<label>Tác giả đăng ký sáng kiến
|
||||
<input {...register('ban_cam_ket.tac_gia_dang_ky')} />
|
||||
</label>
|
||||
|
||||
<label>CCCD/Hộ chiếu số
|
||||
<input {...register('ban_cam_ket.cccd')} />
|
||||
</label>
|
||||
|
||||
<label>Đơn vị
|
||||
<input {...register('ban_cam_ket.don_vi')} />
|
||||
</label>
|
||||
|
||||
<label>Tên bài báo
|
||||
<textarea rows={2} {...register('ban_cam_ket.ten_bai_bao')} />
|
||||
</label>
|
||||
|
||||
<label>Năm xét
|
||||
<input type="number" {...register('ban_cam_ket.nam_xet')} />
|
||||
</label>
|
||||
|
||||
<fieldset>
|
||||
<legend>Vai trò đối với bài báo</legend>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
checked={vaiTro?.tac_gia_chinh}
|
||||
onChange={() => pickRole('tac_gia_chinh')} />
|
||||
Tác giả chính
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
checked={vaiTro?.dong_tac_gia}
|
||||
onChange={() => pickRole('dong_tac_gia')} />
|
||||
Đồng tác giả
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Cam kết nội dung</legend>
|
||||
<label><input type="checkbox" {...register('ban_cam_ket.cam_ket.quyen_so_huu_1')} />
|
||||
Tôi là chủ sở hữu hợp pháp của bài báo…
|
||||
</label>
|
||||
<label><input type="checkbox" {...register('ban_cam_ket.cam_ket.quyen_so_huu_2')} />
|
||||
Trường hợp bài báo là sản phẩm của nhiệm vụ NCKH…
|
||||
</label>
|
||||
<label><input type="checkbox" {...register('ban_cam_ket.cam_ket.dong_thuan')} />
|
||||
Tất cả đồng tác giả đã biết, đồng ý…
|
||||
</label>
|
||||
<label><input type="checkbox" {...register('ban_cam_ket.cam_ket.bai_bao_uy_tin')} />
|
||||
Cam kết bài báo không thuộc "Tạp chí săn mồi"
|
||||
</label>
|
||||
<label><input type="checkbox" {...register('ban_cam_ket.cam_ket.tuan_thu_phap_luat')} />
|
||||
Tuân thủ pháp luật sở hữu trí tuệ
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<label>Người cam kết (ký tên)
|
||||
<input {...register('ban_cam_ket.nguoi_cam_ket')} />
|
||||
</label>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Replicate the same pattern for the other five sections, using `useFieldArray` from react-hook-form for the dynamic tables (`danh_sach_ap_dung`, `danh_sach_tac_gia`, `danh_sach_tham_gia`, `ty_le_dong_gop`).
|
||||
|
||||
---
|
||||
|
||||
## 11. End-to-End Sanity Check
|
||||
|
||||
Before shipping, run this contract test:
|
||||
|
||||
```ts
|
||||
import blank from './assets/data_blank.json';
|
||||
import { sangKienSchema } from './types/sangKien.zod';
|
||||
|
||||
// 1) The blank file must satisfy the zod schema *with optional fields permitted*.
|
||||
sangKienSchema.partial().parse(blank);
|
||||
|
||||
// 2) A round-trip via the backend must return a non-empty Blob.
|
||||
const out = await renderDocx(blank as any);
|
||||
console.assert(out.size > 5000, 'docx render produced suspiciously small output');
|
||||
```
|
||||
|
||||
If both assertions pass, the frontend, the schema, and the docx renderer are in agreement — and any bug from there onward is a UI bug, not a contract drift.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 406 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="SKI">
|
||||
<rect width="64" height="64" rx="10" fill="#1e3a8a"/>
|
||||
<text x="32" y="40" text-anchor="middle" fill="#ffffff" font-size="18" font-family="system-ui,sans-serif" font-weight="600">SKI</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 289 B |
Reference in New Issue
Block a user