Improve display of commercial processing status for avoirs

Adjusted the display of the "Traité por commercial" status by wrapping the icon in a div to ensure consistent tooltip behavior across different browsers.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869/7eY9Hl8
This commit is contained in:
michaelschal committed 2025-09-08 20:50:41 +00:00
1 parent c2edd36ccc
commit acbc36fda1
2 files changed
+3 -582

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+3 -582
View File
@@ -989,7 +989,9 @@ export default function Avoirs() {
<div className="flex items-center">
{avoir.amount !== null && avoir.amount !== undefined ? `${Number(avoir.amount).toFixed(2)} €` : 'Non spécifié'}
{avoir.commercialProcessed && (
<UserCheck className="ml-2 h-4 w-4 text-purple-600" title="Traité par commercial" />
<div title="Traité par commercial">
<UserCheck className="ml-2 h-4 w-4 text-purple-600" />
</div>
)}
</div>
</td>
@@ -1692,584 +1694,3 @@ export default function Avoirs() {
</div>
);
}
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Rechercher par référence, fournisseur, commentaire..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* Filtrage maintenant géré par le contexte global Layout */}
</div>
{/* Avoirs List - Format Table */}
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
{isLoading ? (
<div className="p-4">
<div className="animate-pulse space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-16 bg-gray-200 rounded"></div>
))}
</div>
</div>
) : filteredAvoirs.length === 0 ? (
<div className="p-12 text-center">
<FileText className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-4 text-sm font-medium text-gray-900">Aucun avoir</h3>
<p className="mt-1 text-sm text-gray-500">
{searchTerm ? "Aucun avoir ne correspond à votre recherche." : "Commencez par créer votre premier avoir."}
</p>
{!searchTerm && (
<Button className="mt-4" onClick={() => setIsCreateDialogOpen(true)}>
<Plus className="h-4 w-4 mr-2" />
Créer un avoir
</Button>
)}
</div>
) : (
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fournisseur
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Statut
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Référence
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Magasin
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Montant
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Créé le
</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Vérification
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredAvoirs?.filter(avoir => avoir && avoir.id).map((avoir) => {
const canEditDelete = ['admin', 'directeur'].includes((user as any)?.role);
return (
<tr key={avoir?.id || Math.random()} className={`hover:bg-gray-50 ${avoir?.nocodbVerified ? 'bg-gray-100 opacity-75' : ''}`}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{avoir.supplier?.name || 'Fournisseur non défini'}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<Select
value={avoir?.status || ''}
onValueChange={(newStatus) => handleStatusChange(avoir.id, newStatus)}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="En attente de demande">En attente de demande</SelectItem>
<SelectItem value="Demandé">Demandé</SelectItem>
<SelectItem value="Reçu">Reçu</SelectItem>
</SelectContent>
</Select>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
#{avoir.invoiceReference || 'Sans référence'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<div className="flex items-center">
<div
className="w-3 h-3 rounded-full mr-2"
style={{ backgroundColor: avoir.group?.color || '#gray' }}
/>
{avoir.group?.name || 'Magasin non défini'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
<div className="flex items-center">
{avoir.amount !== null && avoir.amount !== undefined ? `${Number(avoir.amount).toFixed(2)} €` : 'Non spécifié'}
{avoir.commercialProcessed && (
<div title="Avoir fait par commercial">
<UserCheck className="h-4 w-4 text-blue-600 ml-2" />
</div>
)}
{avoir.webhookSent && (
<div title="Webhook envoyé">
<Send className="h-4 w-4 text-green-600 ml-2" />
</div>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div>
<div>
{avoir?.createdAt
? format(new Date(avoir.createdAt), "dd/MM/yyyy", { locale: fr })
: 'Non défini'
}
</div>
<div className="text-xs text-gray-400">
Par {avoir?.creator?.firstName && avoir?.creator?.lastName
? `${avoir.creator.firstName} ${avoir.creator.lastName}`
: avoir?.creator?.username || 'Utilisateur inconnu'
}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-center text-sm">
{avoir.invoiceReference?.trim() ? (
<div className="flex items-center justify-center">
{verifyingAvoirs.has(avoir.id) ? (
<Clock className="h-4 w-4 text-blue-500 animate-spin" />
) : (avoirVerificationResults[avoir.id]?.exists === true || avoir.nocodbVerified) ? (
<div title={`Vérifié: ${avoirVerificationResults[avoir.id]?.exists ? 'cache' : avoir.nocodbVerified ? 'validé' : 'inconnu'}`}>
<CheckCircle className="h-4 w-4 text-green-500 cursor-help" />
</div>
) : avoirVerificationResults[avoir.id]?.exists === false ? (
<XCircle className="h-4 w-4 text-red-500 cursor-help" />
) : (
<button
onClick={() => handleVerifyAvoirInvoice(avoir)}
className="h-4 w-4 text-gray-400 hover:text-blue-500 transition-colors"
title="Cliquer pour vérifier la facture"
>
<Search className="h-4 w-4" />
</button>
)}
</div>
) : (
<span className="text-gray-400 text-xs">Sans réf.</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center space-x-2">
{/* 📋 Icône Upload pour avoirs "Reçu" */}
{avoir?.status === 'Reçu' && (
<Button
variant="outline"
size="sm"
className="h-8 w-8 p-0 text-blue-600 hover:text-blue-700 hover:bg-blue-50"
title="Envoyer fichier avoir"
onClick={() => handleOpenAvoirModal(avoir)}
>
<Upload className="h-4 w-4" />
</Button>
)}
{/* Bouton Valider - apparaît si vérification réussie et pas encore validé */}
{avoirVerificationResults[avoir.id]?.exists === true && !avoir.nocodbVerified && (
<Button
variant="outline"
size="sm"
className="h-8 w-8 p-0 text-green-600 hover:text-green-700 hover:bg-green-50"
title="Valider l'avoir"
onClick={() => handleValidateAvoir(avoir.id)}
>
<CheckCircle className="h-4 w-4" />
</Button>
)}
{/* Bouton Dévalider - pour admins sur avoirs validés */}
{avoir.nocodbVerified && canEditDelete && (
<Button
variant="outline"
size="sm"
className="h-8 w-8 p-0 text-orange-600 hover:text-orange-700 hover:bg-orange-50"
title="Dévalider l'avoir"
onClick={() => handleDevalidateAvoir(avoir.id)}
>
<XCircle className="h-4 w-4" />
</Button>
)}
{/* Bouton Commentaire - apparaît seulement s'il y a un commentaire */}
{avoir.comment && avoir.comment.trim() !== '' && (
<Button
variant="outline"
size="sm"
className="h-8 w-8 p-0 text-purple-600 hover:text-purple-700 hover:bg-purple-50"
title="Voir le commentaire"
onClick={() => handleViewComment(avoir.comment || '')}
>
<MessageSquare className="h-4 w-4" />
</Button>
)}
{canEditDelete && (
<>
<Button
variant="outline"
size="sm"
className="h-8 w-8 p-0"
title="Modifier"
onClick={() => handleEdit(avoir)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
className="h-8 w-8 p-0 text-red-600 hover:text-red-700 hover:bg-red-50"
title="Supprimer"
onClick={() => handleDelete(avoir)}
>
<Trash2 className="h-4 w-4" />
</Button>
</>
)}
{!canEditDelete && avoir?.status !== 'Reçu' && (
<span className="text-gray-400 text-xs">Lecture seule</span>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
</div>
{/* Edit Modal */}
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Modifier l'avoir</DialogTitle>
<DialogDescription>
Modifiez les informations de l'avoir.
</DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit(onEditSubmit)} className="space-y-4">
<FormField
control={editForm.control}
name="supplierId"
render={({ field }) => (
<FormItem>
<FormLabel>Fournisseur *</FormLabel>
<Select
value={field.value.toString()}
onValueChange={(value) => field.onChange(parseInt(value))}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Sélectionnez un fournisseur" />
</SelectTrigger>
</FormControl>
<SelectContent>
{suppliers.map((supplier) => (
<SelectItem key={supplier.id} value={supplier.id.toString()}>
{supplier.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="invoiceReference"
render={({ field }) => (
<FormItem>
<FormLabel>Référence facture</FormLabel>
<FormControl>
<Input placeholder="Référence de la facture (optionnel)" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Montant</FormLabel>
<FormControl>
<Input
type="number"
step="0.01"
placeholder="Montant en euros (optionnel)"
value={editAmountValue}
onChange={(e) => {
// ✅ Utiliser seulement l'état local, complètement séparé de React Hook Form
setEditAmountValue(e.target.value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Statut</FormLabel>
<Select value={field.value} onValueChange={field.onChange}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="En attente de demande">En attente de demande</SelectItem>
<SelectItem value="Demandé">Demandé</SelectItem>
<SelectItem value="Reçu">Reçu</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="comment"
render={({ field }) => (
<FormItem>
<FormLabel>Commentaire</FormLabel>
<FormControl>
<Textarea
placeholder="Commentaire ou description (optionnel)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="commercialProcessed"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
Avoir fait par commercial
</FormLabel>
</div>
</FormItem>
)}
/>
<div className="flex justify-end space-x-2 pt-4">
<Button type="button" variant="outline" onClick={() => setIsEditDialogOpen(false)}>
Annuler
</Button>
<Button
type="submit"
disabled={editAvoirMutation.isPending}
>
{editAvoirMutation.isPending ? "Modification..." : "Modifier"}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
{/* Delete Modal */}
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmer la suppression</AlertDialogTitle>
<AlertDialogDescription>
Êtes-vous sûr de vouloir supprimer cet avoir ? Cette action ne peut pas être annulée.
{selectedAvoir && (
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
<div className="text-sm">
<div><strong>Fournisseur:</strong> {selectedAvoir?.supplier?.name || 'Fournisseur non défini'}</div>
<div><strong>Référence:</strong> {selectedAvoir?.invoiceReference || 'Sans référence'}</div>
<div><strong>Montant:</strong> {selectedAvoir?.amount !== null && selectedAvoir?.amount !== undefined ? `${Number(selectedAvoir.amount).toFixed(2)} €` : 'Non spécifié'}</div>
</div>
</div>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
disabled={deleteAvoirMutation.isPending}
className="bg-red-600 hover:bg-red-700"
>
{deleteAvoirMutation.isPending ? "Suppression..." : "Supprimer"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 🌐 Modal d'envoi d'avoir (comme rapprochement) */}
<Dialog open={showAvoirModal} onOpenChange={setShowAvoirModal}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Envoyer Fichier Avoir</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
{selectedAvoirForUpload && (
<div className="bg-gray-50 p-3 rounded-md">
<div className="text-sm">
<div className="font-medium">{selectedAvoirForUpload.supplier?.name || 'Fournisseur non défini'}</div>
<div className="text-gray-600">
Référence: {selectedAvoirForUpload.invoiceReference || 'Non renseigné'}
</div>
<div className="text-gray-600">
Magasin: {selectedAvoirForUpload?.group?.name || 'Magasin non défini'}
</div>
<div className="text-gray-600">
Montant: {selectedAvoirForUpload?.amount !== null && selectedAvoirForUpload?.amount !== undefined ? `${Number(selectedAvoirForUpload.amount).toFixed(2)} €` : 'Non spécifié'}
</div>
</div>
</div>
)}
<div className="grid gap-2">
<Label htmlFor="avoir-pdf-file">Fichier PDF</Label>
<input
id="avoir-pdf-file"
type="file"
accept=".pdf"
onChange={handleFileChange}
className="w-full p-2 border border-gray-300 rounded-md"
/>
{selectedFile && (
<div className="text-sm text-green-600">
Fichier sélectionné: {selectedFile.name}
</div>
)}
</div>
{selectedAvoirForUpload && (
<div className="text-xs text-gray-500">
Envoi via webhook configuré pour {selectedAvoirForUpload.group?.name || 'magasin'}
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={handleCloseAvoirModal}>
Annuler
</Button>
<Button
onClick={handleSendAvoir}
disabled={!selectedFile || isUploading}
>
{isUploading ? "Envoi..." : "Envoyer"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* ⏱️ Modal d'attente pour le traitement du webhook */}
<Dialog open={showWaitingModal} onOpenChange={() => {}}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<Clock className="w-5 h-5 text-blue-600 animate-spin" />
<span>Traitement en cours</span>
</DialogTitle>
</DialogHeader>
<div className="grid gap-6 py-6">
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-4 relative">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-xs font-bold text-blue-600">{processingSeconds}s</span>
</div>
</div>
<div className="space-y-2">
<h3 className="font-medium text-gray-900">
Traitement de votre avoir en cours...
</h3>
<p className="text-sm text-gray-600">
Le workflow peut prendre jusqu'à 1 minute.
<br />
Veuillez patienter.
</p>
</div>
<div className="mt-4 bg-gray-100 rounded-full h-2 w-full">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-1000 ease-out"
style={{ width: `${Math.min((processingSeconds / 60) * 100, 100)}%` }}
></div>
</div>
<div className="mt-2 text-xs text-gray-500">
{processingSeconds < 60 ? `${60 - processingSeconds}s restantes (max)` : 'Finalisation...'}
</div>
</div>
{selectedAvoirForUpload && (
<div className="bg-blue-50 p-3 rounded-md">
<div className="text-sm">
<div className="font-medium text-blue-900">
{selectedAvoirForUpload.supplier?.name || 'Fournisseur non défini'}
</div>
<div className="text-blue-700">
Référence: {selectedAvoirForUpload.invoiceReference || 'Non renseigné'}
</div>
<div className="text-blue-700">
Fichier: {selectedFile?.name}
</div>
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
{/* Modal de commentaire */}
<Dialog open={showCommentModal} onOpenChange={setShowCommentModal}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Commentaire</DialogTitle>
<DialogDescription>
Détails du commentaire de cet avoir
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-700 whitespace-pre-wrap">
{selectedComment}
</p>
</div>
</div>
<div className="flex justify-end">
<Button variant="outline" onClick={() => setShowCommentModal(false)}>
Fermer
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
}