create-numz-app 1.0.0 → 1.0.1

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
@@ -31,7 +31,7 @@ npm link # makes `project-gen` available globally
31
31
  ## Usage
32
32
 
33
33
  ```bash
34
- project-gen
34
+ npm create numz-app
35
35
  ```
36
36
 
37
37
  The CLI will ask you:
package/bin/cli.js CHANGED
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // bin/cli.js — interactive CLI
3
-
4
2
  import prompts from 'prompts';
5
3
  import chalk from 'chalk';
6
4
  import ora from 'ora';
7
5
  import path from 'path';
6
+ import { parseSQL } from '../src/parser.js';
8
7
  import { generateProject } from '../src/index.js';
9
8
 
10
9
  console.log('');
@@ -13,63 +12,62 @@ console.log(chalk.bold.blue('║ numz — Full-Stack Builder ║'));
13
12
  console.log(chalk.bold.blue('╚══════════════════════════════════════╝'));
14
13
  console.log(chalk.gray(' Paste SQL → get Express + React CRUD app\n'));
15
14
 
16
- const answers = await prompts([
17
- {
18
- type: 'text',
19
- name: 'projectName',
20
- message: 'Project name (folder to create):',
21
- initial: 'my-app',
22
- validate: (v) => /^[\w-]+$/.test(v.trim()) || 'Letters, numbers, hyphens only',
23
- },
24
- {
25
- type: 'text',
26
- name: 'dbName',
27
- message: 'MySQL database name:',
28
- validate: (v) => v.trim().length > 0 || 'Required',
29
- },
30
- {
31
- type: 'text',
32
- name: 'outputDir',
33
- message: 'Output directory (blank = current folder):',
34
- initial: '',
35
- },
36
- {
37
- type: 'text',
38
- name: 'sql',
15
+ const base = await prompts([
16
+ { type:'text', name:'projectName', message:'Project name:', initial:'my-app',
17
+ validate:(v)=>/^[\w-]+$/.test(v.trim())||'Letters, numbers, hyphens only' },
18
+ { type:'text', name:'dbName', message:'MySQL database name:',
19
+ validate:(v)=>v.trim().length>0||'Required' },
20
+ { type:'text', name:'outputDir', message:'Output directory (blank = current folder):', initial:'' },
21
+ { type:'text', name:'sql',
39
22
  message: chalk.yellow('Paste your SQL CREATE TABLE statements then press Enter:'),
40
- validate: (v) =>
41
- v.toLowerCase().includes('create table') || 'Must include at least one CREATE TABLE',
42
- },
43
- ], {
44
- onCancel: () => { console.log(chalk.red('\nCancelled.')); process.exit(1); },
45
- });
23
+ validate:(v)=>v.toLowerCase().includes('create table')||'Must include at least one CREATE TABLE' },
24
+ ], { onCancel:()=>{ console.log(chalk.red('\nCancelled.')); process.exit(1); } });
25
+
26
+ // Parse SQL now so we can offer table choices for reports
27
+ const parsedTables = parseSQL(base.sql.trim());
28
+ if (!parsedTables.length) {
29
+ console.log(chalk.red('No CREATE TABLE statements detected. Exiting.')); process.exit(1);
30
+ }
31
+
32
+ const reportsQ = await prompts([
33
+ { type:'confirm', name:'wantReports', message:'Generate a Reports page?', initial:true },
34
+ ], { onCancel:()=>{ console.log(chalk.red('\nCancelled.')); process.exit(1); } });
35
+
36
+ let reportTables = [];
46
37
 
47
- const outputDir = answers.outputDir.trim()
48
- ? path.resolve(answers.outputDir.trim())
49
- : process.cwd();
38
+ if (reportsQ.wantReports) {
39
+ const sel = await prompts([
40
+ { type:'multiselect', name:'reportTables',
41
+ message:'Which tables should appear in reports? (Space to select)',
42
+ choices: parsedTables.map((t)=>({ title:t.tableName, value:t.tableName })),
43
+ min:1, hint:'Space to select, Enter to confirm', instructions:false },
44
+ ], { onCancel:()=>{ console.log(chalk.red('\nCancelled.')); process.exit(1); } });
50
45
 
46
+ reportTables = sel.reportTables || [];
47
+ if (!reportTables.length) { console.log(chalk.yellow('No tables selected — skipping Reports page.')); reportsQ.wantReports = false; }
48
+ }
49
+
50
+ const outputDir = base.outputDir.trim() ? path.resolve(base.outputDir.trim()) : process.cwd();
51
51
  console.log('');
52
52
  const spinner = ora('Generating project files…').start();
53
53
 
54
54
  try {
55
55
  const { baseDir, files } = generateProject({
56
- dbName: answers.dbName.trim(),
57
- sql: answers.sql.trim(),
58
- projectName: answers.projectName.trim(),
56
+ dbName: base.dbName.trim(),
57
+ sql: base.sql.trim(),
58
+ projectName: base.projectName.trim(),
59
59
  outputDir,
60
+ withReports: reportsQ.wantReports,
61
+ reportTables,
60
62
  });
61
63
 
62
- spinner.succeed(chalk.green(`Done! ${files.length} files written.\n`));
63
-
64
+ spinner.succeed(chalk.green('Done! ' + files.length + ' files written.\n'));
64
65
  console.log(chalk.bold('📁 ' + baseDir));
65
66
  for (const f of files) console.log(chalk.gray(' ├── ') + chalk.cyan(f));
66
-
67
67
  console.log(chalk.bold.yellow('\n▶ Next steps:\n'));
68
- console.log(` cd ${answers.projectName}/backend → npm install → edit .env → npm run dev`);
69
- console.log(` cd ${answers.projectName}/frontend → npm install → npm run dev`);
70
- console.log(`\n Then open ${chalk.bold('http://localhost:5173')}\n`);
71
-
68
+ console.log(' cd ' + base.projectName + '/backend → npm install → edit .env → npm run dev');
69
+ console.log(' cd ' + base.projectName + '/frontend → npm install → npm run dev');
70
+ console.log('\n Open ' + chalk.bold('http://localhost:5173') + '\n');
72
71
  } catch (err) {
73
- spinner.fail(chalk.red('Error: ' + err.message));
74
- process.exit(1);
72
+ spinner.fail(chalk.red('Error: ' + err.message)); process.exit(1);
75
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-numz-app",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Generate a full Express + React CRUD project from a DB name and SQL CREATE TABLE statements.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,8 @@
15
15
  "ora": "^8.1.0",
16
16
  "prompts": "^2.4.2"
17
17
  },
18
- "engines": { "node": ">=18.0.0" },
18
+ "engines": {
19
+ "node": ">=18.0.0"
20
+ },
19
21
  "license": "MIT"
20
- }
22
+ }
@@ -70,7 +70,7 @@ export default router;
70
70
  `;
71
71
  }
72
72
 
73
- export function generateServer(tables) {
73
+ export function generateServer(tables, withReports) {
74
74
  const imports = tables
75
75
  .map((t) => `import ${camel(t.tableName)}Router from './routes/${t.tableName.toLowerCase()}.js';`)
76
76
  .join('\n');
@@ -78,10 +78,14 @@ export function generateServer(tables) {
78
78
  .map((t) => `app.use('/api/${t.tableName.toLowerCase()}', ${camel(t.tableName)}Router);`)
79
79
  .join('\n');
80
80
 
81
+ const reportsImport = withReports ? `import reportsRouter from './routes/reports.js';` : '';
82
+ const reportsUse = withReports ? `app.use('/api/reports', reportsRouter);` : '';
83
+
81
84
  return `import express from 'express';
82
85
  import cors from 'cors';
83
86
  import dotenv from 'dotenv';
84
87
  ${imports}
88
+ ${reportsImport}
85
89
 
86
90
  dotenv.config();
87
91
  const app = express();
@@ -90,12 +94,75 @@ app.use(cors({ origin: 'http://localhost:5173', credentials: true }));
90
94
  app.use(express.json());
91
95
 
92
96
  ${uses}
97
+ ${reportsUse}
93
98
 
94
99
  const PORT = process.env.PORT || 5000;
95
100
  app.listen(PORT, () => console.log('Server running on port ' + PORT));
96
101
  `;
97
102
  }
98
103
 
104
+ // Generates backend/routes/reports.js
105
+ // Security: only whitelisted tables can be queried
106
+ // Date columns are auto-detected from the schema and used for range filtering
107
+ export function generateReportsRoute(reportTables, allTables) {
108
+ // Build { tablename: 'DateColumnName' } from schema
109
+ const dateCols = {};
110
+ for (const name of reportTables) {
111
+ const t = allTables.find((t) => t.tableName.toLowerCase() === name.toLowerCase());
112
+ if (t) {
113
+ const dc = t.columns.find((c) => c.type === 'date' || c.type === 'datetime');
114
+ dateCols[t.tableName.toLowerCase()] = dc ? dc.name : null;
115
+ }
116
+ }
117
+
118
+ const allowed = JSON.stringify(reportTables.map((n) => n.toLowerCase()));
119
+ const dateColMap = JSON.stringify(dateCols, null, 2);
120
+
121
+ return `import express from 'express';
122
+ import db from '../db.js';
123
+
124
+ const router = express.Router();
125
+
126
+ // Only these tables can be queried via reports (security whitelist)
127
+ const ALLOWED = ${allowed};
128
+
129
+ // Auto-detected date column per table — used for date range filtering
130
+ const DATE_COLS = ${dateColMap};
131
+
132
+ // GET /api/reports/:table?from=YYYY-MM-DD&to=YYYY-MM-DD
133
+ router.get('/:table', (req, res) => {
134
+ const table = req.params.table.toLowerCase();
135
+
136
+ if (!ALLOWED.includes(table)) {
137
+ return res.status(400).json({ error: 'Invalid table name' });
138
+ }
139
+
140
+ const { from, to } = req.query;
141
+ const dateCol = DATE_COLS[table];
142
+ const params = [];
143
+
144
+ let sql = 'SELECT * FROM ' + table;
145
+
146
+ // Apply date range filter only if this table has a date column
147
+ if (dateCol && (from || to)) {
148
+ const conds = [];
149
+ if (from) { conds.push(dateCol + ' >= ?'); params.push(from); }
150
+ if (to) { conds.push(dateCol + ' <= ?'); params.push(to); }
151
+ sql += ' WHERE ' + conds.join(' AND ');
152
+ }
153
+
154
+ if (dateCol) sql += ' ORDER BY ' + dateCol + ' DESC';
155
+
156
+ db.query(sql, params, (err, results) => {
157
+ if (err) return res.status(500).json({ error: err.message });
158
+ res.json(results);
159
+ });
160
+ });
161
+
162
+ export default router;
163
+ `;
164
+ }
165
+
99
166
  export function generateDb(dbName) {
100
167
  return `import mysql from 'mysql2';
101
168
  import dotenv from 'dotenv';
@@ -160,13 +160,16 @@ export default ${name};
160
160
  `;
161
161
  }
162
162
 
163
- export function generateApp(tables) {
163
+ export function generateApp(tables, withReports) {
164
164
  const imports = tables.map((t) => `import ${pascal(t.tableName)} from './pages/${pascal(t.tableName)}';`).join('\n');
165
165
  const routes = tables.map((t) => ` <Route path="/${t.tableName.toLowerCase()}" element={<${pascal(t.tableName)} />} />`).join('\n');
166
166
  const first = tables[0]?.tableName.toLowerCase() ?? 'home';
167
+ const reportsImport = withReports ? `import Reports from './pages/Reports';` : '';
168
+ const reportsRoute = withReports ? ` <Route path="/reports" element={<Reports />} />` : '';
167
169
  return `import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
168
170
  import Login from './pages/Login';
169
171
  ${imports}
172
+ ${reportsImport}
170
173
 
171
174
  function App() {
172
175
  return (
@@ -175,6 +178,7 @@ function App() {
175
178
  <Route path="/" element={<Navigate to="/login" />} />
176
179
  <Route path="/login" element={<Login />} />
177
180
  ${routes}
181
+ ${reportsRoute}
178
182
  </Routes>
179
183
  </Router>
180
184
  );
@@ -184,10 +188,13 @@ export default App;
184
188
  `;
185
189
  }
186
190
 
187
- export function generateNavbar(tables) {
191
+ export function generateNavbar(tables, withReports) {
188
192
  const links = tables
189
193
  .map((t) => ` <Link to="/${t.tableName.toLowerCase()}" className="hover:text-blue-300 font-medium">${pascal(t.tableName)}</Link>`)
190
194
  .join('\n');
195
+ const reportsLink = withReports
196
+ ? ` <Link to="/reports" className="hover:text-blue-300 font-medium">Reports</Link>`
197
+ : '';
191
198
  return `import { Link } from 'react-router-dom';
192
199
 
193
200
  function Navbar() {
@@ -195,6 +202,7 @@ function Navbar() {
195
202
  <nav className="bg-blue-700 text-white px-6 py-3 flex gap-6 items-center shadow">
196
203
  <span className="font-bold text-lg mr-4">SIMS</span>
197
204
  ${links}
205
+ ${reportsLink}
198
206
  </nav>
199
207
  );
200
208
  }
@@ -252,13 +260,23 @@ export default Login;
252
260
  `;
253
261
  }
254
262
 
255
- export function generateFrontendPackage(name) {
263
+ export function generateFrontendPackage(name, withReports) {
264
+ const deps = {
265
+ axios: '^1.6.0',
266
+ react: '^18.2.0',
267
+ 'react-dom': '^18.2.0',
268
+ 'react-router-dom': '^6.20.0',
269
+ };
270
+ if (withReports) {
271
+ deps['jspdf'] = '^2.5.1';
272
+ deps['jspdf-autotable'] = '^3.8.1';
273
+ }
256
274
  return JSON.stringify({
257
275
  name: `${name}-frontend`,
258
276
  version: '1.0.0',
259
277
  type: 'module',
260
278
  scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
261
- dependencies: { axios: '^1.6.0', react: '^18.2.0', 'react-dom': '^18.2.0', 'react-router-dom': '^6.20.0' },
279
+ dependencies: deps,
262
280
  devDependencies: { '@vitejs/plugin-react': '^4.2.0', autoprefixer: '^10.4.16', postcss: '^8.4.31', tailwindcss: '^3.3.5', vite: '^5.0.0' },
263
281
  }, null, 2);
264
282
  }
@@ -306,6 +324,168 @@ export function generateHTML(title) {
306
324
  `;
307
325
  }
308
326
 
327
+ // ── Reports page ───────────────────────────────────────────────────────────
328
+ // reportTables = string[] of table names the user chose
329
+ // allTables = full parsed schema array
330
+ export function generateReportsPage(reportTables, allTables) {
331
+ // Resolve full schema for each chosen table
332
+ const resolved = reportTables
333
+ .map((n) => allTables.find((t) => t.tableName.toLowerCase() === n.toLowerCase()))
334
+ .filter(Boolean);
335
+
336
+ // Build the <option> list for the table-selector dropdown
337
+ const tableOptions = resolved
338
+ .map((t) => ' <option value="' + t.tableName + '">' + pascal(t.tableName) + '</option>')
339
+ .join('\n');
340
+
341
+ const firstTable = resolved[0]?.tableName ?? '';
342
+
343
+ // Build date-column map as a JS object literal (safe — no template interpolation issues)
344
+ const dateColEntries = resolved.map((t) => {
345
+ const dc = t.columns.find((c) => c.type === 'date' || c.type === 'datetime');
346
+ return ' ' + t.tableName + ': ' + (dc ? JSON.stringify(dc.name) : 'null');
347
+ }).join(',\n');
348
+
349
+ return [
350
+ "import { useState, useEffect } from 'react';",
351
+ "import axios from 'axios';",
352
+ "import jsPDF from 'jspdf';",
353
+ "import autoTable from 'jspdf-autotable';",
354
+ "import Navbar from '../Navbar';",
355
+ '',
356
+ '// Date column per table — used for server-side date range filtering',
357
+ 'const DATE_COL = {',
358
+ dateColEntries,
359
+ '};',
360
+ '',
361
+ 'function Reports() {',
362
+ " const today = new Date().toISOString().split('T')[0];",
363
+ " const [table, setTable] = useState('" + firstTable + "');",
364
+ " const [fromDate, setFrom] = useState('');",
365
+ " const [toDate, setTo] = useState('');",
366
+ " const [records, setRecords] = useState([]);",
367
+ " const [msg, setMsg] = useState('');",
368
+ '',
369
+ ' const fetchReport = async () => {',
370
+ ' if (!table) return;',
371
+ ' try {',
372
+ ' const params = {};',
373
+ ' if (fromDate) params.from = fromDate;',
374
+ ' if (toDate) params.to = toDate;',
375
+ ' const res = await axios.get(`/api/reports/${table.toLowerCase()}`, { params });',
376
+ ' setRecords(res.data);',
377
+ " setMsg(res.data.length === 0 ? 'No records found for this filter.' : '');",
378
+ ' } catch {',
379
+ " setMsg('Failed to load report.');",
380
+ ' }',
381
+ ' };',
382
+ '',
383
+ ' // Reload when the selected table changes',
384
+ ' useEffect(() => { fetchReport(); }, [table]);',
385
+ '',
386
+ ' // Download as CSV — no extra library needed',
387
+ ' const downloadCSV = () => {',
388
+ ' if (!records.length) return;',
389
+ ' const headers = Object.keys(records[0]).join(",");',
390
+ ' const rows = records.map((r) => Object.values(r).map(String).join(",")).join("\\n");',
391
+ ' const blob = new Blob([headers + "\\n" + rows], { type: "text/csv" });',
392
+ ' const a = document.createElement("a");',
393
+ ' a.href = URL.createObjectURL(blob);',
394
+ ' a.download = `${table}-report.csv`;',
395
+ ' a.click();',
396
+ ' };',
397
+ '',
398
+ ' // Download as PDF using jsPDF + autoTable',
399
+ ' const downloadPDF = () => {',
400
+ ' if (!records.length) return;',
401
+ ' const doc = new jsPDF("landscape");',
402
+ ' const headers = Object.keys(records[0]);',
403
+ ' const rows = records.map((r) => Object.values(r).map(String));',
404
+ ' doc.setFontSize(14);',
405
+ ' doc.text(`${table} Report`, 14, 15);',
406
+ ' doc.setFontSize(10);',
407
+ ' doc.text(`Period: ${fromDate || "All"} → ${toDate || "All"}`, 14, 22);',
408
+ ' autoTable(doc, { head: [headers], body: rows, startY: 28 });',
409
+ ' doc.save(`${table}-report.pdf`);',
410
+ ' };',
411
+ '',
412
+ ' const cols = records.length ? Object.keys(records[0]) : [];',
413
+ '',
414
+ ' return (',
415
+ ' <div>',
416
+ ' <Navbar />',
417
+ ' <div className="p-6">',
418
+ ' <h2 className="text-xl font-bold mb-4 text-blue-700">Reports</h2>',
419
+ '',
420
+ ' {/* Filter bar */}',
421
+ ' <div className="flex flex-wrap gap-3 mb-4 items-end">',
422
+ ' <div>',
423
+ ' <label className="block text-xs text-gray-500 mb-1">Table</label>',
424
+ ' <select value={table} onChange={(e) => setTable(e.target.value)} className="border p-2 rounded">',
425
+ tableOptions,
426
+ ' </select>',
427
+ ' </div>',
428
+ ' <div>',
429
+ ' <label className="block text-xs text-gray-500 mb-1">From Date</label>',
430
+ ' <input type="date" value={fromDate} max={today}',
431
+ ' onChange={(e) => setFrom(e.target.value)} className="border p-2 rounded" />',
432
+ ' </div>',
433
+ ' <div>',
434
+ ' <label className="block text-xs text-gray-500 mb-1">To Date</label>',
435
+ ' <input type="date" value={toDate} min={fromDate} max={today}',
436
+ ' onChange={(e) => setTo(e.target.value)} className="border p-2 rounded" />',
437
+ ' </div>',
438
+ ' <button onClick={fetchReport}',
439
+ ' className="bg-blue-700 text-white px-4 py-2 rounded hover:bg-blue-800">',
440
+ ' Generate Report',
441
+ ' </button>',
442
+ ' <button onClick={() => { setFrom(""); setTo(""); }}',
443
+ ' className="bg-gray-400 text-white px-4 py-2 rounded hover:bg-gray-500">',
444
+ ' Clear',
445
+ ' </button>',
446
+ ' </div>',
447
+ '',
448
+ ' {msg && <p className="text-gray-500 mb-3 text-sm">{msg}</p>}',
449
+ '',
450
+ ' {/* Download buttons — only shown when there are results */}',
451
+ ' {records.length > 0 && (',
452
+ ' <div className="flex gap-2 mb-4 items-center">',
453
+ ' <button onClick={downloadCSV}',
454
+ ' className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 text-sm">',
455
+ ' ⬇ Download CSV',
456
+ ' </button>',
457
+ ' <button onClick={downloadPDF}',
458
+ ' className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 text-sm">',
459
+ ' ⬇ Download PDF',
460
+ ' </button>',
461
+ ' <span className="text-gray-400 text-sm">{records.length} record(s)</span>',
462
+ ' </div>',
463
+ ' )}',
464
+ '',
465
+ ' {/* Results table */}',
466
+ ' <div className="overflow-x-auto">',
467
+ ' <table className="w-full border text-sm">',
468
+ ' <thead className="bg-blue-700 text-white">',
469
+ ' <tr>{cols.map((c) => <th key={c} className="p-2 border">{c}</th>)}</tr>',
470
+ ' </thead>',
471
+ ' <tbody>',
472
+ ' {records.map((r, i) => (',
473
+ ' <tr key={i} className="hover:bg-gray-50">',
474
+ ' {cols.map((c) => <td key={c} className="p-2 border">{String(r[c] ?? "")}</td>)}',
475
+ ' </tr>',
476
+ ' ))}',
477
+ ' </tbody>',
478
+ ' </table>',
479
+ ' </div>',
480
+ ' </div>',
481
+ ' </div>',
482
+ ' );',
483
+ '}',
484
+ '',
485
+ 'export default Reports;',
486
+ ].join('\n');
487
+ }
488
+
309
489
  // ── helpers ────────────────────────────────────────────────────────────────
310
490
 
311
491
  function pascal(str) { return str.charAt(0).toUpperCase() + str.slice(1); }
package/src/index.js CHANGED
@@ -1,43 +1,55 @@
1
- // src/index.js — orchestrator: parse SQL → generate all files → write to disk
1
+ // src/index.js — orchestrator
2
2
  import path from 'path';
3
3
  import { parseSQL } from './parser.js';
4
- import { generateRoute, generateServer, generateDb, generateEnv, generateBackendPackage } from './generator/backend.js';
5
- import { generatePage, generateApp, generateNavbar, generateLogin, generateFrontendPackage, generateViteConfig, generateMain, generateCSS, generateHTML } from './generator/frontend.js';
4
+ import { generateRoute, generateServer, generateDb, generateEnv, generateBackendPackage, generateReportsRoute } from './generator/backend.js';
5
+ import { generatePage, generateApp, generateNavbar, generateLogin, generateFrontendPackage, generateViteConfig, generateMain, generateCSS, generateHTML, generateReportsPage } from './generator/frontend.js';
6
6
  import { writeFiles } from './writer.js';
7
7
 
8
- export function generateProject({ dbName, sql, projectName, outputDir }) {
8
+ export function generateProject({ dbName, sql, projectName, outputDir, withReports = false, reportTables = [] }) {
9
9
  const outDir = outputDir || process.cwd();
10
10
 
11
- // 1. Parse SQL
11
+ // 1. Parse SQL into structured schema
12
12
  const tables = parseSQL(sql);
13
13
  if (!tables.length) throw new Error('No CREATE TABLE statements found.');
14
14
 
15
15
  const baseDir = path.join(outDir, projectName);
16
16
  const files = {};
17
17
 
18
- // 2. Backend files
18
+ // ── Backend ────────────────────────────────────────────────────────────
19
19
  files['backend/package.json'] = generateBackendPackage(projectName);
20
- files['backend/server.js'] = generateServer(tables);
20
+ files['backend/server.js'] = generateServer(tables, withReports);
21
21
  files['backend/db.js'] = generateDb(dbName);
22
22
  files['backend/.env'] = generateEnv(dbName);
23
+
23
24
  for (const t of tables) {
24
- files[`backend/routes/${t.tableName.toLowerCase()}.js`] = generateRoute(t);
25
+ files['backend/routes/' + t.tableName.toLowerCase() + '.js'] = generateRoute(t);
26
+ }
27
+
28
+ // Reports backend route (only if user opted in)
29
+ if (withReports && reportTables.length) {
30
+ files['backend/routes/reports.js'] = generateReportsRoute(reportTables, tables);
25
31
  }
26
32
 
27
- // 3. Frontend files
28
- files['frontend/package.json'] = generateFrontendPackage(projectName);
33
+ // ── Frontend ───────────────────────────────────────────────────────────
34
+ files['frontend/package.json'] = generateFrontendPackage(projectName, withReports);
29
35
  files['frontend/vite.config.js'] = generateViteConfig();
30
36
  files['frontend/index.html'] = generateHTML(projectName);
31
37
  files['frontend/src/main.jsx'] = generateMain();
32
38
  files['frontend/src/index.css'] = generateCSS();
33
- files['frontend/src/App.jsx'] = generateApp(tables);
34
- files['frontend/src/Navbar.jsx'] = generateNavbar(tables);
39
+ files['frontend/src/App.jsx'] = generateApp(tables, withReports);
40
+ files['frontend/src/Navbar.jsx'] = generateNavbar(tables, withReports);
35
41
  files['frontend/src/pages/Login.jsx'] = generateLogin(tables[0].tableName.toLowerCase());
42
+
36
43
  for (const t of tables) {
37
- files[`frontend/src/pages/${pascal(t.tableName)}.jsx`] = generatePage(t, tables);
44
+ files['frontend/src/pages/' + pascal(t.tableName) + '.jsx'] = generatePage(t, tables);
45
+ }
46
+
47
+ // Reports frontend page (only if user opted in)
48
+ if (withReports && reportTables.length) {
49
+ files['frontend/src/pages/Reports.jsx'] = generateReportsPage(reportTables, tables);
38
50
  }
39
51
 
40
- // 4. Write everything
52
+ // 2. Write all files to disk
41
53
  const written = writeFiles(baseDir, files);
42
54
  return { baseDir, files: written };
43
55
  }