186 lines
No EOL
7.1 KiB
TypeScript
186 lines
No EOL
7.1 KiB
TypeScript
import React from "react";
|
|
import { Check, Clock, User } from "lucide-react";
|
|
|
|
type TimelineStatus = "open" | "waiting_staff" | "waiting_client" | "closed";
|
|
|
|
interface TimelineProps {
|
|
currentStatus: TimelineStatus;
|
|
lastMessageBy?: "client" | "staff";
|
|
}
|
|
|
|
const TIMELINE_STEPS = [
|
|
{ id: "open", label: "Ticket ouvert", icon: Check },
|
|
{ id: "waiting_staff", label: "Attente retour Odentas", icon: Clock },
|
|
{ id: "waiting_client", label: "Attente retour client", icon: User },
|
|
{ id: "closed", label: "Résolu", icon: Check },
|
|
] as const;
|
|
|
|
function getStepStatus(stepId: string, currentStatus: TimelineStatus, lastMessageBy?: "client" | "staff") {
|
|
if (stepId === "open") {
|
|
return "completed"; // Toujours complété car le ticket existe
|
|
}
|
|
|
|
if (stepId === "waiting_staff") {
|
|
// Complété si on a dépassé cette étape
|
|
if (currentStatus === "waiting_client" || currentStatus === "closed") return "completed";
|
|
// En cours si on attend le staff
|
|
if (currentStatus === "waiting_staff") return "current";
|
|
// En cours si ticket ouvert et dernier message du client
|
|
if (currentStatus === "open" && lastMessageBy === "client") return "current";
|
|
return "upcoming";
|
|
}
|
|
|
|
if (stepId === "waiting_client") {
|
|
// Complété si on a atteint la fermeture
|
|
if (currentStatus === "closed") return "completed";
|
|
// En cours si on attend le client
|
|
if (currentStatus === "waiting_client") return "current";
|
|
// En cours si ticket ouvert et dernier message du staff
|
|
if (currentStatus === "open" && lastMessageBy === "staff") return "current";
|
|
return "upcoming";
|
|
}
|
|
|
|
if (stepId === "closed") {
|
|
return currentStatus === "closed" ? "current" : "upcoming";
|
|
}
|
|
|
|
return "upcoming";
|
|
}
|
|
|
|
export default function TicketTimeline({ currentStatus, lastMessageBy }: TimelineProps) {
|
|
return (
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4 sm:p-6 mb-6">
|
|
{/* Version mobile : vertical */}
|
|
<div className="flex flex-col space-y-4 sm:hidden">
|
|
{TIMELINE_STEPS.map((step, index) => {
|
|
const status = getStepStatus(step.id, currentStatus, lastMessageBy);
|
|
const Icon = step.icon;
|
|
|
|
return (
|
|
<React.Fragment key={step.id}>
|
|
<div className="flex items-center gap-3">
|
|
{/* Icône */}
|
|
<div
|
|
className={`
|
|
relative flex items-center justify-center w-10 h-10 rounded-full border-2 transition-all duration-300 flex-shrink-0
|
|
${status === "current"
|
|
? "bg-blue-600 border-blue-600 text-white shadow-lg shadow-blue-500/25"
|
|
: status === "completed"
|
|
? "bg-emerald-600 border-emerald-600 text-white"
|
|
: "bg-slate-100 border-slate-300 text-slate-400"
|
|
}
|
|
`}
|
|
>
|
|
<Icon className="w-4 h-4" />
|
|
{status === "current" && (
|
|
<div className="absolute -inset-1 bg-blue-600/20 rounded-full animate-pulse" />
|
|
)}
|
|
</div>
|
|
|
|
{/* Label */}
|
|
<div className="flex-1">
|
|
<div
|
|
className={`
|
|
text-sm font-medium transition-colors duration-300
|
|
${status === "current"
|
|
? "text-blue-600"
|
|
: status === "completed"
|
|
? "text-emerald-600"
|
|
: "text-slate-500"
|
|
}
|
|
`}
|
|
>
|
|
{step.label}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Connecteur vertical */}
|
|
{index < TIMELINE_STEPS.length - 1 && (
|
|
<div className="flex justify-start pl-5">
|
|
<div
|
|
className={`
|
|
w-0.5 h-4 transition-all duration-500
|
|
${status === "completed"
|
|
? "bg-gradient-to-b from-emerald-500 to-blue-500"
|
|
: status === "current" && getStepStatus(TIMELINE_STEPS[index + 1].id, currentStatus, lastMessageBy) !== "upcoming"
|
|
? "bg-gradient-to-b from-blue-500 to-emerald-500"
|
|
: "bg-slate-200"
|
|
}
|
|
`}
|
|
/>
|
|
</div>
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Version desktop : horizontal */}
|
|
<div className="hidden sm:flex items-center justify-between">
|
|
{TIMELINE_STEPS.map((step, index) => {
|
|
const status = getStepStatus(step.id, currentStatus, lastMessageBy);
|
|
const Icon = step.icon;
|
|
|
|
return (
|
|
<React.Fragment key={step.id}>
|
|
<div className="flex flex-col items-center space-y-2">
|
|
{/* Icône */}
|
|
<div
|
|
className={`
|
|
relative flex items-center justify-center w-12 h-12 rounded-full border-2 transition-all duration-300
|
|
${status === "current"
|
|
? "bg-blue-600 border-blue-600 text-white shadow-lg shadow-blue-500/25"
|
|
: status === "completed"
|
|
? "bg-emerald-600 border-emerald-600 text-white"
|
|
: "bg-slate-100 border-slate-300 text-slate-400"
|
|
}
|
|
`}
|
|
>
|
|
<Icon className="w-5 h-5" />
|
|
{status === "current" && (
|
|
<div className="absolute -inset-1 bg-blue-600/20 rounded-full animate-pulse" />
|
|
)}
|
|
</div>
|
|
|
|
{/* Label */}
|
|
<div className="text-center">
|
|
<div
|
|
className={`
|
|
text-sm font-medium transition-colors duration-300
|
|
${status === "current"
|
|
? "text-blue-600"
|
|
: status === "completed"
|
|
? "text-emerald-600"
|
|
: "text-slate-500"
|
|
}
|
|
`}
|
|
>
|
|
{step.label}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Connecteur horizontal */}
|
|
{index < TIMELINE_STEPS.length - 1 && (
|
|
<div className="flex-1 px-4">
|
|
<div
|
|
className={`
|
|
h-0.5 transition-all duration-500
|
|
${status === "completed"
|
|
? "bg-gradient-to-r from-emerald-500 to-blue-500"
|
|
: status === "current" && getStepStatus(TIMELINE_STEPS[index + 1].id, currentStatus, lastMessageBy) !== "upcoming"
|
|
? "bg-gradient-to-r from-blue-500 to-emerald-500"
|
|
: "bg-slate-200"
|
|
}
|
|
`}
|
|
/>
|
|
</div>
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |