feat(ui) : refonte du menu latéral et de la navigation téléphone (#569)

Menu PC et navigation téléphone tirés d'une configuration unique
(client/src/lib/navigation.ts), selon docs/PLAN-OPTIMISATION-WEBUI.md
(P1 § 2.1 et § 2.6).

- menu regroupé par usage (Au quotidien, Fournisseurs, Planning et infos,
  Administration), libellés clarifiés, chaque icône une seule fois ;
  droits d'accès PC inchangés
- l'envoi de BAP quitte le menu : bouton « Envoyer un BAP » (admin) dans
  l'en-tête du Rapprochement BL / factures
- téléphone : 4 raccourcis selon le rôle + « Menu » avec tous les modules
  autorisés, magasin, profil et déconnexion ; pages sans version mobile
  affichées dans le cadre téléphone
- titre de l'onglet du navigateur tiré du menu
- onglets du Rapprochement lisibles sur téléphone
- suppression de PhoneBottomNav.tsx (inutilisé)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JrRFddV2BaqDCxGY1j52UJ
This commit is contained in:
LogiFlowandClaude Opus 5.5 authored and GitHub committed 2026-10-10 09:35:35 +02:00
1 parent bd3bb5e64a
commit b964e137bf
9 files changed
+826 -1205

No files matched your search

@@ -0,0 +1,314 @@
import { useEffect, useRef, useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
import { Clock, FileUp, Send } from "lucide-react";
// URL utilisée quand aucun webhook BAP n'est configuré dans les paramètres
const DEFAULT_BAP_WEBHOOK_URL = "https://workflow.ffnancy.fr/webhook/a3d03176-b72f-412d-8fb9-f920b9fbab4d";
interface BapUploadModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
// Envoi d'un bon à payer (PDF) au workflow configuré, avec fenêtre d'attente
export default function BapUploadModal({ open, onOpenChange }: BapUploadModalProps) {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedRecipient, setSelectedRecipient] = useState<string>('');
const [isUploading, setIsUploading] = useState(false);
const [showWaitingModal, setShowWaitingModal] = useState(false);
const [processingSeconds, setProcessingSeconds] = useState(0);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const { toast } = useToast();
const stopProcessingTimer = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
useEffect(() => stopProcessingTimer, []);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file && file.type === 'application/pdf') {
setSelectedFile(file);
} else {
toast({
title: "Erreur",
description: "Veuillez sélectionner un fichier PDF",
variant: "destructive",
});
}
};
const startProcessingTimer = () => {
stopProcessingTimer();
setProcessingSeconds(0);
timerRef.current = setInterval(() => {
setProcessingSeconds(prev => {
if (prev >= 60) {
stopProcessingTimer();
return 60;
}
return prev + 1;
});
}, 1000);
};
const handleCloseWaitingModal = () => {
setShowWaitingModal(false);
setProcessingSeconds(0);
stopProcessingTimer();
};
const clearSelectedFile = () => {
setSelectedFile(null);
if (fileInputRef.current) fileInputRef.current.value = '';
};
const handleSendBap = async () => {
if (!selectedFile || !selectedRecipient) {
toast({
title: "Erreur",
description: "Veuillez sélectionner un fichier PDF et un destinataire",
variant: "destructive",
});
return;
}
// Récupérer l'URL webhook configurée
let webhookUrl = '';
try {
const configResponse = await fetch('/api/webhook-bap-config', {
credentials: 'include'
});
if (configResponse.ok) {
const config = await configResponse.json();
webhookUrl = config?.webhookUrl;
}
} catch (error) {
console.warn('Impossible de récupérer la config webhook, utilisation URL par défaut');
}
if (!webhookUrl) {
webhookUrl = DEFAULT_BAP_WEBHOOK_URL;
}
// Fermer la fenêtre de sélection et ouvrir celle d'attente
onOpenChange(false);
setShowWaitingModal(true);
setIsUploading(true);
startProcessingTimer();
try {
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('recipient', selectedRecipient);
formData.append('type', 'BAP');
formData.append('fileName', selectedFile.name);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 secondes
const response = await fetch(webhookUrl, {
method: 'POST',
body: formData,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Erreur ${response.status}: ${response.statusText}`);
}
handleCloseWaitingModal();
toast({
title: "Succès",
description: "Fichier BAP envoyé avec succès",
});
clearSelectedFile();
setSelectedRecipient('');
} catch (error: any) {
handleCloseWaitingModal();
let errorMessage = "Impossible d'envoyer le fichier BAP";
if (error.name === 'AbortError') {
errorMessage = "Le traitement a pris trop de temps (timeout de 60 secondes)";
} else if (error.message) {
errorMessage = error.message;
}
toast({
title: "Erreur",
description: errorMessage,
variant: "destructive",
});
// Réouvrir la fenêtre de sélection en cas d'erreur
onOpenChange(true);
} finally {
setIsUploading(false);
}
};
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileUp className="h-5 w-5 text-blue-600" />
Envoyer un bon à payer (BAP)
</DialogTitle>
</DialogHeader>
<div className="space-y-6">
<div className="space-y-3">
<Label className="text-sm font-semibold text-gray-800">Fichier PDF</Label>
{!selectedFile ? (
<button
type="button"
className="w-full border-2 border-dashed border-blue-300 rounded-xl p-8 bg-blue-50 hover:bg-blue-100 transition-colors group"
onClick={() => fileInputRef.current?.click()}
>
<div className="text-center space-y-4">
<div className="mx-auto h-16 w-16 bg-blue-600 rounded-full flex items-center justify-center group-hover:bg-blue-700 transition-colors">
<FileUp className="h-8 w-8 text-white" />
</div>
<div>
<p className="text-lg font-semibold text-gray-800 mb-2">
Choisir un fichier PDF
</p>
<p className="text-sm text-gray-600">
Cliquez pour parcourir vos fichiers
</p>
</div>
</div>
</button>
) : (
<div className="border-2 border-green-300 rounded-xl p-6 bg-green-50">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 bg-green-600 rounded-full flex items-center justify-center">
<FileUp className="h-6 w-6 text-white" />
</div>
<div className="flex-1 min-w-0">
<p className="font-semibold text-green-800 truncate">{selectedFile.name}</p>
<p className="text-sm text-green-700">
{(selectedFile.size / 1024 / 1024).toFixed(2)} Mo • PDF
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={clearSelectedFile}
className="border-green-300 text-green-700 hover:bg-green-100"
>
Changer
</Button>
</div>
</div>
)}
<input
ref={fileInputRef}
type="file"
accept=".pdf"
onChange={handleFileChange}
className="hidden"
/>
</div>
<div className="space-y-3">
<Label className="text-sm font-semibold text-gray-800">Envoyer à</Label>
<Select value={selectedRecipient} onValueChange={setSelectedRecipient}>
<SelectTrigger className="h-12 border-2 border-gray-200 rounded-lg hover:border-blue-300 transition-colors">
<SelectValue placeholder="Choisir le destinataire" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Laurie" className="py-3">
<div className="flex items-center space-x-3">
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-purple-600">L</span>
</div>
<span className="font-medium">Laurie</span>
</div>
</SelectItem>
<SelectItem value="Jeremy" className="py-3">
<div className="flex items-center space-x-3">
<div className="h-8 w-8 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-blue-600">J</span>
</div>
<span className="font-medium">Jeremy</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex gap-3 pt-4 border-t border-gray-200">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="flex-1 h-12 border-2 border-gray-200 hover:border-gray-300"
>
Annuler
</Button>
<Button
onClick={handleSendBap}
disabled={!selectedFile || !selectedRecipient || isUploading}
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white font-medium"
>
{isUploading ? (
<>
<div className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full mr-2"></div>
Envoi en cours...
</>
) : (
<>
<Send className="h-4 w-4 mr-2" />
Envoyer le BAP
</>
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Fenêtre d'attente pendant l'envoi */}
<Dialog open={showWaitingModal} onOpenChange={() => { }}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
Traitement en cours...
</DialogTitle>
</DialogHeader>
<div className="flex flex-col items-center space-y-4 text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<div className="text-sm text-gray-600">
Envoi du fichier vers le workflow...
</div>
<div className="text-sm text-gray-500">
{processingSeconds < 60
? `${processingSeconds}s écoulées`
: '60s+ écoulées'
}
</div>
</div>
</DialogContent>
</Dialog>
</>
);
}