// Kassa: Records, Debts. Plus Reports. — Wired to real API.
// LOKAL sana (UTC emas) — hisobot backend'da klinika mahalliy kuni sifatida talqin qilinadi.
// toISOString() UTC beradi: mahalliy 00:00–05:00 da kechagi kun chiqib, hisobot noto'g'ri kunni olardi.
function localISO(d) {
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
function todayISO() {
return localISO(new Date());
}
function daysAgoISO(n) {
const d = new Date();
d.setDate(d.getDate() - n);
return localISO(d);
}
function KassaPage({ onNav, toast }) {
const [dateRange, setDateRange] = useState('today');
const [doctorFilter, setDoctorFilter] = useState([]);
const [statusFilter, setStatusFilter] = useState([]);
const [page, setPage] = useState(1);
const pageSize = 12;
const [visits, setVisits] = useState([]);
const [doctors, setDoctors] = useState([]);
const [summary, setSummary] = useState(null);
const [loading, setLoading] = useState(true);
const queryArgs = useMemo(() => {
const t = new Date();
let date_from, date_to;
const todayStart = new Date(t); todayStart.setHours(0,0,0,0);
if (dateRange === 'today') {
date_from = todayStart.toISOString();
} else if (dateRange === 'yesterday') {
const ys = new Date(todayStart); ys.setDate(ys.getDate() - 1);
const ye = new Date(todayStart);
date_from = ys.toISOString();
date_to = ye.toISOString();
} else if (dateRange === 'week') {
const ws = new Date(todayStart); ws.setDate(ws.getDate() - 7);
date_from = ws.toISOString();
} else if (dateRange === 'month') {
const ms = new Date(todayStart); ms.setMonth(ms.getMonth() - 1);
date_from = ms.toISOString();
}
const doctor_id = doctorFilter[0] || null;
const payment_status = statusFilter.length === 1 ? statusFilter[0] : null;
return { date_from, date_to, doctor_id, payment_status };
}, [dateRange, doctorFilter, statusFilter]);
useEffect(() => {
let mounted = true;
window.API.listDoctors().then(d => { if (mounted) setDoctors(d.map(window.normalizeDoctor)); });
return () => { mounted = false; };
}, []);
useEffect(() => {
let mounted = true;
setLoading(true);
Promise.all([
window.API.listKassa({ ...queryArgs, limit: 500 }),
window.API.kassaSummary(queryArgs),
]).then(([v, s]) => {
if (!mounted) return;
// multi-status filter — client side
let filtered = v;
if (statusFilter.length > 1) {
filtered = v.filter(x => statusFilter.includes(x.payment_status));
}
setVisits(filtered);
setSummary(s);
}).catch(err => toast({ kind: 'danger', title: 'Yuklash xato', msg: err.message }))
.finally(() => mounted && setLoading(false));
return () => { mounted = false; };
}, [queryArgs, statusFilter, toast]);
const totals = {
total: summary?.total_amount ?? 0,
paid: summary?.paid_amount ?? 0,
debt: summary?.debt_amount ?? 0,
count: summary?.visits_count ?? visits.length,
};
const paged = visits.slice((page - 1) * pageSize, page * pageSize);
const handleExcelDownload = async () => {
try {
if (dateRange === 'today') {
await window.API.downloadDaily(todayISO());
} else {
// period
const from = queryArgs.date_from ? queryArgs.date_from.slice(0, 10) : daysAgoISO(7);
const to = todayISO();
await window.API.downloadPeriod(from, to);
}
toast({ kind: 'success', title: 'Hisobot yuklab olindi' });
} catch (err) {
toast({ kind: 'danger', title: 'Excel xato', msg: err.message });
}
};
return (
>
}
/>
{/* Filters */}
Shifokor
{/* Totals */}
{/* Table */}
{!loading && paged.length === 0 ? (
) : (
| Sana |
Bemor |
Shifokor |
Jami |
To'lov |
Holat |
{paged.map(v => (
onNav('patient-detail', { patientId: v.patient.id })}>
| {uzDateTime(v.created_at)} |
{v.patient.full_name} |
{v.doctor?.full_name || —} |
{uzMoneyShort(v.total_amount)} |
|
|
))}
)}
{paged.length > 0 && }
);
}
/* =========================================================
DEBTS
========================================================= */
function DebtsPage({ onNav, toast }) {
const [debts, setDebts] = useState([]);
const [paymentModal, setPaymentModal] = useState(null);
const [loading, setLoading] = useState(true);
const load = () => {
setLoading(true);
window.API.listDebts({ limit: 500 })
.then(d => setDebts(d))
.catch(err => toast({ kind: 'danger', title: 'Xato', msg: err.message }))
.finally(() => setLoading(false));
};
useEffect(() => { load(); }, []);
const totalDebt = debts.reduce((s, v) => s + (Number(v.total_amount) - Number(v.paid_amount)), 0);
const handlePayment = async (amount, method) => {
try {
await window.API.addPayment(paymentModal.id, { amount, method });
toast({ kind: 'success', title: "To'lov qabul qilindi", msg: `${uzMoney(amount)} — ${paymentModal.patient.full_name}` });
setPaymentModal(null);
load();
} catch (err) {
toast({ kind: 'danger', title: 'Xato', msg: err.message });
}
};
return (
{debts.length > 0 && (
{debts.length} ta bemorda qarz mavjud
Jami summa: {uzMoney(totalDebt)}
)}
{!loading && debts.length === 0 ? (
) : (
| Bemor |
Telefon |
Sana |
Jami |
To'langan |
Qarz |
Vaqt |
|
{debts.map(v => {
const debt = Number(v.total_amount) - Number(v.paid_amount);
const daysAgo = Math.floor((Date.now() - new Date(v.created_at).getTime()) / 86400000);
return (
|
|
{v.patient.phone || '—'} |
{uzDate(v.created_at)} |
{uzMoneyShort(v.total_amount)} |
{uzMoneyShort(v.paid_amount)} |
{uzMoneyShort(debt)} |
7 ? 'danger' : daysAgo > 3 ? 'warning' : 'neutral'}>{daysAgo > 0 ? `${daysAgo} kun` : 'Bugun'}
|
e.stopPropagation()}>
|
);
})}
)}
{paymentModal && (
setPaymentModal(null)} onSubmit={handlePayment} />
)}
);
}
function PaymentModal({ visit, onClose, onSubmit }) {
const debt = Number(visit._debt ?? (Number(visit.total_amount) - Number(visit.paid_amount)));
const [amount, setAmount] = useState(debt);
const [type, setType] = useState('cash');
const [submitting, setSubmitting] = useState(false);
const valid = amount > 0 && amount <= debt;
const errorMsg = amount <= 0 ? "Summa noldan katta bo'lishi kerak"
: amount > debt ? `Qoldiq qarzdan (${uzMoneyShort(debt)}) oshmasligi kerak`
: null;
const handle = async () => {
if (!valid) return;
setSubmitting(true);
try { await onSubmit(amount, type); }
finally { setSubmitting(false); }
};
return (
>}>
Bemor
{visit.patient.full_name}
Jami
{uzMoneyShort(visit.total_amount)}
To'landi
{uzMoneyShort(visit.paid_amount)}
setAmount(parseInt(e.target.value.replace(/\D/g,'')) || 0)} />
{errorMsg && (
{errorMsg}
)}
To'lov turi
{[{id:'cash',l:'Naqd',i:'money'},{id:'card',l:'Karta',i:'credit-card'},{id:'transfer',l:"O'tkazma",i:'arrows-left-right'}].map(t => (
))}
);
}
/* =========================================================
REPORTS
========================================================= */
function ReportsPage({ toast }) {
const [tab, setTab] = useState('daily');
const [dailyDate, setDailyDate] = useState(todayISO());
const [from, setFrom] = useState(daysAgoISO(7));
const [to, setTo] = useState(todayISO());
const [downloading, setDownloading] = useState(false);
const downloadDaily = async () => {
setDownloading(true);
try {
await window.API.downloadDaily(dailyDate);
toast({ kind: 'success', title: 'Yuklab olindi' });
} catch (err) {
toast({ kind: 'danger', title: 'Xato', msg: err.message });
} finally { setDownloading(false); }
};
const downloadPeriod = async () => {
setDownloading(true);
try {
await window.API.downloadPeriod(from, to);
toast({ kind: 'success', title: 'Yuklab olindi' });
} catch (err) {
toast({ kind: 'danger', title: 'Xato', msg: err.message });
} finally { setDownloading(false); }
};
return (
{tab === 'daily' && (
Kunlik kassa hisobot
Bir kunlik barcha vizitlar, xizmatlar, to'lovlar va qarz jadval bilan Excel'ga yuklanadi.
setDailyDate(e.target.value)} style={{ width: 200 }} />
)}
{tab === 'period' && (
Davriy hisobot
Tanlangan davr uchun: umumiy ro'yxat, xizmat bo'yicha, shifokor bo'yicha, to'lov turi bo'yicha alohida sahifalar.
setFrom(e.target.value)} style={{ width: 180 }} />
setTo(e.target.value)} style={{ width: 180 }} />
)}
);
}
Object.assign(window, { KassaPage, DebtsPage, PaymentModal, ReportsPage });