// Core pages: Login, Reception Dashboard, Doctor Dashboard, New X-ray wizard /* ========================================================= LOGIN ========================================================= */ function LoginPage({ onLogin }) { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const submit = async (e) => { e.preventDefault(); setLoading(true); setError(null); try { const data = await window.API.login(username, password); onLogin(data.user); } catch (err) { setError(err.message || 'Login muvaffaqiyatsiz'); } finally { setLoading(false); } }; return ( <>

Your Smile Rentgen

3D rentgen markazi tizimi

setUsername(e.target.value)} placeholder="username" icon autoFocus /> setPassword(e.target.value)} placeholder="••••••••" icon />
Parolni unutdingizmi?
{error &&
{error}
}
© 2026 Your Smile Rentgen. Barcha huquqlar himoyalangan.
); } /* ========================================================= RECEPTION DASHBOARD ========================================================= */ function ReceptionDashboard({ onNav, me }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [notifs, setNotifs] = useState([]); useEffect(() => { let mounted = true; setLoading(true); window.API.receptionDashboard() .then(d => { if (mounted) { setData(d); setLoading(false); } }) .catch(() => { if (mounted) setLoading(false); }); window.API.listNotifications(false) .then(rows => { if (mounted) setNotifs(Array.isArray(rows) ? rows.slice(0, 8) : []); }) .catch(() => { if (mounted) setNotifs([]); }); return () => { mounted = false; }; }, []); const k = data?.kpis || { registered: 0, revenue: 0, debt: 0, pending: 0 }; const recent = data?.recent || []; const weekData = data?.week_revenue || []; const hourly = data?.hourly_load || []; const hour = new Date().getHours(); const greeting = hour < 12 ? 'Xayrli tong' : hour < 18 ? 'Xayrli kun' : 'Xayrli kech'; const firstName = me?.full_name?.trim()?.split(/\s+/)?.[0] || me?.full_name || 'foydalanuvchi'; // Sparklines from week revenue const spark1 = makeSpark(weekData.length ? weekData.map(w => w.value / 100000) : [4, 7, 6, 8, 11, 9, 14]); const spark2 = makeSpark(weekData.length ? weekData.map(w => w.value) : [1.2, 1.4, 1.1, 1.8, 1.6, 2.2, 1.35]); const spark3 = makeSpark([8, 6, 9, 7, 5, 6, 4]); const spark4 = makeSpark([3, 5, 4, 6, 4, 7, 5]); return (
{/* Aurora hero */}
{uzDate(new Date())} · Reception

{greeting}, {firstName} 👋

{loading ? "Ma'lumot yuklanmoqda..." : `Bugun ${k.registered} ta bemor ro'yxatga olindi, ${k.pending} ta rentgen tayyorlanmoqda.`}

{/* KPI cards */}
uzMoneyShort(v)} delta={8} sparkline={spark2} /> uzMoneyShort(v)} delta={-4} sparkline={spark3} />
{/* Two-column: today's records + activity */}
Bugungi yozuvlar
Oxirgi 8 ta vizit
{recent.length === 0 && !loading && ( )} {recent.map(v => ( onNav('patient-detail', { patientId: v.patient.id })}> ))}
Vaqt Bemor Xizmat Summa Holat
Bugun yozuvlar yo'q
{uzDateTime(v.created_at).split(', ')[1]} {v.patient.full_name} {v.doctor ? v.doctor.full_name : '—'} {uzMoneyShort(v.total_amount)}
So'nggi xabarlar
{notifs.length} ta
{notifs.length === 0 && (
Hozircha xabar yo'q
)} {notifs.map(n => (

{n.title}

{window.uzDateTime ? window.uzDateTime(n.created_at) : n.created_at}
))}
Soatlik yuklanish
{new Date().toLocaleDateString('uz-UZ')}
); } function HourlyLoadChart({ hourly }) { // Show 8h-22h const source = (hourly && hourly.length) ? hourly : Array.from({ length: 24 }, (_, h) => ({ hour: h, count: 0 })); const data = source.slice(8, 22); const max = Math.max(...data.map(d => d.count), 1); const now = new Date().getHours(); return (
{data.map(d => { const isNow = d.hour === now; return (
{d.hour}
); })}
); } /* ========================================================= NEW X-RAY VISIT — the most important screen ========================================================= */ function normalizePatient(p) { if (!p) return p; return { ...p, name: p.full_name || p.name, fullName: p.full_name || p.fullName, birth: p.birth_date ? window.uzDate(p.birth_date) : (p.birth || '\u2014'), birthDate: p.birth_date ? new Date(p.birth_date) : (p.birthDate || null), }; } function normalizeDoctor(d) { if (!d) return d; return { ...d, name: d.full_name || d.name, speciality: d.specialty || d.speciality || '\u2014', clinic: d.clinic || '\u2014', telegram: d.telegram_username || d.telegram, }; } function normalizeService(s) { return { ...s, price: Number(s.price), cat: s.category || s.cat }; } function NewVisitPage({ onNav, toast }) { const [patient, setPatient] = useState(null); const [doctor, setDoctor] = useState(null); const [noDoctor, setNoDoctor] = useState(false); const [selectedSvcs, setSelectedSvcs] = useState([]); const [payType, setPayType] = useState('cash'); const [paymentStatus, setPaymentStatus] = useState('full'); const [paidAmount, setPaidAmount] = useState(''); const [showNewPatient, setShowNewPatient] = useState(false); const [showNewDoctor, setShowNewDoctor] = useState(false); const [saving, setSaving] = useState(false); // Data from API const [services, setServices] = useState([]); const [doctors, setDoctors] = useState([]); const [patients, setPatients] = useState([]); useEffect(() => { let mounted = true; Promise.all([ window.API.listServices(), window.API.listDoctors(), window.API.listPatients({ limit: 200 }), ]).then(([svc, docs, pts]) => { if (!mounted) return; setServices(svc.map(normalizeService)); setDoctors(docs.map(normalizeDoctor)); setPatients(pts.map(normalizePatient)); }).catch(err => { console.error('Load failed', err); toast({ kind: 'danger', title: "Ma'lumot yuklanmadi", msg: err.message }); }); return () => { mounted = false; }; }, [toast]); const total = selectedSvcs.reduce((s, x) => s + Number(x.price), 0); const paid = paymentStatus === 'full' ? total : paymentStatus === 'debt' ? 0 : parseInt((paidAmount || '0').replace(/\D/g,'')) || 0; const debt = Math.max(0, total - paid); const toggleSvc = (s) => { setSelectedSvcs(prev => prev.find(x => x.id === s.id) ? prev.filter(x => x.id !== s.id) : [...prev, s]); }; const canSave = patient && selectedSvcs.length > 0 && !saving; const handleSave = async () => { if (!canSave) return; setSaving(true); try { const payload = { services: selectedSvcs.map(s => ({ service_id: s.id, quantity: 1 })), payment_method: paymentStatus === 'debt' ? 'debt' : payType, paid_amount: paid, }; if (typeof patient.id === 'number') { payload.patient_id = patient.id; } else { // New patient (created via modal \u2014 has temp shape) payload.patient_new = { full_name: patient.full_name || patient.name, phone: patient.phone || null, birth_date: patient.birth_date || (patient.birthDate ? patient.birthDate.toISOString().slice(0,10) : null), passport: patient.passport || null, }; } if (doctor && typeof doctor.id === 'number' && String(doctor.id).match(/^\d+$/)) { payload.doctor_id = Number(doctor.id); } else if (doctor && !noDoctor) { payload.doctor_new = { full_name: doctor.full_name || doctor.name, phone: doctor.phone || null, specialty: doctor.specialty || doctor.speciality || null, clinic: doctor.clinic || null, }; } const result = await window.API.createXrayRecord(payload); toast({ kind: 'success', title: 'Rentgen yozildi!', msg: `${result.patient.full_name} \u2014 ${uzMoney(total)}` }); setTimeout(() => onNav('dashboard'), 700); } catch (err) { toast({ kind: 'danger', title: 'Saqlash xato', msg: err.message }); } finally { setSaving(false); } }; const grouped = useMemo(() => groupServicesByCat(services), [services]); return (
onNav('dashboard')}>Bekor qilish} />
{/* Left — sections */}
{/* 1. Bemor */}
Bemor
F.I.O. yoki telefon orqali qidiring
{patient && ( )}
{!patient ? ( <> setShowNewPatient(true)} createLabel="+ Yangi bemor qo'shish" />
{patients.length ? `Bazada: ${patients.length} ta bemor` : "Bemorlar yuklanmoqda..."}
) : (
{patient.fullName || patient.full_name || patient.name || '—'}
{[patient.phone, patient.birth, patient.birthDate ? `${age(patient.birthDate)} yosh` : null].filter(Boolean).join(' · ') || 'Ma\'lumot kiritilmagan'}
)}
{/* 2. Shifokor */}
Shifokor (ixtiyoriy)
Yo'naltirgan shifokor
{doctor && ( )}
{!doctor && !noDoctor ? ( <> setShowNewDoctor(true)} createLabel="+ Yangi shifokor qo'shish" /> ) : doctor ? (
{doctor.name}
{doctor.speciality} · {doctor.clinic}
{doctor.telegram && Telegram}
) : (
Shifokorsiz — to'g'ridan-to'g'ri bemor.
)}
{/* 3. Services */}
0} />
Xizmatlar
Kamida 1 ta xizmat tanlang
{selectedSvcs.length > 0 && ( {selectedSvcs.length} ta tanlandi )}
{SERVICE_CATEGORIES.map(cat => (
{cat.label}
{(grouped[cat.id] || []).map(s => { const sel = selectedSvcs.find(x => x.id === s.id); return (
toggleSvc(s)}>
{sel && }
{s.name}
{uzMoneyShort(s.price)} so'm
); })}
))}
{/* 4. Payment */}
0} />
To'lov
To'lov turi va holatini tanlang
Holati
{[ { id: 'full', label: "To'landi", tone: 'success' }, { id: 'partial', label: 'Qisman', tone: 'warning' }, { id: 'debt', label: 'Qarzga', tone: 'danger' }, ].map(s => ( ))}
{/* To'lov turi faqat pul olinganda kerak (qarzga bo'lsa — yashiriladi) */} {paymentStatus !== 'debt' && ( <>
To'lov turi
{[ { id: 'cash', label: 'Naqd', icon: 'money' }, { id: 'card', label: 'Karta', icon: 'credit-card' }, { id: 'transfer', label: "O'tkazma", icon: 'arrows-left-right' }, ].map(t => ( ))}
)} {paymentStatus === 'partial' && (
setPaidAmount(e.target.value.replace(/\D/g,''))} style={{ paddingRight: 60 }} /> so'm
{paidAmount && (
Qarz: {uzMoney(total - paid)}
)}
)}
{/* Right — sticky summary */}
Yig'indi
Bemor
{patient?.name || 'Tanlanmagan'}
Shifokor
{doctor?.name || (noDoctor ? "Tanlanmagan" : '\u2014')}
Xizmatlar ({selectedSvcs.length})
{selectedSvcs.length === 0 ? (
Hech narsa tanlanmagan
) : (
{selectedSvcs.map(s => (
{s.name} {uzMoneyShort(s.price)}
))}
)}
Jami {uzMoneyShort(total)}
To'landi {uzMoneyShort(paid)}
Qarz 0 ? 'var(--color-danger)' : 'var(--fg-4)', fontWeight: 600 }}>{uzMoneyShort(debt)}
{!canSave && (
Bemor va kamida 1 xizmat kerak
)}
{/* New patient modal */} setShowNewPatient(false)} onCreate={(p) => { setPatient(p); setShowNewPatient(false); toast({ kind: 'success', title: 'Bemor qo\'shildi', msg: p.name }); }} /> setShowNewDoctor(false)} onCreate={(d) => { setDoctor(d); setShowNewDoctor(false); toast({ kind: 'success', title: 'Shifokor qo\'shildi', msg: d.name }); }} />
); } function SectionNum({ n, done, optional }) { return (
{done ? : n}
); } function NewPatientModal({ open, onClose, onCreate }) { const [form, setForm] = useState({ name: '', birth: '', phone: '+998 ', passport: '' }); const [saving, setSaving] = useState(false); if (!open) return null; const valid = form.name.trim() && form.phone.length > 8; const save = async () => { setSaving(true); try { const bd = form.birth ? parseUzDate(form.birth) : null; const isoBirth = bd ? bd.toISOString().slice(0, 10) : null; const created = await window.API.createPatient({ full_name: form.name.trim(), phone: form.phone || null, birth_date: isoBirth, passport: form.passport || null, }); const norm = window.normalizePatient ? window.normalizePatient(created) : { ...created, name: created.full_name, fullName: created.full_name, birth: isoBirth ? form.birth : '—', birthDate: bd, }; onCreate(norm); } catch (err) { alert("Xato: " + err.message); } finally { setSaving(false); } }; return ( }>
setForm({ ...form, name: e.target.value })} autoFocus />
setForm({ ...form, birth: e.target.value })} /> setForm({ ...form, phone: e.target.value })} />
setForm({ ...form, passport: e.target.value })} />
); } function NewDoctorModal({ open, onClose, onCreate }) { const [form, setForm] = useState({ name: '', speciality: '', clinic: '', phone: '+998 ', telegram: '' }); const [saving, setSaving] = useState(false); if (!open) return null; const valid = form.name.trim(); const save = async () => { setSaving(true); try { const created = await window.API.createDoctorShort({ full_name: form.name.trim(), phone: form.phone || null, specialty: form.speciality || null, clinic: form.clinic || null, telegram_username: form.telegram || null, }); const norm = window.normalizeDoctor ? window.normalizeDoctor(created) : { ...created, name: created.full_name, speciality: created.specialty, }; onCreate(norm); } catch (err) { alert("Xato: " + err.message); } finally { setSaving(false); } }; return ( }>
setForm({ ...form, name: e.target.value })} autoFocus />
setForm({ ...form, speciality: e.target.value })} /> setForm({ ...form, clinic: e.target.value })} />
setForm({ ...form, phone: e.target.value })} /> setForm({ ...form, telegram: e.target.value })} />
); } function parseUzDate(s) { const m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/); if (!m) return new Date(1990, 0, 1); return new Date(+m[3], +m[2] - 1, +m[1]); } Object.assign(window, { LoginPage, ReceptionDashboard, NewVisitPage, SectionNum, NewPatientModal, NewDoctorModal, parseUzDate, normalizePatient, normalizeDoctor, normalizeService });