CognxSafeTrack commited on
Commit
3b473c3
Β·
1 Parent(s): a343cb3

feat(admin): add viewing full chat history in user list

Browse files
apps/admin/src/App.tsx CHANGED
@@ -317,7 +317,24 @@ function TrackDays() {
317
  function UserList() {
318
  const { apiKey } = useAuth();
319
  const [users, setUsers] = useState<any[]>([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true);
 
 
320
  useEffect(() => { fetch(`${API_URL}/v1/admin/users`, { headers: ah(apiKey!) }).then(r => r.json()).then(d => { setUsers(d.users || d); setTotal(d.total || 0); setLoading(false); }); }, []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  if (loading) return <div className="p-8 text-slate-400">Chargement...</div>;
322
  return (
323
  <div className="p-8">
@@ -325,7 +342,7 @@ function UserList() {
325
  <div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
326
  <table className="w-full text-sm">
327
  <thead className="bg-slate-50 text-xs text-slate-500 uppercase">
328
- <tr>{['TΓ©lΓ©phone', 'Nom', 'Langue', 'Secteur', 'Inscrip.', 'RΓ©ponses', 'Date'].map(h => <th key={h} className="px-5 py-3 text-left">{h}</th>)}</tr>
329
  </thead>
330
  <tbody>
331
  {users.map((u: any) => (
@@ -337,12 +354,58 @@ function UserList() {
337
  <td className="px-5 py-3 text-center">{u._count?.enrollments || 0}</td>
338
  <td className="px-5 py-3 text-center">{u._count?.responses || 0}</td>
339
  <td className="px-5 py-3 text-slate-400 text-xs">{new Date(u.createdAt).toLocaleDateString('fr-FR')}</td>
 
 
 
340
  </tr>
341
  ))}
342
- {!users.length && <tr><td colSpan={7} className="px-5 py-8 text-center text-slate-400">Aucun utilisateur</td></tr>}
343
  </tbody>
344
  </table>
345
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  </div>
347
  );
348
  }
 
317
  function UserList() {
318
  const { apiKey } = useAuth();
319
  const [users, setUsers] = useState<any[]>([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true);
320
+ const [selectedUser, setSelectedUser] = useState<any>(null); const [messages, setMessages] = useState<any[]>([]); const [loadingMsg, setLoadingMsg] = useState(false);
321
+
322
  useEffect(() => { fetch(`${API_URL}/v1/admin/users`, { headers: ah(apiKey!) }).then(r => r.json()).then(d => { setUsers(d.users || d); setTotal(d.total || 0); setLoading(false); }); }, []);
323
+
324
+ const viewMessages = async (userId: string) => {
325
+ setLoadingMsg(true); setSelectedUser({ id: userId });
326
+ try {
327
+ const res = await fetch(`${API_URL}/v1/admin/users/${userId}/messages`, { headers: ah(apiKey!) });
328
+ const data = await res.json();
329
+ setSelectedUser(data.user);
330
+ setMessages(data.messages || []);
331
+ } catch (e) {
332
+ alert("Erreur lors du chargement des messages.");
333
+ } finally {
334
+ setLoadingMsg(false);
335
+ }
336
+ };
337
+
338
  if (loading) return <div className="p-8 text-slate-400">Chargement...</div>;
339
  return (
340
  <div className="p-8">
 
342
  <div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
343
  <table className="w-full text-sm">
344
  <thead className="bg-slate-50 text-xs text-slate-500 uppercase">
345
+ <tr>{['TΓ©lΓ©phone', 'Nom', 'Langue', 'Secteur', 'Inscrip.', 'RΓ©ponses', 'Date', 'Actions'].map(h => <th key={h} className="px-5 py-3 text-left">{h}</th>)}</tr>
346
  </thead>
347
  <tbody>
348
  {users.map((u: any) => (
 
354
  <td className="px-5 py-3 text-center">{u._count?.enrollments || 0}</td>
355
  <td className="px-5 py-3 text-center">{u._count?.responses || 0}</td>
356
  <td className="px-5 py-3 text-slate-400 text-xs">{new Date(u.createdAt).toLocaleDateString('fr-FR')}</td>
357
+ <td className="px-5 py-3 text-right">
358
+ <button onClick={() => viewMessages(u.id)} className="text-xs bg-slate-100 hover:bg-slate-200 text-slate-700 px-3 py-1.5 rounded-lg font-medium transition-colors">Conversation</button>
359
+ </td>
360
  </tr>
361
  ))}
362
+ {!users.length && <tr><td colSpan={8} className="px-5 py-8 text-center text-slate-400">Aucun utilisateur</td></tr>}
363
  </tbody>
364
  </table>
365
  </div>
366
+
367
+ {selectedUser && (
368
+ <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
369
+ <div className="bg-slate-50 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col overflow-hidden">
370
+ <div className="bg-white px-6 py-4 flex items-center justify-between border-b border-slate-200">
371
+ <div>
372
+ <h3 className="font-bold text-slate-800">{selectedUser.name || 'Chat Utilisateur'}</h3>
373
+ <p className="text-xs text-slate-500">{selectedUser.phone}</p>
374
+ </div>
375
+ <button onClick={() => { setSelectedUser(null); setMessages([]); }} className="p-2 hover:bg-slate-100 rounded-full"><X className="w-5 h-5 text-slate-500" /></button>
376
+ </div>
377
+ <div className="flex-1 overflow-y-auto p-6 space-y-4 bg-[#e5ddd5]">
378
+ {loadingMsg ? (
379
+ <div className="text-center text-slate-500 py-10">Chargement de l'historique...</div>
380
+ ) : messages.length === 0 ? (
381
+ <div className="text-center text-slate-500 py-10 bg-white/50 rounded-xl">Aucun message pour cet utilisateur.</div>
382
+ ) : (
383
+ messages.map((m: any) => {
384
+ const isBot = m.direction === 'OUTBOUND';
385
+ return (
386
+ <div key={m.id} className={`flex ${isBot ? 'justify-start' : 'justify-end'}`}>
387
+ <div className={`max-w-[80%] rounded-2xl px-4 py-2.5 shadow-sm text-sm ${isBot ? 'bg-white text-slate-800 rounded-tl-none' : 'bg-[#dcf8c6] text-slate-900 rounded-tr-none'}`}>
388
+ {m.mediaUrl && (
389
+ <div className="mb-2">
390
+ {m.mediaUrl.endsWith('.mp3') || m.mediaUrl.endsWith('.ogg') || m.mediaUrl.endsWith('.webm') ?
391
+ <audio src={m.mediaUrl} controls className="h-10 max-w-full" /> :
392
+ <a href={m.mediaUrl} target="_blank" rel="noreferrer" className="text-blue-600 underline">Voir Media</a>
393
+ }
394
+ </div>
395
+ )}
396
+ {m.content && <p className="whitespace-pre-wrap">{m.content}</p>}
397
+ <p className={`text-[10px] mt-1 text-right ${isBot ? 'text-slate-400' : 'text-slate-500'}`}>
398
+ {new Date(m.createdAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
399
+ </p>
400
+ </div>
401
+ </div>
402
+ );
403
+ })
404
+ )}
405
+ </div>
406
+ </div>
407
+ </div>
408
+ )}
409
  </div>
410
  );
411
  }
apps/api/src/routes/admin.ts CHANGED
@@ -69,6 +69,23 @@ export async function adminRoutes(fastify: FastifyInstance) {
69
  return { users, total, page, limit };
70
  });
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  // ── Enrollments ────────────────────────────────────────────────────────────
73
  fastify.get('/enrollments', async () => {
74
  const enrollments = await prisma.enrollment.findMany({
 
69
  return { users, total, page, limit };
70
  });
71
 
72
+ fastify.get('/users/:userId/messages', async (req, reply) => {
73
+ const { userId } = req.params as { userId: string };
74
+ const messages = await prisma.message.findMany({
75
+ where: { userId },
76
+ orderBy: { createdAt: 'asc' },
77
+ });
78
+
79
+ const user = await prisma.user.findUnique({
80
+ where: { id: userId },
81
+ select: { id: true, name: true, phone: true }
82
+ });
83
+
84
+ if (!user) return reply.status(404).send({ error: 'User not found' });
85
+
86
+ return { user, messages };
87
+ });
88
+
89
  // ── Enrollments ────────────────────────────────────────────────────────────
90
  fastify.get('/enrollments', async () => {
91
  const enrollments = await prisma.enrollment.findMany({