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>
112 lines
4.4 KiB
TypeScript
112 lines
4.4 KiB
TypeScript
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>
|
||
);
|
||
}
|