Upload 19 files
Browse files- README.md +20 -11
- components/App.tsx +308 -0
- components/AuditForm.tsx +1 -2
- components/AuditHistory.tsx +74 -34
- index.html +5 -3
- index.tsx +1 -1
- metadata.json +5 -3
- package.json +5 -5
README.md
CHANGED
|
@@ -1,11 +1,20 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
};
|
| 26 |
|
| 27 |
const sendToWebhook = async (audit: Audit) => {
|
| 28 |
-
if (!audit.webhookUrl) return alert('
|
| 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 |
-
|
| 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.
|
| 55 |
} finally {
|
| 56 |
setSyncingId(null);
|
| 57 |
}
|
| 58 |
};
|
| 59 |
|
| 60 |
return (
|
| 61 |
-
<div className="space-y-6 animate-in fade-in
|
| 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
|
| 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
|
| 104 |
<button
|
| 105 |
-
onClick={() =>
|
| 106 |
-
|
|
|
|
| 107 |
>
|
| 108 |
-
|
|
|
|
| 109 |
</button>
|
| 110 |
<button
|
| 111 |
onClick={() => sendToWebhook(audit)}
|
| 112 |
disabled={syncingId === audit.id}
|
| 113 |
-
className={`flex
|
| 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 |
-
|
|
|
|
| 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": "
|
| 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 |
+
}
|