feat: redesign employee panel with blanc cassé theme
- Update employee layout with off-white sidebar and logo - Add mobile responsive menu - Simplify header with UserDropdown only - Redesign verification page with admin-style design - Change employee avatar to blue color - Remove unused stats cards 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8d2012bb2c
commit
e7fef17831
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Loading } from "@/components/ui/Loading";
|
||||
import toast from "react-hot-toast";
|
||||
import { Ticket, BarChart3 } from "lucide-react";
|
||||
import { Ticket, BarChart3, Menu, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Logo from "@/components/Logo";
|
||||
|
|
@ -19,6 +19,7 @@ export default function EmployeLayout({
|
|||
const { user, isAuthenticated, isLoading, logout } = useAuth();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isAuthenticated) {
|
||||
|
|
@ -55,67 +56,104 @@ export default function EmployeLayout({
|
|||
label: "Dashboard",
|
||||
href: "/employe/dashboard",
|
||||
icon: <BarChart3 className="w-5 h-5" />,
|
||||
color: "green",
|
||||
},
|
||||
{
|
||||
label: "Validation des Tickets",
|
||||
href: "/employe/verification",
|
||||
icon: <Ticket className="w-5 h-5" />,
|
||||
color: "emerald",
|
||||
},
|
||||
];
|
||||
|
||||
const isActive = (href: string) => pathname === href;
|
||||
|
||||
const getActiveStyles = (color: string) => {
|
||||
const styles: Record<string, string> = {
|
||||
green: "bg-gradient-to-r from-green-600 to-green-700 text-white shadow-lg shadow-green-500/30",
|
||||
emerald: "bg-gradient-to-r from-emerald-600 to-teal-600 text-white shadow-lg shadow-emerald-500/30",
|
||||
};
|
||||
return styles[color] || styles.green;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gray-50">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-white shadow-lg border-r border-gray-200 flex flex-col">
|
||||
{/* Logo/Header */}
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<h2 className="text-xl font-bold text-gray-900">Thé Tip Top</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Espace Employé</p>
|
||||
</div>
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="lg:hidden fixed top-4 left-4 z-50 p-2 rounded-xl bg-white shadow-lg hover:bg-gray-50 border border-gray-100"
|
||||
>
|
||||
{isOpen ? (
|
||||
<X className="w-6 h-6 text-gray-600" />
|
||||
) : (
|
||||
<Menu className="w-6 h-6 text-gray-600" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`
|
||||
flex items-center gap-3 px-4 py-3 rounded-lg transition-colors
|
||||
${
|
||||
isActive(item.href)
|
||||
? "bg-green-600 text-white"
|
||||
: "text-gray-700 hover:bg-gray-100"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{item.icon}
|
||||
<span className="font-medium">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
{/* Overlay for mobile */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="lg:hidden fixed inset-0 bg-black/50 backdrop-blur-sm z-30"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`
|
||||
fixed top-0 left-0 h-full bg-[#faf8f5] shadow-xl z-40 transition-transform duration-300 ease-in-out border-r border-gray-200
|
||||
${isOpen ? "translate-x-0" : "-translate-x-full"}
|
||||
lg:translate-x-0 lg:static
|
||||
w-72
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Logo/Header */}
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<Logo size="md" showText={false} />
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-[#1e3a5f]">Thé Tip Top</h2>
|
||||
<p className="text-sm text-gray-500">Espace Employé</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider px-4 mb-3">Menu Principal</p>
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className={`
|
||||
flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200
|
||||
${
|
||||
isActive(item.href)
|
||||
? getActiveStyles(item.color)
|
||||
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{item.icon}
|
||||
<span className="font-medium">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm border-b border-gray-200 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Logo size="md" showText={false} />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">
|
||||
Thé Tip Top - Espace Employé
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header className="bg-[#faf8f5] shadow-sm border-b border-gray-200 px-6 py-3">
|
||||
<div className="flex items-center justify-end">
|
||||
<UserDropdown
|
||||
user={user}
|
||||
profilePath="/employe/profil"
|
||||
onLogout={handleLogout}
|
||||
accentColor="green"
|
||||
accentColor="blue"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useState, useEffect } from "react";
|
|||
import { employeeService } from "@/services/employee.service";
|
||||
import { Ticket } from "@/types";
|
||||
import toast from "react-hot-toast";
|
||||
import { StatusBadge, StatCard } from "@/components/ui";
|
||||
import {
|
||||
Search,
|
||||
CheckCircle,
|
||||
|
|
@ -12,7 +11,8 @@ import {
|
|||
RefreshCw,
|
||||
Users,
|
||||
Clock,
|
||||
BarChart3,
|
||||
Ticket as TicketIcon,
|
||||
Gift,
|
||||
} from "lucide-react";
|
||||
|
||||
export default function EmployeeVerificationPage() {
|
||||
|
|
@ -73,7 +73,7 @@ export default function EmployeeVerificationPage() {
|
|||
setValidating(true);
|
||||
try {
|
||||
await employeeService.validateTicket(selectedTicket.id, "APPROVE");
|
||||
toast.success("✅ Ticket validé ! Le lot peut être remis au client.");
|
||||
toast.success("Ticket validé ! Le lot peut être remis au client.");
|
||||
setShowModal(false);
|
||||
setSelectedTicket(null);
|
||||
loadPendingTickets();
|
||||
|
|
@ -125,42 +125,44 @@ export default function EmployeeVerificationPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8">
|
||||
<div className="p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-[#2d5a3d] mb-2">
|
||||
Validation des Tickets
|
||||
</h1>
|
||||
<p className="text-gray-600 text-lg">
|
||||
Scannez ou recherchez un code pour valider les lots gagnés
|
||||
</p>
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-emerald-500 to-teal-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<TicketIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent">
|
||||
Validation des Tickets
|
||||
</h1>
|
||||
<p className="text-gray-500">
|
||||
Scannez ou recherchez un code pour valider les lots gagnés
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Section */}
|
||||
<div className="bg-gradient-to-r from-green-600 to-green-700 rounded-2xl shadow-lg p-8 mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center">
|
||||
<Search className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-white">
|
||||
Rechercher un ticket
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Entrez le code du ticket (ex: TTABC123)"
|
||||
value={searchCode}
|
||||
onChange={(e) => setSearchCode(e.target.value.toUpperCase())}
|
||||
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
|
||||
className="w-full px-5 py-4 border-2 border-white/20 bg-white/10 text-white placeholder-white/60 rounded-xl focus:ring-2 focus:ring-white focus:border-transparent font-mono text-lg backdrop-blur-sm"
|
||||
maxLength={12}
|
||||
/>
|
||||
{/* Search Section - Style blanc cassé */}
|
||||
<div className="bg-[#faf8f5] rounded-2xl p-6 mb-6 border border-gray-200">
|
||||
<div className="flex flex-col md:flex-row gap-4 items-center">
|
||||
<div className="flex-1 w-full">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Entrez le code du ticket (ex: TTABC123)"
|
||||
value={searchCode}
|
||||
onChange={(e) => setSearchCode(e.target.value.toUpperCase())}
|
||||
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
|
||||
className="w-full pl-12 pr-4 py-3 bg-white border border-gray-200 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:border-transparent font-mono text-lg"
|
||||
maxLength={12}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
className="flex items-center gap-2 px-8 py-4 bg-white text-green-700 rounded-xl hover:bg-green-50 transition-all font-bold shadow-lg hover:shadow-xl hover:scale-[1.02]"
|
||||
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-emerald-600 to-teal-600 text-white rounded-xl hover:from-emerald-700 hover:to-teal-700 transition-all font-semibold shadow-lg hover:shadow-xl"
|
||||
>
|
||||
<Search className="w-5 h-5" />
|
||||
Rechercher
|
||||
|
|
@ -168,15 +170,16 @@ export default function EmployeeVerificationPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lots en attente de remise */}
|
||||
<div className="bg-white rounded-2xl shadow-md border border-gray-100 overflow-hidden">
|
||||
<div className="px-6 py-5 border-b border-gray-100 flex items-center justify-between bg-gradient-to-r from-gray-50 to-white">
|
||||
|
||||
{/* Table des tickets en attente */}
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gradient-to-r from-gray-50 to-gray-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-yellow-100 rounded-lg flex items-center justify-center">
|
||||
<Clock className="w-5 h-5 text-yellow-600" />
|
||||
<div className="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center">
|
||||
<Clock className="w-5 h-5 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-800">
|
||||
<h2 className="text-lg font-bold text-gray-800">
|
||||
Lots en attente de remise
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">{tickets.length} ticket(s) en attente</p>
|
||||
|
|
@ -184,9 +187,10 @@ export default function EmployeeVerificationPage() {
|
|||
</div>
|
||||
<button
|
||||
onClick={loadPendingTickets}
|
||||
className="flex items-center gap-2 px-4 py-2 text-green-700 bg-green-50 hover:bg-green-100 rounded-lg transition-colors font-medium"
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-white text-gray-700 rounded-xl font-semibold hover:bg-gray-50 transition-colors shadow-sm border border-gray-200"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
Actualiser
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -194,10 +198,10 @@ export default function EmployeeVerificationPage() {
|
|||
<div className="overflow-x-auto">
|
||||
{tickets.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="w-10 h-10 text-green-600" />
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<p className="text-gray-800 mb-2 font-semibold text-lg">
|
||||
<p className="text-gray-900 font-medium text-lg mb-2">
|
||||
Aucun lot en attente
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
|
|
@ -206,69 +210,88 @@ export default function EmployeeVerificationPage() {
|
|||
</div>
|
||||
) : (
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-50 border-b border-gray-100">
|
||||
<thead className="bg-gradient-to-r from-gray-50 to-gray-100">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Code Ticket
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Client
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Lot Gagné
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Date
|
||||
</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-bold text-gray-500 uppercase tracking-wider">
|
||||
Action
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{tickets.map((ticket, index) => (
|
||||
<tr key={ticket.id} className={`hover:bg-green-50/50 transition-colors ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50/30'}`}>
|
||||
<td className="px-6 py-5">
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{tickets.map((ticket) => (
|
||||
<tr key={ticket.id} className="hover:bg-gray-50 transition-colors">
|
||||
{/* CODE TICKET */}
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-full flex items-center justify-center text-white font-bold text-sm">
|
||||
{ticket.user ? `${ticket.user.firstName?.charAt(0) || ''}${ticket.user.lastName?.charAt(0) || ''}` : 'N/A'}
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-lg flex items-center justify-center">
|
||||
<TicketIcon className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<span className="text-sm font-mono font-semibold text-gray-900">
|
||||
{ticket.code}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* CLIENT */}
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center text-white text-xs font-bold">
|
||||
{ticket.user ? `${ticket.user.firstName?.charAt(0) || ''}${ticket.user.lastName?.charAt(0) || ''}` : '?'}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-900">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{ticket.user ? `${ticket.user.firstName} ${ticket.user.lastName}` : 'N/A'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div className="text-xs text-gray-500">
|
||||
{ticket.user?.email || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-5">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-900">
|
||||
|
||||
{/* LOT GAGNÉ */}
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Gift className="w-4 h-4 text-purple-500" />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{ticket.prize?.name || 'N/A'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 font-mono bg-gray-100 inline-block px-2 py-1 rounded mt-1">
|
||||
{ticket.code}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
{ticket.playedAt
|
||||
? new Date(ticket.playedAt).toLocaleDateString('fr-FR')
|
||||
: 'N/A'}
|
||||
</div>
|
||||
|
||||
{/* DATE */}
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{ticket.playedAt
|
||||
? new Date(ticket.playedAt).toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
})
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-right">
|
||||
|
||||
{/* ACTIONS */}
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedTicket(ticket);
|
||||
setShowModal(true);
|
||||
}}
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-green-600 to-green-700 text-white text-sm font-semibold rounded-lg hover:from-green-700 hover:to-green-800 transition-all shadow-sm hover:shadow-md"
|
||||
className="px-4 py-2 text-sm font-medium bg-gradient-to-r from-emerald-600 to-teal-600 text-white rounded-lg hover:from-emerald-700 hover:to-teal-700 transition-colors"
|
||||
>
|
||||
Traiter
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -281,155 +304,161 @@ export default function EmployeeVerificationPage() {
|
|||
|
||||
{/* 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="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl shadow-2xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-emerald-600 to-teal-600 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-white">Détails du Ticket</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowModal(false);
|
||||
setSelectedTicket(null);
|
||||
setShowRejectInput(false);
|
||||
setRejectReason("");
|
||||
}}
|
||||
className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center text-white hover:bg-white/30 transition-colors"
|
||||
>
|
||||
<XCircle className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Ticket Code */}
|
||||
<div className="bg-gray-50 rounded-lg p-6">
|
||||
<p className="text-sm font-medium text-gray-600 mb-2">
|
||||
Code du ticket
|
||||
</p>
|
||||
<p className="text-xl font-mono font-bold text-gray-900">
|
||||
{selectedTicket.code}
|
||||
</p>
|
||||
<div className="p-6 space-y-6 overflow-y-auto max-h-[calc(90vh-200px)]">
|
||||
{/* Code et Statut */}
|
||||
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-gray-50 to-gray-100 rounded-xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-xl flex items-center justify-center">
|
||||
<TicketIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Code ticket</p>
|
||||
<p className="text-lg font-mono font-bold text-gray-900">{selectedTicket.code}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="px-3 py-1 bg-amber-100 text-amber-700 rounded-full text-sm font-medium">
|
||||
En attente
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Client Info */}
|
||||
<div className="bg-blue-50 rounded-lg p-6 border-l-4 border-blue-500">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-blue-600" />
|
||||
Informations du Client
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="p-4 bg-blue-50 rounded-xl border border-blue-100">
|
||||
<p className="text-xs text-blue-600 font-semibold mb-2 uppercase">Client</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center text-white font-bold">
|
||||
{selectedTicket.user ? `${selectedTicket.user.firstName?.charAt(0) || ''}${selectedTicket.user.lastName?.charAt(0) || ''}` : '?'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Nom complet</p>
|
||||
<p className="text-base font-semibold text-gray-900">
|
||||
<p className="font-semibold text-gray-900">
|
||||
{selectedTicket.user ? `${selectedTicket.user.firstName} ${selectedTicket.user.lastName}` : 'N/A'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">{selectedTicket.user?.email || 'N/A'}</p>
|
||||
{selectedTicket.user?.phone && (
|
||||
<p className="text-sm text-gray-500">{selectedTicket.user.phone}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Email</p>
|
||||
<p className="text-sm text-gray-900">
|
||||
{selectedTicket.user?.email || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
{selectedTicket.user?.phone && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Téléphone</p>
|
||||
<p className="text-sm text-gray-900">
|
||||
{selectedTicket.user.phone}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prize Info */}
|
||||
<div className="bg-green-50 rounded-lg p-6 border-l-4 border-green-500">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
Lot Gagné
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<p className="text-xl font-bold text-green-600">
|
||||
{selectedTicket.prize?.name || 'N/A'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{selectedTicket.prize?.description || 'Description non disponible'}
|
||||
</p>
|
||||
{/* Lot gagné */}
|
||||
<div className="p-4 bg-purple-50 rounded-xl border border-purple-100">
|
||||
<p className="text-xs text-purple-600 font-semibold mb-2 uppercase">Lot à remettre</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center">
|
||||
<Gift className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">{selectedTicket.prize?.name}</p>
|
||||
<p className="text-sm text-gray-500">{selectedTicket.prize?.description}</p>
|
||||
</div>
|
||||
{selectedTicket.playedAt && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Date de gain</p>
|
||||
<p className="text-sm text-gray-900">
|
||||
{new Date(selectedTicket.playedAt).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date */}
|
||||
<div className="p-4 bg-gray-50 rounded-xl">
|
||||
<p className="text-xs text-gray-500 font-semibold mb-1 uppercase">Date de participation</p>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{selectedTicket.playedAt
|
||||
? new Date(selectedTicket.playedAt).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
: 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Reject Reason Input */}
|
||||
{showRejectInput && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<div className="p-4 bg-red-50 rounded-xl border border-red-100">
|
||||
<label className="block text-sm font-medium text-red-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"
|
||||
className="w-full px-4 py-2 border border-red-200 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
{/* Actions */}
|
||||
<div className="px-6 py-4 bg-gray-50 border-t flex gap-3 justify-end">
|
||||
{!showRejectInput ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowModal(false);
|
||||
setSelectedTicket(null);
|
||||
}}
|
||||
disabled={validating}
|
||||
className="px-4 py-2.5 bg-gray-200 text-gray-700 rounded-xl font-semibold hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowRejectInput(true)}
|
||||
disabled={validating}
|
||||
className="px-4 py-2.5 bg-red-600 text-white rounded-xl font-semibold hover:bg-red-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
Rejeter
|
||||
</button>
|
||||
<button
|
||||
onClick={handleValidate}
|
||||
disabled={validating}
|
||||
className="px-4 py-2.5 bg-gradient-to-r from-emerald-600 to-teal-600 text-white rounded-xl font-semibold hover:from-emerald-700 hover:to-teal-700 transition-colors 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.5 bg-gray-200 text-gray-700 rounded-xl font-semibold hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
Retour
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReject}
|
||||
disabled={validating || !rejectReason.trim()}
|
||||
className="px-4 py-2.5 bg-red-600 text-white rounded-xl font-semibold hover:bg-red-700 transition-colors 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>
|
||||
|
|
@ -437,4 +466,3 @@ export default function EmployeeVerificationPage() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user