Checkpoint: Ajout du bouton "Imports & Données" au-dessus du bouton Paramètres dans la sidebar (icône Upload, couleur indigo). Création du composant ImportModal avec 3 onglets : Inventaire postes (ISI-APP), Établissements (import CSV), Utilisateurs (import CSV). Enrichissement de la page Paramètres avec 3 onglets : Paramètres (contenu existant), Établissements (CRUD + activer/désactiver), Utilisateurs (CRUD + rattachement multi-établissements par checkboxes).
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
// └── Paramètres → /parametres
|
// └── Paramètres → /parametres
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { ImportModal } from './ImportModal';
|
||||||
import { useLocation } from 'wouter';
|
import { useLocation } from 'wouter';
|
||||||
import {
|
import {
|
||||||
BarChart3,
|
BarChart3,
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
PanelLeftOpen,
|
PanelLeftOpen,
|
||||||
TableProperties,
|
TableProperties,
|
||||||
|
Upload,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// Palette de couleurs par section principale
|
// Palette de couleurs par section principale
|
||||||
@@ -203,6 +205,7 @@ interface AppSidebarProps {
|
|||||||
export function AppSidebar({ collapsed = false, onToggle }: AppSidebarProps) {
|
export function AppSidebar({ collapsed = false, onToggle }: AppSidebarProps) {
|
||||||
const [location, navigate] = useLocation();
|
const [location, navigate] = useLocation();
|
||||||
const [openMenus, setOpenMenus] = useState<Set<string>>(() => getInitialOpenSections(location));
|
const [openMenus, setOpenMenus] = useState<Set<string>>(() => getInitialOpenSections(location));
|
||||||
|
const [showImport, setShowImport] = useState(false);
|
||||||
|
|
||||||
// Mode accordéon : ferme les autres sections du même niveau lors de l'ouverture
|
// Mode accordéon : ferme les autres sections du même niveau lors de l'ouverture
|
||||||
const toggleSection = (id: string) => {
|
const toggleSection = (id: string) => {
|
||||||
@@ -385,8 +388,23 @@ export function AppSidebar({ collapsed = false, onToggle }: AppSidebarProps) {
|
|||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Paramètres — entrée bien visible en bas de sidebar */}
|
{/* Imports & Données + Paramètres — entrées bien visibles en bas de sidebar */}
|
||||||
<div className="px-2 pb-3 pt-2 border-t border-white/10">
|
<div className="px-2 pb-3 pt-2 border-t border-white/10 space-y-1">
|
||||||
|
{/* Bouton Imports & Données */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowImport(true)}
|
||||||
|
className="w-full flex items-center gap-2.5 px-3 py-2.5 rounded-lg transition-all duration-150 text-left group text-white/70 hover:bg-white/12 hover:text-white"
|
||||||
|
>
|
||||||
|
<div className="w-7 h-7 rounded-md flex items-center justify-center flex-shrink-0 transition-colors bg-indigo-500/30 group-hover:bg-indigo-500/50">
|
||||||
|
<Upload className="w-3.5 h-3.5 text-indigo-200" />
|
||||||
|
</div>
|
||||||
|
{!collapsed && (
|
||||||
|
<span className="flex-1 text-xs font-semibold tracking-wide truncate">Imports & Données</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Modale d'import */}
|
||||||
|
<ImportModal open={showImport} onClose={() => setShowImport(false)} />
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(SETTINGS_ITEM.path)}
|
onClick={() => navigate(SETTINGS_ITEM.path)}
|
||||||
className={`w-full flex items-center gap-2.5 px-3 py-2.5 rounded-lg transition-all duration-150 text-left group ${
|
className={`w-full flex items-center gap-2.5 px-3 py-2.5 rounded-lg transition-all duration-150 text-left group ${
|
||||||
|
|||||||
450
client/src/components/ImportModal.tsx
Normal file
450
client/src/components/ImportModal.tsx
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
// ImportModal.tsx — Fenêtre d'import de données (Inventaire, Établissements, Utilisateurs)
|
||||||
|
// Design: Corporate Modernism — Itinova Budget SI
|
||||||
|
|
||||||
|
import { useState, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Upload,
|
||||||
|
X,
|
||||||
|
Monitor,
|
||||||
|
Building2,
|
||||||
|
Users,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertCircle,
|
||||||
|
FileSpreadsheet,
|
||||||
|
Info,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface Etablissement {
|
||||||
|
code: string;
|
||||||
|
nom: string;
|
||||||
|
groupe?: string;
|
||||||
|
ville?: string;
|
||||||
|
actif: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Utilisateur {
|
||||||
|
id: string;
|
||||||
|
nom: string;
|
||||||
|
prenom: string;
|
||||||
|
email: string;
|
||||||
|
role: 'admin' | 'standard' | 'lecture';
|
||||||
|
etablissements: string[]; // codes établissements
|
||||||
|
actif: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Storage helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const ETAB_KEY = 'budgetsi_etablissements';
|
||||||
|
const USER_KEY = 'budgetsi_utilisateurs';
|
||||||
|
|
||||||
|
export function loadEtablissements(): Etablissement[] {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(ETAB_KEY);
|
||||||
|
return raw ? JSON.parse(raw) : [];
|
||||||
|
} catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveEtablissements(data: Etablissement[]) {
|
||||||
|
localStorage.setItem(ETAB_KEY, JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadUtilisateurs(): Utilisateur[] {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(USER_KEY);
|
||||||
|
return raw ? JSON.parse(raw) : [];
|
||||||
|
} catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveUtilisateurs(data: Utilisateur[]) {
|
||||||
|
localStorage.setItem(USER_KEY, JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Sub-components ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type ImportTab = 'inventaire' | 'etablissements' | 'utilisateurs';
|
||||||
|
|
||||||
|
interface DropZoneProps {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
accept: string;
|
||||||
|
onFile: (file: File) => void;
|
||||||
|
status: 'idle' | 'success' | 'error';
|
||||||
|
statusMsg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropZone({ label, description, accept, onFile, status, statusMsg }: DropZoneProps) {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [dragging, setDragging] = useState(false);
|
||||||
|
|
||||||
|
const handleFile = (file: File) => {
|
||||||
|
if (!file) return;
|
||||||
|
onFile(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all duration-200 ${
|
||||||
|
dragging
|
||||||
|
? 'border-blue-400 bg-blue-50'
|
||||||
|
: status === 'success'
|
||||||
|
? 'border-emerald-400 bg-emerald-50'
|
||||||
|
: status === 'error'
|
||||||
|
? 'border-red-400 bg-red-50'
|
||||||
|
: 'border-border hover:border-blue-300 hover:bg-blue-50/40'
|
||||||
|
}`}
|
||||||
|
onClick={() => inputRef.current?.click()}
|
||||||
|
onDragOver={e => { e.preventDefault(); setDragging(true); }}
|
||||||
|
onDragLeave={() => setDragging(false)}
|
||||||
|
onDrop={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragging(false);
|
||||||
|
const file = e.dataTransfer.files[0];
|
||||||
|
if (file) handleFile(file);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
accept={accept}
|
||||||
|
className="hidden"
|
||||||
|
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f); e.target.value = ''; }}
|
||||||
|
/>
|
||||||
|
{status === 'success' ? (
|
||||||
|
<CheckCircle2 className="w-10 h-10 text-emerald-500 mx-auto mb-3" />
|
||||||
|
) : status === 'error' ? (
|
||||||
|
<AlertCircle className="w-10 h-10 text-red-500 mx-auto mb-3" />
|
||||||
|
) : (
|
||||||
|
<FileSpreadsheet className="w-10 h-10 text-blue-400 mx-auto mb-3" />
|
||||||
|
)}
|
||||||
|
<p className="font-semibold text-sm text-foreground">{label}</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">{description}</p>
|
||||||
|
{statusMsg && (
|
||||||
|
<p className={`text-xs mt-2 font-medium ${status === 'success' ? 'text-emerald-600' : 'text-red-600'}`}>
|
||||||
|
{statusMsg}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{status === 'idle' && (
|
||||||
|
<button className="mt-3 inline-flex items-center gap-1.5 px-4 py-1.5 bg-blue-600 text-white text-xs rounded-lg hover:bg-blue-700 transition-colors">
|
||||||
|
<Upload className="w-3.5 h-3.5" />
|
||||||
|
Choisir un fichier
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface ImportModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImportModal({ open, onClose }: ImportModalProps) {
|
||||||
|
const [tab, setTab] = useState<ImportTab>('inventaire');
|
||||||
|
const [inventaireStatus, setInventaireStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||||
|
const [inventaireMsg, setInventaireMsg] = useState('');
|
||||||
|
const [etabStatus, setEtabStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||||
|
const [etabMsg, setEtabMsg] = useState('');
|
||||||
|
const [userStatus, setUserStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||||
|
const [userMsg, setUserMsg] = useState('');
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
// ── Inventaire ISI-APP ──────────────────────────────────────────────────────
|
||||||
|
const handleInventaireFile = async (file: File) => {
|
||||||
|
try {
|
||||||
|
// Validation basique du format
|
||||||
|
if (!file.name.match(/\.(xlsx|xls|csv)$/i)) {
|
||||||
|
setInventaireStatus('error');
|
||||||
|
setInventaireMsg('Format non supporté. Utilisez .xlsx, .xls ou .csv');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Stocker le nom du fichier importé
|
||||||
|
localStorage.setItem('budgetsi_inventaire_import', JSON.stringify({
|
||||||
|
filename: file.name,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
size: file.size,
|
||||||
|
}));
|
||||||
|
setInventaireStatus('success');
|
||||||
|
setInventaireMsg(`Fichier "${file.name}" importé avec succès`);
|
||||||
|
toast.success('Inventaire importé', { description: `Fichier ${file.name} enregistré` });
|
||||||
|
} catch {
|
||||||
|
setInventaireStatus('error');
|
||||||
|
setInventaireMsg('Erreur lors de l\'import du fichier');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Établissements ──────────────────────────────────────────────────────────
|
||||||
|
const handleEtabFile = async (file: File) => {
|
||||||
|
try {
|
||||||
|
if (!file.name.match(/\.(xlsx|xls|csv)$/i)) {
|
||||||
|
setEtabStatus('error');
|
||||||
|
setEtabMsg('Format non supporté. Utilisez .xlsx, .xls ou .csv');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = await file.text();
|
||||||
|
const lines = text.split('\n').filter(l => l.trim());
|
||||||
|
if (lines.length < 2) {
|
||||||
|
setEtabStatus('error');
|
||||||
|
setEtabMsg('Fichier vide ou format invalide');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Parsing CSV simple (séparateur ; ou ,)
|
||||||
|
const sep = lines[0].includes(';') ? ';' : ',';
|
||||||
|
const headers = lines[0].split(sep).map(h => h.trim().toLowerCase().replace(/"/g, ''));
|
||||||
|
const codeIdx = headers.findIndex(h => h.includes('code'));
|
||||||
|
const nomIdx = headers.findIndex(h => h.includes('nom') || h.includes('libelle') || h.includes('établissement'));
|
||||||
|
const groupeIdx = headers.findIndex(h => h.includes('groupe') || h.includes('association'));
|
||||||
|
const villeIdx = headers.findIndex(h => h.includes('ville') || h.includes('commune'));
|
||||||
|
|
||||||
|
if (codeIdx === -1 || nomIdx === -1) {
|
||||||
|
setEtabStatus('error');
|
||||||
|
setEtabMsg('Colonnes "code" et "nom" requises dans le fichier');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const etabs: Etablissement[] = lines.slice(1).map(line => {
|
||||||
|
const cols = line.split(sep).map(c => c.trim().replace(/"/g, ''));
|
||||||
|
return {
|
||||||
|
code: cols[codeIdx] || '',
|
||||||
|
nom: cols[nomIdx] || '',
|
||||||
|
groupe: groupeIdx >= 0 ? cols[groupeIdx] : undefined,
|
||||||
|
ville: villeIdx >= 0 ? cols[villeIdx] : undefined,
|
||||||
|
actif: true,
|
||||||
|
};
|
||||||
|
}).filter(e => e.code && e.nom);
|
||||||
|
|
||||||
|
saveEtablissements(etabs);
|
||||||
|
setEtabStatus('success');
|
||||||
|
setEtabMsg(`${etabs.length} établissements importés`);
|
||||||
|
toast.success('Établissements importés', { description: `${etabs.length} établissements chargés` });
|
||||||
|
} catch {
|
||||||
|
setEtabStatus('error');
|
||||||
|
setEtabMsg('Erreur lors de la lecture du fichier');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Utilisateurs ────────────────────────────────────────────────────────────
|
||||||
|
const handleUserFile = async (file: File) => {
|
||||||
|
try {
|
||||||
|
if (!file.name.match(/\.(xlsx|xls|csv)$/i)) {
|
||||||
|
setUserStatus('error');
|
||||||
|
setUserMsg('Format non supporté. Utilisez .xlsx, .xls ou .csv');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = await file.text();
|
||||||
|
const lines = text.split('\n').filter(l => l.trim());
|
||||||
|
if (lines.length < 2) {
|
||||||
|
setUserStatus('error');
|
||||||
|
setUserMsg('Fichier vide ou format invalide');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sep = lines[0].includes(';') ? ';' : ',';
|
||||||
|
const headers = lines[0].split(sep).map(h => h.trim().toLowerCase().replace(/"/g, ''));
|
||||||
|
const nomIdx = headers.findIndex(h => h.includes('nom'));
|
||||||
|
const prenomIdx = headers.findIndex(h => h.includes('prenom') || h.includes('prénom'));
|
||||||
|
const emailIdx = headers.findIndex(h => h.includes('email') || h.includes('mail'));
|
||||||
|
const roleIdx = headers.findIndex(h => h.includes('role') || h.includes('profil'));
|
||||||
|
const etabIdx = headers.findIndex(h => h.includes('etablissement') || h.includes('code'));
|
||||||
|
|
||||||
|
if (nomIdx === -1 || emailIdx === -1) {
|
||||||
|
setUserStatus('error');
|
||||||
|
setUserMsg('Colonnes "nom" et "email" requises dans le fichier');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const users: Utilisateur[] = lines.slice(1).map((line, i) => {
|
||||||
|
const cols = line.split(sep).map(c => c.trim().replace(/"/g, ''));
|
||||||
|
const roleRaw = roleIdx >= 0 ? cols[roleIdx].toLowerCase() : '';
|
||||||
|
const role: Utilisateur['role'] = roleRaw.includes('admin') ? 'admin'
|
||||||
|
: roleRaw.includes('lecture') ? 'lecture'
|
||||||
|
: 'standard';
|
||||||
|
const etabsRaw = etabIdx >= 0 ? cols[etabIdx] : '';
|
||||||
|
const etablissements = etabsRaw ? etabsRaw.split('|').map(e => e.trim()).filter(Boolean) : [];
|
||||||
|
return {
|
||||||
|
id: `user_${Date.now()}_${i}`,
|
||||||
|
nom: cols[nomIdx] || '',
|
||||||
|
prenom: prenomIdx >= 0 ? cols[prenomIdx] : '',
|
||||||
|
email: cols[emailIdx] || '',
|
||||||
|
role,
|
||||||
|
etablissements,
|
||||||
|
actif: true,
|
||||||
|
};
|
||||||
|
}).filter(u => u.nom && u.email);
|
||||||
|
|
||||||
|
saveUtilisateurs(users);
|
||||||
|
setUserStatus('success');
|
||||||
|
setUserMsg(`${users.length} utilisateurs importés`);
|
||||||
|
toast.success('Utilisateurs importés', { description: `${users.length} utilisateurs chargés` });
|
||||||
|
} catch {
|
||||||
|
setUserStatus('error');
|
||||||
|
setUserMsg('Erreur lors de la lecture du fichier');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs: { id: ImportTab; label: string; icon: React.ElementType; color: string }[] = [
|
||||||
|
{ id: 'inventaire', label: 'Inventaire postes', icon: Monitor, color: 'text-blue-600' },
|
||||||
|
{ id: 'etablissements', label: 'Établissements', icon: Building2, color: 'text-emerald-600' },
|
||||||
|
{ id: 'utilisateurs', label: 'Utilisateurs', icon: Users, color: 'text-violet-600' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}>
|
||||||
|
<div className="bg-background rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col border border-border">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-9 h-9 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||||
|
<Upload className="w-4.5 h-4.5 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="font-bold text-base text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||||
|
Imports & Données
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-muted-foreground">Importer des données depuis des fichiers CSV ou Excel</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-8 h-8 rounded-lg flex items-center justify-center text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex border-b border-border px-6">
|
||||||
|
{tabs.map(t => {
|
||||||
|
const Icon = t.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
onClick={() => setTab(t.id)}
|
||||||
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors -mb-px ${
|
||||||
|
tab === t.id
|
||||||
|
? `border-blue-600 ${t.color}`
|
||||||
|
: 'border-transparent text-muted-foreground hover:text-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||||
|
{tab === 'inventaire' && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-start gap-3 p-4 bg-blue-50 border border-blue-200 rounded-xl">
|
||||||
|
<Info className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-blue-800">
|
||||||
|
<p className="font-semibold mb-1">Inventaire des postes (ISI-APP)</p>
|
||||||
|
<p className="text-xs text-blue-700">
|
||||||
|
Importez l'export de l'inventaire depuis ISI-APP au format Excel (.xlsx) ou CSV (.csv).
|
||||||
|
Les colonnes attendues sont : <strong>Code établissement, Nom, Type (fixe/portable), Date achat, Modèle</strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DropZone
|
||||||
|
label="Glissez votre fichier d'inventaire ici"
|
||||||
|
description="Formats acceptés : .xlsx, .xls, .csv — Export ISI-APP"
|
||||||
|
accept=".xlsx,.xls,.csv"
|
||||||
|
onFile={handleInventaireFile}
|
||||||
|
status={inventaireStatus}
|
||||||
|
statusMsg={inventaireMsg}
|
||||||
|
/>
|
||||||
|
{inventaireStatus !== 'idle' && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setInventaireStatus('idle'); setInventaireMsg(''); }}
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground underline"
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'etablissements' && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-start gap-3 p-4 bg-emerald-50 border border-emerald-200 rounded-xl">
|
||||||
|
<Info className="w-4 h-4 text-emerald-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-emerald-800">
|
||||||
|
<p className="font-semibold mb-1">Liste des établissements</p>
|
||||||
|
<p className="text-xs text-emerald-700">
|
||||||
|
Importez la liste des établissements au format CSV. Colonnes attendues :
|
||||||
|
<strong> code, nom</strong> (obligatoires), <em>groupe, ville</em> (optionnels).
|
||||||
|
Séparateur : <code>;</code> ou <code>,</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DropZone
|
||||||
|
label="Glissez votre fichier d'établissements ici"
|
||||||
|
description="Formats acceptés : .xlsx, .xls, .csv"
|
||||||
|
accept=".xlsx,.xls,.csv"
|
||||||
|
onFile={handleEtabFile}
|
||||||
|
status={etabStatus}
|
||||||
|
statusMsg={etabMsg}
|
||||||
|
/>
|
||||||
|
{etabStatus !== 'idle' && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setEtabStatus('idle'); setEtabMsg(''); }}
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground underline"
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'utilisateurs' && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-start gap-3 p-4 bg-violet-50 border border-violet-200 rounded-xl">
|
||||||
|
<Info className="w-4 h-4 text-violet-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-violet-800">
|
||||||
|
<p className="font-semibold mb-1">Liste des utilisateurs</p>
|
||||||
|
<p className="text-xs text-violet-700">
|
||||||
|
Importez la liste des utilisateurs au format CSV. Colonnes attendues :
|
||||||
|
<strong> nom, email</strong> (obligatoires), <em>prenom, role (admin/standard/lecture), etablissements</em> (optionnels).
|
||||||
|
Pour plusieurs établissements, séparez les codes par <code>|</code> dans la colonne <em>etablissements</em>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DropZone
|
||||||
|
label="Glissez votre fichier d'utilisateurs ici"
|
||||||
|
description="Formats acceptés : .xlsx, .xls, .csv"
|
||||||
|
accept=".xlsx,.xls,.csv"
|
||||||
|
onFile={handleUserFile}
|
||||||
|
status={userStatus}
|
||||||
|
statusMsg={userMsg}
|
||||||
|
/>
|
||||||
|
{userStatus !== 'idle' && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setUserStatus('idle'); setUserMsg(''); }}
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground underline"
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-6 py-4 border-t border-border flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-5 py-2 text-sm rounded-lg bg-muted hover:bg-muted/80 text-foreground transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Fermer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user