@mostajs/setup 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,8 +58,10 @@ npm install @mostajs/setup @mostajs/orm
58
58
  │ ├── install.route.ts # Factory POST /api/setup/install
59
59
  │ ├── detect-modules.route.ts # Factory GET /api/setup/detect-modules
60
60
  │ ├── install-modules.route.ts # Factory POST /api/setup/install-modules
61
- └── reconfig.route.ts # Factory GET+POST /api/setup/reconfig
61
+ ├── reconfig.route.ts # Factory GET+POST /api/setup/reconfig
62
+ │ └── upload-jar.route.ts # Factory GET+POST+DELETE /api/setup/upload-jar
62
63
  ├── components/
64
+ │ ├── SetupWizard.tsx # Wizard d'installation complet (6 etapes)
63
65
  │ └── ReconfigPanel.tsx # UI reconfiguration (modules + DB)
64
66
  ├── types/
65
67
  │ └── index.ts # Tous les types TypeScript
@@ -347,242 +349,68 @@ Ce handler :
347
349
 
348
350
  ### Etape 4 — Creer la page Setup (frontend)
349
351
 
350
- Voici un exemple **minimal** de page setup. Adaptez le style a votre design system.
352
+ Le module fournit un composant **`SetupWizard`** pret a l'emploi avec inline styles (aucune dependance Tailwind/shadcn).
353
+ Votre page Next.js n'est qu'un **wrapper** d'une vingtaine de lignes :
351
354
 
352
355
  ```tsx
353
356
  // src/app/setup/page.tsx
354
357
  'use client'
355
358
 
356
- import { useState, useEffect } from 'react'
357
359
  import { useRouter } from 'next/navigation'
358
- import { resolveModuleDependencies } from '@mostajs/setup/data/module-definitions'
359
- import type { ModuleDefinition } from '@mostajs/setup'
360
-
361
- type Dialect = 'mongodb' | 'sqlite' | 'postgres' | 'mysql' | 'mariadb'
362
- | 'oracle' | 'mssql' | 'cockroachdb' | 'db2' | 'hana'
363
- | 'hsqldb' | 'spanner' | 'sybase'
360
+ import SetupWizard from '@mostajs/setup/components/SetupWizard'
361
+ import { t } from '@/i18n' // ou toute fonction de traduction
364
362
 
365
363
  export default function SetupPage() {
366
364
  const router = useRouter()
367
365
 
368
- // --- Etat du wizard ---
369
- const [step, setStep] = useState(1) // 1=modules, 2=db, 3=admin, 4=install
370
-
371
- // Modules
372
- const [availableModules, setAvailableModules] = useState<ModuleDefinition[]>([])
373
- const [selectedModules, setSelectedModules] = useState<string[]>([])
374
- const [installedModules, setInstalledModules] = useState<string[]>([])
375
-
376
- // DB
377
- const [dialect, setDialect] = useState<Dialect>('mongodb')
378
- const [dbConfig, setDbConfig] = useState({ host: 'localhost', port: 27017, name: 'mydb', user: '', password: '' })
379
- const [dbOk, setDbOk] = useState(false)
380
-
381
- // Admin
382
- const [admin, setAdmin] = useState({ email: '', password: '', firstName: '', lastName: '' })
383
-
384
- // Install
385
- const [installing, setInstalling] = useState(false)
386
- const [result, setResult] = useState<{ ok: boolean; error?: string } | null>(null)
387
-
388
- // --- Charger les modules depuis l'API au montage ---
389
- useEffect(() => {
390
- fetch('/api/setup/detect-modules')
391
- .then(r => r.json())
392
- .then((data: { modules: ModuleDefinition[]; installed: string[] }) => {
393
- setAvailableModules(data.modules || [])
394
- setInstalledModules(data.installed || [])
395
- // Pre-cocher les modules required + default + deja installes
396
- const preChecked = new Set([
397
- ...(data.modules || []).filter(m => m.required || m.default).map(m => m.key),
398
- ...(data.installed || []),
399
- ])
400
- setSelectedModules(Array.from(preChecked))
401
- })
402
- .catch(() => {})
403
- }, [])
404
-
405
- // --- Toggle un module (avec resolution des dependances) ---
406
- function toggleModule(key: string) {
407
- const mod = availableModules.find(m => m.key === key)
408
- if (!mod || mod.required) return
409
-
410
- setSelectedModules(prev => {
411
- if (prev.includes(key)) {
412
- // Decochage : retirer aussi les modules qui dependent de celui-ci
413
- const toRemove = new Set([key])
414
- let changed = true
415
- while (changed) {
416
- changed = false
417
- for (const m of availableModules) {
418
- if (toRemove.has(m.key) || m.required) continue
419
- if (m.dependsOn?.some(dep => toRemove.has(dep)) && prev.includes(m.key)) {
420
- toRemove.add(m.key)
421
- changed = true
422
- }
423
- }
424
- }
425
- return prev.filter(k => !toRemove.has(k))
426
- } else {
427
- // Cochage : ajouter les dependances transitives
428
- return resolveModuleDependencies([...prev, key], availableModules)
429
- }
430
- })
431
- }
432
-
433
- // --- Tester la connexion DB ---
434
- async function testDb() {
435
- const res = await fetch('/api/setup/test-db', {
436
- method: 'POST',
437
- headers: { 'Content-Type': 'application/json' },
438
- body: JSON.stringify({ dialect, ...dbConfig }),
439
- })
440
- const data = await res.json()
441
- setDbOk(data.ok === true)
442
- }
366
+ return (
367
+ <SetupWizard
368
+ t={t}
369
+ onComplete={() => router.push('/login')}
370
+ dbNamePrefix="secuaccessdb"
371
+ endpoints={{
372
+ detectModules: '/api/setup/detect-modules',
373
+ testDb: '/api/setup/test-db',
374
+ installModules: '/api/setup/install-modules',
375
+ install: '/api/setup/install',
376
+ uploadJar: '/api/setup/upload-jar',
377
+ }}
378
+ />
379
+ )
380
+ }
381
+ ```
443
382
 
444
- // --- Lancer l'installation ---
445
- async function install() {
446
- setInstalling(true)
447
- setResult(null)
448
-
449
- // 1. Installer les modules npm
450
- const modRes = await fetch('/api/setup/install-modules', {
451
- method: 'POST',
452
- headers: { 'Content-Type': 'application/json' },
453
- body: JSON.stringify({ modules: selectedModules }),
454
- })
455
- if (!modRes.ok) {
456
- setResult({ ok: false, error: 'Erreur installation modules' })
457
- setInstalling(false)
458
- return
459
- }
383
+ #### Props de SetupWizard
460
384
 
461
- // 2. Configurer la DB + seeder
462
- const res = await fetch('/api/setup/install', {
463
- method: 'POST',
464
- headers: { 'Content-Type': 'application/json' },
465
- body: JSON.stringify({ dialect, db: dbConfig, admin, modules: selectedModules }),
466
- })
467
- const data = await res.json()
468
- setResult(data.data || { ok: false, error: data.error?.message })
469
- setInstalling(false)
470
- }
385
+ | Prop | Type | Default | Description |
386
+ |------|------|---------|-------------|
387
+ | `t` | `(key: string) => string` | `(k) => k` | Fonction de traduction (recoit des cles `setup.xxx`) |
388
+ | `onComplete` | `() => void` | — | Callback apres installation reussie (ex: `router.push('/login')`) |
389
+ | `dbNamePrefix` | `string` | `'mydb'` | Prefixe pour le nom de base par defaut |
390
+ | `persistState` | `boolean` | `true` | Persister l'etat du wizard dans `sessionStorage` |
391
+ | `endpoints` | `object` | voir ci-dessous | URLs des routes API |
471
392
 
472
- return (
473
- <div style={{ maxWidth: 600, margin: '50px auto', padding: 20 }}>
474
- <h1>Setup — Mon Application</h1>
475
-
476
- {/* ── Etape 1 : Modules ── */}
477
- {step === 1 && (
478
- <div>
479
- <h2>1. Modules</h2>
480
- {availableModules.map(mod => (
481
- <label key={mod.key} style={{ display: 'block', margin: '8px 0' }}>
482
- <input
483
- type="checkbox"
484
- checked={selectedModules.includes(mod.key)}
485
- disabled={mod.required}
486
- onChange={() => toggleModule(mod.key)}
487
- />
488
- {' '}{mod.icon} {mod.label}
489
- {mod.required && <em> (requis)</em>}
490
- {mod.discovered && <strong> NOUVEAU</strong>}
491
- {installedModules.includes(mod.key) && <span> ✓</span>}
492
- </label>
493
- ))}
494
- <button onClick={() => setStep(2)}>Suivant →</button>
495
- </div>
496
- )}
497
-
498
- {/* ── Etape 2 : Base de donnees ── */}
499
- {step === 2 && (
500
- <div>
501
- <h2>2. Base de donnees</h2>
502
- <select value={dialect} onChange={e => setDialect(e.target.value as Dialect)}>
503
- <option value="mongodb">MongoDB</option>
504
- <option value="sqlite">SQLite</option>
505
- <option value="postgres">PostgreSQL</option>
506
- <option value="mysql">MySQL</option>
507
- {/* ... autres dialectes ... */}
508
- </select>
509
-
510
- {dialect !== 'sqlite' && (
511
- <>
512
- <input placeholder="Host" value={dbConfig.host}
513
- onChange={e => setDbConfig({ ...dbConfig, host: e.target.value })} />
514
- <input placeholder="Port" type="number" value={dbConfig.port}
515
- onChange={e => setDbConfig({ ...dbConfig, port: +e.target.value })} />
516
- <input placeholder="User" value={dbConfig.user}
517
- onChange={e => setDbConfig({ ...dbConfig, user: e.target.value })} />
518
- <input placeholder="Password" type="password" value={dbConfig.password}
519
- onChange={e => setDbConfig({ ...dbConfig, password: e.target.value })} />
520
- </>
521
- )}
522
- <input placeholder="Nom de la base" value={dbConfig.name}
523
- onChange={e => setDbConfig({ ...dbConfig, name: e.target.value })} />
524
-
525
- <button onClick={testDb}>Tester la connexion</button>
526
- {dbOk && <span style={{ color: 'green' }}> ✓ Connexion OK</span>}
527
-
528
- <br />
529
- <button onClick={() => setStep(1)}>← Retour</button>
530
- <button onClick={() => setStep(3)} disabled={dialect !== 'sqlite' && !dbOk}>
531
- Suivant →
532
- </button>
533
- </div>
534
- )}
535
-
536
- {/* ── Etape 3 : Administrateur ── */}
537
- {step === 3 && (
538
- <div>
539
- <h2>3. Administrateur</h2>
540
- <input placeholder="Prenom" value={admin.firstName}
541
- onChange={e => setAdmin({ ...admin, firstName: e.target.value })} />
542
- <input placeholder="Nom" value={admin.lastName}
543
- onChange={e => setAdmin({ ...admin, lastName: e.target.value })} />
544
- <input placeholder="Email" type="email" value={admin.email}
545
- onChange={e => setAdmin({ ...admin, email: e.target.value })} />
546
- <input placeholder="Mot de passe (min 6)" type="password" value={admin.password}
547
- onChange={e => setAdmin({ ...admin, password: e.target.value })} />
548
-
549
- <br />
550
- <button onClick={() => setStep(2)}>← Retour</button>
551
- <button onClick={() => setStep(4)}>Suivant →</button>
552
- </div>
553
- )}
554
-
555
- {/* ── Etape 4 : Installation ── */}
556
- {step === 4 && (
557
- <div>
558
- <h2>4. Recapitulatif</h2>
559
- <p><strong>DB :</strong> {dialect} — {dbConfig.name}</p>
560
- <p><strong>Admin :</strong> {admin.email}</p>
561
- <p><strong>Modules :</strong> {selectedModules.join(', ')}</p>
562
-
563
- <button onClick={install} disabled={installing}>
564
- {installing ? 'Installation en cours...' : 'Installer'}
565
- </button>
566
-
567
- {result?.ok && (
568
- <div style={{ color: 'green', marginTop: 16 }}>
569
- ✓ Installation terminee !
570
- <br />
571
- <button onClick={() => router.push('/login')}>Aller au login →</button>
572
- </div>
573
- )}
574
- {result && !result.ok && (
575
- <div style={{ color: 'red', marginTop: 16 }}>
576
- ✗ Erreur : {result.error}
577
- </div>
578
- )}
579
- </div>
580
- )}
581
- </div>
582
- )
393
+ **Endpoints par defaut :**
394
+ ```json
395
+ {
396
+ "detectModules": "/api/setup/detect-modules",
397
+ "testDb": "/api/setup/test-db",
398
+ "installModules": "/api/setup/install-modules",
399
+ "install": "/api/setup/install",
400
+ "uploadJar": "/api/setup/upload-jar"
583
401
  }
584
402
  ```
585
403
 
404
+ **Cles i18n attendues :** `setup.steps.*`, `setup.welcome.*`, `setup.modules.*`, `setup.dialect.*`, `setup.database.*`, `setup.admin.*`, `setup.summary.*`, `setup.back`, `setup.next`.
405
+
406
+ #### Le wizard inclut :
407
+ - **6 etapes** : Accueil → Modules → Dialecte → Base de donnees → Admin → Recapitulatif
408
+ - **13 dialectes** avec distinction Premium (grises, non cliquables) et badges JDBC
409
+ - **Upload JAR** integre pour les dialectes JDBC (hsqldb, oracle, db2, hana, sybase)
410
+ - **Persistence sessionStorage** pour survivre aux hot-reloads Next.js
411
+ - **Resolution des dependances** entre modules
412
+ - **Retry automatique** apres npm install (le serveur peut redemarrer)
413
+
586
414
  ### Etape 5 — Middleware : rediriger vers /setup
587
415
 
588
416
  ```typescript
@@ -948,7 +776,25 @@ const { GET, POST } = createReconfigHandlers()
948
776
  export { GET, POST }
949
777
  ```
950
778
 
951
- #### 2. Page de reconfiguration
779
+ #### 2. Route API JAR Upload (drivers JDBC)
780
+
781
+ **`src/app/api/setup/upload-jar/route.ts`**
782
+ ```typescript
783
+ // Author: Dr Hamid MADANI drmdh@msn.com
784
+ import { createUploadJarHandlers } from '@mostajs/setup/api/upload-jar'
785
+
786
+ const { GET, POST, DELETE } = createUploadJarHandlers()
787
+ export { GET, POST, DELETE }
788
+ ```
789
+
790
+ > La logique d'upload est dans `@mostajs/orm` (`saveJarFile`, `deleteJarFile`, `listJarFiles`).
791
+ > La route factory dans `@mostajs/setup` ne fait que la deleguer.
792
+ >
793
+ > - **GET** — liste les JARs et le statut des dialects JDBC
794
+ > - **POST** — upload un fichier `.jar` (multipart/form-data, champ `jar`)
795
+ > - **DELETE** — supprime un JAR (`{ "fileName": "hsqldb-2.7.2.jar" }`)
796
+
797
+ #### 3. Page de reconfiguration
952
798
 
953
799
  **`src/app/dashboard/settings/reconfig/page.tsx`**
954
800
  ```tsx
@@ -962,6 +808,7 @@ export default function ReconfigPage() {
962
808
  <ReconfigPanel
963
809
  apiEndpoint="/api/setup/reconfig"
964
810
  detectEndpoint="/api/setup/detect-modules"
811
+ jarEndpoint="/api/setup/upload-jar"
965
812
  showSeedOption
966
813
  onDbChanged={() => window.location.reload()}
967
814
  onSeedRequested={async () => {
@@ -0,0 +1,19 @@
1
+ export interface SetupWizardProps {
2
+ /** Translation function — receives i18n key, returns translated string */
3
+ t?: (key: string) => string;
4
+ /** Called when setup is complete and user clicks "Go to Login" */
5
+ onComplete?: () => void;
6
+ /** API endpoints */
7
+ endpoints?: {
8
+ detectModules?: string;
9
+ testDb?: string;
10
+ installModules?: string;
11
+ install?: string;
12
+ uploadJar?: string;
13
+ };
14
+ /** Default database name prefix (e.g. 'secuaccessdb') */
15
+ dbNamePrefix?: string;
16
+ /** Whether to persist wizard state in sessionStorage (default: true) */
17
+ persistState?: boolean;
18
+ }
19
+ export default function SetupWizard({ t: tProp, onComplete, endpoints, dbNamePrefix, persistState, }: SetupWizardProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,491 @@
1
+ // @mostajs/setup — Setup Wizard (reusable, inline-styles, no Tailwind/shadcn dependency)
2
+ // Author: Dr Hamid MADANI drmdh@msn.com
3
+ 'use client';
4
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
5
+ import { useState, useEffect, useCallback } from 'react';
6
+ // ── Constants ────────────────────────────────────────────────
7
+ const STEPS = ['welcome', 'modules', 'dialect', 'database', 'admin', 'summary'];
8
+ const DIALECT_DEFAULTS = {
9
+ mongodb: { host: 'localhost', port: 27017, name: 'mydb_prod', user: '', password: '' },
10
+ sqlite: { host: '', port: 0, name: 'mydb', user: '', password: '' },
11
+ postgres: { host: 'localhost', port: 5432, name: 'mydb', user: 'postgres', password: '' },
12
+ mysql: { host: 'localhost', port: 3306, name: 'mydb', user: 'root', password: '' },
13
+ mariadb: { host: 'localhost', port: 3306, name: 'mydb', user: 'root', password: '' },
14
+ oracle: { host: 'localhost', port: 1521, name: 'mydb', user: 'system', password: '' },
15
+ mssql: { host: 'localhost', port: 1433, name: 'mydb', user: 'sa', password: '' },
16
+ cockroachdb: { host: 'localhost', port: 26257, name: 'mydb', user: 'root', password: '' },
17
+ db2: { host: 'localhost', port: 50000, name: 'mydb', user: 'db2inst1', password: '' },
18
+ hana: { host: 'localhost', port: 30015, name: 'mydb', user: 'SYSTEM', password: '' },
19
+ hsqldb: { host: 'localhost', port: 9001, name: 'mydb', user: 'SA', password: '' },
20
+ spanner: { host: '', port: 0, name: 'my-project/my-instance/mydb', user: '', password: '' },
21
+ sybase: { host: 'localhost', port: 5000, name: 'mydb', user: 'sa', password: '' },
22
+ };
23
+ const DIALECT_INFO = [
24
+ { key: 'mongodb', name: 'MongoDB', icon: '🍃' },
25
+ { key: 'sqlite', name: 'SQLite', icon: '📁' },
26
+ { key: 'postgres', name: 'PostgreSQL', icon: '🐘' },
27
+ { key: 'mysql', name: 'MySQL', icon: '🐬' },
28
+ { key: 'mariadb', name: 'MariaDB', icon: '🦭' },
29
+ { key: 'mssql', name: 'SQL Server', icon: '🟦' },
30
+ { key: 'oracle', name: 'Oracle', icon: '🔴', premium: true, jdbc: true },
31
+ { key: 'cockroachdb', name: 'CockroachDB', icon: '🪳' },
32
+ { key: 'db2', name: 'IBM DB2', icon: '🏢', premium: true, jdbc: true },
33
+ { key: 'hana', name: 'SAP HANA', icon: '💎', premium: true, jdbc: true },
34
+ { key: 'hsqldb', name: 'HyperSQL', icon: '⚡', jdbc: true },
35
+ { key: 'spanner', name: 'Cloud Spanner', icon: '☁️', premium: true },
36
+ { key: 'sybase', name: 'Sybase ASE', icon: '🔷', premium: true, jdbc: true },
37
+ ];
38
+ const JDBC_DIALECTS = ['hsqldb', 'oracle', 'db2', 'hana', 'sybase'];
39
+ const DRIVER_HINTS = {
40
+ mongodb: 'npm install mongoose',
41
+ sqlite: 'npm install better-sqlite3',
42
+ postgres: 'npm install pg',
43
+ mysql: 'npm install mysql2',
44
+ mariadb: 'npm install mariadb',
45
+ oracle: 'npm install oracledb',
46
+ mssql: 'npm install mssql',
47
+ cockroachdb: 'npm install pg',
48
+ db2: 'npm install ibm_db',
49
+ hana: 'npm install @sap/hana-client',
50
+ hsqldb: 'Uploader hsqldb*.jar dans jar_files/',
51
+ spanner: 'npm install @google-cloud/spanner',
52
+ sybase: 'npm install sybase',
53
+ };
54
+ const STORAGE_KEY = 'setup-wizard-state';
55
+ // ── Inline Styles ────────────────────────────────────────────
56
+ const S = {
57
+ wrapper: { display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', padding: 16, fontFamily: 'system-ui, sans-serif' },
58
+ container: { width: '100%', maxWidth: 672 },
59
+ // Stepper
60
+ stepperRow: { display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 32, gap: 8, flexWrap: 'wrap' },
61
+ stepCircle: (state) => ({
62
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
63
+ width: 32, height: 32, borderRadius: '50%', fontSize: 13, fontWeight: 500,
64
+ backgroundColor: state === 'future' ? '#e5e7eb' : '#0284c7',
65
+ color: state === 'future' ? '#6b7280' : '#fff',
66
+ ...(state === 'current' ? { boxShadow: '0 0 0 4px #bae6fd' } : {}),
67
+ }),
68
+ stepLabel: (active) => ({
69
+ fontSize: 13,
70
+ fontWeight: active ? 600 : 400,
71
+ color: active ? '#0369a1' : '#6b7280',
72
+ }),
73
+ stepLine: (done) => ({
74
+ width: 32, height: 2,
75
+ backgroundColor: done ? '#0284c7' : '#e5e7eb',
76
+ }),
77
+ // Card
78
+ card: { backgroundColor: '#fff', border: '1px solid #e5e7eb', borderRadius: 16, boxShadow: '0 10px 25px -5px rgba(0,0,0,0.1)', padding: 32 },
79
+ // Section header
80
+ sectionHeader: { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 },
81
+ sectionIcon: { fontSize: 24, color: '#0284c7', lineHeight: 1 },
82
+ sectionTitle: { fontSize: 20, fontWeight: 700, color: '#111827' },
83
+ sectionDesc: { fontSize: 13, color: '#6b7280', marginTop: 4 },
84
+ // Buttons
85
+ btn: (variant = 'primary', disabled = false) => ({
86
+ display: 'inline-flex', alignItems: 'center', gap: 8,
87
+ padding: variant === 'lg' ? '12px 24px' : '8px 16px',
88
+ border: variant === 'outline' ? '1px solid #d1d5db' : 'none',
89
+ borderRadius: 8,
90
+ fontSize: variant === 'lg' ? 15 : 13,
91
+ fontWeight: 600,
92
+ cursor: disabled ? 'not-allowed' : 'pointer',
93
+ opacity: disabled ? 0.5 : 1,
94
+ backgroundColor: variant === 'outline' ? '#fff' : '#0284c7',
95
+ color: variant === 'outline' ? '#374151' : '#fff',
96
+ transition: 'all 0.15s',
97
+ }),
98
+ // Form
99
+ formGroup: { marginBottom: 12 },
100
+ label: { display: 'block', marginBottom: 4, fontWeight: 500, fontSize: 13, color: '#374151' },
101
+ input: { width: '100%', padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: 6, fontSize: 14, boxSizing: 'border-box', outline: 'none' },
102
+ formRow: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 },
103
+ // Grid
104
+ grid2: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 12 },
105
+ grid3: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 12 },
106
+ // Module card
107
+ moduleCard: (selected, disabled) => ({
108
+ padding: 16, borderRadius: 8, textAlign: 'left',
109
+ border: `2px solid ${selected ? '#0284c7' : '#e5e7eb'}`,
110
+ backgroundColor: selected ? '#f0f9ff' : '#fff',
111
+ cursor: disabled ? 'not-allowed' : 'pointer',
112
+ opacity: disabled ? 0.5 : 1,
113
+ transition: 'all 0.2s',
114
+ }),
115
+ moduleHeader: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 },
116
+ moduleLeft: { display: 'flex', alignItems: 'center', gap: 8 },
117
+ moduleName: { fontWeight: 600, fontSize: 14, color: '#111827' },
118
+ moduleDesc: { fontSize: 12, color: '#6b7280' },
119
+ badge: (type) => ({
120
+ display: 'inline-block', padding: '1px 6px', borderRadius: 4, fontSize: 10, fontWeight: 600, marginLeft: 4,
121
+ backgroundColor: type === 'required' ? '#dbeafe' : type === 'installed' ? '#d1fae5' :
122
+ type === 'new' ? '#f3e8ff' : '#fef3c7',
123
+ color: type === 'required' ? '#1e40af' : type === 'installed' ? '#065f46' :
124
+ type === 'new' ? '#6b21a8' : '#92400e',
125
+ }),
126
+ // Dialect card
127
+ dialectCard: (selected, premium) => ({
128
+ padding: 16, borderRadius: 8, textAlign: 'center',
129
+ border: `2px solid ${!premium && selected ? '#0284c7' : '#e5e7eb'}`,
130
+ backgroundColor: !premium && selected ? '#f0f9ff' : '#fff',
131
+ cursor: premium ? 'not-allowed' : 'pointer',
132
+ opacity: premium ? 0.4 : 1,
133
+ filter: premium ? 'grayscale(0.5)' : 'none',
134
+ transition: 'all 0.2s',
135
+ }),
136
+ dialectIcon: { fontSize: 24, marginBottom: 4 },
137
+ dialectName: { fontSize: 12, fontWeight: 600, color: '#111827' },
138
+ // Alert
139
+ alert: (type) => ({
140
+ padding: 12, borderRadius: 8, marginBottom: 16, fontSize: 13,
141
+ backgroundColor: type === 'success' ? '#d1fae5' : type === 'error' ? '#fee2e2' :
142
+ type === 'warning' ? '#fef3c7' : '#dbeafe',
143
+ color: type === 'success' ? '#065f46' : type === 'error' ? '#991b1b' :
144
+ type === 'warning' ? '#92400e' : '#1e40af',
145
+ border: `1px solid ${type === 'success' ? '#a7f3d0' : type === 'error' ? '#fecaca' :
146
+ type === 'warning' ? '#fde68a' : '#bfdbfe'}`,
147
+ }),
148
+ // Summary card
149
+ summaryCard: { border: '1px solid #e5e7eb', borderRadius: 8, padding: 16, marginBottom: 12 },
150
+ summaryTitle: { fontWeight: 600, fontSize: 14, color: '#374151', marginBottom: 8 },
151
+ summaryText: { fontSize: 13, color: '#6b7280' },
152
+ // Checkbox row
153
+ checkRow: { display: 'flex', alignItems: 'flex-start', gap: 12, padding: '8px 0' },
154
+ checkbox: { width: 16, height: 16, marginTop: 2, cursor: 'pointer', accentColor: '#0284c7' },
155
+ // Nav row
156
+ navRow: { display: 'flex', justifyContent: 'space-between', paddingTop: 16 },
157
+ // JAR section
158
+ jarBox: { padding: 16, borderRadius: 8, backgroundColor: '#f0f9ff', border: '1px solid #bae6fd', marginTop: 12 },
159
+ jarTitle: { fontSize: 13, fontWeight: 600, color: '#0369a1', marginBottom: 8 },
160
+ // Module badges
161
+ moduleBadges: { display: 'flex', alignItems: 'center', gap: 4 },
162
+ // Center
163
+ center: { textAlign: 'center' },
164
+ // Spacer
165
+ spacer: (px) => ({ height: px }),
166
+ // Flex
167
+ flex: (gap = 8) => ({ display: 'flex', alignItems: 'center', gap }),
168
+ flexBetween: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
169
+ flexWrap: { display: 'flex', flexWrap: 'wrap', gap: 8 },
170
+ };
171
+ // ── Helpers ──────────────────────────────────────────────────
172
+ function resolveModuleDeps(selected, all) {
173
+ const set = new Set(selected);
174
+ let changed = true;
175
+ while (changed) {
176
+ changed = false;
177
+ for (const m of all) {
178
+ if (set.has(m.key) && m.dependsOn) {
179
+ for (const dep of m.dependsOn) {
180
+ if (!set.has(dep)) {
181
+ set.add(dep);
182
+ changed = true;
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ return Array.from(set);
189
+ }
190
+ async function safeJson(res) {
191
+ try {
192
+ return JSON.parse(await res.text());
193
+ }
194
+ catch {
195
+ return null;
196
+ }
197
+ }
198
+ async function fetchRetry(url, init, retries = 3, delay = 2000) {
199
+ for (let i = 0; i < retries; i++) {
200
+ try {
201
+ const res = await fetch(url, init);
202
+ if (res.status === 404 && i < retries - 1) {
203
+ await new Promise(r => setTimeout(r, delay));
204
+ continue;
205
+ }
206
+ return res;
207
+ }
208
+ catch (err) {
209
+ if (i < retries - 1) {
210
+ await new Promise(r => setTimeout(r, delay));
211
+ continue;
212
+ }
213
+ throw err;
214
+ }
215
+ }
216
+ return fetch(url, init);
217
+ }
218
+ // ── JAR Upload Sub-component ─────────────────────────────────
219
+ function JarUploadInline({ dialect, jarEndpoint }) {
220
+ const [uploading, setUploading] = useState(false);
221
+ const [message, setMessage] = useState(null);
222
+ const [jarStatus, setJarStatus] = useState(null);
223
+ useEffect(() => {
224
+ fetch(jarEndpoint)
225
+ .then(r => r.json())
226
+ .then(data => {
227
+ if (data.ok) {
228
+ const s = data.dialects?.find((d) => d.dialect === dialect);
229
+ setJarStatus(s || { hasJar: false, jarFile: null });
230
+ }
231
+ })
232
+ .catch(() => { });
233
+ }, [dialect, jarEndpoint]);
234
+ const handleUpload = async (e) => {
235
+ const file = e.target.files?.[0];
236
+ if (!file || !file.name.endsWith('.jar'))
237
+ return;
238
+ setUploading(true);
239
+ setMessage(null);
240
+ try {
241
+ const fd = new FormData();
242
+ fd.append('jar', file);
243
+ const res = await fetch(jarEndpoint, { method: 'POST', body: fd });
244
+ const result = await res.json();
245
+ if (result.ok) {
246
+ setMessage({ ok: true, text: `${result.fileName} uploade` });
247
+ setJarStatus({ hasJar: true, jarFile: result.fileName });
248
+ }
249
+ else {
250
+ setMessage({ ok: false, text: result.error || 'Erreur' });
251
+ }
252
+ }
253
+ catch {
254
+ setMessage({ ok: false, text: 'Erreur reseau' });
255
+ }
256
+ finally {
257
+ setUploading(false);
258
+ e.target.value = '';
259
+ }
260
+ };
261
+ return (_jsxs("div", { style: S.jarBox, children: [_jsxs("div", { style: S.flex(8), children: [_jsx("span", { style: S.jarTitle, children: "Driver JDBC" }), jarStatus?.hasJar ? (_jsx("span", { style: { ...S.badge('installed'), marginLeft: 0 }, children: jarStatus.jarFile })) : (_jsx("span", { style: { fontSize: 12, color: '#6b7280' }, children: "Aucun JAR installe" }))] }), _jsxs("div", { style: { ...S.flex(12), marginTop: 8 }, children: [_jsxs("label", { style: { ...S.btn('primary', uploading), cursor: uploading ? 'wait' : 'pointer', fontSize: 12, padding: '6px 12px' }, children: [uploading ? 'Upload...' : 'Uploader un .jar', _jsx("input", { type: "file", accept: ".jar", onChange: handleUpload, disabled: uploading, style: { display: 'none' } })] }), _jsx("span", { style: { fontSize: 11, color: '#9ca3af' }, children: "Ex: hsqldb*.jar, ojdbc*.jar, db2jcc*.jar" })] }), message && (_jsx("p", { style: { fontSize: 12, color: message.ok ? '#059669' : '#dc2626', marginTop: 8 }, children: message.text }))] }));
262
+ }
263
+ // ── Main Component ───────────────────────────────────────────
264
+ export default function SetupWizard({ t: tProp, onComplete, endpoints = {}, dbNamePrefix = 'mydb', persistState = true, }) {
265
+ const t = tProp || ((k) => k);
266
+ const ep = {
267
+ detectModules: endpoints.detectModules || '/api/setup/detect-modules',
268
+ testDb: endpoints.testDb || '/api/setup/test-db',
269
+ installModules: endpoints.installModules || '/api/setup/install-modules',
270
+ install: endpoints.install || '/api/setup/install',
271
+ uploadJar: endpoints.uploadJar || '/api/setup/upload-jar',
272
+ };
273
+ // --- State ---
274
+ const [currentStep, setCurrentStep] = useState(0);
275
+ const [dialect, setDialect] = useState('mongodb');
276
+ const [dbConfig, setDbConfig] = useState({ ...DIALECT_DEFAULTS.mongodb, name: `${dbNamePrefix}_prod` });
277
+ const [dbTestResult, setDbTestResult] = useState(null);
278
+ const [dbTesting, setDbTesting] = useState(false);
279
+ const [adminConfig, setAdminConfig] = useState({ firstName: '', lastName: '', email: '', password: '', confirmPassword: '' });
280
+ const [seedOptions, setSeedOptions] = useState({ activities: true, demoUsers: false, demoData: false });
281
+ const [availableModules, setAvailableModules] = useState([]);
282
+ const [selectedModules, setSelectedModules] = useState([]);
283
+ const [detectedModules, setDetectedModules] = useState([]);
284
+ const [modulesDetected, setModulesDetected] = useState(false);
285
+ const [installing, setInstalling] = useState(false);
286
+ const [installResult, setInstallResult] = useState(null);
287
+ const [hydrated, setHydrated] = useState(false);
288
+ const step = STEPS[currentStep];
289
+ // --- Persist / Restore ---
290
+ useEffect(() => {
291
+ if (!persistState) {
292
+ setHydrated(true);
293
+ return;
294
+ }
295
+ try {
296
+ const raw = sessionStorage.getItem(STORAGE_KEY);
297
+ if (raw) {
298
+ const saved = JSON.parse(raw);
299
+ if (saved.currentStep != null)
300
+ setCurrentStep(saved.currentStep);
301
+ if (saved.dialect)
302
+ setDialect(saved.dialect);
303
+ if (saved.dbConfig)
304
+ setDbConfig(saved.dbConfig);
305
+ if (saved.adminConfig)
306
+ setAdminConfig(saved.adminConfig);
307
+ if (saved.seedOptions)
308
+ setSeedOptions(saved.seedOptions);
309
+ if (saved.selectedModules?.length)
310
+ setSelectedModules(saved.selectedModules);
311
+ }
312
+ }
313
+ catch { }
314
+ setHydrated(true);
315
+ }, [persistState]);
316
+ useEffect(() => {
317
+ if (!hydrated || !persistState)
318
+ return;
319
+ try {
320
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ currentStep, dialect, dbConfig, adminConfig, seedOptions, selectedModules }));
321
+ }
322
+ catch { }
323
+ }, [hydrated, persistState, currentStep, dialect, dbConfig, adminConfig, seedOptions, selectedModules]);
324
+ // --- Detect modules ---
325
+ useEffect(() => {
326
+ fetch(ep.detectModules)
327
+ .then(r => r.json())
328
+ .then((data) => {
329
+ if (data.modules)
330
+ setAvailableModules(data.modules);
331
+ if (data.installed)
332
+ setDetectedModules(data.installed);
333
+ if (selectedModules.length === 0) {
334
+ const mods = data.modules || [];
335
+ const pre = new Set([
336
+ ...mods.filter(m => m.required || m.default).map(m => m.key),
337
+ ...(data.installed || []),
338
+ ]);
339
+ setSelectedModules(Array.from(pre));
340
+ }
341
+ setModulesDetected(true);
342
+ })
343
+ .catch(() => setModulesDetected(true));
344
+ // eslint-disable-next-line react-hooks/exhaustive-deps
345
+ }, []);
346
+ // --- Module toggle ---
347
+ const toggleModule = useCallback((key) => {
348
+ const mod = availableModules.find(m => m.key === key);
349
+ if (!mod || mod.required)
350
+ return;
351
+ setSelectedModules(prev => {
352
+ if (prev.includes(key)) {
353
+ const toRemove = new Set([key]);
354
+ let changed = true;
355
+ while (changed) {
356
+ changed = false;
357
+ for (const m of availableModules) {
358
+ if (toRemove.has(m.key) || m.required)
359
+ continue;
360
+ if (m.dependsOn?.some(dep => toRemove.has(dep)) && prev.includes(m.key)) {
361
+ toRemove.add(m.key);
362
+ changed = true;
363
+ }
364
+ }
365
+ }
366
+ return prev.filter(k => !toRemove.has(k));
367
+ }
368
+ else {
369
+ return resolveModuleDeps([...prev, key], availableModules);
370
+ }
371
+ });
372
+ }, [availableModules]);
373
+ // --- Dialect select ---
374
+ function selectDialect(d) {
375
+ setDialect(d);
376
+ const defaults = DIALECT_DEFAULTS[d];
377
+ setDbConfig({ ...defaults, name: defaults.name === 'mydb' ? dbNamePrefix : defaults.name === 'mydb_prod' ? `${dbNamePrefix}_prod` : defaults.name.replace('mydb', dbNamePrefix) });
378
+ setDbTestResult(null);
379
+ }
380
+ // --- Test DB ---
381
+ async function testDb() {
382
+ setDbTesting(true);
383
+ setDbTestResult(null);
384
+ try {
385
+ const res = await fetch(ep.testDb, {
386
+ method: 'POST',
387
+ headers: { 'Content-Type': 'application/json' },
388
+ body: JSON.stringify({ dialect, ...dbConfig }),
389
+ });
390
+ const data = await res.json();
391
+ setDbTestResult(res.ok ? data : { ok: false, error: data.error?.message || 'Erreur' });
392
+ }
393
+ catch (err) {
394
+ setDbTestResult({ ok: false, error: err instanceof Error ? err.message : 'Erreur' });
395
+ }
396
+ setDbTesting(false);
397
+ }
398
+ // --- Install ---
399
+ async function runInstallation() {
400
+ setInstalling(true);
401
+ setInstallResult(null);
402
+ try {
403
+ const modRes = await fetch(ep.installModules, {
404
+ method: 'POST',
405
+ headers: { 'Content-Type': 'application/json' },
406
+ body: JSON.stringify({ modules: selectedModules }),
407
+ });
408
+ if (!modRes.ok) {
409
+ const d = await safeJson(modRes);
410
+ setInstallResult({ ok: false, error: d?.error?.message || 'Erreur installation modules' });
411
+ setInstalling(false);
412
+ return;
413
+ }
414
+ const res = await fetchRetry(ep.install, {
415
+ method: 'POST',
416
+ headers: { 'Content-Type': 'application/json' },
417
+ body: JSON.stringify({
418
+ dialect,
419
+ db: dbConfig,
420
+ admin: { email: adminConfig.email, password: adminConfig.password, firstName: adminConfig.firstName, lastName: adminConfig.lastName },
421
+ seed: seedOptions,
422
+ modules: selectedModules,
423
+ }),
424
+ });
425
+ const data = await safeJson(res);
426
+ if (res.ok && data) {
427
+ setInstallResult({ ok: true, needsRestart: data.data?.needsRestart });
428
+ }
429
+ else {
430
+ setInstallResult({ ok: false, error: data?.error?.message || `Erreur installation (HTTP ${res.status})` });
431
+ }
432
+ }
433
+ catch (err) {
434
+ setInstallResult({ ok: false, error: err instanceof Error ? err.message : 'Erreur' });
435
+ }
436
+ setInstalling(false);
437
+ }
438
+ // --- Navigation ---
439
+ function canGoNext() {
440
+ switch (step) {
441
+ case 'welcome': return true;
442
+ case 'modules': return selectedModules.length > 0;
443
+ case 'dialect': return true;
444
+ case 'database':
445
+ if (dialect === 'sqlite' || dialect === 'spanner')
446
+ return dbConfig.name.trim() !== '';
447
+ return dbTestResult?.ok === true;
448
+ case 'admin':
449
+ return adminConfig.firstName.trim() !== '' && adminConfig.lastName.trim() !== '' &&
450
+ adminConfig.email.trim() !== '' && adminConfig.password.length >= 6 &&
451
+ adminConfig.password === adminConfig.confirmPassword;
452
+ case 'summary': return false;
453
+ }
454
+ }
455
+ function goNext() { if (currentStep < STEPS.length - 1)
456
+ setCurrentStep(currentStep + 1); }
457
+ function goBack() { if (currentStep > 0)
458
+ setCurrentStep(currentStep - 1); }
459
+ function dbSummaryLabel() {
460
+ if (dialect === 'sqlite')
461
+ return `SQLite — ./data/${dbConfig.name}.db`;
462
+ if (dialect === 'spanner')
463
+ return `Cloud Spanner — ${dbConfig.name}`;
464
+ const info = DIALECT_INFO.find(d => d.key === dialect);
465
+ return `${info?.name || dialect} — ${dbConfig.host}:${dbConfig.port}/${dbConfig.name}`;
466
+ }
467
+ function handleComplete() {
468
+ if (persistState) {
469
+ try {
470
+ sessionStorage.removeItem(STORAGE_KEY);
471
+ }
472
+ catch { }
473
+ }
474
+ onComplete?.();
475
+ }
476
+ // ── Render ─────────────────────────────────────────────────
477
+ return (_jsx("div", { style: S.wrapper, children: _jsxs("div", { style: S.container, children: [_jsx("div", { style: S.stepperRow, children: STEPS.map((s, i) => (_jsxs("div", { style: S.flex(8), children: [_jsx("div", { style: S.stepCircle(i < currentStep ? 'done' : i === currentStep ? 'current' : 'future'), children: i < currentStep ? '✓' : i + 1 }), _jsx("span", { style: S.stepLabel(i === currentStep), children: t(`setup.steps.${s}`) }), i < STEPS.length - 1 && _jsx("div", { style: S.stepLine(i < currentStep) })] }, s))) }), _jsxs("div", { style: S.card, children: [step === 'welcome' && (_jsxs("div", { style: S.center, children: [_jsx("div", { style: { width: 80, height: 80, borderRadius: '50%', backgroundColor: '#e0f2fe', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 24px', fontSize: 40 }, children: "\uD83D\uDEE1\uFE0F" }), _jsx("h2", { style: { fontSize: 24, fontWeight: 700, color: '#111827', marginBottom: 8 }, children: t('setup.welcome.title') }), _jsx("p", { style: { color: '#6b7280', marginBottom: 24 }, children: t('setup.welcome.description') }), _jsxs("button", { style: S.btn('lg'), onClick: goNext, children: [t('setup.welcome.start'), " \u2192"] })] })), step === 'modules' && (_jsxs("div", { children: [_jsxs("div", { style: S.sectionHeader, children: [_jsx("span", { style: S.sectionIcon, children: "\uD83D\uDCE6" }), _jsxs("div", { children: [_jsx("div", { style: S.sectionTitle, children: t('setup.modules.title') }), _jsx("div", { style: S.sectionDesc, children: t('setup.modules.description') })] })] }), _jsx("div", { style: S.grid2, children: availableModules.map(mod => {
478
+ const isSelected = selectedModules.includes(mod.key);
479
+ const isDetected = detectedModules.includes(mod.key);
480
+ return (_jsxs("div", { style: S.moduleCard(isSelected, !!mod.required), onClick: () => toggleModule(mod.key), children: [_jsxs("div", { style: S.moduleHeader, children: [_jsxs("div", { style: S.moduleLeft, children: [_jsx("span", { style: { fontSize: 20 }, children: mod.icon }), _jsx("span", { style: S.moduleName, children: mod.label })] }), _jsxs("div", { style: S.moduleBadges, children: [mod.discovered && _jsx("span", { style: S.badge('new'), children: "Nouveau" }), isDetected && _jsx("span", { style: S.badge('installed'), children: t('setup.modules.installed') }), mod.required && _jsx("span", { style: S.badge('required'), children: t('setup.modules.required') }), _jsx("input", { type: "checkbox", checked: isSelected, disabled: mod.required, readOnly: true, style: S.checkbox })] })] }), _jsx("div", { style: S.moduleDesc, children: mod.description }), mod.dependsOn?.length ? (_jsxs("div", { style: { fontSize: 11, color: '#9ca3af', marginTop: 4 }, children: ["Depend de : ", mod.dependsOn.join(', ')] })) : null] }, mod.key));
481
+ }) }), _jsxs("div", { style: S.navRow, children: [_jsxs("button", { style: S.btn('outline'), onClick: goBack, children: ["\u2190 ", t('setup.back')] }), _jsxs("button", { style: S.btn('primary', !canGoNext()), onClick: goNext, disabled: !canGoNext(), children: [t('setup.next'), " \u2192"] })] })] })), step === 'dialect' && (_jsxs("div", { children: [_jsxs("div", { style: S.sectionHeader, children: [_jsx("span", { style: S.sectionIcon, children: "\uD83D\uDCBE" }), _jsxs("div", { children: [_jsx("div", { style: S.sectionTitle, children: t('setup.dialect.title') }), _jsx("div", { style: S.sectionDesc, children: t('setup.dialect.description') })] })] }), _jsx("div", { style: S.grid3, children: DIALECT_INFO.map(d => (_jsxs("div", { style: S.dialectCard(dialect === d.key, !!d.premium), onClick: () => !d.premium && selectDialect(d.key), title: d.premium ? `${d.name} — disponible en version Premium` : d.name, children: [_jsx("div", { style: S.dialectIcon, children: d.icon }), _jsx("div", { style: S.dialectName, children: d.name }), d.premium && (_jsx("span", { style: { ...S.badge('premium'), marginLeft: 0, marginTop: 4 }, children: "Premium" }))] }, d.key))) }), _jsxs("div", { style: S.navRow, children: [_jsxs("button", { style: S.btn('outline'), onClick: goBack, children: ["\u2190 ", t('setup.back')] }), _jsxs("button", { style: S.btn('primary'), onClick: goNext, children: [t('setup.next'), " \u2192"] })] })] })), step === 'database' && (_jsxs("div", { children: [_jsxs("div", { style: S.sectionHeader, children: [_jsx("span", { style: S.sectionIcon, children: "\uD83D\uDDC4\uFE0F" }), _jsxs("div", { children: [_jsx("div", { style: S.sectionTitle, children: t('setup.database.title') }), _jsx("div", { style: S.sectionDesc, children: t('setup.database.description') })] })] }), dialect === 'sqlite' ? (_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.name') }), _jsx("input", { style: S.input, value: dbConfig.name, onChange: e => { setDbConfig({ ...dbConfig, name: e.target.value }); setDbTestResult(null); }, placeholder: dbNamePrefix }), _jsxs("p", { style: { fontSize: 11, color: '#9ca3af', marginTop: 4 }, children: [t('setup.database.sqliteInfo'), " ", _jsxs("code", { style: { fontFamily: 'monospace', backgroundColor: '#f3f4f6', padding: '1px 4px', borderRadius: 3 }, children: ["./data/", dbConfig.name, ".db"] })] })] })) : dialect === 'spanner' ? (_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.spannerPath') }), _jsx("input", { style: S.input, value: dbConfig.name, onChange: e => { setDbConfig({ ...dbConfig, name: e.target.value }); setDbTestResult(null); }, placeholder: "my-project/my-instance/mydb" }), _jsx("p", { style: { fontSize: 11, color: '#9ca3af', marginTop: 4 }, children: t('setup.database.spannerInfo') })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { style: S.formRow, children: [_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.host') }), _jsx("input", { style: S.input, value: dbConfig.host, onChange: e => { setDbConfig({ ...dbConfig, host: e.target.value }); setDbTestResult(null); } })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.port') }), _jsx("input", { style: S.input, type: "number", value: dbConfig.port, onChange: e => { setDbConfig({ ...dbConfig, port: parseInt(e.target.value) || 0 }); setDbTestResult(null); } })] })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.name') }), _jsx("input", { style: S.input, value: dbConfig.name, onChange: e => { setDbConfig({ ...dbConfig, name: e.target.value }); setDbTestResult(null); } })] }), dialect !== 'hsqldb' && (_jsxs("div", { style: S.formRow, children: [_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.user') }), _jsx("input", { style: S.input, value: dbConfig.user, onChange: e => { setDbConfig({ ...dbConfig, user: e.target.value }); setDbTestResult(null); } })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.database.password') }), _jsx("input", { style: S.input, type: "password", value: dbConfig.password, onChange: e => { setDbConfig({ ...dbConfig, password: e.target.value }); setDbTestResult(null); } })] })] }))] })), dialect !== 'mongodb' && dialect !== 'sqlite' && (_jsxs("div", { style: { ...S.alert('warning'), marginTop: 12 }, children: [t('setup.database.driverHint'), ' ', _jsx("code", { style: { fontFamily: 'monospace', backgroundColor: '#fef3c7', padding: '1px 4px', borderRadius: 3 }, children: DRIVER_HINTS[dialect] })] })), JDBC_DIALECTS.includes(dialect) && (_jsx(JarUploadInline, { dialect: dialect, jarEndpoint: ep.uploadJar })), dialect !== 'sqlite' && dialect !== 'spanner' && (_jsxs("div", { style: { ...S.flex(16), marginTop: 16 }, children: [_jsxs("button", { style: S.btn('outline', dbTesting), onClick: testDb, disabled: dbTesting, children: [dbTesting ? '⏳ ' : '🗄️ ', dbTesting ? t('setup.database.testing') : t('setup.database.test')] }), dbTestResult && (_jsx("span", { style: { fontSize: 13, color: dbTestResult.ok ? '#059669' : '#dc2626' }, children: dbTestResult.ok
482
+ ? `✅ ${t('setup.database.success')}${dbTestResult.dbVersion ? ` (v${dbTestResult.dbVersion})` : ''}`
483
+ : `❌ ${t('setup.database.error')}: ${dbTestResult.error}` }))] })), _jsxs("div", { style: S.navRow, children: [_jsxs("button", { style: S.btn('outline'), onClick: goBack, children: ["\u2190 ", t('setup.back')] }), _jsxs("button", { style: S.btn('primary', !canGoNext()), onClick: goNext, disabled: !canGoNext(), children: [t('setup.next'), " \u2192"] })] })] })), step === 'admin' && (_jsxs("div", { children: [_jsxs("div", { style: S.sectionHeader, children: [_jsx("span", { style: S.sectionIcon, children: "\uD83D\uDC64" }), _jsxs("div", { children: [_jsx("div", { style: S.sectionTitle, children: t('setup.admin.title') }), _jsx("div", { style: S.sectionDesc, children: t('setup.admin.description') })] })] }), _jsxs("div", { style: S.formRow, children: [_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.firstName') }), _jsx("input", { style: S.input, value: adminConfig.firstName, onChange: e => setAdminConfig({ ...adminConfig, firstName: e.target.value }) })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.lastName') }), _jsx("input", { style: S.input, value: adminConfig.lastName, onChange: e => setAdminConfig({ ...adminConfig, lastName: e.target.value }) })] })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.email') }), _jsx("input", { style: S.input, type: "email", value: adminConfig.email, onChange: e => setAdminConfig({ ...adminConfig, email: e.target.value }), placeholder: "admin@example.com" })] }), _jsxs("div", { style: S.formRow, children: [_jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.password') }), _jsx("input", { style: S.input, type: "password", value: adminConfig.password, onChange: e => setAdminConfig({ ...adminConfig, password: e.target.value }) })] }), _jsxs("div", { style: S.formGroup, children: [_jsx("label", { style: S.label, children: t('setup.admin.confirmPassword') }), _jsx("input", { style: S.input, type: "password", value: adminConfig.confirmPassword, onChange: e => setAdminConfig({ ...adminConfig, confirmPassword: e.target.value }) })] })] }), adminConfig.password && adminConfig.confirmPassword && adminConfig.password !== adminConfig.confirmPassword && (_jsx("p", { style: { fontSize: 13, color: '#dc2626' }, children: t('setup.admin.passwordMismatch') })), _jsxs("div", { style: S.navRow, children: [_jsxs("button", { style: S.btn('outline'), onClick: goBack, children: ["\u2190 ", t('setup.back')] }), _jsxs("button", { style: S.btn('primary', !canGoNext()), onClick: goNext, disabled: !canGoNext(), children: [t('setup.next'), " \u2192"] })] })] })), step === 'summary' && (_jsxs("div", { children: [_jsxs("div", { style: S.sectionHeader, children: [_jsx("span", { style: S.sectionIcon, children: "\u2699\uFE0F" }), _jsxs("div", { children: [_jsx("div", { style: S.sectionTitle, children: t('setup.summary.title') }), _jsx("div", { style: S.sectionDesc, children: t('setup.summary.description') })] })] }), _jsxs("div", { style: S.summaryCard, children: [_jsx("div", { style: S.summaryTitle, children: t('setup.summary.dbConfig') }), _jsxs("div", { style: S.summaryText, children: [_jsx("span", { style: { fontFamily: 'monospace' }, children: dbSummaryLabel() }), dialect !== 'sqlite' && dbConfig.user && _jsxs("span", { style: { display: 'block', marginTop: 4 }, children: ["Utilisateur: ", dbConfig.user] })] })] }), _jsxs("div", { style: S.summaryCard, children: [_jsx("div", { style: S.summaryTitle, children: t('setup.summary.adminConfig') }), _jsxs("div", { style: S.summaryText, children: [_jsxs("div", { children: [adminConfig.firstName, " ", adminConfig.lastName] }), _jsx("div", { children: adminConfig.email })] })] }), _jsxs("div", { style: S.summaryCard, children: [_jsx("div", { style: S.summaryTitle, children: t('setup.modules.title') }), _jsx("div", { style: S.flexWrap, children: selectedModules.map(key => {
484
+ const mod = availableModules.find(m => m.key === key);
485
+ return mod ? (_jsxs("span", { style: {
486
+ display: 'inline-flex', alignItems: 'center', gap: 4,
487
+ fontSize: 12, backgroundColor: '#f0f9ff', color: '#0369a1',
488
+ border: '1px solid #bae6fd', borderRadius: 16, padding: '4px 10px',
489
+ }, children: [mod.icon, " ", mod.label] }, key)) : null;
490
+ }) })] }), _jsxs("div", { style: S.summaryCard, children: [_jsx("div", { style: S.summaryTitle, children: t('setup.summary.seedTitle') }), _jsx("p", { style: { ...S.summaryText, marginBottom: 12 }, children: t('setup.summary.seedInfo') }), _jsxs("div", { style: S.checkRow, children: [_jsx("input", { type: "checkbox", style: S.checkbox, checked: seedOptions.activities, onChange: e => setSeedOptions({ ...seedOptions, activities: e.target.checked, demoData: e.target.checked ? seedOptions.demoData : false }), disabled: installing || !!installResult?.ok }), _jsxs("div", { children: [_jsx("div", { style: { fontSize: 13, fontWeight: 500 }, children: t('setup.summary.seedActivities') }), _jsx("div", { style: { fontSize: 12, color: '#9ca3af' }, children: t('setup.summary.seedActivitiesDesc') })] })] }), _jsxs("div", { style: S.checkRow, children: [_jsx("input", { type: "checkbox", style: S.checkbox, checked: seedOptions.demoUsers, onChange: e => setSeedOptions({ ...seedOptions, demoUsers: e.target.checked }), disabled: installing || !!installResult?.ok }), _jsxs("div", { children: [_jsx("div", { style: { fontSize: 13, fontWeight: 500 }, children: t('setup.summary.seedDemoUsers') }), _jsx("div", { style: { fontSize: 12, color: '#9ca3af' }, children: t('setup.summary.seedDemoUsersDesc') })] })] }), _jsxs("div", { style: S.checkRow, children: [_jsx("input", { type: "checkbox", style: S.checkbox, checked: seedOptions.demoData, onChange: e => setSeedOptions({ ...seedOptions, demoData: e.target.checked, activities: e.target.checked ? true : seedOptions.activities }), disabled: installing || !!installResult?.ok }), _jsxs("div", { children: [_jsx("div", { style: { fontSize: 13, fontWeight: 500 }, children: t('setup.summary.seedDemoData') }), _jsx("div", { style: { fontSize: 12, color: '#9ca3af' }, children: t('setup.summary.seedDemoDataDesc') })] })] })] }), installResult && (_jsx("div", { style: S.alert(installResult.ok ? 'success' : 'error'), children: installResult.ok ? (_jsxs(_Fragment, { children: [_jsxs("div", { style: { fontWeight: 600, marginBottom: 4 }, children: ["\u2705 ", t('setup.summary.success')] }), _jsx("div", { children: t('setup.summary.successDesc') }), installResult.needsRestart && (_jsx("div", { style: { color: '#92400e', fontWeight: 500, marginTop: 8 }, children: t('setup.summary.needsRestart') }))] })) : (_jsxs("div", { children: ["\u274C ", installResult.error] })) })), _jsx("div", { style: S.navRow, children: !installResult?.ok ? (_jsxs(_Fragment, { children: [_jsxs("button", { style: S.btn('outline', installing), onClick: goBack, disabled: installing, children: ["\u2190 ", t('setup.back')] }), _jsxs("button", { style: S.btn('primary', installing), onClick: runInstallation, disabled: installing, children: [installing ? '⏳ ' : '⚙️ ', installing ? t('setup.summary.installing') : t('setup.summary.install')] })] })) : (_jsx("div", { style: { width: '100%', textAlign: 'center' }, children: _jsxs("button", { style: S.btn('lg'), onClick: handleComplete, children: [t('setup.summary.goToLogin'), " \u2192"] }) })) })] }))] })] }) }));
491
+ }
package/dist/index.d.ts CHANGED
@@ -13,5 +13,6 @@ export { createInstallModulesHandler } from './api/install-modules.route';
13
13
  export { createReconfigHandlers } from './api/reconfig.route';
14
14
  export { createUploadJarHandlers } from './api/upload-jar.route';
15
15
  export { default as ReconfigPanel } from './components/ReconfigPanel';
16
+ export { default as SetupWizard } from './components/SetupWizard';
16
17
  export { setupMenuContribution } from './lib/menu';
17
18
  export type { DialectType, DialectInfo, DbConfig, InstallConfig, SeedOptions, SeedDefinition, MostaSetupConfig, ModuleDefinition, } from './types/index';
package/dist/index.js CHANGED
@@ -20,5 +20,6 @@ export { createReconfigHandlers } from './api/reconfig.route';
20
20
  export { createUploadJarHandlers } from './api/upload-jar.route';
21
21
  // Components
22
22
  export { default as ReconfigPanel } from './components/ReconfigPanel';
23
+ export { default as SetupWizard } from './components/SetupWizard';
23
24
  // Menu contribution
24
25
  export { setupMenuContribution } from './lib/menu';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/setup",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Reusable setup wizard module — multi-dialect DB configuration, .env.local writer, seed runner",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "MIT",
@@ -43,6 +43,11 @@
43
43
  "import": "./dist/components/ReconfigPanel.js",
44
44
  "default": "./dist/components/ReconfigPanel.js"
45
45
  },
46
+ "./components/SetupWizard": {
47
+ "types": "./dist/components/SetupWizard.d.ts",
48
+ "import": "./dist/components/SetupWizard.js",
49
+ "default": "./dist/components/SetupWizard.js"
50
+ },
46
51
  "./api/reconfig": {
47
52
  "types": "./dist/api/reconfig.route.d.ts",
48
53
  "import": "./dist/api/reconfig.route.js",
@@ -81,7 +86,7 @@
81
86
  "prepublishOnly": "npm run build"
82
87
  },
83
88
  "dependencies": {
84
- "@mostajs/orm": "^1.3.0",
89
+ "@mostajs/orm": "^1.3.1",
85
90
  "bcryptjs": "^2.4.3"
86
91
  },
87
92
  "devDependencies": {