// Admin pages: Services, Doctors mgmt, Users, Audit log, Settings — wired to real API. function ServicesPage({ toast }) { const [items, setItems] = useState([]); const [editing, setEditing] = useState(null); // {id, name, price} const [showNew, setShowNew] = useState(false); const [loading, setLoading] = useState(true); const reload = () => { setLoading(true); window.API.listServices({ include_inactive: true }) .then(rows => { setItems(rows.map(s => ({ ...s, price: Number(s.price) }))); setLoading(false); }) .catch(err => { toast({ kind: 'danger', title: 'Yuklash xato', msg: err.message }); setLoading(false); }); }; useEffect(() => { reload(); }, []); const grouped = useMemo(() => groupServicesByCat(items.map(s => ({ ...s, cat: s.category }))), [items]); const saveEdit = async () => { try { await window.API.updateService(editing.id, { name: editing.name, price: Number(editing.price) }); toast({ kind: 'success', title: 'Saqlandi' }); setEditing(null); reload(); } catch (err) { toast({ kind: 'danger', title: 'Xato', msg: err.message }); } }; const toggleActive = async (s) => { try { await window.API.updateService(s.id, { is_active: !s.is_active }); reload(); } catch (err) { toast({ kind: 'danger', title: 'Xato', msg: err.message }); } }; const deleteService = async (s) => { if (!confirm(`"${s.name}" xizmatini butunlay o'chirmoqchimisiz?\n\nBu amalni qaytarib bo'lmaydi. Agar xizmat qabullarda ishlatilgan bo'lsa, o'chirib bo'lmaydi (avval toggle bilan nofaol qiling).`)) return; try { await window.API.deleteService(s.id); toast({ kind: 'success', title: 'O\'chirildi', msg: s.name }); reload(); } catch (err) { toast({ kind: 'danger', title: 'O\'chirib bo\'lmadi', msg: err.message }); } }; return (
setShowNew(true)}>Yangi xizmat} /> {SERVICE_CATEGORIES.map(cat => (
{cat.label} {(grouped[cat.id] || []).length}
{(grouped[cat.id] || []).map(s => ( ))}
Nom Narx Holat
{editing?.id === s.id ? ( setEditing({ ...editing, name: e.target.value })} /> ) : s.name} {editing?.id === s.id ? ( setEditing({ ...editing, price: parseInt(e.target.value.replace(/\D/g,'')) || 0 })} /> ) : {uzMoneyShort(s.price)} so'm}
{editing?.id === s.id ? ( <> ) : ( <> )}
))} setShowNew(false)} onCreated={() => { setShowNew(false); reload(); toast({ kind: 'success', title: "Xizmat qo'shildi" }); }} />
); } function NewServiceModal({ open, onClose, onCreated }) { const [form, setForm] = useState({ name: '', category: 'dental', price: '' }); const [saving, setSaving] = useState(false); if (!open) return null; const save = async () => { setSaving(true); try { await window.API.createService({ name: form.name.trim(), category: form.category, price: parseInt(form.price.replace(/\D/g, '')) || 0, }); onCreated(); } catch (err) { alert("Xato: " + err.message); } finally { setSaving(false); } }; return ( }>
setForm({...form, name: e.target.value})} placeholder="3D rentgen (panoramik)" autoFocus /> setForm({...form, price: e.target.value})} placeholder="0" />
); } /* ========================================================= DOCTORS MGMT ========================================================= */ function EditDoctorModal({ open, doctor, onClose, onSaved, toast }) { const [form, setForm] = useState({ full_name: '', phone: '', specialty: '', clinic: '', telegram_username: '' }); const [saving, setSaving] = useState(false); useEffect(() => { if (!open || !doctor) return; setForm({ full_name: doctor.full_name || doctor.name || '', phone: doctor.phone || '', specialty: doctor.specialty || doctor.speciality || '', clinic: doctor.clinic || '', telegram_username: doctor.telegram_username || doctor.telegram || '', }); }, [open, doctor?.id]); const save = async () => { if (!form.full_name.trim()) { toast?.({ kind: 'warning', title: "F.I.O. talab qilinadi" }); return; } setSaving(true); try { await window.API.updateDoctor(doctor.id, { full_name: form.full_name.trim(), phone: form.phone.trim() || null, specialty: form.specialty.trim() || null, clinic: form.clinic.trim() || null, telegram_username: form.telegram_username.trim().replace(/^@/, '') || null, }); toast?.({ kind: 'success', title: 'Saqlandi' }); onSaved?.(); } catch (err) { toast?.({ kind: 'danger', title: 'Saqlash xato', msg: err.message }); } finally { setSaving(false); } }; if (!open) return null; return ( }>
setForm({...form, full_name: e.target.value})} autoFocus /> setForm({...form, phone: e.target.value})} placeholder="+998 ..." /> setForm({...form, specialty: e.target.value})} placeholder="Stomatolog, LOR, ..." /> setForm({...form, clinic: e.target.value})} placeholder="Klinika nomi" /> setForm({...form, telegram_username: e.target.value})} placeholder="username" />
); } function DoctorsPage({ toast }) { const [q, setQ] = useState(''); const [doctors, setDoctors] = useState([]); const [showAdd, setShowAdd] = useState(false); const [editing, setEditing] = useState(null); const me = (window.Auth && window.Auth.getUser && window.Auth.getUser()) || null; const canDelete = me && (me.role === 'admin' || me.role === 'super_admin'); const canEdit = me && (me.role === 'admin' || me.role === 'super_admin' || me.role === 'reception'); const reload = () => window.API.listDoctors().then(d => setDoctors(d.map(window.normalizeDoctor))); useEffect(() => { reload(); }, []); const filtered = q ? doctors.filter(d => d.name.toLowerCase().includes(q.toLowerCase())) : doctors; // Find doctor full data for editing (passing raw from list response includes specialty etc) const startEdit = async (d) => { try { const all = await window.API.listDoctors(); const raw = all.find(x => x.id === d.id); setEditing(raw || d); } catch (err) { toast?.({ kind: 'danger', title: 'Yuklash xato', msg: err.message }); } }; const deleteDoctor = async (d) => { if (!confirm(`"${d.name}" shifokorini butunlay o'chirmoqchimisiz?\n\nQabullari shu shifokorga bog'langan bo'lsa — qabullar saqlanadi, lekin shifokor "yo'q" bo'lib qoladi.\n\nAgar shifokor to'lov qabul qilgan bo'lsa, o'chirib bo'lmaydi (foydalanuvchi sahifasida deactivate qiling).`)) return; try { await window.API.deleteDoctor(d.id); toast?.({ kind: 'success', title: "Shifokor o'chirildi", msg: d.name }); reload(); } catch (err) { toast?.({ kind: 'danger', title: "O'chirib bo'lmadi", msg: err.message }); } }; return (
setShowAdd(true)}>Yangi shifokor} />
setQ(e.target.value)} style={{ paddingLeft: 36 }} />
{(canEdit || canDelete) && } {filtered.length === 0 && ( )} {filtered.map((d) => ( {(canEdit || canDelete) && ( )} ))}
F.I.O.MutaxassisligiKlinikaTelefonTelegramAmallar
Shifokor topilmadi
{d.name}
{d.speciality || '—'} {d.clinic || '—'} {d.phone || '—'} {d.telegram ? {d.telegram} : }
{canEdit && ( )} {canDelete && ( )}
setShowAdd(false)} onCreate={() => { setShowAdd(false); reload(); }} /> setEditing(null)} onSaved={() => { setEditing(null); reload(); }} />
); } /* ========================================================= USERS ========================================================= */ function UsersPage() { const [role, setRole] = useState('all'); const [users, setUsers] = useState([]); const reload = () => window.API.listUsers().then(setUsers); useEffect(() => { reload(); }, []); const filtered = role === 'all' ? users : users.filter(u => u.role === role); return (
u.role === 'super_admin').length }, { id: 'admin', label: 'Admin', count: users.filter(u => u.role === 'admin').length }, { id: 'reception', label: 'Reception', count: users.filter(u => u.role === 'reception').length }, { id: 'doctor', label: 'Shifokor', count: users.filter(u => u.role === 'doctor').length }, ]} value={role} onChange={setRole} />
{filtered.map(u => (
{u.is_active ? 'Faol' : 'Nofaol'}
{u.full_name}
{u.email || u.username}
{ROLE_LABELS[u.role]}
))}
); } /* ========================================================= AUDIT LOG ========================================================= */ function AuditPage() { const [rows, setRows] = useState([]); const [filter, setFilter] = useState('all'); useEffect(() => { window.API.listAudit({ limit: 200 }).then(setRows).catch(() => {}); }, []); const filtered = filter === 'all' ? rows : rows.filter(a => a.action.startsWith(filter)); return (
{filtered.length === 0 ? ( ) : ( {filtered.map(a => ( ))}
VaqtFoydalanuvchiAmalEntityTafsilot
{uzDateTime(a.created_at)} {a.user ?
{a.user.full_name}
: system}
{a.action} {a.entity_type || '—'} {a.description || '—'}
)}
); } /* ========================================================= SETTINGS ========================================================= */ function SettingsPage({ toast }) { const [tab, setTab] = useState('general'); const [settings, setSettings] = useState({}); useEffect(() => { window.API.listSettings().then(rows => { const obj = {}; for (const r of rows) obj[r.key] = r.value || ''; setSettings(obj); }).catch(() => {}); }, []); const update = async (key, value) => { try { await window.API.updateSetting(key, value); setSettings(s => ({ ...s, [key]: value })); toast({ kind: 'success', title: 'Saqlandi', msg: key }); } catch (err) { toast({ kind: 'danger', title: 'Xato', msg: err.message }); } }; const SettingField = ({ k, label, hint, type = 'text' }) => { const [v, setV] = useState(settings[k] || ''); useEffect(() => setV(settings[k] || ''), [settings[k]]); return (
setV(e.target.value)} style={{ flex: 1 }} />
); }; return (
{[ { id: 'general', label: 'Umumiy', icon: 'gear' }, { id: 'telegram', label: 'Telegram', icon: 'paper-plane-tilt' }, { id: 'watcher', label: 'Watcher', icon: 'eye' }, ].map(t => (
setTab(t.id)}> {t.label}
))}
{tab === 'general' && (
Umumiy
)} {tab === 'telegram' && (
Telegram bot sozlamalari
4 ta qiymat kerak (300 MB fayl uchun Pyrogram MTProto):
1. Bot token: @BotFather/newbot
2-3. API ID + Hash: my.telegram.org/apps → "API development tools"
4. Kanal ID: yopiq kanal yarating, botingizni admin qo'shing
)} {tab === 'watcher' && (
File Watcher

3D rentgen apparati natijalarini avtomatik kuzatish.

)}
); } Object.assign(window, { ServicesPage, DoctorsPage, UsersPage, AuditPage, SettingsPage, NewServiceModal });