From 8f738f6469e4bfc91bbcb5b5045bc83bcfe61aae Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Wed, 3 Jun 2026 01:11:18 +0330 Subject: [PATCH] =?UTF-8?q?feat(plans):=20Stage=204=20=E2=80=94=20full=20a?= =?UTF-8?q?dmin=20plan/feature=20editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The admin → Plans screen now edits EVERYTHING per plan (the backend already accepted it; only the UI was partial): - All limits (orders/day, tables, terminals, branches, menu categories, menu items, customers, report history, SMS, AI-3D) with an "unlimited (∞)" toggle. - Display names (fa/en), monthly price, sort order, billable-online, active on/off. - Per-plan feature checkboxes grouped by module, plus an "all features (*)" toggle (Enterprise). Sourced from the live feature catalog (/api/admin/features). - Plans listed in sort order (Free·Starter·Pro·Business·Enterprise). - i18n fa/en/ar. Admin tsc + build clean. --- web/admin/messages/ar.json | 24 ++- web/admin/messages/en.json | 24 ++- web/admin/messages/fa.json | 24 ++- .../src/components/admin/admin-screens.tsx | 201 +++++++++++++++--- web/admin/src/lib/api/admin-types.ts | 4 + 5 files changed, 241 insertions(+), 36 deletions(-) diff --git a/web/admin/messages/ar.json b/web/admin/messages/ar.json index 192b36f..9490911 100644 --- a/web/admin/messages/ar.json +++ b/web/admin/messages/ar.json @@ -1114,7 +1114,29 @@ "title": "خطط الاشتراك", "monthlyPrice": "السعر الشهري (تومان)", "maxOrders": "حد الطلبات اليومي", - "saved": "تم الحفظ" + "saved": "تم الحفظ", + "active": "مفعل", + "nameFa": "الاسم (فارسي)", + "nameEn": "الاسم (إنجليزي)", + "sortOrder": "الترتيب", + "billable": "قابل للدفع عبر الإنترنت", + "limitsTitle": "الحدود", + "featuresTitle": "الميزات", + "allFeatures": "كل الميزات", + "allFeaturesNote": "تشمل هذه الباقة جميع الميزات الحالية والمستقبلية.", + "save": "حفظ", + "limits": { + "maxOrders": "طلبات/يوم", + "maxTables": "الطاولات", + "maxTerminals": "أجهزة POS", + "maxBranches": "الفروع", + "maxCategories": "فئات القائمة", + "maxItems": "أصناف القائمة", + "maxCustomers": "العملاء", + "maxReportDays": "سجل التقارير (أيام)", + "maxSms": "رسائل/شهر", + "maxAi3d": "3D/شهر" + } }, "settings": { "title": "إعدادات التطبيق", diff --git a/web/admin/messages/en.json b/web/admin/messages/en.json index e8b8069..578d82b 100644 --- a/web/admin/messages/en.json +++ b/web/admin/messages/en.json @@ -1107,7 +1107,29 @@ "title": "Subscription plans", "monthlyPrice": "Monthly price (Toman)", "maxOrders": "Max orders per day", - "saved": "Plan saved" + "saved": "Plan saved", + "active": "Active", + "nameFa": "Name (Persian)", + "nameEn": "Name (English)", + "sortOrder": "Sort order", + "billable": "Billable online", + "limitsTitle": "Limits", + "featuresTitle": "Features", + "allFeatures": "All features", + "allFeaturesNote": "This plan includes all features (current and future).", + "save": "Save", + "limits": { + "maxOrders": "Orders/day", + "maxTables": "Tables", + "maxTerminals": "POS terminals", + "maxBranches": "Branches", + "maxCategories": "Menu categories", + "maxItems": "Menu items", + "maxCustomers": "Customers", + "maxReportDays": "Report history (days)", + "maxSms": "SMS/month", + "maxAi3d": "AI 3D/month" + } }, "settings": { "title": "Application settings", diff --git a/web/admin/messages/fa.json b/web/admin/messages/fa.json index f161721..c62bb8b 100644 --- a/web/admin/messages/fa.json +++ b/web/admin/messages/fa.json @@ -1107,7 +1107,29 @@ "title": "پلن‌ها و قیمت‌گذاری", "monthlyPrice": "قیمت ماهانه (تومان)", "maxOrders": "سقف سفارش روزانه", - "saved": "پلن ذخیره شد" + "saved": "پلن ذخیره شد", + "active": "فعال", + "nameFa": "نام (فارسی)", + "nameEn": "نام (انگلیسی)", + "sortOrder": "ترتیب", + "billable": "قابل پرداخت آنلاین", + "limitsTitle": "محدودیت‌ها", + "featuresTitle": "امکانات", + "allFeatures": "همه امکانات", + "allFeaturesNote": "این پلن به همه امکانات (فعلی و آینده) دسترسی دارد.", + "save": "ذخیره", + "limits": { + "maxOrders": "سفارش روزانه", + "maxTables": "میزها", + "maxTerminals": "پایانه POS", + "maxBranches": "شعب", + "maxCategories": "دسته منو", + "maxItems": "آیتم منو", + "maxCustomers": "مشتریان", + "maxReportDays": "تاریخچه گزارش (روز)", + "maxSms": "پیامک ماهانه", + "maxAi3d": "تولید ۳D ماهانه" + } }, "settings": { "title": "تنظیمات اپلیکیشن", diff --git a/web/admin/src/components/admin/admin-screens.tsx b/web/admin/src/components/admin/admin-screens.tsx index 0e91226..3c9883f 100644 --- a/web/admin/src/components/admin/admin-screens.tsx +++ b/web/admin/src/components/admin/admin-screens.tsx @@ -18,6 +18,7 @@ import type { AdminNotificationRow, AdminPlan, AdminStats, + PlanLimitsData, GatewayCredentials, PaymentGatewayConfig, PlatformFeature, @@ -131,45 +132,167 @@ function StatCard({ label, value }: { label: string; value: number }) { ); } -function PlanCard({ plan, onSave }: { plan: AdminPlan; onSave: (p: AdminPlan) => void }) { +const PLAN_UNLIMITED = 2147483647; + +const LIMIT_FIELDS: { key: keyof PlanLimitsData; label: string }[] = [ + { key: "maxOrdersPerDay", label: "maxOrders" }, + { key: "maxTables", label: "maxTables" }, + { key: "maxTerminals", label: "maxTerminals" }, + { key: "maxBranches", label: "maxBranches" }, + { key: "maxMenuCategories", label: "maxCategories" }, + { key: "maxMenuItems", label: "maxItems" }, + { key: "maxCustomers", label: "maxCustomers" }, + { key: "maxReportHistoryDays", label: "maxReportDays" }, + { key: "maxSmsPerMonth", label: "maxSms" }, + { key: "maxMenuAi3dPerMonth", label: "maxAi3d" }, +]; + +function LimitField({ label, value, onChange }: { label: string; value: number; onChange: (n: number) => void }) { + const unlimited = value >= PLAN_UNLIMITED; + return ( +
+
+ {label} + +
+ onChange(Math.max(0, Number(e.target.value)))} + /> +
+ ); +} + +function PlanCard({ + plan, + features, + onSave, + saving, +}: { + plan: AdminPlan; + features: PlatformFeature[]; + onSave: (p: AdminPlan) => void; + saving: boolean; +}) { const t = useTranslations("admin.plans"); - const [price, setPrice] = useState(plan.monthlyPriceToman); - const [maxOrders, setMaxOrders] = useState(plan.limits.maxOrdersPerDay); + const [draft, setDraft] = useState(plan); + // Re-sync from server after a save/refetch. + useEffect(() => { setDraft(plan); }, [plan]); - // Sync server values if they change (e.g. after successful save + refetch) - useEffect(() => { setPrice(plan.monthlyPriceToman); }, [plan.monthlyPriceToman]); - useEffect(() => { setMaxOrders(plan.limits.maxOrdersPerDay); }, [plan.limits.maxOrdersPerDay]); + const setField = (k: K, v: AdminPlan[K]) => + setDraft((d) => ({ ...d, [k]: v })); + const setLimit = (k: keyof PlanLimitsData, v: number) => + setDraft((d) => ({ ...d, limits: { ...d.limits, [k]: v } })); - const flush = () => - onSave({ ...plan, monthlyPriceToman: price, limits: { ...plan.limits, maxOrdersPerDay: maxOrders } }); + const wildcard = draft.featureKeys.includes("*"); + const toggleFeature = (key: string, on: boolean) => + setDraft((d) => { + const set = new Set(d.featureKeys.filter((k) => k !== "*")); + if (on) set.add(key); + else set.delete(key); + return { ...d, featureKeys: Array.from(set) }; + }); + + const groups = Array.from(new Set(features.map((f) => f.moduleGroup))); return ( - - {plan.displayNameFa} -

{plan.tier}

+ +
+ {draft.displayNameFa || draft.tier} +

{draft.tier}

+
+
- - -
); @@ -182,6 +305,10 @@ export function AdminPlansScreen() { queryKey: ["admin", "plans"], queryFn: () => adminGet("/api/admin/plans"), }); + const { data: features = [] } = useQuery({ + queryKey: ["admin", "features"], + queryFn: () => adminGet("/api/admin/features"), + }); const save = useMutation({ mutationFn: (plan: AdminPlan) => @@ -201,11 +328,19 @@ export function AdminPlansScreen() { }, }); + const ordered = [...plans].sort((a, b) => a.sortOrder - b.sortOrder); + return (

{t("title")}

- {plans.map((plan) => ( - save.mutate(p)} /> + {ordered.map((plan) => ( + save.mutate(p)} + saving={save.isPending} + /> ))}
); diff --git a/web/admin/src/lib/api/admin-types.ts b/web/admin/src/lib/api/admin-types.ts index b83250a..56f6aa0 100644 --- a/web/admin/src/lib/api/admin-types.ts +++ b/web/admin/src/lib/api/admin-types.ts @@ -8,11 +8,15 @@ export type AdminStats = { export type PlanLimitsData = { maxOrdersPerDay: number; + maxTables: number; maxTerminals: number; maxCustomers: number; maxSmsPerMonth: number; maxBranches: number; maxReportHistoryDays: number; + maxMenuCategories: number; + maxMenuItems: number; + maxMenuAi3dPerMonth: number; }; export type AdminPlan = {