| import React, { useState, useEffect } from 'react'; |
| import { Audit, Checklist, ItemType, AuditResponse } from '../types'; |
| import { Camera, Check, X, ChevronRight, ChevronLeft, Save } from 'lucide-react'; |
|
|
| interface AuditFormProps { |
| audit: Audit; |
| checklist: Checklist; |
| onFinish: (audit: Audit) => void; |
| onCancel: () => void; |
| } |
|
|
| const AuditForm: React.FC<AuditFormProps> = ({ audit, checklist, onFinish, onCancel }) => { |
| const [currentSectionIndex, setCurrentSectionIndex] = useState(0); |
| const [responses, setResponses] = useState<AuditResponse[]>(audit.responses); |
| const currentSection = checklist.sections[currentSectionIndex]; |
|
|
| const updateResponse = (itemId: string, itemLabel: string, value: any, comment?: string, photos?: string[]) => { |
| setResponses(prev => { |
| const existing = prev.find(r => r.itemId === itemId); |
| if (existing) { |
| return prev.map(r => r.itemId === itemId ? { |
| ...r, |
| value: value !== undefined ? value : r.value, |
| comment: comment !== undefined ? comment : r.comment, |
| photos: photos !== undefined ? photos : r.photos |
| } : r); |
| } |
| return [...prev, { |
| itemId, |
| itemLabel, |
| value: value ?? null, |
| comment: comment ?? '', |
| photos: photos ?? [] |
| }]; |
| }); |
| }; |
|
|
| const getResponse = (itemId: string) => { |
| return responses.find(r => r.itemId === itemId) || { itemId, itemLabel: '', value: null, comment: '', photos: [] }; |
| }; |
|
|
| const handleFinish = () => { |
| const finalAudit: Audit = { |
| ...audit, |
| responses, |
| status: 'COMPLETED', |
| completedAt: Date.now() |
| }; |
| onFinish(finalAudit); |
| }; |
|
|
| const capturePhoto = (itemId: string, label: string) => { |
| const input = document.createElement('input'); |
| input.type = 'file'; |
| input.accept = 'image/*'; |
| input.capture = 'environment'; |
| input.onchange = (e: any) => { |
| const file = e.target.files[0]; |
| if (file) { |
| const reader = new FileReader(); |
| reader.onload = () => { |
| const currentPhotos = getResponse(itemId).photos; |
| updateResponse(itemId, label, undefined, undefined, [...currentPhotos, reader.result as string]); |
| }; |
| reader.readAsDataURL(file); |
| } |
| }; |
| input.click(); |
| }; |
|
|
| const removePhoto = (itemId: string, label: string, photoIndex: number) => { |
| const currentPhotos = getResponse(itemId).photos; |
| updateResponse(itemId, label, undefined, undefined, currentPhotos.filter((_, i) => i !== photoIndex)); |
| }; |
|
|
| const progress = Math.round((responses.length / checklist.sections.reduce((acc, s) => acc + s.items.length, 0)) * 100); |
|
|
| return ( |
| <div className="bg-white rounded-3xl shadow-xl overflow-hidden border border-slate-200 animate-in fade-in slide-in-from-bottom-4 duration-300"> |
| <div className="bg-indigo-600 p-6 sm:p-8 text-white relative overflow-hidden"> |
| <div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -translate-y-16 translate-x-16"></div> |
| <div className="relative z-10 flex justify-between items-start mb-6"> |
| <div> |
| <h2 className="text-2xl font-black">{checklist.name}</h2> |
| <p className="text-indigo-100/70 text-sm font-bold mt-1">Inspecteur: {audit.inspectorName || 'Inconnu'}</p> |
| </div> |
| <button |
| onClick={onCancel} |
| className="bg-white/10 hover:bg-white/20 p-2 rounded-xl transition-all" |
| > |
| <X size={20} /> |
| </button> |
| </div> |
| |
| <div className="relative z-10 space-y-2"> |
| <div className="flex justify-between text-[10px] font-black uppercase tracking-widest opacity-80"> |
| <span>Progression</span> |
| <span>{progress}%</span> |
| </div> |
| <div className="h-3 bg-indigo-900/30 rounded-full overflow-hidden p-0.5"> |
| <div className="h-full bg-white rounded-full transition-all duration-700 ease-out" style={{ width: `${progress}%` }}></div> |
| </div> |
| </div> |
| </div> |
| |
| <div className="flex border-b border-slate-100 bg-slate-50/50 overflow-x-auto scrollbar-hide"> |
| {checklist.sections.map((section, idx) => ( |
| <button |
| key={section.id} |
| onClick={() => setCurrentSectionIndex(idx)} |
| className={`px-6 py-4 whitespace-nowrap text-xs font-black uppercase tracking-wider transition-all border-b-2 ${ |
| idx === currentSectionIndex |
| ? 'border-indigo-600 text-indigo-600 bg-white' |
| : 'border-transparent text-slate-400 hover:text-slate-600' |
| }`} |
| > |
| {section.title} |
| </button> |
| ))} |
| </div> |
| |
| <div className="p-6 sm:p-8 space-y-10 min-h-[400px]"> |
| <h3 className="text-xl font-black text-slate-800 border-l-4 border-indigo-600 pl-4"> |
| {currentSection.title} |
| </h3> |
| |
| {currentSection.items.map((item) => { |
| const resp = getResponse(item.id); |
| return ( |
| <div key={item.id} className="space-y-4 pb-8 border-b border-slate-100 last:border-0 group"> |
| <div className="flex justify-between items-start gap-4"> |
| <div> |
| <h4 className="font-bold text-slate-800 text-lg group-hover:text-indigo-600 transition-colors">{item.label}</h4> |
| {item.description && <p className="text-sm text-slate-400 font-medium mt-1 leading-relaxed">{item.description}</p>} |
| </div> |
| {item.required && <span className="text-[10px] bg-rose-50 text-rose-500 px-3 py-1 rounded-full font-black uppercase tracking-widest">Requis</span>} |
| </div> |
| |
| <div className="flex flex-wrap gap-2"> |
| {item.type === ItemType.PassFail && ( |
| <div className="flex gap-2 w-full sm:w-auto"> |
| <button |
| onClick={() => updateResponse(item.id, item.label, 'PASS')} |
| className={`flex-1 sm:flex-none flex items-center justify-center gap-2 px-8 py-3 rounded-2xl border-2 font-black transition-all ${ |
| resp.value === 'PASS' |
| ? 'bg-emerald-500 border-emerald-500 text-white shadow-lg shadow-emerald-100' |
| : 'bg-white border-slate-100 text-slate-400 hover:border-emerald-200 hover:text-emerald-600' |
| }`} |
| > |
| <Check size={20} /> CONFORME |
| </button> |
| <button |
| onClick={() => updateResponse(item.id, item.label, 'FAIL')} |
| className={`flex-1 sm:flex-none flex items-center justify-center gap-2 px-8 py-3 rounded-2xl border-2 font-black transition-all ${ |
| resp.value === 'FAIL' |
| ? 'bg-rose-500 border-rose-500 text-white shadow-lg shadow-rose-100' |
| : 'bg-white border-slate-100 text-slate-400 hover:border-rose-200 hover:text-rose-600' |
| }`} |
| > |
| <X size={20} /> NON-CONFORME |
| </button> |
| </div> |
| )} |
| |
| {item.type === ItemType.Text && ( |
| <textarea |
| value={resp.value || ''} |
| onChange={(e) => updateResponse(item.id, item.label, e.target.value)} |
| placeholder="Notes détaillées..." |
| className="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 text-sm font-bold text-slate-700 focus:ring-4 focus:ring-indigo-50 focus:border-indigo-200 focus:outline-none transition-all" |
| rows={3} |
| /> |
| )} |
| |
| {item.type === ItemType.Number && ( |
| <input |
| type="number" |
| value={resp.value || ''} |
| onChange={(e) => updateResponse(item.id, item.label, e.target.value)} |
| placeholder="0.00" |
| className="w-full sm:w-48 bg-slate-50 border border-slate-100 rounded-2xl p-4 text-sm font-bold text-slate-700 focus:ring-4 focus:ring-indigo-50 focus:border-indigo-200 focus:outline-none" |
| /> |
| )} |
| |
| {item.type === ItemType.Dropdown && ( |
| <select |
| value={resp.value || ''} |
| onChange={(e) => updateResponse(item.id, item.label, e.target.value)} |
| className="w-full sm:w-64 bg-slate-50 border border-slate-100 rounded-2xl p-4 text-sm font-bold text-slate-700 focus:ring-4 focus:ring-indigo-50 focus:border-indigo-200 focus:outline-none cursor-pointer" |
| > |
| <option value="">Sélectionner une option</option> |
| {item.options?.map(opt => <option key={opt} value={opt}>{opt}</option>)} |
| </select> |
| )} |
| </div> |
| |
| <div className="flex flex-col sm:flex-row gap-4"> |
| <div className="flex-1"> |
| <input |
| type="text" |
| value={resp.comment || ''} |
| onChange={(e) => updateResponse(item.id, item.label, undefined, e.target.value)} |
| placeholder="Observation facultative..." |
| className="w-full text-xs font-bold text-slate-500 bg-white border-b border-slate-100 py-2 focus:border-indigo-400 focus:outline-none transition-all" |
| /> |
| </div> |
| <div className="flex gap-2"> |
| <button |
| onClick={() => capturePhoto(item.id, item.label)} |
| className="flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-600 hover:bg-indigo-50 hover:text-indigo-600 rounded-xl text-xs font-black transition-all" |
| > |
| <Camera size={14} /> PHOTOS ({resp.photos.length}) |
| </button> |
| </div> |
| </div> |
| |
| {resp.photos.length > 0 && ( |
| <div className="flex flex-wrap gap-3 mt-4"> |
| {resp.photos.map((p, idx) => ( |
| <div key={idx} className="relative w-20 h-20 rounded-2xl overflow-hidden border border-slate-200 group/photo shadow-sm"> |
| <img src={p} className="w-full h-full object-cover transition-transform group-hover/photo:scale-110" /> |
| <button |
| onClick={() => removePhoto(item.id, item.label, idx)} |
| className="absolute top-1 right-1 bg-rose-500 text-white p-1 rounded-lg opacity-0 group-hover/photo:opacity-100 transition-all hover:scale-110" |
| > |
| <X size={12} /> |
| </button> |
| </div> |
| ))} |
| </div> |
| )} |
| </div> |
| ); |
| })} |
| </div> |
| |
| <div className="bg-slate-50 p-6 sm:p-8 flex justify-between items-center border-t border-slate-100"> |
| <button |
| onClick={() => setCurrentSectionIndex(Math.max(0, currentSectionIndex - 1))} |
| disabled={currentSectionIndex === 0} |
| className="flex items-center gap-2 px-6 py-3 text-slate-400 hover:text-indigo-600 disabled:opacity-20 font-black transition-all" |
| > |
| <ChevronLeft size={24} /> PRÉCÉDENT |
| </button> |
| |
| {currentSectionIndex < checklist.sections.length - 1 ? ( |
| <button |
| onClick={() => setCurrentSectionIndex(currentSectionIndex + 1)} |
| className="flex items-center gap-2 bg-indigo-600 text-white px-8 py-3 rounded-2xl hover:bg-indigo-700 shadow-xl shadow-indigo-100 font-black transition-all" |
| > |
| SUIVANT <ChevronRight size={24} /> |
| </button> |
| ) : ( |
| <button |
| onClick={handleFinish} |
| className="flex items-center gap-3 bg-emerald-600 text-white px-10 py-4 rounded-2xl hover:bg-emerald-700 shadow-xl shadow-emerald-100 font-black transition-all" |
| > |
| <Save size={24} /> TERMINER L'AUDIT |
| </button> |
| )} |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default AuditForm; |