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:
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user