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>;
|
type PlanForm = z.infer<typeof planSchema>;
|
||||||
|
|
||||||
const periodSchema = z.object({
|
const periodSchema = z.object({
|
||||||
plan_uuid: z.string().min(1, 'پنل الزامی است'),
|
plan_uuid: z.string().min(1, 'پلن الزامی است'),
|
||||||
label: z.string().min(1, 'عنوان دوره الزامی است'),
|
label: z.string().min(1, 'عنوان دوره الزامی است'),
|
||||||
duration_months: z.coerce.number().min(1),
|
duration_months: z.coerce.number().min(1),
|
||||||
price_rials: z.coerce.number().min(0),
|
price_rials: z.coerce.number().min(0),
|
||||||
@@ -70,6 +70,19 @@ const PLAN_DISPLAY: Record<string, string> = {
|
|||||||
professional: 'حرفهای',
|
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 ─────────────────────────────────────────────────────────────
|
// ── Plans tab ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function PlansTab() {
|
function PlansTab() {
|
||||||
@@ -98,13 +111,13 @@ function PlansTab() {
|
|||||||
|
|
||||||
const createPlanMut = useMutation({
|
const createPlanMut = useMutation({
|
||||||
mutationFn: (body: PlanForm) => api.post('/api/v1/admin/subscription/plan', body),
|
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),
|
onError: (e: any) => toast.error(e.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatePlanMut = useMutation({
|
const updatePlanMut = useMutation({
|
||||||
mutationFn: ({ uuid, body }: { uuid: string; body: PlanForm }) => api.patch(`/api/v1/admin/subscription/plan/${uuid}`, body),
|
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),
|
onError: (e: any) => toast.error(e.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,7 +173,7 @@ function PlansTab() {
|
|||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
|
<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'); }}>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -237,7 +250,7 @@ function PlansTab() {
|
|||||||
<Modal
|
<Modal
|
||||||
open={planModal !== null}
|
open={planModal !== null}
|
||||||
onClose={() => setPlanModal(null)}
|
onClose={() => setPlanModal(null)}
|
||||||
title={planModal === 'create' ? 'پنل جدید' : 'ویرایش پنل'}
|
title={planModal === 'create' ? 'پلن جدید' : 'ویرایش پلن'}
|
||||||
>
|
>
|
||||||
<form onSubmit={planForm.handleSubmit((d) => {
|
<form onSubmit={planForm.handleSubmit((d) => {
|
||||||
if (planModal === 'create') createPlanMut.mutate(d);
|
if (planModal === 'create') createPlanMut.mutate(d);
|
||||||
@@ -259,20 +272,20 @@ function PlansTab() {
|
|||||||
<input {...planForm.register('max_secretaries')} type="number" min={1} dir="ltr" />
|
<input {...planForm.register('max_secretaries')} type="number" min={1} dir="ltr" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div>
|
||||||
<label>قابلیتها</label>
|
<span className="field-label">قابلیتهای پلن</span>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', border: '1px solid var(--border)', borderRadius: 'var(--r-sm)', background: 'var(--surface)', overflow: 'hidden' }}>
|
||||||
{(Object.keys(FEATURE_LABELS) as (keyof typeof FEATURE_LABELS)[]).map((k) => (
|
{FEATURE_KEYS.map((k, i) => (
|
||||||
<label key={k} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13.5, cursor: 'pointer' }}>
|
<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' }}>
|
||||||
<input type="checkbox" {...planForm.register(`features.${k}` as any)} style={{ accentColor: 'var(--primary)' }} />
|
<span style={{ color: 'var(--text)' }}>{FEATURE_LABELS[k]}</span>
|
||||||
{FEATURE_LABELS[k]}
|
<SwitchToggle {...planForm.register(`features.${k}` as any)} />
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13.5, cursor: 'pointer' }}>
|
<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)' }}>
|
||||||
<input type="checkbox" {...planForm.register('active')} style={{ accentColor: 'var(--primary)' }} />
|
<span style={{ color: 'var(--text)', fontWeight: 600 }}>پلن فعال است</span>
|
||||||
فعال
|
<SwitchToggle {...planForm.register('active')} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
|
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
|
||||||
@@ -373,7 +386,7 @@ function ReportTab() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
<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>
|
||||||
<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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="مدیریت اشتراکها" description="تعریف پنلها، دورهها و گزارش فروش" />
|
<PageHeader title="مدیریت اشتراکها" description="تعریف پلنها، دورهها و گزارش فروش" />
|
||||||
|
|
||||||
<div className="seg" style={{ marginBottom: 20 }}>
|
<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>
|
<button className={tab === 'report' ? 'on' : ''} onClick={() => setTab('report')}>گزارش فروش</button>
|
||||||
</div>
|
</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 { 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); }
|
.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 ──────────────────────────────────────────────── */
|
||||||
.pagination {
|
.pagination {
|
||||||
display: flex; align-items: center; gap: 6px; padding: 16px 0;
|
display: flex; align-items: center; gap: 6px; padding: 16px 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user