Files
musadeq/frontend/src/pages/dashboard/MultiEntityDashboard.tsx

271 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { Building2, TrendingUp, AlertTriangle, ChevronDown, Loader2, RefreshCw, Crown } from 'lucide-react';
import { motion } from 'framer-motion';
import apiClient from '../../api/client';
interface CompanyStats {
id: string;
name: string;
taxId: string;
totalInvoices: number;
totalTax: number;
failedCount: number;
riskScore: number;
aiStats: {
totalTokens: number;
totalCost: number;
};
}
const getRiskStatus = (score: number) => {
if (score >= 70) return 'High';
if (score >= 30) return 'Medium';
return 'Low';
};
const RiskGauge = ({ score }: { score: number }) => {
const status = getRiskStatus(score);
const getColor = () => {
if (status === 'High') return 'text-red-500';
if (status === 'Medium') return 'text-orange-500';
return 'text-emerald-500';
};
const getLabel = () => {
if (status === 'High') return 'مرتفع';
if (status === 'Medium') return 'متوسط';
return 'منخفض';
};
return (
<div className="flex flex-col items-center">
<div className="relative w-16 h-16">
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 36 36">
<path
className="text-slate-700"
strokeWidth="3"
stroke="currentColor"
fill="none"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
/>
<path
className={getColor()}
strokeWidth="3"
strokeDasharray={`${score}, 100`}
stroke="currentColor"
fill="none"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center text-lg font-bold text-white">
{score}
</div>
</div>
<span className={`text-xs mt-1 ${getColor()}`}>{getLabel()}</span>
</div>
);
};
const Cpu = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24" height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"/>
<rect x="9" y="9" width="6" height="6"/>
<line x1="9" y1="1" x2="9" y2="4"/>
<line x1="15" y1="1" x2="15" y2="4"/>
<line x1="9" y1="20" x2="9" y2="23"/>
<line x1="15" y1="20" x2="15" y2="23"/>
<line x1="20" y1="9" x2="23" y2="9"/>
<line x1="20" y1="15" x2="23" y2="15"/>
<line x1="1" y1="9" x2="4" y2="9"/>
<line x1="1" y1="15" x2="4" y2="15"/>
</svg>
);
export const MultiEntityDashboard = () => {
const [companies, setCompanies] = useState<CompanyStats[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = async () => {
setIsLoading(true);
setError(null);
try {
const { data } = await apiClient.get('/dashboard/multi-entity');
setCompanies(data);
} catch (err: any) {
console.error('Failed to fetch multi-entity stats', err);
setError('فشل في جلب بيانات الشركات. تأكد من تسجيل الدخول.');
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
if (isLoading) {
return (
<div className="flex-1 flex flex-col justify-center items-center py-32">
<Loader2 className="w-12 h-12 text-emerald-400 animate-spin mb-4" />
<p className="text-slate-400">جاري تحميل بيانات الشركات...</p>
</div>
);
}
return (
<div className="space-y-8 animate-in fade-in duration-700">
{/* Header */}
<header className="flex justify-between items-center">
<div>
<h2 className="text-3xl font-light tracking-tight text-slate-900 dark:text-white flex items-center gap-3">
<Crown className="w-7 h-7 text-emerald-500" />
<span className="font-semibold text-emerald-500">مُصادَق</span> | لوحة تحكم الشركات
</h2>
<p className="text-slate-300 mt-2 text-sm">نظرة عامة على الموقف الضريبي لجميع عملائك (Elite View)</p>
</div>
<div className="flex gap-4">
<button
onClick={fetchData}
className="px-4 py-2 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-white rounded-lg text-sm border border-slate-200 dark:border-slate-700 transition-colors flex items-center gap-2"
>
<RefreshCw className="w-4 h-4" /> تحديث
</button>
<button className="px-4 py-2 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-white rounded-lg text-sm border border-slate-200 dark:border-slate-700 transition-colors flex items-center gap-2">
آخر 30 يوم <ChevronDown className="w-4 h-4" />
</button>
</div>
</header>
{/* Error State */}
{error && (
<div className="bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 rounded-xl p-4 flex items-center gap-3 text-red-600 dark:text-red-400">
<AlertTriangle className="w-5 h-5 flex-shrink-0" />
<span className="text-sm font-medium">{error}</span>
<button onClick={fetchData} className="mr-auto text-sm underline hover:no-underline">إعادة المحاولة</button>
</div>
)}
{/* Empty State */}
{!error && companies.length === 0 && (
<div className="text-center py-20">
<Building2 className="w-16 h-16 text-slate-300 dark:text-slate-600 mx-auto mb-6" />
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-2">لا توجد شركات بعد</h3>
<p className="text-slate-500 mb-6">أضف شركات عملائك لتبدأ بمتابعة الموقف الضريبي لكل شركة.</p>
<button className="px-5 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white font-medium rounded-lg text-sm shadow-lg shadow-emerald-500/20 transition-all">
+ إضافة شركة جديدة
</button>
</div>
)}
{/* Grid */}
{companies.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{companies.map((company, index) => {
const status = getRiskStatus(company.riskScore);
const approvedEstimate = Math.max(0, company.totalInvoices - company.failedCount);
const approvedPct = company.totalInvoices > 0 ? Math.round((approvedEstimate / company.totalInvoices) * 100) : 0;
const failedPct = company.totalInvoices > 0 ? Math.round((company.failedCount / company.totalInvoices) * 100) : 0;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.08 }}
key={company.id}
className="card-premium p-6 relative overflow-hidden group"
>
{/* AI Usage Badge */}
<div className="absolute top-0 right-0 p-2">
<div className="flex items-center gap-1 bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded-md text-[10px] font-bold text-slate-300 dark:text-slate-200 border border-slate-200 dark:border-slate-700">
<Cpu className="w-3 h-3 text-purple-400" />
<span>{company.aiStats?.totalTokens > 1000 ? `${(company.aiStats.totalTokens / 1000).toFixed(1)}k` : company.aiStats?.totalTokens || 0} tokens</span>
</div>
</div>
{/* Ambient glow */}
<div className={`absolute -inset-20 opacity-0 group-hover:opacity-10 blur-3xl transition-opacity duration-500 rounded-full
${status === 'High' ? 'bg-red-500' : status === 'Medium' ? 'bg-orange-500' : 'bg-emerald-500'}
`} />
<div className="relative z-10">
<div className="flex justify-between items-start mb-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center border border-slate-200 dark:border-slate-700">
<Building2 className="w-5 h-5 text-emerald-500" />
</div>
<div>
<h3 className="text-slate-900 dark:text-white font-medium text-lg">{company.name}</h3>
<p className="text-xs text-slate-300">{company.taxId || '—'}</p>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div>
<p className="text-xs text-slate-500 mb-1">إجمالي الضريبة</p>
<p className="text-2xl font-light text-slate-900 dark:text-white">
{company.totalTax > 0 ? `${(company.totalTax / 1000).toFixed(1)}k` : '0'}
</p>
<div className="flex items-center gap-1 mt-1 text-xs text-emerald-500">
<TrendingUp className="w-3 h-3" /> JOD
</div>
</div>
<div className="flex justify-end">
<div>
<p className="text-xs text-slate-500 mb-1 text-center">درجة الخطر الضريبي</p>
<RiskGauge score={company.riskScore} />
</div>
</div>
</div>
<div className="pt-4 border-t border-slate-100 dark:border-slate-800/50">
<div className="flex justify-between items-center mb-2">
<p className="text-sm text-slate-600 dark:text-slate-300">{company.totalInvoices} فاتورة</p>
<div className="flex items-center gap-2">
{company.aiStats?.totalCost > 0 && (
<span className="text-[10px] font-black text-purple-400 bg-purple-500/10 px-2 py-0.5 rounded border border-purple-500/20">
${company.aiStats.totalCost.toFixed(3)}
</span>
)}
{company.failedCount > 0 && (
<span className="text-xs text-red-400 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" /> {company.failedCount} مرفوضة
</span>
)}
</div>
</div>
{/* Progress Bar */}
<div className="h-1.5 w-full bg-slate-100 dark:bg-slate-800 rounded-full overflow-hidden flex">
<div className="h-full bg-emerald-500 transition-all" style={{ width: `${approvedPct}%` }}></div>
<div className="h-full bg-red-500 transition-all" style={{ width: `${failedPct}%` }}></div>
</div>
<div className="flex justify-between mt-2 text-[10px] text-slate-400">
<span>ناجحة ({approvedPct}%)</span>
{company.failedCount > 0 && <span className="text-red-400">مرفوضة ({failedPct}%)</span>}
</div>
</div>
</div>
</motion.div>
);
})}
</div>
)}
</div>
);
};