| 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; | |