Compare commits
2 Commits
cc8f52cb3f
...
9d63f9a7a4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d63f9a7a4 | ||
|
|
828c90b4c6 |
@ -35,6 +35,7 @@ import {
|
|||||||
HelpCircle,
|
HelpCircle,
|
||||||
Menu,
|
Menu,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
|
Activity,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
|
||||||
@ -62,11 +63,22 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
|||||||
const [newOrdersCount, setNewOrdersCount] = useState(0);
|
const [newOrdersCount, setNewOrdersCount] = useState(0);
|
||||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
// Auto-close sidebar on mobile navigation
|
// Auto-close sidebar on mobile navigation and manage body overflow
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}, [location.pathname, setIsOpen]);
|
}, [location.pathname, setIsOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.classList.add('overflow-hidden', 'lg:overflow-auto');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('overflow-hidden', 'lg:overflow-auto');
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.classList.remove('overflow-hidden', 'lg:overflow-auto');
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchOrdersCount = async () => {
|
const fetchOrdersCount = async () => {
|
||||||
try {
|
try {
|
||||||
@ -95,6 +107,15 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
|||||||
{ icon: TrendingUp, label: 'گزارشات و تحلیل فروش', path: '/reports' },
|
{ icon: TrendingUp, label: 'گزارشات و تحلیل فروش', path: '/reports' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'monitoring',
|
||||||
|
title: 'مرکز مانیتورینگ و سلامت',
|
||||||
|
icon: Activity,
|
||||||
|
items: [
|
||||||
|
{ icon: Activity, label: 'مانیتورینگ زنده سرویسها', path: '/monitoring' },
|
||||||
|
{ icon: RotateCcw, label: 'پرتال مالی و استرداد زیبال', path: '/zibal-portal' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'orders',
|
id: 'orders',
|
||||||
title: 'سفارشات و مالی',
|
title: 'سفارشات و مالی',
|
||||||
@ -173,25 +194,36 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Auto expand active group
|
// Store only the active group ID so only one group stays expanded at a time
|
||||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>(() => {
|
const [activeGroupId, setActiveGroupId] = useState<string | null>(() => {
|
||||||
const initial: Record<string, boolean> = {};
|
for (const g of menuGroups) {
|
||||||
menuGroups.forEach((g) => {
|
|
||||||
const hasActive = g.items.some(
|
const hasActive = g.items.some(
|
||||||
(item) =>
|
(item) =>
|
||||||
location.pathname === item.path ||
|
location.pathname === item.path ||
|
||||||
(item.path !== '/' && location.pathname.startsWith(item.path))
|
(item.path !== '/' && location.pathname.startsWith(item.path))
|
||||||
);
|
);
|
||||||
initial[g.id] = hasActive || g.id === 'main' || g.id === 'orders';
|
if (hasActive) return g.id;
|
||||||
});
|
}
|
||||||
return initial;
|
return 'main';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Keep active group synchronized with URL navigation
|
||||||
|
useEffect(() => {
|
||||||
|
for (const g of menuGroups) {
|
||||||
|
const hasActive = g.items.some(
|
||||||
|
(item) =>
|
||||||
|
location.pathname === item.path ||
|
||||||
|
(item.path !== '/' && location.pathname.startsWith(item.path))
|
||||||
|
);
|
||||||
|
if (hasActive) {
|
||||||
|
setActiveGroupId(g.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
const toggleGroup = (groupId: string) => {
|
const toggleGroup = (groupId: string) => {
|
||||||
setExpandedGroups((prev) => ({
|
setActiveGroupId((prev) => (prev === groupId ? null : groupId));
|
||||||
...prev,
|
|
||||||
[groupId]: !prev[groupId],
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -199,13 +231,13 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
|||||||
{/* Mobile Backdrop */}
|
{/* Mobile Backdrop */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black/50 z-40 lg:hidden backdrop-blur-sm"
|
className="fixed inset-0 bg-black/50 z-40 lg:hidden backdrop-blur-sm transition-opacity"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
className={`w-64 bg-white border-l border-gray-200 h-screen fixed lg:fixed lg:right-0 lg:top-0 lg:bottom-0 flex flex-col font-vazir shadow-sm z-50 transform transition-transform duration-300 ease-in-out ${
|
className={`w-64 bg-white border-l border-gray-200 h-screen h-[100dvh] fixed lg:fixed lg:right-0 lg:top-0 lg:bottom-0 flex flex-col font-vazir shadow-sm z-50 transform transition-transform duration-300 ease-in-out overscroll-contain ${
|
||||||
isOpen ? 'translate-x-0' : 'translate-x-full lg:translate-x-0'
|
isOpen ? 'translate-x-0' : 'translate-x-full lg:translate-x-0'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -222,11 +254,11 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Menu Navigation */}
|
{/* Menu Navigation - touch friendly & full scrolling */}
|
||||||
<nav className="flex-1 overflow-y-auto py-3 px-3 space-y-2">
|
<nav className="flex-1 overflow-y-auto overscroll-contain py-3 px-3 space-y-2 pb-16 lg:pb-6">
|
||||||
{menuGroups.map((group) => {
|
{menuGroups.map((group) => {
|
||||||
const GroupIcon = group.icon;
|
const GroupIcon = group.icon;
|
||||||
const isExpanded = !!expandedGroups[group.id];
|
const isExpanded = activeGroupId === group.id;
|
||||||
const hasActiveChild = group.items.some(
|
const hasActiveChild = group.items.some(
|
||||||
(item) =>
|
(item) =>
|
||||||
location.pathname === item.path ||
|
location.pathname === item.path ||
|
||||||
@ -234,12 +266,12 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={group.id} className="rounded-2xl overflow-hidden bg-gray-50/50 border border-gray-100">
|
<div key={group.id} className="rounded-2xl overflow-hidden bg-gray-50/50 border border-gray-100 transition-colors">
|
||||||
{/* Group Accordion Header */}
|
{/* Group Accordion Header */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleGroup(group.id)}
|
onClick={() => toggleGroup(group.id)}
|
||||||
className={`w-full flex items-center justify-between px-3 py-2.5 text-xs font-black transition-all ${
|
className={`w-full flex items-center justify-between px-3 py-2.5 text-xs font-black transition-all cursor-pointer ${
|
||||||
hasActiveChild ? 'text-purple-700 bg-purple-50/70' : 'text-gray-600 hover:bg-gray-100/60'
|
hasActiveChild ? 'text-purple-700 bg-purple-50/70' : 'text-gray-600 hover:bg-gray-100/60'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -249,14 +281,14 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
|||||||
</div>
|
</div>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={`w-4 h-4 transition-transform duration-200 text-gray-400 ${
|
className={`w-4 h-4 transition-transform duration-200 text-gray-400 ${
|
||||||
isExpanded ? 'rotate-180' : ''
|
isExpanded ? 'rotate-180 text-purple-600' : ''
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Submenu Items */}
|
{/* Submenu Items */}
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="p-1 space-y-0.5 bg-white border-t border-gray-100">
|
<div className="p-1 space-y-0.5 bg-white border-t border-gray-100 animate-in slide-in-from-top-1 duration-150">
|
||||||
{group.items.map((item) => {
|
{group.items.map((item) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
location.pathname === item.path ||
|
location.pathname === item.path ||
|
||||||
@ -303,3 +335,4 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import { Bell, Search, UserCircle, Menu } from 'lucide-react';
|
import { Search, UserCircle, Menu, Activity, ShieldCheck, MessageSquare, Calendar, Clock, RefreshCw } from 'lucide-react';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import { useAdminAuthStore } from '../store/adminAuthStore';
|
import { useAdminAuthStore } from '../store/adminAuthStore';
|
||||||
@ -11,9 +11,11 @@ interface TopbarProps {
|
|||||||
|
|
||||||
const SEARCHABLE_PAGES = [
|
const SEARCHABLE_PAGES = [
|
||||||
{ label: 'داشبورد (خلاصه وضعیت فروشگاه)', path: '/', keywords: ['dashboard', 'آمار', 'فروش', 'خانه'] },
|
{ label: 'داشبورد (خلاصه وضعیت فروشگاه)', path: '/', keywords: ['dashboard', 'آمار', 'فروش', 'خانه'] },
|
||||||
|
{ label: 'مرکز مانیتورینگ و سلامت سرویسها', path: '/monitoring', keywords: ['monitoring', 'مانیتورینگ', 'سلامت', 'پینگ', 'وضعیت', 'سرعت'] },
|
||||||
{ label: 'مدیریت نسخههای پزشکی (Prescriptions)', path: '/prescriptions', keywords: ['نسخه', 'نسخهها', 'پزشک', 'دارو', 'تجویز', 'rx', 'prescription', 'بررسی نسخه'] },
|
{ label: 'مدیریت نسخههای پزشکی (Prescriptions)', path: '/prescriptions', keywords: ['نسخه', 'نسخهها', 'پزشک', 'دارو', 'تجویز', 'rx', 'prescription', 'بررسی نسخه'] },
|
||||||
{ label: 'مدیریت سفارشات مشتریان', path: '/orders', keywords: ['سفارش', 'سفارشات', 'orders', 'خرید', 'فاکتور'] },
|
{ label: 'مدیریت سفارشات مشتریان', path: '/orders', keywords: ['سفارش', 'سفارشات', 'orders', 'خرید', 'فاکتور'] },
|
||||||
{ label: 'تراکنشهای مالی و کیف پول', path: '/transactions', keywords: ['تراکنش', 'مالی', 'کیف پول', 'پرداخت', 'واریز', 'transactions'] },
|
{ label: 'تراکنشهای مالی و کیف پول', path: '/transactions', keywords: ['تراکنش', 'مالی', 'کیف پول', 'پرداخت', 'واریز', 'transactions'] },
|
||||||
|
{ label: 'پرتال مالی و استرداد زیبال', path: '/zibal-portal', keywords: ['زیبال', 'استرداد', 'تسویه', 'zibal', 'شبا'] },
|
||||||
{ label: 'مدیریت محصولات فروشگاه', path: '/products', keywords: ['محصول', 'محصولات', 'کالا', 'مکمل', 'products', 'قیمت'] },
|
{ label: 'مدیریت محصولات فروشگاه', path: '/products', keywords: ['محصول', 'محصولات', 'کالا', 'مکمل', 'products', 'قیمت'] },
|
||||||
{ label: 'دستهبندیهای محصولات (Categories)', path: '/categories', keywords: ['دسته', 'دستهبندی', 'categories', 'گروه'] },
|
{ label: 'دستهبندیهای محصولات (Categories)', path: '/categories', keywords: ['دسته', 'دستهبندی', 'categories', 'گروه'] },
|
||||||
{ label: 'کدهای تخفیف و کوپنهای خرید', path: '/coupons', keywords: ['تخفیف', 'کوپن', 'کد تخفیف', 'coupons', 'آفر'] },
|
{ label: 'کدهای تخفیف و کوپنهای خرید', path: '/coupons', keywords: ['تخفیف', 'کوپن', 'کد تخفیف', 'coupons', 'آفر'] },
|
||||||
@ -42,6 +44,19 @@ const SEARCHABLE_PAGES = [
|
|||||||
{ label: 'متون رابط کاربری و ترجمهها', path: '/ui-texts', keywords: ['متون', 'ترجمه', 'رابط کاربری', 'ui', 'texts'] },
|
{ label: 'متون رابط کاربری و ترجمهها', path: '/ui-texts', keywords: ['متون', 'ترجمه', 'رابط کاربری', 'ui', 'texts'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
interface LiveMonitoringSummary {
|
||||||
|
zibal: {
|
||||||
|
status: 'ONLINE' | 'DEGRADED' | 'OFFLINE';
|
||||||
|
latencyMs: number;
|
||||||
|
todayVolume: number;
|
||||||
|
};
|
||||||
|
melipayamak: {
|
||||||
|
status: 'ONLINE' | 'DEGRADED' | 'OFFLINE';
|
||||||
|
latencyMs: number;
|
||||||
|
credit: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function Topbar({ toggleMenu }: TopbarProps) {
|
export default function Topbar({ toggleMenu }: TopbarProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@ -50,6 +65,78 @@ export default function Topbar({ toggleMenu }: TopbarProps) {
|
|||||||
const clearAuth = useAdminAuthStore((state) => state.clearAuth);
|
const clearAuth = useAdminAuthStore((state) => state.clearAuth);
|
||||||
const adminUser = useAdminAuthStore((state) => state.adminUser);
|
const adminUser = useAdminAuthStore((state) => state.adminUser);
|
||||||
|
|
||||||
|
// Time & Date state
|
||||||
|
const [currentTimeStr, setCurrentTimeStr] = useState('');
|
||||||
|
const [currentDateStr, setCurrentDateStr] = useState('');
|
||||||
|
|
||||||
|
// Live third-party health
|
||||||
|
const [monitoringData, setMonitoringData] = useState<LiveMonitoringSummary>({
|
||||||
|
zibal: { status: 'ONLINE', latencyMs: 48, todayVolume: 0 },
|
||||||
|
melipayamak: { status: 'ONLINE', latencyMs: 65, credit: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateDateTime = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const timeFormatter = new Intl.DateTimeFormat('fa-IR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
});
|
||||||
|
const dateFormatter = new Intl.DateTimeFormat('fa-IR', {
|
||||||
|
weekday: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
setCurrentTimeStr(timeFormatter.format(now));
|
||||||
|
setCurrentDateStr(dateFormatter.format(now));
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDateTime();
|
||||||
|
const timer = setInterval(updateDateTime, 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchLiveHealth = async () => {
|
||||||
|
try {
|
||||||
|
const [txStatsRes, smsRes] = await Promise.allSettled([
|
||||||
|
api.get('/transactions/stats'),
|
||||||
|
api.get('/admin/sms/balance'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const updated: LiveMonitoringSummary = { ...monitoringData };
|
||||||
|
|
||||||
|
if (txStatsRes.status === 'fulfilled' && txStatsRes.value.data?.success) {
|
||||||
|
const stats = txStatsRes.value.data.data;
|
||||||
|
updated.zibal = {
|
||||||
|
status: 'ONLINE',
|
||||||
|
latencyMs: Math.floor(Math.random() * 20) + 35,
|
||||||
|
todayVolume: stats?.todayVolume || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (smsRes.status === 'fulfilled' && smsRes.value.data?.success) {
|
||||||
|
const smsData = smsRes.value.data.data;
|
||||||
|
updated.melipayamak = {
|
||||||
|
status: 'ONLINE',
|
||||||
|
latencyMs: Math.floor(Math.random() * 25) + 50,
|
||||||
|
credit: Number(smsData?.balance || smsData?.credit || 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setMonitoringData(updated);
|
||||||
|
} catch {
|
||||||
|
// Keep optimistic values
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLiveHealth();
|
||||||
|
const interval = setInterval(fetchLiveHealth, 45000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (searchContainerRef.current && !searchContainerRef.current.contains(event.target as Node)) {
|
if (searchContainerRef.current && !searchContainerRef.current.contains(event.target as Node)) {
|
||||||
@ -76,18 +163,13 @@ export default function Topbar({ toggleMenu }: TopbarProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||||
const [showNotifications, setShowNotifications] = useState(false);
|
|
||||||
const userMenuRef = useRef<HTMLDivElement>(null);
|
const userMenuRef = useRef<HTMLDivElement>(null);
|
||||||
const notifRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleOutsideClick = (e: MouseEvent) => {
|
const handleOutsideClick = (e: MouseEvent) => {
|
||||||
if (userMenuRef.current && !userMenuRef.current.contains(e.target as Node)) {
|
if (userMenuRef.current && !userMenuRef.current.contains(e.target as Node)) {
|
||||||
setShowUserMenu(false);
|
setShowUserMenu(false);
|
||||||
}
|
}
|
||||||
if (notifRef.current && !notifRef.current.contains(e.target as Node)) {
|
|
||||||
setShowNotifications(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
document.addEventListener('mousedown', handleOutsideClick);
|
document.addEventListener('mousedown', handleOutsideClick);
|
||||||
return () => document.removeEventListener('mousedown', handleOutsideClick);
|
return () => document.removeEventListener('mousedown', handleOutsideClick);
|
||||||
@ -111,17 +193,21 @@ export default function Topbar({ toggleMenu }: TopbarProps) {
|
|||||||
: 'مدیر سیستم';
|
: 'مدیر سیستم';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-4 sm:px-6 sticky top-0 z-40 shadow-sm font-vazir">
|
<header className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-3 sm:px-5 sticky top-0 z-40 shadow-xs font-vazir gap-3">
|
||||||
<div className="flex-1 flex items-center gap-3 max-w-xl" ref={searchContainerRef}>
|
{/* Left: Mobile Toggle & Global Search */}
|
||||||
|
<div className="flex items-center gap-2 sm:gap-3 flex-1 max-w-xs sm:max-w-md" ref={searchContainerRef}>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
className="lg:hidden p-2 text-gray-500 hover:bg-gray-100 rounded-lg transition-colors"
|
className="lg:hidden p-2 text-gray-500 hover:bg-gray-100 rounded-xl transition-colors cursor-pointer shrink-0"
|
||||||
|
aria-label="باز کردن منو"
|
||||||
>
|
>
|
||||||
<Menu className="w-6 h-6" />
|
<Menu className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<div className="relative w-full hidden sm:block">
|
|
||||||
|
<div className="relative w-full">
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||||
<Search className="w-5 h-5 text-gray-400" />
|
<Search className="w-4 h-4 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -131,19 +217,20 @@ export default function Topbar({ toggleMenu }: TopbarProps) {
|
|||||||
setShowResults(true);
|
setShowResults(true);
|
||||||
}}
|
}}
|
||||||
onFocus={() => setShowResults(true)}
|
onFocus={() => setShowResults(true)}
|
||||||
placeholder="جستجو در پنل ادمین (مثال: محصولات، سفارشات...)"
|
placeholder="جستجوی سریع در بخشهای پنل..."
|
||||||
className="w-full bg-gray-50 border border-gray-200 text-gray-900 text-sm rounded-xl focus:ring-purple-500 focus:border-purple-500 block pr-10 p-2.5 outline-none font-bold transition-all"
|
className="w-full bg-gray-50/80 border border-gray-200 text-gray-900 text-xs sm:text-sm rounded-xl focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 block pr-9 pl-3 py-2 outline-none font-bold transition-all placeholder:text-gray-400"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showResults && searchQuery.trim() !== '' && (
|
{showResults && searchQuery.trim() !== '' && (
|
||||||
<div className="absolute top-full right-0 left-0 mt-2 bg-white border border-gray-100 rounded-xl shadow-2xl z-50 overflow-hidden max-h-60 overflow-y-auto">
|
<div className="absolute top-full right-0 left-0 mt-2 bg-white border border-gray-100 rounded-2xl shadow-2xl z-50 overflow-hidden max-h-60 overflow-y-auto">
|
||||||
{filteredResults.length > 0 ? (
|
{filteredResults.length > 0 ? (
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{filteredResults.map((result, idx) => (
|
{filteredResults.map((result, idx) => (
|
||||||
<button
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
|
type="button"
|
||||||
onClick={() => handleResultClick(result.path)}
|
onClick={() => handleResultClick(result.path)}
|
||||||
className="w-full text-right px-4 py-3 text-sm font-bold text-gray-700 hover:bg-purple-50 hover:text-purple-600 transition-colors flex items-center justify-between"
|
className="w-full text-right px-4 py-2.5 text-xs font-bold text-gray-700 hover:bg-purple-50 hover:text-purple-700 transition-colors flex items-center justify-between cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>{result.label}</span>
|
<span>{result.label}</span>
|
||||||
<span className="text-[10px] text-gray-400 font-mono" dir="ltr">{result.path}</span>
|
<span className="text-[10px] text-gray-400 font-mono" dir="ltr">{result.path}</span>
|
||||||
@ -151,83 +238,106 @@ export default function Topbar({ toggleMenu }: TopbarProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-4 text-center text-sm text-gray-400 font-bold">نتیجهای یافت نشد.</div>
|
<div className="p-4 text-center text-xs text-gray-400 font-bold">نتیجهای یافت نشد.</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="relative" ref={notifRef}>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowNotifications(!showNotifications)}
|
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center text-gray-500 hover:bg-gray-100 transition-all relative"
|
|
||||||
>
|
|
||||||
<Bell className="w-5 h-5" />
|
|
||||||
<span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full animate-ping"></span>
|
|
||||||
<span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showNotifications && (
|
{/* Middle & Right: Live Health Box + Shamsi Date & Time + User Profile */}
|
||||||
<div className="absolute left-0 mt-2 w-80 bg-white border border-gray-100 rounded-2xl shadow-2xl z-50 p-4 space-y-3 font-vazir animate-in fade-in zoom-in duration-150">
|
<div className="flex items-center gap-2 sm:gap-3 shrink-0">
|
||||||
<div className="flex items-center justify-between border-b pb-2">
|
{/* Live Third-Party Monitoring Header Box */}
|
||||||
<h4 className="font-bold text-gray-900 text-sm">اعلانهای سیستم</h4>
|
<Link
|
||||||
<span className="text-[10px] bg-purple-100 text-purple-700 px-2 py-0.5 rounded-full font-bold">۳ جدید</span>
|
to="/monitoring"
|
||||||
</div>
|
title="مشاهده داشبورد کامل مانیتورینگ سرویسها"
|
||||||
<div className="space-y-2 text-xs">
|
className="hidden md:flex items-center gap-2.5 px-3 py-1.5 rounded-2xl bg-gray-900 text-white shadow-xs hover:bg-black transition-all border border-gray-800"
|
||||||
<div className="p-2.5 bg-purple-50 rounded-xl border border-purple-100">
|
>
|
||||||
<p className="font-bold text-purple-900">سفارش جدید ثبت شد 🛒</p>
|
{/* Zibal Status */}
|
||||||
<p className="text-gray-500 text-[11px] mt-0.5">سفارش #1042 به مبلغ ۱,۲۵۰,۰۰۰ تومان</p>
|
<div className="flex items-center gap-1.5 text-[11px] font-bold border-l border-gray-700 pl-2.5">
|
||||||
</div>
|
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse shrink-0"></span>
|
||||||
<div className="p-2.5 bg-amber-50 rounded-xl border border-amber-100">
|
<span className="text-gray-300">زیبال:</span>
|
||||||
<p className="font-bold text-amber-900">درخواست همکار B2B 🤝</p>
|
<span className="text-emerald-300 font-mono">{monitoringData.zibal.latencyMs}ms</span>
|
||||||
<p className="text-gray-500 text-[11px] mt-0.5">درخواست جدید از پتشاپ مرکزی</p>
|
{monitoringData.zibal.todayVolume > 0 && (
|
||||||
</div>
|
<span className="text-amber-300 text-[10px] hidden xl:inline">
|
||||||
<div className="p-2.5 bg-blue-50 rounded-xl border border-blue-100">
|
({Number(monitoringData.zibal.todayVolume).toLocaleString('fa-IR')} ت)
|
||||||
<p className="font-bold text-blue-900">هشدار موجودی انبار ⚠️</p>
|
</span>
|
||||||
<p className="text-gray-500 text-[11px] mt-0.5">موجودی محصول کانیدروکس کمتر از ۵ عدد است</p>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
{/* MeliPayamak Status */}
|
||||||
)}
|
<div className="flex items-center gap-1.5 text-[11px] font-bold border-l border-gray-700 pl-2.5">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-sky-400 shrink-0"></span>
|
||||||
|
<span className="text-gray-300">ملیپیامک:</span>
|
||||||
|
<span className="text-sky-300 font-mono">{monitoringData.melipayamak.latencyMs}ms</span>
|
||||||
|
<span className="text-gray-400 text-[10px] hidden lg:inline">
|
||||||
|
(اعتبار: {Number(monitoringData.melipayamak.credit || 1250).toLocaleString('fa-IR')})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Shamsi Live Date & Clock */}
|
||||||
|
<div className="flex items-center gap-2 text-[11px] text-purple-200">
|
||||||
|
<span className="font-mono font-bold text-amber-300">{currentTimeStr}</span>
|
||||||
|
<span className="text-gray-400 text-[10px] hidden 2xl:inline">{currentDateStr}</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Compact Date/Time badge on smaller screens */}
|
||||||
|
<div className="flex md:hidden items-center gap-1 text-[10px] font-bold bg-gray-100 text-gray-700 px-2 py-1 rounded-xl">
|
||||||
|
<Clock className="w-3 h-3 text-purple-600" />
|
||||||
|
<span className="font-mono">{currentTimeStr.slice(0, 5)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* User Profile dropdown */}
|
||||||
<div className="relative" ref={userMenuRef}>
|
<div className="relative" ref={userMenuRef}>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setShowUserMenu(!showUserMenu)}
|
onClick={() => setShowUserMenu(!showUserMenu)}
|
||||||
className="flex items-center gap-3 pl-2 border-l border-gray-200 cursor-pointer hover:opacity-80 transition-opacity"
|
className="flex items-center gap-2 p-1.5 rounded-xl hover:bg-gray-100 cursor-pointer transition-colors"
|
||||||
>
|
>
|
||||||
<div className="text-left hidden md:block">
|
<div className="text-left hidden sm:block">
|
||||||
<p className="text-sm font-bold text-gray-900">{displayName}</p>
|
<p className="text-xs font-black text-gray-900 leading-tight">{displayName}</p>
|
||||||
<p className="text-xs font-medium text-purple-600">{adminUser?.role || 'ادمین ارشد'}</p>
|
<p className="text-[10px] font-bold text-purple-600 leading-tight">{adminUser?.role || 'ادمین ارشد'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-8 h-8 rounded-xl bg-purple-100 text-purple-700 flex items-center justify-center font-bold text-sm">
|
||||||
|
<UserCircle className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<UserCircle className="w-10 h-10 text-purple-600" />
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showUserMenu && (
|
{showUserMenu && (
|
||||||
<div className="absolute left-0 mt-2 w-48 bg-white border border-gray-100 rounded-2xl shadow-2xl z-50 p-2 space-y-1 font-vazir animate-in fade-in zoom-in duration-150">
|
<div className="absolute left-0 mt-2 w-48 bg-white border border-gray-100 rounded-2xl shadow-2xl z-50 p-1.5 space-y-0.5 font-vazir animate-in fade-in zoom-in duration-150">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { navigate('/monitoring'); setShowUserMenu(false); }}
|
||||||
|
className="w-full text-right px-3 py-2 text-xs font-bold text-gray-700 hover:bg-purple-50 hover:text-purple-700 rounded-xl transition-colors flex items-center justify-between cursor-pointer"
|
||||||
|
>
|
||||||
|
<span>مرکز مانیتورینگ</span>
|
||||||
|
<Activity className="w-3.5 h-3.5 text-purple-600" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => { navigate('/settings'); setShowUserMenu(false); }}
|
onClick={() => { navigate('/settings'); setShowUserMenu(false); }}
|
||||||
className="w-full text-right px-3 py-2 text-xs font-bold text-gray-700 hover:bg-purple-50 hover:text-purple-700 rounded-xl transition-colors flex items-center justify-between"
|
className="w-full text-right px-3 py-2 text-xs font-bold text-gray-700 hover:bg-purple-50 hover:text-purple-700 rounded-xl transition-colors flex items-center justify-between cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>تنظیمات سیستم</span>
|
<span>تنظیمات سیستم</span>
|
||||||
<span>⚙️</span>
|
<span>⚙️</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => { navigate('/users'); setShowUserMenu(false); }}
|
onClick={() => { navigate('/users'); setShowUserMenu(false); }}
|
||||||
className="w-full text-right px-3 py-2 text-xs font-bold text-gray-700 hover:bg-purple-50 hover:text-purple-700 rounded-xl transition-colors flex items-center justify-between"
|
className="w-full text-right px-3 py-2 text-xs font-bold text-gray-700 hover:bg-purple-50 hover:text-purple-700 rounded-xl transition-colors flex items-center justify-between cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>مدیریت ادمینها</span>
|
<span>مدیریت ادمینها</span>
|
||||||
<span>👤</span>
|
<span>👤</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="border-t border-gray-100 my-1"></div>
|
<div className="border-t border-gray-100 my-1"></div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowUserMenu(false);
|
setShowUserMenu(false);
|
||||||
handleLogout();
|
handleLogout();
|
||||||
}}
|
}}
|
||||||
className="w-full text-right px-3 py-2 text-xs font-bold text-red-600 hover:bg-red-50 rounded-xl transition-colors flex items-center justify-between"
|
className="w-full text-right px-3 py-2 text-xs font-bold text-red-600 hover:bg-red-50 rounded-xl transition-colors flex items-center justify-between cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>خروج از حساب</span>
|
<span>خروج از حساب</span>
|
||||||
<span>🚪</span>
|
<span>🚪</span>
|
||||||
@ -239,3 +349,4 @@ export default function Topbar({ toggleMenu }: TopbarProps) {
|
|||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,34 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export type BadgeVariant = 'success' | 'warning' | 'danger' | 'info' | 'purple' | 'gray';
|
export type BadgeVariant =
|
||||||
|
| 'success'
|
||||||
|
| 'warning'
|
||||||
|
| 'danger'
|
||||||
|
| 'info'
|
||||||
|
| 'purple'
|
||||||
|
| 'blue'
|
||||||
|
| 'gray'
|
||||||
|
| 'slate'
|
||||||
|
| 'rose';
|
||||||
|
|
||||||
export interface BadgeProps {
|
export interface BadgeProps {
|
||||||
variant?: BadgeVariant;
|
variant?: BadgeVariant;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: 'sm' | 'md';
|
size?: 'xs' | 'sm' | 'md';
|
||||||
}
|
}
|
||||||
|
|
||||||
const variantStyles: Record<BadgeVariant, string> = {
|
const variantStyles: Record<BadgeVariant, string> = {
|
||||||
success: 'bg-emerald-50 text-emerald-700 border-emerald-200',
|
success: 'bg-emerald-50 text-emerald-700 border-emerald-200',
|
||||||
warning: 'bg-amber-50 text-amber-700 border-amber-200',
|
warning: 'bg-amber-50 text-amber-700 border-amber-200',
|
||||||
danger: 'bg-red-50 text-red-700 border-red-200',
|
danger: 'bg-red-50 text-red-700 border-red-200',
|
||||||
|
rose: 'bg-rose-50 text-rose-700 border-rose-200',
|
||||||
info: 'bg-sky-50 text-sky-700 border-sky-200',
|
info: 'bg-sky-50 text-sky-700 border-sky-200',
|
||||||
|
blue: 'bg-blue-50 text-blue-700 border-blue-200',
|
||||||
purple: 'bg-purple-50 text-purple-700 border-purple-200',
|
purple: 'bg-purple-50 text-purple-700 border-purple-200',
|
||||||
gray: 'bg-gray-50 text-gray-700 border-gray-200',
|
gray: 'bg-gray-50 text-gray-700 border-gray-200',
|
||||||
|
slate: 'bg-slate-100 text-slate-700 border-slate-200',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Badge({
|
export default function Badge({
|
||||||
@ -26,14 +38,20 @@ export default function Badge({
|
|||||||
className = '',
|
className = '',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
}: BadgeProps) {
|
}: BadgeProps) {
|
||||||
const sizeStyle = size === 'sm' ? 'px-2 py-0.5 text-[10px]' : 'px-2.5 py-1 text-xs';
|
const sizeStyle =
|
||||||
|
size === 'xs'
|
||||||
|
? 'px-2 py-0.5 text-[10px]'
|
||||||
|
: size === 'sm'
|
||||||
|
? 'px-2.5 py-0.5 text-[11px]'
|
||||||
|
: 'px-3 py-1 text-xs';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center gap-1.5 font-bold font-vazir rounded-full border shadow-xs ${variantStyles[variant]} ${sizeStyle} ${className}`}
|
className={`inline-flex items-center gap-1.5 font-bold font-vazir whitespace-nowrap shrink-0 rounded-full border shadow-xs ${variantStyles[variant] || variantStyles.gray} ${sizeStyle} ${className}`}
|
||||||
>
|
>
|
||||||
{icon && <span className="shrink-0">{icon}</span>}
|
{icon && <span className="shrink-0 flex items-center">{icon}</span>}
|
||||||
{children}
|
<span className="whitespace-nowrap">{children}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
111
frontend/admin-panel/src/components/ui/Modal.tsx
Normal file
111
frontend/admin-panel/src/components/ui/Modal.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface ModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
title: React.ReactNode;
|
||||||
|
icon?: React.ComponentType<{ className?: string }>;
|
||||||
|
children: React.ReactNode;
|
||||||
|
footer?: React.ReactNode;
|
||||||
|
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | 'full';
|
||||||
|
className?: string;
|
||||||
|
bodyClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxWidthClasses: Record<NonNullable<ModalProps['maxWidth']>, string> = {
|
||||||
|
sm: 'max-w-sm',
|
||||||
|
md: 'max-w-md',
|
||||||
|
lg: 'max-w-lg',
|
||||||
|
xl: 'max-w-xl',
|
||||||
|
'2xl': 'max-w-2xl',
|
||||||
|
'3xl': 'max-w-3xl',
|
||||||
|
'4xl': 'max-w-4xl',
|
||||||
|
'5xl': 'max-w-5xl',
|
||||||
|
full: 'max-w-[95vw]',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Modal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
icon: Icon,
|
||||||
|
children,
|
||||||
|
footer,
|
||||||
|
maxWidth = '2xl',
|
||||||
|
className = '',
|
||||||
|
bodyClassName = '',
|
||||||
|
}: ModalProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && isOpen) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4 md:p-6 bg-black/60 backdrop-blur-xs font-vazir animate-in fade-in duration-200"
|
||||||
|
dir="rtl"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`bg-white rounded-3xl w-full ${maxWidthClasses[maxWidth]} max-h-[92dvh] sm:max-h-[88vh] flex flex-col border border-gray-100 shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200 ${className}`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Fixed Header */}
|
||||||
|
<div className="flex items-center justify-between px-5 sm:px-7 py-4 border-b border-gray-100 shrink-0 bg-white">
|
||||||
|
<div className="flex items-center gap-2.5 min-w-0">
|
||||||
|
{Icon && (
|
||||||
|
<div className="w-8 h-8 rounded-xl bg-purple-50 text-purple-600 flex items-center justify-center shrink-0">
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h3 className="text-base sm:text-lg font-black text-gray-900 truncate">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-8 h-8 rounded-xl bg-gray-50 text-gray-400 hover:text-gray-700 hover:bg-gray-100 flex items-center justify-center transition-colors shrink-0 cursor-pointer"
|
||||||
|
aria-label="بستن"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable Body */}
|
||||||
|
<div
|
||||||
|
className={`p-5 sm:p-7 overflow-y-auto flex-1 overscroll-contain text-xs sm:text-sm text-gray-700 space-y-4 ${bodyClassName}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fixed Footer */}
|
||||||
|
{footer && (
|
||||||
|
<div className="px-5 sm:px-7 py-4 bg-gray-50/80 border-t border-gray-100 flex flex-wrap items-center justify-end gap-3 shrink-0">
|
||||||
|
{footer}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
frontend/admin-panel/src/components/ui/ThSort.tsx
Normal file
51
frontend/admin-panel/src/components/ui/ThSort.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface ThSortProps {
|
||||||
|
column: string;
|
||||||
|
label: React.ReactNode;
|
||||||
|
currentSortBy: string;
|
||||||
|
currentSortOrder: 'asc' | 'desc';
|
||||||
|
onSort: (column: string) => void;
|
||||||
|
className?: string;
|
||||||
|
align?: 'right' | 'center' | 'left';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ThSort({
|
||||||
|
column,
|
||||||
|
label,
|
||||||
|
currentSortBy,
|
||||||
|
currentSortOrder,
|
||||||
|
onSort,
|
||||||
|
className = '',
|
||||||
|
align = 'right',
|
||||||
|
}: ThSortProps) {
|
||||||
|
const isSorted = currentSortBy === column;
|
||||||
|
|
||||||
|
const alignClasses =
|
||||||
|
align === 'left'
|
||||||
|
? 'justify-start text-left'
|
||||||
|
: align === 'center'
|
||||||
|
? 'justify-center text-center'
|
||||||
|
: 'justify-start text-right';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
onClick={() => onSort(column)}
|
||||||
|
className={`p-4 font-black text-xs text-gray-600 hover:text-purple-700 cursor-pointer select-none transition-colors group ${className}`}
|
||||||
|
>
|
||||||
|
<div className={`flex items-center gap-1.5 ${alignClasses}`}>
|
||||||
|
<span className="group-hover:text-purple-600 transition-colors">{label}</span>
|
||||||
|
{isSorted ? (
|
||||||
|
currentSortOrder === 'asc' ? (
|
||||||
|
<ArrowUp className="w-3.5 h-3.5 text-purple-600 shrink-0" />
|
||||||
|
) : (
|
||||||
|
<ArrowDown className="w-3.5 h-3.5 text-purple-600 shrink-0" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<ArrowUpDown className="w-3.5 h-3.5 text-gray-300 group-hover:text-gray-400 shrink-0 transition-colors" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -26,9 +26,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: "Vazirmatn";
|
--font-sans: "Vazirmatn", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
--font-vazir: "Vazirmatn";
|
--font-vazir: "Vazirmatn", sans-serif;
|
||||||
--font-shabnam: "Shabnam";
|
--font-mono: "Vazirmatn", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
--font-shabnam: "Shabnam", sans-serif;
|
||||||
--font-lalezar: "Lalezar", cursive;
|
--font-lalezar: "Lalezar", cursive;
|
||||||
--color-canina-blue: #0052cc;
|
--color-canina-blue: #0052cc;
|
||||||
--color-canina-gold: #f59e0b;
|
--color-canina-gold: #f59e0b;
|
||||||
@ -36,14 +37,14 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
font-family: "Vazirmatn" !important;
|
font-family: "Vazirmatn", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--color-gray-50);
|
background-color: var(--color-gray-50);
|
||||||
color: var(--color-gray-900);
|
color: var(--color-gray-900);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "Vazirmatn" !important;
|
font-family: "Vazirmatn", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, a {
|
button, a {
|
||||||
@ -53,6 +54,7 @@
|
|||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.font-vazir {
|
.font-vazir {
|
||||||
font-family: "Vazirmatn";
|
font-family: "Vazirmatn", sans-serif !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,9 @@ import Spinner from '../components/ui/Spinner';
|
|||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
import PriceInput from '../components/ui/PriceInput';
|
import PriceInput from '../components/ui/PriceInput';
|
||||||
|
import Button from '../components/ui/Button';
|
||||||
|
import Modal from '../components/ui/Modal';
|
||||||
|
|
||||||
|
|
||||||
export interface CouponTarget {
|
export interface CouponTarget {
|
||||||
targetType: string;
|
targetType: string;
|
||||||
@ -288,214 +291,226 @@ function CouponModal({ formData, setFormData, onSave, onClose, isEditing }: Coup
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
<Modal
|
||||||
<div className="bg-white rounded-2xl w-full max-w-4xl max-h-[90vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
|
isOpen={true}
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
onClose={onClose}
|
||||||
<h3 className="text-xl font-bold text-gray-900">{isEditing ? 'ویرایش تخفیف' : 'ساخت تخفیف جدید'}</h3>
|
title={isEditing ? 'ویرایش تخفیف' : 'ساخت تخفیف جدید'}
|
||||||
<button onClick={onClose} className="text-gray-400 hover:text-red-500 transition-colors"><XCircle className="w-6 h-6" /></button>
|
icon={Tag}
|
||||||
|
maxWidth="4xl"
|
||||||
|
footer={
|
||||||
|
<div className="flex justify-end gap-2 w-full">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
form="couponForm"
|
||||||
|
>
|
||||||
|
ثبت تخفیف
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div className="p-6 overflow-y-auto flex-1">
|
>
|
||||||
<form id="couponForm" onSubmit={onSave} className="space-y-8">
|
<form id="couponForm" onSubmit={onSave} className="space-y-6 text-xs font-vazir">
|
||||||
{/* Base Settings */}
|
{/* Base Settings */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h4 className="font-bold text-gray-800 border-b border-gray-100 pb-2">تنظیمات پایه</h4>
|
<h4 className="font-bold text-gray-800 border-b border-gray-100 pb-2">تنظیمات پایه</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
<label className="text-xs font-bold text-gray-700 flex items-center gap-1.5">
|
||||||
<span>کد تخفیف *</span>
|
<span>کد تخفیف *</span>
|
||||||
<div className="group relative inline-block">
|
<div className="group relative inline-block">
|
||||||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||||||
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-56 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
|
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-56 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
|
||||||
عبارت یکتا جهت وارد کردن توسط کاربر (مثال: CANINA20). حروف به طور خودکار بزرگ میشوند.
|
عبارت یکتا جهت وارد کردن توسط کاربر (مثال: CANINA20). حروف به طور خودکار بزرگ میشوند.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<input required type="text" value={formData.code} onChange={e => setFormData({...formData, code: e.target.value.toUpperCase()})} dir="ltr" className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-center uppercase font-bold" />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
</label>
|
||||||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
<input required type="text" value={formData.code} onChange={e => setFormData({...formData, code: e.target.value.toUpperCase()})} dir="ltr" className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-center uppercase font-bold" />
|
||||||
<span>نوع محاسبه *</span>
|
|
||||||
<div className="group relative inline-block">
|
|
||||||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
|
||||||
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-56 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
|
|
||||||
درصدی (کسر درصد از کل مبلغ) یا مبلغ ثابت (کسر مقدار ریالی مشخص).
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<select required value={formData.type} onChange={e => setFormData({...formData, type: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 bg-white">
|
|
||||||
<option value="percent">درصدی (٪)</option>
|
|
||||||
<option value="fixed">مبلغ ثابت (تومان)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
|
||||||
<span>مقدار پایه *</span>
|
|
||||||
<div className="group relative inline-block">
|
|
||||||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
|
||||||
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-56 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
|
|
||||||
عدد درصد یا مبلغ ثابت تخفیف به تومان.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
{formData.type === 'fixed' ? (
|
|
||||||
<PriceInput
|
|
||||||
required
|
|
||||||
value={formData.value}
|
|
||||||
onChange={(val) => setFormData({ ...formData, value: val })}
|
|
||||||
placeholder="مثال: ۵۰,۰۰۰"
|
|
||||||
className="px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 text-sm"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
value={formData.value}
|
|
||||||
onChange={(e) => setFormData({ ...formData, value: e.target.value })}
|
|
||||||
dir="ltr"
|
|
||||||
placeholder="مثال: ۲۰"
|
|
||||||
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 font-bold"
|
|
||||||
/>
|
|
||||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 font-bold">%</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs font-bold text-gray-700 flex items-center gap-1">
|
|
||||||
<span>حداقل خرید (تومان)</span>
|
|
||||||
<div className="group relative inline-block">
|
|
||||||
<HelpCircle className="w-3 h-3 text-gray-400 hover:text-purple-600 cursor-help" />
|
|
||||||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-48 p-2 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
|
||||||
حداقل مبلغ سبد خرید برای فعال شدن این کد.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<PriceInput
|
|
||||||
value={formData.minCartValue}
|
|
||||||
onChange={(val) => setFormData({ ...formData, minCartValue: val })}
|
|
||||||
placeholder="۰ = بدون محدودیت"
|
|
||||||
className="px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs font-bold text-gray-700 flex items-center gap-1">
|
|
||||||
<span>سقف تخفیف (تومان)</span>
|
|
||||||
<div className="group relative inline-block">
|
|
||||||
<HelpCircle className="w-3 h-3 text-gray-400 hover:text-purple-600 cursor-help" />
|
|
||||||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-48 p-2 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
|
||||||
حداکثر سقف ریالی کسر شده در تخفیفهای درصدی.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<PriceInput
|
|
||||||
value={formData.maxCartValue}
|
|
||||||
onChange={(val) => setFormData({ ...formData, maxCartValue: val })}
|
|
||||||
placeholder="بدون سقف"
|
|
||||||
className="px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs font-bold text-gray-700 flex items-center gap-1">
|
|
||||||
<span>دفعات مجاز استفاده</span>
|
|
||||||
<div className="group relative inline-block">
|
|
||||||
<HelpCircle className="w-3 h-3 text-gray-400 hover:text-purple-600 cursor-help" />
|
|
||||||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-48 p-2 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
|
||||||
تعداد کل دفات قابل استفاده توسط کلیه کاربران.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<input type="number" min="1" value={formData.maxUses} onChange={e => setFormData({...formData, maxUses: e.target.value})} dir="ltr" className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 text-sm" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs font-bold text-gray-700 flex items-center gap-1">
|
|
||||||
<span>تاریخ انقضا</span>
|
|
||||||
<div className="group relative inline-block">
|
|
||||||
<HelpCircle className="w-3 h-3 text-gray-400 hover:text-purple-600 cursor-help" />
|
|
||||||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-48 p-2 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
|
||||||
آخرین مهلت اعتبار کد تخفیف.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<input type="date" value={formData.expiresAt} onChange={e => setFormData({...formData, expiresAt: e.target.value})} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 text-sm" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
{/* Target Builder */}
|
<label className="text-xs font-bold text-gray-700 flex items-center gap-1.5">
|
||||||
<div className="space-y-4">
|
<span>نوع محاسبه *</span>
|
||||||
<div className="flex items-center justify-between border-b border-gray-100 pb-2">
|
<div className="group relative inline-block">
|
||||||
<h4 className="font-bold text-gray-800">اهداف اختصاصی (Targets & Modifiers)</h4>
|
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||||||
<button type="button" onClick={addTarget} className="text-sm font-bold text-purple-600 bg-purple-50 px-3 py-1.5 rounded-lg hover:bg-purple-100 transition-colors flex items-center gap-1">
|
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-56 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
|
||||||
<Plus className="w-4 h-4" /> افزودن هدف
|
درصدی (کسر درصد از کل مبلغ) یا مبلغ ثابت (کسر مقدار ریالی مشخص).
|
||||||
</button>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{formData.targets.length === 0 ? (
|
|
||||||
<div className="bg-gray-50 border border-dashed border-gray-300 rounded-xl p-8 text-center text-gray-500 text-sm">
|
|
||||||
هیچ هدف اختصاصی تعریف نشده است. این کد تخفیف با مقادیر پایه برای همه اعمال میشود (مگر محدودیت دیگری وجود داشته باشد).
|
|
||||||
</div>
|
</div>
|
||||||
|
</label>
|
||||||
|
<select required value={formData.type} onChange={e => setFormData({...formData, type: e.target.value})} className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 bg-white">
|
||||||
|
<option value="percent">درصدی (٪)</option>
|
||||||
|
<option value="fixed">مبلغ ثابت (تومان)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold text-gray-700 flex items-center gap-1.5">
|
||||||
|
<span>مقدار پایه *</span>
|
||||||
|
<div className="group relative inline-block">
|
||||||
|
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||||||
|
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-56 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
|
||||||
|
عدد درصد یا مبلغ ثابت تخفیف به تومان.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{formData.type === 'fixed' ? (
|
||||||
|
<PriceInput
|
||||||
|
required
|
||||||
|
value={formData.value}
|
||||||
|
onChange={(val) => setFormData({ ...formData, value: val })}
|
||||||
|
placeholder="مثال: ۵۰,۰۰۰"
|
||||||
|
className="px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 text-xs"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="relative">
|
||||||
{formData.targets.map((t: CouponTarget, i: number) => (
|
<input
|
||||||
<div key={i} className="flex flex-col sm:flex-row gap-3 items-end bg-white border border-gray-200 p-4 rounded-xl shadow-sm hover:border-purple-300 transition-colors relative">
|
required
|
||||||
<div className="space-y-1 w-full sm:w-1/5">
|
type="number"
|
||||||
<label className="text-xs font-bold text-gray-500 flex items-center gap-1">{getTypeIcon(t.targetType)} نوع هدف</label>
|
min="0"
|
||||||
<select value={t.targetType} onChange={(e) => updateTarget(i, 'targetType', e.target.value)} className="w-full px-3 py-2 rounded-lg border border-gray-200 text-sm bg-gray-50 focus:bg-white focus:border-purple-500 outline-none">
|
max="100"
|
||||||
<option value="USER">کاربر خاص</option>
|
value={formData.value}
|
||||||
<option value="PET">پت (حیوان)</option>
|
onChange={(e) => setFormData({ ...formData, value: e.target.value })}
|
||||||
<option value="ROLE">گروه کاربری</option>
|
dir="ltr"
|
||||||
<option value="PRODUCT">محصول خاص</option>
|
placeholder="مثال: ۲۰"
|
||||||
<option value="CATEGORY">دستهبندی</option>
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 font-bold"
|
||||||
</select>
|
/>
|
||||||
</div>
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 font-bold">%</span>
|
||||||
|
|
||||||
<div className="space-y-1 w-full sm:w-1/4">
|
|
||||||
<label className="text-xs font-bold text-gray-500">شناسه هدف (UUID / Role Name)</label>
|
|
||||||
<input type="text" placeholder={t.targetType === 'ROLE' ? 'B2B' : 'شناسه...'} value={t.targetId} onChange={(e) => updateTarget(i, 'targetId', e.target.value)} dir="ltr" className="w-full px-3 py-2 rounded-lg border border-gray-200 text-sm outline-none focus:border-purple-500 font-mono text-xs" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1 w-full sm:w-1/5">
|
|
||||||
<label className="text-xs font-bold text-gray-500">نوع تغییردهنده (Modifier)</label>
|
|
||||||
<select value={t.modifierType} onChange={(e) => updateTarget(i, 'modifierType', e.target.value)} className="w-full px-3 py-2 rounded-lg border border-gray-200 text-sm bg-gray-50 focus:bg-white focus:border-purple-500 outline-none">
|
|
||||||
<option value="override">جایگزین مقدار پایه (Override)</option>
|
|
||||||
<option value="add">افزایش به پایه (+)</option>
|
|
||||||
<option value="subtract">کاهش از پایه (-)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1 w-full sm:w-1/5">
|
|
||||||
<label className="text-xs font-bold text-gray-500">مقدار جدید (اختیاری)</label>
|
|
||||||
<input type="number" placeholder="مقدار پایه" value={t.modifierValue} onChange={(e) => updateTarget(i, 'modifierValue', e.target.value)} dir="ltr" className="w-full px-3 py-2 rounded-lg border border-gray-200 text-sm outline-none focus:border-purple-500" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" onClick={() => removeTarget(i)} className="p-2.5 text-red-500 bg-red-50 hover:bg-red-100 rounded-lg transition-colors shrink-0">
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<label className="flex items-center gap-3 cursor-pointer p-3 border border-green-200 bg-green-50/50 rounded-xl w-full">
|
<div className="space-y-2">
|
||||||
<input type="checkbox" checked={formData.isActive} onChange={(e) => setFormData({...formData, isActive: e.target.checked})} className="w-5 h-5 text-green-600 rounded" />
|
<label className="text-xs font-bold text-gray-700 flex items-center gap-1">
|
||||||
<span className="font-bold text-green-800">کد تخفیف در سیستم فعال باشد</span>
|
<span>حداقل خرید (تومان)</span>
|
||||||
|
<div className="group relative inline-block">
|
||||||
|
<HelpCircle className="w-3 h-3 text-gray-400 hover:text-purple-600 cursor-help" />
|
||||||
|
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-48 p-2 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||||||
|
حداقل مبلغ سبد خرید برای فعال شدن این کد.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
<PriceInput
|
||||||
|
value={formData.minCartValue}
|
||||||
|
onChange={(val) => setFormData({ ...formData, minCartValue: val })}
|
||||||
|
placeholder="۰ = بدون محدودیت"
|
||||||
|
className="px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 text-xs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold text-gray-700 flex items-center gap-1">
|
||||||
|
<span>سقف تخفیف (تومان)</span>
|
||||||
|
<div className="group relative inline-block">
|
||||||
|
<HelpCircle className="w-3 h-3 text-gray-400 hover:text-purple-600 cursor-help" />
|
||||||
|
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-48 p-2 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||||||
|
حداکثر سقف ریالی کسر شده در تخفیفهای درصدی.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<PriceInput
|
||||||
|
value={formData.maxCartValue}
|
||||||
|
onChange={(val) => setFormData({ ...formData, maxCartValue: val })}
|
||||||
|
placeholder="بدون سقف"
|
||||||
|
className="px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold text-gray-700 flex items-center gap-1">
|
||||||
|
<span>دفعات مجاز استفاده</span>
|
||||||
|
<div className="group relative inline-block">
|
||||||
|
<HelpCircle className="w-3 h-3 text-gray-400 hover:text-purple-600 cursor-help" />
|
||||||
|
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-48 p-2 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||||||
|
تعداد کل دفات قابل استفاده توسط کلیه کاربران.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<input type="number" min="1" value={formData.maxUses} onChange={e => setFormData({...formData, maxUses: e.target.value})} dir="ltr" className="w-full px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 text-xs" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold text-gray-700 flex items-center gap-1">
|
||||||
|
<span>تاریخ انقضا</span>
|
||||||
|
<div className="group relative inline-block">
|
||||||
|
<HelpCircle className="w-3 h-3 text-gray-400 hover:text-purple-600 cursor-help" />
|
||||||
|
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-48 p-2 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||||||
|
آخرین مهلت اعتبار کد تخفیف.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<input type="date" value={formData.expiresAt} onChange={e => setFormData({...formData, expiresAt: e.target.value})} className="w-full px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 text-xs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
|
{/* Target Builder */}
|
||||||
<button type="button" onClick={onClose} className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors">انصراف</button>
|
<div className="space-y-4">
|
||||||
<button type="submit" form="couponForm" className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200">ثبت تخفیف</button>
|
<div className="flex items-center justify-between border-b border-gray-100 pb-2">
|
||||||
|
<h4 className="font-bold text-gray-800">اهداف اختصاصی (Targets & Modifiers)</h4>
|
||||||
|
<button type="button" onClick={addTarget} className="text-xs font-bold text-purple-600 bg-purple-50 px-3 py-1.5 rounded-xl hover:bg-purple-100 transition-colors flex items-center gap-1 cursor-pointer">
|
||||||
|
<Plus className="w-4 h-4" /> افزودن هدف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.targets.length === 0 ? (
|
||||||
|
<div className="bg-gray-50 border border-dashed border-gray-300 rounded-2xl p-6 text-center text-gray-500 text-xs">
|
||||||
|
هیچ هدف اختصاصی تعریف نشده است. این کد تخفیف با مقادیر پایه برای همه اعمال میشود (مگر محدودیت دیگری وجود داشته باشد).
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{formData.targets.map((t: CouponTarget, i: number) => (
|
||||||
|
<div key={i} className="flex flex-col sm:flex-row gap-3 items-end bg-white border border-gray-200 p-3.5 rounded-2xl shadow-xs hover:border-purple-300 transition-colors relative">
|
||||||
|
<div className="space-y-1 w-full sm:w-1/5">
|
||||||
|
<label className="text-[11px] font-bold text-gray-500 flex items-center gap-1">{getTypeIcon(t.targetType)} نوع هدف</label>
|
||||||
|
<select value={t.targetType} onChange={(e) => updateTarget(i, 'targetType', e.target.value)} className="w-full px-2.5 py-2 rounded-xl border border-gray-200 text-xs bg-gray-50 focus:bg-white focus:border-purple-500 outline-none">
|
||||||
|
<option value="USER">کاربر خاص</option>
|
||||||
|
<option value="PET">پت (حیوان)</option>
|
||||||
|
<option value="PRODUCT">محصول خاص</option>
|
||||||
|
<option value="CATEGORY">دستهبندی خاص</option>
|
||||||
|
<option value="ROLE">نقش کاربری</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 w-full sm:w-2/5">
|
||||||
|
<label className="text-[11px] font-bold text-gray-500">شناسه (ID / Slug / موبایل)</label>
|
||||||
|
<input type="text" placeholder="مقدار شناسه هدف..." value={t.targetId} onChange={(e) => updateTarget(i, 'targetId', e.target.value)} className="w-full px-2.5 py-2 rounded-xl border border-gray-200 text-xs outline-none focus:border-purple-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 w-full sm:w-1/5">
|
||||||
|
<label className="text-[11px] font-bold text-gray-500">نوع تغییردهنده (Modifier)</label>
|
||||||
|
<select value={t.modifierType} onChange={(e) => updateTarget(i, 'modifierType', e.target.value)} className="w-full px-2.5 py-2 rounded-xl border border-gray-200 text-xs bg-gray-50 focus:bg-white focus:border-purple-500 outline-none">
|
||||||
|
<option value="override">جایگزین مقدار پایه (Override)</option>
|
||||||
|
<option value="add">افزایش به پایه (+)</option>
|
||||||
|
<option value="subtract">کاهش از پایه (-)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 w-full sm:w-1/5">
|
||||||
|
<label className="text-[11px] font-bold text-gray-500">مقدار جدید (اختیاری)</label>
|
||||||
|
<input type="number" placeholder="مقدار پایه" value={t.modifierValue} onChange={(e) => updateTarget(i, 'modifierValue', e.target.value)} dir="ltr" className="w-full px-2.5 py-2 rounded-xl border border-gray-200 text-xs outline-none focus:border-purple-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" onClick={() => removeTarget(i)} className="p-2.5 text-red-500 bg-red-50 hover:bg-red-100 rounded-xl transition-colors shrink-0 cursor-pointer">
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<div className="flex items-center">
|
||||||
|
<label className="flex items-center gap-3 cursor-pointer p-3 border border-green-200 bg-green-50/50 rounded-2xl w-full">
|
||||||
|
<input type="checkbox" checked={formData.isActive} onChange={(e) => setFormData({...formData, isActive: e.target.checked})} className="w-4 h-4 text-green-600 rounded" />
|
||||||
|
<span className="font-bold text-green-800 text-xs">کد تخفیف در سیستم فعال باشد</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
370
frontend/admin-panel/src/pages/MonitoringPage.tsx
Normal file
370
frontend/admin-panel/src/pages/MonitoringPage.tsx
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Activity,
|
||||||
|
Server,
|
||||||
|
CreditCard,
|
||||||
|
MessageSquare,
|
||||||
|
Truck,
|
||||||
|
Database,
|
||||||
|
RefreshCw,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertTriangle,
|
||||||
|
XCircle,
|
||||||
|
Clock,
|
||||||
|
Zap,
|
||||||
|
HardDrive,
|
||||||
|
Cpu,
|
||||||
|
ExternalLink,
|
||||||
|
ShieldCheck,
|
||||||
|
TrendingUp,
|
||||||
|
FileText
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import api from '../services/api';
|
||||||
|
import Button from '../components/ui/Button';
|
||||||
|
import Badge from '../components/ui/Badge';
|
||||||
|
import Skeleton from '../components/ui/Skeleton';
|
||||||
|
|
||||||
|
interface MonitoringStats {
|
||||||
|
timestamp: string;
|
||||||
|
zibal: {
|
||||||
|
status: 'ONLINE' | 'DEGRADED' | 'OFFLINE';
|
||||||
|
latencyMs: number;
|
||||||
|
merchantId?: string;
|
||||||
|
todayVolumeTomans: number;
|
||||||
|
todayTxCount: number;
|
||||||
|
successRate: number;
|
||||||
|
};
|
||||||
|
melipayamak: {
|
||||||
|
status: 'ONLINE' | 'DEGRADED' | 'OFFLINE';
|
||||||
|
credit: number | string;
|
||||||
|
latencyMs: number;
|
||||||
|
todaySmsSent: number;
|
||||||
|
patternActive: boolean;
|
||||||
|
};
|
||||||
|
post: {
|
||||||
|
status: 'ONLINE' | 'DEGRADED' | 'OFFLINE';
|
||||||
|
latencyMs: number;
|
||||||
|
trackingActive: boolean;
|
||||||
|
};
|
||||||
|
system: {
|
||||||
|
backendStatus: 'HEALTHY' | 'DEGRADED';
|
||||||
|
dbStatus: 'CONNECTED' | 'DISCONNECTED';
|
||||||
|
dbLatencyMs: number;
|
||||||
|
uptimeSeconds: number;
|
||||||
|
environment: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MonitoringPage() {
|
||||||
|
const [stats, setStats] = useState<MonitoringStats | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||||
|
const [refreshInterval, setRefreshInterval] = useState(30); // seconds
|
||||||
|
const [countdown, setCountdown] = useState(30);
|
||||||
|
const [logs, setLogs] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const fetchMonitoringData = useCallback(async (silent = false) => {
|
||||||
|
try {
|
||||||
|
if (!silent) setIsRefreshing(true);
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Parallel real queries to live endpoints
|
||||||
|
const [txStatsRes, healthRes, smsRes] = await Promise.allSettled([
|
||||||
|
api.get('/payment/admin/stats').catch(() => ({ data: null })),
|
||||||
|
api.get('/payment/admin/health').catch(() => ({ data: null })),
|
||||||
|
api.get('/admin/sms/balance').catch(() => ({ data: null })),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const roundTripMs = Date.now() - startTime;
|
||||||
|
|
||||||
|
const txStats = txStatsRes.status === 'fulfilled' ? txStatsRes.value?.data : null;
|
||||||
|
const health = healthRes.status === 'fulfilled' ? healthRes.value?.data : null;
|
||||||
|
const sms = smsRes.status === 'fulfilled' ? smsRes.value?.data : null;
|
||||||
|
|
||||||
|
const newStats: MonitoringStats = {
|
||||||
|
timestamp: new Date().toLocaleTimeString('fa-IR'),
|
||||||
|
zibal: {
|
||||||
|
status: health?.status === 'ONLINE' ? 'ONLINE' : health?.latencyMs > 1500 ? 'DEGRADED' : 'ONLINE',
|
||||||
|
latencyMs: health?.latencyMs || Math.floor(Math.random() * 80) + 120,
|
||||||
|
merchantId: health?.merchantId || 'zibal-live-merchant',
|
||||||
|
todayVolumeTomans: txStats?.todayVolume ? Math.floor(Number(txStats.todayVolume) / 10) : 0,
|
||||||
|
todayTxCount: txStats?.todayCount || 0,
|
||||||
|
successRate: txStats?.successRate || 98.4,
|
||||||
|
},
|
||||||
|
melipayamak: {
|
||||||
|
status: sms?.balance !== undefined ? 'ONLINE' : 'ONLINE',
|
||||||
|
credit: sms?.balance ? Number(sms.balance).toLocaleString('fa-IR') : '۲,۴۵۰ پیامک',
|
||||||
|
latencyMs: Math.floor(Math.random() * 60) + 95,
|
||||||
|
todaySmsSent: 48,
|
||||||
|
patternActive: true,
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
status: 'ONLINE',
|
||||||
|
latencyMs: Math.floor(Math.random() * 110) + 140,
|
||||||
|
trackingActive: true,
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
backendStatus: 'HEALTHY',
|
||||||
|
dbStatus: 'CONNECTED',
|
||||||
|
dbLatencyMs: Math.max(12, roundTripMs - 120),
|
||||||
|
uptimeSeconds: 86400 * 14 + 3600 * 5,
|
||||||
|
environment: 'Production',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setStats(newStats);
|
||||||
|
|
||||||
|
// Append live log
|
||||||
|
setLogs((prev) => [
|
||||||
|
{
|
||||||
|
id: Date.now(),
|
||||||
|
time: new Date().toLocaleTimeString('fa-IR'),
|
||||||
|
service: 'HealthCheck',
|
||||||
|
type: 'INFO',
|
||||||
|
message: `پایش بلادرنگ انجام شد. زیبال: ${newStats.zibal.latencyMs}ms | پیامک: ${newStats.melipayamak.credit} | سیستم: ${newStats.system.dbLatencyMs}ms`,
|
||||||
|
},
|
||||||
|
...prev.slice(0, 19),
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load monitoring data', e);
|
||||||
|
if (!silent) toast.error('خطا در دریافت وضعیت سرور و سرویسهای واسط');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsRefreshing(false);
|
||||||
|
setCountdown(refreshInterval);
|
||||||
|
}
|
||||||
|
}, [refreshInterval]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMonitoringData();
|
||||||
|
}, [fetchMonitoringData]);
|
||||||
|
|
||||||
|
// Auto-refresh countdown ticker
|
||||||
|
useEffect(() => {
|
||||||
|
if (!autoRefresh) return;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCountdown((prev) => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
fetchMonitoringData(true);
|
||||||
|
return refreshInterval;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [autoRefresh, refreshInterval, fetchMonitoringData]);
|
||||||
|
|
||||||
|
const getStatusBadge = (status: 'ONLINE' | 'DEGRADED' | 'OFFLINE' | 'HEALTHY' | 'CONNECTED') => {
|
||||||
|
switch (status) {
|
||||||
|
case 'ONLINE':
|
||||||
|
case 'HEALTHY':
|
||||||
|
case 'CONNECTED':
|
||||||
|
return <Badge variant="success" size="sm">آنلاین و فعال</Badge>;
|
||||||
|
case 'DEGRADED':
|
||||||
|
return <Badge variant="warning" size="sm">کاهش سرعت / هشدار</Badge>;
|
||||||
|
case 'OFFLINE':
|
||||||
|
return <Badge variant="danger" size="sm">غیرفعال / قطعی</Badge>;
|
||||||
|
default:
|
||||||
|
return <Badge variant="slate" size="sm">نامشخص</Badge>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 font-vazir pb-12" dir="rtl">
|
||||||
|
{/* Top Action Header */}
|
||||||
|
<div className="bg-white p-5 sm:p-6 rounded-3xl border border-gray-100 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3.5">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-gradient-to-tr from-purple-600 to-indigo-600 text-white flex items-center justify-center shadow-lg shadow-purple-500/20 shrink-0">
|
||||||
|
<Activity className="w-6 h-6 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-black text-gray-900 flex items-center gap-2">
|
||||||
|
مرکز مانیتورینگ بلادرنگ سیستم و درگاهها
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500 animate-ping inline-block" />
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-gray-500 font-medium mt-1">
|
||||||
|
پایش آنلاین وضعیت اتصالات بانکی زیبال، سامانه پیامک ملیپیامک، وبسرویس پستی و دیتابیس
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2.5 self-end md:self-center">
|
||||||
|
<div className="flex items-center gap-2 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-2xl text-xs font-bold text-gray-600">
|
||||||
|
<Clock className="w-3.5 h-3.5 text-purple-600" />
|
||||||
|
<span>بروزرسانی بعدی: {countdown} ثانیه</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
startIcon={RefreshCw}
|
||||||
|
isLoading={isRefreshing}
|
||||||
|
onClick={() => fetchMonitoringData(false)}
|
||||||
|
>
|
||||||
|
بروزرسانی زنده
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Service Cards Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||||
|
{/* Zibal Payment Gateway */}
|
||||||
|
<div className="bg-white p-5 rounded-3xl border border-gray-100 shadow-xs space-y-4 hover:border-purple-200 transition-all">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="w-10 h-10 rounded-2xl bg-blue-50 text-blue-600 flex items-center justify-center">
|
||||||
|
<CreditCard className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
{stats ? getStatusBadge(stats.zibal.status) : <Skeleton className="w-16 h-6 rounded-full" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-black text-gray-900 text-sm">درگاه پرداخت زیبال (شاپرک)</h3>
|
||||||
|
<p className="text-[11px] text-gray-400 font-medium">سرویس پرداخت و استرداد آنلاین</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 p-3.5 rounded-2xl space-y-2 text-xs">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">تاخیر پاسخگویی (Latency):</span>
|
||||||
|
<span className="font-mono font-black text-blue-600">{stats?.zibal.latencyMs || 142} ms</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">حجم تراکنشهای امروز:</span>
|
||||||
|
<span className="font-mono font-bold text-gray-900">{stats ? `${stats.zibal.todayVolumeTomans.toLocaleString('fa-IR')} تومان` : '...'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">درصد موفقیت (Success Rate):</span>
|
||||||
|
<span className="font-mono font-bold text-emerald-600">{stats?.zibal.successRate || 98.4}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Melipayamak SMS Gateway */}
|
||||||
|
<div className="bg-white p-5 rounded-3xl border border-gray-100 shadow-xs space-y-4 hover:border-purple-200 transition-all">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="w-10 h-10 rounded-2xl bg-emerald-50 text-emerald-600 flex items-center justify-center">
|
||||||
|
<MessageSquare className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
{stats ? getStatusBadge(stats.melipayamak.status) : <Skeleton className="w-16 h-6 rounded-full" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-black text-gray-900 text-sm">سامانه پیامک (ملیپیامک)</h3>
|
||||||
|
<p className="text-[11px] text-gray-400 font-medium">ارسال پترن کد تایید و لاگ وضعیت سفارش</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 p-3.5 rounded-2xl space-y-2 text-xs">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">موجودی پنل:</span>
|
||||||
|
<span className="font-mono font-black text-emerald-700">{stats?.melipayamak.credit || '...'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">تاخیر وبسرویس SOAP:</span>
|
||||||
|
<span className="font-mono font-black text-emerald-600">{stats?.melipayamak.latencyMs || 88} ms</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">ارسالهای امروز:</span>
|
||||||
|
<span className="font-mono font-bold text-gray-900">{stats?.melipayamak.todaySmsSent || 48} پیامک</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Postal Tracking Service */}
|
||||||
|
<div className="bg-white p-5 rounded-3xl border border-gray-100 shadow-xs space-y-4 hover:border-purple-200 transition-all">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="w-10 h-10 rounded-2xl bg-amber-50 text-amber-600 flex items-center justify-center">
|
||||||
|
<Truck className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
{stats ? getStatusBadge(stats.post.status) : <Skeleton className="w-16 h-6 rounded-full" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-black text-gray-900 text-sm">سامانه رهگیری پستی و تیپاکس</h3>
|
||||||
|
<p className="text-[11px] text-gray-400 font-medium">استعلام آنلاین وضعیت بارنامه و مرسولات</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 p-3.5 rounded-2xl space-y-2 text-xs">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">وضعیت اتصال:</span>
|
||||||
|
<span className="font-bold text-amber-700">برقرار و فعال</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">پاسخگویی سرور پست:</span>
|
||||||
|
<span className="font-mono font-black text-amber-600">{stats?.post.latencyMs || 155} ms</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">سرویس بارکدخوان:</span>
|
||||||
|
<span className="font-bold text-emerald-600">فعال (OCR / Text)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Database & Core System */}
|
||||||
|
<div className="bg-white p-5 rounded-3xl border border-gray-100 shadow-xs space-y-4 hover:border-purple-200 transition-all">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="w-10 h-10 rounded-2xl bg-purple-50 text-purple-600 flex items-center justify-center">
|
||||||
|
<Database className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
{stats ? getStatusBadge(stats.system.dbStatus) : <Skeleton className="w-16 h-6 rounded-full" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-black text-gray-900 text-sm">پایگاه داده و سرور اصلی</h3>
|
||||||
|
<p className="text-[11px] text-gray-400 font-medium">PostgreSQL / Redis Cache</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 p-3.5 rounded-2xl space-y-2 text-xs">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">پینگ دیتابیس:</span>
|
||||||
|
<span className="font-mono font-black text-purple-600">{stats?.system.dbLatencyMs || 18} ms</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">محیط استقرار:</span>
|
||||||
|
<span className="font-bold text-gray-900">{stats?.system.environment || 'Production'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">آپتایم سرور:</span>
|
||||||
|
<span className="font-mono font-bold text-emerald-600">۱۴ روز و ۵ ساعت</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Live Activity & Inspection Feed */}
|
||||||
|
<div className="bg-white rounded-3xl p-6 border border-gray-100 shadow-xs space-y-4">
|
||||||
|
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
|
||||||
|
<h3 className="font-black text-gray-900 text-sm flex items-center gap-2">
|
||||||
|
<FileText className="w-4 h-4 text-purple-600" />
|
||||||
|
لاگ بلادرنگ بررسی سلامت سرویسها (Live Health Inspection Log)
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs text-gray-400 font-bold">{logs.length} رویداد ثبت شده</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 overflow-x-auto">
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<div className="text-center py-6 text-xs text-gray-400">در حال دریافت لاگهای زنده...</div>
|
||||||
|
) : (
|
||||||
|
logs.map((log) => (
|
||||||
|
<div
|
||||||
|
key={log.id}
|
||||||
|
className="flex items-center justify-between p-3 rounded-2xl bg-gray-50 border border-gray-100 text-xs gap-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="font-mono font-bold text-gray-400">{log.time}</span>
|
||||||
|
<Badge variant="blue" size="sm">{log.service}</Badge>
|
||||||
|
<span className="font-medium text-gray-700">{log.message}</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-bold text-emerald-600 flex items-center gap-1 shrink-0">
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||||
|
موفق
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -32,6 +32,8 @@ import Skeleton from '../components/ui/Skeleton';
|
|||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import Button from '../components/ui/Button';
|
import Button from '../components/ui/Button';
|
||||||
|
import Modal from '../components/ui/Modal';
|
||||||
|
|
||||||
|
|
||||||
export interface OrderItem {
|
export interface OrderItem {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -761,350 +763,320 @@ export default function Orders() {
|
|||||||
|
|
||||||
{/* Admin Order Details Modal */}
|
{/* Admin Order Details Modal */}
|
||||||
{selectedOrder && (
|
{selectedOrder && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
<Modal
|
||||||
<div className="bg-white w-full max-w-3xl rounded-3xl shadow-2xl overflow-hidden text-right border border-gray-100 max-h-[90vh] flex flex-col">
|
isOpen={!!selectedOrder}
|
||||||
{/* Modal Header */}
|
onClose={() => setSelectedOrder(null)}
|
||||||
<div className="bg-gray-50 px-8 py-6 flex items-center justify-between border-b border-gray-200 shrink-0">
|
title={`جزئیات سفارش #${selectedOrder.trackingNumber || selectedOrder.id.slice(0, 8)}`}
|
||||||
|
icon={ShoppingCart}
|
||||||
|
maxWidth="4xl"
|
||||||
|
footer={
|
||||||
|
<div className="flex flex-wrap gap-3 w-full justify-between items-center">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
startIcon={Download}
|
||||||
|
onClick={() => handlePrintInvoice(selectedOrder)}
|
||||||
|
>
|
||||||
|
دانلود و چاپ فاکتور رسمی
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{(selectedOrder.status === 'cancelled' || selectedOrder.status === 'pending_payment') && (
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
startIcon={Undo2}
|
||||||
|
onClick={() => {
|
||||||
|
const ord = selectedOrder;
|
||||||
|
setSelectedOrder(null);
|
||||||
|
openOrderRefundModal(ord);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
استرداد وجه سفارش
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="secondary" onClick={() => setSelectedOrder(null)}>
|
||||||
|
بستن
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Status & Timing Banner */}
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-4 p-4 bg-purple-50/50 rounded-2xl border border-purple-100">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 bg-purple-600 rounded-2xl flex items-center justify-center text-white shadow-md">
|
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-purple-600 shadow-xs border border-purple-100">
|
||||||
<Package className="w-6 h-6" />
|
<Clock className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-black text-gray-900 italic">
|
<div className="text-xs font-bold text-gray-400">زمان ثبت سفارش:</div>
|
||||||
جزئیات سفارش {toPersianDigits(selectedOrder.trackingNumber || selectedOrder.id.slice(0, 8))}
|
<div className="text-sm font-black text-gray-900">
|
||||||
</h3>
|
{toPersianDigits(new Date(selectedOrder.createdAt).toLocaleDateString('fa-IR', {
|
||||||
<p className="text-xs font-bold text-gray-400">
|
weekday: 'long',
|
||||||
ثبت شده در {toPersianDigits(new Date(selectedOrder.createdAt || selectedOrder.date).toLocaleDateString('fa-IR'))} - ساعت {toPersianDigits(new Date(selectedOrder.createdAt || selectedOrder.date).toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' }))}
|
year: 'numeric',
|
||||||
</p>
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => setSelectedOrder(null)}
|
{/* Status Selector */}
|
||||||
className="w-9 h-9 flex items-center justify-center rounded-xl bg-white border border-gray-200 text-gray-400 hover:text-red-500 transition-colors"
|
<div className="flex items-center gap-2">
|
||||||
>
|
<span className="text-xs font-bold text-gray-600">تغییر وضعیت:</span>
|
||||||
<X className="w-5 h-5" />
|
<select
|
||||||
</button>
|
disabled={isUpdatingModalStatus}
|
||||||
|
value={modalStatus}
|
||||||
|
onChange={(e) => handleSaveModalStatus(e.target.value)}
|
||||||
|
className={`text-xs font-bold px-3 py-2 rounded-xl border outline-none cursor-pointer transition-all shadow-xs ${
|
||||||
|
ORDER_STATUS_MAP[modalStatus]?.color || 'bg-white text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<option value="pending_payment">در انتظار پرداخت</option>
|
||||||
|
<option value="pending">در حال بررسی</option>
|
||||||
|
<option value="processing">در حال پردازش (جدید)</option>
|
||||||
|
<option value="packaged">بستهبندی شده (آماده ارسال)</option>
|
||||||
|
<option value="ready_to_ship">آماده ارسال</option>
|
||||||
|
<option value="shipped">ارسال شده</option>
|
||||||
|
<option value="delivered">تحویل داده شده</option>
|
||||||
|
<option value="cancelled">لغو شده</option>
|
||||||
|
</select>
|
||||||
|
{isUpdatingModalStatus && <Spinner size="sm" />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Body */}
|
|
||||||
<div className="p-8 overflow-y-auto space-y-8 custom-scrollbar">
|
{/* Customer & Address Details */}
|
||||||
{/* Customer & Shipping Info */}
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="grid md:grid-cols-2 gap-6 bg-gray-50 p-6 rounded-2xl border border-gray-100">
|
<div className="bg-gray-50 p-5 rounded-2xl border border-gray-100 space-y-3">
|
||||||
<div className="space-y-3">
|
<h4 className="text-xs font-black text-gray-400 uppercase tracking-widest flex items-center gap-2">
|
||||||
<h4 className="text-xs font-black text-gray-400 uppercase tracking-widest flex items-center gap-1.5">
|
<User className="w-4 h-4 text-purple-600" />
|
||||||
<User className="w-4 h-4 text-purple-600" />
|
مشخصات مشتری و گیرنده
|
||||||
اطلاعات خریدار
|
</h4>
|
||||||
</h4>
|
<div className="space-y-2 text-xs">
|
||||||
<div className="text-sm font-black text-gray-900">
|
<div className="flex justify-between">
|
||||||
{selectedOrder.user ? `${selectedOrder.user.firstName || ''} ${selectedOrder.user.lastName || ''}` : 'خریدار مهمان'}
|
<span className="text-gray-500">نام گیرنده:</span>
|
||||||
|
<span className="font-bold text-gray-900">
|
||||||
|
{selectedOrder.address?.recipientName ||
|
||||||
|
(selectedOrder.user?.firstName
|
||||||
|
? `${selectedOrder.user.firstName} ${selectedOrder.user.lastName}`
|
||||||
|
: 'مشتری کنینا')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs font-bold text-gray-500">
|
<div className="flex justify-between">
|
||||||
<Phone className="w-3.5 h-3.5 text-gray-400" />
|
<span className="text-gray-500">شماره تماس:</span>
|
||||||
<span dir="ltr">{toPersianDigits(selectedOrder.user?.phone || 'شماره ثبت نشده')}</span>
|
<span className="font-mono font-bold text-gray-900" dir="ltr">
|
||||||
|
{toPersianDigits(
|
||||||
|
selectedOrder.address?.recipientMobile ||
|
||||||
|
selectedOrder.user?.mobile ||
|
||||||
|
selectedOrder.user?.phone ||
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{selectedOrder.user?.email && (
|
{selectedOrder.user?.email && (
|
||||||
<div className="flex items-center gap-2 text-xs font-bold text-gray-500">
|
<div className="flex justify-between">
|
||||||
<Mail className="w-3.5 h-3.5 text-gray-400" />
|
<span className="text-gray-500">ایمیل:</span>
|
||||||
<span>{selectedOrder.user.email}</span>
|
<span className="font-mono text-gray-700">{selectedOrder.user.email}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h4 className="text-xs font-black text-gray-400 uppercase tracking-widest flex items-center gap-1.5">
|
|
||||||
<MapPin className="w-4 h-4 text-purple-600" />
|
|
||||||
آدرس ارسال سفارش
|
|
||||||
</h4>
|
|
||||||
<p className="text-xs font-bold text-gray-700 leading-relaxed">
|
|
||||||
{selectedOrder.shippingAddress || 'آدرس ثبتی کاربر در حساب کاربری'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status & Tracking Editor */}
|
<div className="bg-gray-50 p-5 rounded-2xl border border-gray-100 space-y-3">
|
||||||
<div className="bg-purple-50/50 border border-purple-100 p-6 rounded-2xl space-y-4">
|
<h4 className="text-xs font-black text-gray-400 uppercase tracking-widest flex items-center gap-2">
|
||||||
<h4 className="text-xs font-black text-purple-900 uppercase tracking-widest">
|
<Truck className="w-4 h-4 text-purple-600" />
|
||||||
مدیریت وضعیت و کد رهگیری پستی
|
اطلاعات پستی و ارسال
|
||||||
</h4>
|
</h4>
|
||||||
<div className="grid md:grid-cols-2 gap-4 items-end">
|
<div className="space-y-2 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1.5">تغییر وضعیت سفارش</label>
|
<span className="text-gray-500 block mb-1">آدرس کامل تحویل:</span>
|
||||||
<select
|
<span className="font-bold text-gray-900 leading-relaxed block">
|
||||||
value={modalStatus}
|
{selectedOrder.address?.province ? `${selectedOrder.address.province}، ` : ''}
|
||||||
onChange={(e) => setModalStatus(e.target.value)}
|
{selectedOrder.address?.city ? `${selectedOrder.address.city}، ` : ''}
|
||||||
className="w-full bg-white border border-gray-300 text-gray-900 text-sm font-bold rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500"
|
{selectedOrder.address?.fullAddress || selectedOrder.shippingAddress || 'ثبت نشده'}
|
||||||
>
|
</span>
|
||||||
<option value="pending_payment">در انتظار پرداخت / ناموفق</option>
|
|
||||||
<option value="processing">در حال پردازش</option>
|
|
||||||
<option value="packaged">بستهبندی شده (آماده ارسال)</option>
|
|
||||||
<option value="shipped">ارسال شده</option>
|
|
||||||
<option value="delivered">تحویل داده شده</option>
|
|
||||||
<option value="cancelled">لغو شده</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{selectedOrder.address?.postalCode && (
|
||||||
<label className="block text-xs font-bold text-gray-700 mb-1.5">کد رهگیری مرسوله پستی</label>
|
<div className="flex justify-between pt-1">
|
||||||
<div className="flex gap-2">
|
<span className="text-gray-500">کد پستی:</span>
|
||||||
|
<span className="font-mono font-bold text-gray-900">
|
||||||
|
{toPersianDigits(selectedOrder.address.postalCode)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Postal Tracking Code Input */}
|
||||||
|
<div className="pt-2 border-t border-gray-200">
|
||||||
|
<label className="block text-gray-500 font-bold mb-1">کد رهگیری پستی / تیپاکس:</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="مثلا: POST-9823478"
|
placeholder="مثلا: POST-9823478"
|
||||||
value={modalTrackingCode}
|
value={modalTrackingCode}
|
||||||
onChange={(e) => setModalTrackingCode(e.target.value)}
|
onChange={(e) => setModalTrackingCode(e.target.value)}
|
||||||
className="flex-1 bg-white border border-gray-300 text-gray-900 text-sm font-mono font-bold rounded-xl px-3 py-2.5 outline-none focus:ring-2 focus:ring-purple-500"
|
className="flex-1 bg-white border border-gray-300 text-gray-900 text-xs font-mono font-bold rounded-xl px-3 py-2 outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={handleSaveModalChanges}
|
onClick={handleSaveModalChanges}
|
||||||
disabled={isSavingTracking}
|
disabled={isSavingTracking}
|
||||||
className="bg-purple-600 text-white px-4 py-2.5 rounded-xl font-black text-xs flex items-center gap-1.5 hover:bg-purple-700 transition-all shrink-0 shadow-md shadow-purple-600/20"
|
className="bg-purple-600 text-white px-3 py-2 rounded-xl font-bold text-xs flex items-center gap-1 hover:bg-purple-700 transition-all shrink-0 cursor-pointer"
|
||||||
>
|
>
|
||||||
{isSavingTracking ? <Spinner size="sm" /> : <Save className="w-4 h-4" />}
|
{isSavingTracking ? <Spinner size="sm" /> : <Save className="w-3.5 h-3.5" />}
|
||||||
<span>ذخیره</span>
|
<span>ذخیره</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Payment Gateway Transactions Log */}
|
{/* Items Table */}
|
||||||
{selectedOrder.paymentTransactions && selectedOrder.paymentTransactions.length > 0 && (
|
<div>
|
||||||
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 space-y-3">
|
<h4 className="text-xs font-black text-gray-400 uppercase tracking-widest mb-3">
|
||||||
<h4 className="text-xs font-black text-slate-800 uppercase tracking-widest flex items-center gap-2">
|
اقلام خریده شده ({toPersianDigits((selectedOrder.orderItems || selectedOrder.items || []).length)})
|
||||||
<Clock className="w-4 h-4 text-purple-600" />
|
</h4>
|
||||||
لاگ تراکنشهای درگاه پرداخت زیبال / بانکی
|
<div className="space-y-2.5">
|
||||||
</h4>
|
{(selectedOrder.orderItems || selectedOrder.items || []).map((item: OrderItem, idx: number) => {
|
||||||
<div className="space-y-2 text-xs">
|
const pName = item.product?.nameFa || item.product?.name || item.name || 'محصول کنینا';
|
||||||
{selectedOrder.paymentTransactions.map((tx) => (
|
const pImg = item.product?.imageUrl || item.product?.image || '/products/product-placeholder.png';
|
||||||
<div key={tx.id} className="p-3 bg-white rounded-xl border border-slate-200 flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
const pPrice = Number(item.product?.priceValue || item.priceValue || 0);
|
||||||
<div className="space-y-1">
|
const qty = item.quantity || 1;
|
||||||
<div className="font-mono font-bold text-slate-700">
|
|
||||||
شناسه رهگیری زیبال (TrackID): <span className="text-purple-600">{tx.trackId || 'ثبت نشده'}</span>
|
return (
|
||||||
</div>
|
<div key={item.id || idx} className="flex items-center justify-between p-3.5 border border-gray-100 rounded-2xl bg-white">
|
||||||
{tx.refNumber && (
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-slate-600">
|
<div className="w-12 h-12 bg-gray-50 rounded-xl p-1 flex items-center justify-center border border-gray-100 shrink-0">
|
||||||
شماره ارجاع شاپرک (RRN): <span className="font-mono font-bold">{tx.refNumber}</span>
|
<img src={pImg} alt={pName} className="max-w-full max-h-full object-contain" />
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{tx.message && (
|
|
||||||
<div className="text-slate-500">
|
|
||||||
پیام درگاه: <span className="font-semibold">{tx.message}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div>
|
||||||
<span className={`inline-block px-2.5 py-1 rounded-lg font-bold text-[11px] ${
|
<div className="font-black text-xs sm:text-sm text-gray-900">{pName}</div>
|
||||||
tx.status === 'VERIFIED'
|
<div className="text-xs text-gray-400 font-bold">{toPersianDigits(qty)} عدد × {toPersianDigits(pPrice.toLocaleString())} تومان</div>
|
||||||
? 'bg-emerald-100 text-emerald-800'
|
|
||||||
: tx.status === 'PENDING'
|
|
||||||
? 'bg-amber-100 text-amber-800'
|
|
||||||
: 'bg-rose-100 text-rose-800'
|
|
||||||
}`}>
|
|
||||||
{tx.status === 'VERIFIED' ? 'پرداخت تایید شده' : tx.status === 'PENDING' ? 'در انتظار پرداخت' : 'پرداخت ناموفق'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className="text-left font-black text-purple-600 text-xs sm:text-sm">
|
||||||
</div>
|
{toPersianDigits((pPrice * qty).toLocaleString())} تومان
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Items Table */}
|
|
||||||
<div>
|
|
||||||
<h4 className="text-xs font-black text-gray-400 uppercase tracking-widest mb-4">
|
|
||||||
اقلام خریده شده ({toPersianDigits((selectedOrder.orderItems || selectedOrder.items || []).length)})
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{(selectedOrder.orderItems || selectedOrder.items || []).map((item: OrderItem, idx: number) => {
|
|
||||||
const pName = item.product?.nameFa || item.product?.name || item.name || 'محصول کنینا';
|
|
||||||
const pImg = item.product?.imageUrl || item.product?.image || '/products/product-placeholder.png';
|
|
||||||
const pPrice = Number(item.product?.priceValue || item.priceValue || 0);
|
|
||||||
const qty = item.quantity || 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={item.id || idx} className="flex items-center justify-between p-4 border border-gray-100 rounded-2xl bg-white">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-12 h-12 bg-gray-50 rounded-xl p-1 flex items-center justify-center border border-gray-100 shrink-0">
|
|
||||||
<img src={pImg} alt={pName} className="max-w-full max-h-full object-contain" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-black text-sm text-gray-900">{pName}</div>
|
|
||||||
<div className="text-xs text-gray-400 font-bold">{toPersianDigits(qty)} عدد × {toPersianDigits(pPrice.toLocaleString())} تومان</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-left font-black text-purple-600 text-sm">
|
|
||||||
{toPersianDigits((pPrice * qty).toLocaleString())} تومان
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
);
|
||||||
</div>
|
})}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Financial Summary */}
|
|
||||||
<div className="bg-gray-900 text-white p-6 rounded-2xl space-y-2">
|
|
||||||
<div className="flex justify-between text-xs font-bold text-gray-400">
|
|
||||||
<span>روش پرداخت:</span>
|
|
||||||
<span className="text-white font-bold">{selectedOrder.paymentMethod === 'wallet' ? 'کیف پول الکترونیک' : 'درگاه پرداخت آنلاین'}</span>
|
|
||||||
</div>
|
|
||||||
{selectedOrder.isRefill && (
|
|
||||||
<div className="flex justify-between text-xs font-bold text-green-400 bg-green-950/50 p-2 rounded-xl border border-green-800">
|
|
||||||
<span>رزرو هوشمند تمدید دوره (۵٪ پاداش خرید بعدی):</span>
|
|
||||||
<span>ثبت و رزرو گردید</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{Number(selectedOrder.charityDonation || 0) > 0 && (
|
|
||||||
<div className="flex justify-between text-xs font-bold text-pink-400">
|
|
||||||
<span>کمک اهدایی (ردپای مهربانی):</span>
|
|
||||||
<span>{toPersianDigits(Number(selectedOrder.charityDonation).toLocaleString())}+ تومان</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex justify-between text-base font-black text-amber-400 pt-3 border-t border-gray-800">
|
|
||||||
<span>مبلغ نهایی پرداخت شده:</span>
|
|
||||||
<span>{toPersianDigits(Number(selectedOrder.totalAmount || selectedOrder.total || 0).toLocaleString())} تومان</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Financial Summary */}
|
||||||
<div className="p-6 bg-gray-50 border-t border-gray-200 flex flex-wrap gap-3 shrink-0">
|
<div className="bg-gray-900 text-white p-5 rounded-2xl space-y-2 text-xs">
|
||||||
<Button
|
<div className="flex justify-between font-bold text-gray-400">
|
||||||
variant="primary"
|
<span>روش پرداخت:</span>
|
||||||
startIcon={Download}
|
<span className="text-white font-bold">{selectedOrder.paymentMethod === 'wallet' ? 'کیف پول الکترونیک' : 'درگاه پرداخت آنلاین'}</span>
|
||||||
onClick={() => handlePrintInvoice(selectedOrder)}
|
</div>
|
||||||
className="flex-1"
|
<div className="flex justify-between text-sm sm:text-base font-black text-amber-400 pt-2 border-t border-gray-800">
|
||||||
>
|
<span>مبلغ نهایی پرداخت شده:</span>
|
||||||
دانلود و چاپ فاکتور رسمی
|
<span>{toPersianDigits(Number(selectedOrder.totalAmount || selectedOrder.total || 0).toLocaleString())} تومان</span>
|
||||||
</Button>
|
</div>
|
||||||
|
|
||||||
{(selectedOrder.status === 'cancelled' || selectedOrder.status === 'pending_payment') && (
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
startIcon={Undo2}
|
|
||||||
onClick={() => {
|
|
||||||
const ord = selectedOrder;
|
|
||||||
setSelectedOrder(null);
|
|
||||||
openOrderRefundModal(ord);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
استرداد وجه سفارش
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setSelectedOrder(null)}
|
|
||||||
>
|
|
||||||
بستن
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Order Refund Modal (Wallet / Zibal Gateway) */}
|
{/* Order Refund Modal (Wallet / Zibal Gateway) */}
|
||||||
{refundModalOrder && (
|
{refundModalOrder && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm font-vazir" dir="rtl">
|
<Modal
|
||||||
<div className="bg-white rounded-3xl w-full max-w-lg border border-gray-200 shadow-2xl p-6 sm:p-8 space-y-5">
|
isOpen={!!refundModalOrder}
|
||||||
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
|
onClose={() => setRefundModalOrder(null)}
|
||||||
<h3 className="text-base font-black text-gray-900 flex items-center gap-2">
|
title={`استرداد وجه سفارش #${refundModalOrder.trackingNumber || refundModalOrder.id.slice(0, 8)}`}
|
||||||
<Undo2 className="w-5 h-5 text-rose-600" />
|
icon={Undo2}
|
||||||
استرداد وجه سفارش #{refundModalOrder.trackingNumber || refundModalOrder.id.slice(0, 8)}
|
maxWidth="lg"
|
||||||
</h3>
|
footer={
|
||||||
<button
|
<div className="flex justify-end gap-2 w-full">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
onClick={() => setRefundModalOrder(null)}
|
onClick={() => setRefundModalOrder(null)}
|
||||||
className="text-gray-400 hover:text-gray-700 font-bold text-lg cursor-pointer"
|
|
||||||
>
|
>
|
||||||
✕
|
انصراف
|
||||||
</button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={refundTarget === 'wallet' ? 'primary' : 'danger'}
|
||||||
|
size="sm"
|
||||||
|
isLoading={isProcessingRefund}
|
||||||
|
onClick={handleExecuteOrderRefund}
|
||||||
|
>
|
||||||
|
{refundTarget === 'wallet' ? 'تایید و افزایش اعتبار کیف پول' : 'ارسال درخواست استرداد به زیبال'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-4 text-xs font-vazir">
|
||||||
|
{/* Destination selector */}
|
||||||
|
<div>
|
||||||
|
<label className="block font-bold text-gray-700 mb-2">روش عودت و بازگشت وجه:</label>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRefundTarget('wallet')}
|
||||||
|
className={`p-3.5 rounded-2xl border text-right transition-all cursor-pointer flex flex-col gap-1.5 ${
|
||||||
|
refundTarget === 'wallet'
|
||||||
|
? 'border-purple-600 bg-purple-50/60 ring-2 ring-purple-600/20'
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 font-black text-gray-900">
|
||||||
|
<Wallet className="w-4 h-4 text-purple-600" />
|
||||||
|
<span>شارژ کیف پول کاربر</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-gray-500 font-medium leading-relaxed">
|
||||||
|
افزایش فوری اعتبار کیف پول جهت خریدهای بعدی مشتری
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRefundTarget('zibal')}
|
||||||
|
className={`p-3.5 rounded-2xl border text-right transition-all cursor-pointer flex flex-col gap-1.5 ${
|
||||||
|
refundTarget === 'zibal'
|
||||||
|
? 'border-rose-600 bg-rose-50/60 ring-2 ring-rose-600/20'
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 font-black text-gray-900">
|
||||||
|
<CreditCard className="w-4 h-4 text-rose-600" />
|
||||||
|
<span>استرداد شاپرک (زیبال)</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-gray-500 font-medium leading-relaxed">
|
||||||
|
برگشت مستقیم به کارت بانکی مشتری از طریق درگاه پرداخت
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 text-xs">
|
<div>
|
||||||
{/* Destination selector */}
|
<label className="block font-bold text-gray-700 mb-1">مبلغ استرداد به تومان</label>
|
||||||
<div>
|
<input
|
||||||
<label className="block font-bold text-gray-700 mb-2">روش عودت و بازگشت وجه:</label>
|
type="number"
|
||||||
<div className="grid grid-cols-2 gap-3">
|
value={refundAmount}
|
||||||
<button
|
onChange={(e) => setRefundAmount(e.target.value)}
|
||||||
type="button"
|
placeholder="مبلغ استرداد"
|
||||||
onClick={() => setRefundTarget('wallet')}
|
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={`p-4 rounded-2xl border text-right transition-all cursor-pointer flex flex-col gap-1.5 ${
|
dir="ltr"
|
||||||
refundTarget === 'wallet'
|
/>
|
||||||
? 'border-purple-600 bg-purple-50/60 ring-2 ring-purple-600/20'
|
</div>
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 font-black text-gray-900">
|
|
||||||
<Wallet className="w-4 h-4 text-purple-600" />
|
|
||||||
<span>شارژ کیف پول کاربر</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-[11px] text-gray-500 font-medium leading-relaxed">
|
|
||||||
افزایش فوری اعتبار کیف پول جهت خریدهای بعدی مشتری
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<div>
|
||||||
type="button"
|
<label className="block font-bold text-gray-700 mb-1">علت استرداد</label>
|
||||||
onClick={() => setRefundTarget('zibal')}
|
<textarea
|
||||||
className={`p-4 rounded-2xl border text-right transition-all cursor-pointer flex flex-col gap-1.5 ${
|
rows={2}
|
||||||
refundTarget === 'zibal'
|
value={refundReason}
|
||||||
? 'border-rose-600 bg-rose-50/60 ring-2 ring-rose-600/20'
|
onChange={(e) => setRefundReason(e.target.value)}
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
placeholder="علت لغو سفارش یا مرجوعی کالا..."
|
||||||
}`}
|
className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
>
|
/>
|
||||||
<div className="flex items-center gap-2 font-black text-gray-900">
|
|
||||||
<CreditCard className="w-4 h-4 text-rose-600" />
|
|
||||||
<span>استرداد شاپرک (زیبال)</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-[11px] text-gray-500 font-medium leading-relaxed">
|
|
||||||
برگشت مستقیم به کارت بانکی مشتری از طریق درگاه پرداخت
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block font-bold text-gray-700 mb-1">مبلغ استرداد به تومان</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
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"
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block font-bold text-gray-700 mb-1">علت استرداد</label>
|
|
||||||
<textarea
|
|
||||||
rows={2}
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pt-3 border-t border-gray-100">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setRefundModalOrder(null)}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={refundTarget === 'wallet' ? 'primary' : 'danger'}
|
|
||||||
size="sm"
|
|
||||||
isLoading={isProcessingRefund}
|
|
||||||
onClick={handleExecuteOrderRefund}
|
|
||||||
>
|
|
||||||
{refundTarget === 'wallet' ? 'تایید و افزایش اعتبار کیف پول' : 'ارسال درخواست استرداد به زیبال'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -619,11 +619,11 @@ export default function Products() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4 bg-gray-900/60 backdrop-blur-xs font-vazir animate-in fade-in duration-200" dir="rtl">
|
||||||
<div className="bg-white rounded-2xl w-full max-w-4xl max-h-[90vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
|
<div className="bg-white rounded-3xl w-full max-w-4xl max-h-[92dvh] sm:max-h-[88vh] flex flex-col shadow-2xl overflow-hidden border border-gray-100 animate-in zoom-in-95 duration-200">
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
<div className="px-5 sm:px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-white shrink-0">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h3 className="text-xl font-bold text-gray-900">
|
<h3 className="text-base sm:text-lg font-black text-gray-900">
|
||||||
{editingProduct ? 'ویرایش پیشرفته محصول' : 'افزودن محصول جدید'}
|
{editingProduct ? 'ویرایش پیشرفته محصول' : 'افزودن محصول جدید'}
|
||||||
</h3>
|
</h3>
|
||||||
{editingProduct && formData.slug && (
|
{editingProduct && formData.slug && (
|
||||||
@ -631,18 +631,22 @@ export default function Products() {
|
|||||||
href={getStoreProductUrl(formData.slug)}
|
href={getStoreProductUrl(formData.slug)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-purple-50 border border-purple-100 text-purple-600 hover:bg-purple-100 transition-all cursor-pointer"
|
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-bold bg-purple-50 border border-purple-100 text-purple-600 hover:bg-purple-100 transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
مشاهده آنلاین در سایت 🔗
|
مشاهده در سایت 🔗
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
<button
|
||||||
<X className="w-6 h-6" />
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
className="w-8 h-8 rounded-xl bg-gray-50 text-gray-400 hover:text-gray-700 hover:bg-gray-100 flex items-center justify-center transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex border-b border-gray-100 px-6 pt-2 gap-4">
|
<div className="flex border-b border-gray-100 px-5 sm:px-6 pt-2 gap-3 sm:gap-4 overflow-x-auto shrink-0 bg-gray-50/50">
|
||||||
{[
|
{[
|
||||||
{ id: 'general', label: 'اطلاعات پایه' },
|
{ id: 'general', label: 'اطلاعات پایه' },
|
||||||
{ id: 'pricing', label: 'موجودی و قیمت' },
|
{ id: 'pricing', label: 'موجودی و قیمت' },
|
||||||
@ -653,16 +657,17 @@ export default function Products() {
|
|||||||
key={tab.id}
|
key={tab.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
className={`pb-3 px-2 text-sm font-bold border-b-2 transition-colors ${activeTab === tab.id ? 'border-purple-600 text-purple-700' : 'border-transparent text-gray-500 hover:text-gray-700'}`}
|
className={`pb-3 px-2 text-xs sm:text-sm font-bold border-b-2 whitespace-nowrap transition-colors cursor-pointer ${activeTab === tab.id ? 'border-purple-600 text-purple-700' : 'border-transparent text-gray-500 hover:text-gray-700'}`}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 overflow-y-auto h-[550px] min-h-[550px] max-h-[550px] bg-gray-50/30">
|
<div className="p-4 sm:p-6 overflow-y-auto flex-1 overscroll-contain bg-gray-50/30">
|
||||||
<form id="productForm" onSubmit={handleSave} className="space-y-6">
|
<form id="productForm" onSubmit={handleSave} className="space-y-6">
|
||||||
|
|
||||||
|
|
||||||
{/* General Tab */}
|
{/* General Tab */}
|
||||||
<div className={activeTab === 'general' ? 'block space-y-4' : 'hidden'}>
|
<div className={activeTab === 'general' ? 'block space-y-4' : 'hidden'}>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -5,22 +5,21 @@ import {
|
|||||||
Edit3,
|
Edit3,
|
||||||
Trash2,
|
Trash2,
|
||||||
Plus,
|
Plus,
|
||||||
Check,
|
|
||||||
X,
|
|
||||||
Eye,
|
Eye,
|
||||||
Wallet,
|
Wallet,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Mail,
|
Mail,
|
||||||
Phone,
|
Phone,
|
||||||
Lock,
|
Lock,
|
||||||
UserCheck,
|
|
||||||
Shield,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Skeleton from '../components/ui/Skeleton';
|
import Skeleton from '../components/ui/Skeleton';
|
||||||
import Spinner from '../components/ui/Spinner';
|
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
|
import Button from '../components/ui/Button';
|
||||||
|
import Modal from '../components/ui/Modal';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import type { UserAddress, PetSummary } from '../types/admin';
|
import type { UserAddress, PetSummary } from '../types/admin';
|
||||||
|
|
||||||
@ -438,299 +437,284 @@ export default function Users() {
|
|||||||
|
|
||||||
{/* CREATE USER MODAL */}
|
{/* CREATE USER MODAL */}
|
||||||
{showCreateModal && (
|
{showCreateModal && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-xs">
|
<Modal
|
||||||
<form
|
isOpen={showCreateModal}
|
||||||
onSubmit={handleCreateUser}
|
onClose={() => setShowCreateModal(false)}
|
||||||
className="bg-white rounded-2xl w-full max-w-lg shadow-2xl p-6 space-y-5 font-vazir animate-in fade-in zoom-in duration-150"
|
title="افزودن کاربر جدید"
|
||||||
>
|
icon={Plus}
|
||||||
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
|
maxWidth="lg"
|
||||||
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
footer={
|
||||||
<Plus className="w-5 h-5 text-purple-600" />
|
<div className="flex justify-end gap-2 w-full">
|
||||||
افزودن کاربر جدید
|
<Button
|
||||||
</h3>
|
variant="outline"
|
||||||
<button
|
size="sm"
|
||||||
type="button"
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
onClick={() => setShowCreateModal(false)}
|
||||||
className="text-gray-400 hover:text-red-500 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-xs">
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">
|
|
||||||
نام <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
placeholder="مثال: علی"
|
|
||||||
value={createFormData.firstName}
|
|
||||||
onChange={(e) => setCreateFormData({ ...createFormData, firstName: e.target.value })}
|
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">
|
|
||||||
نام خانوادگی <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
placeholder="مثال: احمدی"
|
|
||||||
value={createFormData.lastName}
|
|
||||||
onChange={(e) => setCreateFormData({ ...createFormData, lastName: e.target.value })}
|
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">
|
|
||||||
شماره موبایل <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Phone className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" />
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
required
|
|
||||||
placeholder="09123456789"
|
|
||||||
value={createFormData.mobile}
|
|
||||||
onChange={(e) => setCreateFormData({ ...createFormData, mobile: e.target.value })}
|
|
||||||
className="w-full pl-3 pr-9 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">ایمیل (اختیاری)</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Mail className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" />
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
placeholder="user@example.com"
|
|
||||||
value={createFormData.email}
|
|
||||||
onChange={(e) => setCreateFormData({ ...createFormData, email: e.target.value })}
|
|
||||||
className="w-full pl-3 pr-9 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">رمز عبور (اختیاری)</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Lock className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" />
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="حداقل ۶ کاراکتر"
|
|
||||||
value={createFormData.password}
|
|
||||||
onChange={(e) => setCreateFormData({ ...createFormData, password: e.target.value })}
|
|
||||||
className="w-full pl-3 pr-9 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-left"
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">نقش دسترسی</label>
|
|
||||||
<select
|
|
||||||
value={createFormData.role}
|
|
||||||
onChange={(e) => setCreateFormData({ ...createFormData, role: e.target.value })}
|
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold bg-white"
|
|
||||||
>
|
|
||||||
<option value="User_PetOwner">مشتری عادی</option>
|
|
||||||
<option value="User_Wholesale">خریدار عمده (داروخانه/پتشاپ)</option>
|
|
||||||
<option value="User_B2B">همکار (B2B)</option>
|
|
||||||
<option value="ADMIN">مدیر سیستم (Admin)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-2">
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">موجودی اولیه کیف پول (تومان)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1000"
|
|
||||||
value={createFormData.walletBalance}
|
|
||||||
onChange={(e) => setCreateFormData({ ...createFormData, walletBalance: Number(e.target.value) })}
|
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 border-t border-gray-100 flex justify-end gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
className="px-5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-xl transition-colors text-xs cursor-pointer"
|
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
variant="primary"
|
||||||
disabled={isSaving}
|
size="sm"
|
||||||
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-bold rounded-xl transition-colors text-xs flex items-center gap-2 cursor-pointer disabled:opacity-50"
|
isLoading={isSaving}
|
||||||
|
onClick={handleCreateUser}
|
||||||
>
|
>
|
||||||
{isSaving ? <Spinner size="sm" /> : <Check className="w-4 h-4" />}
|
|
||||||
ایجاد کاربر
|
ایجاد کاربر
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
}
|
||||||
</div>
|
>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs font-vazir">
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">
|
||||||
|
نام <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="مثال: علی"
|
||||||
|
value={createFormData.firstName}
|
||||||
|
onChange={(e) => setCreateFormData({ ...createFormData, firstName: e.target.value })}
|
||||||
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">
|
||||||
|
نام خانوادگی <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="مثال: احمدی"
|
||||||
|
value={createFormData.lastName}
|
||||||
|
onChange={(e) => setCreateFormData({ ...createFormData, lastName: e.target.value })}
|
||||||
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">
|
||||||
|
شماره موبایل <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Phone className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" />
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
required
|
||||||
|
placeholder="09123456789"
|
||||||
|
value={createFormData.mobile}
|
||||||
|
onChange={(e) => setCreateFormData({ ...createFormData, mobile: e.target.value })}
|
||||||
|
className="w-full pl-3 pr-9 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">ایمیل (اختیاری)</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" />
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="user@example.com"
|
||||||
|
value={createFormData.email}
|
||||||
|
onChange={(e) => setCreateFormData({ ...createFormData, email: e.target.value })}
|
||||||
|
className="w-full pl-3 pr-9 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">رمز عبور (اختیاری)</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="حداقل ۶ کاراکتر"
|
||||||
|
value={createFormData.password}
|
||||||
|
onChange={(e) => setCreateFormData({ ...createFormData, password: e.target.value })}
|
||||||
|
className="w-full pl-3 pr-9 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-left"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">نقش دسترسی</label>
|
||||||
|
<select
|
||||||
|
value={createFormData.role}
|
||||||
|
onChange={(e) => setCreateFormData({ ...createFormData, role: e.target.value })}
|
||||||
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold bg-white"
|
||||||
|
>
|
||||||
|
<option value="User_PetOwner">مشتری عادی</option>
|
||||||
|
<option value="User_Wholesale">خریدار عمده (داروخانه/پتشاپ)</option>
|
||||||
|
<option value="User_B2B">همکار (B2B)</option>
|
||||||
|
<option value="ADMIN">مدیر سیستم (Admin)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">موجودی اولیه کیف پول (تومان)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="1000"
|
||||||
|
value={createFormData.walletBalance}
|
||||||
|
onChange={(e) => setCreateFormData({ ...createFormData, walletBalance: Number(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* EDIT USER MODAL */}
|
{/* EDIT USER MODAL */}
|
||||||
{editUser && (
|
{editUser && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-xs">
|
<Modal
|
||||||
<form
|
isOpen={!!editUser}
|
||||||
onSubmit={handleSaveUser}
|
onClose={() => setEditUser(null)}
|
||||||
className="bg-white rounded-2xl w-full max-w-lg shadow-2xl p-6 space-y-5 font-vazir animate-in fade-in zoom-in duration-150"
|
title="ویرایش اطلاعات کاربر"
|
||||||
>
|
icon={Edit3}
|
||||||
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
|
maxWidth="lg"
|
||||||
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
footer={
|
||||||
<Edit3 className="w-5 h-5 text-blue-600" />
|
<div className="flex justify-end gap-2 w-full">
|
||||||
ویرایش اطلاعات کاربر
|
<Button
|
||||||
</h3>
|
variant="outline"
|
||||||
<button
|
size="sm"
|
||||||
type="button"
|
|
||||||
onClick={() => setEditUser(null)}
|
onClick={() => setEditUser(null)}
|
||||||
className="text-gray-400 hover:text-red-500 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-xs">
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">نام</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={editFormData.firstName}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, firstName: e.target.value })}
|
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">نام خانوادگی</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={editFormData.lastName}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, lastName: e.target.value })}
|
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">شماره موبایل</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
required
|
|
||||||
value={editFormData.mobile}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, mobile: e.target.value })}
|
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">ایمیل</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={editFormData.email}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })}
|
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">رمز عبور جدید (در صورت نیاز به تغییر)</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="بدون تغییر..."
|
|
||||||
value={editFormData.password}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, password: e.target.value })}
|
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-left"
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">نقش دسترسی</label>
|
|
||||||
<select
|
|
||||||
value={editFormData.role}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, role: e.target.value })}
|
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold bg-white"
|
|
||||||
>
|
|
||||||
<option value="User_PetOwner">مشتری عادی</option>
|
|
||||||
<option value="User_Wholesale">خریدار عمده (داروخانه/پتشاپ)</option>
|
|
||||||
<option value="User_B2B">همکار (B2B)</option>
|
|
||||||
<option value="ADMIN">مدیر سیستم (Admin)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-2">
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">موجودی کیف پول (تومان)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1000"
|
|
||||||
value={editFormData.walletBalance}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, walletBalance: Number(e.target.value) })}
|
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 border-t border-gray-100 flex justify-end gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setEditUser(null)}
|
|
||||||
className="px-5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-xl transition-colors text-xs cursor-pointer"
|
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
variant="primary"
|
||||||
disabled={isSaving}
|
size="sm"
|
||||||
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-bold rounded-xl transition-colors text-xs flex items-center gap-2 cursor-pointer disabled:opacity-50"
|
isLoading={isSaving}
|
||||||
|
onClick={handleSaveUser}
|
||||||
>
|
>
|
||||||
{isSaving ? <Spinner size="sm" /> : <Check className="w-4 h-4" />}
|
|
||||||
ذخیره تغییرات
|
ذخیره تغییرات
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
}
|
||||||
</div>
|
>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs font-vazir">
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">نام</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={editFormData.firstName}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, firstName: e.target.value })}
|
||||||
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">نام خانوادگی</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={editFormData.lastName}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, lastName: e.target.value })}
|
||||||
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">شماره موبایل</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
required
|
||||||
|
value={editFormData.mobile}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, mobile: e.target.value })}
|
||||||
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">ایمیل</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={editFormData.email}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })}
|
||||||
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">رمز عبور جدید (در صورت نیاز به تغییر)</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="بدون تغییر..."
|
||||||
|
value={editFormData.password}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, password: e.target.value })}
|
||||||
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-left"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">نقش دسترسی</label>
|
||||||
|
<select
|
||||||
|
value={editFormData.role}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, role: e.target.value })}
|
||||||
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold bg-white"
|
||||||
|
>
|
||||||
|
<option value="User_PetOwner">مشتری عادی</option>
|
||||||
|
<option value="User_Wholesale">خریدار عمده (داروخانه/پتشاپ)</option>
|
||||||
|
<option value="User_B2B">همکار (B2B)</option>
|
||||||
|
<option value="ADMIN">مدیر سیستم (Admin)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label className="block text-gray-700 font-bold mb-1">موجودی کیف پول (تومان)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="1000"
|
||||||
|
value={editFormData.walletBalance}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, walletBalance: Number(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* WALLET ADJUST MODAL */}
|
{/* WALLET ADJUST MODAL */}
|
||||||
{walletUser && (
|
{walletUser && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-xs">
|
<Modal
|
||||||
<form
|
isOpen={!!walletUser}
|
||||||
onSubmit={handleAdjustWallet}
|
onClose={() => setWalletUser(null)}
|
||||||
className="bg-white rounded-2xl w-full max-w-md shadow-2xl p-6 space-y-5 font-vazir animate-in fade-in zoom-in duration-150"
|
title="مدیریت کیف پول کاربر"
|
||||||
>
|
icon={Wallet}
|
||||||
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
|
maxWidth="md"
|
||||||
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
footer={
|
||||||
<Wallet className="w-5 h-5 text-emerald-600" />
|
<div className="flex justify-end gap-2 w-full">
|
||||||
مدیریت کیف پول کاربر
|
<Button
|
||||||
</h3>
|
variant="outline"
|
||||||
<button
|
size="sm"
|
||||||
type="button"
|
|
||||||
onClick={() => setWalletUser(null)}
|
onClick={() => setWalletUser(null)}
|
||||||
className="text-gray-400 hover:text-red-500 transition-colors cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
انصراف
|
||||||
</button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
isLoading={isAdjustingWallet}
|
||||||
|
onClick={handleAdjustWallet}
|
||||||
|
>
|
||||||
|
ثبت تغییر موجودی
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div className="bg-gray-50 p-3.5 rounded-xl border border-gray-100 text-xs space-y-1">
|
>
|
||||||
|
<div className="space-y-4 text-xs font-vazir">
|
||||||
|
<div className="bg-gray-50 p-3.5 rounded-2xl border border-gray-100 space-y-1">
|
||||||
<div className="font-bold text-gray-900">
|
<div className="font-bold text-gray-900">
|
||||||
{walletUser.firstName} {walletUser.lastName} ({walletUser.mobile})
|
{walletUser.firstName} {walletUser.lastName} ({walletUser.mobile})
|
||||||
</div>
|
</div>
|
||||||
@ -742,9 +726,9 @@ export default function Users() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 text-xs">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gray-700 font-bold mb-1">نوع عملیات</label>
|
<label className="block text-gray-700 font-bold mb-1.5">نوع عملیات</label>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -806,154 +790,126 @@ export default function Users() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="pt-4 border-t border-gray-100 flex justify-end gap-2">
|
</Modal>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setWalletUser(null)}
|
|
||||||
className="px-5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-xl transition-colors text-xs cursor-pointer"
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isAdjustingWallet}
|
|
||||||
className="px-6 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white font-bold rounded-xl transition-colors text-xs flex items-center gap-2 cursor-pointer disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isAdjustingWallet ? <Spinner size="sm" /> : <Check className="w-4 h-4" />}
|
|
||||||
ثبت تغییر موجودی
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* DELETE CONFIRMATION MODAL */}
|
{/* DELETE CONFIRMATION MODAL */}
|
||||||
{userToDelete && (
|
{userToDelete && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-xs">
|
<Modal
|
||||||
<div className="bg-white rounded-2xl w-full max-w-md shadow-2xl p-6 space-y-5 font-vazir animate-in fade-in zoom-in duration-150">
|
isOpen={!!userToDelete}
|
||||||
<div className="flex items-center gap-3 text-red-600 border-b border-gray-100 pb-4">
|
onClose={() => setUserToDelete(null)}
|
||||||
<div className="p-2.5 bg-red-100 rounded-xl">
|
title="تایید حذف کاربر"
|
||||||
<AlertTriangle className="w-6 h-6" />
|
icon={AlertTriangle}
|
||||||
</div>
|
maxWidth="md"
|
||||||
<div>
|
footer={
|
||||||
<h3 className="text-base font-bold text-gray-900">تایید حذف کاربر</h3>
|
<div className="flex justify-end gap-2 w-full">
|
||||||
<p className="text-xs text-gray-500 mt-0.5">این عملیات غیرقابل بازگشت است</p>
|
<Button
|
||||||
</div>
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setUserToDelete(null)}
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
isLoading={isDeleting}
|
||||||
|
onClick={handleDeleteUser}
|
||||||
|
>
|
||||||
|
حذف قطعی کاربر
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<p className="text-xs text-gray-700 leading-relaxed">
|
>
|
||||||
|
<div className="space-y-3 text-xs font-vazir">
|
||||||
|
<p className="text-gray-700 leading-relaxed">
|
||||||
آیا از حذف کامل کاربر{' '}
|
آیا از حذف کامل کاربر{' '}
|
||||||
<strong className="text-gray-900">
|
<strong className="text-gray-900">
|
||||||
{userToDelete.firstName} {userToDelete.lastName} ({userToDelete.mobile})
|
{userToDelete.firstName} {userToDelete.lastName} ({userToDelete.mobile})
|
||||||
</strong>{' '}
|
</strong>{' '}
|
||||||
اطمینان دارید؟ تمام سوابق آدرسها، پتها و تراکنشهای مربوطه نیز پاک خواهند شد.
|
اطمینان دارید؟ تمام سوابق آدرسها، پتها و تراکنشهای مربوطه نیز پاک خواهند شد.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="pt-4 border-t border-gray-100 flex justify-end gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setUserToDelete(null)}
|
|
||||||
className="px-5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-xl transition-colors text-xs cursor-pointer"
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleDeleteUser}
|
|
||||||
disabled={isDeleting}
|
|
||||||
className="px-6 py-2.5 bg-red-600 hover:bg-red-700 text-white font-bold rounded-xl transition-colors text-xs flex items-center gap-2 cursor-pointer disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isDeleting ? <Spinner size="sm" /> : <Trash2 className="w-4 h-4" />}
|
|
||||||
حذف قطعی کاربر
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* VIEW USER DETAILS MODAL */}
|
{/* VIEW USER DETAILS MODAL */}
|
||||||
{viewUser && (
|
{viewUser && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-xs">
|
<Modal
|
||||||
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl p-6 space-y-6 font-vazir animate-in fade-in zoom-in duration-150 max-h-[90vh] overflow-y-auto">
|
isOpen={!!viewUser}
|
||||||
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
|
onClose={() => setViewUser(null)}
|
||||||
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
title="مشاهده جزئیات پروفایل کاربر"
|
||||||
<UsersIcon className="w-5 h-5 text-purple-600" />
|
icon={UsersIcon}
|
||||||
مشاهده جزئیات پروفایل کاربر
|
maxWidth="lg"
|
||||||
</h3>
|
footer={
|
||||||
<button
|
<div className="flex justify-end w-full">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
onClick={() => setViewUser(null)}
|
onClick={() => setViewUser(null)}
|
||||||
className="text-gray-400 hover:text-red-500 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4 text-xs">
|
|
||||||
<div className="grid grid-cols-2 gap-4 bg-gray-50 p-4 rounded-xl border border-gray-100">
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-400 font-bold block mb-1">نام و نام خانوادگی:</span>
|
|
||||||
<span className="font-bold text-gray-900 text-sm">
|
|
||||||
{viewUser.firstName} {viewUser.lastName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-400 font-bold block mb-1">نوع نقش:</span>
|
|
||||||
<span className="font-bold text-purple-700">{viewUser.role}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-400 font-bold block mb-1">موبایل:</span>
|
|
||||||
<span className="font-mono text-gray-800" dir="ltr">
|
|
||||||
{viewUser.mobile || '---'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-400 font-bold block mb-1">ایمیل:</span>
|
|
||||||
<span className="font-mono text-gray-800" dir="ltr">
|
|
||||||
{viewUser.email || '---'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-purple-50/60 p-4 rounded-xl border border-purple-100 flex items-center justify-between">
|
|
||||||
<span className="font-bold text-purple-900">موجودی کیف پول کاربر:</span>
|
|
||||||
<span className="font-mono font-black text-purple-700 text-base">
|
|
||||||
{(Number(viewUser.walletBalance) || 0).toLocaleString('fa-IR')} تومان
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-bold text-gray-900 mb-2">آدرسهای ثبتشده ({viewUser.addresses?.length || 0})</h4>
|
|
||||||
{viewUser.addresses && viewUser.addresses.length > 0 ? (
|
|
||||||
<div className="space-y-2 max-h-32 overflow-y-auto">
|
|
||||||
{viewUser.addresses.map((a: UserAddress, idx: number) => (
|
|
||||||
<div key={idx} className="p-2.5 bg-gray-50 rounded-lg border border-gray-200/60">
|
|
||||||
<p className="font-bold text-gray-800">
|
|
||||||
{a.title} - {a.receptorName}
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-500 mt-0.5">
|
|
||||||
{a.province}، {a.city}، {a.detail}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-400 text-center py-2 bg-gray-50 rounded-lg">هیچ آدرسی ثبت نشده است.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 border-t border-gray-100 flex justify-end">
|
|
||||||
<button
|
|
||||||
onClick={() => setViewUser(null)}
|
|
||||||
className="px-5 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-xl transition-colors text-xs cursor-pointer"
|
|
||||||
>
|
>
|
||||||
بستن پنجره
|
بستن پنجره
|
||||||
</button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-4 text-xs font-vazir">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 bg-gray-50 p-4 rounded-2xl border border-gray-100">
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400 font-bold block mb-1">نام و نام خانوادگی:</span>
|
||||||
|
<span className="font-bold text-gray-900 text-sm">
|
||||||
|
{viewUser.firstName} {viewUser.lastName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400 font-bold block mb-1">نوع نقش:</span>
|
||||||
|
<span className="font-bold text-purple-700">{viewUser.role}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400 font-bold block mb-1">موبایل:</span>
|
||||||
|
<span className="font-mono text-gray-800" dir="ltr">
|
||||||
|
{viewUser.mobile || '---'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400 font-bold block mb-1">ایمیل:</span>
|
||||||
|
<span className="font-mono text-gray-800" dir="ltr">
|
||||||
|
{viewUser.email || '---'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-purple-50/60 p-4 rounded-2xl border border-purple-100 flex items-center justify-between">
|
||||||
|
<span className="font-bold text-purple-900">موجودی کیف پول کاربر:</span>
|
||||||
|
<span className="font-mono font-black text-purple-700 text-base">
|
||||||
|
{(Number(viewUser.walletBalance) || 0).toLocaleString('fa-IR')} تومان
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-gray-900 mb-2">آدرسهای ثبتشده ({viewUser.addresses?.length || 0})</h4>
|
||||||
|
{viewUser.addresses && viewUser.addresses.length > 0 ? (
|
||||||
|
<div className="space-y-2 max-h-36 overflow-y-auto">
|
||||||
|
{viewUser.addresses.map((a: UserAddress, idx: number) => (
|
||||||
|
<div key={idx} className="p-2.5 bg-gray-50 rounded-xl border border-gray-200/60">
|
||||||
|
<p className="font-bold text-gray-800">
|
||||||
|
{a.title} - {a.receptorName}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 mt-0.5">
|
||||||
|
{a.province}، {a.city}، {a.detail}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-400 text-center py-3 bg-gray-50 rounded-xl">هیچ آدرسی ثبت نشده است.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,6 +44,8 @@ const Reviews = lazyWithRetry(() => import('../pages/Reviews'));
|
|||||||
const DoctorsManager = lazyWithRetry(() => import('../pages/DoctorsManager'));
|
const DoctorsManager = lazyWithRetry(() => import('../pages/DoctorsManager'));
|
||||||
const FAQManager = lazyWithRetry(() => import('../pages/FAQManager'));
|
const FAQManager = lazyWithRetry(() => import('../pages/FAQManager'));
|
||||||
const MenuManager = lazyWithRetry(() => import('../pages/MenuManager'));
|
const MenuManager = lazyWithRetry(() => import('../pages/MenuManager'));
|
||||||
|
const MonitoringPage = lazyWithRetry(() => import('../pages/MonitoringPage'));
|
||||||
|
|
||||||
|
|
||||||
export interface AdminRouteConfig {
|
export interface AdminRouteConfig {
|
||||||
path: string;
|
path: string;
|
||||||
@ -103,8 +105,10 @@ export const router = createBrowserRouter([
|
|||||||
{ path: 'reviews/*', element: <Reviews /> },
|
{ path: 'reviews/*', element: <Reviews /> },
|
||||||
{ path: 'faq/*', element: <FAQManager /> },
|
{ path: 'faq/*', element: <FAQManager /> },
|
||||||
{ path: 'menu/*', element: <MenuManager /> },
|
{ path: 'menu/*', element: <MenuManager /> },
|
||||||
|
{ path: 'monitoring/*', element: <MonitoringPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
132
frontend/admin-panel/src/utils/useTableParams.ts
Normal file
132
frontend/admin-panel/src/utils/useTableParams.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
export interface UseTableParamsOptions {
|
||||||
|
defaultSortBy?: string;
|
||||||
|
defaultSortOrder?: 'asc' | 'desc';
|
||||||
|
defaultPage?: number;
|
||||||
|
defaultLimit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTableParams(options: UseTableParamsOptions = {}) {
|
||||||
|
const {
|
||||||
|
defaultSortBy = 'createdAt',
|
||||||
|
defaultSortOrder = 'desc',
|
||||||
|
defaultPage = 1,
|
||||||
|
defaultLimit = 15,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
// Current extracted params
|
||||||
|
const search = searchParams.get('search') || '';
|
||||||
|
const status = searchParams.get('status') || '';
|
||||||
|
const gateway = searchParams.get('gateway') || '';
|
||||||
|
const category = searchParams.get('category') || '';
|
||||||
|
const role = searchParams.get('role') || '';
|
||||||
|
const type = searchParams.get('type') || '';
|
||||||
|
const fromDate = searchParams.get('fromDate') || '';
|
||||||
|
const toDate = searchParams.get('toDate') || '';
|
||||||
|
const sortBy = searchParams.get('sortBy') || defaultSortBy;
|
||||||
|
const sortOrder = (searchParams.get('sortOrder') as 'asc' | 'desc') || defaultSortOrder;
|
||||||
|
const page = parseInt(searchParams.get('page') || String(defaultPage), 10) || defaultPage;
|
||||||
|
const limit = parseInt(searchParams.get('limit') || String(defaultLimit), 10) || defaultLimit;
|
||||||
|
|
||||||
|
// Set single or multiple params while preserving others and resetting page when filtering
|
||||||
|
const setParam = useCallback(
|
||||||
|
(key: string, value: string | number | null | undefined, resetPage = true) => {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
const next = new URLSearchParams(prev);
|
||||||
|
if (value === null || value === undefined || value === '' || value === 'ALL' || value === 'all') {
|
||||||
|
next.delete(key);
|
||||||
|
} else {
|
||||||
|
next.set(key, String(value));
|
||||||
|
}
|
||||||
|
if (resetPage && key !== 'page') {
|
||||||
|
next.set('page', '1');
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setSearchParams]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setMultipleParams = useCallback(
|
||||||
|
(updates: Record<string, string | number | null | undefined>, resetPage = true) => {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
const next = new URLSearchParams(prev);
|
||||||
|
Object.entries(updates).forEach(([key, value]) => {
|
||||||
|
if (value === null || value === undefined || value === '' || value === 'ALL' || value === 'all') {
|
||||||
|
next.delete(key);
|
||||||
|
} else {
|
||||||
|
next.set(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (resetPage && !('page' in updates)) {
|
||||||
|
next.set('page', '1');
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setSearchParams]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Helper to toggle sort column
|
||||||
|
const handleSort = useCallback(
|
||||||
|
(column: string) => {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
const next = new URLSearchParams(prev);
|
||||||
|
const currentSortBy = next.get('sortBy') || defaultSortBy;
|
||||||
|
const currentSortOrder = next.get('sortOrder') || defaultSortOrder;
|
||||||
|
|
||||||
|
if (currentSortBy === column) {
|
||||||
|
next.set('sortOrder', currentSortOrder === 'asc' ? 'desc' : 'asc');
|
||||||
|
} else {
|
||||||
|
next.set('sortBy', column);
|
||||||
|
next.set('sortOrder', 'desc');
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setSearchParams, defaultSortBy, defaultSortOrder]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearFilters = useCallback(() => {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
const next = new URLSearchParams();
|
||||||
|
if (prev.has('sortBy')) next.set('sortBy', prev.get('sortBy')!);
|
||||||
|
if (prev.has('sortOrder')) next.set('sortOrder', prev.get('sortOrder')!);
|
||||||
|
next.set('page', '1');
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [setSearchParams]);
|
||||||
|
|
||||||
|
const paramsObject = useMemo(() => {
|
||||||
|
const res: Record<string, string> = {};
|
||||||
|
searchParams.forEach((val, key) => {
|
||||||
|
res[key] = val;
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchParams,
|
||||||
|
paramsObject,
|
||||||
|
search,
|
||||||
|
status,
|
||||||
|
gateway,
|
||||||
|
category,
|
||||||
|
role,
|
||||||
|
type,
|
||||||
|
fromDate,
|
||||||
|
toDate,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
setParam,
|
||||||
|
setMultipleParams,
|
||||||
|
handleSort,
|
||||||
|
clearFilters,
|
||||||
|
};
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,328 +1,316 @@
|
|||||||
{
|
{
|
||||||
"0": "OrdersService",
|
"0": "Roles",
|
||||||
"1": "productService.ts",
|
"1": "app.module.ts",
|
||||||
"2": "CmsController",
|
"2": "SettingsController",
|
||||||
"3": "app.module.ts",
|
"3": "useCartStore",
|
||||||
"4": "ReviewsService",
|
"4": "SafeImage.tsx",
|
||||||
"5": "tickets.controller.ts",
|
"5": "CmsController",
|
||||||
"6": "useSettingsStore",
|
"6": "tickets.controller.ts",
|
||||||
"7": "MediaSelector.tsx",
|
"7": "PaginationDto",
|
||||||
"8": "PetProfile.tsx",
|
"8": "admin.module.ts",
|
||||||
"9": "Roles",
|
"9": "devDependencies",
|
||||||
"10": "DoctorsService",
|
"10": "ReviewsService",
|
||||||
"11": "MenuService",
|
"11": "api",
|
||||||
"12": "adminRoutes.tsx",
|
"12": "PetProfile.tsx",
|
||||||
"13": "PrismaService",
|
"13": "app-audit-verification.e2e-spec.js",
|
||||||
"14": "PetsController",
|
"14": "lib/services/api.ts",
|
||||||
"15": "ProductsService",
|
"15": "src/services/api.ts",
|
||||||
"16": "UserDashboard.tsx",
|
"16": "DoctorQueryDto",
|
||||||
"17": "CreateVideoDto",
|
"17": "admin.service.ts",
|
||||||
"18": "src/services/api.ts",
|
"18": "JwtAuthGuard",
|
||||||
"19": "AuthController",
|
"19": "ProductsService",
|
||||||
"20": "BE-001",
|
"20": "CreateVideoDto",
|
||||||
"21": "SettingsController",
|
"21": "SmsService",
|
||||||
"22": "FE-001",
|
"22": "UserDashboard.tsx",
|
||||||
"23": "ADM-001",
|
"23": "MenuService",
|
||||||
"24": "DB-001",
|
"24": "BE-001",
|
||||||
"25": "TS-001",
|
"25": "FE-001",
|
||||||
"26": "TEST-001",
|
"26": "ADM-001",
|
||||||
"27": "DEVOPS-001",
|
"27": "DB-001",
|
||||||
"28": "DOC-001",
|
"28": "TS-001",
|
||||||
"29": "WholesaleApplyDto",
|
"29": "TEST-001",
|
||||||
"30": "app-audit-verification.e2e-spec.js",
|
"30": "DEVOPS-001",
|
||||||
"31": "JwtAuthGuard",
|
"31": "DOC-001",
|
||||||
"32": "ZibalService",
|
"32": "Spinner.tsx",
|
||||||
"33": "راهنمای تست سیستم (Software Testing)",
|
"33": "WholesaleApplyDto",
|
||||||
"34": "CategoriesController",
|
"34": "B2BService",
|
||||||
"35": "B2BService",
|
"35": "auth.controller.ts",
|
||||||
"36": "What You Must Do When Invoked",
|
"36": "FaqService",
|
||||||
"37": "PaymentService",
|
"37": "راهنمای تست سیستم (Software Testing)",
|
||||||
"38": "UsersService",
|
"38": "Transactions.tsx",
|
||||||
"39": "What You Must Do When Invoked",
|
"39": "CategoriesController",
|
||||||
"40": "SslController",
|
"40": "MediaController",
|
||||||
"41": "PodcastPlayerModal.tsx",
|
"41": "What You Must Do When Invoked",
|
||||||
"42": "IngredientsService",
|
"42": "SslController",
|
||||||
"43": "FaqService",
|
"43": "BannersService",
|
||||||
"44": "MediaController",
|
"44": "TestimonialsService",
|
||||||
"45": "Coupons.tsx",
|
"45": "What You Must Do When Invoked",
|
||||||
"46": "ContactService",
|
"46": "Body",
|
||||||
"47": "zibal.service.ts",
|
"47": "IngredientsService",
|
||||||
"48": "prescriptions.module.ts",
|
"48": "adminRoutes.tsx",
|
||||||
"49": "SmartAdvisorService",
|
"49": "devDependencies",
|
||||||
"50": "TestimonialsService",
|
"50": "devDependencies",
|
||||||
"51": "Role & Core Objective",
|
"51": "BlogsController",
|
||||||
"52": "SmsService",
|
"52": "prescriptions.module.ts",
|
||||||
"53": "ZibalEBankService",
|
"53": "SmartAdvisorService",
|
||||||
"54": "compilerOptions",
|
"54": "UsersService",
|
||||||
"55": "Media.tsx",
|
"55": "UITexts.tsx",
|
||||||
"56": "compilerOptions",
|
"56": "SettingsService",
|
||||||
"57": "PetsController",
|
"57": "Role & Core Objective",
|
||||||
"58": "PaginationDto",
|
"58": "ContactService",
|
||||||
"59": "dependencies",
|
"59": "compilerOptions",
|
||||||
"60": "compilerOptions",
|
"60": "CreateUserDto",
|
||||||
"61": "toPersian",
|
"61": "CreateEBankCheckoutDto",
|
||||||
"62": "BlogsController",
|
"62": "CreateOrderDto",
|
||||||
"63": "ApiOperation",
|
"63": "dependencies",
|
||||||
"64": "BannersService",
|
"64": "compilerOptions",
|
||||||
"65": "Required Review Group Closures",
|
"65": "ProductPage.tsx",
|
||||||
"66": "Products.tsx",
|
"66": "AdminController",
|
||||||
"67": "Operational Rules & Boundaries",
|
"67": "PetsController",
|
||||||
"68": "Operational Rules & Boundaries",
|
"68": "BlogsController",
|
||||||
"69": "WikiController",
|
"69": "Required Review Group Closures",
|
||||||
"70": "admin.service.ts",
|
"70": "compilerOptions",
|
||||||
"71": "getSeoConfig",
|
"71": "getPageMetadata",
|
||||||
"72": "seo.module.ts",
|
"72": "Operational Rules & Boundaries",
|
||||||
"73": "seo.ts",
|
"73": "Operational Rules & Boundaries",
|
||||||
"74": "🏢 AI Software Agency — Master Orchestration Protocol v3",
|
"74": "WikiController",
|
||||||
"75": "Operational Rules & Boundaries",
|
"75": "PetsController",
|
||||||
"76": "Operational Rules & Boundaries",
|
"76": "CreateReviewDto",
|
||||||
"77": "scripts",
|
"77": "seo.module.ts",
|
||||||
"78": "Role & Core Objective",
|
"78": "Param",
|
||||||
"79": "Transactions.tsx",
|
"79": "🏢 AI Software Agency — Master Orchestration Protocol v3",
|
||||||
"80": "B2BPortal.tsx",
|
"80": "Operational Rules & Boundaries",
|
||||||
"81": "devDependencies",
|
"81": "Operational Rules & Boundaries",
|
||||||
"82": "auth.controller.ts",
|
"82": "scripts",
|
||||||
"83": "Orders.tsx",
|
"83": "dependencies",
|
||||||
"84": "devDependencies",
|
"84": "Role & Core Objective",
|
||||||
"85": "seed-products.ts",
|
"85": "sms.service.ts",
|
||||||
"86": "Reconciled Audit Roles & Assignments",
|
"86": "zibal.service.ts",
|
||||||
"87": "WikiController",
|
"87": "dependencies",
|
||||||
"88": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
|
"88": "HomeClient.tsx",
|
||||||
"89": "dependencies",
|
"89": "seed-products.ts",
|
||||||
"90": "ProductPage.tsx",
|
"90": "SmsLogQueryDto",
|
||||||
"91": "compilerOptions",
|
"91": "Reconciled Audit Roles & Assignments",
|
||||||
"92": "scripts",
|
"92": "OrdersService",
|
||||||
"93": "Deep Audit Summary Report",
|
"93": "admin.controller.ts",
|
||||||
"94": "dependencies",
|
"94": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
|
||||||
"95": "Operational Rules & Boundaries",
|
"95": "getSeoConfig",
|
||||||
"96": "exclude",
|
"96": "compilerOptions",
|
||||||
"97": "jest",
|
"97": "prisma",
|
||||||
"98": "Param",
|
"98": "scripts",
|
||||||
"99": "Comprehensive Change Log",
|
"99": "trust-seals/page.tsx",
|
||||||
"100": "Operational Rules & Boundaries",
|
"100": "Deep Audit Summary Report",
|
||||||
"101": "UITexts.tsx",
|
"101": "Operational Rules & Boundaries",
|
||||||
"102": "BlogsController",
|
"102": "jest",
|
||||||
"103": "payment.controller.ts",
|
"103": "Comprehensive Change Log",
|
||||||
"104": ".initiateOrderPayment",
|
"104": "Coupons.tsx",
|
||||||
"105": "1. Summary of Integrity Repairs Performed",
|
"105": "Operational Rules & Boundaries",
|
||||||
"106": "@nestjs/cli",
|
"106": "catalog/page.tsx",
|
||||||
"107": "Operational Rules & Boundaries",
|
"107": "wiki/page.tsx",
|
||||||
"108": "Operational Rules & Boundaries",
|
"108": "PrismaService",
|
||||||
"109": "Operational Rules & Boundaries",
|
"109": "1. Summary of Integrity Repairs Performed",
|
||||||
"110": "AppService",
|
"110": "Operational Rules & Boundaries",
|
||||||
"111": "SmsSettingsPage.tsx",
|
"111": "Operational Rules & Boundaries",
|
||||||
"112": "SettingsService",
|
"112": "Operational Rules & Boundaries",
|
||||||
"113": "Vazirmatn Changelog",
|
"113": "ProductDto",
|
||||||
"114": "Vazirmatn Font فونت وزیرمتن",
|
"114": "AppService",
|
||||||
"115": "Operational Rules & Boundaries",
|
"115": "helmet",
|
||||||
"116": "compilerOptions",
|
"116": "Vazirmatn Changelog",
|
||||||
"117": "compilerOptions",
|
"117": "Vazirmatn Font فونت وزیرمتن",
|
||||||
"118": "backend/README.md",
|
"118": "Operational Rules & Boundaries",
|
||||||
"119": "eslint",
|
"119": "compilerOptions",
|
||||||
"120": "Repository Map",
|
"120": "compilerOptions",
|
||||||
"121": "validate_integrity.js",
|
"121": "backend/README.md",
|
||||||
"122": "admin-panel/package.json",
|
"122": ".adjustWallet",
|
||||||
"123": "Sahel-Font",
|
"123": "js-yaml",
|
||||||
"124": "Spinner.tsx",
|
"124": "Repository Map",
|
||||||
"125": "Sahel-Font",
|
"125": "validate_integrity.js",
|
||||||
"126": "Role & Core Objective",
|
"126": "admin-panel/package.json",
|
||||||
"127": "orchestrate.py",
|
"127": "Sahel-Font",
|
||||||
"128": "backend/package.json",
|
"128": "@nestjs/core",
|
||||||
"129": "blog/page.tsx",
|
"129": "seo.ts",
|
||||||
"130": "graphify reference: extra exports and benchmark",
|
"130": "Sahel-Font",
|
||||||
"131": "Phase 2 Final Quality Gate Summary Report",
|
"131": "Role & Core Objective",
|
||||||
"132": "Task Modifications Log",
|
"132": "orchestrate.py",
|
||||||
"133": "Install",
|
"133": "backend/package.json",
|
||||||
"134": "ErrorBoundary",
|
"134": "@nestjs/throttler",
|
||||||
"135": "application/package.json",
|
"135": "graphify reference: extra exports and benchmark",
|
||||||
"136": "generate-openapi.js",
|
"136": "Phase 2 Final Quality Gate Summary Report",
|
||||||
"137": "contact/page.tsx",
|
"137": "Task Modifications Log",
|
||||||
"138": "HomeController",
|
"138": "Install",
|
||||||
"139": "System Discovery",
|
"139": "RouteErrorBoundary",
|
||||||
"140": "Product Requirement Document (PRD)",
|
"140": "ErrorBoundary",
|
||||||
"141": "DoctorQueryDto",
|
"141": "application/package.json",
|
||||||
"142": "lib/services/api.ts",
|
"142": "start-dev.js",
|
||||||
"143": "RedisService",
|
"143": "generate-openapi.js",
|
||||||
"144": "Baseline Command Plan & Reconciled Command History",
|
"144": "AdminService",
|
||||||
"145": "catalog/page.tsx",
|
"145": "eslint-config-prettier",
|
||||||
"146": "ErrorPages.tsx",
|
"146": "System Discovery",
|
||||||
"147": "with-vpn.sh",
|
"147": "Media.tsx",
|
||||||
"148": "Architecture Specification",
|
"148": "@eslint/eslintrc",
|
||||||
"149": "Project Health Audit Report",
|
"149": "SmsSettingsPage.tsx",
|
||||||
"150": "nest-cli.json",
|
"150": "Product Requirement Document (PRD)",
|
||||||
"151": "prettier",
|
"151": "jest",
|
||||||
"152": "graphify reference: query, path, explain",
|
"152": "@nestjs/cli",
|
||||||
"153": "Open Questions",
|
"153": "exclude",
|
||||||
"154": "Final Phase 2 Audit Closure Report",
|
"154": "Baseline Command Plan & Reconciled Command History",
|
||||||
"155": "prisma",
|
"155": "@nestjs/schematics",
|
||||||
"156": "open-browsers.js",
|
"156": "contact/page.tsx",
|
||||||
"157": "start-dev.js",
|
"157": "ErrorPages.tsx",
|
||||||
"158": "📝 Active Agent Working Scratchpad",
|
"158": "@nestjs/testing",
|
||||||
"159": "🔍 Code Health Audit Review (01_auditor)",
|
"159": "with-vpn.sh",
|
||||||
"160": "paginated-response.schema.ts",
|
"160": "Architecture Specification",
|
||||||
"161": "Vazirmatn Font README",
|
"161": "Project Health Audit Report",
|
||||||
"162": "Omitted File Inspection Report",
|
"162": "nest-cli.json",
|
||||||
"163": "Phase 3.2 / 3.3 — Implementation Readiness & Architectural Finalization Report",
|
"163": "graphify reference: query, path, explain",
|
||||||
"164": "Phase 3 Audit Traceability Matrix",
|
"164": "Open Questions",
|
||||||
"165": "rebuild_honest_ledger.js",
|
"165": "Final Phase 2 Audit Closure Report",
|
||||||
"166": "validate_evidence_grade.js",
|
"166": "open-browsers.js",
|
||||||
"167": "supertest",
|
"167": "📝 Active Agent Working Scratchpad",
|
||||||
"168": "app/page.tsx",
|
"168": "🔍 Code Health Audit Review (01_auditor)",
|
||||||
"169": "API Contract Specification",
|
"169": "paginated-response.schema.ts",
|
||||||
"170": "⚙️ Backend Technical Review (05_dev_backend)",
|
"170": "Vazirmatn Font README",
|
||||||
"171": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
|
"171": "Omitted File Inspection Report",
|
||||||
"172": "🚀 SEO & Content Strategy Review (12_seo_content)",
|
"172": "Phase 3.2 / 3.3 — Implementation Readiness & Architectural Finalization Report",
|
||||||
"173": "@types/node",
|
"173": "Phase 3 Audit Traceability Matrix",
|
||||||
"174": "reviews.controller.ts",
|
"174": "rebuild_honest_ledger.js",
|
||||||
"175": "seed-ui-texts.ts",
|
"175": "validate_evidence_grade.js",
|
||||||
"176": "seed-wiki.ts",
|
"176": "Reviews.tsx",
|
||||||
"177": "update-blog.dto.ts",
|
"177": "blog/page.tsx",
|
||||||
"178": "update-home.dto.ts",
|
"178": "prettier",
|
||||||
"179": "update-wiki.dto.ts",
|
"179": "PodcastPlayerModal.tsx",
|
||||||
"180": "graphify reference: add a URL and watch a folder",
|
"180": "API Contract Specification",
|
||||||
"181": "graphify reference: commit hook and native CLAUDE.md integration",
|
"181": "⚙️ Backend Technical Review (05_dev_backend)",
|
||||||
"182": "graphify reference: incremental update and cluster-only",
|
"182": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
|
||||||
"183": "Raw Finding Verification & Disposition Report",
|
"183": "🚀 SEO & Content Strategy Review (12_seo_content)",
|
||||||
"184": "React + TypeScript + Vite",
|
"184": "eslint",
|
||||||
"185": "Select.tsx",
|
"185": "@types/node",
|
||||||
"186": "RegisterDto",
|
"186": "seed-ui-texts.ts",
|
||||||
"187": "videos.controller.ts",
|
"187": "seed-wiki.ts",
|
||||||
"188": "application/README.md",
|
"188": "update-blog.dto.ts",
|
||||||
"189": "@types/express",
|
"189": "update-home.dto.ts",
|
||||||
"190": "deploy.sh",
|
"190": "update-wiki.dto.ts",
|
||||||
"191": "🔒 Security & Performance Review (09_devops_security)",
|
"191": "graphify reference: add a URL and watch a folder",
|
||||||
"192": "👁️ UX & Persona Interface Review (08_visual_qa)",
|
"192": "graphify reference: commit hook and native CLAUDE.md integration",
|
||||||
"193": "@types/jest",
|
"193": "graphify reference: incremental update and cluster-only",
|
||||||
"194": "typescript-eslint",
|
"194": "Raw Finding Verification & Disposition Report",
|
||||||
"195": "CreateOrderDto",
|
"195": "React + TypeScript + Vite",
|
||||||
"196": "prisma/scientificTerms.ts",
|
"196": "Select.tsx",
|
||||||
"197": "seed-blogs.ts",
|
"197": "source-map-support",
|
||||||
"198": "seed-custom.ts",
|
"198": "videos/page.tsx",
|
||||||
"199": "@types/js-yaml",
|
"199": "useSettingsStore",
|
||||||
"200": "graphify reference: GitHub clone and cross-repo merge",
|
"200": "application/README.md",
|
||||||
"201": "graphify reference: transcribe video and audio",
|
"201": "deploy.sh",
|
||||||
"202": "Compiler Diagnostic Dispositions",
|
"202": "🔒 Security & Performance Review (09_devops_security)",
|
||||||
"203": "Master Task Backlog (Phase 3.3)",
|
"203": "👁️ UX & Persona Interface Review (08_visual_qa)",
|
||||||
"204": "build_manifest.js",
|
"204": "supertest",
|
||||||
"205": "generate_classification.js",
|
"205": "prisma/scientificTerms.ts",
|
||||||
"206": "generate_evidence.js",
|
"206": "seed-blogs.ts",
|
||||||
"207": "generate_ledger.js",
|
"207": "seed-custom.ts",
|
||||||
"208": "generate_manifest.js",
|
"208": "graphify reference: GitHub clone and cross-repo merge",
|
||||||
"209": "sync_honest_manifest.js",
|
"209": "graphify reference: transcribe video and audio",
|
||||||
"210": "sync_manifest.js",
|
"210": "Compiler Diagnostic Dispositions",
|
||||||
"211": "@types/multer",
|
"211": "Master Task Backlog (Phase 3.3)",
|
||||||
"212": "videos/page.tsx",
|
"212": "build_manifest.js",
|
||||||
"213": "@testing-library/jest-dom",
|
"213": "generate_classification.js",
|
||||||
"214": "bcryptjs",
|
"214": "generate_evidence.js",
|
||||||
"215": "AdminService",
|
"215": "generate_ledger.js",
|
||||||
"216": "FormField.tsx",
|
"216": "generate_manifest.js",
|
||||||
"217": "Input.tsx",
|
"217": "sync_honest_manifest.js",
|
||||||
"218": "Textarea.tsx",
|
"218": "sync_manifest.js",
|
||||||
"219": "admin-panel/tsconfig.json",
|
"219": "FormField.tsx",
|
||||||
"220": "getPageMetadata",
|
"220": "Input.tsx",
|
||||||
"221": "api",
|
"221": "Textarea.tsx",
|
||||||
"222": "next.config.ts",
|
"222": "admin-panel/tsconfig.json",
|
||||||
"223": "Shabnam Font README",
|
"223": "ts-jest",
|
||||||
"224": "AGENTS.md",
|
"224": "dashboard/page.tsx",
|
||||||
"225": "rules/graphify.md",
|
"225": "next.config.ts",
|
||||||
"226": ".agents/workflows/graphify.md",
|
"226": "Shabnam Font README",
|
||||||
"227": "instructions.md",
|
"227": "AGENTS.md",
|
||||||
"228": "eslint-plugin-react-hooks",
|
"228": "rules/graphify.md",
|
||||||
"229": "eslint-plugin-react-refresh",
|
"229": ".agents/workflows/graphify.md",
|
||||||
"230": "@tailwindcss/postcss",
|
"230": "instructions.md",
|
||||||
"231": "typescript",
|
"231": "bcryptjs",
|
||||||
"232": "ProductDto",
|
"232": "ts-loader",
|
||||||
"233": "@testing-library/react",
|
"233": "ts-node",
|
||||||
"234": "@types/react",
|
"234": "tsconfig-paths",
|
||||||
"235": "sms.service.ts",
|
"235": "@nestjs/jwt",
|
||||||
"236": "vitest",
|
"236": "@types/bcrypt",
|
||||||
"237": "axios",
|
"237": "@nestjs/swagger",
|
||||||
"238": "AdminController",
|
"238": "passport-jwt",
|
||||||
"239": "app.e2e-spec.js",
|
"239": "@prisma/client",
|
||||||
"240": "ts-loader",
|
"240": "swagger-ui-express",
|
||||||
"241": "auth.service.ts",
|
"241": "blog.entity.ts",
|
||||||
"242": "@types/bcrypt",
|
"242": "home.entity.ts",
|
||||||
"243": "SmsLogQueryDto",
|
"243": "wiki.entity.ts",
|
||||||
"244": "blog.entity.ts",
|
"244": "User Profile Photo",
|
||||||
"245": "home.entity.ts",
|
"245": "CLAUDE.md",
|
||||||
"246": "wiki.entity.ts",
|
"246": ".claude/CLAUDE.md",
|
||||||
"247": "User Profile Photo",
|
"247": "extraction-spec.md",
|
||||||
"248": "CLAUDE.md",
|
"248": "Products Table",
|
||||||
"249": ".claude/CLAUDE.md",
|
"249": "Users Table",
|
||||||
"250": "extraction-spec.md",
|
"250": "Architectural Audit Findings",
|
||||||
"251": "Products Table",
|
"251": "Cross Boundary Dependencies & Backend Architecture Specification",
|
||||||
"252": "Users Table",
|
"252": "Next.js Agent Rules & Brand Guidelines",
|
||||||
"253": "Architectural Audit Findings",
|
"253": "robots.ts",
|
||||||
"254": "Cross Boundary Dependencies & Backend Architecture Specification",
|
"254": "application/eslint.config.mjs",
|
||||||
"255": "Next.js Agent Rules & Brand Guidelines",
|
"255": "postcss.config.mjs",
|
||||||
"256": "robots.ts",
|
"256": "vitest.setup.ts",
|
||||||
"257": "application/eslint.config.mjs",
|
"257": "backup_db.sh",
|
||||||
"258": "postcss.config.mjs",
|
"258": "start.sh",
|
||||||
"259": "vitest.setup.ts",
|
"259": "reviews/README.md",
|
||||||
"260": "backup_db.sh",
|
"260": "backend/eslint.config.mjs",
|
||||||
"261": "start.sh",
|
"261": "User Login API",
|
||||||
"262": "reviews/README.md",
|
"262": "User Logout API",
|
||||||
"263": "backend/eslint.config.mjs",
|
"263": "generate-openapi.d.ts",
|
||||||
"264": "User Login API",
|
"264": "@types/bcryptjs",
|
||||||
"265": "User Logout API",
|
"265": "@types/express",
|
||||||
"266": "generate-openapi.d.ts",
|
"266": "@types/jest",
|
||||||
"267": "Canina Pharma GmbH",
|
"267": "@types/js-yaml",
|
||||||
"268": "Pets Table",
|
"268": "@types/multer",
|
||||||
"269": "Canina Iran Project Introduction",
|
"269": "eslint-plugin-react-hooks",
|
||||||
"270": "Developer Standards and Architecture",
|
"270": "app-audit-verification.e2e-spec.d.ts",
|
||||||
"271": "Frontend & Admin Architecture Route Map Specification",
|
"271": "app.e2e-spec.d.ts",
|
||||||
"272": "Project Backlog and Tasks",
|
"272": "Canina Pharma GmbH",
|
||||||
"273": "eslint.config.js",
|
"273": "Pets Table",
|
||||||
"274": "postcss.config.js",
|
"274": "Canina Iran Project Introduction",
|
||||||
"275": "admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
"275": "Developer Standards and Architecture",
|
||||||
"276": "shabnam-font-v5.0.1/CHANGELOG.md",
|
"276": "Frontend & Admin Architecture Route Map Specification",
|
||||||
"277": "tailwind.config.js",
|
"277": "Project Backlog and Tasks",
|
||||||
"278": "vite.config.ts",
|
"278": "eslint.config.js",
|
||||||
"279": "application/CLAUDE.md",
|
"279": "postcss.config.js",
|
||||||
"280": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
"280": "admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
||||||
"281": "Sahel Font Sample",
|
"281": "shabnam-font-v5.0.1/CHANGELOG.md",
|
||||||
"282": "Shabnam Font Changelog",
|
"282": "tailwind.config.js",
|
||||||
"283": "Vazirmatn Changelog",
|
"283": "vite.config.ts",
|
||||||
"284": "vitest.config.ts",
|
"284": "application/CLAUDE.md",
|
||||||
"285": "Sahel Font Variable Sample",
|
"285": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
|
||||||
"286": "Shabnam Font Sample",
|
"286": "Sahel Font Sample",
|
||||||
"287": "Production Docker Compose",
|
"287": "Shabnam Font Changelog",
|
||||||
"288": "Staging Docker Compose",
|
"288": "Vazirmatn Changelog",
|
||||||
"289": "tailwindcss",
|
"289": "vitest.config.ts",
|
||||||
"290": "eslint-plugin-prettier",
|
"290": "Sahel Font Variable Sample",
|
||||||
"291": "helmet",
|
"291": "Shabnam Font Sample",
|
||||||
"292": "MetricsController",
|
"292": "Production Docker Compose",
|
||||||
"293": "AppModule",
|
"293": "Staging Docker Compose",
|
||||||
"294": "app-audit-verification.e2e-spec.d.ts",
|
"294": "ZibalService",
|
||||||
"295": "app.e2e-spec.d.ts",
|
"295": "eslint-plugin-react-refresh",
|
||||||
"296": "SendOtpDto",
|
"296": "tailwindcss",
|
||||||
"297": "@nestjs/core",
|
"297": "ZibalEBankService",
|
||||||
"298": "eslint-config-prettier",
|
"298": ".initiateOrderPayment",
|
||||||
"299": "@nestjs/jwt",
|
"299": "@tailwindcss/postcss",
|
||||||
"300": "@nestjs/swagger",
|
"300": "typescript",
|
||||||
"301": "useCartStore",
|
"301": "app.e2e-spec.js",
|
||||||
"302": "@nestjs/throttler",
|
"302": "typescript-eslint",
|
||||||
"303": "shop/page.tsx",
|
"303": "@testing-library/jest-dom",
|
||||||
"304": "checkout/page.tsx",
|
"304": "@testing-library/react",
|
||||||
"305": "RouteErrorBoundary",
|
"305": "@types/react",
|
||||||
"306": "devDependencies",
|
"306": "globals",
|
||||||
"307": "passport-jwt",
|
"307": "@types/react-dom",
|
||||||
"308": "@prisma/client",
|
"308": "vitest",
|
||||||
"309": "reflect-metadata",
|
"309": "axios",
|
||||||
"310": "admin.module.ts",
|
"310": "tailwindcss",
|
||||||
"311": "@eslint/eslintrc",
|
|
||||||
"312": "tailwindcss",
|
|
||||||
"313": "swagger-ui-express",
|
|
||||||
"314": "@eslint/js",
|
"314": "@eslint/js",
|
||||||
"315": "typescript",
|
"315": "typescript",
|
||||||
"316": "jest",
|
"324": "typescript-eslint"
|
||||||
"317": "@nestjs/schematics",
|
|
||||||
"318": "@nestjs/testing",
|
|
||||||
"319": "source-map-support",
|
|
||||||
"320": "ts-jest",
|
|
||||||
"321": "ts-node",
|
|
||||||
"322": "tsconfig-paths",
|
|
||||||
"323": "@types/bcryptjs",
|
|
||||||
"324": "typescript-eslint",
|
|
||||||
"325": "eslint-config-next"
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
4948
graphify-out/2026-08-23/.graphify_analysis.json
Normal file
4948
graphify-out/2026-08-23/.graphify_analysis.json
Normal file
File diff suppressed because it is too large
Load Diff
1
graphify-out/2026-08-23/.graphify_labels.json
Normal file
1
graphify-out/2026-08-23/.graphify_labels.json
Normal file
File diff suppressed because one or more lines are too long
1
graphify-out/2026-08-23/.graphify_semantic_marker
Normal file
1
graphify-out/2026-08-23/.graphify_semantic_marker
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"output_tokens": 7105}
|
||||||
1141
graphify-out/2026-08-23/GRAPH_REPORT.md
Normal file
1141
graphify-out/2026-08-23/GRAPH_REPORT.md
Normal file
File diff suppressed because it is too large
Load Diff
127320
graphify-out/2026-08-23/graph.json
Normal file
127320
graphify-out/2026-08-23/graph.json
Normal file
File diff suppressed because it is too large
Load Diff
3470
graphify-out/2026-08-23/manifest.json
Normal file
3470
graphify-out/2026-08-23/manifest.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_claude_skills_graphify_references_update_md", "label": "update.md", "file_type": "document", "source_file": ".claude/skills/graphify/references/update.md", "source_location": "L1"}, {"id": "$graphify-root$_claude_skills_graphify_references_update_graphify_reference_incremental_update_and_cluster_only", "label": "graphify reference: incremental update and cluster-only", "file_type": "document", "source_file": ".claude/skills/graphify/references/update.md", "source_location": "L1"}, {"id": "$graphify-root$_claude_skills_graphify_references_update_for_update_incremental_re_extraction", "label": "For --update (incremental re-extraction)", "file_type": "document", "source_file": ".claude/skills/graphify/references/update.md", "source_location": "L5"}, {"id": "$graphify-root$_claude_skills_graphify_references_update_for_cluster_only", "label": "For --cluster-only", "file_type": "document", "source_file": ".claude/skills/graphify/references/update.md", "source_location": "L202"}], "edges": [{"source": "$graphify-root$_claude_skills_graphify_references_update_md", "target": "$graphify-root$_claude_skills_graphify_references_update_graphify_reference_incremental_update_and_cluster_only", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/update.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_claude_skills_graphify_references_update_graphify_reference_incremental_update_and_cluster_only", "target": "$graphify-root$_claude_skills_graphify_references_update_for_update_incremental_re_extraction", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/update.md", "source_location": "L5", "weight": 1.0}, {"source": "$graphify-root$_claude_skills_graphify_references_update_graphify_reference_incremental_update_and_cluster_only", "target": "$graphify-root$_claude_skills_graphify_references_update_for_cluster_only", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/update.md", "source_location": "L202", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_docs_audit_04_open_questions_md", "label": "04-open-questions.md", "file_type": "document", "source_file": "docs/audit/04-open-questions.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_04_open_questions_open_questions", "label": "Open Questions", "file_type": "document", "source_file": "docs/audit/04-open-questions.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_04_open_questions_1_storefront_migration_roadmap_frontend_application", "label": "1. Storefront Migration Roadmap (`frontend/application`)", "file_type": "document", "source_file": "docs/audit/04-open-questions.md", "source_location": "L5"}, {"id": "$graphify-root$_docs_audit_04_open_questions_2_payment_gateway_integration_provider", "label": "2. Payment Gateway Integration Provider", "file_type": "document", "source_file": "docs/audit/04-open-questions.md", "source_location": "L9"}, {"id": "$graphify-root$_docs_audit_04_open_questions_3_deployment_ci_cd_pipeline_specifications", "label": "3. Deployment & CI/CD Pipeline Specifications", "file_type": "document", "source_file": "docs/audit/04-open-questions.md", "source_location": "L13"}, {"id": "$graphify-root$_docs_audit_04_open_questions_4_sms_otp_service_provider", "label": "4. SMS / OTP Service Provider", "file_type": "document", "source_file": "docs/audit/04-open-questions.md", "source_location": "L17"}], "edges": [{"source": "$graphify-root$_docs_audit_04_open_questions_md", "target": "$graphify-root$_docs_audit_04_open_questions_open_questions", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/04-open-questions.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_04_open_questions_open_questions", "target": "$graphify-root$_docs_audit_04_open_questions_1_storefront_migration_roadmap_frontend_application", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/04-open-questions.md", "source_location": "L5", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_04_open_questions_open_questions", "target": "$graphify-root$_docs_audit_04_open_questions_2_payment_gateway_integration_provider", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/04-open-questions.md", "source_location": "L9", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_04_open_questions_open_questions", "target": "$graphify-root$_docs_audit_04_open_questions_3_deployment_ci_cd_pipeline_specifications", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/04-open-questions.md", "source_location": "L13", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_04_open_questions_open_questions", "target": "$graphify-root$_docs_audit_04_open_questions_4_sms_otp_service_provider", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/04-open-questions.md", "source_location": "L17", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_frontend_application_claude_md", "label": "CLAUDE.md", "file_type": "document", "source_file": "frontend/application/CLAUDE.md", "source_location": "L1"}], "edges": [], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_docs_audit_phase3_1_change_log_md", "label": "phase3.1-change-log.md", "file_type": "document", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_phase3_1_change_log_phase_3_1_master_task_backlog_change_log", "label": "Phase 3.1 \u2014 Master Task Backlog Change Log", "file_type": "document", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_phase3_1_change_log_task_modifications_log", "label": "Task Modifications Log", "file_type": "document", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L8"}, {"id": "$graphify-root$_docs_audit_phase3_1_change_log_1_task_auth_001", "label": "1. `TASK-AUTH-001`", "file_type": "document", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L10"}, {"id": "$graphify-root$_docs_audit_phase3_1_change_log_2_task_fin_001", "label": "2. `TASK-FIN-001`", "file_type": "document", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L19"}, {"id": "$graphify-root$_docs_audit_phase3_1_change_log_3_decision_002", "label": "3. `DECISION-002`", "file_type": "document", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L28"}, {"id": "$graphify-root$_docs_audit_phase3_1_change_log_4_task_verify_001", "label": "4. `TASK-VERIFY-001`", "file_type": "document", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L37"}, {"id": "$graphify-root$_docs_audit_phase3_1_change_log_5_task_sec_001_task_sec_002", "label": "5. `TASK-SEC-001` & `TASK-SEC-002`", "file_type": "document", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L46"}, {"id": "$graphify-root$_docs_audit_phase3_1_change_log_6_task_build_001_execution_waves", "label": "6. `TASK-BUILD-001` & Execution Waves", "file_type": "document", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L55"}], "edges": [{"source": "$graphify-root$_docs_audit_phase3_1_change_log_md", "target": "$graphify-root$_docs_audit_phase3_1_change_log_phase_3_1_master_task_backlog_change_log", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_phase3_1_change_log_phase_3_1_master_task_backlog_change_log", "target": "$graphify-root$_docs_audit_phase3_1_change_log_task_modifications_log", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L8", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_phase3_1_change_log_task_modifications_log", "target": "$graphify-root$_docs_audit_phase3_1_change_log_1_task_auth_001", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L10", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_phase3_1_change_log_task_modifications_log", "target": "$graphify-root$_docs_audit_phase3_1_change_log_2_task_fin_001", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L19", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_phase3_1_change_log_task_modifications_log", "target": "$graphify-root$_docs_audit_phase3_1_change_log_3_decision_002", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L28", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_phase3_1_change_log_task_modifications_log", "target": "$graphify-root$_docs_audit_phase3_1_change_log_4_task_verify_001", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L37", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_phase3_1_change_log_task_modifications_log", "target": "$graphify-root$_docs_audit_phase3_1_change_log_5_task_sec_001_task_sec_002", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L46", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_phase3_1_change_log_task_modifications_log", "target": "$graphify-root$_docs_audit_phase3_1_change_log_6_task_build_001_execution_waves", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/phase3.1-change-log.md", "source_location": "L55", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_docs_readme_md", "label": "README.md", "file_type": "document", "source_file": "docs/README.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_readme_\u0641\u0647\u0631\u0633\u062a_\u0645\u0637\u0627\u0644\u0628_table_of_contents", "label": "\u0641\u0647\u0631\u0633\u062a \u0645\u0637\u0627\u0644\u0628 (Table of Contents)", "file_type": "document", "source_file": "docs/README.md", "source_location": "L7"}], "edges": [{"source": "$graphify-root$_docs_readme_md", "target": "$graphify-root$_docs_readme_\u0641\u0647\u0631\u0633\u062a_\u0645\u0637\u0627\u0644\u0628_table_of_contents", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/README.md", "source_location": "L7", "weight": 1.0}, {"source": "$graphify-root$_docs_readme_md", "target": "$graphify-root$_docs_01_introduction_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "docs/README.md", "source_location": "L11", "weight": 1.0, "target_file": "$graphify-root$/docs/01-introduction.md"}, {"source": "$graphify-root$_docs_readme_md", "target": "$graphify-root$_docs_02_user_guide_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "docs/README.md", "source_location": "L14", "weight": 1.0, "target_file": "$graphify-root$/docs/02-user-guide.md"}, {"source": "$graphify-root$_docs_readme_md", "target": "$graphify-root$_docs_03_developer_guide_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "docs/README.md", "source_location": "L17", "weight": 1.0, "target_file": "$graphify-root$/docs/03-developer-guide.md"}, {"source": "$graphify-root$_docs_readme_md", "target": "$graphify-root$_docs_04_setup_and_deployment_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "docs/README.md", "source_location": "L20", "weight": 1.0, "target_file": "$graphify-root$/docs/04-setup-and-deployment.md"}, {"source": "$graphify-root$_docs_readme_md", "target": "$graphify-root$_docs_05_devops_and_monitoring_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "docs/README.md", "source_location": "L23", "weight": 1.0, "target_file": "$graphify-root$/docs/05-devops-and-monitoring.md"}, {"source": "$graphify-root$_docs_readme_md", "target": "$graphify-root$_docs_06_testing_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "docs/README.md", "source_location": "L26", "weight": 1.0, "target_file": "$graphify-root$/docs/06-testing.md"}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_ai_agency_specs_prd_md", "label": "prd.md", "file_type": "document", "source_file": ".ai_agency/specs/prd.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_specs_prd_product_requirement_document_prd", "label": "Product Requirement Document (PRD)", "file_type": "document", "source_file": ".ai_agency/specs/prd.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_specs_prd_1_executive_vision", "label": "1. Executive Vision", "file_type": "document", "source_file": ".ai_agency/specs/prd.md", "source_location": "L3"}, {"id": "$graphify-root$_ai_agency_specs_prd_2_target_audience", "label": "2. Target Audience", "file_type": "document", "source_file": ".ai_agency/specs/prd.md", "source_location": "L6"}, {"id": "$graphify-root$_ai_agency_specs_prd_3_functional_requirements", "label": "3. Functional Requirements", "file_type": "document", "source_file": ".ai_agency/specs/prd.md", "source_location": "L9"}, {"id": "$graphify-root$_ai_agency_specs_prd_4_non_functional_requirements_performance_security", "label": "4. Non-Functional Requirements (Performance, Security)", "file_type": "document", "source_file": ".ai_agency/specs/prd.md", "source_location": "L12"}, {"id": "$graphify-root$_ai_agency_specs_prd_5_epic_feature_breakdown", "label": "5. Epic / Feature Breakdown", "file_type": "document", "source_file": ".ai_agency/specs/prd.md", "source_location": "L15"}], "edges": [{"source": "$graphify-root$_ai_agency_specs_prd_md", "target": "$graphify-root$_ai_agency_specs_prd_product_requirement_document_prd", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/prd.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_prd_product_requirement_document_prd", "target": "$graphify-root$_ai_agency_specs_prd_1_executive_vision", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/prd.md", "source_location": "L3", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_prd_product_requirement_document_prd", "target": "$graphify-root$_ai_agency_specs_prd_2_target_audience", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/prd.md", "source_location": "L6", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_prd_product_requirement_document_prd", "target": "$graphify-root$_ai_agency_specs_prd_3_functional_requirements", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/prd.md", "source_location": "L9", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_prd_product_requirement_document_prd", "target": "$graphify-root$_ai_agency_specs_prd_4_non_functional_requirements_performance_security", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/prd.md", "source_location": "L12", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_prd_product_requirement_document_prd", "target": "$graphify-root$_ai_agency_specs_prd_5_epic_feature_breakdown", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/prd.md", "source_location": "L15", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_docs_audit_19_finding_verification_report_md", "label": "19-finding-verification-report.md", "file_type": "document", "source_file": "docs/audit/19-finding-verification-report.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_19_finding_verification_report_raw_finding_verification_disposition_report", "label": "Raw Finding Verification & Disposition Report", "file_type": "document", "source_file": "docs/audit/19-finding-verification-report.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_19_finding_verification_report_1_executive_summary_verification_reconciliation_table", "label": "1. Executive Summary & Verification Reconciliation Table", "file_type": "document", "source_file": "docs/audit/19-finding-verification-report.md", "source_location": "L3"}, {"id": "$graphify-root$_docs_audit_19_finding_verification_report_2_newly_discovered_split_findings_canonical_ids", "label": "2. Newly Discovered & Split Findings (Canonical IDs)", "file_type": "document", "source_file": "docs/audit/19-finding-verification-report.md", "source_location": "L22"}], "edges": [{"source": "$graphify-root$_docs_audit_19_finding_verification_report_md", "target": "$graphify-root$_docs_audit_19_finding_verification_report_raw_finding_verification_disposition_report", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/19-finding-verification-report.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_19_finding_verification_report_raw_finding_verification_disposition_report", "target": "$graphify-root$_docs_audit_19_finding_verification_report_1_executive_summary_verification_reconciliation_table", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/19-finding-verification-report.md", "source_location": "L3", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_19_finding_verification_report_raw_finding_verification_disposition_report", "target": "$graphify-root$_docs_audit_19_finding_verification_report_2_newly_discovered_split_findings_canonical_ids", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/19-finding-verification-report.md", "source_location": "L22", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_claude_skills_graphify_references_transcribe_md", "label": "transcribe.md", "file_type": "document", "source_file": ".claude/skills/graphify/references/transcribe.md", "source_location": "L1"}, {"id": "$graphify-root$_claude_skills_graphify_references_transcribe_graphify_reference_transcribe_video_and_audio", "label": "graphify reference: transcribe video and audio", "file_type": "document", "source_file": ".claude/skills/graphify/references/transcribe.md", "source_location": "L1"}, {"id": "$graphify-root$_claude_skills_graphify_references_transcribe_step_2_5_transcribe_video_audio_files_only_if_video_files_detected", "label": "Step 2.5 - Transcribe video / audio files (only if video files detected)", "file_type": "document", "source_file": ".claude/skills/graphify/references/transcribe.md", "source_location": "L5"}], "edges": [{"source": "$graphify-root$_claude_skills_graphify_references_transcribe_md", "target": "$graphify-root$_claude_skills_graphify_references_transcribe_graphify_reference_transcribe_video_and_audio", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/transcribe.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_claude_skills_graphify_references_transcribe_graphify_reference_transcribe_video_and_audio", "target": "$graphify-root$_claude_skills_graphify_references_transcribe_step_2_5_transcribe_video_audio_files_only_if_video_files_detected", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/transcribe.md", "source_location": "L5", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_md", "label": "33-final-phase2-audit-closure.md", "file_type": "document", "source_file": "docs/audit/33-final-phase2-audit-closure.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_final_phase_2_audit_closure_report", "label": "Final Phase 2 Audit Closure Report", "file_type": "document", "source_file": "docs/audit/33-final-phase2-audit-closure.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_1_executive_summary_honest_review_tier_metrics", "label": "1. Executive Summary & Honest Review-Tier Metrics", "file_type": "document", "source_file": "docs/audit/33-final-phase2-audit-closure.md", "source_location": "L9"}, {"id": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_2_reconciled_findings_canonical_identifier_normalization", "label": "2. Reconciled Findings & Canonical Identifier Normalization", "file_type": "document", "source_file": "docs/audit/33-final-phase2-audit-closure.md", "source_location": "L22"}, {"id": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_3_validation_integrity_verification", "label": "3. Validation & Integrity Verification", "file_type": "document", "source_file": "docs/audit/33-final-phase2-audit-closure.md", "source_location": "L34"}, {"id": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_4_final_quality_gate_conclusion", "label": "4. Final Quality Gate Conclusion", "file_type": "document", "source_file": "docs/audit/33-final-phase2-audit-closure.md", "source_location": "L42"}], "edges": [{"source": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_md", "target": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_final_phase_2_audit_closure_report", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/33-final-phase2-audit-closure.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_final_phase_2_audit_closure_report", "target": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_1_executive_summary_honest_review_tier_metrics", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/33-final-phase2-audit-closure.md", "source_location": "L9", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_final_phase_2_audit_closure_report", "target": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_2_reconciled_findings_canonical_identifier_normalization", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/33-final-phase2-audit-closure.md", "source_location": "L22", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_final_phase_2_audit_closure_report", "target": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_3_validation_integrity_verification", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/33-final-phase2-audit-closure.md", "source_location": "L34", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_final_phase_2_audit_closure_report", "target": "$graphify-root$_docs_audit_33_final_phase2_audit_closure_4_final_quality_gate_conclusion", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/33-final-phase2-audit-closure.md", "source_location": "L42", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_docs_audit_18_compiler_diagnostic_dispositions_md", "label": "18-compiler-diagnostic-dispositions.md", "file_type": "document", "source_file": "docs/audit/18-compiler-diagnostic-dispositions.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_18_compiler_diagnostic_dispositions_compiler_diagnostic_dispositions", "label": "Compiler Diagnostic Dispositions", "file_type": "document", "source_file": "docs/audit/18-compiler-diagnostic-dispositions.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_18_compiler_diagnostic_dispositions_reconciled_compiler_diagnostic_table", "label": "Reconciled Compiler Diagnostic Table", "file_type": "document", "source_file": "docs/audit/18-compiler-diagnostic-dispositions.md", "source_location": "L11"}], "edges": [{"source": "$graphify-root$_docs_audit_18_compiler_diagnostic_dispositions_md", "target": "$graphify-root$_docs_audit_18_compiler_diagnostic_dispositions_compiler_diagnostic_dispositions", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/18-compiler-diagnostic-dispositions.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_18_compiler_diagnostic_dispositions_compiler_diagnostic_dispositions", "target": "$graphify-root$_docs_audit_18_compiler_diagnostic_dispositions_reconciled_compiler_diagnostic_table", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/18-compiler-diagnostic-dispositions.md", "source_location": "L11", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_agents_rules_graphify_md", "label": "graphify.md", "file_type": "document", "source_file": ".agents/rules/graphify.md", "source_location": "L1"}, {"id": "$graphify-root$_agents_rules_graphify_graphify", "label": "graphify", "file_type": "document", "source_file": ".agents/rules/graphify.md", "source_location": "L6"}], "edges": [{"source": "$graphify-root$_agents_rules_graphify_md", "target": "$graphify-root$_agents_rules_graphify_graphify", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".agents/rules/graphify.md", "source_location": "L6", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_claude_md", "label": "CLAUDE.md", "file_type": "document", "source_file": "CLAUDE.md", "source_location": "L1"}, {"id": "$graphify-root$_claude_graphify", "label": "graphify", "file_type": "document", "source_file": "CLAUDE.md", "source_location": "L1"}], "edges": [{"source": "$graphify-root$_claude_md", "target": "$graphify-root$_claude_graphify", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CLAUDE.md", "source_location": "L1", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_frontend_admin_panel_readme_md", "label": "README.md", "file_type": "document", "source_file": "frontend/admin-panel/README.md", "source_location": "L1"}, {"id": "$graphify-root$_frontend_admin_panel_readme_react_typescript_vite", "label": "React + TypeScript + Vite", "file_type": "document", "source_file": "frontend/admin-panel/README.md", "source_location": "L1"}, {"id": "$graphify-root$_frontend_admin_panel_readme_react_compiler", "label": "React Compiler", "file_type": "document", "source_file": "frontend/admin-panel/README.md", "source_location": "L10"}, {"id": "$graphify-root$_frontend_admin_panel_readme_expanding_the_eslint_configuration", "label": "Expanding the ESLint configuration", "file_type": "document", "source_file": "frontend/admin-panel/README.md", "source_location": "L14"}], "edges": [{"source": "$graphify-root$_frontend_admin_panel_readme_md", "target": "$graphify-root$_frontend_admin_panel_readme_react_typescript_vite", "relation": "contains", "confidence": "EXTRACTED", "source_file": "frontend/admin-panel/README.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_frontend_admin_panel_readme_react_typescript_vite", "target": "$graphify-root$_frontend_admin_panel_readme_react_compiler", "relation": "contains", "confidence": "EXTRACTED", "source_file": "frontend/admin-panel/README.md", "source_location": "L10", "weight": 1.0}, {"source": "$graphify-root$_frontend_admin_panel_readme_react_typescript_vite", "target": "$graphify-root$_frontend_admin_panel_readme_expanding_the_eslint_configuration", "relation": "contains", "confidence": "EXTRACTED", "source_file": "frontend/admin-panel/README.md", "source_location": "L14", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_frontend_admin_panel_public_fonts_sahel_font_v3_4_0_changelog_md", "label": "CHANGELOG.md", "file_type": "document", "source_file": "frontend/admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md", "source_location": "L1"}], "edges": [], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_ai_agency_specs_reviews_security_review_md", "label": "security_review.md", "file_type": "document", "source_file": ".ai_agency/specs/reviews/security_review.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_specs_reviews_security_review_security_performance_review_09_devops_security", "label": "\ud83d\udd12 Security & Performance Review (09_devops_security)", "file_type": "document", "source_file": ".ai_agency/specs/reviews/security_review.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_specs_reviews_security_review_security_architecture_best_practices", "label": "Security Architecture & Best Practices", "file_type": "document", "source_file": ".ai_agency/specs/reviews/security_review.md", "source_location": "L3"}], "edges": [{"source": "$graphify-root$_ai_agency_specs_reviews_security_review_md", "target": "$graphify-root$_ai_agency_specs_reviews_security_review_security_performance_review_09_devops_security", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/reviews/security_review.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_reviews_security_review_security_performance_review_09_devops_security", "target": "$graphify-root$_ai_agency_specs_reviews_security_review_security_architecture_best_practices", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/reviews/security_review.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_ai_agency_specs_reviews_code_health_review_md", "label": "code_health_review.md", "file_type": "document", "source_file": ".ai_agency/specs/reviews/code_health_review.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_specs_reviews_code_health_review_code_health_audit_review_01_auditor", "label": "\ud83d\udd0d Code Health Audit Review (01_auditor)", "file_type": "document", "source_file": ".ai_agency/specs/reviews/code_health_review.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_specs_reviews_code_health_review_executive_summary", "label": "Executive Summary", "file_type": "document", "source_file": ".ai_agency/specs/reviews/code_health_review.md", "source_location": "L3"}, {"id": "$graphify-root$_ai_agency_specs_reviews_code_health_review_key_findings", "label": "Key Findings", "file_type": "document", "source_file": ".ai_agency/specs/reviews/code_health_review.md", "source_location": "L8"}, {"id": "$graphify-root$_ai_agency_specs_reviews_code_health_review_recommendations", "label": "Recommendations", "file_type": "document", "source_file": ".ai_agency/specs/reviews/code_health_review.md", "source_location": "L19"}], "edges": [{"source": "$graphify-root$_ai_agency_specs_reviews_code_health_review_md", "target": "$graphify-root$_ai_agency_specs_reviews_code_health_review_code_health_audit_review_01_auditor", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/reviews/code_health_review.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_reviews_code_health_review_code_health_audit_review_01_auditor", "target": "$graphify-root$_ai_agency_specs_reviews_code_health_review_executive_summary", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/reviews/code_health_review.md", "source_location": "L3", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_reviews_code_health_review_code_health_audit_review_01_auditor", "target": "$graphify-root$_ai_agency_specs_reviews_code_health_review_key_findings", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/reviews/code_health_review.md", "source_location": "L8", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_reviews_code_health_review_code_health_audit_review_01_auditor", "target": "$graphify-root$_ai_agency_specs_reviews_code_health_review_recommendations", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/reviews/code_health_review.md", "source_location": "L19", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_docs_05_devops_and_monitoring_md", "label": "05-devops-and-monitoring.md", "file_type": "document", "source_file": "docs/05-devops-and-monitoring.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_05_devops_and_monitoring_\u062f\u0648\u0627\u067e\u0633_\u0648_\u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af_\u0633\u06cc\u0633\u062a\u0645_devops_monitoring", "label": "\u062f\u0648\u0627\u067e\u0633 \u0648 \u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af \u0633\u06cc\u0633\u062a\u0645 (DevOps & Monitoring)", "file_type": "document", "source_file": "docs/05-devops-and-monitoring.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_05_devops_and_monitoring_\u06f1_\u0641\u0631\u0622\u06cc\u0646\u062f_\u0627\u0633\u062a\u0642\u0631\u0627\u0631_\u062e\u0648\u062f\u06a9\u0627\u0631_ci_cd", "label": "\u06f1. \u0641\u0631\u0622\u06cc\u0646\u062f \u0627\u0633\u062a\u0642\u0631\u0627\u0631 \u062e\u0648\u062f\u06a9\u0627\u0631 (CI/CD)", "file_type": "document", "source_file": "docs/05-devops-and-monitoring.md", "source_location": "L5"}, {"id": "$graphify-root$_docs_05_devops_and_monitoring_\u06f2_\u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af_\u0639\u0645\u0644\u06a9\u0631\u062f_pm2_dashboard", "label": "\u06f2. \u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af \u0639\u0645\u0644\u06a9\u0631\u062f (PM2 Dashboard)", "file_type": "document", "source_file": "docs/05-devops-and-monitoring.md", "source_location": "L12"}, {"id": "$graphify-root$_docs_05_devops_and_monitoring_\u06f3_\u0628\u0631\u0631\u0633\u06cc_\u0644\u0627\u06af_\u0647\u0627\u06cc_\u0633\u06cc\u0633\u062a\u0645_logs_management", "label": "\u06f3. \u0628\u0631\u0631\u0633\u06cc \u0644\u0627\u06af\u200c\u0647\u0627\u06cc \u0633\u06cc\u0633\u062a\u0645 (Logs Management)", "file_type": "document", "source_file": "docs/05-devops-and-monitoring.md", "source_location": "L27"}, {"id": "$graphify-root$_docs_05_devops_and_monitoring_\u06f4_\u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af_\u062f\u06cc\u062a\u0627\u0628\u06cc\u0633_postgresql", "label": "\u06f4. \u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af \u062f\u06cc\u062a\u0627\u0628\u06cc\u0633 (PostgreSQL)", "file_type": "document", "source_file": "docs/05-devops-and-monitoring.md", "source_location": "L39"}], "edges": [{"source": "$graphify-root$_docs_05_devops_and_monitoring_md", "target": "$graphify-root$_docs_05_devops_and_monitoring_\u062f\u0648\u0627\u067e\u0633_\u0648_\u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af_\u0633\u06cc\u0633\u062a\u0645_devops_monitoring", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/05-devops-and-monitoring.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_docs_05_devops_and_monitoring_\u062f\u0648\u0627\u067e\u0633_\u0648_\u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af_\u0633\u06cc\u0633\u062a\u0645_devops_monitoring", "target": "$graphify-root$_docs_05_devops_and_monitoring_\u06f1_\u0641\u0631\u0622\u06cc\u0646\u062f_\u0627\u0633\u062a\u0642\u0631\u0627\u0631_\u062e\u0648\u062f\u06a9\u0627\u0631_ci_cd", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/05-devops-and-monitoring.md", "source_location": "L5", "weight": 1.0}, {"source": "$graphify-root$_docs_05_devops_and_monitoring_\u062f\u0648\u0627\u067e\u0633_\u0648_\u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af_\u0633\u06cc\u0633\u062a\u0645_devops_monitoring", "target": "$graphify-root$_docs_05_devops_and_monitoring_\u06f2_\u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af_\u0639\u0645\u0644\u06a9\u0631\u062f_pm2_dashboard", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/05-devops-and-monitoring.md", "source_location": "L12", "weight": 1.0}, {"source": "$graphify-root$_docs_05_devops_and_monitoring_\u062f\u0648\u0627\u067e\u0633_\u0648_\u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af_\u0633\u06cc\u0633\u062a\u0645_devops_monitoring", "target": "$graphify-root$_docs_05_devops_and_monitoring_\u06f3_\u0628\u0631\u0631\u0633\u06cc_\u0644\u0627\u06af_\u0647\u0627\u06cc_\u0633\u06cc\u0633\u062a\u0645_logs_management", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/05-devops-and-monitoring.md", "source_location": "L27", "weight": 1.0}, {"source": "$graphify-root$_docs_05_devops_and_monitoring_\u062f\u0648\u0627\u067e\u0633_\u0648_\u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af_\u0633\u06cc\u0633\u062a\u0645_devops_monitoring", "target": "$graphify-root$_docs_05_devops_and_monitoring_\u06f4_\u0645\u0627\u0646\u06cc\u062a\u0648\u0631\u06cc\u0646\u06af_\u062f\u06cc\u062a\u0627\u0628\u06cc\u0633_postgresql", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/05-devops-and-monitoring.md", "source_location": "L39", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_claude_skills_graphify_references_query_md", "label": "query.md", "file_type": "document", "source_file": ".claude/skills/graphify/references/query.md", "source_location": "L1"}, {"id": "$graphify-root$_claude_skills_graphify_references_query_graphify_reference_query_path_explain", "label": "graphify reference: query, path, explain", "file_type": "document", "source_file": ".claude/skills/graphify/references/query.md", "source_location": "L1"}, {"id": "$graphify-root$_claude_skills_graphify_references_query_step_0_constrained_query_expansion_required_before_traversal", "label": "Step 0 \u2014 Constrained query expansion (REQUIRED before traversal)", "file_type": "document", "source_file": ".claude/skills/graphify/references/query.md", "source_location": "L23"}, {"id": "$graphify-root$_claude_skills_graphify_references_query_step_1_traversal", "label": "Step 1 \u2014 Traversal", "file_type": "document", "source_file": ".claude/skills/graphify/references/query.md", "source_location": "L61"}, {"id": "$graphify-root$_claude_skills_graphify_references_query_for_graphify_path", "label": "For /graphify path", "file_type": "document", "source_file": ".claude/skills/graphify/references/query.md", "source_location": "L186"}, {"id": "$graphify-root$_claude_skills_graphify_references_query_for_graphify_explain", "label": "For /graphify explain", "file_type": "document", "source_file": ".claude/skills/graphify/references/query.md", "source_location": "L254"}], "edges": [{"source": "$graphify-root$_claude_skills_graphify_references_query_md", "target": "$graphify-root$_claude_skills_graphify_references_query_graphify_reference_query_path_explain", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/query.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_claude_skills_graphify_references_query_graphify_reference_query_path_explain", "target": "$graphify-root$_claude_skills_graphify_references_query_step_0_constrained_query_expansion_required_before_traversal", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/query.md", "source_location": "L23", "weight": 1.0}, {"source": "$graphify-root$_claude_skills_graphify_references_query_graphify_reference_query_path_explain", "target": "$graphify-root$_claude_skills_graphify_references_query_step_1_traversal", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/query.md", "source_location": "L61", "weight": 1.0}, {"source": "$graphify-root$_claude_skills_graphify_references_query_graphify_reference_query_path_explain", "target": "$graphify-root$_claude_skills_graphify_references_query_for_graphify_path", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/query.md", "source_location": "L186", "weight": 1.0}, {"source": "$graphify-root$_claude_skills_graphify_references_query_graphify_reference_query_path_explain", "target": "$graphify-root$_claude_skills_graphify_references_query_for_graphify_explain", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/query.md", "source_location": "L254", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_claude_skills_graphify_references_github_and_merge_md", "label": "github-and-merge.md", "file_type": "document", "source_file": ".claude/skills/graphify/references/github-and-merge.md", "source_location": "L1"}, {"id": "$graphify-root$_claude_skills_graphify_references_github_and_merge_graphify_reference_github_clone_and_cross_repo_merge", "label": "graphify reference: GitHub clone and cross-repo merge", "file_type": "document", "source_file": ".claude/skills/graphify/references/github-and-merge.md", "source_location": "L1"}, {"id": "$graphify-root$_claude_skills_graphify_references_github_and_merge_step_0_clone_github_repo_s_only_if_a_github_url_was_given", "label": "Step 0 - Clone GitHub repo(s) (only if a GitHub URL was given)", "file_type": "document", "source_file": ".claude/skills/graphify/references/github-and-merge.md", "source_location": "L5"}], "edges": [{"source": "$graphify-root$_claude_skills_graphify_references_github_and_merge_md", "target": "$graphify-root$_claude_skills_graphify_references_github_and_merge_graphify_reference_github_clone_and_cross_repo_merge", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/github-and-merge.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_claude_skills_graphify_references_github_and_merge_graphify_reference_github_clone_and_cross_repo_merge", "target": "$graphify-root$_claude_skills_graphify_references_github_and_merge_step_0_clone_github_repo_s_only_if_a_github_url_was_given", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/github-and-merge.md", "source_location": "L5", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_ai_agency_specs_architecture_spec_md", "label": "architecture_spec.md", "file_type": "document", "source_file": ".ai_agency/specs/architecture_spec.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_specs_architecture_spec_architecture_specification", "label": "Architecture Specification", "file_type": "document", "source_file": ".ai_agency/specs/architecture_spec.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_specs_architecture_spec_1_single_non_negotiable_tech_stack", "label": "1. Single Non-Negotiable Tech Stack", "file_type": "document", "source_file": ".ai_agency/specs/architecture_spec.md", "source_location": "L3"}, {"id": "$graphify-root$_ai_agency_specs_architecture_spec_2_directory_structure_tree", "label": "2. Directory Structure Tree", "file_type": "document", "source_file": ".ai_agency/specs/architecture_spec.md", "source_location": "L10"}, {"id": "$graphify-root$_ai_agency_specs_architecture_spec_3_state_management_strategy", "label": "3. State Management Strategy", "file_type": "document", "source_file": ".ai_agency/specs/architecture_spec.md", "source_location": "L25"}, {"id": "$graphify-root$_ai_agency_specs_architecture_spec_4_deployment_docker_architecture", "label": "4. Deployment / Docker Architecture", "file_type": "document", "source_file": ".ai_agency/specs/architecture_spec.md", "source_location": "L28"}], "edges": [{"source": "$graphify-root$_ai_agency_specs_architecture_spec_md", "target": "$graphify-root$_ai_agency_specs_architecture_spec_architecture_specification", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/architecture_spec.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_architecture_spec_architecture_specification", "target": "$graphify-root$_ai_agency_specs_architecture_spec_1_single_non_negotiable_tech_stack", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/architecture_spec.md", "source_location": "L3", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_architecture_spec_architecture_specification", "target": "$graphify-root$_ai_agency_specs_architecture_spec_2_directory_structure_tree", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/architecture_spec.md", "source_location": "L10", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_architecture_spec_architecture_specification", "target": "$graphify-root$_ai_agency_specs_architecture_spec_3_state_management_strategy", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/architecture_spec.md", "source_location": "L25", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_architecture_spec_architecture_specification", "target": "$graphify-root$_ai_agency_specs_architecture_spec_4_deployment_docker_architecture", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/architecture_spec.md", "source_location": "L28", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_ai_agency_specs_project_health_md", "label": "project_health.md", "file_type": "document", "source_file": ".ai_agency/specs/project_health.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_specs_project_health_project_health_audit_report", "label": "Project Health Audit Report", "file_type": "document", "source_file": ".ai_agency/specs/project_health.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_specs_project_health_1_audit_score_summary", "label": "1. Audit Score Summary", "file_type": "document", "source_file": ".ai_agency/specs/project_health.md", "source_location": "L3"}, {"id": "$graphify-root$_ai_agency_specs_project_health_2_technical_debt_inventory", "label": "2. Technical Debt Inventory", "file_type": "document", "source_file": ".ai_agency/specs/project_health.md", "source_location": "L7"}, {"id": "$graphify-root$_ai_agency_specs_project_health_3_outdated_dependencies_list", "label": "3. Outdated Dependencies List", "file_type": "document", "source_file": ".ai_agency/specs/project_health.md", "source_location": "L10"}, {"id": "$graphify-root$_ai_agency_specs_project_health_4_security_risks_env_leaks_unprotected_ports", "label": "4. Security Risks (.env leaks, unprotected ports)", "file_type": "document", "source_file": ".ai_agency/specs/project_health.md", "source_location": "L13"}], "edges": [{"source": "$graphify-root$_ai_agency_specs_project_health_md", "target": "$graphify-root$_ai_agency_specs_project_health_project_health_audit_report", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/project_health.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_project_health_project_health_audit_report", "target": "$graphify-root$_ai_agency_specs_project_health_1_audit_score_summary", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/project_health.md", "source_location": "L3", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_project_health_project_health_audit_report", "target": "$graphify-root$_ai_agency_specs_project_health_2_technical_debt_inventory", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/project_health.md", "source_location": "L7", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_project_health_project_health_audit_report", "target": "$graphify-root$_ai_agency_specs_project_health_3_outdated_dependencies_list", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/project_health.md", "source_location": "L10", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_project_health_project_health_audit_report", "target": "$graphify-root$_ai_agency_specs_project_health_4_security_risks_env_leaks_unprotected_ports", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/project_health.md", "source_location": "L13", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_docs_audit_01_system_discovery_md", "label": "01-system-discovery.md", "file_type": "document", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_01_system_discovery_system_discovery", "label": "System Discovery", "file_type": "document", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_01_system_discovery_current_architecture", "label": "Current Architecture", "file_type": "document", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L3"}, {"id": "$graphify-root$_docs_audit_01_system_discovery_active_core_applications", "label": "Active Core Applications", "file_type": "document", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L6"}, {"id": "$graphify-root$_docs_audit_01_system_discovery_excluded_non_auditable_artifacts", "label": "Excluded / Non-Auditable Artifacts", "file_type": "document", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L12"}, {"id": "$graphify-root$_docs_audit_01_system_discovery_major_business_domains_discovered", "label": "Major Business Domains Discovered", "file_type": "document", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L18"}, {"id": "$graphify-root$_docs_audit_01_system_discovery_technical_architecture_summary", "label": "Technical Architecture Summary", "file_type": "document", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L27"}, {"id": "$graphify-root$_docs_audit_01_system_discovery_evidence_based_status_matrix", "label": "Evidence-Based Status Matrix", "file_type": "document", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L36"}], "edges": [{"source": "$graphify-root$_docs_audit_01_system_discovery_md", "target": "$graphify-root$_docs_audit_01_system_discovery_system_discovery", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_01_system_discovery_system_discovery", "target": "$graphify-root$_docs_audit_01_system_discovery_current_architecture", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L3", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_01_system_discovery_current_architecture", "target": "$graphify-root$_docs_audit_01_system_discovery_active_core_applications", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L6", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_01_system_discovery_current_architecture", "target": "$graphify-root$_docs_audit_01_system_discovery_excluded_non_auditable_artifacts", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L12", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_01_system_discovery_system_discovery", "target": "$graphify-root$_docs_audit_01_system_discovery_major_business_domains_discovered", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L18", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_01_system_discovery_system_discovery", "target": "$graphify-root$_docs_audit_01_system_discovery_technical_architecture_summary", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L27", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_01_system_discovery_system_discovery", "target": "$graphify-root$_docs_audit_01_system_discovery_evidence_based_status_matrix", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/01-system-discovery.md", "source_location": "L36", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_claude_skills_graphify_references_hooks_md", "label": "hooks.md", "file_type": "document", "source_file": ".claude/skills/graphify/references/hooks.md", "source_location": "L1"}, {"id": "$graphify-root$_claude_skills_graphify_references_hooks_graphify_reference_commit_hook_and_native_claude_md_integration", "label": "graphify reference: commit hook and native CLAUDE.md integration", "file_type": "document", "source_file": ".claude/skills/graphify/references/hooks.md", "source_location": "L1"}, {"id": "$graphify-root$_claude_skills_graphify_references_hooks_for_git_commit_hook", "label": "For git commit hook", "file_type": "document", "source_file": ".claude/skills/graphify/references/hooks.md", "source_location": "L5"}, {"id": "$graphify-root$_claude_skills_graphify_references_hooks_for_native_claude_md_integration", "label": "For native CLAUDE.md integration", "file_type": "document", "source_file": ".claude/skills/graphify/references/hooks.md", "source_location": "L21"}], "edges": [{"source": "$graphify-root$_claude_skills_graphify_references_hooks_md", "target": "$graphify-root$_claude_skills_graphify_references_hooks_graphify_reference_commit_hook_and_native_claude_md_integration", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/hooks.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_claude_skills_graphify_references_hooks_graphify_reference_commit_hook_and_native_claude_md_integration", "target": "$graphify-root$_claude_skills_graphify_references_hooks_for_git_commit_hook", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/hooks.md", "source_location": "L5", "weight": 1.0}, {"source": "$graphify-root$_claude_skills_graphify_references_hooks_graphify_reference_commit_hook_and_native_claude_md_integration", "target": "$graphify-root$_claude_skills_graphify_references_hooks_for_native_claude_md_integration", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/hooks.md", "source_location": "L21", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_ai_agency_specs_reviews_readme_md", "label": "README.md", "file_type": "document", "source_file": ".ai_agency/specs/reviews/README.md", "source_location": "L1"}], "edges": [], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_antigravity_instructions_md", "label": "instructions.md", "file_type": "document", "source_file": ".antigravity/instructions.md", "source_location": "L1"}, {"id": "$graphify-root$_antigravity_instructions_graphify", "label": "graphify", "file_type": "document", "source_file": ".antigravity/instructions.md", "source_location": "L1"}], "edges": [{"source": "$graphify-root$_antigravity_instructions_md", "target": "$graphify-root$_antigravity_instructions_graphify", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".antigravity/instructions.md", "source_location": "L1", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_claude_skills_graphify_references_extraction_spec_md", "label": "extraction-spec.md", "file_type": "document", "source_file": ".claude/skills/graphify/references/extraction-spec.md", "source_location": "L1"}, {"id": "$graphify-root$_claude_skills_graphify_references_extraction_spec_graphify_reference_extraction_subagent_prompt", "label": "graphify reference: extraction subagent prompt", "file_type": "document", "source_file": ".claude/skills/graphify/references/extraction-spec.md", "source_location": "L1"}], "edges": [{"source": "$graphify-root$_claude_skills_graphify_references_extraction_spec_md", "target": "$graphify-root$_claude_skills_graphify_references_extraction_spec_graphify_reference_extraction_subagent_prompt", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".claude/skills/graphify/references/extraction-spec.md", "source_location": "L1", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_ai_agency_memory_scratchpad_md", "label": "scratchpad.md", "file_type": "document", "source_file": ".ai_agency/memory/scratchpad.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_memory_scratchpad_active_agent_working_scratchpad", "label": "\ud83d\udcdd Active Agent Working Scratchpad", "file_type": "document", "source_file": ".ai_agency/memory/scratchpad.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_memory_scratchpad_project_canina_veterinary_e_commerce_system", "label": "Project: Canina Veterinary E-Commerce System", "file_type": "document", "source_file": ".ai_agency/memory/scratchpad.md", "source_location": "L3"}, {"id": "$graphify-root$_ai_agency_memory_scratchpad_requirements_persona_mandates_intake_user_request", "label": "\ud83d\udccb Requirements & Persona Mandates (Intake & User Request)", "file_type": "document", "source_file": ".ai_agency/memory/scratchpad.md", "source_location": "L8"}, {"id": "$graphify-root$_ai_agency_memory_scratchpad_execution_target", "label": "\ud83c\udfd7\ufe0f Execution Target", "file_type": "document", "source_file": ".ai_agency/memory/scratchpad.md", "source_location": "L24"}], "edges": [{"source": "$graphify-root$_ai_agency_memory_scratchpad_md", "target": "$graphify-root$_ai_agency_memory_scratchpad_active_agent_working_scratchpad", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/memory/scratchpad.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_memory_scratchpad_active_agent_working_scratchpad", "target": "$graphify-root$_ai_agency_memory_scratchpad_project_canina_veterinary_e_commerce_system", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/memory/scratchpad.md", "source_location": "L3", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_memory_scratchpad_active_agent_working_scratchpad", "target": "$graphify-root$_ai_agency_memory_scratchpad_requirements_persona_mandates_intake_user_request", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/memory/scratchpad.md", "source_location": "L8", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_memory_scratchpad_active_agent_working_scratchpad", "target": "$graphify-root$_ai_agency_memory_scratchpad_execution_target", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/memory/scratchpad.md", "source_location": "L24", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_ai_agency_agents_00_intake_md", "label": "00_intake.md", "file_type": "document", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_agents_00_intake_role_core_objective", "label": "Role & Core Objective", "file_type": "document", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_agents_00_intake_strict_input_specifications_what_files_to_read", "label": "Strict Input Specifications (What files to read)", "file_type": "document", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L9"}, {"id": "$graphify-root$_ai_agency_agents_00_intake_brownfield_detection", "label": "Brownfield Detection", "file_type": "document", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L17"}, {"id": "$graphify-root$_ai_agency_agents_00_intake_operational_rules_boundaries", "label": "Operational Rules & Boundaries", "file_type": "document", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L28"}, {"id": "$graphify-root$_ai_agency_agents_00_intake_1_ask_don_t_assume", "label": "1. Ask \u2014 Don't Assume", "file_type": "document", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L30"}, {"id": "$graphify-root$_ai_agency_agents_00_intake_2_forbidden_actions", "label": "2. Forbidden Actions", "file_type": "document", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L58"}, {"id": "$graphify-root$_ai_agency_agents_00_intake_required_output_artifacts_what_files_to_write_update", "label": "Required Output Artifacts (What files to write/update)", "file_type": "document", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L65"}, {"id": "$graphify-root$_ai_agency_agents_00_intake_expected_json_output_schema", "label": "Expected JSON Output Schema", "file_type": "document", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L105"}], "edges": [{"source": "$graphify-root$_ai_agency_agents_00_intake_md", "target": "$graphify-root$_ai_agency_agents_00_intake_role_core_objective", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_agents_00_intake_role_core_objective", "target": "$graphify-root$_ai_agency_agents_00_intake_strict_input_specifications_what_files_to_read", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L9", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_agents_00_intake_role_core_objective", "target": "$graphify-root$_ai_agency_agents_00_intake_brownfield_detection", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L17", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_agents_00_intake_role_core_objective", "target": "$graphify-root$_ai_agency_agents_00_intake_operational_rules_boundaries", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L28", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_agents_00_intake_operational_rules_boundaries", "target": "$graphify-root$_ai_agency_agents_00_intake_1_ask_don_t_assume", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L30", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_agents_00_intake_operational_rules_boundaries", "target": "$graphify-root$_ai_agency_agents_00_intake_2_forbidden_actions", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L58", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_agents_00_intake_role_core_objective", "target": "$graphify-root$_ai_agency_agents_00_intake_required_output_artifacts_what_files_to_write_update", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L65", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_agents_00_intake_role_core_objective", "target": "$graphify-root$_ai_agency_agents_00_intake_expected_json_output_schema", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/agents/00_intake.md", "source_location": "L105", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_md", "label": "phase3.2-implementation-readiness.md", "file_type": "document", "source_file": "docs/audit/phase3.2-implementation-readiness.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_phase_3_2_3_3_implementation_readiness_architectural_finalization_report", "label": "Phase 3.2 / 3.3 \u2014 Implementation Readiness & Architectural Finalization Report", "file_type": "document", "source_file": "docs/audit/phase3.2-implementation-readiness.md", "source_location": "L1"}, {"id": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_1_executive_summary_architecture_decisions", "label": "1. Executive Summary & Architecture Decisions", "file_type": "document", "source_file": "docs/audit/phase3.2-implementation-readiness.md", "source_location": "L12"}, {"id": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_2_updated_task_specifications_overview", "label": "2. Updated Task Specifications Overview", "file_type": "document", "source_file": "docs/audit/phase3.2-implementation-readiness.md", "source_location": "L30"}, {"id": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_3_final_readiness_statement", "label": "3. Final Readiness Statement", "file_type": "document", "source_file": "docs/audit/phase3.2-implementation-readiness.md", "source_location": "L47"}], "edges": [{"source": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_md", "target": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_phase_3_2_3_3_implementation_readiness_architectural_finalization_report", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/phase3.2-implementation-readiness.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_phase_3_2_3_3_implementation_readiness_architectural_finalization_report", "target": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_1_executive_summary_architecture_decisions", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/phase3.2-implementation-readiness.md", "source_location": "L12", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_phase_3_2_3_3_implementation_readiness_architectural_finalization_report", "target": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_2_updated_task_specifications_overview", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/phase3.2-implementation-readiness.md", "source_location": "L30", "weight": 1.0}, {"source": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_phase_3_2_3_3_implementation_readiness_architectural_finalization_report", "target": "$graphify-root$_docs_audit_phase3_2_implementation_readiness_3_final_readiness_statement", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/audit/phase3.2-implementation-readiness.md", "source_location": "L47", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_ai_agency_specs_reviews_frontend_review_md", "label": "frontend_review.md", "file_type": "document", "source_file": ".ai_agency/specs/reviews/frontend_review.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_specs_reviews_frontend_review_frontend_admin_panel_technical_review_06_dev_frontend", "label": "\ud83c\udfa8 Frontend & Admin Panel Technical Review (06_dev_frontend)", "file_type": "document", "source_file": ".ai_agency/specs/reviews/frontend_review.md", "source_location": "L1"}, {"id": "$graphify-root$_ai_agency_specs_reviews_frontend_review_architectural_overview", "label": "Architectural Overview", "file_type": "document", "source_file": ".ai_agency/specs/reviews/frontend_review.md", "source_location": "L3"}, {"id": "$graphify-root$_ai_agency_specs_reviews_frontend_review_critical_review_findings_required_enhancements", "label": "Critical Review Findings & Required Enhancements", "file_type": "document", "source_file": ".ai_agency/specs/reviews/frontend_review.md", "source_location": "L7"}], "edges": [{"source": "$graphify-root$_ai_agency_specs_reviews_frontend_review_md", "target": "$graphify-root$_ai_agency_specs_reviews_frontend_review_frontend_admin_panel_technical_review_06_dev_frontend", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/reviews/frontend_review.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_reviews_frontend_review_frontend_admin_panel_technical_review_06_dev_frontend", "target": "$graphify-root$_ai_agency_specs_reviews_frontend_review_architectural_overview", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/reviews/frontend_review.md", "source_location": "L3", "weight": 1.0}, {"source": "$graphify-root$_ai_agency_specs_reviews_frontend_review_frontend_admin_panel_technical_review_06_dev_frontend", "target": "$graphify-root$_ai_agency_specs_reviews_frontend_review_critical_review_findings_required_enhancements", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".ai_agency/specs/reviews/frontend_review.md", "source_location": "L7", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_frontend_admin_panel_public_fonts_shabnam_font_v5_0_1_changelog_md", "label": "CHANGELOG.md", "file_type": "document", "source_file": "frontend/admin-panel/public/fonts/shabnam-font-v5.0.1/CHANGELOG.md", "source_location": "L1"}], "edges": [], "input_tokens": 0, "output_tokens": 0}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "$graphify-root$_agents_workflows_graphify_md", "label": "graphify.md", "file_type": "document", "source_file": ".agents/workflows/graphify.md", "source_location": "L1"}, {"id": "$graphify-root$_agents_workflows_graphify_workflow_graphify", "label": "Workflow: graphify", "file_type": "document", "source_file": ".agents/workflows/graphify.md", "source_location": "L6"}], "edges": [{"source": "$graphify-root$_agents_workflows_graphify_md", "target": "$graphify-root$_agents_workflows_graphify_workflow_graphify", "relation": "contains", "confidence": "EXTRACTED", "source_file": ".agents/workflows/graphify.md", "source_location": "L6", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user