MMOON commited on
Commit
b7fe4e7
·
verified ·
1 Parent(s): 615c709

Upload 19 files

Browse files
README.md CHANGED
@@ -1,11 +1,20 @@
1
- ---
2
- title: AUDITPRO
3
- emoji: 🐠
4
- colorFrom: purple
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- short_description: GEstion d'audit SYNC
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
3
+ </div>
4
+
5
+ # Run and deploy your AI Studio app
6
+
7
+ This contains everything you need to run your app locally.
8
+
9
+ View your app in AI Studio: https://ai.studio/apps/drive/1QQEqFaCBtUdCzdO0zj5R5Wm1A7F6bUua
10
+
11
+ ## Run Locally
12
+
13
+ **Prerequisites:** Node.js
14
+
15
+
16
+ 1. Install dependencies:
17
+ `npm install`
18
+ 2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
19
+ 3. Run the app:
20
+ `npm run dev`
components/App.tsx ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import {
3
+ ClipboardCheck,
4
+ Settings,
5
+ History,
6
+ PlusCircle,
7
+ Package,
8
+ ArrowLeft,
9
+ LayoutDashboard,
10
+ Cloud,
11
+ CloudOff,
12
+ Download,
13
+ Upload,
14
+ Key,
15
+ ArrowRight,
16
+ HelpCircle,
17
+ X,
18
+ Database
19
+ } from 'lucide-react';
20
+ import { Checklist, Audit, AppSettings } from '../types';
21
+ import {
22
+ getAllFromStore,
23
+ saveToStore,
24
+ getFromStore,
25
+ deleteFromStore,
26
+ exportDatabase,
27
+ importDatabase
28
+ } from '../db';
29
+ import ChecklistManager from './ChecklistManager';
30
+ import AuditForm from './AuditForm';
31
+ import AuditHistory from './AuditHistory';
32
+ import SettingsPanel from './SettingsPanel';
33
+ import Onboarding from './Onboarding';
34
+
35
+ const App: React.FC = () => {
36
+ const [view, setView] = useState<'home' | 'checklist-manager' | 'audit' | 'history' | 'settings' | 'onboarding' | 'guide'>('home');
37
+ const [activeChecklist, setActiveChecklist] = useState<Checklist | null>(null);
38
+ const [activeAudit, setActiveAudit] = useState<Audit | null>(null);
39
+ const [checklists, setChecklists] = useState<Checklist[]>([]);
40
+ const [audits, setAudits] = useState<Audit[]>([]);
41
+ const [settings, setSettings] = useState<AppSettings>({
42
+ webhookUrl: '',
43
+ sheetId: '',
44
+ inspectorName: '',
45
+ onboardingComplete: false
46
+ });
47
+ const [isLoading, setIsLoading] = useState(true);
48
+ const [hasChanges, setHasChanges] = useState(false);
49
+ const fileInputRef = useRef<HTMLInputElement>(null);
50
+
51
+ useEffect(() => {
52
+ loadInitialData();
53
+ }, []);
54
+
55
+ const loadInitialData = async () => {
56
+ setIsLoading(true);
57
+ try {
58
+ const cl = await getAllFromStore<Checklist>('checklists');
59
+ const ad = await getAllFromStore<Audit>('audits');
60
+ const st = await getFromStore<AppSettings>('settings', 'main');
61
+
62
+ setChecklists(cl.sort((a, b) => b.lastModified - a.lastModified));
63
+ setAudits(ad.sort((a, b) => b.startedAt - a.startedAt));
64
+
65
+ if (st) {
66
+ setSettings(st);
67
+ if (!st.onboardingComplete && cl.length === 0 && ad.length === 0) {
68
+ setView('onboarding');
69
+ }
70
+ }
71
+ } catch (e) {
72
+ console.error("DB Init Error:", e);
73
+ } finally {
74
+ setIsLoading(false);
75
+ }
76
+ };
77
+
78
+ const exportDataPackage = async () => {
79
+ const packageData = await exportDatabase();
80
+ const blob = new Blob([JSON.stringify(packageData, null, 2)], { type: 'application/json' });
81
+ const url = URL.createObjectURL(blob);
82
+ const link = document.createElement('a');
83
+ link.href = url;
84
+ link.download = `AuditPro_Backup_${new Date().toISOString().split('T')[0]}.auditpro`;
85
+ link.click();
86
+ URL.revokeObjectURL(url);
87
+ setHasChanges(false);
88
+ };
89
+
90
+ const handleImportFile = (e: React.ChangeEvent<HTMLInputElement>) => {
91
+ const file = e.target.files?.[0];
92
+ if (!file) return;
93
+ const reader = new FileReader();
94
+ reader.onload = async (event) => {
95
+ try {
96
+ const json = JSON.parse(event.target?.result as string);
97
+ await importDatabase(json);
98
+ await loadInitialData();
99
+ setView('home');
100
+ setHasChanges(false);
101
+ } catch (err) {
102
+ alert("Fichier .auditpro invalide.");
103
+ }
104
+ };
105
+ reader.readAsText(file);
106
+ };
107
+
108
+ const startAudit = (checklist: Checklist) => {
109
+ const newAudit: Audit = {
110
+ id: `insp_${Date.now().toString(36)}`,
111
+ checklistId: checklist.id,
112
+ checklistName: checklist.name,
113
+ inspectorName: settings.inspectorName,
114
+ status: 'IN_PROGRESS',
115
+ startedAt: Date.now(),
116
+ responses: [],
117
+ webhookUrl: settings.webhookUrl,
118
+ sheetId: settings.sheetId
119
+ };
120
+ setActiveAudit(newAudit);
121
+ setActiveChecklist(checklist);
122
+ saveToStore('audits', newAudit);
123
+ setView('audit');
124
+ setHasChanges(true);
125
+ };
126
+
127
+ if (isLoading) return null;
128
+
129
+ if (!settings.onboardingComplete && view !== 'onboarding') {
130
+ return (
131
+ <div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-6 text-center">
132
+ <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">
133
+ <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">
134
+ <Key size={44} />
135
+ </div>
136
+ <div className="space-y-3">
137
+ <h1 className="text-4xl font-black text-slate-900 tracking-tight">AuditPro Sync</h1>
138
+ <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>
139
+ </div>
140
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-6">
141
+ <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">
142
+ <Upload size={28} className="group-hover:-translate-y-1 transition-transform" />
143
+ <span>IMPORTER</span>
144
+ </button>
145
+ <input type="file" ref={fileInputRef} onChange={handleImportFile} accept=".auditpro" className="hidden" />
146
+ <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">
147
+ <PlusCircle size={28} className="group-hover:rotate-90 transition-transform" />
148
+ <span>NOUVEAU</span>
149
+ </button>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ );
154
+ }
155
+
156
+ return (
157
+ <div className="min-h-screen bg-slate-50 flex flex-col font-sans selection:bg-indigo-100">
158
+ <header className="bg-white/80 backdrop-blur-md border-b border-slate-200 px-4 py-4 sticky top-0 z-40 shadow-sm">
159
+ <div className="max-w-5xl mx-auto flex items-center justify-between">
160
+ <div className="flex items-center gap-3 cursor-pointer" onClick={() => setView('home')}>
161
+ <div className="bg-indigo-600 p-2.5 rounded-xl text-white shadow-lg shadow-indigo-100">
162
+ <ClipboardCheck size={22} />
163
+ </div>
164
+ <h1 className="text-xl font-black text-slate-800 tracking-tight hidden sm:block">AuditPro <span className="text-indigo-600">Sync</span></h1>
165
+ </div>
166
+ <div className="flex items-center gap-2">
167
+ <button onClick={() => setView('guide')} className="p-2 text-slate-400 hover:text-indigo-600 transition-all" title="Guide d'utilisation">
168
+ <HelpCircle size={24} />
169
+ </button>
170
+ <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'}`}>
171
+ <Download size={16} /> <span className="hidden sm:inline">Sauvegarder l'espace</span>
172
+ </button>
173
+ <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'}`}>
174
+ <Settings size={22} />
175
+ </button>
176
+ </div>
177
+ </div>
178
+ </header>
179
+
180
+ <main className="flex-1 max-w-5xl w-full mx-auto p-4 sm:p-8 pb-32">
181
+ {view === 'home' && (
182
+ <div className="space-y-10 animate-in fade-in duration-700">
183
+ {/* Sync Header */}
184
+ <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'}`}>
185
+ <div className="flex items-center gap-6">
186
+ <div className={`p-5 rounded-3xl ${!!settings.sheetId ? 'bg-white/20 backdrop-blur-md' : 'bg-slate-100 text-slate-400'}`}>
187
+ {!!settings.sheetId ? <Cloud size={40} /> : <CloudOff size={40} />}
188
+ </div>
189
+ <div className="text-center md:text-left">
190
+ <h3 className="text-2xl font-black">{!!settings.sheetId ? 'Souveraineté Connectée' : 'Mode Autonome'}</h3>
191
+ <p className={`text-sm font-bold mt-1 opacity-80 ${!!settings.sheetId ? 'text-indigo-100' : 'text-slate-400'}`}>
192
+ {!!settings.sheetId ? `Flux actif vers ID ${settings.sheetId.substring(0,12)}...` : 'Configurez vos IDs Google Sheets pour activer la synchronisation.'}
193
+ </p>
194
+ </div>
195
+ </div>
196
+ {!settings.sheetId && (
197
+ <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>
198
+ )}
199
+ </div>
200
+
201
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
202
+ <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">
203
+ <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">
204
+ <PlusCircle size={44} />
205
+ </div>
206
+ <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>
207
+ </button>
208
+ <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">
209
+ <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">
210
+ <History size={44} />
211
+ </div>
212
+ <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>
213
+ </button>
214
+ </div>
215
+
216
+ <div className="space-y-6">
217
+ <h2 className="text-3xl font-black text-slate-900 tracking-tight px-2 flex items-center gap-3">
218
+ <LayoutDashboard className="text-indigo-600" /> Lancer un Audit
219
+ </h2>
220
+ {checklists.length === 0 ? (
221
+ <div className="bg-white border-2 border-dashed border-slate-200 rounded-[3rem] p-20 text-center space-y-6">
222
+ <Package size={72} className="mx-auto text-slate-200" />
223
+ <p className="text-slate-400 font-bold text-lg">Aucun modèle prêt. Créez votre première checklist pour démarrer.</p>
224
+ <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>
225
+ </div>
226
+ ) : (
227
+ <div className="grid grid-cols-1 gap-4">
228
+ {checklists.map(cl => (
229
+ <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">
230
+ <div className="flex items-center gap-6">
231
+ <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>
232
+ <div>
233
+ <h5 className="text-2xl font-black text-slate-800 tracking-tight">{cl.name}</h5>
234
+ <div className="flex gap-4 mt-2">
235
+ <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>
236
+ <span className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Version {cl.version}</span>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ <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">
241
+ AUDITER <ArrowRight size={20} />
242
+ </button>
243
+ </div>
244
+ ))}
245
+ </div>
246
+ )}
247
+ </div>
248
+ </div>
249
+ )}
250
+
251
+ {view === 'guide' && (
252
+ <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">
253
+ <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>
254
+ <div className="space-y-4 text-center">
255
+ <h2 className="text-5xl font-black text-slate-900 tracking-tighter">Guide AuditPro</h2>
256
+ <p className="text-slate-400 font-bold text-lg">Maîtrisez votre outil portable en 3 minutes.</p>
257
+ </div>
258
+
259
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-10">
260
+ <div className="p-10 bg-indigo-50 rounded-[2.5rem] space-y-6">
261
+ <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>
262
+ <h4 className="font-black text-2xl text-indigo-900">Structure</h4>
263
+ <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>
264
+ </div>
265
+ <div className="p-10 bg-amber-50 rounded-[2.5rem] space-y-6">
266
+ <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>
267
+ <h4 className="font-black text-2xl text-amber-900">Terrain</h4>
268
+ <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>
269
+ </div>
270
+ <div className="p-10 bg-emerald-50 rounded-[2.5rem] space-y-6">
271
+ <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>
272
+ <h4 className="font-black text-2xl text-emerald-900">Cloud</h4>
273
+ <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>
274
+ </div>
275
+ </div>
276
+
277
+ <div className="bg-slate-900 p-10 rounded-[2.5rem] flex items-start gap-8 shadow-2xl shadow-indigo-200">
278
+ <Database className="text-indigo-400 shrink-0" size={48} />
279
+ <div className="space-y-4">
280
+ <h4 className="text-2xl font-black text-white">Règle de Souveraineté</h4>
281
+ <p className="text-indigo-100/60 leading-relaxed font-medium">
282
+ 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.
283
+ </p>
284
+ <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>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ )}
289
+
290
+ {view === 'checklist-manager' && <ChecklistManager onSave={() => { setHasChanges(true); loadInitialData(); setView('home'); }} />}
291
+ {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')} />}
292
+ {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'); } }} />}
293
+ {view === 'settings' && <SettingsPanel settings={settings} onSave={async (s) => { await saveToStore('settings', { ...s, id: 'main' }); setHasChanges(true); await loadInitialData(); setView('home'); }} />}
294
+ {view === 'onboarding' && <Onboarding onComplete={async (s) => { await saveToStore('settings', { ...s, id: 'main' }); await loadInitialData(); setView('home'); }} />}
295
+ </main>
296
+
297
+ <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">
298
+ <div className="flex justify-around items-center">
299
+ <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>
300
+ <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>
301
+ <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>
302
+ </div>
303
+ </footer>
304
+ </div>
305
+ );
306
+ };
307
+
308
+ export default App;
components/AuditForm.tsx CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import React, { useState, useEffect } from 'react';
3
  import { Audit, Checklist, ItemType, AuditResponse } from '../types';
4
  import { Camera, Check, X, ChevronRight, ChevronLeft, Save } from 'lucide-react';
@@ -264,4 +263,4 @@ const AuditForm: React.FC<AuditFormProps> = ({ audit, checklist, onFinish, onCan
264
  );
265
  };
266
 
267
- export default AuditForm;
 
 
1
  import React, { useState, useEffect } from 'react';
2
  import { Audit, Checklist, ItemType, AuditResponse } from '../types';
3
  import { Camera, Check, X, ChevronRight, ChevronLeft, Save } from 'lucide-react';
 
263
  );
264
  };
265
 
266
+ export default AuditForm;
components/AuditHistory.tsx CHANGED
@@ -1,8 +1,19 @@
1
 
2
  import React, { useState } from 'react';
3
  import { Audit } from '../types';
4
- import { CloudUpload, Trash2, Download, Eye, FileText, CheckCircle2, CloudOff, Loader2 } from 'lucide-react';
 
 
 
 
 
 
 
 
 
 
5
  import { saveToStore } from '../db';
 
6
 
7
  interface AuditHistoryProps {
8
  audits: Audit[];
@@ -12,20 +23,36 @@ interface AuditHistoryProps {
12
 
13
  const AuditHistory: React.FC<AuditHistoryProps> = ({ audits, onDelete, onView }) => {
14
  const [syncingId, setSyncingId] = useState<string | null>(null);
 
 
15
 
16
- const exportAsJson = (audit: Audit) => {
17
- const data = JSON.stringify(audit, null, 2);
18
- const blob = new Blob([data], { type: 'application/json' });
19
- const url = URL.createObjectURL(blob);
20
- const link = document.createElement('a');
21
- link.href = url;
22
- link.download = `audit-${audit.checklistName.replace(/\s+/g, '-').toLowerCase()}-${new Date(audit.startedAt).toISOString().split('T')[0]}.json`;
23
- link.click();
24
- URL.revokeObjectURL(url);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  };
26
 
27
  const sendToWebhook = async (audit: Audit) => {
28
- if (!audit.webhookUrl) return alert('Aucune URL de Webhook configurée.');
29
 
30
  setSyncingId(audit.id);
31
  try {
@@ -34,12 +61,7 @@ const AuditHistory: React.FC<AuditHistoryProps> = ({ audits, onDelete, onView })
34
  headers: { 'Content-Type': 'application/json' },
35
  body: JSON.stringify({
36
  event: 'audit_sync',
37
- timestamp: new Date().toISOString(),
38
- data: {
39
- ...audit,
40
- // On s'assure que le sheetId est bien présent dans les data pour le script
41
- sheetId: audit.sheetId
42
- }
43
  })
44
  });
45
 
@@ -51,23 +73,47 @@ const AuditHistory: React.FC<AuditHistoryProps> = ({ audits, onDelete, onView })
51
  alert('Erreur Webhook : ' + response.statusText);
52
  }
53
  } catch (err) {
54
- alert('Erreur réseau. L\'audit est préservé dans votre clé locale.');
55
  } finally {
56
  setSyncingId(null);
57
  }
58
  };
59
 
60
  return (
61
- <div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
62
  <div className="flex items-center justify-between px-2">
63
- <h2 className="text-2xl font-black text-slate-800">Historique</h2>
64
  <span className="bg-slate-200 text-slate-600 text-[10px] font-black px-3 py-1 rounded-full uppercase tracking-widest">{audits.length} Audits</span>
65
  </div>
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  {audits.length === 0 ? (
68
  <div className="bg-white rounded-[2rem] p-16 border-2 border-dashed border-slate-200 text-center space-y-4">
69
  <FileText size={64} className="mx-auto text-slate-200" />
70
- <p className="text-slate-400 font-bold italic">Aucun audit trouvé dans votre clé.</p>
71
  </div>
72
  ) : (
73
  <div className="grid grid-cols-1 gap-4">
@@ -89,28 +135,22 @@ const AuditHistory: React.FC<AuditHistoryProps> = ({ audits, onDelete, onView })
89
  <p>{new Date(audit.startedAt).toLocaleString()}</p>
90
  <p className="uppercase tracking-widest">Par {audit.inspectorName || 'Inconnu'}</p>
91
  </div>
92
- <div className="mt-2">
93
- <span className={`text-[9px] px-2.5 py-1 rounded-lg font-black uppercase tracking-widest ${
94
- audit.status === 'SENT' ? 'bg-emerald-50 text-emerald-600' :
95
- audit.status === 'COMPLETED' ? 'bg-amber-50 text-amber-600' : 'bg-blue-50 text-blue-600'
96
- }`}>
97
- {audit.status === 'SENT' ? 'Synchronisé' : audit.status === 'COMPLETED' ? 'Local' : 'En cours'}
98
- </span>
99
- </div>
100
  </div>
101
  </div>
102
 
103
- <div className="flex items-center gap-2 flex-wrap md:flex-nowrap">
104
  <button
105
- onClick={() => onView(audit)}
106
- className="flex-1 md:flex-none px-4 py-2 text-slate-500 hover:bg-slate-100 rounded-xl text-xs font-black transition-all"
 
107
  >
108
- VOIR
 
109
  </button>
110
  <button
111
  onClick={() => sendToWebhook(audit)}
112
  disabled={syncingId === audit.id}
113
- className={`flex-1 md:flex-none flex items-center justify-center gap-2 px-6 py-2 rounded-xl text-xs font-black transition-all ${
114
  audit.status === 'SENT'
115
  ? 'bg-slate-100 text-slate-400'
116
  : 'bg-indigo-600 text-white shadow-lg shadow-indigo-100'
 
1
 
2
  import React, { useState } from 'react';
3
  import { Audit } from '../types';
4
+ import {
5
+ CloudUpload,
6
+ Trash2,
7
+ Download,
8
+ FileText,
9
+ CheckCircle2,
10
+ Loader2,
11
+ Sparkles,
12
+ AlertCircle,
13
+ X
14
+ } from 'lucide-react';
15
  import { saveToStore } from '../db';
16
+ import { GoogleGenAI } from "@google/genai";
17
 
18
  interface AuditHistoryProps {
19
  audits: Audit[];
 
23
 
24
  const AuditHistory: React.FC<AuditHistoryProps> = ({ audits, onDelete, onView }) => {
25
  const [syncingId, setSyncingId] = useState<string | null>(null);
26
+ const [analyzingId, setAnalyzingId] = useState<string | null>(null);
27
+ const [aiReport, setAiReport] = useState<{title: string, content: string} | null>(null);
28
 
29
+ const generateAiSummary = async (audit: Audit) => {
30
+ setAnalyzingId(audit.id);
31
+ try {
32
+ const ai = new GoogleGenAI({ apiKey: process.env.API_KEY || '' });
33
+ const prompt = `En tant qu'expert en audit, analyse ces résultats d'audit pour le modèle "${audit.checklistName}".
34
+ Voici les réponses : ${JSON.stringify(audit.responses.map(r => ({ label: r.itemLabel, val: r.value, note: r.comment })))}
35
+ Donne un résumé exécutif très court, liste les 3 points critiques et suggère une action corrective prioritaire. Réponds en français.`;
36
+
37
+ const response = await ai.models.generateContent({
38
+ model: 'gemini-3-flash-preview',
39
+ contents: prompt,
40
+ });
41
+
42
+ setAiReport({
43
+ title: `Analyse IA : ${audit.checklistName}`,
44
+ content: response.text || "Erreur de génération."
45
+ });
46
+ } catch (err) {
47
+ alert("L'IA n'est pas disponible pour le moment.");
48
+ console.error(err);
49
+ } finally {
50
+ setAnalyzingId(null);
51
+ }
52
  };
53
 
54
  const sendToWebhook = async (audit: Audit) => {
55
+ if (!audit.webhookUrl) return alert('Configurez vos IDs Google dans les réglages.');
56
 
57
  setSyncingId(audit.id);
58
  try {
 
61
  headers: { 'Content-Type': 'application/json' },
62
  body: JSON.stringify({
63
  event: 'audit_sync',
64
+ data: { ...audit, sheetId: audit.sheetId }
 
 
 
 
 
65
  })
66
  });
67
 
 
73
  alert('Erreur Webhook : ' + response.statusText);
74
  }
75
  } catch (err) {
76
+ alert('Erreur réseau. Données préservées localement.');
77
  } finally {
78
  setSyncingId(null);
79
  }
80
  };
81
 
82
  return (
83
+ <div className="space-y-6 animate-in fade-in duration-500">
84
  <div className="flex items-center justify-between px-2">
85
+ <h2 className="text-2xl font-black text-slate-800 tracking-tight">Historique</h2>
86
  <span className="bg-slate-200 text-slate-600 text-[10px] font-black px-3 py-1 rounded-full uppercase tracking-widest">{audits.length} Audits</span>
87
  </div>
88
 
89
+ {aiReport && (
90
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm animate-in fade-in">
91
+ <div className="bg-white rounded-[2.5rem] shadow-2xl max-w-2xl w-full max-h-[80vh] overflow-hidden flex flex-col border border-slate-200">
92
+ <div className="p-8 border-b border-slate-100 flex items-center justify-between bg-indigo-50/50">
93
+ <div className="flex items-center gap-3 text-indigo-600">
94
+ <Sparkles size={24} />
95
+ <h3 className="font-black text-xl">{aiReport.title}</h3>
96
+ </div>
97
+ <button onClick={() => setAiReport(null)} className="p-2 hover:bg-white rounded-xl transition-all">
98
+ <X size={24} />
99
+ </button>
100
+ </div>
101
+ <div className="p-8 overflow-y-auto text-slate-700 leading-relaxed font-medium">
102
+ <div className="prose prose-slate max-w-none">
103
+ {aiReport.content.split('\n').map((line, i) => <p key={i} className="mb-4">{line}</p>)}
104
+ </div>
105
+ </div>
106
+ <div className="p-6 bg-slate-50 border-t border-slate-100 flex justify-end">
107
+ <button onClick={() => setAiReport(null)} className="bg-slate-900 text-white px-8 py-3 rounded-2xl font-black text-sm">FERMER</button>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ )}
112
+
113
  {audits.length === 0 ? (
114
  <div className="bg-white rounded-[2rem] p-16 border-2 border-dashed border-slate-200 text-center space-y-4">
115
  <FileText size={64} className="mx-auto text-slate-200" />
116
+ <p className="text-slate-400 font-bold italic">Aucun audit trouvé.</p>
117
  </div>
118
  ) : (
119
  <div className="grid grid-cols-1 gap-4">
 
135
  <p>{new Date(audit.startedAt).toLocaleString()}</p>
136
  <p className="uppercase tracking-widest">Par {audit.inspectorName || 'Inconnu'}</p>
137
  </div>
 
 
 
 
 
 
 
 
138
  </div>
139
  </div>
140
 
141
+ <div className="flex items-center gap-2">
142
  <button
143
+ onClick={() => generateAiSummary(audit)}
144
+ disabled={analyzingId === audit.id}
145
+ className="flex items-center gap-2 px-4 py-2 bg-indigo-50 text-indigo-600 hover:bg-indigo-600 hover:text-white rounded-xl text-xs font-black transition-all shadow-sm"
146
  >
147
+ {analyzingId === audit.id ? <Loader2 size={14} className="animate-spin" /> : <Sparkles size={14} />}
148
+ ANALYSE IA
149
  </button>
150
  <button
151
  onClick={() => sendToWebhook(audit)}
152
  disabled={syncingId === audit.id}
153
+ className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-black transition-all ${
154
  audit.status === 'SENT'
155
  ? 'bg-slate-100 text-slate-400'
156
  : 'bg-indigo-600 text-white shadow-lg shadow-indigo-100'
index.html CHANGED
@@ -1,4 +1,3 @@
1
-
2
  <!DOCTYPE html>
3
  <html lang="fr">
4
  <head>
@@ -49,6 +48,7 @@
49
  }
50
  }
51
  </script>
 
52
  </head>
53
  <body class="bg-slate-50 text-slate-900">
54
  <div id="loading-screen">
@@ -61,7 +61,8 @@
61
 
62
  <div id="root"></div>
63
 
64
- <script type="module" src="./index.tsx"></script>
 
65
 
66
  <script type="module">
67
  window.addEventListener('load', () => {
@@ -74,5 +75,6 @@
74
  }
75
  });
76
  </script>
 
77
  </body>
78
- </html>
 
 
1
  <!DOCTYPE html>
2
  <html lang="fr">
3
  <head>
 
48
  }
49
  }
50
  </script>
51
+ <link rel="stylesheet" href="/index.css">
52
  </head>
53
  <body class="bg-slate-50 text-slate-900">
54
  <div id="loading-screen">
 
61
 
62
  <div id="root"></div>
63
 
64
+ <!-- En production, on charge le fichier compilé par esbuild -->
65
+ <script type="module" src="./bundle.js"></script>
66
 
67
  <script type="module">
68
  window.addEventListener('load', () => {
 
75
  }
76
  });
77
  </script>
78
+ <script type="module" src="/index.tsx"></script>
79
  </body>
80
+ </html>
index.tsx CHANGED
@@ -1,7 +1,7 @@
1
 
2
  import React from 'react';
3
  import { createRoot } from 'react-dom/client';
4
- import App from './App';
5
 
6
  const rootElement = document.getElementById('root');
7
  if (!rootElement) {
 
1
 
2
  import React from 'react';
3
  import { createRoot } from 'react-dom/client';
4
+ import App from './components/App';
5
 
6
  const rootElement = document.getElementById('root');
7
  if (!rootElement) {
metadata.json CHANGED
@@ -1,7 +1,9 @@
 
1
  {
2
  "name": "AuditPro Versatile",
3
- "description": "A comprehensive audit and inspection management tool featuring local storage, offline capabilities, and webhook connectivity.",
4
  "requestFramePermissions": [
5
- "camera"
 
6
  ]
7
- }
 
1
+
2
  {
3
  "name": "AuditPro Versatile",
4
+ "description": "A comprehensive audit and inspection management tool featuring local storage, offline capabilities, AI analysis, and webhook connectivity.",
5
  "requestFramePermissions": [
6
+ "camera",
7
+ "microphone"
8
  ]
9
+ }
package.json CHANGED
@@ -1,4 +1,3 @@
1
-
2
  {
3
  "name": "audit-pro-cloud",
4
  "version": "1.3.0",
@@ -6,16 +5,17 @@
6
  "type": "module",
7
  "scripts": {
8
  "dev": "npx serve .",
9
- "build": "esbuild index.tsx --bundle --outfile=bundle.js --minify --platform=browser --format=esm --target=es2020 --loader:.tsx=tsx --loader:.ts=ts",
10
- "start": "npx serve ."
11
  },
12
  "dependencies": {
13
  "lucide-react": "^0.475.0",
14
  "react": "^19.0.0",
15
  "react-dom": "^19.0.0",
16
- "xlsx": "0.18.5"
 
17
  },
18
  "devDependencies": {
19
  "esbuild": "^0.25.0"
20
  }
21
- }
 
 
1
  {
2
  "name": "audit-pro-cloud",
3
  "version": "1.3.0",
 
5
  "type": "module",
6
  "scripts": {
7
  "dev": "npx serve .",
8
+ "build": "npx esbuild index.tsx --bundle --outfile=bundle.js --minify --platform=browser --format=esm --target=es2020 --loader:.tsx=tsx --loader:.ts=ts --external:react --external:react-dom --external:lucide-react --external:xlsx --external:@google/genai",
9
+ "start": "serve -s . -p 7860"
10
  },
11
  "dependencies": {
12
  "lucide-react": "^0.475.0",
13
  "react": "^19.0.0",
14
  "react-dom": "^19.0.0",
15
+ "xlsx": "0.18.5",
16
+ "@google/genai": "latest"
17
  },
18
  "devDependencies": {
19
  "esbuild": "^0.25.0"
20
  }
21
+ }