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>
This commit is contained in:
@@ -16,24 +16,10 @@ import FeatureGate from '../components/ui/FeatureGate';
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { useUrlState } from '../hooks/useUrlState';
|
||||
import ServiceTariffModal from '../components/ServiceTariffModal';
|
||||
import ServiceInsuranceModal from '../components/ServiceInsuranceModal';
|
||||
import ServiceItemFormModal from '../components/ServiceItemFormModal';
|
||||
import ServiceCategoryTab from '../components/ServiceCategoryTab';
|
||||
|
||||
interface Tariff {
|
||||
uuid: string;
|
||||
year: number;
|
||||
price_rials: number;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
interface TariffList {
|
||||
current_year: number;
|
||||
default_price_rials: number;
|
||||
data: Tariff[];
|
||||
}
|
||||
|
||||
interface TenantInsurance {
|
||||
uuid: string;
|
||||
insurance_name: string | null;
|
||||
@@ -51,7 +37,6 @@ interface CoverageRow {
|
||||
|
||||
const TABS = [
|
||||
{ id: 'info', label: 'اطلاعات سرویس' },
|
||||
{ id: 'tariffs', label: 'تعرفهها' },
|
||||
{ id: 'insurance', label: 'بیمهها' },
|
||||
{ id: 'categories',label: 'دستهبندیها' },
|
||||
{ id: 'goods', label: 'کالاهای مرتبط' },
|
||||
@@ -164,57 +149,6 @@ function InfoTab({ item }: { item: ServiceItem }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TariffsTab({ item, onManage, canUpdate }: { item: ServiceItem; onManage: () => void; canUpdate: boolean }) {
|
||||
const { data, isLoading } = useQuery<ApiResponse<TariffList>>({
|
||||
queryKey: ['service-tariffs', item.uuid],
|
||||
queryFn: () => api.get(`/api/v1/service-items/${item.uuid}/tariffs`),
|
||||
});
|
||||
|
||||
const list = data?.data;
|
||||
const tariffs = list?.data ?? [];
|
||||
|
||||
return (
|
||||
<div className="card card-pad">
|
||||
<div className="card-title-row">
|
||||
<div>
|
||||
<b style={{ fontSize: 14 }}>تعرفههای سالانه</b>
|
||||
<div className="muted" style={{ fontSize: 12 }}>
|
||||
قیمت هر سال شمسی؛ قیمت سال جاری مبنای صورتحساب است.
|
||||
</div>
|
||||
</div>
|
||||
{canUpdate && <button className="btn primary sm" onClick={onManage}>مدیریت تعرفهها</button>}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="muted" style={{ fontSize: 13, padding: '12px 0' }}>در حال بارگذاری...</div>
|
||||
) : tariffs.length === 0 ? (
|
||||
<div className="empty" style={{ padding: '28px 0' }}>
|
||||
<BanknotesIcon style={{ width: 30, height: 30 }} />
|
||||
<p className="muted">تعرفهای ثبت نشده است</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 12 }}>
|
||||
{tariffs.map((t) => (
|
||||
<div key={t.uuid} style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10,
|
||||
padding: '10px 12px', borderRadius: 'var(--r-sm)',
|
||||
border: '1px solid var(--border)',
|
||||
background: t.year === list?.current_year ? 'var(--primary-soft)' : 'var(--surface)',
|
||||
}}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
|
||||
<b>{formatYear(t.year)}</b>
|
||||
{t.year === list?.current_year && <span className="badge green" style={{ fontSize: 10 }}>سال جاری</span>}
|
||||
{!t.is_active && <span className="badge gray" style={{ fontSize: 10 }}>غیرفعال</span>}
|
||||
</span>
|
||||
<b style={{ fontSize: 13.5, color: 'var(--primary)' }}>{formatRial(t.price_rials)}</b>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InsuranceTab({ item, onManage, canUpdate }: { item: ServiceItem; onManage: () => void; canUpdate: boolean }) {
|
||||
const { data: contractsData, isLoading } = useQuery<{ data: { data: TenantInsurance[] } }>({
|
||||
queryKey: ['tenant-insurances'],
|
||||
@@ -460,7 +394,6 @@ function ServiceDetailPageInner() {
|
||||
const tab = (TABS.some((t) => t.id === urlState.tab) ? urlState.tab : 'info') as TabId;
|
||||
const setTab = (id: TabId) => setUrlState({ tab: id });
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [tariffOpen, setTariffOpen] = useState(false);
|
||||
const [insuranceOpen, setInsuranceOpen] = useState(false);
|
||||
const [toggleOpen, setToggleOpen] = useState(false);
|
||||
|
||||
@@ -541,7 +474,6 @@ function ServiceDetailPageInner() {
|
||||
</div>
|
||||
|
||||
{tab === 'info' && <InfoTab item={item} />}
|
||||
{tab === 'tariffs' && <TariffsTab item={item} onManage={() => setTariffOpen(true)} canUpdate={canUpdate} />}
|
||||
{tab === 'insurance' && <InsuranceTab item={item} onManage={() => setInsuranceOpen(true)} canUpdate={canUpdate} />}
|
||||
{tab === 'categories' && (
|
||||
<ServiceCategoryTab
|
||||
@@ -559,7 +491,6 @@ function ServiceDetailPageInner() {
|
||||
onClose={() => setEditOpen(false)}
|
||||
onManageInsurance={() => setInsuranceOpen(true)}
|
||||
/>
|
||||
<ServiceTariffModal item={tariffOpen ? item : null} onClose={() => setTariffOpen(false)} />
|
||||
<ServiceInsuranceModal item={insuranceOpen ? item : null} onClose={() => setInsuranceOpen(false)} />
|
||||
|
||||
<ConfirmDialog
|
||||
|
||||
Reference in New Issue
Block a user