966 lines
37 KiB
TypeScript
966 lines
37 KiB
TypeScript
'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>
|
|
)
|
|
}
|