feat(tax): implement tax rate history tracking and API endpoints
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user