CognxSafeTrack commited on
Commit ·
fe40cec
1
Parent(s): 0aedaaf
ux: clear messages for tenant selection in admin dashboard
Browse files
apps/admin/src/pages/DashboardPage.tsx
CHANGED
|
@@ -1,18 +1,29 @@
|
|
| 1 |
import { useEffect, useState } from 'react';
|
| 2 |
-
import { Users, PlayCircle, CheckCircle, Lightbulb, DollarSign, Download } from 'lucide-react';
|
| 3 |
import { useAuth } from '../lib/auth';
|
|
|
|
| 4 |
import { API_URL } from '../lib/api';
|
| 5 |
|
| 6 |
export default function DashboardPage() {
|
| 7 |
const { apiKey, logout } = useAuth();
|
|
|
|
| 8 |
const [stats, setStats] = useState<any>(null);
|
| 9 |
const [enrollments, setEnrollments] = useState<any[]>([]);
|
| 10 |
const [loading, setLoading] = useState(true);
|
| 11 |
|
| 12 |
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
(async () => {
|
|
|
|
| 14 |
try {
|
| 15 |
-
const h = {
|
|
|
|
|
|
|
|
|
|
| 16 |
const [sRes, eRes] = await Promise.all([
|
| 17 |
fetch(`${API_URL}/v1/admin/stats`, { headers: h }),
|
| 18 |
fetch(`${API_URL}/v1/admin/enrollments`, { headers: h })
|
|
@@ -22,7 +33,7 @@ export default function DashboardPage() {
|
|
| 22 |
setEnrollments(await eRes.json());
|
| 23 |
} finally { setLoading(false); }
|
| 24 |
})();
|
| 25 |
-
}, [apiKey, logout]);
|
| 26 |
|
| 27 |
const exportCSV = () => {
|
| 28 |
if (!enrollments.length) return alert('Aucune inscription.');
|
|
@@ -34,7 +45,29 @@ export default function DashboardPage() {
|
|
| 34 |
a.click();
|
| 35 |
};
|
| 36 |
|
| 37 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
const statCards = [
|
| 40 |
{ icon: <Users className="w-6 h-6 text-slate-400" />, label: 'Utilisateurs', value: stats?.totalUsers || 0, color: 'text-slate-900' },
|
|
|
|
| 1 |
import { useEffect, useState } from 'react';
|
| 2 |
+
import { Users, PlayCircle, CheckCircle, Lightbulb, DollarSign, Download, Building2, Loader2 } from 'lucide-react';
|
| 3 |
import { useAuth } from '../lib/auth';
|
| 4 |
+
import { useTenant } from '../lib/tenant';
|
| 5 |
import { API_URL } from '../lib/api';
|
| 6 |
|
| 7 |
export default function DashboardPage() {
|
| 8 |
const { apiKey, logout } = useAuth();
|
| 9 |
+
const { selectedOrgId } = useTenant();
|
| 10 |
const [stats, setStats] = useState<any>(null);
|
| 11 |
const [enrollments, setEnrollments] = useState<any[]>([]);
|
| 12 |
const [loading, setLoading] = useState(true);
|
| 13 |
|
| 14 |
useEffect(() => {
|
| 15 |
+
if (!selectedOrgId) {
|
| 16 |
+
setLoading(false);
|
| 17 |
+
return;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
(async () => {
|
| 21 |
+
setLoading(true);
|
| 22 |
try {
|
| 23 |
+
const h = {
|
| 24 |
+
'Authorization': `Bearer ${apiKey}`,
|
| 25 |
+
'x-organization-id': selectedOrgId
|
| 26 |
+
};
|
| 27 |
const [sRes, eRes] = await Promise.all([
|
| 28 |
fetch(`${API_URL}/v1/admin/stats`, { headers: h }),
|
| 29 |
fetch(`${API_URL}/v1/admin/enrollments`, { headers: h })
|
|
|
|
| 33 |
setEnrollments(await eRes.json());
|
| 34 |
} finally { setLoading(false); }
|
| 35 |
})();
|
| 36 |
+
}, [apiKey, logout, selectedOrgId]);
|
| 37 |
|
| 38 |
const exportCSV = () => {
|
| 39 |
if (!enrollments.length) return alert('Aucune inscription.');
|
|
|
|
| 45 |
a.click();
|
| 46 |
};
|
| 47 |
|
| 48 |
+
if (!selectedOrgId) {
|
| 49 |
+
return (
|
| 50 |
+
<div className="flex flex-col items-center justify-center min-h-[80vh] text-slate-400">
|
| 51 |
+
<Building2 className="w-16 h-16 mb-6 opacity-20" />
|
| 52 |
+
<h2 className="text-2xl font-bold text-slate-900">Bienvenue sur EdTech Admin</h2>
|
| 53 |
+
<p className="max-w-md text-center mt-3 text-lg">
|
| 54 |
+
Pour commencer, veuillez sélectionner une **organisation** dans le menu déroulant à gauche.
|
| 55 |
+
</p>
|
| 56 |
+
<div className="mt-8 p-4 bg-blue-50 text-blue-700 rounded-2xl text-sm font-medium border border-blue-100">
|
| 57 |
+
💡 L'isolation des données garantit que vous ne voyez que les statistiques de l'organisation active.
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
if (loading) {
|
| 64 |
+
return (
|
| 65 |
+
<div className="flex flex-col items-center justify-center min-h-[80vh] text-slate-400">
|
| 66 |
+
<Loader2 className="w-10 h-10 animate-spin mb-4 text-slate-900" />
|
| 67 |
+
<p className="text-lg font-medium">Analyse des données en cours...</p>
|
| 68 |
+
</div>
|
| 69 |
+
);
|
| 70 |
+
}
|
| 71 |
|
| 72 |
const statCards = [
|
| 73 |
{ icon: <Users className="w-6 h-6 text-slate-400" />, label: 'Utilisateurs', value: stats?.totalUsers || 0, color: 'text-slate-900' },
|
apps/admin/src/pages/TrackListPage.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { useEffect, useState } from 'react';
|
| 2 |
import { useNavigate } from 'react-router-dom';
|
| 3 |
-
import { BookOpen, Plus, Edit2, Trash2, ChevronRight } from 'lucide-react';
|
| 4 |
import { useAuth } from '../lib/auth';
|
| 5 |
import { useTenant } from '../lib/tenant';
|
| 6 |
import { API_URL } from '../lib/api';
|
|
@@ -36,7 +36,24 @@ export default function TrackListPage() {
|
|
| 36 |
load();
|
| 37 |
};
|
| 38 |
|
| 39 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
return (
|
| 42 |
<div className="p-8">
|
|
|
|
| 1 |
import { useEffect, useState } from 'react';
|
| 2 |
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { BookOpen, Plus, Edit2, Trash2, ChevronRight, Building2 } from 'lucide-react';
|
| 4 |
import { useAuth } from '../lib/auth';
|
| 5 |
import { useTenant } from '../lib/tenant';
|
| 6 |
import { API_URL } from '../lib/api';
|
|
|
|
| 36 |
load();
|
| 37 |
};
|
| 38 |
|
| 39 |
+
if (!selectedOrgId) {
|
| 40 |
+
return (
|
| 41 |
+
<div className="flex flex-col items-center justify-center min-h-[60vh] text-slate-400">
|
| 42 |
+
<Building2 className="w-12 h-12 mb-4 opacity-20" />
|
| 43 |
+
<h3 className="text-lg font-bold text-slate-900">Aucune organisation sélectionnée</h3>
|
| 44 |
+
<p className="max-w-xs text-center mt-2">Veuillez sélectionner une organisation dans le menu en haut à gauche pour voir ses parcours.</p>
|
| 45 |
+
</div>
|
| 46 |
+
);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
if (loading) {
|
| 50 |
+
return (
|
| 51 |
+
<div className="flex flex-col items-center justify-center min-h-[60vh] text-slate-400">
|
| 52 |
+
<div className="w-8 h-8 border-4 border-slate-200 border-t-slate-900 rounded-full animate-spin mb-4"></div>
|
| 53 |
+
<p>Chargement des parcours...</p>
|
| 54 |
+
</div>
|
| 55 |
+
);
|
| 56 |
+
}
|
| 57 |
|
| 58 |
return (
|
| 59 |
<div className="p-8">
|
apps/admin/src/pages/UserListPage.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
| 1 |
import { useEffect, useState } from 'react';
|
| 2 |
-
import { X } from 'lucide-react';
|
| 3 |
import { useAuth } from '../lib/auth';
|
|
|
|
| 4 |
import { API_URL } from '../lib/api';
|
| 5 |
|
| 6 |
export default function UserListPage() {
|
| 7 |
const { apiKey } = useAuth();
|
|
|
|
| 8 |
const [users, setUsers] = useState<any[]>([]);
|
| 9 |
const [total, setTotal] = useState(0);
|
| 10 |
const [loading, setLoading] = useState(true);
|
|
@@ -12,9 +14,18 @@ export default function UserListPage() {
|
|
| 12 |
const [messages, setMessages] = useState<any[]>([]);
|
| 13 |
const [loadingMsg, setLoadingMsg] = useState(false);
|
| 14 |
|
| 15 |
-
const ah = (k: string) => ({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
fetch(`${API_URL}/v1/admin/users`, { headers: ah(apiKey!) })
|
| 19 |
.then(r => r.json())
|
| 20 |
.then(d => {
|
|
@@ -22,7 +33,7 @@ export default function UserListPage() {
|
|
| 22 |
setTotal(d.total || 0);
|
| 23 |
setLoading(false);
|
| 24 |
});
|
| 25 |
-
}, [apiKey]);
|
| 26 |
|
| 27 |
const viewMessages = async (userId: string) => {
|
| 28 |
setLoadingMsg(true);
|
|
@@ -39,7 +50,24 @@ export default function UserListPage() {
|
|
| 39 |
}
|
| 40 |
};
|
| 41 |
|
| 42 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
return (
|
| 45 |
<div className="p-8">
|
|
|
|
| 1 |
import { useEffect, useState } from 'react';
|
| 2 |
+
import { X, Building2, Loader2 } from 'lucide-react';
|
| 3 |
import { useAuth } from '../lib/auth';
|
| 4 |
+
import { useTenant } from '../lib/tenant';
|
| 5 |
import { API_URL } from '../lib/api';
|
| 6 |
|
| 7 |
export default function UserListPage() {
|
| 8 |
const { apiKey } = useAuth();
|
| 9 |
+
const { selectedOrgId } = useTenant();
|
| 10 |
const [users, setUsers] = useState<any[]>([]);
|
| 11 |
const [total, setTotal] = useState(0);
|
| 12 |
const [loading, setLoading] = useState(true);
|
|
|
|
| 14 |
const [messages, setMessages] = useState<any[]>([]);
|
| 15 |
const [loadingMsg, setLoadingMsg] = useState(false);
|
| 16 |
|
| 17 |
+
const ah = (k: string) => ({
|
| 18 |
+
'Authorization': `Bearer ${k}`,
|
| 19 |
+
'Content-Type': 'application/json',
|
| 20 |
+
...(selectedOrgId ? { 'x-organization-id': selectedOrgId } : {})
|
| 21 |
+
});
|
| 22 |
|
| 23 |
useEffect(() => {
|
| 24 |
+
if (!selectedOrgId) {
|
| 25 |
+
setLoading(false);
|
| 26 |
+
return;
|
| 27 |
+
}
|
| 28 |
+
setLoading(true);
|
| 29 |
fetch(`${API_URL}/v1/admin/users`, { headers: ah(apiKey!) })
|
| 30 |
.then(r => r.json())
|
| 31 |
.then(d => {
|
|
|
|
| 33 |
setTotal(d.total || 0);
|
| 34 |
setLoading(false);
|
| 35 |
});
|
| 36 |
+
}, [apiKey, selectedOrgId]);
|
| 37 |
|
| 38 |
const viewMessages = async (userId: string) => {
|
| 39 |
setLoadingMsg(true);
|
|
|
|
| 50 |
}
|
| 51 |
};
|
| 52 |
|
| 53 |
+
if (!selectedOrgId) {
|
| 54 |
+
return (
|
| 55 |
+
<div className="flex flex-col items-center justify-center min-h-[60vh] text-slate-400">
|
| 56 |
+
<Building2 className="w-12 h-12 mb-4 opacity-20" />
|
| 57 |
+
<h3 className="text-lg font-bold text-slate-900">Aucune organisation sélectionnée</h3>
|
| 58 |
+
<p className="max-w-xs text-center mt-2">Sélectionnez une organisation pour gérer ses utilisateurs.</p>
|
| 59 |
+
</div>
|
| 60 |
+
);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
if (loading) {
|
| 64 |
+
return (
|
| 65 |
+
<div className="flex flex-col items-center justify-center min-h-[60vh] text-slate-400">
|
| 66 |
+
<Loader2 className="w-8 h-8 animate-spin mb-4 text-slate-900" />
|
| 67 |
+
<p>Chargement des utilisateurs...</p>
|
| 68 |
+
</div>
|
| 69 |
+
);
|
| 70 |
+
}
|
| 71 |
|
| 72 |
return (
|
| 73 |
<div className="p-8">
|