AUDITPRO / components /AuditForm.tsx
MMOON's picture
Upload 19 files
b7fe4e7 verified
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;