Panduan Kod 03 · Modul
← Kod 02
Panduan Kod · Bahagian 03  ·  Sistem OMFS

Ciri Sebenar di Sebalik Tabir

Kod 01 ajar asas; Kod 02 bedah satu fail setiap bentuk. Di sini kita naik satu tingkat: bagaimana 6 ciri penuh berfungsi dari hujung ke hujung — menggabungkan bentuk-bentuk yang anda dah kenal. Masih bahasa mudah, untuk bukan-coder.

📊 Laporan PG201/211 ⬇️ Muat turun 📧 E-mel 🛡️ Ralat 🤖 AI ⚖️ Keadilan on-call
00 Mula

Apa halaman ini (dan bukan apa)

Panduan Lengkap (bukan-coder) menerangkan ciri dari sudut pengguna — apa butang buat apa. Halaman ini berbeza: ia terangkan bagaimana ciri itu berfungsi di sebalik tabir, dalam bahasa mudah, supaya anda nampak fail mana yang terlibat.

Dalam Kod 02 anda belajar ~10 bentuk fail (route, service, komponen…). Satu ciri sebenar — seperti "hantar laporan melalui e-mel" — ialah beberapa bentuk itu bekerja bersama. Halaman ini menjejak enam ciri begitu, supaya bila anda nak ubah salah satu, anda tahu di mana hendak mula.

Cara baca Setiap ciri ada: Apa ia (untuk pengguna) → Bagaimana ia jalan (fail + aliran) → Perkataan baruTanya Claude Code (ayat untuk pergi lebih dalam pada kod hidup). Tak perlu hafal — gunakan sebagai peta rujukan.
Nota penting Laluan fail di sini disahkan pada 2026-06-18. Kod berubah; kalau satu fail dah berpindah, jangan panik — tanya Claude Code "di mana sekarang fail X?" dan ia akan cari versi terkini.
01 Laporan

Laporan PG201/PG211 — borang MOH

PG201 dan PG211 ialah borang laporan rasmi KKM (MOH) untuk OMFS. Sistem ini mengisi borang Excel itu secara automatik daripada data pesakit — menggantikan kerja menaip tangan yang berjam-jam.

Apa ia: pentadbir tekan "jana laporan", pilih bulan, dan sistem keluarkan fail Excel yang sudah berisi mengikut format MOH yang tepat. Format itu sangat ketat — sel yang salah boleh menyebabkan penyemak MOH menolaknya.

Analogi Bayangkan borang kerajaan dengan ratusan kotak kecil, dan setiap kotak ada peraturan: "kotak ini cuma untuk ibu mengandung umur bawah 6"… mustahil betul. Sistem ini ialah kerani teliti yang tahu semua peraturan itu dan mengisinya tanpa silap.

Bagaimana ia jalan

Kod laporan dahulu satu fail gergasi (1,159 baris). Ia dipecahkan (M6-P03) kepada modul fokus, dengan satu fail barrel yang mengumpul semula:

src/lib/xlsx-utils.ts (barrel)
// Barrel = satu pintu yang re-export banyak modul kecil.
export { generatePg201Excel, … } from './xlsx-pg201'; ← penjana PG201
export { generatePg211Excel, … } from './xlsx-pg211'; ← penjana PG211
export { generatePG101BExcel, … } from './xlsx-pg101b'; ← PG101B
// + xlsx-patient-report, xlsx-lampiran-c, xlsx-styles, xlsx-template-helpers

Borang kosong rasmi disimpan sebagai public/templates/PG201.xlsx dan PG211.xlsx. Penjana membuka template itu, menyalin helaian, lalu mengisi sel mengikut data — mengekalkan reka letak MOH dengan tepat.

Gotcha sebenar (M7) Sesetengah sel ialah "jangan isi" (kelabu di borang). Fungsi isDoNotFill() menguatkuasakan 4 peraturan MOH — cth ibu mengandung mesti umur ≤ 6, pesara mesti ≥ 30. Dan lajur tertentu mesti ditulis nombor 0 (bukan kosong), atau formula pengesah MOH jadi salah. Inilah jenis butiran yang membezakan "nampak siap" dengan "betul-betul diterima".
Perkataan baru Barrel — fail yang mengumpul & re-export banyak modul, supaya import kekal kemas. Template — borang kosong rasmi yang diisi. Excel buffer — fail Excel dalam bentuk data mentah, sebelum dimuat turun. MOH/KKM — Kementerian Kesihatan; pemilik format borang.
Tanya Claude Code Terangkan bagaimana src/lib/xlsx-pg211.ts mengisi borang PG211 dari template. Apa itu isDoNotFill() dan 4 peraturan yang ia kuatkuasakan?
02 Laporan

Eksport & muat turun laporan

Selepas penjana menyiapkan fail Excel, ia perlu sampai ke tangan pengguna. Itu kerja halaman eksport.

Apa ia: di halaman seperti "Laporan" (pentadbir) atau "Muat Turun", pengguna pilih tempoh, tekan jana, dan fail Excel turun ke komputer mereka. Menariknya: fail itu dibina di dalam browser pengguna, bukan di server.

Analogi Daripada menyuruh dapur (server) masak dan poskan fail, sistem hantar resipi + bahan ke meja anda dan ia dimasak terus di depan anda. Lebih pantas, dan data sensitif tak perlu singgah lama di server.

Bagaimana ia jalan

Halaman terlibat: src/pages/admin/ReportingPage.tsx dan src/pages/shared/MuatTurunPage.tsx. Aliran ringkas:

Pengguna pilih tempoh & tekan jana

Halaman minta data lawatan untuk tempoh itu dari backend (route + service).

Penjana xlsx dipanggil

Fungsi dari barrel xlsx-utils (m1) membina fail Excel dalam ingatan browser.

Fail diserahkan kepada pengguna

Browser cetuskan "muat turun" — fail masuk folder Downloads. Atau, dihantar melalui e-mel (m3).

Kenapa cara ini Identiti hospital (nama, fasiliti) diselit ke laporan dari tetapan hospital_config — jadi laporan setiap hospital tertera nama hospital itu sendiri, tanpa kod ditukar. Ini sebahagian daripada usaha menjadikan sistem boleh diedar ke hospital lain.
Perkataan baru Eksport — keluarkan data dalam bentuk fail (Excel, PDF). Browser-side — kerja dibuat dalam pelayar pengguna, bukan server. Download — fail dipindah ke komputer pengguna.
Tanya Claude Code Jejak aliran bila pengguna jana laporan PG201 di src/pages/admin/ReportingPage.tsx — dari tekan butang sampai fail Excel turun. Fail mana yang terlibat?
03 Laporan

Hantar laporan melalui e-mel

Selain muat turun, laporan boleh dihantar terus ke peti masuk e-mel. Sistem guna perkhidmatan luar bernama Resend untuk benar-benar menghantar e-mel itu.

Apa ia: pengguna pilih "hantar e-mel", dan fail Excel sampai sebagai lampiran. Kalau e-mel belum disediakan (tiada kunci API), sistem dengan sopan kata "sila muat turun fail" — ia tidak rosak.

Bagaimana ia jalan

Semua logik e-mel duduk di satu tempat: worker/email.ts, dipanggil oleh route seperti worker/routes/registrar/email.ts.

worker/email.ts (dipendekkan)
export async function sendReportEmail(env, input) {     ← hantar laporan Excel sebagai e-mel
  if (!env.RESEND_API_KEY)                       ← belum disediakan?
    return { ok: false, error: '… Sila muat turun fail.' }; ← jatuh balik ke muat turun, tak rosak

  const res = await fetch('https://api.resend.com/emails', { ← panggil perkhidmatan Resend…
    method: 'POST',
    signal: AbortSignal.timeout(10_000),         ← berhenti tunggu lepas 10 saat (elak tergantung)
    body: JSON.stringify({ from, to, subject, attachments }), ← siapa, tajuk, lampiran
  });
  if (!res.ok) return { ok: false, error: 'Gagal menghantar e-mel…' }; ← mesej ralat dalam BM
  return { ok: true, to: email };           ← berjaya
}

Kunci rahsia (RESEND_API_KEY, alamat penghantar) tidak ditulis dalam kod — ia disimpan sebagai secret di Cloudflare. Ini elak rahsia bocor ke dalam repositori kod.

Corak elok: jatuh balik dengan anggun Perhatikan: kalau e-mel gagal atau belum disediakan, sistem tidak terhenti — ia beri mesej jelas dan pengguna boleh muat turun fail. Sentiasa sediakan laluan alternatif bila bergantung pada perkhidmatan luar.
Perkataan baru Resend — perkhidmatan luar yang benar-benar menghantar e-mel. API key — kunci rahsia untuk guna perkhidmatan luar. Secret — nilai sulit disimpan di luar kod (di Cloudflare), tak pernah dalam repo. Attachment — fail dilampirkan pada e-mel. Timeout — had masa menunggu sebelum berhenti.
Tanya Claude Code Terangkan worker/email.ts baris demi baris. Apa jadi kalau RESEND_API_KEY tak diset? Dan kenapa rahsia tak boleh ditulis dalam kod?
04 Kualiti

Pengendalian ralat (error handling)

Program akan menghadapi masalah: rangkaian putus, data pelik, perkhidmatan luar gagal. Yang membezakan sistem matang ialah bagaimana ia mengendalikan masalah — tak pernah gagal secara senyap.

Peraturan rumah: setiap "tangkapan ralat" mesti sama ada tunjuk mesej (toast) kepada pengguna, atau dilaporkan untuk pemilik tahu. Gagal senyap (.catch(() => {}) kosong) dilarang — sebab pepijat yang tak nampak ialah pepijat yang tak dibaiki.

Tiga alat kecil

src/lib/swallow.ts
export function swallow(reason) {            ← "biar ralat ini SENGAJA" — tapi catat sebabnya
  return (err) => {
    if (import.meta.env.DEV) console.warn('[swallow]', reason, err); ← semasa membina: tunjuk amaran
  };
}

Perhatikan bezanya dengan .catch(() => {}) kosong: swallow('sebab') memaksa anda menulis kenapa ralat ini selamat diabaikan — jadi niatnya jelas kepada pembaca seterusnya. Dua alat lagi:

  • src/lib/errorReporter.ts — memintas console.error/warn, dan menghantar ralat tak dijangka ke backend (/api/client-errors) supaya pemilik nampak. Ia membuang ralat berulang (dedup) dan hadkan kuantiti, supaya tak membanjiri.
  • worker/db/rows.ts — satu pembantu kecil rows<T>() yang menggantikan tabiat buruk as any[] selepas query database, dengan jenis yang selamat.
Idea besar Ralat tidak ditakuti — ia diuruskan. Sama ada tunjuk kepada pengguna, atau hantar kepada pemilik. Tidak pernah dibiar hilang. Itu yang menjadikan sistem boleh dipercayai dari masa ke masa.
Perkataan baru Error handling — cara program menghadapi masalah. catch — "tangkap" ralat supaya program tak terhempas. Toast — mesej kecil timbul untuk pengguna. Silent failure — gagal tanpa sesiapa tahu (dilarang). Dedup — buang yang berulang. any — jenis "apa-apa saja" TypeScript yang bahaya; dielakkan.
Tanya Claude Code Apa beza swallow('reason') dengan .catch kosong? Dan bagaimana src/lib/errorReporter.ts menghantar ralat ke pemilik? Terangkan mudah.
05 Bantuan

Pembantu AI ("Tanya AI")

Sistem ada pembantu AI terbina — pengguna boleh bertanya soalan (cth tentang cara isi PG201) dan dapat jawapan, termasuk dengan menghantar gambar.

Apa ia: satu tetingkap chat. Pengguna taip soalan, AI jawab. Perbualan disimpan supaya boleh disambung kemudian, dan boleh ada beberapa "sesi" berbeza.

Bagaimana ia jalan

UI di src/components/panduan/AiChatAssistant.tsx; otaknya di hook useAiChat.ts. Hook itu menguruskan keadaan: senarai mesej, input, gambar, status "sedang menjawab", dan sejarah sesi.

src/components/panduan/ai-chat/useAiChat.ts (dipendekkan)
export function useAiChat({ open }) {           ← hook custom: otak tetingkap chat AI
  const [messages, setMessages] = useState([]);  ← senarai mesej dalam perbualan
  const [input, setInput] = useState('');     ← apa pengguna sedang taip
  const [images, setImages] = useState([]); ← gambar dilampir (maks 5)
  const [loading, setLoading] = useState(false); ← AI sedang menjawab?
  // + simpan/muat sesi perbualan supaya boleh disambung
}

Anda akan kenal corak ini — ia custom hook yang sama seperti Kod 02 (e11), cuma lebih besar. Sejarah perbualan disimpan dalam browser pengguna, dan ada had (cth maksimum 20 sesi) supaya tak menggunakan ruang berlebihan.

Perkataan baru Session (sesi) — satu perbualan berasingan yang boleh disambung. State — ingatan komponen (mesej, input…). Hook — fungsi React use… (lihat Kod 02 e11). TTL — tempoh hayat; sejarah lama dibuang automatik.
Tanya Claude Code Terangkan bagaimana src/components/panduan/ai-chat/useAiChat.ts menyimpan & memuat sesi perbualan. Di mana sejarah chat disimpan, dan bila ia dibuang?
06 On-call

Enjin keadilan on-call (fairness)

Bahagian paling pintar dalam sistem: ia menjana jadual on-call secara automatik — secara adil. Inilah yang dahulu mengambil berjam-jam kerja tangan setiap bulan.

Apa ia: pengurus tekan "jana", dan sistem mengisi kalendar on-call — cuba seboleh mungkin membahagi tugas sama rata, sambil mematuhi peraturan keselamatan.

Analogi Bayangkan membahagi giliran kerja malam antara 10 orang, sepanjang sebulan, supaya tiada siapa terbeban lebih — dan tiada siapa kerja dua malam berturut tanpa tidur. Buat ini dengan tangan = sakit kepala. Enjin ini buat dalam saat, dan boleh diulang dengan hasil sama.

Bagaimana ia jalan

Fail teras: worker/oncall/fairness.ts — satu enjin tulen (tiada database, tiada permintaan; jadi mudah diuji). Caranya: untuk setiap hari, untuk setiap slot, pilih calon dengan skor keadilan paling rendah.

Skor keadilan menggabungkan:

  • Beban terkumpul — tugas berat diberi berat lebih; dibawa merentas bulan melalui sejarah.
  • Penalti silang-bulan — "kalau bulan lepas kau buat tugas berat, langkau bulan ini" (lembut).
  • Pemecah-seri rawak berbenih — supaya input sama sentiasa beri hasil sama (boleh diulang).
Peraturan keras (R16) — keselamatan tak boleh ditawar Ada tapisan keras yang tak boleh dilanggar: seseorang yang tak tersedia (cuti) atau yang bekerja hari sebelumnya tidak dipilih — rentetan 48 jam tanpa tidur ialah isu keselamatan, bukan tawar-menawar. Ada juga had tugas sebulan, dan kredit cuti: pulang dari cuti bersalin atau kursus panjang tidak "dibayar balik" dengan timbunan tugas catch-up.

Kalau satu slot kehabisan calon yang sah selepas tapisan, ia dibiar kosong dan dicatat sebagai konflik untuk pengurus selesaikan — sistem tidak memaksa pilihan tak selamat. Pertukaran giliran (swap) antara staf diuruskan oleh worker/oncall/swaps.ts.

Perkataan baru Fairness engine — kod yang membahagi tugas secara adil. Pure function — kod yang hasilnya bergantung hanya pada inputnya (tiada DB) — mudah diuji. Greedy — strategi "pilih yang terbaik setiap langkah". Seeded RNG — rawak yang boleh diulang. Hard filter — peraturan yang tak boleh dilanggar. Conflict — slot tak terisi untuk pengurus selesaikan.
Tanya Claude Code Terangkan strategi dalam worker/oncall/fairness.ts macam saya bukan coder. Apa itu "skor keadilan", dan kenapa orang yang kerja semalam tak boleh dipilih hari ini?
07 Seterusnya

Ke mana selepas ini

Anda kini nampak enam ciri penuh, dan tahu fail mana yang terlibat dalam setiap satu. Set Panduan Kod kini lengkap untuk pembaca pemula.

1. Pilih satu ciri & buka failnya

Guna peta di halaman ini. Buka fail teras ciri itu di editor, dan baca perlahan dengan kamus simbol Kod 01.

2. Minta Claude Code terangkannya

Guna kotak "Tanya Claude Code" di setiap bahagian untuk pergi lebih dalam pada kod hidup.

3. Cuba ubah satu benda kecil

Tanya Claude Code perubahan paling selamat untuk dicuba — belajar dengan buat.

Ingat Anda telah pergi dari "apa itu kod" (Kod 01), ke "bagaimana satu fail dibaca" (Kod 02), ke "bagaimana satu ciri penuh berfungsi" (halaman ini). Itu perjalanan sebenar seorang pembelajar. Teruskan satu fail pada satu masa, dengan Claude Code sebagai cikgu. Anda mampu.