AUDITPRO / components /App.tsx
MMOON's picture
Upload 19 files
b7fe4e7 verified
import React, { useState, useEffect, useRef } from 'react';
import {
ClipboardCheck,
Settings,
History,
PlusCircle,
Package,
ArrowLeft,
LayoutDashboard,
Cloud,
CloudOff,
Download,
Upload,
Key,
ArrowRight,
HelpCircle,
X,
Database
} from 'lucide-react';
import { Checklist, Audit, AppSettings } from '../types';
import {
getAllFromStore,
saveToStore,
getFromStore,
deleteFromStore,
exportDatabase,
importDatabase
} from '../db';
import ChecklistManager from './ChecklistManager';
import AuditForm from './AuditForm';
import AuditHistory from './AuditHistory';
import SettingsPanel from './SettingsPanel';
import Onboarding from './Onboarding';
const App: React.FC = () => {
const [view, setView] = useState<'home' | 'checklist-manager' | 'audit' | 'history' | 'settings' | 'onboarding' | 'guide'>('home');
const [activeChecklist, setActiveChecklist] = useState<Checklist | null>(null);
const [activeAudit, setActiveAudit] = useState<Audit | null>(null);
const [checklists, setChecklists] = useState<Checklist[]>([]);
const [audits, setAudits] = useState<Audit[]>([]);
const [settings, setSettings] = useState<AppSettings>({
webhookUrl: '',
sheetId: '',
inspectorName: '',
onboardingComplete: false
});
const [isLoading, setIsLoading] = useState(true);
const [hasChanges, setHasChanges] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
loadInitialData();
}, []);
const loadInitialData = async () => {
setIsLoading(true);
try {
const cl = await getAllFromStore<Checklist>('checklists');
const ad = await getAllFromStore<Audit>('audits');
const st = await getFromStore<AppSettings>('settings', 'main');
setChecklists(cl.sort((a, b) => b.lastModified - a.lastModified));
setAudits(ad.sort((a, b) => b.startedAt - a.startedAt));
if (st) {
setSettings(st);
if (!st.onboardingComplete && cl.length === 0 && ad.length === 0) {
setView('onboarding');
}
}
} catch (e) {
console.error("DB Init Error:", e);
} finally {
setIsLoading(false);
}
};
const exportDataPackage = async () => {
const packageData = await exportDatabase();
const blob = new Blob([JSON.stringify(packageData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `AuditPro_Backup_${new Date().toISOString().split('T')[0]}.auditpro`;
link.click();
URL.revokeObjectURL(url);
setHasChanges(false);
};
const handleImportFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (event) => {
try {
const json = JSON.parse(event.target?.result as string);
await importDatabase(json);
await loadInitialData();
setView('home');
setHasChanges(false);
} catch (err) {
alert("Fichier .auditpro invalide.");
}
};
reader.readAsText(file);
};
const startAudit = (checklist: Checklist) => {
const newAudit: Audit = {
id: `insp_${Date.now().toString(36)}`,
checklistId: checklist.id,
checklistName: checklist.name,
inspectorName: settings.inspectorName,
status: 'IN_PROGRESS',
startedAt: Date.now(),
responses: [],
webhookUrl: settings.webhookUrl,
sheetId: settings.sheetId
};
setActiveAudit(newAudit);
setActiveChecklist(checklist);
saveToStore('audits', newAudit);
setView('audit');
setHasChanges(true);
};
if (isLoading) return null;
if (!settings.onboardingComplete && view !== 'onboarding') {
return (
<div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-6 text-center">
<div className="bg-white p-10 sm:p-16 rounded-[3.5rem] shadow-2xl border border-slate-200 max-w-xl w-full space-y-8 animate-in zoom-in-95 duration-500">
<div className="bg-indigo-600 w-24 h-24 rounded-[2rem] flex items-center justify-center text-white mx-auto shadow-2xl shadow-indigo-100 rotate-3">
<Key size={44} />
</div>
<div className="space-y-3">
<h1 className="text-4xl font-black text-slate-900 tracking-tight">AuditPro Sync</h1>
<p className="text-slate-500 font-bold leading-relaxed max-w-sm mx-auto">Votre espace de travail est local et privé. Importez votre fichier ou créez un profil.</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-6">
<button onClick={() => fileInputRef.current?.click()} className="flex flex-col items-center justify-center gap-3 bg-slate-900 text-white p-6 rounded-3xl font-black hover:bg-slate-800 transition-all shadow-xl group">
<Upload size={28} className="group-hover:-translate-y-1 transition-transform" />
<span>IMPORTER</span>
</button>
<input type="file" ref={fileInputRef} onChange={handleImportFile} accept=".auditpro" className="hidden" />
<button onClick={() => setView('onboarding')} className="flex flex-col items-center justify-center gap-3 bg-indigo-50 text-indigo-600 p-6 rounded-3xl font-black hover:bg-indigo-100 transition-all border-2 border-transparent hover:border-indigo-200 group">
<PlusCircle size={28} className="group-hover:rotate-90 transition-transform" />
<span>NOUVEAU</span>
</button>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-slate-50 flex flex-col font-sans selection:bg-indigo-100">
<header className="bg-white/80 backdrop-blur-md border-b border-slate-200 px-4 py-4 sticky top-0 z-40 shadow-sm">
<div className="max-w-5xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3 cursor-pointer" onClick={() => setView('home')}>
<div className="bg-indigo-600 p-2.5 rounded-xl text-white shadow-lg shadow-indigo-100">
<ClipboardCheck size={22} />
</div>
<h1 className="text-xl font-black text-slate-800 tracking-tight hidden sm:block">AuditPro <span className="text-indigo-600">Sync</span></h1>
</div>
<div className="flex items-center gap-2">
<button onClick={() => setView('guide')} className="p-2 text-slate-400 hover:text-indigo-600 transition-all" title="Guide d'utilisation">
<HelpCircle size={24} />
</button>
<button onClick={exportDataPackage} className={`flex items-center gap-2 px-5 py-2.5 rounded-2xl text-[11px] font-black tracking-widest uppercase transition-all ${hasChanges ? 'bg-amber-500 text-white shadow-xl animate-pulse' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'}`}>
<Download size={16} /> <span className="hidden sm:inline">Sauvegarder l'espace</span>
</button>
<button onClick={() => setView('settings')} className={`p-2.5 rounded-xl transition-all ${view === 'settings' ? 'bg-indigo-600 text-white' : 'text-slate-500 hover:bg-slate-100'}`}>
<Settings size={22} />
</button>
</div>
</div>
</header>
<main className="flex-1 max-w-5xl w-full mx-auto p-4 sm:p-8 pb-32">
{view === 'home' && (
<div className="space-y-10 animate-in fade-in duration-700">
{/* Sync Header */}
<div className={`p-8 rounded-[2.5rem] border flex flex-col md:flex-row items-center justify-between gap-8 transition-all ${!!settings.sheetId ? 'bg-indigo-600 text-white shadow-2xl shadow-indigo-100 border-indigo-500' : 'bg-white border-slate-200 shadow-sm'}`}>
<div className="flex items-center gap-6">
<div className={`p-5 rounded-3xl ${!!settings.sheetId ? 'bg-white/20 backdrop-blur-md' : 'bg-slate-100 text-slate-400'}`}>
{!!settings.sheetId ? <Cloud size={40} /> : <CloudOff size={40} />}
</div>
<div className="text-center md:text-left">
<h3 className="text-2xl font-black">{!!settings.sheetId ? 'Souveraineté Connectée' : 'Mode Autonome'}</h3>
<p className={`text-sm font-bold mt-1 opacity-80 ${!!settings.sheetId ? 'text-indigo-100' : 'text-slate-400'}`}>
{!!settings.sheetId ? `Flux actif vers ID ${settings.sheetId.substring(0,12)}...` : 'Configurez vos IDs Google Sheets pour activer la synchronisation.'}
</p>
</div>
</div>
{!settings.sheetId && (
<button onClick={() => setView('settings')} className="bg-slate-900 text-white px-10 py-4 rounded-2xl text-xs font-black tracking-widest uppercase hover:bg-slate-800 transition-all shadow-xl">CONFIGURER</button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<button onClick={() => setView('checklist-manager')} className="flex items-center gap-8 p-10 bg-white border border-slate-200 rounded-[3rem] hover:shadow-2xl hover:-translate-y-1 transition-all group text-left">
<div className="bg-indigo-50 text-indigo-600 p-6 rounded-[1.5rem] group-hover:bg-indigo-600 group-hover:text-white transition-all shadow-sm">
<PlusCircle size={44} />
</div>
<div><h4 className="text-3xl font-black text-slate-800 tracking-tight">Modèles</h4><p className="text-slate-400 font-bold mt-1">Éditez vos formulaires métier.</p></div>
</button>
<button onClick={() => setView('history')} className="flex items-center gap-8 p-10 bg-white border border-slate-200 rounded-[3rem] hover:shadow-2xl hover:-translate-y-1 transition-all group text-left">
<div className="bg-amber-50 text-amber-600 p-6 rounded-[1.5rem] group-hover:bg-amber-600 group-hover:text-white transition-all shadow-sm">
<History size={44} />
</div>
<div><h4 className="text-3xl font-black text-slate-800 tracking-tight">Historique</h4><p className="text-slate-400 font-bold mt-1">Gérez vos rapports et analyses IA.</p></div>
</button>
</div>
<div className="space-y-6">
<h2 className="text-3xl font-black text-slate-900 tracking-tight px-2 flex items-center gap-3">
<LayoutDashboard className="text-indigo-600" /> Lancer un Audit
</h2>
{checklists.length === 0 ? (
<div className="bg-white border-2 border-dashed border-slate-200 rounded-[3rem] p-20 text-center space-y-6">
<Package size={72} className="mx-auto text-slate-200" />
<p className="text-slate-400 font-bold text-lg">Aucun modèle prêt. Créez votre première checklist pour démarrer.</p>
<button onClick={() => setView('checklist-manager')} className="bg-indigo-600 text-white px-12 py-4 rounded-2xl font-black text-sm hover:scale-105 transition-all shadow-xl shadow-indigo-100">CRÉER UN MODÈLE</button>
</div>
) : (
<div className="grid grid-cols-1 gap-4">
{checklists.map(cl => (
<div key={cl.id} className="bg-white border border-slate-200 p-8 rounded-[2.5rem] flex items-center justify-between group hover:border-indigo-400 hover:shadow-2xl transition-all cursor-default">
<div className="flex items-center gap-6">
<div className="bg-slate-50 p-5 rounded-2xl text-slate-300 group-hover:text-indigo-500 group-hover:bg-indigo-50 transition-all shadow-inner"><Package size={32} /></div>
<div>
<h5 className="text-2xl font-black text-slate-800 tracking-tight">{cl.name}</h5>
<div className="flex gap-4 mt-2">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{cl.sections.reduce((acc,s)=>acc+s.items.length,0)} points de contrôle</span>
<span className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Version {cl.version}</span>
</div>
</div>
</div>
<button onClick={() => startAudit(cl)} className="bg-indigo-600 text-white px-10 py-4 rounded-2xl text-sm font-black flex items-center gap-3 hover:bg-indigo-700 hover:scale-105 transition-all shadow-xl shadow-indigo-100">
AUDITER <ArrowRight size={20} />
</button>
</div>
))}
</div>
)}
</div>
</div>
)}
{view === 'guide' && (
<div className="bg-white rounded-[3.5rem] p-10 sm:p-20 border border-slate-200 shadow-2xl space-y-12 animate-in zoom-in-95 duration-500 relative">
<button onClick={() => setView('home')} className="absolute top-10 right-10 p-4 bg-slate-50 rounded-2xl hover:text-rose-500 transition-all"><X size={32}/></button>
<div className="space-y-4 text-center">
<h2 className="text-5xl font-black text-slate-900 tracking-tighter">Guide AuditPro</h2>
<p className="text-slate-400 font-bold text-lg">Maîtrisez votre outil portable en 3 minutes.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
<div className="p-10 bg-indigo-50 rounded-[2.5rem] space-y-6">
<div className="bg-indigo-600 text-white w-12 h-12 rounded-2xl flex items-center justify-center font-black text-xl shadow-lg shadow-indigo-100">1</div>
<h4 className="font-black text-2xl text-indigo-900">Structure</h4>
<p className="text-sm text-indigo-800/70 leading-relaxed font-bold">Importez vos points de contrôle via Excel ou créez-les manuellement. Chaque point peut inclure des photos.</p>
</div>
<div className="p-10 bg-amber-50 rounded-[2.5rem] space-y-6">
<div className="bg-amber-600 text-white w-12 h-12 rounded-2xl flex items-center justify-center font-black text-xl shadow-lg shadow-amber-100">2</div>
<h4 className="font-black text-2xl text-amber-900">Terrain</h4>
<p className="text-sm text-amber-800/70 leading-relaxed font-bold">Réalisez vos audits hors-ligne. Les photos et notes sont stockées instantanément dans votre navigateur.</p>
</div>
<div className="p-10 bg-emerald-50 rounded-[2.5rem] space-y-6">
<div className="bg-emerald-600 text-white w-12 h-12 rounded-2xl flex items-center justify-center font-black text-xl shadow-lg shadow-emerald-100">3</div>
<h4 className="font-black text-2xl text-emerald-900">Cloud</h4>
<p className="text-sm text-emerald-800/70 leading-relaxed font-bold">Synchronisez vers Google Sheets en un clic. L'IA Gemini analyse vos résultats pour vous.</p>
</div>
</div>
<div className="bg-slate-900 p-10 rounded-[2.5rem] flex items-start gap-8 shadow-2xl shadow-indigo-200">
<Database className="text-indigo-400 shrink-0" size={48} />
<div className="space-y-4">
<h4 className="text-2xl font-black text-white">Règle de Souveraineté</h4>
<p className="text-indigo-100/60 leading-relaxed font-medium">
Aucun serveur central ne stocke vos données. Si vous changez de navigateur ou d'appareil, vous devez réimporter votre fichier <code>.auditpro</code> de sauvegarde.
</p>
<button onClick={() => setView('home')} className="bg-indigo-600 text-white px-10 py-3.5 rounded-2xl font-black text-sm hover:bg-indigo-700 transition-all">D'ACCORD</button>
</div>
</div>
</div>
)}
{view === 'checklist-manager' && <ChecklistManager onSave={() => { setHasChanges(true); loadInitialData(); setView('home'); }} />}
{view === 'audit' && activeAudit && activeChecklist && <AuditForm audit={activeAudit} checklist={activeChecklist} onFinish={async (a) => { await saveToStore('audits', a); setHasChanges(true); await loadInitialData(); setView('home'); }} onCancel={() => setView('home')} />}
{view === 'history' && <AuditHistory audits={audits} onDelete={async (id) => { await deleteFromStore('audits', id); setHasChanges(true); await loadInitialData(); }} onView={async (a) => { const cl = await getFromStore<Checklist>('checklists', a.checklistId); if(cl){ setActiveAudit(a); setActiveChecklist(cl); setView('audit'); } }} />}
{view === 'settings' && <SettingsPanel settings={settings} onSave={async (s) => { await saveToStore('settings', { ...s, id: 'main' }); setHasChanges(true); await loadInitialData(); setView('home'); }} />}
{view === 'onboarding' && <Onboarding onComplete={async (s) => { await saveToStore('settings', { ...s, id: 'main' }); await loadInitialData(); setView('home'); }} />}
</main>
<footer className="fixed bottom-0 left-0 right-0 p-4 bg-white/80 backdrop-blur-xl border-t border-slate-200 z-30 sm:hidden">
<div className="flex justify-around items-center">
<button onClick={() => setView('home')} className={`p-4 rounded-2xl transition-all ${view === 'home' ? 'text-indigo-600 bg-indigo-50 shadow-inner' : 'text-slate-400'}`}><LayoutDashboard size={28} /></button>
<button onClick={() => setView('checklist-manager')} className={`p-4 rounded-2xl transition-all ${view === 'checklist-manager' ? 'text-indigo-600 bg-indigo-50 shadow-inner' : 'text-slate-400'}`}><PlusCircle size={28} /></button>
<button onClick={() => setView('history')} className={`p-4 rounded-2xl transition-all ${view === 'history' ? 'text-indigo-600 bg-indigo-50 shadow-inner' : 'text-slate-400'}`}><History size={28} /></button>
</div>
</footer>
</div>
);
};
export default App;