200 lines
7.3 KiB
TypeScript
200 lines
7.3 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { pb } from '../../config/pocketbase';
|
|
|
|
interface Ticket {
|
|
id: string;
|
|
subject: string;
|
|
description: string;
|
|
category: string;
|
|
status: 'Ouvert' | 'En analyse' | 'Résolu';
|
|
created: string;
|
|
}
|
|
|
|
interface Message {
|
|
id: string;
|
|
content: string;
|
|
created: string;
|
|
expand?: {
|
|
author?: {
|
|
name?: string;
|
|
email?: string;
|
|
avatar?: string;
|
|
};
|
|
};
|
|
}
|
|
|
|
interface TicketDetailProps {
|
|
ticketId: string;
|
|
onBack: () => void;
|
|
}
|
|
|
|
export default function TicketDetail({ ticketId, onBack }: TicketDetailProps) {
|
|
const [ticket, setTicket] = useState<Ticket | null>(null);
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
const [newMessage, setNewMessage] = useState('');
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isSending, setIsSending] = useState(false);
|
|
|
|
const currentUser = pb.authStore.record;
|
|
|
|
// Récupération du ticket et de ses messages associés
|
|
const fetchTicketData = async () => {
|
|
try {
|
|
const ticketData = await pb.collection('aegis_tickets').getOne<Ticket>(ticketId);
|
|
setTicket(ticketData);
|
|
|
|
const messageRecords = await pb.collection('aegis_ticket_messages').getFullList<Message>({
|
|
filter: `ticket = "${ticketId}"`,
|
|
expand: 'author',
|
|
sort: 'created',
|
|
});
|
|
setMessages(messageRecords);
|
|
} catch (error) {
|
|
console.error("Erreur lors du chargement de la discussion :", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchTicketData();
|
|
}, [ticketId]);
|
|
|
|
// Envoi d'un nouveau message dans la discussion
|
|
const handleSendMessage = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!newMessage.trim() || !currentUser) return;
|
|
|
|
setIsSending(true);
|
|
try {
|
|
await pb.collection('aegis_ticket_messages').create({
|
|
ticket: ticketId,
|
|
author: currentUser.id,
|
|
content: newMessage.trim(),
|
|
});
|
|
|
|
setNewMessage('');
|
|
await fetchTicketData(); // Recharger les messages
|
|
} catch (error) {
|
|
console.error("Erreur lors de l'envoi du message :", error);
|
|
alert("Impossible d'envoyer le message.");
|
|
} finally {
|
|
setIsSending(false);
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex justify-center items-center py-20">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-900"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!ticket) {
|
|
return (
|
|
<div className="bg-white rounded-xl p-6 text-center border border-slate-200">
|
|
<p className="text-sm text-slate-500 mb-4">Ticket introuvable ou accès non autorisé.</p>
|
|
<button onClick={onBack} className="px-4 py-2 bg-slate-900 text-white rounded-lg text-sm font-medium">
|
|
Retour aux tickets
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 max-w-4xl mx-auto">
|
|
{/* Barre de navigation / Retour */}
|
|
<div className="flex items-center justify-between bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
|
<button
|
|
onClick={onBack}
|
|
className="flex items-center space-x-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
<span>Retour au Centre de Support</span>
|
|
</button>
|
|
<div className="flex items-center space-x-3">
|
|
<span className="text-xs font-semibold px-2.5 py-1 bg-slate-100 text-slate-700 rounded border border-slate-200 uppercase">
|
|
{ticket.category}
|
|
</span>
|
|
<span className={`px-3 py-1 rounded-full text-xs font-medium border ${
|
|
ticket.status === 'Résolu' ? 'bg-emerald-50 text-emerald-700 border-emerald-100' : 'bg-blue-50 text-blue-700 border-blue-100'
|
|
}`}>
|
|
{ticket.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* En-tête du ticket & Description initiale */}
|
|
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm space-y-4">
|
|
<h1 className="text-xl font-bold text-slate-900">{ticket.subject}</h1>
|
|
<div className="bg-slate-50 p-4 rounded-lg border border-slate-100 text-sm text-slate-700 whitespace-pre-wrap">
|
|
<p className="text-xs font-semibold text-slate-400 mb-1">Description initiale :</p>
|
|
{ticket.description}
|
|
</div>
|
|
<p className="text-xs text-slate-400">
|
|
Créé le {new Date(ticket.created).toLocaleString('fr-FR')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Fil de discussion / Messages */}
|
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden flex flex-col h-[500px]">
|
|
<div className="p-4 border-b border-slate-100 bg-slate-50/50">
|
|
<h2 className="text-sm font-semibold text-slate-800">Échanges avec l'équipe GISE</h2>
|
|
</div>
|
|
|
|
{/* Liste des messages */}
|
|
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
|
{messages.length === 0 ? (
|
|
<div className="text-center py-12 text-sm text-slate-400 italic">
|
|
Aucun message pour l'instant. Démarrez la conversation ci-dessous.
|
|
</div>
|
|
) : (
|
|
messages.map((msg) => {
|
|
const isMyMessage = msg.author === currentUser?.id;
|
|
const authorName = msg.expand?.author?.name || msg.expand?.author?.email || 'Expert GISE';
|
|
|
|
return (
|
|
<div key={msg.id} className={`flex flex-col ${isMyMessage ? 'items-end' : 'items-start'}`}>
|
|
<div className="flex items-center space-x-2 mb-1">
|
|
<span className="text-xs font-medium text-slate-600">{authorName}</span>
|
|
<span className="text-[10px] text-slate-400">
|
|
{new Date(msg.created).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
</span>
|
|
</div>
|
|
<div className={`max-w-[80%] rounded-2xl px-4 py-3 text-sm shadow-sm ${
|
|
isMyMessage
|
|
? 'bg-blue-900 text-white rounded-br-none'
|
|
: 'bg-slate-100 text-slate-800 rounded-bl-none border border-slate-200/60'
|
|
}`}>
|
|
{msg.content}
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
{/* Formulaire de réponse */}
|
|
<form onSubmit={handleSendMessage} className="p-4 border-t border-slate-100 bg-white flex items-center space-x-3">
|
|
<input
|
|
type="text"
|
|
value={newMessage}
|
|
onChange={(e) => setNewMessage(e.target.value)}
|
|
placeholder="Écrivez votre message ou complément d'information..."
|
|
className="flex-1 rounded-lg border border-slate-300 px-4 py-2.5 text-sm focus:border-blue-900 focus:outline-none focus:ring-1 focus:ring-blue-900"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={isSending || !newMessage.trim()}
|
|
className="bg-blue-900 hover:bg-blue-800 text-white px-5 py-2.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50 shadow-sm"
|
|
>
|
|
{isSending ? 'Envoi...' : 'Envoyer'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |