AUDITPRO / components /ChecklistManager.tsx
MMOON's picture
Upload 5 files
8c03bde verified
import React, { useState } from 'react';
import { Checklist, ChecklistSection, ChecklistItem, ItemType } from '../types';
import { saveToStore } from '../db';
import {
Plus,
Trash2,
Save,
FileSpreadsheet,
Upload
} from 'lucide-react';
import * as XLSX from 'xlsx';
const generateId = (prefix: string) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 5)}`;
interface ChecklistManagerProps {
onSave: () => void;
}
const ChecklistManager: React.FC<ChecklistManagerProps> = ({ onSave }) => {
const [name, setName] = useState('');
const [sections, setSections] = useState<ChecklistSection[]>([
{ id: generateId('sec'), title: 'Section Principale', items: [] }
]);
const addSection = () => {
setSections([...sections, { id: generateId('sec'), title: 'Nouvelle Section', items: [] }]);
};
const removeSection = (id: string) => {
setSections(sections.filter(s => s.id !== id));
};
const addItem = (sectionId: string) => {
setSections(sections.map(s => s.id === sectionId ? {
...s,
items: [...s.items, {
id: generateId('item'),
label: 'Nouveau point',
type: ItemType.PassFail,
required: true
}]
} : s));
};
const updateItem = (sectionId: string, itemId: string, updates: any) => {
setSections(sections.map(s => s.id === sectionId ? {
...s,
items: s.items.map(i => i.id === itemId ? { ...i, ...updates } : i)
} : s));
};
const removeItem = (sectionId: string, itemId: string) => {
setSections(sections.map(s => s.id === sectionId ? {
...s,
items: s.items.filter(i => i.id !== itemId)
} : s));
};
const handleSave = async () => {
if (!name.trim()) return alert('Nom du modèle requis');
const now = Date.now();
const newChecklist: Checklist = {
id: generateId('cl'),
name,
version: '1.0.0',
sections,
createdAt: now,
lastModified: now
};
await saveToStore('checklists', newChecklist);
onSave();
};
const downloadExcelTemplate = () => {
const data = [
["Section", "Point de controle", "Type (PASS_FAIL, TEXT, NUMBER, DROPDOWN)", "Requis (OUI/NON)", "Options (si DROPDOWN, separer par ;)"],
["Zone Reception", "Etat des palettes", "PASS_FAIL", "OUI", ""],
["Zone Reception", "Temperature produit", "NUMBER", "OUI", ""],
["Hygiene", "Proprete des mains", "PASS_FAIL", "OUI", ""],
["Stockage", "Type de stockage", "DROPDOWN", "NON", "Frigo;Ambiant;Sec"]
];
const ws = XLSX.utils.aoa_to_sheet(data);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Modele");
XLSX.writeFile(wb, "modele_audit.xlsx");
};
const handleImportExcel = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const bstr = event.target?.result;
const wb = XLSX.read(bstr, { type: 'binary' });
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
const data: any[] = XLSX.utils.sheet_to_json(ws);
const sectionsMap = new Map<string, ChecklistItem[]>();
data.forEach((row: any) => {
const sectionName = row["Section"] || "Sans Titre";
const itemLabel = row["Point de controle"] || "Point sans nom";
let typeStr = (row["Type (PASS_FAIL, TEXT, NUMBER, DROPDOWN)"] || "PASS_FAIL").toUpperCase();
const required = (row["Requis (OUI/NON)"] || "OUI").toUpperCase() === "OUI";
const optionsStr = row["Options (si DROPDOWN, separer par ;)"] || "";
const type = Object.values(ItemType).includes(typeStr as ItemType)
? (typeStr as ItemType)
: ItemType.PassFail;
const newItem: ChecklistItem = {
id: generateId('item'),
label: itemLabel,
type,
required,
options: optionsStr ? optionsStr.split(';').map((s: string) => s.trim()) : undefined
};
if (!sectionsMap.has(sectionName)) {
sectionsMap.set(sectionName, []);
}
sectionsMap.get(sectionName)?.push(newItem);
});
const newSections: ChecklistSection[] = Array.from(sectionsMap.entries()).map(([title, items]) => ({
id: generateId('sec'),
title,
items
}));
if (newSections.length > 0) {
setName(file.name.replace(/\.[^/.]+$/, ""));
setSections(newSections);
}
};
reader.readAsBinaryString(file);
};
return (
<div className="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden animate-in fade-in zoom-in-95 duration-300">
<div className="p-6 border-b border-slate-100 flex flex-col md:flex-row gap-4 items-center justify-between bg-slate-50/50">
<h2 className="text-xl font-extrabold text-slate-800">Éditeur de Modèle</h2>
<div className="flex flex-wrap gap-2 justify-center">
<button
onClick={downloadExcelTemplate}
className="flex items-center gap-2 px-5 py-2.5 bg-emerald-50 border border-emerald-100 rounded-2xl text-emerald-700 text-sm font-bold hover:bg-emerald-100 transition-all"
>
<FileSpreadsheet size={18} /> Modèle Excel
</button>
<label className="flex items-center gap-2 px-5 py-2.5 bg-white border border-slate-200 rounded-2xl text-slate-600 text-sm font-bold cursor-pointer hover:bg-slate-50 transition-all">
<Upload size={18} /> Importer Excel
<input type="file" accept=".xlsx, .xls, .csv" onChange={handleImportExcel} className="hidden" />
</label>
<button
onClick={handleSave}
className="flex items-center gap-2 bg-indigo-600 text-white px-8 py-2.5 rounded-2xl hover:bg-indigo-700 shadow-lg shadow-indigo-100 font-extrabold transition-all"
>
<Save size={18} /> Enregistrer
</button>
</div>
</div>
<div className="p-6 sm:p-8 space-y-8">
<div className="space-y-2">
<label className="text-sm font-extrabold text-slate-400 uppercase tracking-widest">Nom de la Checklist</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Ex: Audit Qualité Atelier"
className="w-full text-2xl font-black border-none focus:ring-0 p-0 placeholder-slate-200 text-slate-800 transition-all"
/>
<div className="h-0.5 bg-slate-100 w-full rounded-full overflow-hidden">
<div className={`h-full bg-indigo-500 transition-all duration-500 ${name ? 'w-full' : 'w-0'}`}></div>
</div>
</div>
<div className="space-y-6">
{sections.map((section, sIdx) => (
<div key={section.id} className="bg-slate-50 rounded-3xl p-6 border border-slate-100 space-y-4 shadow-sm">
<div className="flex items-center justify-between">
<input
type="text"
value={section.title}
onChange={(e) => {
const newSections = [...sections];
newSections[sIdx].title = e.target.value;
setSections(newSections);
}}
className="bg-transparent text-xl font-black text-indigo-900 border-none focus:ring-0 p-0"
/>
<button
onClick={() => removeSection(section.id)}
className="p-2 text-slate-300 hover:text-rose-500 hover:bg-rose-50 rounded-xl transition-all"
>
<Trash2 size={20} />
</button>
</div>
<div className="space-y-3">
{section.items.map((item) => (
<div key={item.id} className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex flex-col sm:flex-row gap-4 items-start sm:items-center group">
<div className="flex-1 space-y-2 w-full">
<input
type="text"
value={item.label}
onChange={(e) => updateItem(section.id, item.id, { label: e.target.value })}
className="w-full font-bold text-slate-700 border-none focus:ring-0 p-0 text-base"
/>
<div className="flex flex-wrap gap-2">
<select
value={item.type}
onChange={(e) => updateItem(section.id, item.id, { type: e.target.value as ItemType })}
className="text-xs bg-slate-100 rounded-lg px-3 py-1.5 border-none font-bold text-slate-600 focus:ring-2 focus:ring-indigo-100"
>
<option value={ItemType.PassFail}>OK/KO</option>
<option value={ItemType.Text}>Texte</option>
<option value={ItemType.Number}>Nombre</option>
<option value={ItemType.Dropdown}>Liste</option>
</select>
<button
onClick={() => updateItem(section.id, item.id, { required: !item.required })}
className={`text-[10px] px-3 py-1.5 rounded-lg font-black uppercase tracking-wider transition-all ${item.required ? 'bg-indigo-600 text-white shadow-md shadow-indigo-100' : 'bg-slate-100 text-slate-400'}`}
>
{item.required ? 'Requis' : 'Optionnel'}
</button>
</div>
</div>
<button
onClick={() => removeItem(section.id, item.id)}
className="text-slate-200 hover:text-rose-500 p-2 sm:opacity-0 group-hover:opacity-100 transition-all"
>
<Trash2 size={18} />
</button>
</div>
))}
<button
onClick={() => addItem(section.id)}
className="w-full py-4 border-2 border-dashed border-slate-200 rounded-2xl text-slate-300 hover:text-indigo-600 hover:border-indigo-200 transition-all flex items-center justify-center gap-2 text-sm font-bold"
>
<Plus size={18} /> Nouveau point
</button>
</div>
</div>
))}
<button
onClick={addSection}
className="w-full py-5 bg-indigo-50 text-indigo-600 rounded-3xl flex items-center justify-center gap-2 font-black hover:bg-indigo-100 transition-all"
>
<Plus size={24} /> Nouvelle section
</button>
</div>
</div>
</div>
);
};
export default ChecklistManager;