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