the-tip-top-frontend/app/employe/verification/page-new.tsx
2025-11-17 23:38:02 +01:00

463 lines
17 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { employeeService } from "@/services/employee.service";
import { Ticket } from "@/types";
import toast from "react-hot-toast";
import {
Search,
CheckCircle,
XCircle,
RefreshCw,
AlertCircle,
Users,
Clock,
BarChart3,
} from "lucide-react";
export default function EmployeeVerificationPage() {
const [tickets, setTickets] = useState<Ticket[]>([]);
const [loading, setLoading] = useState(true);
const [searchCode, setSearchCode] = useState("");
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
const [showModal, setShowModal] = useState(false);
const [validating, setValidating] = useState(false);
const [rejectReason, setRejectReason] = useState("");
const [showRejectInput, setShowRejectInput] = useState(false);
useEffect(() => {
loadPendingTickets();
}, []);
const loadPendingTickets = async () => {
try {
setLoading(true);
const data = await employeeService.getPendingTickets();
setTickets(data);
} catch (error: any) {
console.error("Error loading tickets:", error);
toast.error("Erreur lors du chargement des tickets");
} finally {
setLoading(false);
}
};
const handleSearch = () => {
if (!searchCode.trim()) {
toast.error("Veuillez entrer un code de ticket");
return;
}
const ticket = tickets.find(
(t) => t.code.toLowerCase() === searchCode.toLowerCase()
);
if (ticket) {
setSelectedTicket(ticket);
setShowModal(true);
setSearchCode("");
} else {
toast.error("Ticket non trouvé ou déjà traité");
}
};
const handleValidate = async () => {
if (!selectedTicket) return;
setValidating(true);
try {
await employeeService.validateTicket(selectedTicket.id, "validate");
toast.success("✅ Ticket validé ! Le lot peut être remis au client.");
setShowModal(false);
setSelectedTicket(null);
loadPendingTickets();
} catch (error: any) {
toast.error(error.message || "Erreur lors de la validation");
} finally {
setValidating(false);
}
};
const handleReject = async () => {
if (!selectedTicket) return;
if (!rejectReason.trim()) {
toast.error("Veuillez indiquer la raison du rejet");
return;
}
setValidating(true);
try {
await employeeService.validateTicket(
selectedTicket.id,
"reject",
rejectReason
);
toast.success("Ticket rejeté");
setShowModal(false);
setSelectedTicket(null);
setShowRejectInput(false);
setRejectReason("");
loadPendingTickets();
} catch (error: any) {
toast.error(error.message || "Erreur lors du rejet");
} finally {
setValidating(false);
}
};
const getStatusBadge = (status: string) => {
const badges = {
PENDING: (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<Clock className="w-3 h-3 mr-1" />
En attente
</span>
),
REJECTED: (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<XCircle className="w-3 h-3 mr-1" />
Rejeté
</span>
),
CLAIMED: (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<CheckCircle className="w-3 h-3 mr-1" />
Réclamé
</span>
),
};
return badges[status] || badges.PENDING;
};
if (loading) {
return (
<div className="p-8">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-1/4"></div>
<div className="h-32 bg-gray-200 rounded"></div>
</div>
</div>
);
}
return (
<div className="p-8 bg-gray-50 min-h-screen">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Validation des Tickets
</h1>
<p className="text-gray-600">
Scannez ou recherchez un code pour valider les lots gagnés
</p>
</div>
{/* Search Section */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Rechercher un ticket
</h2>
<div className="flex gap-4">
<div className="flex-1">
<input
type="text"
placeholder="Entrez le code du ticket (ex: ABC123)"
value={searchCode}
onChange={(e) => setSearchCode(e.target.value.toUpperCase())}
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent font-mono text-lg"
maxLength={10}
/>
</div>
<button
onClick={handleSearch}
className="flex items-center gap-2 px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium"
>
<Search className="w-5 h-5" />
Rechercher
</button>
</div>
</div>
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<StatCard
title="Tickets en attente"
value={tickets.length}
icon={<Clock className="w-6 h-6" />}
color="yellow"
/>
<StatCard
title="Aujourd'hui"
value={0}
icon={<Users className="w-6 h-6" />}
color="green"
/>
<StatCard
title="Total traités"
value={0}
icon={<BarChart3 className="w-6 h-6" />}
color="blue"
/>
</div>
{/* Tickets Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">
Tickets en attente ({tickets.length})
</h2>
<button
onClick={loadPendingTickets}
className="flex items-center gap-2 px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
>
<RefreshCw className="w-4 h-4" />
Actualiser
</button>
</div>
<div className="overflow-x-auto">
{tickets.length === 0 ? (
<div className="text-center py-16">
<div className="text-6xl mb-4"></div>
<p className="text-gray-600 mb-2 font-medium">
Aucun ticket en attente
</p>
<p className="text-sm text-gray-500">
Tous les tickets ont é traités !
</p>
</div>
) : (
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Code Ticket
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Client
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Lot Gagné
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Statut
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{tickets.map((ticket) => (
<tr key={ticket.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<span className="font-mono text-sm font-semibold text-gray-900">
{ticket.code}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm">
<div className="font-medium text-gray-900">
{ticket.user?.firstName} {ticket.user?.lastName}
</div>
<div className="text-gray-500">{ticket.user?.email}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{ticket.prize?.name || "N/A"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{ticket.playedAt
? new Date(ticket.playedAt).toLocaleDateString("fr-FR")
: "N/A"}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(ticket.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => {
setSelectedTicket(ticket);
setShowModal(true);
}}
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
Traiter
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
{/* Modal de validation */}
{showModal && selectedTicket && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg max-w-2xl w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Détails du Ticket
</h2>
<div className="space-y-6">
{/* Ticket Info */}
<div className="bg-gray-50 rounded-lg p-6">
<div className="grid grid-cols-2 gap-6">
<div>
<p className="text-sm font-medium text-gray-600 mb-1">
Code du ticket
</p>
<p className="text-xl font-mono font-bold text-gray-900">
{selectedTicket.code}
</p>
</div>
<div>
<p className="text-sm font-medium text-gray-600 mb-1">Statut</p>
{getStatusBadge(selectedTicket.status)}
</div>
</div>
<div className="mt-4 pt-4 border-t border-gray-200">
<p className="text-sm font-medium text-gray-600 mb-1">
Lot gagné
</p>
<p className="text-lg font-semibold text-green-600">
{selectedTicket.prize?.name}
</p>
<p className="text-sm text-gray-500 mt-1">
{selectedTicket.prize?.description}
</p>
</div>
<div className="mt-4 pt-4 border-t border-gray-200">
<p className="text-sm font-medium text-gray-600 mb-1">Client</p>
<p className="text-base text-gray-900">
{selectedTicket.user?.firstName} {selectedTicket.user?.lastName}
</p>
<p className="text-sm text-gray-500">
{selectedTicket.user?.email}
</p>
{selectedTicket.user?.phone && (
<p className="text-sm text-gray-500">
{selectedTicket.user?.phone}
</p>
)}
</div>
</div>
{/* Reject Reason Input */}
{showRejectInput && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Raison du rejet *
</label>
<textarea
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
placeholder="Ex: Ticket endommagé, code illisible..."
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
rows={3}
/>
</div>
)}
{/* Actions */}
<div className="flex gap-3 justify-end pt-4">
{!showRejectInput ? (
<>
<button
onClick={() => {
setShowModal(false);
setSelectedTicket(null);
}}
disabled={validating}
className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 font-medium"
>
Annuler
</button>
<button
onClick={() => setShowRejectInput(true)}
disabled={validating}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 font-medium flex items-center gap-2"
>
<XCircle className="w-4 h-4" />
Rejeter
</button>
<button
onClick={handleValidate}
disabled={validating}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 font-medium flex items-center gap-2"
>
<CheckCircle className="w-4 h-4" />
{validating ? "Validation..." : "Valider et Remettre"}
</button>
</>
) : (
<>
<button
onClick={() => {
setShowRejectInput(false);
setRejectReason("");
}}
disabled={validating}
className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 font-medium"
>
Retour
</button>
<button
onClick={handleReject}
disabled={validating || !rejectReason.trim()}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 font-medium flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<XCircle className="w-4 h-4" />
{validating ? "Rejet..." : "Confirmer le Rejet"}
</button>
</>
)}
</div>
</div>
</div>
</div>
)}
</div>
);
}
interface StatCardProps {
title: string;
value: number;
icon: React.ReactNode;
color: "yellow" | "green" | "blue";
}
function StatCard({ title, value, icon, color }: StatCardProps) {
const colors = {
yellow: "bg-yellow-100 text-yellow-600",
green: "bg-green-100 text-green-600",
blue: "bg-blue-100 text-blue-600",
};
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">{title}</p>
<p className="text-3xl font-bold text-gray-900 mt-2">
{value.toLocaleString("fr-FR")}
</p>
</div>
<div className={`p-3 rounded-lg ${colors[color]}`}>{icon}</div>
</div>
</div>
);
}