feat(subscription): add switch toggle component and update plan terminology

This commit is contained in:
hamed
2026-06-24 03:32:50 +03:30
parent 15e7d92fc1
commit 253414ef3e
2 changed files with 47 additions and 18 deletions
+31 -18
View File
@@ -40,7 +40,7 @@ const planSchema = z.object({
type PlanForm = z.infer<typeof planSchema>;
const periodSchema = z.object({
plan_uuid: z.string().min(1, نل الزامی است'),
plan_uuid: z.string().min(1, 'پلن الزامی است'),
label: z.string().min(1, 'عنوان دوره الزامی است'),
duration_months: z.coerce.number().min(1),
price_rials: z.coerce.number().min(0),
@@ -70,6 +70,19 @@ const PLAN_DISPLAY: Record<string, string> = {
professional: 'حرفه‌ای',
};
// ── Switch toggle ─────────────────────────────────────────────────────────
const SwitchToggle = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
function SwitchToggle(props, ref) {
return (
<span className="switch" onClick={(e) => e.stopPropagation()}>
<input type="checkbox" ref={ref} {...props} />
<span className="switch-track"><span className="switch-thumb" /></span>
</span>
);
},
);
// ── Plans tab ─────────────────────────────────────────────────────────────
function PlansTab() {
@@ -98,13 +111,13 @@ function PlansTab() {
const createPlanMut = useMutation({
mutationFn: (body: PlanForm) => api.post('/api/v1/admin/subscription/plan', body),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['admin-subscription-plans'] }); setPlanModal(null); planForm.reset(); toast.success(نل ایجاد شد'); },
onSuccess: () => { qc.invalidateQueries({ queryKey: ['admin-subscription-plans'] }); setPlanModal(null); planForm.reset(); toast.success('پلن ایجاد شد'); },
onError: (e: any) => toast.error(e.message),
});
const updatePlanMut = useMutation({
mutationFn: ({ uuid, body }: { uuid: string; body: PlanForm }) => api.patch(`/api/v1/admin/subscription/plan/${uuid}`, body),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['admin-subscription-plans'] }); setPlanModal(null); toast.success(نل بروزرسانی شد'); },
onSuccess: () => { qc.invalidateQueries({ queryKey: ['admin-subscription-plans'] }); setPlanModal(null); toast.success('پلن بروزرسانی شد'); },
onError: (e: any) => toast.error(e.message),
});
@@ -160,7 +173,7 @@ function PlansTab() {
<>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
<button className="btn primary sm" onClick={() => { planForm.reset({ features: emptyFeatures(), active: true, level: 0, max_secretaries: 1 }); setPlanModal('create'); }}>
<PlusIcon style={{ width: 16 }} /> پنل جدید
<PlusIcon style={{ width: 16 }} /> پلن جدید
</button>
</div>
@@ -237,7 +250,7 @@ function PlansTab() {
<Modal
open={planModal !== null}
onClose={() => setPlanModal(null)}
title={planModal === 'create' ? نل جدید' : 'ویرایش پنل'}
title={planModal === 'create' ? 'پلن جدید' : 'ویرایش پلن'}
>
<form onSubmit={planForm.handleSubmit((d) => {
if (planModal === 'create') createPlanMut.mutate(d);
@@ -259,20 +272,20 @@ function PlansTab() {
<input {...planForm.register('max_secretaries')} type="number" min={1} dir="ltr" />
</div>
</div>
<div className="field">
<label>قابلیتها</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 }}>
{(Object.keys(FEATURE_LABELS) as (keyof typeof FEATURE_LABELS)[]).map((k) => (
<label key={k} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13.5, cursor: 'pointer' }}>
<input type="checkbox" {...planForm.register(`features.${k}` as any)} style={{ accentColor: 'var(--primary)' }} />
{FEATURE_LABELS[k]}
<div>
<span className="field-label">قابلیتهای پلن</span>
<div style={{ display: 'flex', flexDirection: 'column', border: '1px solid var(--border)', borderRadius: 'var(--r-sm)', background: 'var(--surface)', overflow: 'hidden' }}>
{FEATURE_KEYS.map((k, i) => (
<label key={k} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, fontSize: 13.5, cursor: 'pointer', padding: '11px 14px', borderTop: i ? '1px solid var(--border)' : 'none' }}>
<span style={{ color: 'var(--text)' }}>{FEATURE_LABELS[k]}</span>
<SwitchToggle {...planForm.register(`features.${k}` as any)} />
</label>
))}
</div>
</div>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13.5, cursor: 'pointer' }}>
<input type="checkbox" {...planForm.register('active')} style={{ accentColor: 'var(--primary)' }} />
فعال
<label style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, fontSize: 13.5, cursor: 'pointer', padding: '11px 14px', border: '1px solid var(--border)', borderRadius: 'var(--r-sm)', background: 'var(--surface)' }}>
<span style={{ color: 'var(--text)', fontWeight: 600 }}>پلن فعال است</span>
<SwitchToggle {...planForm.register('active')} />
</label>
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
@@ -373,7 +386,7 @@ function ReportTab() {
<thead>
<tr style={{ borderBottom: '1px solid var(--border)' }}>
<th style={{ textAlign: 'right', padding: '10px 16px', color: 'var(--text-3)', fontWeight: 500 }}>نوع</th>
<th style={{ textAlign: 'right', padding: '10px 16px', color: 'var(--text-3)', fontWeight: 500 }}>پنل</th>
<th style={{ textAlign: 'right', padding: '10px 16px', color: 'var(--text-3)', fontWeight: 500 }}>پلن</th>
<th style={{ textAlign: 'right', padding: '10px 16px', color: 'var(--text-3)', fontWeight: 500 }}>نوع اشتراک</th>
<th style={{ textAlign: 'right', padding: '10px 16px', color: 'var(--text-3)', fontWeight: 500 }}>شروع</th>
<th style={{ textAlign: 'right', padding: '10px 16px', color: 'var(--text-3)', fontWeight: 500 }}>انقضا</th>
@@ -420,10 +433,10 @@ export default function AdminSubscriptionPage() {
return (
<>
<PageHeader title="مدیریت اشتراک‌ها" description="تعریف پنل‌ها، دوره‌ها و گزارش فروش" />
<PageHeader title="مدیریت اشتراک‌ها" description="تعریف پلن‌ها، دوره‌ها و گزارش فروش" />
<div className="seg" style={{ marginBottom: 20 }}>
<button className={tab === 'plans' ? 'on' : ''} onClick={() => setTab('plans')}>پنلها و دورهها</button>
<button className={tab === 'plans' ? 'on' : ''} onClick={() => setTab('plans')}>پلنها و دورهها</button>
<button className={tab === 'report' ? 'on' : ''} onClick={() => setTab('report')}>گزارش فروش</button>
</div>
+16
View File
@@ -533,6 +533,22 @@ table.t tbody tr:hover .row-actions { opacity: 1; }
.seg button { padding: 7px 14px; border-radius: 8px; font-size: 13px; font-weight: 600; color: var(--text-2); transition: .14s; cursor: pointer; font-family: inherit; }
.seg button.on { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); }
/* ── Switch toggle ───────────────────────────────────────────── */
.switch { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; }
.switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; z-index: 2; }
.switch-track {
position: absolute; inset: 0; display: block;
background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px; transition: background .18s, border-color .18s;
}
.switch-thumb {
position: absolute; top: 50%; right: 3px; width: 16px; height: 16px; margin-top: -8px;
border-radius: 50%; background: var(--text-3); box-shadow: var(--shadow-sm);
transition: transform .18s, background .18s;
}
.switch input:checked ~ .switch-track { background: var(--primary); border-color: var(--primary); }
.switch input:checked ~ .switch-track .switch-thumb { background: #fff; transform: translateX(-20px); }
.switch input:focus-visible ~ .switch-track { box-shadow: 0 0 0 4px var(--ring); }
/* ── Pagination ──────────────────────────────────────────────── */
.pagination {
display: flex; align-items: center; gap: 6px; padding: 16px 0;