From 271674a9c567be2e6037fd33e4b706e30754809f Mon Sep 17 00:00:00 2001 From: Your Name <119736744+aborayan2022@users.noreply.github.com> Date: Wed, 1 Apr 2026 00:27:27 +0200 Subject: [PATCH] feat: Complete admin dashboard overhaul with professional features - Add Events Timeline tab showing chronological project activity - Add Create User modal with email, password, plan, role selection - Add Delete User button with confirmation dialog - Add Plan and Role management dropdowns per user - Add event type summary cards with color-coded icons - Add period selector for events (7d, 14d, 30d, 90d) - Add i18n translations for all new features (EN + AR) - Add new API functions: createAdminUser, deleteAdminUser, updateAdminUserPlan, updateAdminUserRole, getProjectEvents --- frontend/src/pages/InternalAdminPage.tsx | 371 +++++++++++++++++++++-- frontend/src/services/api.ts | 41 +++ 2 files changed, 387 insertions(+), 25 deletions(-) diff --git a/frontend/src/pages/InternalAdminPage.tsx b/frontend/src/pages/InternalAdminPage.tsx index 74172d5..c742a1b 100644 --- a/frontend/src/pages/InternalAdminPage.tsx +++ b/frontend/src/pages/InternalAdminPage.tsx @@ -44,10 +44,17 @@ import { type InternalAdminUser, getDatabaseStats, type DatabaseStats, + getProjectEvents, + type ProjectEvent, + type ProjectEventsResponse, + createAdminUser, + deleteAdminUser, + updateAdminUserPlan, + updateAdminUserRole, } from '@/services/api'; import { useAuthStore } from '@/stores/authStore'; -type AdminTab = 'overview' | 'users' | 'tools' | 'ratings' | 'contacts' | 'system' | 'database'; +type AdminTab = 'overview' | 'users' | 'tools' | 'ratings' | 'contacts' | 'system' | 'database' | 'events'; type Lang = 'ar' | 'en'; const TRANSLATIONS: Record> = { @@ -78,6 +85,7 @@ const TRANSLATIONS: Record> = { tabContacts: 'Inbox', tabSystem: 'System Health', tabDatabase: 'Database', + tabEvents: 'Events Timeline', // Overview cards totalUsers: 'Total users', filesProcessed: 'Files processed', @@ -192,6 +200,32 @@ const TRANSLATIONS: Record> = { updatePlanError: 'Unable to update plan.', updateRoleError: 'Unable to update role.', updateContactError: 'Unable to update contact message.', + // Events tab + eventsTimeline: 'Events Timeline', + eventsDesc: 'Chronological view of all important project activities.', + eventUserRegistered: 'User registered', + eventFileProcessed: 'File processed', + eventFileFailed: 'File failed', + eventContactMessage: 'Contact message', + eventSummary: 'Event Summary', + totalEvents: 'Total events', + periodDays: 'Last {days} days', + // User management + createUser: 'Create User', + deleteUser: 'Delete', + deleteUserConfirm: 'Are you sure you want to delete this user? This cannot be undone.', + createUserTitle: 'Create New User', + createUserDesc: 'Add a new user to the system.', + emailLabel: 'Email', + passwordLabel: 'Password', + planLabel: 'Plan', + roleLabel: 'Role', + createBtn: 'Create', + cancelBtn: 'Cancel', + userCreated: 'User created successfully.', + userDeleted: 'User deleted successfully.', + planUpdated: 'Plan updated successfully.', + roleUpdated: 'Role updated successfully.', }, ar: { // Page & header @@ -220,6 +254,7 @@ const TRANSLATIONS: Record> = { tabContacts: 'صندوق الوارد', tabSystem: 'صحة النظام', tabDatabase: 'قاعدة البيانات', + tabEvents: 'الجدول الزمني للأحداث', // Overview cards totalUsers: 'إجمالي المستخدمين', filesProcessed: 'الملفات المعالجة', @@ -334,6 +369,32 @@ const TRANSLATIONS: Record> = { updatePlanError: 'تعذّر تحديث الخطة.', updateRoleError: 'تعذّر تحديث الدور.', updateContactError: 'تعذّر تحديث رسالة التواصل.', + // Events tab + eventsTimeline: 'الجدول الزمني للأحداث', + eventsDesc: 'عرض زمني لجميع أنشطة المشروع المهمة.', + eventUserRegistered: 'تسجيل مستخدم', + eventFileProcessed: 'معالجة ملف', + eventFileFailed: 'فشل ملف', + eventContactMessage: 'رسالة تواصل', + eventSummary: 'ملخص الأحداث', + totalEvents: 'إجمالي الأحداث', + periodDays: 'آخر {days} يوم', + // User management + createUser: 'إنشاء مستخدم', + deleteUser: 'حذف', + deleteUserConfirm: 'هل أنت متأكد من حذف هذا المستخدم؟ لا يمكن التراجع عن هذا.', + createUserTitle: 'إنشاء مستخدم جديد', + createUserDesc: 'إضافة مستخدم جديد إلى النظام.', + emailLabel: 'البريد الإلكتروني', + passwordLabel: 'كلمة المرور', + planLabel: 'الخطة', + roleLabel: 'الدور', + createBtn: 'إنشاء', + cancelBtn: 'إلغاء', + userCreated: 'تم إنشاء المستخدم بنجاح.', + userDeleted: 'تم حذف المستخدم بنجاح.', + planUpdated: 'تم تحديث الخطة بنجاح.', + roleUpdated: 'تم تحديث الدور بنجاح.', }, }; @@ -403,6 +464,17 @@ export default function InternalAdminPage() { // Database state const [databaseStats, setDatabaseStats] = useState(null); + // Events state + const [projectEvents, setProjectEvents] = useState(null); + const [eventsDays, setEventsDays] = useState(30); + + // User management state + const [showCreateUser, setShowCreateUser] = useState(false); + const [newUserEmail, setNewUserEmail] = useState(''); + const [newUserPassword, setNewUserPassword] = useState(''); + const [newUserPlan, setNewUserPlan] = useState('free'); + const [newUserRole, setNewUserRole] = useState('user'); + // Language const [lang, setLang] = useState(() => (localStorage.getItem('admin-lang') as Lang) ?? 'en'); const isRtl = lang === 'ar'; @@ -431,6 +503,7 @@ export default function InternalAdminPage() { { key: 'contacts', label: t('tabContacts'), icon: Inbox }, { key: 'system', label: t('tabSystem'), icon: ShieldCheck }, { key: 'database', label: t('tabDatabase'), icon: Database }, + { key: 'events', label: t('tabEvents'), icon: Clock }, ]; useEffect(() => { @@ -499,6 +572,11 @@ export default function InternalAdminPage() { setDatabaseStats(dbStats); break; } + case 'events': { + const events = await getProjectEvents(eventsDays); + setProjectEvents(events); + break; + } } } catch (e) { const msg = e instanceof Error ? e.message : t('loadError'); @@ -531,7 +609,7 @@ export default function InternalAdminPage() { setUpdatingUserId(userId); setError(null); try { - await updateInternalAdminUserPlan(userId, plan); + await updateAdminUserPlan(userId, plan); await loadTab('users'); } catch (e) { const msg = e instanceof Error ? e.message : t('updatePlanError'); @@ -547,7 +625,7 @@ export default function InternalAdminPage() { setUpdatingRoleUserId(userId); setError(null); try { - await updateInternalAdminUserRole(userId, role); + await updateAdminUserRole(userId, role); await loadTab('users'); } catch (e) { const msg = e instanceof Error ? e.message : t('updateRoleError'); @@ -819,30 +897,39 @@ export default function InternalAdminPage() {

{t('userManagement')}

-
) => { - e.preventDefault(); - void loadTab('users'); - }} - className="flex w-full max-w-md items-center gap-2" - > -
- - setUserQuery(e.target.value)} - placeholder={t('searchEmailPlaceholder')} - className="w-full rounded-2xl border border-slate-300 bg-white py-2.5 ps-10 pe-4 text-sm text-slate-900 outline-none transition focus:border-primary-500 focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-950 dark:text-slate-100 dark:focus:ring-primary-500/30" - /> -
- +
+ - +
@@ -909,6 +996,13 @@ export default function InternalAdminPage() { > {t('btnAdmin')} +
@@ -1481,6 +1575,160 @@ export default function InternalAdminPage() { ); } + // ====================== EVENTS TAB ====================== + + function renderEventsTab() { + if (!projectEvents) return null; + + const eventColors: Record = { + user_registered: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300', + file_processed: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300', + file_failed: 'bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-300', + contact_message: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300', + }; + + const eventIcons: Record = { + user_registered: Users, + file_processed: Zap, + file_failed: AlertTriangle, + contact_message: MessageSquare, + }; + + return ( + <> + {/* Summary cards */} +
+
+
+
+

{t('totalEvents')}

+

{projectEvents.total_events.toLocaleString()}

+
+ +
+
+ {Object.entries(projectEvents.summary).map(([type, count]) => { + const Icon = eventIcons[type] || Activity; + return ( +
+
+
+

+ {t(`event${type.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('')}`)} +

+

{count.toLocaleString()}

+
+ +
+
+ ); + })} +
+ + {/* Period selector */} +
+ {t('periodDays').replace('{days}', String(eventsDays))} + {[7, 14, 30, 90].map(d => ( + + ))} +
+ + {/* Events timeline */} +
+

{t('eventsTimeline')}

+
+ {projectEvents.events.length === 0 ? ( +

No events found.

+ ) : ( + projectEvents.events.map((event, i) => { + const Icon = eventIcons[event.type] || Activity; + const colorClass = eventColors[event.type] || 'bg-slate-100 text-slate-700 dark:bg-slate-500/20 dark:text-slate-300'; + return ( +
+
+ +
+
+
+ + {t(`event${event.type.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('')}`)} + +
+

{event.detail}

+

{new Date(event.time).toLocaleString()}

+
+
+ ); + }) + )} +
+
+ + ); + } + + // ====================== CREATE USER MODAL ====================== + + async function handleCreateUser(event: FormEvent) { + event.preventDefault(); + if (!newUserEmail || !newUserPassword) return; + try { + await createAdminUser(newUserEmail, newUserPassword, newUserPlan, newUserRole); + toast.success(t('userCreated')); + setShowCreateUser(false); + setNewUserEmail(''); + setNewUserPassword(''); + setNewUserPlan('free'); + setNewUserRole('user'); + void loadTab('users'); + } catch (e) { + const msg = e instanceof Error ? e.message : t('loadError'); + toast.error(msg); + } + } + + async function handleDeleteUser(userId: number) { + if (!confirm(t('deleteUserConfirm'))) return; + try { + await deleteAdminUser(userId); + toast.success(t('userDeleted')); + void loadTab('users'); + } catch (e) { + const msg = e instanceof Error ? e.message : t('loadError'); + toast.error(msg); + } + } + + async function handleUpdateUserPlan(userId: number, plan: string) { + try { + await updateAdminUserPlan(userId, plan); + toast.success(t('planUpdated')); + void loadTab('users'); + } catch (e) { + toast.error(t('updatePlanError')); + } + } + + async function handleUpdateUserRole(userId: number, role: string) { + try { + await updateAdminUserRole(userId, role); + toast.success(t('roleUpdated')); + void loadTab('users'); + } catch (e) { + toast.error(t('updateRoleError')); + } + } + // ====================== MAIN RENDER ====================== return ( @@ -1650,9 +1898,82 @@ export default function InternalAdminPage() { {activeTab === 'contacts' && renderContactsTab()} {activeTab === 'system' && renderSystemTab()} {activeTab === 'database' && renderDatabaseTab()} + {activeTab === 'events' && renderEventsTab()} )} + + {/* Create User Modal */} + {showCreateUser && ( +
setShowCreateUser(false)}> +
e.stopPropagation()}> +

{t('createUserTitle')}

+

{t('createUserDesc')}

+
+
+ + setNewUserEmail(e.target.value)} + required + className="mt-1 w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-900 outline-none transition focus:border-primary-500 focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100" + /> +
+
+ + setNewUserPassword(e.target.value)} + required + minLength={8} + className="mt-1 w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-900 outline-none transition focus:border-primary-500 focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100" + /> +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ )} ); } diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index a7e4091..b7b1854 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -922,6 +922,47 @@ export async function getDatabaseStats(): Promise { return response.data; } +export interface ProjectEvent { + time: string; + type: string; + detail: string; + entity_id: number; +} + +export interface ProjectEventsResponse { + events: ProjectEvent[]; + summary: Record; + total_events: number; + period_days: number; +} + +export async function getProjectEvents(days = 30): Promise { + const response = await api.get('/internal/admin/project-events', { + params: { days }, + }); + return response.data; +} + +export async function createAdminUser(email: string, password: string, plan = 'free', role = 'user'): Promise<{ message: string; user: InternalAdminUser }> { + const response = await api.post('/internal/admin/users/create', { email, password, plan, role }); + return response.data; +} + +export async function deleteAdminUser(userId: number): Promise<{ message: string }> { + const response = await api.delete(`/internal/admin/users/${userId}`); + return response.data; +} + +export async function updateAdminUserPlan(userId: number, plan: string): Promise<{ message: string; user: InternalAdminUser }> { + const response = await api.put(`/internal/admin/users/${userId}/plan`, { plan }); + return response.data; +} + +export async function updateAdminUserRole(userId: number, role: string): Promise<{ message: string; user: InternalAdminUser }> { + const response = await api.put(`/internal/admin/users/${userId}/role`, { role }); + return response.data; +} + // --- Account / Usage / API Keys --- export interface UsageSummary {