feat(discount): apply rule-based discounts on the session payment step
PaymentStep now loads /session/{uuid}/discount-suggestions and lets the
operator apply a suggested rule (PATCH discount_rule_uuid) or remove it,
alongside the existing manual discount. Adds an amount summary (before /
discount + source rule label / final payable). Verified end-to-end.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ export interface SessionCardData {
|
||||
discount_type?: 'percent' | 'fixed' | null;
|
||||
discount_value?: number;
|
||||
discount_rials?: number;
|
||||
applied_discount_rule_label?: string | null;
|
||||
paid_total_rials?: number;
|
||||
paid_at?: number | null;
|
||||
payments?: SessionPaymentEntry[];
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '../../lib/api';
|
||||
import type { ApiResponse } from '../../lib/api';
|
||||
import { formatRial } from '../../lib/utils';
|
||||
import type { DiscountSuggestion } from '../../types';
|
||||
import type { SessionCardData } from '../SessionServiceCard';
|
||||
import SearchableSelect from '../ui/SearchableSelect';
|
||||
import PersianDateInput from '../ui/PersianDateInput';
|
||||
@@ -54,6 +56,7 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
|
||||
const invalidate = () => {
|
||||
qc.invalidateQueries({ queryKey: ['patient-sessions', recordUuid] });
|
||||
qc.invalidateQueries({ queryKey: ['patient-wallet', recordUuid] });
|
||||
qc.invalidateQueries({ queryKey: ['discount-suggestions', sessionUuid] });
|
||||
};
|
||||
|
||||
const discountMut = useMutation({
|
||||
@@ -68,13 +71,22 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
const suggestionsQ = useQuery({
|
||||
queryKey: ['discount-suggestions', sessionUuid],
|
||||
queryFn: () => api.get<ApiResponse<DiscountSuggestion[]>>(`/api/v1/session/${sessionUuid}/discount-suggestions`),
|
||||
});
|
||||
const suggestions: DiscountSuggestion[] = (suggestionsQ.data?.data as any)?.data ?? (suggestionsQ.data?.data as any) ?? [];
|
||||
|
||||
const applyDiscount = () => {
|
||||
if (!discountType || !discountValue) return;
|
||||
discountMut.mutate({ discount_type: discountType, discount_value: Number(discountValue) });
|
||||
};
|
||||
const applyRule = (ruleUuid: string) => {
|
||||
discountMut.mutate({ discount_rule_uuid: ruleUuid });
|
||||
};
|
||||
const removeDiscount = () => {
|
||||
setDiscountType(''); setDiscountValue('');
|
||||
discountMut.mutate({ discount_type: null });
|
||||
discountMut.mutate({ discount_rule_uuid: null });
|
||||
};
|
||||
const submitPayment = (method: string) => {
|
||||
if (!amount) return;
|
||||
@@ -85,6 +97,8 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
|
||||
const discountRials = session.discount_rials ?? 0;
|
||||
const debt = session.patient_debt_rials ?? 0;
|
||||
const payments = session.payments ?? [];
|
||||
const appliedRuleLabel = session.applied_discount_rule_label ?? null;
|
||||
const payable = Math.max(0, finalPrice - discountRials);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -95,6 +109,31 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
|
||||
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 700, color: '#525252' }}>{formatRial(finalPrice)}</span>
|
||||
</div>
|
||||
|
||||
{/* تخفیفهای پیشنهادی (قوانین تخفیف) */}
|
||||
{suggestions.length > 0 && (
|
||||
<div className="dark:border-[#35343D]" style={{ border: '1px solid #E8EBFF', background: '#F7F8FF', borderRadius: 8, padding: 12, marginBottom: 16 }}>
|
||||
<span style={{ ...fieldLabel, marginBottom: 10, fontWeight: 600, color: '#3B3F9F' }}>تخفیفهای قابل اعمال:</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{suggestions.map((s) => (
|
||||
<div key={s.rule_uuid} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, color: '#374151' }}>
|
||||
{s.rule_name} — <span style={{ color: '#D32F2F' }}>{formatRial(s.discount_rials)}</span>
|
||||
{' '}<span style={{ color: '#6B7280', fontSize: 12 }}>({s.discount_type === 'percent' ? `${s.value}٪` : 'مبلغ ثابت'})</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyRule(s.rule_uuid)}
|
||||
disabled={discountMut.isPending}
|
||||
style={{ height: 32, minWidth: 64, border: 'none', borderRadius: 6, background: '#5559CE', color: '#fff', fontSize: 12.5, fontWeight: 600, cursor: 'pointer' }}
|
||||
>
|
||||
اعمال
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* تخفیف — port of tauri DiscountInput (نوع + مقدار + ثبت) */}
|
||||
<span style={fieldLabel}>تخفیف:</span>
|
||||
<div style={{ display: 'flex', alignItems: 'stretch', gap: 8 }}>
|
||||
@@ -134,10 +173,21 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: 13, marginBottom: 16 }}>
|
||||
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>مبلغ تخفیف:</span>{' '}
|
||||
<span style={{ color: '#D32F2F' }}>{formatRial(discountRials)}</span>
|
||||
</p>
|
||||
{/* خلاصهی مبلغ: قبل از تخفیف / تخفیف / نهایی / منبع */}
|
||||
<div className="dark:border-[#35343D]" style={{ border: '1px solid #e0e0e0', borderRadius: 8, padding: 12, marginBottom: 16, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
|
||||
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>مبلغ قبل از تخفیف:</span>
|
||||
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>{formatRial(finalPrice)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
|
||||
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>مبلغ تخفیف{appliedRuleLabel ? ` (${appliedRuleLabel})` : ''}:</span>
|
||||
<span style={{ color: '#D32F2F' }}>{formatRial(discountRials)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, fontWeight: 700, borderTop: '1px dashed #e0e0e0', paddingTop: 6 }}>
|
||||
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>مبلغ نهایی قابل پرداخت:</span>
|
||||
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>{formatRial(payable)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* تاریخ پرداخت */}
|
||||
<span className="dark:text-[#A1A1A1]" style={{ ...fieldLabel, color: '#3b3b3b' }}>تاریخ پرداخت</span>
|
||||
|
||||
Reference in New Issue
Block a user