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.
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.
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.
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:
// 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.
Terangkan bagaimana src/lib/xlsx-pg211.ts mengisi borang PG211 dari template. Apa itu isDoNotFill() dan 4 peraturan yang ia kuatkuasakan?
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.
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).
Jejak aliran bila pengguna jana laporan PG201 di src/pages/admin/ReportingPage.tsx — dari tekan butang sampai fail Excel turun. Fail mana yang terlibat?
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.
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.
Terangkan worker/email.ts baris demi baris. Apa jadi kalau RESEND_API_KEY tak diset? Dan kenapa rahsia tak boleh ditulis dalam kod?
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
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.
Apa beza swallow('reason') dengan .catch kosong? Dan bagaimana src/lib/errorReporter.ts menghantar ralat ke pemilik? Terangkan mudah.
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.
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.
Terangkan bagaimana src/components/panduan/ai-chat/useAiChat.ts menyimpan & memuat sesi perbualan. Di mana sejarah chat disimpan, dan bila ia dibuang?
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.
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).
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.
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?
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.
📚 Kod 01 — Asas
Apa itu kod, 4 bahasa, peta bina-dari-kosong, cara guna Claude Code.
🔬 Kod 02 — Contoh Fail
10 bentuk fail, baris demi baris: route, service, kontrak, komponen, hook…
🔨 Panduan Developer
Rujukan teknikal padat: seni bina, fail penting, resipi perubahan bergred.