Skip to content

SDD-023 · Hồ sơ một người và scorecard ​

Một phần của SDD-023. Ảnh thành viên và tên gọi ở nhà, hồ sơ một người kèm scorecard, rồi ba vòng feedback SRC-621 tới SRC-624.

14. Ảnh thành viên + tên gọi ở nhà (SRC-607) ​

Vòng feedback 2026-08-26 trên bản chạy thật, bảy sửa đổi:

  • Mỗi thành viên một card riêng, có ảnh chân dung: ảnh riêng (photo_media_id, migration 0083) → avatar Google của tài khoản đã liên kết → placeholder chữ cái đầu. Bấm thẳng vào ảnh để upload tại chỗ; ảnh bắt buộc là media NỘI BỘ — cùng cổng cứng Q-146 với family_photos, kiểm ở server vì đây là ảnh người thật, có trẻ em.
  • Bé có "tên gọi ở nhà" (learners.nickname): thứ bố mẹ thật sự gọi con — mentor mở đầu câu chuyện bằng tên này thì đúng giọng gia đình. Sửa trong chế độ edit.
  • Trang danh sách: tên con xuất hiện đúng một lần (chip bấm được; bỏ dòng chữ tĩnh trùng).
  • Ghi chú: nội dung đứng đầu và liền mạch; ghi chú gắn tag key (mục tiêu gia đình) được nhấn thêm một bậc chữ. Tag thành siêu dữ liệu: chữ 10px trong dòng meta; bộ sửa tag + Suggest tags + Archive ẩn cho tới khi bấm "edit tags" — mặc định ghi chú là nội dung, không phải bảng điều khiển.
  • Bỏ dòng → next_condition ("One real conversation, even ten minutes"): ghi chú vận hành của journey engine, chiếm một dòng dưới mọi người mà không ai hành động từ đó. StageDot giữ nguyên thông tin giai đoạn.

15. Hồ sơ một người + scorecard (SRC-620) ​

Vòng feedback 2026-08-27 trên bản chạy thật. Bốn sửa đổi, và một câu hỏi phải trả lời trước cả bốn: Dolphin mở màn này ra để làm gì? Câu trả lời của chủ dự án là "nhìn vào đó thì biết nên làm gì tiếp" — nên mọi thứ dưới đây được cân theo đúng câu đó.

15.1 Ảnh xuống đáy cột trái ​

Ảnh thuộc về "nhà này gồm những ai, trông thế nào" — cùng loại với các thẻ người ở trên nó. Trước đây nó nằm ở cột phải, dưới dòng ghi chú, tức là ngang hàng với thứ người ta thật sự vào đây để đọc.

15.2 Hồ sơ một người (bấm ký hiệu ▤ trên thẻ người) ​

Cột phải đổi sang hồ sơ người đó: scorecard → bốn danh sách Pains / Jobs to be done / Needs / Beliefs → toàn bộ ghi chú về người đó.

  • Đổi cột phải, không mở hộp thoại. Đây là thứ đọc trước khi gặp gia đình, người ta ở lại vài phút và cuộn. Hộp thoại thì phải đóng mới làm được việc khác. Cột trái vẫn nguyên nên chuyển người là một cú bấm.
  • Ký hiệu riêng, không biến cả thẻ thành nút. Trong thẻ đã có ba thứ bấm được với ba nghĩa khác nhau (ảnh → upload, StageDot → đổi bước, chế độ sửa → biểu mẫu); một nút bọc ngoài thì mỗi cú bấm hụt mở nhầm màn.
  • Hai tag mới need và belief (lần mở rộng đầu tiên của bộ tag đóng — SDD-024 §2). Không có chúng thì hai trong bốn cột của hồ sơ vĩnh viễn trống.
  • Ghi chú gắn được vào một người lớn (family_notes.contact_id, migration 0160). Trước đó chỉ gắn được vào bé, nên mọi ghi chú về bố mẹ là ghi chú mức cả nhà và hai hồ sơ cạnh nhau hiện y hệt nhau. Việc này cũng gỡ giới hạn F3 (Audit #005): bố và mẹ nay có dấu vết journey riêng, không còn buộc phải cùng một bước.
  • Hồ sơ một người không trộn ghi chú mức cả nhà. Trộn vào thì hai hồ sơ cạnh nhau lại giống nhau — đúng cái làm màn này vô dụng. Ghi chú chung vẫn đọc ở dòng thời gian của cả nhà.

15.3 Scorecard — 5 cho mỗi người, 4 cho cả nhà ​

Tính ở backend (workers/api/src/modules/families/scorecards.ts, thuần, có test) chứ không ở React: cùng định nghĩa này sẽ còn dùng ở danh sách gia đình và báo cáo tuần, mà định nghĩa nằm trong component thì bản thứ hai chắc chắn lệch bản thứ nhất.

ThẻĐo gìVì sao đo như vậy
Understandingbao nhiêu trong 6 trục pain/jtbd/need/belief/goal/fact đã có ghi chúĐếm trục, không đếm ghi chú. Ba mươi ghi chú toàn event thì hồ sơ vẫn rỗng theo nghĩa dùng được; đếm số lượng sẽ thưởng cho việc ghi nhiều, mà ghi nhiều không phải mục tiêu
Journeybước hành trình (SDD-026) quy về thang 100Bước chưa xác nhận bị trừ 20%: một bước máy suy ra từ số ghi chú không cùng loại với một bước có người ký tên
Reach / Accountngười lớn: điện thoại · Zalo · email · đã nối tài khoản. Bé: chỉ "đã có tài khoản chưa"Dùng chung một công thức thì mọi thẻ của bé đỏ vĩnh viễn vì thiếu một số điện thoại không ai muốn có
Momentumsố ngày từ ghi chú gần nhất (≤30 xanh, ≥90 mất dấu)Nhịp gặp hiện là tháng một lần, nên "quá một chu kỳ" là dấu hiệu sớm nhất còn có nghĩa. Sửa ngưỡng khi có nhịp thật đo được, không sửa vì thấy thẻ đỏ nhiều
Evidencefact / (fact + opinion + hypothesis)Mẫu số chỉ gồm note có nêu độ chắc. Lấy tổng mọi note thì mỗi ghi chép sự kiện lại kéo điểm xuống, và cách để thẻ xanh trở thành đừng ghi

Thẻ cả nhà: Understanding · Journey · Momentum · Inbox (ghi chú chưa gắn tag = nợ), cộng thẻ Carer chen lên khi chưa Dolphin nào nhận nhà. Tối đa bốn thẻ hiện ra.

Ba luật giữ hàng thẻ khỏi phình thành bảng điều khiển:

  1. Câu hành động là nội dung, con số chỉ để xếp thứ tự. Bản phác đầu làm ngược lại — 62% cỡ lớn, câu hành động là dòng chú thích xám — và đọc xong vẫn phải tự nghĩ ra việc, tức là hàng thẻ chưa làm gì cả.
  2. Thẻ cả nhà lấy điểm của người YẾU NHẤT, không lấy trung bình. Một nhà hiểu bố rất kỹ và chưa biết gì về mẹ ra 50% — trông như "hiểu vừa phải cả hai", trong khi việc cần làm hoàn toàn cụ thể và nằm ở người bị bỏ quên.
  3. Không thẻ nào chấm điểm gia đình. Đây là công cụ cho Dolphin: thẻ nói việc CỦA CHÚNG TA còn thiếu, không nói gia đình kém.

15.4 Ghi chú mặc định chỉ có nội dung ​

Dòng meta (tác giả · giờ · tag · "edit tags") và ô "Add a reply…" ẩn cho tới khi bấm hai ký hiệu ⓘ và ↩. Với mười ghi chú thì trước đây màn hình có mười ô nhập và mười dòng chữ xám mà gần như không lần đọc nào cần tới — nó đọc như một bảng nhập liệu chứ không như một tập ghi chép. Reply đã có thì vẫn hiện luôn: đó là nội dung, không phải điều khiển.

15.5 Thẻ "cả gia đình" + Follow ​

Vòng feedback thứ hai cùng ngày. Cột trái nay mở đầu bằng một thẻ cấp gia đình, đứng trên mọi thẻ người. Trước đó cấp gia đình không có chỗ đứng nào trên màn hình — chỉ là cái tiêu đề và một nút hình trái tim lẫn giữa các nút ở góc phải — trong khi phần lớn việc của Dolphin là việc với cả nhà.

  • "Follow" thay nút tim, và nhiều người theo cùng lúc được: bảng family_mentors vốn đã là quan hệ nhiều-nhiều, chỉ giao diện cũ là chỉ hiện trạng thái của riêng người đang đăng nhập. Nay ai đang theo thì hiện hết (ảnh chồng nhau + số đếm). Biết nhà này đang có ai để hỏi là thứ dùng được ngay; một cái tim chỉ nói về chính mình thì không.
  • Thẻ này cũng là lối về màn cả nhà khi đang mở hồ sơ một người, cùng ký hiệu ▤ với các thẻ người.
  • GET /v1/mentor/families/{id} nay trả thêm carers[].

15.6 Chip lọc nhỏ hẳn lại ​

Tám chip cỡ n12-chip chiếm hai hàng cao ngang một ghi chú — thanh điều khiển to bằng nội dung, trong khi thứ người ta vào đây để đọc là ghi chú. Nay chúng là chữ 11px cao 24px: vẫn bấm được, không còn tranh chỗ.

15.7 Ghi chú có định dạng — và vì sao KHÔNG lưu HTML ​

Chủ dự án yêu cầu sửa được và tạo mới được ghi chú ở dạng rich text (đậm, xuống dòng). Hiện thực: lưu Markdown subset, không lưu HTML (apps/mentors/src/richText.tsx).

Lý do là một ràng buộc bảo mật cụ thể, không phải sở thích: một trình soạn thảo contenteditable trả về HTML, và lưu HTML nghĩa là ở đâu đó phải có dangerouslySetInnerHTML để hiện lại. Trên Dolphin, nội dung do người dùng gõ ra và mọi mentor đọc được ghi chú của mọi nhà (Q-141) — một người gõ được <script> vào ghi chú thì nó chạy trong phiên của người khác. Cái giá để né là một bộ lọc HTML, mà bộ lọc HTML tự viết là loại mã sai âm thầm và sai lâu.

Markdown subset thì chuỗi lưu xuống vô hại, và hàm hiện dựng ra phần tử React chứ không nối chuỗi HTML — không có đường nào cho thẻ lạ đi vào. Hỗ trợ đúng những gì được yêu cầu: **đậm**, *nghiêng*, xuống dòng, gạch đầu dòng. Ô soạn là textarea + thanh ba nút bọc vùng bôi đen; người quen gõ Markdown gõ thẳng được, và dán chữ từ Zalo vào không kéo theo định dạng rác. Ghi chú cũ không có ký hiệu nào thì hiện y nguyên.

Sửa ghi chú: ký hiệu ✎ trong hàng ký hiệu của mỗi ghi chú, PATCH /v1/mentor/notes/{id} với body (endpoint đã có sẵn).

15.8 Một lỗi im lặng đã sửa cùng đợt ​

Bộ lọc ghi chú gộp nhiều tag luôn lọc AND, nên "Goals and motivation" (goal + jtbd) chỉ khớp ghi chú mang cả hai — trên dữ liệu thật là gần như không ghi chú nào. Bộ lọc trông vẫn chạy, chỉ là luôn trả về rỗng, nên không ai báo hỏng. Nay mỗi bộ lọc khai rõ match: "any" | "all", và mọi bộ lọc gộp nhiều tag đều là "hoặc".

Lịch sử quyết định ​

16. Vòng feedback thứ ba cùng ngày (SRC-621) ​

16.1 Lưu trữ một người là cái bẫy một chiều — đã sửa ​

Chủ dự án hỏi "tự nhiên card của riêng bố đâu rồi?". Không phải lỗi hiển thị: trong D1, family_contacts.con-minh-0 đang ở status='archived', đặt lúc 2026-08-27T11:49:54Z — tức là một cú bấm nút Archive trong chế độ sửa, vài phút sau khi bản mới lên.

Sự thật đáng nói không phải cú bấm nhầm mà là cái nút đó không hỏi lại, không báo gì, và không có đường quay lại. Lưu trữ là cách xoá của hệ này (Q-150) — nó bắt buộc phải đảo ngược được, nếu không thì mỗi lần bấm nhầm là một người biến mất khỏi màn hình trong khi dữ liệu vẫn nằm nguyên trong D1. Người dùng không phân biệt được hai chuyện đó; với họ là mất.

Nay cột trái có khối "Show archived people (n)" gập lại, mỗi người một nút Restore. Khối chỉ xuất hiện khi thật sự có người bị lưu trữ, nên nó không thêm chữ vào màn hình bình thường.

16.2 "NaN people" — hai endpoint, hai hình dạng ​

Thẻ gia đình hiện NaN people. Nguyên nhân: GET /v1/mentor/families/{id} trả về hàng families thô, trong khi contact_count / learner_count là hai cột do endpoint danh sách tự tính bằng subquery. Kiểu FamilyRow dùng chung cho cả hai chỗ nên TypeScript không kêu gì, và undefined + undefined ra NaN hiện thẳng lên màn hình.

Sửa bằng cách đếm từ chính hai mảng đang hiển thị (contacts active + learners) thay vì tin vào hai cột không có ở endpoint này. Đây cũng là con số đúng hơn: nó luôn khớp với số thẻ người mà mắt đang nhìn thấy.

16.3 Bấm cả thẻ, bỏ ký hiệu ▤ ​

Ký hiệu ▤ ở góc bị thay bằng: cả thẻ là một nút. Cách hiện thực có một ràng buộc cứng — không bọc bằng <button>, vì bên trong thẻ đã có nút, ô nhập và <label> tải ảnh, mà HTML cấm lồng nút trong nút (trình duyệt sẽ tự gỡ cấu trúc, mỗi trình duyệt một kiểu). Dùng div role="button" kèm tabIndex và tự xử lý Enter/Space.

Kèm theo đó, mọi điều khiển bên trong phải stopPropagation: ảnh (tải lên), StageDot (đổi bước), và các ô sửa. Không chặn thì một cú bấm ra hai việc.

16.4 Bỏ hai chỗ đọc hai lần ​

  • Tiêu đề <h1> "Nhà Trí Minh" ở đầu trang: thẻ gia đình ngay dưới mang đúng cái tên đó.
  • Dòng "Vũ Huyền Trang · Mother" ở đầu cột giữa: thẻ của chính người đó đang sáng ở cột trái ngay bên cạnh.

16.5 Mentor đang theo — thẻ riêng ​

Tách khỏi thẻ gia đình vì hai thứ trả lời hai câu khác nhau: thẻ gia đình nói "nhà này là nhà nào", thẻ này nói "gọi ai thì hỏi được". Nhồi chung thì hàng avatar bị ép thành ảnh 24px chú thích bằng tooltip — phải rê chuột từng cái mới biết ai, đúng lúc cần biết nhanh nhất.

16.6 Song ngữ EN–VI (apps/mentors/src/lang.tsx) ​

Công tắc ở góc trên phải, hiện cả hai chữ cùng lúc với chữ đang bật được tô đậm — không phải một nút đổi qua đổi lại. Một nút chỉ hiện "VI" thì không nói được nó đang là tiếng Việt hay sẽ đổi sang tiếng Việt.

Ngược chiều với apps/learn/src/lang.tsx: ở learn, chuỗi gốc trong mã là tiếng Việt và catalog dịch sang tiếng Anh; ở đây gốc là tiếng Anh, vì SRC-586 đã chuyển cả cổng Dolphin sang tiếng Anh và mã hiện có viết bằng tiếng Anh. Đảo chiều gốc sẽ phải sửa từng chuỗi một — đúng loại thay đổi lớn không ai rà hết được.

Hai giới hạn cố ý, nói rõ ở đây để không ai tưởng là sót:

Chưa dịchVì sao
Các tab khác của cổngĐÃ ĐÓNG ở §21
Câu hành động trên scorecardĐÃ ĐÓNG ở §20 — backend nay trả khoá + tham số

16.7 Dấu hỏi giải thích bốn cột (apps/mentors/src/profileLists.ts) ​

Bốn nhãn Pain / JTBD / Need / Belief nghe thì rõ, nhưng ranh giới giữa chúng mới là chỗ khó — và người ghi sai ranh giới thì bốn danh sách trộn thành một. Hai cặp dễ lẫn nhất: pain ↔ need (chỗ đau vs thuốc) và belief ↔ opinion (niềm tin của gia đình vs độ chắc của chính ghi chú).

Mỗi mục vì thế có ba phần, thiếu phần nào cũng hụt:

  • là gì — một câu định nghĩa;
  • không phải gì — phần đắt nhất. Định nghĩa suông không tách được hai khái niệm gần nhau, chỉ có ranh giới mới tách được;
  • ví dụ — câu thật, viết như mentor sẽ gõ, lấy từ ngữ cảnh Nemo12 (bố mẹ Việt, con cấp 2-3), không phải mẫu điền chỗ trống.

Dùng popover chứ không phải tooltip: nội dung là ba đoạn có cấu trúc, mà tooltip mất ngay khi chuột rời đi — không ai đọc xong bốn ví dụ trong lúc phải giữ yên con trỏ. Đóng bằng bấm ra ngoài hoặc phím Esc.

17. Sửa dồn về cột giữa, cột trái chỉ để đọc (SRC-622) ​

17.1 Vì sao dồn ​

Trước đợt này việc sửa nằm rải trong thẻ ở cột trái: một thẻ rộng 22rem phải chứa ảnh bấm-để-tải-lên, ô tên, ô điện thoại, ô email, nút Save và nút Archive. Hai hệ quả:

  1. Mọi ô đều hẹp và chồng lên nhau — xem ảnh chụp 2026-08-27 22:30, thẻ của bố cao gấp ba thẻ của mẹ chỉ vì đang mở biểu mẫu.
  2. Cột trái vừa là chỗ đọc lướt vừa là biểu mẫu, nên bấm hụt một cái là mở hộp chọn tệp hoặc lưu trữ mất một người — đúng chuyện đã xảy ra lúc 11:49 cùng ngày (§16.1).

Nay: cột trái chỉ hiển thị. Không ô nhập, không tải ảnh. Cột giữa rộng gấp đôi và vốn đã là chỗ "xem kỹ một người".

Ngoại lệ duy nhất còn ở cột trái là nút Link account khi có gợi ý nối tài khoản: nó là một cú bấm duyệt, không phải biểu mẫu, và chỉ có nghĩa ngay cạnh người được gợi ý — chuyển sang cột giữa thì phải mở từng người ra mới biết có gợi ý hay không.

17.2 Thẻ hồ sơ một người (cột giữa) ​

Đứng trước scorecard: scorecard nói "làm gì tiếp", mà câu đó chỉ có nghĩa sau khi đã biết đang nói về ai.

Thẻ đọc được khi không sửa — nó không phải biểu mẫu ẩn sau công tắc. Những gì đã điền hiện thành chữ và link bấm được; chỉ khi bấm ✎ mới thành ô nhập. Bốn trường mới (migration 0161, trên cả family_contacts lẫn learners):

CộtDùng để
facebook_urlTách riêng vì gần như mọi phụ huynh Việt đều có — xứng đáng một ô có nhãn sẵn
links_jsonMảng [{label, url}]. LinkedIn, Zalo OA, blog, GitHub, trang công ty… danh sách này còn dài ra, và mỗi lần dài ra mà phải chạy một migration thì nó sẽ không bao giờ dài ra — người ta nhét hết vào ô note
interestsSở thích — thứ Dolphin dùng nhiều nhất để mở lời
expertiseLĩnh vực chuyên môn. Với bé nghĩa là môn/mảng em mạnh

Bốn cột chứ không phải một bảng liên kết riêng (đã cân nhắc và KHÔNG dựng): chúng là thuộc tính của một người, đọc cùng lúc với người đó và không bao giờ truy vấn độc lập ("tìm mọi người có Facebook" không phải câu hỏi ai hỏi). Bảng thứ hai chỉ đáng tồn tại khi có truy vấn riêng — cùng lý do đã bỏ family_visits ở 0080.

Hai chi tiết dễ bỏ sót, cả hai đều là lỗi thật nếu thiếu:

  • Nạp lại nháp khi đổi người (useEffect theo id). Thiếu bước này, bấm từ bố sang mẹ sẽ giữ nguyên chữ đang gõ dở của bố trong ô của mẹ — và một cú Save sau đó ghi đè dữ liệu mẹ bằng dữ liệu bố.
  • parseLinks chịu được dữ liệu hỏng. links_json do người gõ và sẽ được sửa qua nhiều đợt; một chuỗi không phải JSON hợp lệ là chuyện sẽ xảy ra. Ném lỗi thì cả màn hồ sơ trắng vì một ô phụ — trả mảng rỗng thì chỉ mất đúng phần đó.
  • Link ra ngoài luôn có rel="noreferrer": URL do người dùng nhập, không để trang đích với tới window.opener của cổng nội bộ.

17.3 Bỏ Archive, mọi Save có Cancel ​

Nút Archive cạnh nút Save đã bị bỏ. Nó nằm sát Save nên bấm hụt là một người biến mất — §16.1 là bằng chứng. Lưu trữ nay chỉ còn một đường: khối "archived" ở cột trái, cạnh nút Restore, tức là cùng chỗ với cách hoàn tác nó.

Mọi nút Save đều có Cancel ngay bên cạnh: thẻ hồ sơ, ô soạn ghi chú mới, và bộ sửa tag (Cancel ở đây trả tag về đúng bản đang lưu, nên bấm lung tung vài chip rồi đổi ý không để lại hậu quả).

Còn lại một nút "Archive" duy nhất trong màn: nó lưu trữ một ghi chú, không phải một người, và nằm sau hai lớp bấm (ⓘ → edit tags). Giữ lại vì đó là đối tượng khác và hiện chưa có yêu cầu bỏ.

17.4 Trang danh sách: tên đọc được, thẻ cao bằng nhau ​

  • Tên hiện gần như trọn vẹn. Trước đây ô tên bị ép max-w-[5.5rem] nên "Nguyễn Cảnh Dũng" hiện ra "Nguyễn Cả…" — cắt đúng chỗ làm mất cả họ lẫn tên, tức là mất luôn tác dụng nhận diện mà hàng ảnh-trên-tên-dưới tồn tại để làm. Nay tên tự xuống dòng, chỉ cắt khi quá 30 ký tự.
  • Chỗ trống cũng chiếm chỗ. Nhà chưa khai bố mẹ / con / người chăm vẫn hiện ảnh placeholder viền đứt cộng một dòng tên rỗng, nên mọi thẻ gia đình cao bằng nhau (đo được: 379px cho cả thẻ đầy lẫn thẻ rỗng). Trước đây nhà thiếu người rút lại thành một dòng chữ, khiến lưới hai cột so le và mắt phải đọc lại từng thẻ mới biết mình đang ở đâu. Viền đứt chứ không liền: ô đó nói "chỗ này chưa có ai" chứ không giả vờ là một người.

18. Dọn thanh công cụ, và một lỗi mất dữ liệu trong im lặng (SRC-623, SRC-624) ​

18.1 Công tắc ngôn ngữ lên header của cả cổng ​

Nó đổi mọi trang, không riêng trang gia đình, nên chỗ đúng của nó là cạnh avatar trong App.tsx — hàng điều khiển cấp tài khoản. Đặt trong thanh công cụ của một màn là nói sai phạm vi của chính nó.

18.2 Bỏ nút ← và công tắc ✎ "sửa gia đình" ​

  • Nút ←: tên cổng ở header đã đưa về danh sách gia đình, và trình duyệt có nút Back thật. Một nút quay-lại tự chế luôn thua nút của trình duyệt vì nó không biết người dùng đến đây từ đâu.
  • Công tắc ✎: sau §17 nó chỉ còn gác ba thứ — nút Link account, nút Restore, và biểu mẫu thêm người — mà cả ba đều tự nói được mình là hành động. Một công tắc toàn cục chỉ để mở ba cái nút là một trạng thái thừa người dùng phải nhớ.

Nút thêm ảnh gia đình nay hiện thường trực. Nó là một nút có nhãn rõ ràng, khác hẳn vùng ảnh chân dung bấm-nhầm-được đã bị gỡ khỏi cột trái ở §17.1.

18.3 Thêm người: hai ký hiệu thay hai biểu mẫu thường trực ​

Trước đây hai biểu mẫu nằm đáy cột trái suốt thời gian bật chế độ sửa, dù thêm người là việc làm một lần rồi thôi. Nay chỉ hai nút; bấm mới mở đúng một biểu mẫu, và mỗi biểu mẫu kết thúc bằng cặp Save + Cancel.

"New child" đứng trước vì đó là cái được gọi tên. "New parent" ở lại vì bỏ nó thì không còn đường nào thêm bố mẹ — im lặng cắt mất một việc đang dùng được là chuyện khác hẳn với dọn giao diện.

18.4 Thẻ bố/mẹ ba dòng ​

  1. tên (kèm hai ký hiệu trạng thái, vì chúng nói về chính người này)
  2. vai trò trong nhà
  3. chức danh → nơi làm việc → khu vực (lấy cái đầu tiên có)

Trước đây tên và vai trò chen chung một dòng rồi mọi thứ còn lại dồn vào dòng hai nối bằng dấu chấm giữa — với tên tiếng Việt dài thì vai trò bị đẩy xuống dòng và trông như một phần của tên. Dòng ba luôn chiếm chỗ kể cả khi rỗng, để các thẻ không so le (cùng luật đã dùng ở §17.4).

18.5 SRC-624 — hai danh sách lệch nhau, dữ liệu mất trong im lặng ​

Chủ dự án báo: "hệ thống đang không lưu được và không hiển thị được thông tin, ví dụ sở thích".

Nguyên nhân. §17.2 thêm bốn cột vào CONTACT_FIELDS — allow-list của buildUpdate — nhưng quên thêm vào ContactBody, là schema zod chạy trước nó. z.object() loại bỏ mọi key không khai báo. Hệ quả: API trả 200, audit_log ghi "đã cập nhật", và dữ liệu bị vứt. Không có gì đỏ, không có gì trong log — chỉ có người dùng gõ vào ô, bấm Save, và thấy nó trống lại.

Đây là loại lỗi tệ nhất trong file này: thành công giả. Một lỗi 500 thì ai cũng thấy; một 200 kèm mất dữ liệu thì phải có người ngồi kiểm lại từng ô mới phát hiện.

Sửa. Bốn trường vào cả hai schema (ContactBody và body của PATCH learner).

Chặn tái phát. Hai danh sách này sẽ còn lệch nữa — mỗi lần thêm một cột là một cơ hội. Nên chúng được tách sang contactSchema.ts (import routes.ts trong test thì kéo theo cả router Hono và env của worker), và rules.test.ts chốt bất biến: mọi tên trong CONTACT_FIELDS / LEARNER_FIELDS đều phải qua được schema mà không bị loại.

Một lỗi cùng họ, phát hiện cùng lúc. splitLayers không bao giờ trả photo_media_id, dù kiểu ContactLayers.mentor khai là có. Nghĩa là ảnh chân dung của bố mẹ tải lên xong lưu đúng vào D1 mà giao diện đọc ra undefined và luôn hiện chữ cái đầu — hỏng âm thầm từ SRC-607. TypeScript không bắt được vì splitLayers trả kiểu suy ra, không khai báo kiểu trả về. Đã sửa và có test.