Merge pull request #426 from R0m1k3/1.1

1.1
This commit is contained in:
LogiFlow authored and GitHub committed 2025-09-09 15:27:27 +02:00
commit eaeee18542
2 files changed
+183 -1

No files matched your search

+136 -1
View File
@@ -12,10 +12,11 @@ import { useStore } from "@/components/Layout";
import { useAuthUnified } from "@/hooks/useAuthUnified";
import { usePermissions } from "@shared/permissions";
import { Pagination, usePagination } from "@/components/ui/pagination";
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter, Upload, CheckCircle, XCircle, Clock } from "lucide-react";
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter, Upload, CheckCircle, XCircle, Clock, MessageSquare } from "lucide-react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import ReconciliationModal from "@/components/modals/ReconciliationModal";
export default function BLReconciliation() {
@@ -66,6 +67,11 @@ export default function BLReconciliation() {
const [verificationResults, setVerificationResults] = useState<Record<number, any>>({});
const [verifyingDeliveries, setVerifyingDeliveries] = useState<Set<number>>(new Set());
// État pour le modal de commentaire
const [showCommentModal, setShowCommentModal] = useState(false);
const [selectedDeliveryForComment, setSelectedDeliveryForComment] = useState<any>(null);
const [commentText, setCommentText] = useState("");
// Récupérer les fournisseurs pour la logique automatique
const { data: suppliers = [] } = useQuery<any[]>({
queryKey: ['/api/suppliers'],
@@ -137,6 +143,42 @@ export default function BLReconciliation() {
}
});
// Mutation pour mettre à jour les notes
const updateNoteMutation = useMutation({
mutationFn: async ({ deliveryId, notes }: { deliveryId: number; notes: string }) => {
const response = await apiRequest(`/api/deliveries/${deliveryId}/notes`, 'PUT', { notes });
return response;
},
onSuccess: () => {
toast({
title: "Commentaire sauvegardé",
description: "Le commentaire a été mis à jour avec succès",
});
// Rafraîchir les données
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] });
handleCloseCommentModal();
},
onError: (error) => {
console.error('Erreur sauvegarde commentaire:', error);
toast({
title: "Erreur",
description: "Impossible de sauvegarder le commentaire",
variant: "destructive",
});
}
});
// Fonction pour sauvegarder le commentaire
const handleSaveComment = () => {
if (!selectedDeliveryForComment) return;
updateNoteMutation.mutate({
deliveryId: selectedDeliveryForComment.id,
notes: commentText
});
};
// Fonction pour déclencher la vérification
const handleVerifyInvoice = (delivery: any, forceRefresh: boolean = false) => {
// Accepter soit une référence de facture soit un numéro BL
@@ -347,6 +389,19 @@ export default function BLReconciliation() {
setShowInvoiceModal(true);
};
// Fonctions pour le modal de commentaire
const handleOpenCommentModal = (delivery: any) => {
setSelectedDeliveryForComment(delivery);
setCommentText(delivery.notes || "");
setShowCommentModal(true);
};
const handleCloseCommentModal = () => {
setShowCommentModal(false);
setSelectedDeliveryForComment(null);
setCommentText("");
};
const handleCloseInvoiceModal = () => {
setShowInvoiceModal(false);
setSelectedDeliveryForInvoice(null);
@@ -926,6 +981,15 @@ export default function BLReconciliation() {
<Upload className="h-4 w-4" />
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={() => handleOpenCommentModal(delivery)}
className={`h-8 w-8 p-0 ${delivery.notes ? 'text-blue-600 hover:text-blue-700' : 'text-gray-400 hover:text-gray-600'}`}
title={delivery.notes ? "Voir/Modifier commentaire" : "Ajouter commentaire"}
>
<MessageSquare className="h-4 w-4" />
</Button>
{!delivery.reconciled ? (
<>
{permissions.canValidate('reconciliation') && (
@@ -1129,6 +1193,17 @@ export default function BLReconciliation() {
<Upload className="h-4 w-4" />
</button>
)}
<button
onClick={() => handleOpenCommentModal(delivery)}
className={`transition-colors duration-200 p-1 rounded ${
delivery.notes
? 'text-blue-600 hover:text-blue-700 hover:bg-blue-50'
: 'text-gray-400 hover:text-gray-600 hover:bg-gray-50'
}`}
title={delivery.notes ? "Voir/Modifier commentaire" : "Ajouter commentaire"}
>
<MessageSquare className="h-4 w-4" />
</button>
{(permissions.canEdit('reconciliation') || permissions.canValidate('reconciliation')) && (
<button
onClick={() => handleDevalidateReconciliation(delivery.id)}
@@ -1306,6 +1381,66 @@ export default function BLReconciliation() {
</div>
</DialogContent>
</Dialog>
{/* Modal de commentaire */}
<Dialog open={showCommentModal} onOpenChange={setShowCommentModal}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<MessageSquare className="w-5 h-5 text-blue-600" />
<span>Commentaire de livraison</span>
</DialogTitle>
</DialogHeader>
{selectedDeliveryForComment && (
<div className="grid gap-4 py-4">
<div className="bg-gray-50 p-4 rounded-lg">
<div className="text-sm font-medium text-gray-900">
{selectedDeliveryForComment.supplier?.name}
</div>
<div className="text-sm text-gray-600">
BL: {selectedDeliveryForComment.blNumber || 'Non renseigné'} |
Facture: {selectedDeliveryForComment.invoiceReference || 'Non renseignée'}
</div>
<div className="text-sm text-gray-600">
Date prévue: {selectedDeliveryForComment.scheduledDate ? new Date(selectedDeliveryForComment.scheduledDate).toLocaleDateString('fr-FR') : 'Non renseignée'}
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="comment">Commentaire</Label>
<Textarea
id="comment"
placeholder="Ajouter un commentaire sur cette livraison..."
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
rows={4}
className="min-h-[100px]"
/>
<div className="text-xs text-gray-500">
{commentText.length}/500 caractères
</div>
</div>
</div>
)}
<DialogFooter>
<Button
variant="outline"
onClick={handleCloseCommentModal}
disabled={updateNoteMutation.isPending}
>
Annuler
</Button>
<Button
onClick={handleSaveComment}
disabled={updateNoteMutation.isPending || commentText.length > 500}
>
{updateNoteMutation.isPending ? "Sauvegarde..." : "Enregistrer"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
+47
View File
@@ -1326,6 +1326,53 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// Route pour mettre à jour uniquement les notes d'une livraison
app.put('/api/deliveries/:id/notes', isAuthenticated, async (req: any, res) => {
try {
const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);
if (!user) {
return res.status(404).json({ message: "User not found" });
}
const id = parseInt(req.params.id);
const delivery = await storage.getDelivery(id);
if (!delivery) {
return res.status(404).json({ message: "Delivery not found" });
}
// Check edit permissions
if (!hasPermission(user.role, 'deliveries', 'edit')) {
return res.status(403).json({ message: "Insufficient permissions to edit deliveries" });
}
// Check group access
if (user.role !== 'admin') {
const userGroupIds = (user as any).userGroups?.map((ug: any) => ug.groupId) || [];
if (!userGroupIds.includes(delivery.groupId)) {
return res.status(403).json({ message: "Access denied" });
}
}
const { notes } = req.body;
// Validate notes length
if (notes && notes.length > 500) {
return res.status(400).json({ message: "Commentaire trop long (maximum 500 caractères)" });
}
console.log('📝 Updating delivery notes:', { id, notes: notes ? notes.slice(0, 50) + '...' : 'empty', user: user.id });
// Update only the notes field
const updatedDelivery = await storage.updateDelivery(id, { notes });
res.json(updatedDelivery);
} catch (error) {
console.error("Error updating delivery notes:", error);
res.status(500).json({ message: "Failed to update delivery notes" });
}
});
// Route de vérification de facture NocoDB
app.post('/api/deliveries/:id/verify-invoice', isAuthenticated, async (req: any, res) => {
try {