- Added new configuration keys for appointment and upgrade commissions, tax settings, and SMS panel fee in SiteConfigController and SiteConfigRepository. - Introduced CommissionService to handle commission calculations for appointments and subscriptions, including tax deductions and SMS fees. - Created FinancialBreakdown entity and repository to log financial transactions. - Updated PaymentController to process commissions upon successful payments for appointments and subscriptions. - Developed FinancialReportPage in the admin panel to display financial breakdowns and summaries. - Added database migration for the new financial_breakdowns table.
596 lines
34 KiB
TypeScript
596 lines
34 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useForm } from 'react-hook-form';
|
|
import { z } from 'zod';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { api } from '../lib/api';
|
|
import type { ApiResponse } from '../lib/api';
|
|
import { Cog6ToothIcon, CheckCircleIcon } from '@heroicons/react/24/outline';
|
|
|
|
// ── 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
|
|
appointment_commission_enabled: z.string(),
|
|
upgrade_commission_enabled: z.string(),
|
|
upgrade_commission_percent: z.string(),
|
|
tax_enabled: z.string(),
|
|
tax_percent: z.string(),
|
|
sms_panel_fee_rials: z.string(),
|
|
// payment gateways
|
|
payment_test_mode: z.string(),
|
|
mellat_terminal_id: z.string(),
|
|
mellat_username: z.string(),
|
|
mellat_password: z.string(),
|
|
sep_terminal_id: z.string(),
|
|
// sms
|
|
sms_provider: z.string(),
|
|
kavenegar_api_key: z.string(),
|
|
kavenegar_sender: z.string(),
|
|
sms_price_rials: z.string(),
|
|
});
|
|
|
|
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;
|
|
upgrade_commission_enabled: string;
|
|
upgrade_commission_percent: string;
|
|
tax_enabled: string;
|
|
tax_percent: string;
|
|
sms_panel_fee_rials: string;
|
|
payment_test_mode: string;
|
|
mellat_terminal_id: string;
|
|
mellat_username: string;
|
|
mellat_password: string;
|
|
sep_terminal_id: string;
|
|
sms_provider: string;
|
|
kavenegar_api_key: string;
|
|
kavenegar_sender: string;
|
|
sms_price_rials: string;
|
|
}
|
|
|
|
// ── Component ─────────────────────────────────────────────────────────────
|
|
|
|
export default function SettingsPage() {
|
|
const qc = useQueryClient();
|
|
const [showMellatPassword, setShowMellatPassword] = useState(false);
|
|
const [showKavenegarKey, setShowKavenegarKey] = useState(false);
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['admin-settings'],
|
|
queryFn: () => api.get<ApiResponse<Settings>>('/api/v1/admin/settings'),
|
|
staleTime: 30_000,
|
|
});
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const settings: Settings | undefined = (data?.data as any)?.data ?? data?.data;
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
reset,
|
|
watch,
|
|
setValue,
|
|
formState: { errors, isDirty },
|
|
} = useForm<FormValues>({ resolver: zodResolver(schema) });
|
|
|
|
useEffect(() => {
|
|
if (settings) {
|
|
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',
|
|
upgrade_commission_enabled: settings.upgrade_commission_enabled ?? '0',
|
|
upgrade_commission_percent: settings.upgrade_commission_percent ?? '20',
|
|
tax_enabled: settings.tax_enabled ?? '0',
|
|
tax_percent: settings.tax_percent ?? '10',
|
|
sms_panel_fee_rials: settings.sms_panel_fee_rials ?? '1500000',
|
|
payment_test_mode: settings.payment_test_mode ?? '0',
|
|
mellat_terminal_id: settings.mellat_terminal_id ?? '',
|
|
mellat_username: settings.mellat_username ?? '',
|
|
mellat_password: settings.mellat_password ?? '',
|
|
sep_terminal_id: settings.sep_terminal_id ?? '',
|
|
sms_provider: settings.sms_provider ?? 'kavenegar',
|
|
kavenegar_api_key: settings.kavenegar_api_key ?? '',
|
|
kavenegar_sender: settings.kavenegar_sender ?? '',
|
|
sms_price_rials: settings.sms_price_rials ?? '500',
|
|
});
|
|
}
|
|
}, [settings, reset]);
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (values: FormValues) =>
|
|
api.patch<ApiResponse<Settings>>('/api/v1/admin/settings', values),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: ['admin-settings'] });
|
|
},
|
|
});
|
|
|
|
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';
|
|
const taxEnabled = watch('tax_enabled') === '1';
|
|
|
|
const onSubmit = (values: FormValues) => {
|
|
mutation.mutate(values);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="fade-in">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap)' }}>
|
|
{Array.from({ length: 3 }).map((_, i) => (
|
|
<div key={i} className="card card-pad">
|
|
<div className="skeleton" style={{ height: 200, borderRadius: 'var(--r)' }} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="fade-in">
|
|
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
|
<div>
|
|
<h1 className="section-title">تنظیمات سایت</h1>
|
|
<div className="muted" style={{ fontSize: 13, marginTop: 3 }}>پیکربندی کلی پلتفرم</div>
|
|
</div>
|
|
{mutation.isSuccess && (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'var(--success)', fontSize: 13.5 }}>
|
|
<CheckCircleIcon style={{ width: 18, height: 18 }} />
|
|
تنظیمات ذخیره شد
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap)' }}>
|
|
|
|
{/* اطلاعات پایه */}
|
|
<div className="card card-pad">
|
|
<div className="card-title-row" style={{ marginBottom: '1.25rem' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<div className="ico" style={{ background: 'var(--primary-soft)', color: 'var(--primary)', width: 36, height: 36, borderRadius: 10 }}>
|
|
<Cog6ToothIcon style={{ width: 18, height: 18 }} />
|
|
</div>
|
|
<h3 style={{ fontSize: 15 }}>اطلاعات پایه</h3>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
|
<div>
|
|
<label style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 6 }}>نام سایت</label>
|
|
<input
|
|
{...register('site_name')}
|
|
style={{
|
|
width: '100%', height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)',
|
|
border: `1px solid ${errors.site_name ? 'var(--danger)' : 'var(--border)'}`,
|
|
background: 'var(--surface)', color: 'var(--text)', fontSize: 13.5,
|
|
boxSizing: 'border-box',
|
|
}}
|
|
/>
|
|
{errors.site_name && <p style={{ color: 'var(--danger)', fontSize: 12, marginTop: 4 }}>{errors.site_name.message}</p>}
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 6 }}>شماره پشتیبانی</label>
|
|
<input
|
|
{...register('support_phone')}
|
|
dir="ltr"
|
|
placeholder="021-12345678"
|
|
style={{
|
|
width: '100%', height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)',
|
|
border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13.5,
|
|
boxSizing: 'border-box',
|
|
}}
|
|
/>
|
|
</div>
|
|
</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' }}>
|
|
<div className="ico" style={{ background: 'var(--info-bg)', color: 'var(--info)', width: 36, height: 36, borderRadius: 10 }}>
|
|
<span style={{ fontSize: 16 }}>🧮</span>
|
|
</div>
|
|
<h3 style={{ fontSize: 15 }}>موتور مالی نمایندگی</h3>
|
|
</div>
|
|
|
|
<p className="muted" style={{ fontSize: 12, marginBottom: '1rem', lineHeight: 1.7 }}>
|
|
ترتیب کسرها: ابتدا هزینه پنل پیامک، سپس مالیات بر ارزش افزوده (استخراجی از مبلغ شامل مالیات)،
|
|
و در نهایت پورسانت نماینده از مبلغِ خالصِ پس از مالیات محاسبه میشود.
|
|
</p>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
|
|
|
{/* پورسانت نوبت */}
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer' }}>
|
|
<input type="hidden" {...register('appointment_commission_enabled')} />
|
|
<div style={{ position: 'relative', width: 44, height: 24, flexShrink: 0 }}
|
|
onClick={() => setValue('appointment_commission_enabled', apptCommissionEnabled ? '0' : '1', { shouldDirty: true })}>
|
|
<div style={{ width: 44, height: 24, borderRadius: 12, background: apptCommissionEnabled ? '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: apptCommissionEnabled ? 2 : 22, boxShadow: '0 1px 3px rgba(0,0,0,.2)' }} />
|
|
</div>
|
|
</div>
|
|
<span style={{ fontSize: 14 }}>
|
|
پورسانت نوبت نمایندگان {apptCommissionEnabled ? 'فعال' : 'غیرفعال'} است
|
|
<span className="muted" style={{ fontSize: 12, marginRight: 6 }}>(درصد از پروفایل هر نماینده خوانده میشود)</span>
|
|
</span>
|
|
</label>
|
|
|
|
{/* پورسانت ارتقاء اشتراک */}
|
|
<div>
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer', marginBottom: upgradeCommissionEnabled ? 12 : 0 }}>
|
|
<input type="hidden" {...register('upgrade_commission_enabled')} />
|
|
<div style={{ position: 'relative', width: 44, height: 24, flexShrink: 0 }}
|
|
onClick={() => setValue('upgrade_commission_enabled', upgradeCommissionEnabled ? '0' : '1', { shouldDirty: true })}>
|
|
<div style={{ width: 44, height: 24, borderRadius: 12, background: upgradeCommissionEnabled ? '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: upgradeCommissionEnabled ? 2 : 22, boxShadow: '0 1px 3px rgba(0,0,0,.2)' }} />
|
|
</div>
|
|
</div>
|
|
<span style={{ fontSize: 14 }}>پورسانت ارتقاء اشتراک {upgradeCommissionEnabled ? 'فعال' : 'غیرفعال'} است</span>
|
|
</label>
|
|
{upgradeCommissionEnabled && (
|
|
<div style={{ maxWidth: 280, paddingRight: 56 }}>
|
|
<label style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 6 }}>درصد پورسانت ارتقاء (۰–۱۰۰)</label>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<input {...register('upgrade_commission_percent')} type="number" min={0} max={100}
|
|
style={{ width: 120, height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13.5, boxSizing: 'border-box' }} />
|
|
<span className="muted" style={{ fontSize: 13 }}>درصد</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* مالیات بر ارزش افزوده */}
|
|
<div>
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer', marginBottom: taxEnabled ? 12 : 0 }}>
|
|
<input type="hidden" {...register('tax_enabled')} />
|
|
<div style={{ position: 'relative', width: 44, height: 24, flexShrink: 0 }}
|
|
onClick={() => setValue('tax_enabled', taxEnabled ? '0' : '1', { shouldDirty: true })}>
|
|
<div style={{ width: 44, height: 24, borderRadius: 12, background: taxEnabled ? '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: taxEnabled ? 2 : 22, boxShadow: '0 1px 3px rgba(0,0,0,.2)' }} />
|
|
</div>
|
|
</div>
|
|
<span style={{ fontSize: 14 }}>مالیات بر ارزش افزوده {taxEnabled ? 'فعال' : 'غیرفعال'} است</span>
|
|
</label>
|
|
{taxEnabled && (
|
|
<div style={{ maxWidth: 280, paddingRight: 56 }}>
|
|
<label style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 6 }}>درصد مالیات (۰–۱۰۰)</label>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<input {...register('tax_percent')} type="number" min={0} max={100}
|
|
style={{ width: 120, height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13.5, boxSizing: 'border-box' }} />
|
|
<span className="muted" style={{ fontSize: 13 }}>درصد</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* هزینه پنل پیامک */}
|
|
<div style={{ maxWidth: 320 }}>
|
|
<label style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 6 }}>هزینه ثابت پنل پیامک (ریال)</label>
|
|
<input {...register('sms_panel_fee_rials')} type="number" min={0} dir="ltr" placeholder="1500000"
|
|
style={{ width: 200, height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13.5, boxSizing: 'border-box' }} />
|
|
<p className="muted" style={{ fontSize: 12, marginTop: 6 }}>
|
|
از مبلغِ هر تراکنش (نوبت و اشتراک) کسر میشود. ۱٬۵۰۰٬۰۰۰ ریال = ۱۵۰٬۰۰۰ تومان.
|
|
</p>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{/* تنظیمات نوبتدهی */}
|
|
<div className="card card-pad">
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: '1.25rem' }}>
|
|
<div className="ico" style={{ background: 'var(--warning-bg)', color: 'var(--warning)', width: 36, height: 36, borderRadius: 10 }}>
|
|
<span style={{ fontSize: 16 }}>⏱</span>
|
|
</div>
|
|
<h3 style={{ fontSize: 15 }}>تنظیمات نوبتدهی</h3>
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
|
<div>
|
|
<label style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 6 }}>
|
|
حداکثر ساعت مجاز برای لغو نوبت
|
|
</label>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<input
|
|
{...register('max_cancel_hours_before')}
|
|
type="number"
|
|
min={0}
|
|
style={{
|
|
width: 100, height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)',
|
|
border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13.5,
|
|
boxSizing: 'border-box',
|
|
}}
|
|
/>
|
|
<span className="muted" style={{ fontSize: 13 }}>ساعت قبل از نوبت</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 13, fontWeight: 500, display: 'block', marginBottom: 6 }}>
|
|
ارسال یادآور قبل از نوبت
|
|
</label>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<input
|
|
{...register('appointment_reminder_hours')}
|
|
type="number"
|
|
min={0}
|
|
style={{
|
|
width: 100, height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)',
|
|
border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13.5,
|
|
boxSizing: 'border-box',
|
|
}}
|
|
/>
|
|
<span className="muted" style={{ fontSize: 13 }}>ساعت قبل از نوبت</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* درگاه پرداخت */}
|
|
<div className="card card-pad">
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: '1.25rem' }}>
|
|
<div className="ico" style={{ background: 'var(--warning-bg)', color: 'var(--warning)', width: 36, height: 36, borderRadius: 10 }}>
|
|
<span style={{ fontSize: 16 }}>💳</span>
|
|
</div>
|
|
<h3 style={{ fontSize: 15 }}>درگاه پرداخت</h3>
|
|
</div>
|
|
|
|
{/* حالت تست */}
|
|
<div style={{ marginBottom: '1.25rem', padding: '12px 16px', borderRadius: 'var(--r-sm)', background: paymentTestMode ? 'var(--warning-bg)' : 'var(--surface)', border: `1px solid ${paymentTestMode ? 'var(--warning)' : 'var(--border)'}` }}>
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer' }}>
|
|
<input type="hidden" {...register('payment_test_mode')} />
|
|
<div
|
|
style={{ position: 'relative', width: 44, height: 24, flexShrink: 0, cursor: 'pointer' }}
|
|
onClick={() => setValue('payment_test_mode', paymentTestMode ? '0' : '1', { shouldDirty: true })}
|
|
>
|
|
<div style={{ width: 44, height: 24, borderRadius: 12, background: paymentTestMode ? 'var(--warning)' : '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: paymentTestMode ? 2 : 22, boxShadow: '0 1px 3px rgba(0,0,0,.2)' }} />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: 14, fontWeight: 600 }}>{paymentTestMode ? 'حالت تست فعال' : 'حالت تست غیرفعال'}</div>
|
|
<div className="muted" style={{ fontSize: 12, marginTop: 2 }}>
|
|
{paymentTestMode ? 'همه پرداختها از درگاه آزمایشی رد میشوند (پول واقعی کسر نمیشود)' : 'پرداختها از درگاه واقعی انجام میشوند'}
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
{/* Mellat */}
|
|
<div style={{ marginBottom: '1.25rem' }}>
|
|
<div style={{ fontSize: 13.5, fontWeight: 600, marginBottom: '0.75rem', display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
<span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--violet)', display: 'inline-block' }} />
|
|
درگاه ملت (Mellat)
|
|
</div>
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '0.75rem' }}>
|
|
<div>
|
|
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>شناسه پایانه</label>
|
|
<input {...register('mellat_terminal_id')} dir="ltr" placeholder="12345678"
|
|
style={{ width: '100%', height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, boxSizing: 'border-box' }}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>نام کاربری</label>
|
|
<input {...register('mellat_username')} dir="ltr" placeholder="username"
|
|
style={{ width: '100%', height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, boxSizing: 'border-box' }}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>رمز عبور</label>
|
|
<div style={{ position: 'relative' }}>
|
|
<input {...register('mellat_password')} type={showMellatPassword ? 'text' : 'password'} dir="ltr" placeholder="••••••••"
|
|
style={{ width: '100%', height: 38, padding: '0 36px 0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, boxSizing: 'border-box' }}
|
|
/>
|
|
<button type="button" onClick={() => setShowMellatPassword(p => !p)}
|
|
style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--muted)', fontSize: 12 }}>
|
|
{showMellatPassword ? 'پنهان' : 'نمایش'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SEP */}
|
|
<div>
|
|
<div style={{ fontSize: 13.5, fontWeight: 600, marginBottom: '0.75rem', display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
<span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--success)', display: 'inline-block' }} />
|
|
درگاه سپ (SEP)
|
|
</div>
|
|
<div style={{ maxWidth: 240 }}>
|
|
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>شناسه پایانه</label>
|
|
<input {...register('sep_terminal_id')} dir="ltr" placeholder="12345678"
|
|
style={{ width: '100%', height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, boxSizing: 'border-box' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* تنظیمات پیامک */}
|
|
<div className="card card-pad">
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: '1.25rem' }}>
|
|
<div className="ico" style={{ background: 'var(--violet-bg)', color: 'var(--violet)', width: 36, height: 36, borderRadius: 10 }}>
|
|
<span style={{ fontSize: 16 }}>📱</span>
|
|
</div>
|
|
<h3 style={{ fontSize: 15 }}>تنظیمات پیامک (SMS)</h3>
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
|
<div>
|
|
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>سرویس پیامک</label>
|
|
<select {...register('sms_provider')}
|
|
style={{ width: '100%', height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13 }}>
|
|
<option value="kavenegar">کاوهنگار</option>
|
|
<option value="rangineh">رنگینه</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>قیمت هر پیامک (ریال)</label>
|
|
<input {...register('sms_price_rials')} type="number" min={0} dir="ltr" placeholder="500"
|
|
style={{ width: '100%', height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, boxSizing: 'border-box' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ marginTop: '1rem' }}>
|
|
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: '0.75rem' }}>تنظیمات کاوهنگار</div>
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem' }}>
|
|
<div>
|
|
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>API Key</label>
|
|
<div style={{ position: 'relative' }}>
|
|
<input {...register('kavenegar_api_key')} type={showKavenegarKey ? 'text' : 'password'} dir="ltr" placeholder="••••••••••••••••"
|
|
style={{ width: '100%', height: 38, padding: '0 36px 0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, boxSizing: 'border-box' }}
|
|
/>
|
|
<button type="button" onClick={() => setShowKavenegarKey(p => !p)}
|
|
style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--muted)', fontSize: 12 }}>
|
|
{showKavenegarKey ? 'پنهان' : 'نمایش'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>شماره فرستنده</label>
|
|
<input {...register('kavenegar_sender')} dir="ltr" placeholder="10008664"
|
|
style={{ width: '100%', height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, boxSizing: 'border-box' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* دکمه ذخیره */}
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
|
|
<button
|
|
type="button"
|
|
className="btn ghost"
|
|
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,
|
|
upgrade_commission_enabled: settings.upgrade_commission_enabled,
|
|
upgrade_commission_percent: settings.upgrade_commission_percent,
|
|
tax_enabled: settings.tax_enabled,
|
|
tax_percent: settings.tax_percent,
|
|
sms_panel_fee_rials: settings.sms_panel_fee_rials,
|
|
payment_test_mode: settings.payment_test_mode,
|
|
mellat_terminal_id: settings.mellat_terminal_id,
|
|
mellat_username: settings.mellat_username,
|
|
mellat_password: settings.mellat_password,
|
|
sep_terminal_id: settings.sep_terminal_id,
|
|
sms_provider: settings.sms_provider,
|
|
kavenegar_api_key: settings.kavenegar_api_key,
|
|
kavenegar_sender: settings.kavenegar_sender,
|
|
sms_price_rials: settings.sms_price_rials,
|
|
})}
|
|
disabled={!isDirty || mutation.isPending}
|
|
>
|
|
بازگشت
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="btn primary"
|
|
disabled={mutation.isPending || !isDirty}
|
|
>
|
|
{mutation.isPending ? 'در حال ذخیره...' : 'ذخیره تنظیمات'}
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|