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 +1 -1
- package/bin/cli.js +44 -46
- package/package.json +5 -3
- package/src/generator/backend.js +68 -1
- package/src/generator/frontend.js +184 -4
- package/src/index.js +26 -14
package/README.md
CHANGED
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
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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:
|
|
57
|
-
sql:
|
|
58
|
-
projectName:
|
|
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(
|
|
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(
|
|
69
|
-
console.log(
|
|
70
|
-
console.log(
|
|
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.
|
|
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": {
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
19
21
|
"license": "MIT"
|
|
20
|
-
}
|
|
22
|
+
}
|
package/src/generator/backend.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
//
|
|
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[
|
|
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
|
-
//
|
|
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[
|
|
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
|
-
//
|
|
52
|
+
// 2. Write all files to disk
|
|
41
53
|
const written = writeFiles(baseDir, files);
|
|
42
54
|
return { baseDir, files: written };
|
|
43
55
|
}
|