feat(tax): implement tax rate history tracking and API endpoints

This commit is contained in:
hamed
2026-06-24 13:17:08 +03:30
parent 148d033114
commit 89e4a424f8
7 changed files with 186 additions and 82 deletions
+34 -77
View File
@@ -5,18 +5,21 @@ import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import { formatDateTime } from '../lib/utils';
import { Cog6ToothIcon, CheckCircleIcon } from '@heroicons/react/24/outline';
interface TaxHistoryRow {
tax_percent: number;
enabled: boolean;
changed_by_name: string | null;
changed_at: number;
}
// ── Schema ────────────────────────────────────────────────────────────────
const schema = z.object({
site_name: z.string().min(1, 'نام سایت الزامی است'),
support_phone: z.string(),
commission_enabled: z.string(),
commission_percent: z.string().refine(v => {
const n = Number(v);
return !isNaN(n) && n >= 0 && n <= 100;
}, 'درصد باید بین ۰ تا ۱۰۰ باشد'),
max_cancel_hours_before: z.string(),
appointment_reminder_hours: z.string(),
// financial engine
@@ -44,8 +47,6 @@ type FormValues = z.infer<typeof schema>;
interface Settings {
site_name: string;
support_phone: string;
commission_enabled: string;
commission_percent: string;
max_cancel_hours_before: string;
appointment_reminder_hours: string;
appointment_commission_enabled: string;
@@ -81,6 +82,14 @@ export default function SettingsPage() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const settings: Settings | undefined = (data?.data as any)?.data ?? data?.data;
const taxHistoryQ = useQuery({
queryKey: ['tax-history'],
queryFn: () => api.get<ApiResponse<TaxHistoryRow[]>>('/api/v1/admin/settings/tax-history'),
staleTime: 30_000,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const taxHistory: TaxHistoryRow[] = (taxHistoryQ.data?.data as any)?.data ?? taxHistoryQ.data?.data ?? [];
const {
register,
handleSubmit,
@@ -95,8 +104,6 @@ export default function SettingsPage() {
reset({
site_name: settings.site_name ?? 'ClinicPro',
support_phone: settings.support_phone ?? '',
commission_enabled: settings.commission_enabled ?? '0',
commission_percent: settings.commission_percent ?? '0',
max_cancel_hours_before: settings.max_cancel_hours_before ?? '24',
appointment_reminder_hours: settings.appointment_reminder_hours ?? '2',
appointment_commission_enabled: settings.appointment_commission_enabled ?? '0',
@@ -123,10 +130,10 @@ export default function SettingsPage() {
api.patch<ApiResponse<Settings>>('/api/v1/admin/settings', values),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin-settings'] });
qc.invalidateQueries({ queryKey: ['tax-history'] });
},
});
const commissionEnabled = watch('commission_enabled') === '1';
const paymentTestMode = watch('payment_test_mode') === '1';
const apptCommissionEnabled = watch('appointment_commission_enabled') === '1';
const upgradeCommissionEnabled = watch('upgrade_commission_enabled') === '1';
@@ -210,71 +217,6 @@ export default function SettingsPage() {
</div>
</div>
{/* تنظیمات کمیسیون */}
<div className="card card-pad">
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: '1.25rem' }}>
<div className="ico" style={{ background: 'var(--success-bg)', color: 'var(--success)', width: 36, height: 36, borderRadius: 10 }}>
<span style={{ fontSize: 18 }}>٪</span>
</div>
<h3 style={{ fontSize: 15 }}>کمیسیون سایت</h3>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{/* toggle فعال/غیرفعال */}
<label style={{ display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer' }}>
<input type="hidden" {...register('commission_enabled')} />
<div
style={{ position: 'relative', width: 44, height: 24, flexShrink: 0, cursor: 'pointer' }}
onClick={() => setValue('commission_enabled', commissionEnabled ? '0' : '1', { shouldDirty: true })}
>
<div style={{
width: 44, height: 24, borderRadius: 12,
background: commissionEnabled ? 'var(--primary)' : 'var(--border)',
transition: 'background .2s', position: 'relative',
}}>
<div style={{
position: 'absolute', top: 2, width: 20, height: 20, borderRadius: '50%', background: 'var(--surface)',
transition: 'right .2s', right: commissionEnabled ? 2 : 22,
boxShadow: '0 1px 3px rgba(0,0,0,.2)',
}} />
</div>
</div>
<span style={{ fontSize: 14 }}>
{commissionEnabled ? 'کمیسیون فعال است' : 'کمیسیون غیرفعال است'}
</span>
</label>
{commissionEnabled && (
<div style={{ maxWidth: 280 }}>
<label style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 6 }}>
درصد کمیسیون از کاربر (۰۱۰۰)
</label>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input
{...register('commission_percent')}
type="number"
min={0}
max={100}
style={{
width: 120, height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)',
border: `1px solid ${errors.commission_percent ? 'var(--danger)' : 'var(--border)'}`,
background: 'var(--surface)', color: 'var(--text)', fontSize: 13.5,
boxSizing: 'border-box',
}}
/>
<span className="muted" style={{ fontSize: 13 }}>درصد</span>
</div>
{errors.commission_percent && (
<p style={{ color: 'var(--danger)', fontSize: 12, marginTop: 4 }}>{errors.commission_percent.message}</p>
)}
<p className="muted" style={{ fontSize: 12, marginTop: 8 }}>
کمیسیون فقط از کاربر دریافت میشود. منشیها کمیسیون ندارند.
</p>
</div>
)}
</div>
</div>
{/* موتور مالی نمایندگی */}
<div className="card card-pad">
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: '1.25rem' }}>
@@ -352,6 +294,23 @@ export default function SettingsPage() {
</div>
</div>
)}
{taxHistory.length > 0 && (
<div style={{ marginTop: 14 }}>
<div style={{ fontSize: 12.5, fontWeight: 600, marginBottom: 8 }}>تاریخچه تغییرات مالیات</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxWidth: 460 }}>
{taxHistory.map((h, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, fontSize: 12.5, padding: '6px 10px', borderRadius: 'var(--r-sm)', background: 'var(--surface-3)' }}>
<span>
{h.enabled ? `${h.tax_percent}٪` : 'غیرفعال'}
{h.changed_by_name && <span className="muted"> {h.changed_by_name}</span>}
</span>
<span className="muted">{formatDateTime(h.changed_at)}</span>
</div>
))}
</div>
</div>
)}
</div>
{/* هزینه پنل پیامک */}
@@ -555,8 +514,6 @@ export default function SettingsPage() {
onClick={() => settings && reset({
site_name: settings.site_name,
support_phone: settings.support_phone,
commission_enabled: settings.commission_enabled,
commission_percent: settings.commission_percent,
max_cancel_hours_before: settings.max_cancel_hours_before,
appointment_reminder_hours: settings.appointment_reminder_hours,
appointment_commission_enabled: settings.appointment_commission_enabled,