💎 Luxury dark redesign + API integration for Trojan Horse and Elite Dashboard

This commit is contained in:
Hamza-Ayed
2026-04-22 02:01:08 +03:00
parent 2e900e395f
commit 660c551098
8 changed files with 548 additions and 324 deletions

View File

@@ -1,22 +1,38 @@
import { Building2, TrendingUp, AlertTriangle, ChevronDown } from 'lucide-react';
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';
// Mock data matching the API structure we built in the backend
const mockCompanies = [
{ id: '1', name: 'Apex Innovations', totalInvoices: 24, pendingAmount: 78450, totalTax: 12500, failedCount: 2, riskScore: 85, status: 'High' },
{ id: '2', name: 'Quantum Solutions', totalInvoices: 26, pendingAmount: 112000, totalTax: 18000, failedCount: 0, riskScore: 32, status: 'Low' },
{ id: '3', name: 'Nomad Ventures', totalInvoices: 45, pendingAmount: 319000, totalTax: 45000, failedCount: 5, riskScore: 68, status: 'Medium' },
{ id: '4', name: 'Elevate Tech', totalInvoices: 12, pendingAmount: 188000, totalTax: 30000, failedCount: 1, riskScore: 85, status: 'High' },
{ id: '5', name: 'Horizon Group', totalInvoices: 33, pendingAmount: 95000, totalTax: 14000, failedCount: 3, riskScore: 68, status: 'Medium' },
];
interface CompanyStats {
id: string;
name: string;
taxId: string;
totalInvoices: number;
totalTax: number;
failedCount: number;
riskScore: number;
}
const RiskGauge = ({ score, status }: { score: number, status: string }) => {
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">
@@ -41,104 +57,170 @@ const RiskGauge = ({ score, status }: { score: number, status: string }) => {
{score}
</div>
</div>
<span className={`text-xs mt-1 ${getColor()}`}>{status}</span>
<span className={`text-xs mt-1 ${getColor()}`}>{getLabel()}</span>
</div>
);
};
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="min-h-screen bg-slate-950 text-slate-300 font-sans p-8 selection:bg-emerald-500/30">
<div className="space-y-8 animate-in fade-in duration-700">
{/* Header */}
<header className="flex justify-between items-center mb-10">
<header className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-light tracking-tight text-white flex items-center gap-3">
<span className="font-semibold text-emerald-400">مُصادَق</span> | لوحة تحكم الشركات
</h1>
<p className="text-slate-400 mt-2 text-sm">نظرة عامة على الموقف الضريبي لجميع عملائك (Elite View)</p>
<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-500 mt-2 text-sm">نظرة عامة على الموقف الضريبي لجميع عملائك (Elite View)</p>
</div>
<div className="flex gap-4">
<button className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded-lg text-sm border border-slate-700 transition-colors flex items-center gap-2">
آخر 30 يوم <ChevronDown className="w-4 h-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-5 py-2 bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-medium rounded-lg text-sm shadow-[0_0_15px_rgba(16,185,129,0.3)] transition-all">
+ إضافة شركة جديدة
<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 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{mockCompanies.map((company, index) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
key={company.id}
className="bg-slate-900/50 backdrop-blur-xl border border-slate-800 rounded-2xl p-6 relative overflow-hidden hover:border-slate-700 transition-colors group"
>
{/* Ambient glow */}
<div className={`absolute -inset-20 opacity-0 group-hover:opacity-20 blur-3xl transition-opacity duration-500 rounded-full
${company.status === 'High' ? 'bg-red-500' : company.status === 'Medium' ? 'bg-orange-500' : 'bg-emerald-500'}
`} />
{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;
<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-800 flex items-center justify-center border border-slate-700">
<Building2 className="w-5 h-5 text-emerald-400" />
</div>
<div>
<h3 className="text-white font-medium text-lg">{company.name}</h3>
<p className="text-xs text-slate-500">الرقم الضريبي: 00{company.id}829471</p>
</div>
</div>
</div>
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"
>
{/* 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="grid grid-cols-2 gap-4 mb-6">
<div>
<p className="text-xs text-slate-400 mb-1">التدفق النقدي (المبيعات)</p>
<p className="text-2xl font-light text-white">${(company.pendingAmount / 1000).toFixed(1)}k</p>
<div className="flex items-center gap-1 mt-1 text-xs text-emerald-400">
<TrendingUp className="w-3 h-3" /> مستقر
<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-400">{company.taxId || '—'}</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end">
<div>
<p className="text-xs text-slate-400 mb-1 text-center">درجة الخطر الضريبي</p>
<RiskGauge score={company.riskScore} status={company.status} />
</div>
</div>
</div>
<div className="pt-4 border-t border-slate-800/50">
<div className="flex justify-between items-center mb-2">
<p className="text-sm text-slate-300">الفواتير المعلقة</p>
<span className="text-xs text-slate-500">{company.totalInvoices} فاتورة | ${company.totalTax} ضرائب</span>
<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>
{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>
{/* 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>
{/* Minimal Progress Bar */}
<div className="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden flex">
<div className="h-full bg-emerald-500" style={{ width: '60%' }}></div>
<div className="h-full bg-orange-500" style={{ width: '25%' }}></div>
<div className="h-full bg-red-500" style={{ width: '15%' }}></div>
</div>
<div className="flex justify-between mt-2 text-[10px] text-slate-500">
<span>تم الرفع (60%)</span>
{company.failedCount > 0 && (
<span className="text-red-400 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" /> {company.failedCount} مرفوضة
</span>
)}
</div>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
);
})}
</div>
)}
</div>
);
};