128 lines
4.4 KiB
TypeScript
128 lines
4.4 KiB
TypeScript
"use client";
|
||
import { useEffect, useState } from "react";
|
||
import { usePageTitle } from "@/hooks/usePageTitle";
|
||
|
||
type Ticket = {
|
||
id: string;
|
||
subject: string;
|
||
status: string;
|
||
priority: string;
|
||
unread_by_client?: number;
|
||
};
|
||
|
||
type Message = {
|
||
id: string;
|
||
ticket_id: string;
|
||
author_id: string | null;
|
||
body: string;
|
||
internal: boolean;
|
||
via: string;
|
||
created_at: string;
|
||
};
|
||
|
||
export default function TicketDetailPage({ params }: { params: { id: string } }) {
|
||
const { id } = params;
|
||
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [ticket, setTicket] = useState<Ticket | null>(null);
|
||
|
||
// Titre dynamique basé sur le ticket
|
||
const ticketTitle = ticket?.subject
|
||
? `${ticket.subject.substring(0, 50)}${ticket.subject.length > 50 ? '...' : ''}`
|
||
: "Ticket support";
|
||
usePageTitle(ticketTitle);
|
||
|
||
const [messages, setMessages] = useState<Message[]>([]);
|
||
const [body, setBody] = useState("");
|
||
const [posting, setPosting] = useState(false);
|
||
|
||
async function load() {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const [tRes, mRes] = await Promise.all([
|
||
fetch(`/api/tickets/${id}`, { credentials: "include", cache: "no-store" }),
|
||
fetch(`/api/tickets/${id}/messages`, { credentials: "include", cache: "no-store" }),
|
||
]);
|
||
if (!tRes.ok) throw new Error(await tRes.text());
|
||
if (!mRes.ok) throw new Error(await mRes.text());
|
||
const t = await tRes.json();
|
||
const m = await mRes.json();
|
||
setTicket(t);
|
||
setMessages(m.items || []);
|
||
} catch (e: any) {
|
||
setError(e?.message || "Erreur de chargement");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
useEffect(() => { load(); }, [id]);
|
||
|
||
useEffect(() => {
|
||
// Mark as read for client side when opening the ticket
|
||
if (!loading && !error && ticket) {
|
||
fetch(`/api/tickets/${id}/read`, { method: 'POST', credentials: 'include' }).catch(() => {});
|
||
}
|
||
}, [loading, error, ticket, id]);
|
||
|
||
async function onSubmit(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
if (!body.trim()) return;
|
||
setPosting(true);
|
||
setError(null);
|
||
try {
|
||
const res = await fetch(`/api/tickets/${id}/messages`, {
|
||
method: "POST",
|
||
credentials: "include",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ body: body.trim() }),
|
||
});
|
||
if (!res.ok) throw new Error(await res.text());
|
||
setBody("");
|
||
await load();
|
||
} catch (e: any) {
|
||
setError(e?.message || "Erreur lors de l’envoi");
|
||
} finally {
|
||
setPosting(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<main className="p-6 space-y-4">
|
||
{loading ? (
|
||
<div>Chargement…</div>
|
||
) : error ? (
|
||
<div className="text-sm text-rose-600">{error}</div>
|
||
) : ticket ? (
|
||
<>
|
||
<h1 className="text-lg font-semibold">{ticket.subject}</h1>
|
||
<div className="rounded-2xl border bg-white p-4 space-y-4">
|
||
<div className="text-sm text-slate-600">
|
||
Statut: <strong>{ticket.status}</strong> • Priorité: <strong>{ticket.priority}</strong>
|
||
</div>
|
||
<div className="space-y-3">
|
||
{messages.map((m) => (
|
||
<div key={m.id} className={`p-3 rounded-lg border ${m.internal ? 'bg-slate-50' : 'bg-white'}`}>
|
||
<div className="text-[11px] text-slate-500 flex items-center gap-2">
|
||
<span>{new Date(m.created_at).toLocaleString('fr-FR')}</span>
|
||
<span>•</span>
|
||
<span>{m.internal ? 'Interne (staff)' : (m.via === 'web' ? 'Client' : 'Staff')}</span>
|
||
</div>
|
||
<div className="text-sm whitespace-pre-wrap mt-1">{m.body}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<form onSubmit={onSubmit} className="space-y-2">
|
||
<textarea value={body} onChange={(e) => setBody(e.target.value)} className="w-full px-3 py-2 rounded-lg border bg-transparent text-sm min-h-[100px]" placeholder="Votre réponse…" />
|
||
<div className="flex items-center">
|
||
<button disabled={posting} className="ml-auto inline-flex items-center px-3 py-2 rounded-lg bg-emerald-600 text-white text-sm hover:bg-emerald-700 disabled:opacity-50">Envoyer</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</>
|
||
) : null}
|
||
</main>
|
||
);
|
||
}
|