- Add utils/export.ts for centralized CSV export functionality - Add EmptyState component for consistent empty state UI - Add Pagination component for reusable pagination controls - Refactor employe/gains-client to use apiFetch and EmptyState - Refactor employe/historique to use apiFetch and EmptyState - Export new components from ui/index.ts Target: reduce duplication from 13.93% to under 3% 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
331 lines
12 KiB
TypeScript
331 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Card } from '@/components/ui/Card';
|
|
import { EmptyState } from '@/components/ui/EmptyState';
|
|
import Button from '@/components/Button';
|
|
import { apiFetch } from '@/hooks/useApi';
|
|
import toast from 'react-hot-toast';
|
|
import {
|
|
Search,
|
|
User,
|
|
Gift,
|
|
CheckCircle,
|
|
Clock,
|
|
XCircle,
|
|
Phone,
|
|
Mail,
|
|
Package,
|
|
} from 'lucide-react';
|
|
|
|
interface ClientPrize {
|
|
ticketId: string;
|
|
ticketCode: string;
|
|
status: string;
|
|
playedAt: string;
|
|
claimedAt: string | null;
|
|
validatedAt: string | null;
|
|
validatedBy: string | null;
|
|
prize: {
|
|
id: string;
|
|
name: string;
|
|
type: string;
|
|
value: string;
|
|
description: string;
|
|
};
|
|
}
|
|
|
|
interface ClientData {
|
|
client: {
|
|
id: string;
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
phone: string;
|
|
};
|
|
prizes: ClientPrize[];
|
|
totalPrizes: number;
|
|
pendingPrizes: number;
|
|
claimedPrizes: number;
|
|
}
|
|
|
|
export default function GainsClientPage() {
|
|
const [searchType, setSearchType] = useState<'email' | 'phone'>('email');
|
|
const [searchValue, setSearchValue] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [clientData, setClientData] = useState<ClientData | null>(null);
|
|
const [validatingTicketId, setValidatingTicketId] = useState<string | null>(null);
|
|
|
|
const handleSearch = async () => {
|
|
if (!searchValue.trim()) {
|
|
toast.error('Veuillez entrer un email ou un téléphone');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const queryParam = searchType === 'email' ? `email=${encodeURIComponent(searchValue)}` : `phone=${encodeURIComponent(searchValue)}`;
|
|
const data = await apiFetch<{ data: ClientData }>(`/employee/client-prizes?${queryParam}`);
|
|
setClientData(data.data);
|
|
toast.success(`Client trouvé: ${data.data.client.firstName} ${data.data.client.lastName}`);
|
|
} catch (error: any) {
|
|
console.error('Error searching client:', error);
|
|
toast.error(error.message || 'Erreur lors de la recherche');
|
|
setClientData(null);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleValidatePrize = async (ticketId: string) => {
|
|
if (!confirm('Confirmer la remise de ce lot au client?')) {
|
|
return;
|
|
}
|
|
|
|
setValidatingTicketId(ticketId);
|
|
try {
|
|
await apiFetch('/employee/validate-ticket', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ ticketId, action: 'APPROVE' }),
|
|
});
|
|
toast.success('Lot marqué comme remis!');
|
|
handleSearch();
|
|
} catch (error: any) {
|
|
console.error('Error validating prize:', error);
|
|
toast.error(error.message || 'Erreur lors de la validation');
|
|
} finally {
|
|
setValidatingTicketId(null);
|
|
}
|
|
};
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
const badges = {
|
|
PENDING: (
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800">
|
|
<Clock className="w-4 h-4 mr-1" />
|
|
À remettre
|
|
</span>
|
|
),
|
|
CLAIMED: (
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
|
|
<CheckCircle className="w-4 h-4 mr-1" />
|
|
Remis
|
|
</span>
|
|
),
|
|
REJECTED: (
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800">
|
|
<XCircle className="w-4 h-4 mr-1" />
|
|
Rejeté
|
|
</span>
|
|
),
|
|
};
|
|
return badges[status as keyof typeof badges] || badges.PENDING;
|
|
};
|
|
|
|
return (
|
|
<div className="p-6 max-w-7xl mx-auto">
|
|
{/* Header */}
|
|
<div className="mb-6">
|
|
<h1 className="text-3xl font-bold mb-2 flex items-center gap-3">
|
|
<Gift className="w-10 h-10 text-purple-600" />
|
|
Gains du Client
|
|
</h1>
|
|
<p className="text-gray-600">
|
|
Recherchez un client pour visualiser tous ses gains et les remettre
|
|
</p>
|
|
</div>
|
|
|
|
{/* Search Section */}
|
|
<Card className="p-6 mb-6">
|
|
<h2 className="text-xl font-bold mb-4">Rechercher un client</h2>
|
|
|
|
<div className="space-y-4">
|
|
{/* Search Type Selection */}
|
|
<div className="flex gap-4">
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="searchType"
|
|
value="email"
|
|
checked={searchType === 'email'}
|
|
onChange={(e) => setSearchType(e.target.value as 'email')}
|
|
className="w-4 h-4"
|
|
/>
|
|
<Mail className="w-5 h-5 text-gray-600" />
|
|
<span className="text-sm font-medium">Email</span>
|
|
</label>
|
|
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="searchType"
|
|
value="phone"
|
|
checked={searchType === 'phone'}
|
|
onChange={(e) => setSearchType(e.target.value as 'phone')}
|
|
className="w-4 h-4"
|
|
/>
|
|
<Phone className="w-5 h-5 text-gray-600" />
|
|
<span className="text-sm font-medium">Téléphone</span>
|
|
</label>
|
|
</div>
|
|
|
|
{/* Search Input */}
|
|
<div className="flex gap-4">
|
|
<input
|
|
type={searchType === 'email' ? 'email' : 'tel'}
|
|
placeholder={searchType === 'email' ? 'exemple@email.com' : '06 12 34 56 78'}
|
|
value={searchValue}
|
|
onChange={(e) => setSearchValue(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
|
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
<Button
|
|
onClick={handleSearch}
|
|
isLoading={loading}
|
|
disabled={loading}
|
|
className="bg-purple-600 hover:bg-purple-700"
|
|
>
|
|
<Search className="w-5 h-5 mr-2" />
|
|
Rechercher
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Client Info & Prizes */}
|
|
{clientData && (
|
|
<>
|
|
{/* Client Info Card */}
|
|
<Card className="p-6 mb-6 bg-gradient-to-r from-purple-50 to-blue-50 border-purple-200">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-start gap-4">
|
|
<div className="p-3 bg-white rounded-lg shadow-sm">
|
|
<User className="w-8 h-8 text-purple-600" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-gray-900">
|
|
{clientData.client.firstName} {clientData.client.lastName}
|
|
</h2>
|
|
<div className="mt-2 space-y-1">
|
|
<p className="text-sm text-gray-600 flex items-center gap-2">
|
|
<Mail className="w-4 h-4" />
|
|
{clientData.client.email}
|
|
</p>
|
|
{clientData.client.phone && (
|
|
<p className="text-sm text-gray-600 flex items-center gap-2">
|
|
<Phone className="w-4 h-4" />
|
|
{clientData.client.phone}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-right">
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="bg-white rounded-lg p-3 shadow-sm">
|
|
<p className="text-xs text-gray-600">Total</p>
|
|
<p className="text-2xl font-bold text-gray-900">{clientData.totalPrizes}</p>
|
|
</div>
|
|
<div className="bg-white rounded-lg p-3 shadow-sm">
|
|
<p className="text-xs text-yellow-600">À remettre</p>
|
|
<p className="text-2xl font-bold text-yellow-600">{clientData.pendingPrizes}</p>
|
|
</div>
|
|
<div className="bg-white rounded-lg p-3 shadow-sm">
|
|
<p className="text-xs text-green-600">Remis</p>
|
|
<p className="text-2xl font-bold text-green-600">{clientData.claimedPrizes}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Prizes List */}
|
|
<Card className="p-6">
|
|
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
|
|
<Package className="w-6 h-6 text-purple-600" />
|
|
Lots gagnés ({clientData.prizes.length})
|
|
</h2>
|
|
|
|
{clientData.prizes.length === 0 ? (
|
|
<EmptyState
|
|
icon="🎁"
|
|
message="Ce client n'a pas encore gagné de lots"
|
|
/>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{clientData.prizes.map((prize) => (
|
|
<div
|
|
key={prize.ticketId}
|
|
className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h3 className="text-lg font-bold text-gray-900">
|
|
{prize.prize.name}
|
|
</h3>
|
|
{getStatusBadge(prize.status)}
|
|
</div>
|
|
|
|
<p className="text-sm text-gray-600 mb-3">
|
|
{prize.prize.description}
|
|
</p>
|
|
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-gray-600">Code ticket:</span>
|
|
<span className="ml-2 font-mono font-semibold">{prize.ticketCode}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">Valeur:</span>
|
|
<span className="ml-2 font-semibold text-purple-600">
|
|
{prize.prize.value}€
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">Gagné le:</span>
|
|
<span className="ml-2">
|
|
{new Date(prize.playedAt).toLocaleDateString('fr-FR')}
|
|
</span>
|
|
</div>
|
|
{prize.claimedAt && (
|
|
<div>
|
|
<span className="text-gray-600">Remis le:</span>
|
|
<span className="ml-2">
|
|
{new Date(prize.claimedAt).toLocaleDateString('fr-FR')}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{prize.validatedBy && (
|
|
<div className="col-span-2">
|
|
<span className="text-gray-600">Remis par:</span>
|
|
<span className="ml-2 font-medium">{prize.validatedBy}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{prize.status === 'PENDING' && (
|
|
<Button
|
|
onClick={() => handleValidatePrize(prize.ticketId)}
|
|
isLoading={validatingTicketId === prize.ticketId}
|
|
disabled={validatingTicketId === prize.ticketId}
|
|
className="ml-4 bg-green-600 hover:bg-green-700"
|
|
>
|
|
<CheckCircle className="w-5 h-5 mr-2" />
|
|
Marquer comme remis
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|