Files
clinicpro/assets/admin/components/AppointmentInvoiceCard.tsx
hamedandClaude Opus 5 4fe0c4f9bf refactor(pricing): make the service the only price source
Price lists, annual tariffs and per-branch price overrides each answered
"what does this service cost?" differently, so a single date could carry
several answers and nobody could say which one was right. Price now lives
only on ServiceItem.price_rials, edited from the services page.

- drop PriceList/PriceListItem, their repositories and the seven
  /api/v1/price-list(s) endpoints; PricingController keeps only quote and
  the appointment price snapshot
- drop Tariff, TariffRepository, TariffService and the two
  /service-items/{uuid}/tariffs endpoints; creating or repricing a service
  no longer upserts a current-year tariff
- drop price_rials from ServiceBranchOverride; the entity stays for its
  duration columns, which DurationCalculator and ServiceSelectionValidator
  still read
- InvoiceService reads the item price directly
- PricingEngine collapses to a single source; breakdown.sources always
  reports service_item, keeping the response contract intact
- remove the price-lists admin page, its route and settings-menu entry, the
  tariff modal and the service detail tariffs tab; useAppointmentInvoice
  moves to its own hook file

Migration drops price_lists, price_list_items, service_tariffs and the
override price column.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 18:00:48 +03:30

112 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { formatDate, formatRial } from '../lib/utils';
import { useAppointmentInvoice } from '../hooks/useAppointmentInvoice';
interface Props {
appointmentUuid: string;
}
/**
* فاکتور تفکیک‌شدهٔ نوبت.
*
* اعدادش snapshot لحظهٔ ثبت‌اند، نه محاسبهٔ امروز: تغییر تعرفه هرگز فاکتور صادرشده را
* عوض نمی‌کند (قانون پنجم مستند). همین جمله زیر کارت هم نوشته می‌شود، چون کاربری که
* قیمت را دیروز عوض کرده و امروز عدد قدیمی می‌بیند وگرنه فکر می‌کند سیستم خراب است.
*/
export default function AppointmentInvoiceCard({ appointmentUuid }: Props) {
const { invoice, loading, missing } = useAppointmentInvoice(appointmentUuid);
if (loading) {
return (
<div className="card" style={{ fontSize: 13, color: 'var(--text-3)' }}>
در حال بارگذاری فاکتور…
</div>
);
}
// نبودِ فاکتور خطا نیست: نوبتی که هنوز ثبت نهایی نشده، فاکتوری هم ندارد.
if (missing || !invoice) {
return (
<div className="card" style={{ fontSize: 13, color: 'var(--text-3)' }}>
برای این نوبت فاکتوری ثبت نشده است.
</div>
);
}
const rows: { label: string; value: number; muted?: boolean }[] = [
{ label: 'قیمت پایه', value: invoice.base_rials },
{ label: 'آیتم‌های اضافه', value: invoice.items_rials },
{ label: 'تخفیف', value: -invoice.discount_rials },
{ label: 'سهم بیمهٔ پایه', value: -invoice.insurance_base_rials },
{ label: 'سهم بیمهٔ تکمیلی', value: -invoice.insurance_supplementary_rials },
{ label: 'مالیات', value: invoice.tax_rials },
];
return (
<div className="card" style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<h3 style={{ fontSize: 15, margin: 0 }}>فاکتور</h3>
{invoice.created_at !== undefined && (
<span style={{ fontSize: 12, color: 'var(--text-3)' }}>
ثبت‌شده در {formatDate(invoice.created_at)}
</span>
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{rows
.filter((row) => row.value !== 0)
.map((row) => (
<div
key={row.label}
style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}
>
<span style={{ color: 'var(--text-2)' }}>{row.label}</span>
<span style={{ color: row.value < 0 ? 'var(--success)' : undefined }}>
{formatRial(Math.abs(row.value))}
{row.value < 0 ? ' ' : ''}
</span>
</div>
))}
{/* فاکتور قدیمی ممکن است ریز تخفیف نداشته باشد؛ نبودنش نباید کل صفحه را ببندد. */}
{(invoice.breakdown?.discounts ?? []).map((line, index) => (
<div
key={index}
style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: 'var(--text-3)' }}
>
<span>{line.label}</span>
<span>{formatRial(Math.abs(line.rials))}</span>
</div>
))}
</div>
<div
style={{
borderTop: '1px solid var(--border)',
paddingTop: 10,
display: 'flex',
justifyContent: 'space-between',
fontSize: 15,
fontWeight: 600,
}}
>
<span>مبلغ نهایی</span>
<span>{formatRial(invoice.final_rials)}</span>
</div>
{invoice.deposit_rials > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
<span style={{ color: 'var(--text-2)' }}>بیعانه</span>
<span>{formatRial(invoice.deposit_rials)}</span>
</div>
)}
<span style={{ fontSize: 12, color: 'var(--text-3)' }}>
قیمت‌ها بر اساس تاریخ همین نوبت محاسبه و ثبت شده‌اند؛ تغییر بعدی تعرفه این فاکتور
را عوض نمی‌کند.
</span>
</div>
);
}