From 6d594822bf3c0fa761c582b83de301eb58791ca5 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Thu, 2 Jul 2026 10:27:41 +0330 Subject: [PATCH] Update SettingsPage.tsx metadata in manifest.json with new mtime and ast_hash --- assets/admin/pages/SettingsPage.tsx | 780 ++++++++++++------------- assets/admin/styles.css | 95 +++ graphify-out/.graphify_labels.json | 17 +- graphify-out/GRAPH_REPORT.md | 253 +++++--- graphify-out/graph.json | 869 ++++++++++++++++------------ graphify-out/manifest.json | 4 +- 6 files changed, 1149 insertions(+), 869 deletions(-) diff --git a/assets/admin/pages/SettingsPage.tsx b/assets/admin/pages/SettingsPage.tsx index 74bfed08..059320f6 100644 --- a/assets/admin/pages/SettingsPage.tsx +++ b/assets/admin/pages/SettingsPage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -6,7 +6,10 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import { formatDateTime } from '../lib/utils'; -import { Cog6ToothIcon, CheckCircleIcon } from '@heroicons/react/24/outline'; +import { + Cog6ToothIcon, ClockIcon, CalculatorIcon, CreditCardIcon, ChatBubbleLeftRightIcon, + MagnifyingGlassIcon, CheckCircleIcon, ExclamationTriangleIcon, EyeIcon, EyeSlashIcon, +} from '@heroicons/react/24/outline'; interface TaxHistoryRow { tax_percent: number; @@ -42,40 +45,107 @@ const schema = z.object({ type FormValues = z.infer; -interface Settings { - site_name: string; - support_phone: 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; - appointment_fee_rials: string; - payment_test_mode: string; - mellat_enabled: string; - mellat_terminal_id: string; - mellat_username: string; - mellat_password: string; - sep_enabled: string; - sep_terminal_id: string; +interface Settings extends FormValues { sms_api_key_configured: boolean; } +const toForm = (s: Partial): FormValues => ({ + site_name: s.site_name ?? 'ClinicPro', + support_phone: s.support_phone ?? '', + max_cancel_hours_before: s.max_cancel_hours_before ?? '24', + appointment_reminder_hours: s.appointment_reminder_hours ?? '2', + appointment_commission_enabled: s.appointment_commission_enabled ?? '0', + upgrade_commission_enabled: s.upgrade_commission_enabled ?? '0', + upgrade_commission_percent: s.upgrade_commission_percent ?? '20', + tax_enabled: s.tax_enabled ?? '0', + tax_percent: s.tax_percent ?? '10', + sms_panel_fee_rials: s.sms_panel_fee_rials ?? '1500000', + appointment_fee_rials: s.appointment_fee_rials ?? '150000', + payment_test_mode: s.payment_test_mode ?? '0', + mellat_enabled: s.mellat_enabled ?? '1', + mellat_terminal_id: s.mellat_terminal_id ?? '', + mellat_username: s.mellat_username ?? '', + mellat_password: s.mellat_password ?? '', + sep_enabled: s.sep_enabled ?? '1', + sep_terminal_id: s.sep_terminal_id ?? '', +}); + +// ── Section definitions (drive the nav rail + search) ─────────────────────── + +type SectionId = 'general' | 'appointments' | 'financial' | 'payment' | 'sms'; + +interface SectionDef { + id: SectionId; + label: string; + desc: string; + Icon: typeof Cog6ToothIcon; + bg: string; fg: string; + keywords: string; +} + +const SECTIONS: SectionDef[] = [ + { id: 'general', label: 'عمومی', desc: 'اطلاعات پایه پلتفرم', Icon: Cog6ToothIcon, bg: 'var(--primary-soft)', fg: 'var(--primary)', keywords: 'نام سایت پشتیبانی شماره تلفن brand' }, + { id: 'appointments', label: 'نوبت‌دهی', desc: 'قوانین لغو و یادآوری', Icon: ClockIcon, bg: 'var(--warning-bg)', fg: 'var(--warning)', keywords: 'نوبت لغو یادآور reminder cancel ساعت' }, + { id: 'financial', label: 'مالی', desc: 'پورسانت، مالیات و کارمزدها', Icon: CalculatorIcon, bg: 'var(--info-bg)', fg: 'var(--info)', keywords: 'پورسانت مالیات کارمزد پیامک نوبت مبلغ ریال tax commission' }, + { id: 'payment', label: 'درگاه پرداخت', desc: 'ملت، سپ و حالت تست', Icon: CreditCardIcon, bg: 'var(--success-bg)', fg: 'var(--success)', keywords: 'درگاه پرداخت ملت سپ mellat sep terminal تست gateway' }, + { id: 'sms', label: 'پیامک', desc: 'پیکربندی سرویس پیامک', Icon: ChatBubbleLeftRightIcon, bg: 'var(--violet-bg)', fg: 'var(--violet)', keywords: 'پیامک sms کاوه‌نگار kavenegar api' }, +]; + +// ── Small presentational helpers ──────────────────────────────────────────── + +function SectionHead({ s }: { s: SectionDef }) { + return ( +
+
+
+

{s.label}

+

{s.desc}

+
+
+ ); +} + +function Toggle({ checked, onChange, label }: { checked: boolean; onChange: () => void; label: string }) { + return ( + + ); +} + +function Field({ label, hint, required, optional, error, children, span2 }: { + label: string; hint?: string; required?: boolean; optional?: boolean; + error?: string; children: React.ReactNode; span2?: boolean; +}) { + return ( +
+ + {children} + {hint &&

{hint}

} + {error &&

{error}

} +
+ ); +} + // ── Component ───────────────────────────────────────────────────────────── export default function SettingsPage() { const qc = useQueryClient(); const [showMellatPassword, setShowMellatPassword] = useState(false); + const [active, setActive] = useState('general'); + const [search, setSearch] = useState(''); + const [savedFlash, setSavedFlash] = useState(false); const { data, isLoading } = useQuery({ queryKey: ['admin-settings'], queryFn: () => api.get>('/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; @@ -88,462 +158,332 @@ export default function SettingsPage() { const taxHistory: TaxHistoryRow[] = (taxHistoryQ.data?.data as any)?.data ?? taxHistoryQ.data?.data ?? []; const { - register, - handleSubmit, - reset, - watch, - setValue, + register, handleSubmit, reset, watch, setValue, formState: { errors, isDirty }, } = useForm({ resolver: zodResolver(schema) }); - useEffect(() => { - if (settings) { - reset({ - site_name: settings.site_name ?? 'ClinicPro', - support_phone: settings.support_phone ?? '', - 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', - appointment_fee_rials: settings.appointment_fee_rials ?? '150000', - payment_test_mode: settings.payment_test_mode ?? '0', - mellat_enabled: settings.mellat_enabled ?? '1', - mellat_terminal_id: settings.mellat_terminal_id ?? '', - mellat_username: settings.mellat_username ?? '', - mellat_password: settings.mellat_password ?? '', - sep_enabled: settings.sep_enabled ?? '1', - sep_terminal_id: settings.sep_terminal_id ?? '', - }); - } - }, [settings, reset]); + useEffect(() => { if (settings) reset(toForm(settings)); }, [settings, reset]); const mutation = useMutation({ - mutationFn: (values: FormValues) => - api.patch>('/api/v1/admin/settings', values), - onSuccess: () => { + mutationFn: (values: FormValues) => api.patch>('/api/v1/admin/settings', values), + onSuccess: (res) => { qc.invalidateQueries({ queryKey: ['admin-settings'] }); qc.invalidateQueries({ queryKey: ['tax-history'] }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fresh: Settings | undefined = (res?.data as any)?.data ?? res?.data; + if (fresh) reset(toForm(fresh)); + setSavedFlash(true); }, }); - const paymentTestMode = watch('payment_test_mode') === '1'; - const mellatEnabled = watch('mellat_enabled') === '1'; - const sepEnabled = watch('sep_enabled') === '1'; - const apptCommissionEnabled = watch('appointment_commission_enabled') === '1'; - const upgradeCommissionEnabled = watch('upgrade_commission_enabled') === '1'; - const taxEnabled = watch('tax_enabled') === '1'; + // auto-hide the "saved" flash + useEffect(() => { + if (!savedFlash) return; + const t = setTimeout(() => setSavedFlash(false), 2600); + return () => clearTimeout(t); + }, [savedFlash]); - const onSubmit = (values: FormValues) => { - mutation.mutate(values); - }; + const onSubmit = (values: FormValues) => mutation.mutate(values); + + // Ctrl/Cmd+S saves + useEffect(() => { + const h = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 's') { + e.preventDefault(); + if (isDirty && !mutation.isPending) handleSubmit(onSubmit)(); + } + }; + window.addEventListener('keydown', h); + return () => window.removeEventListener('keydown', h); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDirty, mutation.isPending]); + + const paymentTestMode = watch('payment_test_mode') === '1'; + const mellatEnabled = watch('mellat_enabled') === '1'; + const sepEnabled = watch('sep_enabled') === '1'; + const apptCommissionEnabled = watch('appointment_commission_enabled') === '1'; + const upgradeCommissionEnabled = watch('upgrade_commission_enabled') === '1'; + const taxEnabled = watch('tax_enabled') === '1'; + + const toggle = (name: keyof FormValues, current: boolean) => + setValue(name, current ? '0' : '1', { shouldDirty: true }); + + // search filters the nav rail + const q = search.trim(); + const filtered = useMemo( + () => q === '' ? SECTIONS : SECTIONS.filter(s => (s.label + ' ' + s.desc + ' ' + s.keywords).includes(q)), + [q], + ); + const current = filtered.find(s => s.id === active) ?? filtered[0] ?? SECTIONS[0]; if (isLoading) { return ( -
-
- {Array.from({ length: 3 }).map((_, i) => ( -
-
-
- ))} -
+
+
+
); } return (
-
-
-

تنظیمات سایت

-
پیکربندی کلی پلتفرم
-
- {mutation.isSuccess && ( -
- - تنظیمات ذخیره شد -
- )} +
+

تنظیمات

+
پیکربندی کلی پلتفرم — تغییرات پس از ذخیره اعمال می‌شوند
-
+
- {/* اطلاعات پایه */} + {/* ── نوار ناوبری کناری ── */} + + + {/* ── محتوای بخش فعال ── */}
-
-
-
- + + + {/* عمومی */} + {current.id === 'general' && ( +
+ + + + + + +
+ )} + + {/* نوبت‌دهی */} + {current.id === 'appointments' && ( +
+ +
+ + ساعت قبل +
+
+ +
+ + ساعت قبل +
+
+
+ )} + + {/* مالی */} + {current.id === 'financial' && ( + <> +
+ ترتیب کسرها: ابتدا هزینه پنل پیامک، سپس مالیات بر ارزش افزوده (استخراجی از مبلغ شامل مالیات)، و در نهایت پورسانت نماینده از مبلغِ خالصِ پس از مالیات.
-

اطلاعات پایه

-
-
-
-
- - - {errors.site_name &&

{errors.site_name.message}

} -
- -
- - -
-
-
- - {/* موتور مالی نمایندگی */} -
-
-
- 🧮 -
-

موتور مالی نمایندگی

-
- -

- ترتیب کسرها: ابتدا هزینه پنل پیامک، سپس مالیات بر ارزش افزوده (استخراجی از مبلغ شامل مالیات)، - و در نهایت پورسانت نماینده از مبلغِ خالصِ پس از مالیات محاسبه می‌شود. -

- -
- - {/* پورسانت نوبت */} -