Checkpoint: Ajout d'un éditeur de prompt de processus d'import dans l'onglet "Inventaire postes" de la modale Imports & Données. Le prompt est affiché dans un bloc pliable/dépliable, modifiable via un textarea monospace (16 lignes), sauvegardable en localStorage (clé budgetsi_import_prompt), réinitialisable aux valeurs par défaut. Le prompt par défaut documente le format attendu, les colonnes obligatoires/optionnelles, les règles de traitement et le comportement attendu. Boutons Modifier / Enregistrer / Annuler / Réinitialiser par défaut.

This commit is contained in:
Manus
2026-06-04 14:38:48 +00:00
parent ade7471956
commit e124a970a7

View File

@@ -1,7 +1,7 @@
// ImportModal.tsx — Fenêtre d'import de données (Inventaire, Établissements, Utilisateurs) // ImportModal.tsx — Fenêtre d'import de données (Inventaire, Établissements, Utilisateurs)
// Design: Corporate Modernism — Itinova Budget SI // Design: Corporate Modernism — Itinova Budget SI
import { useState, useRef } from 'react'; import { useState, useRef, useEffect } from 'react';
import { import {
Upload, Upload,
X, X,
@@ -12,6 +12,12 @@ import {
AlertCircle, AlertCircle,
FileSpreadsheet, FileSpreadsheet,
Info, Info,
Pencil,
Save,
RotateCcw,
ChevronDown,
ChevronUp,
BrainCircuit,
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -39,6 +45,50 @@ export interface Utilisateur {
const ETAB_KEY = 'budgetsi_etablissements'; const ETAB_KEY = 'budgetsi_etablissements';
const USER_KEY = 'budgetsi_utilisateurs'; const USER_KEY = 'budgetsi_utilisateurs';
const IMPORT_PROMPT_KEY = 'budgetsi_import_prompt';
const DEFAULT_IMPORT_PROMPT = `# Processus d'import de l'inventaire ISI-APP
## Format du fichier source
- Export CSV depuis ISI-APP (séparateur : point-virgule ou virgule)
- Encodage : UTF-8 ou Windows-1252
- Première ligne : en-têtes de colonnes
## Colonnes obligatoires
- **Code établissement** : identifiant unique (ex : 1063CRD, 2013ANG…)
Noms acceptés : code, code_etab, code_etablissement, code etablissement
- **Nom établissement** : libellé complet
Noms acceptés : nom, nom_etab, nom_etablissement, etablissement, libelle_etab
## Colonnes optionnelles
- type : fixe / portable / bureau / laptop
- date_achat : date d'achat du poste (JJ/MM/AAAA)
- modele : modèle du matériel
- fabricant : marque (Lenovo, Dell, HP…)
- groupe : groupement ou association de rattachement
- ville : commune de l'établissement
## Règles de traitement
1. Les établissements uniques sont extraits et enregistrés dans la liste des établissements
2. Les établissements déjà présents ne sont pas écrasés (fusion intelligente)
3. Les lignes sans code ou sans nom sont ignorées
4. Les doublons de postes sont conservés (un poste = une ligne)
## Comportement attendu
- Créer ou mettre à jour la liste des établissements (code + nom)
- Enregistrer les métadonnées du fichier importé (nom, date, nb lignes)
- Afficher un résumé : nb lignes traitées, nb établissements détectés, nb nouveaux`;
export function loadImportPrompt(): string {
try {
const raw = localStorage.getItem(IMPORT_PROMPT_KEY);
return raw ? JSON.parse(raw) : DEFAULT_IMPORT_PROMPT;
} catch { return DEFAULT_IMPORT_PROMPT; }
}
export function saveImportPrompt(prompt: string) {
localStorage.setItem(IMPORT_PROMPT_KEY, JSON.stringify(prompt));
}
export function loadEtablissements(): Etablissement[] { export function loadEtablissements(): Etablissement[] {
try { try {
@@ -152,6 +202,52 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
const [userStatus, setUserStatus] = useState<'idle' | 'success' | 'error'>('idle'); const [userStatus, setUserStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [userMsg, setUserMsg] = useState(''); const [userMsg, setUserMsg] = useState('');
// ── Prompt de processus d'import ────────────────────────────────────────────
const [importPrompt, setImportPrompt] = useState<string>(() => loadImportPrompt());
const [promptDraft, setPromptDraft] = useState<string>('');
const [promptEditing, setPromptEditing] = useState(false);
const [promptExpanded, setPromptExpanded] = useState(false);
const [promptSaved, setPromptSaved] = useState(false);
// Sync prompt depuis localStorage à l'ouverture
useEffect(() => {
if (open) {
const p = loadImportPrompt();
setImportPrompt(p);
setPromptDraft(p);
setPromptEditing(false);
}
}, [open]);
const handlePromptEdit = () => {
setPromptDraft(importPrompt);
setPromptEditing(true);
setPromptExpanded(true);
};
const handlePromptSave = () => {
saveImportPrompt(promptDraft);
setImportPrompt(promptDraft);
setPromptEditing(false);
setPromptSaved(true);
toast.success('Prompt enregistré', { description: 'La configuration du processus d\'import a été sauvegardée.' });
setTimeout(() => setPromptSaved(false), 2500);
};
const handlePromptCancel = () => {
setPromptDraft(importPrompt);
setPromptEditing(false);
};
const handlePromptReset = () => {
if (!confirm('Réinitialiser le prompt aux valeurs par défaut ?')) return;
setPromptDraft(DEFAULT_IMPORT_PROMPT);
saveImportPrompt(DEFAULT_IMPORT_PROMPT);
setImportPrompt(DEFAULT_IMPORT_PROMPT);
setPromptEditing(false);
toast.info('Prompt réinitialisé');
};
if (!open) return null; if (!open) return null;
// ── Inventaire ISI-APP ────────────────────────────────────────────────────── // ── Inventaire ISI-APP ──────────────────────────────────────────────────────
@@ -457,6 +553,93 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
<div className="flex-1 overflow-y-auto p-6 space-y-4"> <div className="flex-1 overflow-y-auto p-6 space-y-4">
{tab === 'inventaire' && ( {tab === 'inventaire' && (
<> <>
{/* ── Prompt de processus d'import ── */}
<div className="border border-border rounded-xl overflow-hidden">
{/* En-tête du prompt */}
<div
className="flex items-center justify-between px-4 py-3 bg-muted/40 cursor-pointer select-none"
onClick={() => !promptEditing && setPromptExpanded(e => !e)}
>
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-md bg-blue-100 flex items-center justify-center flex-shrink-0">
<BrainCircuit className="w-3.5 h-3.5 text-blue-600" />
</div>
<div>
<p className="text-sm font-semibold text-foreground">Prompt de processus d'import</p>
<p className="text-xs text-muted-foreground">Instructions de traitement du fichier ISI-APP</p>
</div>
</div>
<div className="flex items-center gap-2">
{!promptEditing && (
<button
onClick={e => { e.stopPropagation(); handlePromptEdit(); }}
className="flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-lg bg-blue-100 text-blue-700 hover:bg-blue-200 transition-colors font-medium"
title="Modifier le prompt"
>
<Pencil className="w-3 h-3" />
Modifier
</button>
)}
{!promptEditing && (
<span className="text-muted-foreground">
{promptExpanded
? <ChevronUp className="w-4 h-4" />
: <ChevronDown className="w-4 h-4" />}
</span>
)}
</div>
</div>
{/* Corps du prompt */}
{(promptExpanded || promptEditing) && (
<div className="border-t border-border">
{promptEditing ? (
<div className="p-4 space-y-3">
<textarea
value={promptDraft}
onChange={e => setPromptDraft(e.target.value)}
rows={16}
className="w-full px-3 py-2.5 text-xs font-mono border border-border rounded-lg bg-muted/30 focus:outline-none focus:ring-2 focus:ring-blue-300 focus:border-blue-400 resize-y leading-relaxed"
placeholder="Décrivez ici le processus d'import, les colonnes attendues, les règles de traitement"
spellCheck={false}
/>
<div className="flex items-center justify-between">
<button
onClick={handlePromptReset}
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<RotateCcw className="w-3 h-3" />
Réinitialiser par défaut
</button>
<div className="flex items-center gap-2">
<button
onClick={handlePromptCancel}
className="px-3 py-1.5 text-xs rounded-lg bg-muted hover:bg-muted/70 text-muted-foreground transition-colors"
>
Annuler
</button>
<button
onClick={handlePromptSave}
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg font-medium transition-all ${
promptSaved
? 'bg-emerald-600 text-white'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
<Save className="w-3 h-3" />
{promptSaved ? 'Enregistré !' : 'Enregistrer'}
</button>
</div>
</div>
</div>
) : (
<pre className="px-4 py-3 text-xs font-mono text-muted-foreground whitespace-pre-wrap leading-relaxed max-h-48 overflow-y-auto bg-muted/20">{importPrompt}</pre>
)}
</div>
)}
</div>
{/* ── Info colonnes ── */}
<div className="flex items-start gap-3 p-4 bg-blue-50 border border-blue-200 rounded-xl"> <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" /> <Info className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-800"> <div className="text-sm text-blue-800">
@@ -471,6 +654,8 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
</p> </p>
</div> </div>
</div> </div>
{/* ── Zone de dépôt ── */}
<DropZone <DropZone
label="Glissez votre fichier d'inventaire ici" label="Glissez votre fichier d'inventaire ici"
description="Formats acceptés : .xlsx, .xls, .csv — Export ISI-APP" description="Formats acceptés : .xlsx, .xls, .csv — Export ISI-APP"