feat(subscription): add switch toggle component and update plan terminology
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user