Initial commit
This commit is contained in:
965
src/components/atelier/AtelierModule.tsx
Normal file
965
src/components/atelier/AtelierModule.tsx
Normal file
@@ -0,0 +1,965 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
Wrench,
|
||||
Clock,
|
||||
Play,
|
||||
CheckCircle2,
|
||||
Bell,
|
||||
User,
|
||||
Package,
|
||||
Eye,
|
||||
Calendar,
|
||||
FileText,
|
||||
AlertCircle,
|
||||
CheckCheck
|
||||
} from 'lucide-react'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
|
||||
// Types
|
||||
interface Produit {
|
||||
id: string
|
||||
reference: string
|
||||
designation: string
|
||||
categorie: string
|
||||
marque?: string
|
||||
typeMonture?: string
|
||||
typeVerre?: string
|
||||
materiau?: string
|
||||
couleur?: string
|
||||
}
|
||||
|
||||
interface Client {
|
||||
id: string
|
||||
nom: string
|
||||
prenom: string
|
||||
email?: string
|
||||
telephone: string
|
||||
}
|
||||
|
||||
interface Patient {
|
||||
id: string
|
||||
odSphere?: number
|
||||
odCylindre?: number
|
||||
odAxe?: number
|
||||
ogSphere?: number
|
||||
ogCylindre?: number
|
||||
ogAxe?: number
|
||||
addition?: number
|
||||
pd?: number
|
||||
hauteur?: number
|
||||
}
|
||||
|
||||
interface LigneVente {
|
||||
id: string
|
||||
produit: Produit
|
||||
quantite: number
|
||||
montantHT: number
|
||||
montantTTC: number
|
||||
}
|
||||
|
||||
interface WorkOrder {
|
||||
id: string
|
||||
numero: string
|
||||
date: string
|
||||
statutAtelier: 'EN_ATTENTE' | 'EN_COURS' | 'TERMINE' | 'PRET' | 'RETIRE'
|
||||
montantTTC: number
|
||||
client?: Client
|
||||
lignes: LigneVente[]
|
||||
patients?: Patient[]
|
||||
dateAtelier?: string
|
||||
dateRetrait?: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
type StatusFilter = 'ALL' | 'EN_ATTENTE' | 'EN_COURS' | 'TERMINE' | 'PRET' | 'RETIRE'
|
||||
|
||||
export default function AtelierModule() {
|
||||
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([])
|
||||
const [selectedOrder, setSelectedOrder] = useState<WorkOrder | null>(null)
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showDetailDialog, setShowDetailDialog] = useState(false)
|
||||
const [showNotifyDialog, setShowNotifyDialog] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<'orders' | 'ready' | 'history'>('orders')
|
||||
|
||||
// Load work orders
|
||||
const loadWorkOrders = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/atelier/orders?XTransformPort=3000')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setWorkOrders(data)
|
||||
} else {
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: 'Impossible de charger les commandes atelier',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading work orders:', error)
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: 'Impossible de charger les commandes atelier',
|
||||
variant: 'destructive'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Update order status
|
||||
const updateOrderStatus = async (orderId: string, newStatus: string) => {
|
||||
console.log('Frontend: updateOrderStatus called')
|
||||
console.log('Frontend: orderId =', orderId)
|
||||
console.log('Frontend: newStatus =', newStatus)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/atelier/orders/${orderId}?XTransformPort=3000`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ statutAtelier: newStatus })
|
||||
})
|
||||
|
||||
console.log('Frontend: response.ok =', response.ok)
|
||||
console.log('Frontend: response.status =', response.status)
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: 'Statut mis à jour',
|
||||
description: 'Le statut de la commande a été mis à jour'
|
||||
})
|
||||
loadWorkOrders()
|
||||
if (selectedOrder?.id === orderId) {
|
||||
setSelectedOrder({ ...selectedOrder, statutAtelier: newStatus as any })
|
||||
}
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
console.log('Frontend: errorData =', errorData)
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: errorData.error || 'Impossible de mettre à jour le statut',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating order status:', error)
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: 'Impossible de mettre à jour le statut',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// View order details
|
||||
const viewOrderDetails = async (order: WorkOrder) => {
|
||||
setSelectedOrder(order)
|
||||
setShowDetailDialog(true)
|
||||
}
|
||||
|
||||
// Mark as ready for pickup
|
||||
const markAsReady = async () => {
|
||||
if (!selectedOrder) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/atelier/orders/${selectedOrder.id}?XTransformPort=3000`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ statutAtelier: 'PRET' })
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: 'Commande prête',
|
||||
description: 'La commande est marquée comme prête pour le retrait'
|
||||
})
|
||||
loadWorkOrders()
|
||||
setShowNotifyDialog(false)
|
||||
setShowDetailDialog(false)
|
||||
} else {
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: 'Impossible de marquer la commande comme prête',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error marking order as ready:', error)
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: 'Impossible de marquer la commande comme prête',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm order pickup (ready → retrieved)
|
||||
const confirmRetrait = async (orderId: string) => {
|
||||
if (!confirm('Confirmer que le client a récupéré ses lunettes ?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/atelier/orders/${orderId}?XTransformPort=3000`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ statutAtelier: 'RETIRE' })
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: 'Retrait confirmé',
|
||||
description: 'La commande a été marquée comme retirée'
|
||||
})
|
||||
loadWorkOrders()
|
||||
} else {
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: 'Impossible de confirmer le retrait',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error confirming pickup:', error)
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: 'Impossible de confirmer le retrait',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Seed sample data
|
||||
const seedSampleData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/atelier/seed?XTransformPort=3000', {
|
||||
method: 'POST'
|
||||
})
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: 'Données ajoutées',
|
||||
description: 'Les données de test ont été créées avec succès'
|
||||
})
|
||||
loadWorkOrders()
|
||||
} else {
|
||||
const error = await response.json()
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: error.error || 'Impossible de créer les données de test',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error seeding data:', error)
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: 'Impossible de créer les données de test',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadWorkOrders()
|
||||
}, [])
|
||||
|
||||
// Filter orders
|
||||
const filteredOrders = workOrders.filter(order => {
|
||||
if (statusFilter === 'ALL') return true
|
||||
return order.statutAtelier === statusFilter
|
||||
})
|
||||
|
||||
const readyOrders = workOrders.filter(order => order.statutAtelier === 'PRET')
|
||||
|
||||
// Get status badge
|
||||
const getStatusBadge = (statut: string) => {
|
||||
const variants: Record<string, { color: string; label: string; icon: React.ReactNode }> = {
|
||||
'EN_ATTENTE': {
|
||||
color: 'bg-gray-500',
|
||||
label: 'En attente',
|
||||
icon: <Clock className="h-3 w-3 mr-1" />
|
||||
},
|
||||
'EN_COURS': {
|
||||
color: 'bg-blue-500',
|
||||
label: 'En cours',
|
||||
icon: <Play className="h-3 w-3 mr-1" />
|
||||
},
|
||||
'TERMINE': {
|
||||
color: 'bg-purple-500',
|
||||
label: 'Terminé',
|
||||
icon: <CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
},
|
||||
'PRET': {
|
||||
color: 'bg-green-500',
|
||||
label: 'Prêt',
|
||||
icon: <Bell className="h-3 w-3 mr-1" />
|
||||
},
|
||||
'RETIRE': {
|
||||
color: 'bg-orange-500',
|
||||
label: 'Retiré',
|
||||
icon: <CheckCheck className="h-3 w-3 mr-1" />
|
||||
}
|
||||
}
|
||||
const status = variants[statut] || variants['EN_ATTENTE']
|
||||
return (
|
||||
<Badge className={status.color}>
|
||||
{status.icon}
|
||||
{status.label}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
// Format vision data
|
||||
const formatVisionData = (patient?: Patient) => {
|
||||
if (!patient) return null
|
||||
return {
|
||||
od: `Sph: ${patient.odSphere || '-'} | Cyl: ${patient.odCylindre || '-'} | Axe: ${patient.odAxe || '-'}`,
|
||||
og: `Sph: ${patient.ogSphere || '-'} | Cyl: ${patient.ogCylindre || '-'} | Axe: ${patient.ogAxe || '-'}`,
|
||||
addition: patient.addition ? `Add: ${patient.addition}` : null,
|
||||
pd: patient.pd ? `PD: ${patient.pd}mm` : null,
|
||||
hauteur: patient.hauteur ? `Haut: ${patient.hauteur}mm` : null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">En attente</CardTitle>
|
||||
<Clock className="h-4 w-4 text-gray-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{workOrders.filter(o => o.statutAtelier === 'EN_ATTENTE').length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">En cours</CardTitle>
|
||||
<Play className="h-4 w-4 text-blue-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{workOrders.filter(o => o.statutAtelier === 'EN_COURS').length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Terminé</CardTitle>
|
||||
<CheckCircle2 className="h-4 w-4 text-purple-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{workOrders.filter(o => o.statutAtelier === 'TERMINE').length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Prêt à retirer</CardTitle>
|
||||
<Bell className="h-4 w-4 text-green-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{workOrders.filter(o => o.statutAtelier === 'PRET').length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Retiré</CardTitle>
|
||||
<CheckCheck className="h-4 w-4 text-orange-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{workOrders.filter(o => o.statutAtelier === 'RETIRE').length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as 'orders' | 'ready')}>
|
||||
<TabsList className="grid w-full max-w-lg grid-cols-3">
|
||||
<TabsTrigger value="orders">Commandes</TabsTrigger>
|
||||
<TabsTrigger value="ready">
|
||||
Prêtes
|
||||
{readyOrders.length > 0 && (
|
||||
<Badge className="ml-2 bg-green-500">{readyOrders.length}</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="history">
|
||||
Historique
|
||||
{workOrders.filter(o => o.statutAtelier === 'RETIRE').length > 0 && (
|
||||
<Badge className="ml-2 bg-orange-500">{workOrders.filter(o => o.statutAtelier === 'RETIRE').length}</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="orders" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Wrench className="h-5 w-5" />
|
||||
Commandes de Montage
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Gérez les commandes de montage de lunettes
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={statusFilter === 'ALL' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setStatusFilter('ALL')}
|
||||
>
|
||||
Toutes
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === 'EN_ATTENTE' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setStatusFilter('EN_ATTENTE')}
|
||||
>
|
||||
En attente
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === 'EN_COURS' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setStatusFilter('EN_COURS')}
|
||||
>
|
||||
En cours
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === 'TERMINE' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setStatusFilter('TERMINE')}
|
||||
>
|
||||
Terminé
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === 'RETIRE' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setStatusFilter('RETIRE')}
|
||||
>
|
||||
Retiré
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Chargement...
|
||||
</div>
|
||||
) : filteredOrders.length === 0 ? (
|
||||
<div className="text-center py-8 space-y-4">
|
||||
<AlertCircle className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">
|
||||
{statusFilter === 'ALL'
|
||||
? 'Aucune commande de montage en cours'
|
||||
: statusFilter === 'RETIRE'
|
||||
? 'Aucune commande retirée'
|
||||
: `Aucune commande avec le statut "${statusFilter === 'EN_ATTENTE' ? 'En attente' : statusFilter === 'EN_COURS' ? 'En cours' : statusFilter === 'TERMINE' ? 'Terminé' : 'Prêt'}"`
|
||||
}
|
||||
</p>
|
||||
{statusFilter === 'ALL' && workOrders.length === 0 && (
|
||||
<Button onClick={seedSampleData} variant="outline">
|
||||
<Wrench className="h-4 w-4 mr-2" />
|
||||
Ajouter des données de test
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="max-h-[600px]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>N° Commande</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Client</TableHead>
|
||||
<TableHead>Produits</TableHead>
|
||||
<TableHead>Statut</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredOrders.map((order) => (
|
||||
<TableRow key={order.id}>
|
||||
<TableCell className="font-medium">{order.numero}</TableCell>
|
||||
<TableCell>
|
||||
{new Date(order.date).toLocaleDateString('fr-FR')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{order.client
|
||||
? `${order.client.prenom} ${order.client.nom}`
|
||||
: 'Client anonyme'
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1">
|
||||
{order.lignes.slice(0, 2).map((ligne) => (
|
||||
<span key={ligne.id} className="text-xs">
|
||||
{ligne.produit.designation}
|
||||
</span>
|
||||
))}
|
||||
{order.lignes.length > 2 && (
|
||||
<span className="text-xs text-gray-500">
|
||||
+{order.lignes.length - 2} autre(s)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(order.statutAtelier)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => viewOrderDetails(order)}
|
||||
>
|
||||
Voir détails
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="ready">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Bell className="h-5 w-5 text-green-500" />
|
||||
Commandes prêtes pour retrait
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Les commandes terminées et prêtes à être remises aux clients
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{readyOrders.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<CheckCircle2 className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">
|
||||
Aucune commande prête pour le retrait
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="max-h-[600px]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>N° Commande</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Client</TableHead>
|
||||
<TableHead>Téléphone</TableHead>
|
||||
<TableHead>Prêt depuis</TableHead>
|
||||
<TableHead className="text-right">Montant TTC</TableHead>
|
||||
<TableHead className="text-center">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{readyOrders.map((order) => (
|
||||
<TableRow key={order.id}>
|
||||
<TableCell className="font-medium">{order.numero}</TableCell>
|
||||
<TableCell>
|
||||
{new Date(order.date).toLocaleDateString('fr-FR')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{order.client
|
||||
? `${order.client.prenom} ${order.client.nom}`
|
||||
: 'Client anonyme'
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{order.client?.telephone || '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{order.dateAtelier
|
||||
? new Date(order.dateAtelier).toLocaleDateString('fr-FR')
|
||||
: new Date(order.date).toLocaleDateString('fr-FR')
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-bold">
|
||||
{order.montantTTC.toFixed(2)} €
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => confirmRetrait(order.id)}
|
||||
className="text-green-600 hover:text-green-700 hover:bg-green-50"
|
||||
>
|
||||
<CheckCheck className="h-4 w-4 mr-2" />
|
||||
Confirmer retrait
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* History Tab */}
|
||||
<TabsContent value="history" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CheckCheck className="h-5 w-5 text-orange-500" />
|
||||
Historique des retraits
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Les commandes qui ont été retirées par les clients
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{workOrders.filter(o => o.statutAtelier === 'RETIRE').length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<CheckCheck className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">
|
||||
Aucune commande retirée
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="max-h-[600px]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>N° Commande</TableHead>
|
||||
<TableHead>Date de vente</TableHead>
|
||||
<TableHead>Client</TableHead>
|
||||
<TableHead>Téléphone</TableHead>
|
||||
<TableHead>Prêt le</TableHead>
|
||||
<TableHead>Retiré le</TableHead>
|
||||
<TableHead className="text-right">Montant TTC</TableHead>
|
||||
<TableHead className="text-center">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{workOrders.filter(o => o.statutAtelier === 'RETIRE').map((order) => (
|
||||
<TableRow key={order.id}>
|
||||
<TableCell className="font-medium">{order.numero}</TableCell>
|
||||
<TableCell>
|
||||
{new Date(order.date).toLocaleDateString('fr-FR')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{order.client
|
||||
? `${order.client.prenom} ${order.client.nom}`
|
||||
: 'Client anonyme'
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{order.client?.telephone || '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{order.dateAtelier
|
||||
? new Date(order.dateAtelier).toLocaleDateString('fr-FR')
|
||||
: '-'
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="text-green-600 font-medium">
|
||||
{order.dateRetrait
|
||||
? new Date(order.dateRetrait).toLocaleDateString('fr-FR')
|
||||
: '-'
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-bold">
|
||||
{order.montantTTC.toFixed(2)} €
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => viewOrderDetails(order)}
|
||||
>
|
||||
Voir détails
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Order Detail Dialog */}
|
||||
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Package className="h-5 w-5" />
|
||||
Détails de la commande {selectedOrder?.numero}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Gérez le montage et le statut de cette commande
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedOrder && (
|
||||
<div className="space-y-6">
|
||||
{/* Status and Actions */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Statut actuel</p>
|
||||
{getStatusBadge(selectedOrder.statutAtelier)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{selectedOrder.statutAtelier === 'EN_ATTENTE' && (
|
||||
<Button
|
||||
onClick={() => updateOrderStatus(selectedOrder.id, 'EN_COURS')}
|
||||
className="bg-blue-500 hover:bg-blue-600"
|
||||
>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Commencer le montage
|
||||
</Button>
|
||||
)}
|
||||
{selectedOrder.statutAtelier === 'EN_COURS' && (
|
||||
<Button
|
||||
onClick={() => updateOrderStatus(selectedOrder.id, 'TERMINE')}
|
||||
className="bg-purple-500 hover:bg-purple-600"
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2" />
|
||||
Marquer comme terminé
|
||||
</Button>
|
||||
)}
|
||||
{selectedOrder.statutAtelier === 'TERMINE' && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSelectedOrder(selectedOrder)
|
||||
setShowNotifyDialog(true)
|
||||
}}
|
||||
className="bg-green-500 hover:bg-green-600"
|
||||
>
|
||||
<Bell className="h-4 w-4 mr-2" />
|
||||
Marquer comme prêt
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Client Information */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-4 w-4 text-gray-500" />
|
||||
<h3 className="font-semibold">Client</h3>
|
||||
</div>
|
||||
{selectedOrder.client ? (
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<p className="font-medium">{selectedOrder.client.prenom} {selectedOrder.client.nom}</p>
|
||||
<p className="text-sm text-gray-600">{selectedOrder.client.telephone}</p>
|
||||
{selectedOrder.client.email && (
|
||||
<p className="text-sm text-gray-600">{selectedOrder.client.email}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">Client anonyme</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Vision Measurements */}
|
||||
{selectedOrder.patients && selectedOrder.patients.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye className="h-4 w-4 text-gray-500" />
|
||||
<h3 className="font-semibold">Mesures de vision</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{selectedOrder.patients.map((patient) => {
|
||||
const vision = formatVisionData(patient)
|
||||
if (!vision) return null
|
||||
return (
|
||||
<Card key={patient.id}>
|
||||
<CardContent className="pt-4 space-y-2">
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-blue-600">Œil Droit (OD)</p>
|
||||
<p className="text-xs text-gray-600">{vision.od}</p>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-green-600">Œil Gauche (OG)</p>
|
||||
<p className="text-xs text-gray-600">{vision.og}</p>
|
||||
</div>
|
||||
{(vision.addition || vision.pd || vision.hauteur) && (
|
||||
<div className="text-xs text-gray-600">
|
||||
{[vision.addition, vision.pd, vision.hauteur].filter(Boolean).join(' | ')}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Products */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="h-4 w-4 text-gray-500" />
|
||||
<h3 className="font-semibold">Produits à monter</h3>
|
||||
</div>
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Référence</TableHead>
|
||||
<TableHead>Désignation</TableHead>
|
||||
<TableHead>Catégorie</TableHead>
|
||||
<TableHead className="text-right">Quantité</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{selectedOrder.lignes.map((ligne) => (
|
||||
<TableRow key={ligne.id}>
|
||||
<TableCell className="font-medium">{ligne.produit.reference}</TableCell>
|
||||
<TableCell>{ligne.produit.designation}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{ligne.produit.categorie}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{ligne.quantite}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dates */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4 text-gray-500" />
|
||||
<h3 className="font-semibold">Dates</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-gray-500">Date de vente:</p>
|
||||
<p className="font-medium">
|
||||
{new Date(selectedOrder.date).toLocaleDateString('fr-FR')} à{' '}
|
||||
{new Date(selectedOrder.date).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">Dernière mise à jour atelier:</p>
|
||||
<p className="font-medium">
|
||||
{selectedOrder.dateAtelier
|
||||
? `${new Date(selectedOrder.dateAtelier).toLocaleDateString('fr-FR')} à ${new Date(selectedOrder.dateAtelier).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`
|
||||
: '-'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{selectedOrder.statutAtelier === 'RETIRE' && selectedOrder.dateRetrait && (
|
||||
<div className="mt-2 pt-2 border-t">
|
||||
<div className="text-sm">
|
||||
<p className="text-gray-500">Date de retrait:</p>
|
||||
<p className="font-medium text-orange-600">
|
||||
{new Date(selectedOrder.dateRetrait).toLocaleDateString('fr-FR')} à{' '}
|
||||
{new Date(selectedOrder.dateRetrait).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedOrder.notes && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-gray-500" />
|
||||
<h3 className="font-semibold">Notes</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 bg-gray-50 p-3 rounded-lg">
|
||||
{selectedOrder.notes}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-lg font-bold">
|
||||
Total TTC: {selectedOrder.montantTTC.toFixed(2)} €
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => setShowDetailDialog(false)}>
|
||||
Fermer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Notify Client Dialog */}
|
||||
<Dialog open={showNotifyDialog} onOpenChange={setShowNotifyDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-green-600">
|
||||
<Bell className="h-5 w-5" />
|
||||
Notifier le client
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Confirmer que cette commande est prête pour le retrait
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedOrder && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<p className="font-medium mb-2">La commande {selectedOrder.numero} est prête!</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{selectedOrder.client
|
||||
? `Le client ${selectedOrder.client.prenom} ${selectedOrder.client.nom} pourra être notifié que ses lunettes sont prêtes.`
|
||||
: 'La commande est marquée comme prête pour retrait.'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="outline" onClick={() => setShowNotifyDialog(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button onClick={markAsReady} className="bg-green-500 hover:bg-green-600">
|
||||
<CheckCircle2 className="h-4 w-4 mr-2" />
|
||||
Confirmer et marquer comme prêt
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user