From 5029edaaebaa260910108c9abb8cfe0e7fbbac5a Mon Sep 17 00:00:00 2001 From: parsa aghaei Date: Sun, 23 Aug 2026 10:32:20 +0330 Subject: [PATCH] feat: complete url modal/tab deeplinking, standardized inputs, and sync dynamic menu --- backend/src/menu/menu.service.ts | 110 +++++++++++++++--- frontend/admin-panel/src/pages/Coupons.tsx | 42 ++++++- .../admin-panel/src/pages/MenuManager.tsx | 47 +++++++- frontend/admin-panel/src/pages/Orders.tsx | 52 +++++++-- frontend/admin-panel/src/pages/Products.tsx | 39 ++++++- frontend/admin-panel/src/pages/Settings.tsx | 19 ++- .../admin-panel/src/pages/Transactions.tsx | 62 ++++++++-- frontend/admin-panel/src/pages/UITexts.tsx | 65 +++++++++-- frontend/application/components/Header.tsx | 88 ++++++++++++-- 9 files changed, 454 insertions(+), 70 deletions(-) diff --git a/backend/src/menu/menu.service.ts b/backend/src/menu/menu.service.ts index 9a936fe..848b834 100644 --- a/backend/src/menu/menu.service.ts +++ b/backend/src/menu/menu.service.ts @@ -30,14 +30,62 @@ export class MenuService implements OnModuleInit { return; } - // Seed with current site navigation - const headerItems = [ - { label: 'راهکارهای درمانی', href: '/shop?view=symptoms', order: 1 }, - { label: 'محصولات تخصصی', href: '/shop', order: 2 }, - { label: 'دانشنامه علمی', href: '/wiki', order: 3 }, - { label: 'مجله سلامت پت', href: '/blog', order: 4 }, - { label: 'شناسنامه پت‌ها', href: '/dashboard/pets', order: 5 }, - { label: 'ویدیوها', href: '/videos', order: 6 }, + // Seed with comprehensive hierarchical site navigation matching application + const headerGroups = [ + { + label: 'دسته‌بندی درمانی محصولات', + href: '/shop?view=symptoms', + order: 1, + icon: 'Pill', + children: [ + { label: 'مفاصل و استخوان', href: '/shop?category=joints', order: 1 }, + { label: 'تقویت سیستم ایمنی و گوارش', href: '/shop?category=immune', order: 2 }, + { label: 'ویتامین‌ها و انرژی‌بخش‌ها', href: '/shop?category=energy', order: 3 }, + { label: 'مراقبت‌های ویژه (پوست، دندان و چشم)', href: '/shop?category=special-care', order: 4 }, + ], + }, + { + label: 'فروشگاه و محصولات', + href: '/shop', + order: 2, + icon: 'ShoppingBag', + children: [ + { label: 'فروشگاه تخصصی محصولات کنینا', href: '/shop', order: 1 }, + { label: 'کاتالوگ دیجیتال و راهنمای بالینی', href: '/catalog', order: 2 }, + ], + }, + { + label: 'دانشنامه و آکادمی', + href: '/wiki', + order: 3, + icon: 'BookOpen', + children: [ + { label: 'دانشنامه علمی کنینا', href: '/wiki', order: 1 }, + { label: 'مجله سلامت پت (وبلاگ)', href: '/blog', order: 2 }, + { label: 'آکادمی ویدئویی و مشاوره دامپزشک', href: '/videos', order: 3 }, + ], + }, + { + label: 'ابزارهای هوشمند', + href: '/profile', + order: 4, + icon: 'Sparkles', + children: [ + { label: 'شناسنامه و سوابق سلامت پت', href: '/profile', order: 1 }, + { label: 'پایش هوشمند مصرف مکمل‌ها', href: '/dashboard', order: 2 }, + ], + }, + { + label: 'درباره کنینا و تماس', + href: '/about', + order: 5, + icon: 'Building2', + children: [ + { label: 'درباره کمپانی کنینا آلمان', href: '/about', order: 1 }, + { label: 'نمادهای اعتماد و مجوزهای رسمی', href: '/trust-seals', order: 2 }, + { label: 'تماس با مرکز پشتیبانی', href: '/contact', order: 3 }, + ], + }, ]; const footerCol1 = [ @@ -48,20 +96,48 @@ export class MenuService implements OnModuleInit { ]; const footerCol2 = [ - { label: 'فروشگاه تخصصی', href: '/shop', order: 1 }, - { label: 'دستهٔ‌بندی درمانی', href: '/shop?view=symptoms', order: 2 }, + { label: 'محصولات تخصصی', href: '/shop', order: 1 }, + { label: 'کاتالوگ دیجیتال', href: '/catalog', order: 2 }, { label: 'دانشنامه علمی', href: '/wiki', order: 3 }, - { label: 'مجله سلامت پت', href: '/blog', order: 4 }, + { label: 'مجله سلامت پت (وبلاگ)', href: '/blog', order: 4 }, ]; const footerCol3 = [ - { label: 'شناسنامه پت‌ها', href: '/dashboard/pets', order: 1 }, + { label: 'شناسنامه پت‌ها', href: '/profile', order: 1 }, { label: 'سفارشات من', href: '/dashboard/orders', order: 2 }, { label: 'ردیابی سفارش', href: '/track', order: 3 }, { label: 'ورود / ثبت‌نام', href: '/?login=1', order: 4 }, ]; - const createItems = (items: typeof headerItems, menuType: MenuType) => + for (const group of headerGroups) { + const parent = await this.prisma.menuItem.create({ + data: { + label: group.label, + href: group.href, + order: group.order, + icon: group.icon, + menuType: 'HEADER', + isActive: true, + isExternal: false, + }, + }); + + if (group.children && group.children.length > 0) { + await this.prisma.menuItem.createMany({ + data: group.children.map((child) => ({ + label: child.label, + href: child.href, + order: child.order, + parentId: parent.id, + menuType: 'HEADER', + isActive: true, + isExternal: false, + })), + }); + } + } + + const createFooterItems = (items: typeof footerCol1, menuType: MenuType) => items.map((item) => ({ ...item, menuType, @@ -71,14 +147,14 @@ export class MenuService implements OnModuleInit { await this.prisma.menuItem.createMany({ data: [ - ...createItems(headerItems, 'HEADER'), - ...createItems(footerCol1, 'FOOTER_COL1'), - ...createItems(footerCol2, 'FOOTER_COL2'), - ...createItems(footerCol3, 'FOOTER_COL3'), + ...createFooterItems(footerCol1, 'FOOTER_COL1'), + ...createFooterItems(footerCol2, 'FOOTER_COL2'), + ...createFooterItems(footerCol3, 'FOOTER_COL3'), ], }); } + async findByType(menuType: string) { const items = await this.prisma.menuItem.findMany({ where: { menuType, isActive: true, parentId: null }, diff --git a/frontend/admin-panel/src/pages/Coupons.tsx b/frontend/admin-panel/src/pages/Coupons.tsx index 72de30e..4bad5d0 100644 --- a/frontend/admin-panel/src/pages/Coupons.tsx +++ b/frontend/admin-panel/src/pages/Coupons.tsx @@ -43,11 +43,14 @@ export interface CouponFormData { targets: CouponTarget[]; } +import { useSearchParams } from 'react-router-dom'; + export default function Coupons() { + const [searchParams, setSearchParams] = useSearchParams(); const [coupons, setCoupons] = useState([]); const [isLoading, setIsLoading] = useState(true); - const [search, setSearch] = useState(''); - const [page, setPage] = useState(1); + const [search, setSearch] = useState(() => searchParams.get('search') || ''); + const [page, setPage] = useState(() => Number(searchParams.get('page')) || 1); const [totalPages, setTotalPages] = useState(1); const limit = 10; @@ -59,20 +62,40 @@ export default function Coupons() { targets: [] }); + const updateUrlParams = (paramsObj: Record) => { + const current = Object.fromEntries(searchParams.entries()); + const merged = { ...current, ...paramsObj }; + const cleaned: Record = {}; + Object.entries(merged).forEach(([k, v]) => { + if (v !== undefined && v !== null && String(v).trim() !== '') cleaned[k] = String(v); + }); + setSearchParams(cleaned, { replace: true }); + }; + const fetchData = useCallback(async () => { try { setIsLoading(true); const res = await api.get('/admin/coupons', { params: { page, limit, search } }); if (res.data?.data) { - setCoupons(res.data.data); + const list: Coupon[] = res.data.data; + setCoupons(list); setTotalPages(res.data.meta?.lastPage || 1); + + const modalParam = searchParams.get('modal'); + const couponId = searchParams.get('couponId'); + if (modalParam === 'create') { + openModal(null); + } else if (modalParam === 'edit' && couponId) { + const found = list.find((c) => c.id === couponId); + if (found) openModal(found); + } } } catch (err) { console.error(err); } finally { setIsLoading(false); } - }, [page, search]); + }, [page, search, searchParams]); useEffect(() => { const timer = setTimeout(() => fetchData(), 500); @@ -82,6 +105,7 @@ export default function Coupons() { const openModal = (coupon: Coupon | null = null) => { if (coupon) { setEditingCoupon(coupon); + updateUrlParams({ modal: 'edit', couponId: coupon.id }); setFormData({ code: coupon.code, type: coupon.type, @@ -95,6 +119,7 @@ export default function Coupons() { }); } else { setEditingCoupon(null); + updateUrlParams({ modal: 'create', couponId: undefined }); setFormData({ code: '', type: 'percent', value: 0, minCartValue: '', maxCartValue: '', maxUses: '', expiresAt: '', isActive: true, targets: [] }); @@ -102,6 +127,12 @@ export default function Coupons() { setIsModalOpen(true); }; + const closeModal = () => { + setIsModalOpen(false); + updateUrlParams({ modal: undefined, couponId: undefined }); + }; + + const handleSave = async (e: React.FormEvent) => { e.preventDefault(); try { @@ -267,11 +298,12 @@ export default function Coupons() { onCancel={() => setDeleteTargetId(null)} /> - {isModalOpen && setIsModalOpen(false)} isEditing={!!editingCoupon} />} + {isModalOpen && } ); } + interface CouponModalProps { formData: CouponFormData; setFormData: React.Dispatch>; diff --git a/frontend/admin-panel/src/pages/MenuManager.tsx b/frontend/admin-panel/src/pages/MenuManager.tsx index 4233bbe..1f1a9a0 100644 --- a/frontend/admin-panel/src/pages/MenuManager.tsx +++ b/frontend/admin-panel/src/pages/MenuManager.tsx @@ -35,8 +35,12 @@ const MENU_TABS: { key: MenuType; label: string; color: string }[] = [ { key: 'FOOTER_COL3', label: '🔗 فوتر — ستون ۳', color: 'violet' }, ]; +import { useSearchParams } from 'react-router-dom'; + export default function MenuManager() { - const [activeTab, setActiveTab] = useState('HEADER'); + const [searchParams, setSearchParams] = useSearchParams(); + const tabFromUrl = (searchParams.get('tab') as MenuType) || 'HEADER'; + const [activeTab, setActiveTabState] = useState(tabFromUrl); const [allItems, setAllItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); @@ -44,6 +48,21 @@ export default function MenuManager() { const [deleteTargetId, setDeleteTargetId] = useState(null); const [isSaving, setIsSaving] = useState(false); + const updateUrlState = (updates: Record) => { + const current = Object.fromEntries(searchParams.entries()); + const merged = { ...current, ...updates }; + const cleaned: Record = {}; + Object.entries(merged).forEach(([k, v]) => { + if (v !== null && v !== undefined && v !== '') cleaned[k] = v; + }); + setSearchParams(cleaned, { replace: true }); + }; + + const setActiveTab = (t: MenuType) => { + setActiveTabState(t); + updateUrlState({ tab: t === 'HEADER' ? null : t }); + }; + const [formData, setFormData] = useState({ menuType: 'HEADER' as MenuType, label: '', @@ -60,15 +79,24 @@ export default function MenuManager() { try { setIsLoading(true); const res = await api.get('/menu/admin'); - const data = Array.isArray(res.data) ? res.data : (res.data?.data || []); + const data: MenuItem[] = Array.isArray(res.data) ? res.data : (res.data?.data || []); setAllItems(data); + + const modalParam = searchParams.get('modal'); + const itemIdParam = searchParams.get('itemId'); + if (modalParam === 'create') { + openModal(null); + } else if (modalParam === 'edit' && itemIdParam) { + const found = data.find((it) => it.id === itemIdParam); + if (found) openModal(found); + } } catch (err) { console.error('Failed to fetch menu items:', err); toast.error('خطا در دریافت آیتم‌های منو'); } finally { setIsLoading(false); } - }, []); + }, [searchParams]); useEffect(() => { fetchAll(); @@ -81,6 +109,7 @@ export default function MenuManager() { const openModal = (item: MenuItem | null = null, parentId?: string) => { if (item) { setEditingItem(item); + updateUrlState({ modal: 'edit', itemId: item.id }); setFormData({ menuType: item.menuType as MenuType, label: item.label, @@ -94,6 +123,7 @@ export default function MenuManager() { }); } else { setEditingItem(null); + updateUrlState({ modal: 'create', itemId: null }); setFormData({ menuType: activeTab, label: '', @@ -109,6 +139,12 @@ export default function MenuManager() { setIsModalOpen(true); }; + const closeModal = () => { + setIsModalOpen(false); + updateUrlState({ modal: null, itemId: null }); + }; + + const handleSave = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.label.trim() || !formData.href.trim()) { @@ -311,7 +347,7 @@ export default function MenuManager() { {isModalOpen && ( setIsModalOpen(false)} + onClose={closeModal} title={editingItem ? 'ویرایش آیتم منو' : 'افزودن آیتم جدید'} icon={Menu} maxWidth="xl" @@ -321,7 +357,7 @@ export default function MenuManager() { variant="secondary" size="sm" type="button" - onClick={() => setIsModalOpen(false)} + onClick={closeModal} > انصراف @@ -337,6 +373,7 @@ export default function MenuManager() { } > +
} > +
{/* Status & Timing Banner */}
@@ -987,7 +1022,7 @@ export default function Orders() { {refundModalOrder && ( setRefundModalOrder(null)} + onClose={closeOrderRefundModal} title={`استرداد وجه سفارش #${refundModalOrder.trackingNumber || refundModalOrder.id.slice(0, 8)}`} icon={Undo2} maxWidth="lg" @@ -996,7 +1031,7 @@ export default function Orders() { @@ -1011,6 +1046,7 @@ export default function Orders() {
} > +
{/* Destination selector */}
diff --git a/frontend/admin-panel/src/pages/Products.tsx b/frontend/admin-panel/src/pages/Products.tsx index a92e3f7..04c570a 100644 --- a/frontend/admin-panel/src/pages/Products.tsx +++ b/frontend/admin-panel/src/pages/Products.tsx @@ -211,8 +211,24 @@ export default function Products() { } }); if (prodRes.data?.data) { - setProducts(prodRes.data.data); + const prodList = prodRes.data.data; + setProducts(prodList); setTotalPages(prodRes.data.meta?.lastPage || 1); + + // Auto-open modal if URL specifies modal + const modalParam = searchParams.get('modal'); + const prodIdParam = searchParams.get('productId'); + const tabParam = searchParams.get('tab'); + if (modalParam === 'create') { + openModal(null); + if (tabParam) setActiveTab(tabParam); + } else if (modalParam === 'edit' && prodIdParam) { + const found = prodList.find((p: Product) => p.id === prodIdParam); + if (found) { + openModal(found); + if (tabParam) setActiveTab(tabParam); + } + } } // Fetch categories separately @@ -232,7 +248,7 @@ export default function Products() { } finally { setIsLoading(false); } - }, [page, search, categoryFilter, suitableForFilter, sortBy, sortOrder]); + }, [page, search, categoryFilter, suitableForFilter, sortBy, sortOrder, searchParams]); useEffect(() => { const timer = setTimeout(() => { @@ -245,6 +261,7 @@ export default function Products() { setMediaImageError(false); if (product) { setEditingProduct(product); + updateUrlParams({ modal: 'edit', productId: product.id, tab: activeTab || 'general' }); const bPrice = product.buyPrice ? Number(product.buyPrice) : ''; const pPrice = Number(product.priceValue || 0); const wPrice = product.wholesalePrice ? Number(product.wholesalePrice) : ''; @@ -295,6 +312,7 @@ export default function Products() { }); } else { setEditingProduct(null); + updateUrlParams({ modal: 'create', productId: undefined, tab: activeTab || 'general' }); setFormData({ artNo: '', nameFa: '', nameEn: '', scientificTagline: '', description: '', shortDescription: '', categoryId: '', buyPrice: '', priceValue: 0, wholesalePrice: '', priceValueMarginPercent: '', wholesaleMarginPercent: '', @@ -307,10 +325,20 @@ export default function Products() { symptoms: [], isPreorder: false, preorderDeposit: '' }); } - setActiveTab('general'); setIsModalOpen(true); }; + const closeModal = () => { + setIsModalOpen(false); + updateUrlParams({ modal: undefined, productId: undefined, tab: undefined }); + }; + + const handleTabChange = (newTab: string) => { + setActiveTab(newTab); + updateUrlParams({ tab: newTab }); + }; + + const handleSave = async (e: React.FormEvent) => { e.preventDefault(); @@ -653,7 +681,7 @@ export default function Products() {
+
diff --git a/frontend/admin-panel/src/pages/Settings.tsx b/frontend/admin-panel/src/pages/Settings.tsx index 0b6bb22..873087e 100644 --- a/frontend/admin-panel/src/pages/Settings.tsx +++ b/frontend/admin-panel/src/pages/Settings.tsx @@ -24,8 +24,25 @@ import Button from '../components/ui/Button'; +import { useSearchParams } from 'react-router-dom'; + export default function Settings() { - const [activeTab, setActiveTab] = useState<'brand' | 'contact' | 'links'>('brand'); + const [searchParams, setSearchParams] = useSearchParams(); + const activeTabParam = (searchParams.get('tab') as 'brand' | 'contact' | 'links') || 'brand'; + const [activeTab, setActiveTabState] = useState<'brand' | 'contact' | 'links'>(activeTabParam); + + const setActiveTab = (tab: 'brand' | 'contact' | 'links') => { + setActiveTabState(tab); + setSearchParams(tab === 'brand' ? {} : { tab }, { replace: true }); + }; + + useEffect(() => { + const tabFromUrl = searchParams.get('tab') as 'brand' | 'contact' | 'links'; + if (tabFromUrl && tabFromUrl !== activeTab) { + setActiveTabState(tabFromUrl); + } + }, [searchParams]); + const [settings, setSettings] = useState({ BRAND_LOGO_URL: '', diff --git a/frontend/admin-panel/src/pages/Transactions.tsx b/frontend/admin-panel/src/pages/Transactions.tsx index ab09f50..14db359 100644 --- a/frontend/admin-panel/src/pages/Transactions.tsx +++ b/frontend/admin-panel/src/pages/Transactions.tsx @@ -123,7 +123,9 @@ export default function Transactions() { const [stats, setStats] = useState(null); const [health, setHealth] = useState(null); const [healthLoading, setHealthLoading] = useState(false); + const [isReconciling, setIsReconciling] = useState(false); const [isLoading, setIsLoading] = useState(true); + const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); @@ -146,6 +148,8 @@ export default function Transactions() { const [isSubmittingRefund, setIsSubmittingRefund] = useState(false); const openReceiptModal = (tx: Transaction) => { + setParam('modal', 'receipt'); + setParam('txId', tx.id); setReceiptData({ trackId: tx.trackId || '-', refNumber: tx.refNumber || '-', @@ -171,12 +175,27 @@ export default function Transactions() { setReceiptModalOpen(true); }; + const closeReceiptModal = () => { + setReceiptModalOpen(false); + setReceiptData(null); + setMultipleParams({ modal: null, txId: null }, false); + }; + const openDetailsModal = (tx: Transaction) => { + setParam('modal', 'details'); + setParam('txId', tx.id); setSelectedTx(tx); setLiveInquiryData(null); setShowRawLogs(false); }; + const closeDetailsModal = () => { + setSelectedTx(null); + setLiveInquiryData(null); + setShowRawLogs(false); + setMultipleParams({ modal: null, txId: null }, false); + }; + const handleCopy = (text: string, label: string) => { navigator.clipboard.writeText(text); toast.success(`${label} با موفقیت کپی شد`); @@ -197,8 +216,9 @@ export default function Transactions() { } }; - const openRefundModal = (tx: Transaction) => { + setParam('modal', 'refund'); + setParam('txId', tx.id); setRefundModalTx(tx); setRefundTarget(tx.type === 'WALLET_TOPUP' || !tx.trackId ? 'wallet' : 'wallet'); setRefundAmount(String(tx.amount || '')); @@ -206,6 +226,12 @@ export default function Transactions() { setTryReverse(true); }; + const closeRefundModal = () => { + setRefundModalTx(null); + setMultipleParams({ modal: null, txId: null }, false); + }; + + const handleExecuteRefund = async () => { if (!refundModalTx) return; @@ -296,9 +322,22 @@ export default function Transactions() { const res = await api.get(`/payment/admin/transactions?${params.toString()}`); if (res.data) { - setTransactions(res.data.transactions || res.data.data || []); + const txList = res.data.transactions || res.data.data || []; + setTransactions(txList); setTotalPages(res.data.totalPages || res.data.meta?.lastPage || 1); setTotalCount(res.data.totalCount || res.data.meta?.total || 0); + + // Auto-open modal if URL specifies modal and txId + const modalType = searchParams.get('modal'); + const txIdParam = searchParams.get('txId'); + if (modalType && txIdParam) { + const foundTx = txList.find((t: Transaction) => t.id === txIdParam); + if (foundTx) { + if (modalType === 'details') openDetailsModal(foundTx); + else if (modalType === 'receipt') openReceiptModal(foundTx); + else if (modalType === 'refund') openRefundModal(foundTx); + } + } } } catch (e) { console.error('Failed to fetch transactions', e); @@ -306,7 +345,8 @@ export default function Transactions() { } finally { setIsLoading(false); } - }, [page, search, statusFilter, gatewayFilter, typeFilter, sortBy, sortOrder]); + }, [page, search, statusFilter, gatewayFilter, typeFilter, sortBy, sortOrder, searchParams]); + useEffect(() => { @@ -762,7 +802,7 @@ export default function Transactions() { {selectedTx && ( setSelectedTx(null)} + onClose={closeDetailsModal} title={`جزئیات تراکنش #${selectedTx.trackId || selectedTx.id.slice(0, 8)}`} icon={Receipt} maxWidth="3xl" @@ -774,7 +814,7 @@ export default function Transactions() { startIcon={Printer} onClick={() => { const tx = selectedTx; - setSelectedTx(null); + closeDetailsModal(); openReceiptModal(tx); }} > @@ -789,7 +829,7 @@ export default function Transactions() { startIcon={Undo2} onClick={() => { const tx = selectedTx; - setSelectedTx(null); + closeDetailsModal(); openRefundModal(tx); }} > @@ -814,13 +854,14 @@ export default function Transactions() { )} -
} + >
{/* Quick Overview Badges */} @@ -981,7 +1022,7 @@ export default function Transactions() { value={refundAmount} onChange={(e) => setRefundAmount(e.target.value)} placeholder="مبلغ استرداد" - className="w-full border border-gray-200 rounded-xl p-3 font-mono outline-none focus:ring-2 focus:ring-purple-500 font-bold" + className="w-full border border-gray-200 rounded-xl p-3 font-mono outline-none focus:ring-2 focus:ring-purple-500 font-bold font-vazir" dir="ltr" />
@@ -993,7 +1034,7 @@ export default function Transactions() { value={refundReason} onChange={(e) => setRefundReason(e.target.value)} placeholder="علت لغو سفارش یا مرجوعی کالا..." - className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-purple-500" + className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-purple-500 font-vazir" /> @@ -1019,10 +1060,11 @@ export default function Transactions() { {receiptModalOpen && receiptData && ( setReceiptModalOpen(false)} + onClose={closeReceiptModal} data={receiptData} /> )} ); } + diff --git a/frontend/admin-panel/src/pages/UITexts.tsx b/frontend/admin-panel/src/pages/UITexts.tsx index 030177c..405f32b 100644 --- a/frontend/admin-panel/src/pages/UITexts.tsx +++ b/frontend/admin-panel/src/pages/UITexts.tsx @@ -673,13 +673,47 @@ const SHARED_FOOTER_FIELDS: SectionField[] = [ { key: 'footer_auth_badge', label: 'متن بج اصالت آلمان', type: 'text' }, ]; +import { useSearchParams } from 'react-router-dom'; + export default function UITexts() { - const [versionTab, setVersionTab] = useState<'v2' | 'v1'>('v2'); - const [v2SubTab, setV2SubTab] = useState<'header' | 'footer' | 'pages'>('pages'); - const [openPageId, setOpenPageId] = useState('home'); - const [openSectionId, setOpenSectionId] = useState('seo'); + const [searchParams, setSearchParams] = useSearchParams(); + + const [versionTab, setVersionTabState] = useState<'v2' | 'v1'>(() => (searchParams.get('version') as 'v2' | 'v1') || 'v2'); + const [v2SubTab, setV2SubTabState] = useState<'header' | 'footer' | 'pages'>(() => (searchParams.get('subTab') as 'header' | 'footer' | 'pages') || 'pages'); + const [openPageId, setOpenPageIdState] = useState(() => searchParams.get('pageId') || 'home'); + const [openSectionId, setOpenSectionIdState] = useState(() => searchParams.get('sectionId') || 'seo'); const [previewDevice, setPreviewDevice] = useState<'desktop' | 'mobile'>('desktop'); + const updateUrlState = (updates: Record) => { + const current = Object.fromEntries(searchParams.entries()); + const merged = { ...current, ...updates }; + const cleaned: Record = {}; + Object.entries(merged).forEach(([k, v]) => { + if (v !== null && v !== undefined && v !== '') cleaned[k] = v; + }); + setSearchParams(cleaned, { replace: true }); + }; + + const setVersionTab = (v: 'v2' | 'v1') => { + setVersionTabState(v); + updateUrlState({ version: v === 'v2' ? null : v }); + }; + + const setV2SubTab = (t: 'header' | 'footer' | 'pages') => { + setV2SubTabState(t); + updateUrlState({ subTab: t === 'pages' ? null : t }); + }; + + const setOpenPageId = (pid: string) => { + setOpenPageIdState(pid); + updateUrlState({ pageId: pid === 'home' ? null : pid }); + }; + + const setOpenSectionId = (sid: string) => { + setOpenSectionIdState(sid); + updateUrlState({ sectionId: sid === 'seo' ? null : sid }); + }; + // State const [texts, setTexts] = useState>({}); const [edits, setEdits] = useState>({}); @@ -690,12 +724,27 @@ export default function UITexts() { const [isSavingAll, setIsSavingAll] = useState(false); // Modals - const [previewModalUrl, setPreviewModalUrl] = useState(null); - const [mediaSelectorKey, setMediaSelectorKey] = useState(null); - const [iconPickerKey, setIconPickerKey] = useState(null); + const [previewModalUrl, setPreviewModalUrl] = useState(() => searchParams.get('previewModal') || null); + const [mediaSelectorKey, setMediaSelectorKeyState] = useState(() => searchParams.get('mediaKey') || null); + const [iconPickerKey, setIconPickerKeyState] = useState(() => searchParams.get('iconKey') || null); + + const setMediaSelectorKey = (k: string | null) => { + setMediaSelectorKeyState(k); + updateUrlState({ mediaKey: k }); + }; + + const setIconPickerKey = (k: string | null) => { + setIconPickerKeyState(k); + updateUrlState({ iconKey: k }); + }; // Legacy v1 State - const [v1ActiveTab, setV1ActiveTab] = useState('all'); + const [v1ActiveTab, setV1ActiveTabState] = useState(() => searchParams.get('v1Tab') || 'all'); + const setV1ActiveTab = (tab: string) => { + setV1ActiveTabState(tab); + updateUrlState({ v1Tab: tab === 'all' ? null : tab }); + }; + useEffect(() => { let isSubscribed = true; diff --git a/frontend/application/components/Header.tsx b/frontend/application/components/Header.tsx index 220f6b3..2afe04e 100644 --- a/frontend/application/components/Header.tsx +++ b/frontend/application/components/Header.tsx @@ -79,6 +79,61 @@ export default function Header({ } ]); + interface HeaderMenuItem { + id: string; + label: string; + href: string; + icon?: string; + children?: { id: string; label: string; href: string }[]; + } + + const [headerNavItems, setHeaderNavItems] = useState([ + { + id: 'category-mega', + label: 'دسته‌بندی درمانی محصولات', + href: '/shop?view=symptoms', + icon: 'Pill', + }, + { + id: 'products', + label: 'فروشگاه و محصولات', + href: '/shop', + children: [ + { id: 'shop-all', label: 'فروشگاه تخصصی محصولات کنینا', href: '/shop' }, + { id: 'catalog', label: 'کاتالوگ دیجیتال و راهنمای بالینی', href: '/catalog' }, + ], + }, + { + id: 'science', + label: 'دانشنامه و آکادمی', + href: '/wiki', + children: [ + { id: 'wiki', label: 'دانشنامه علمی کنینا', href: '/wiki' }, + { id: 'blog', label: 'مجله سلامت پت (وبلاگ)', href: '/blog' }, + { id: 'videos', label: 'آکادمی ویدئویی و مشاوره دامپزشک', href: '/videos' }, + ], + }, + { + id: 'tools', + label: 'ابزارهای هوشمند', + href: '/profile', + children: [ + { id: 'profile', label: 'شناسنامه و سوابق سلامت پت', href: '/profile' }, + { id: 'dashboard', label: 'پایش هوشمند مصرف مکمل‌ها', href: '/dashboard' }, + ], + }, + { + id: 'company', + label: 'درباره کنینا و تماس', + href: '/about', + children: [ + { id: 'about', label: 'درباره کمپانی کنینا آلمان', href: '/about' }, + { id: 'trust-seals', label: 'نمادهای اعتماد و مجوزهای رسمی', href: '/trust-seals' }, + { id: 'contact', label: 'تماس با مرکز پشتیبانی', href: '/contact' }, + ], + }, + ]); + const [isMounted, setIsMounted] = useState(false); useEffect(() => { Promise.resolve().then(() => setIsMounted(true)); @@ -92,24 +147,35 @@ export default function Header({ const isCartDisabled = isCatalogMode || isCatalogDisableCart; useEffect(() => { - const loadNavFilters = async () => { + const loadDynamicNav = async () => { try { - const data = await productService.getNavigationFilters(); - if (data && data.length > 0) { - setMenuItems(data.map(item => ({ - id: item.slug, - title: item.name, - icon: MENU_ICONS[item.slug] || , - solutions: item.symptoms || [] - }))); + const [filtersRes, menuRes] = await Promise.allSettled([ + productService.getNavigationFilters(), + fetch(`${process.env.NEXT_PUBLIC_API_URL || '/api'}/menu/type/HEADER`).then((r) => r.json()), + ]); + + if (filtersRes.status === 'fulfilled' && filtersRes.value && filtersRes.value.length > 0) { + setMenuItems( + filtersRes.value.map((item) => ({ + id: item.slug, + title: item.name, + icon: MENU_ICONS[item.slug] || , + solutions: item.symptoms || [], + })) + ); + } + + if (menuRes.status === 'fulfilled' && Array.isArray(menuRes.value) && menuRes.value.length > 0) { + setHeaderNavItems(menuRes.value); } } catch (err) { - console.error('Failed to load navigation filters:', err); + console.error('Failed to load navigation data:', err); } }; - loadNavFilters(); + loadDynamicNav(); }, []); + useEffect(() => { function handleClickOutside(event: MouseEvent) { if (userProfileRef.current && !userProfileRef.current.contains(event.target as Node)) {