npms-exam-kit 1.0.2 → 2.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/bin/exam-kit.js +657 -278
- package/package.json +1 -3
package/bin/exam-kit.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createRequire } from 'module';
|
|
3
2
|
import { fileURLToPath } from 'url';
|
|
4
3
|
import { dirname, join } from 'path';
|
|
5
4
|
import fs from 'fs-extra';
|
|
@@ -13,339 +12,719 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
13
12
|
const __dirname = dirname(__filename);
|
|
14
13
|
const ROOT = join(__dirname, '..');
|
|
15
14
|
const PROJECTS_DIR = join(ROOT, 'projects');
|
|
16
|
-
|
|
17
15
|
const execAsync = promisify(exec);
|
|
18
16
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
17
|
+
function read(p) { try { return existsSync(p) ? readFileSync(p, 'utf8') : ''; } catch { return ''; } }
|
|
18
|
+
function inDir(p) { try { return existsSync(p) ? readdirSync(p) : []; } catch { return []; } }
|
|
19
|
+
|
|
20
|
+
const GUIDANCE = {
|
|
21
|
+
'backend-package': 'Run: cd backend-project && npm init -y',
|
|
22
|
+
'express': 'Run: cd backend-project && npm install express',
|
|
23
|
+
'cors': 'Run: cd backend-project && npm install cors',
|
|
24
|
+
'nodemon': 'Run: cd backend-project && npm install --save-dev nodemon',
|
|
25
|
+
'mysql2': 'Run: cd backend-project && npm install mysql2',
|
|
26
|
+
'bcryptjs': 'Run: cd backend-project && npm install bcryptjs',
|
|
27
|
+
'frontend-package': 'Run: npx create-vite frontend-project --template react',
|
|
28
|
+
'react-router-dom': 'Run: cd frontend-project && npm install react-router-dom',
|
|
29
|
+
'axios': 'Run: cd frontend-project && npm install axios',
|
|
30
|
+
'tailwindcss': 'Run: cd frontend-project && npm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p',
|
|
31
|
+
'server.js': 'Create backend-project/server.js with express app, cors, session, routes, and app.listen(PORT)',
|
|
32
|
+
'db.js': 'Create backend-project/config/db.js with mysql2/promise pool configuration',
|
|
33
|
+
'routes': 'Create route files in backend-project/routes/ with router.get(), router.post(), router.put(), router.delete()',
|
|
34
|
+
'app.jsx': 'Create frontend-project/src/App.jsx with BrowserRouter, Routes, Route components',
|
|
35
|
+
'pages': 'Create page components in frontend-project/src/pages/ for each form',
|
|
36
|
+
'navbar': 'Create frontend-project/src/components/Navbar.jsx with navigation links and logout',
|
|
37
|
+
'env': 'Create backend-project/.env file: DB_HOST=localhost DB_USER=root DB_PASSWORD= DB_NAME=SIMS PORT=5000',
|
|
38
|
+
'database.sql': 'Create database.sql with CREATE DATABASE and CREATE TABLE statements',
|
|
39
|
+
'vite.config': 'Create frontend-project/vite.config.js with React plugin and server port 5173',
|
|
40
|
+
'main.jsx': 'Create frontend-project/src/main.jsx to mount App component to DOM',
|
|
41
|
+
'index.css': 'Create frontend-project/src/index.css with @tailwind directives',
|
|
42
|
+
'tailwind.config': 'Configure tailwind.config.js with content paths',
|
|
43
|
+
'login-page': 'Create Login.jsx with form inputs for username/password and handleSubmit',
|
|
44
|
+
'reports': 'Create backend routes for daily stockout and stock status reports',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const SIMS_PAGES = ['Login.jsx', 'SparePart.jsx', 'StockIn.jsx', 'StockOut.jsx', 'Reports.jsx'];
|
|
48
|
+
const CRPMS_PAGES = ['Login.jsx', 'Cars.jsx', 'Services.jsx', 'ServiceRecords.jsx', 'Payments.jsx', 'Reports.jsx', 'Bill.jsx'];
|
|
49
|
+
|
|
50
|
+
function buildChecklist(baseDir, projectType) {
|
|
51
|
+
const b = p => join(baseDir, p);
|
|
52
|
+
const fe = p => join(baseDir, 'frontend-project', p);
|
|
53
|
+
const be = p => join(baseDir, 'backend-project', p);
|
|
54
|
+
const src = p => join(baseDir, 'frontend-project', 'src', p);
|
|
55
|
+
const pages = projectType === 'SIMS' ? SIMS_PAGES : CRPMS_PAGES;
|
|
56
|
+
const dbName = projectType === 'SIMS' ? 'SIMS' : 'CRPMS';
|
|
57
|
+
|
|
58
|
+
const checks = [
|
|
59
|
+
{ id: 'C1', cat: 'Preliminary (15%)', label: 'ERD Spare_Part entity drawn',
|
|
60
|
+
auto: () => 'MANUAL', guide: 'Draw Spare_Part entity on paper with entity symbol' },
|
|
61
|
+
{ id: 'C2', cat: 'Preliminary (15%)', label: 'ERD Stock_In entity drawn',
|
|
62
|
+
auto: () => 'MANUAL', guide: 'Draw Stock_In entity on paper with entity symbol' },
|
|
63
|
+
{ id: 'C3', cat: 'Preliminary (15%)', label: 'ERD Stock_Out entity drawn',
|
|
64
|
+
auto: () => 'MANUAL', guide: 'Draw Stock_Out entity on paper with entity symbol' },
|
|
65
|
+
{ id: 'C4', cat: 'Preliminary (15%)', label: 'ERD entity symbol used',
|
|
66
|
+
auto: () => 'MANUAL', guide: 'Use rectangle/box symbol for each entity' },
|
|
67
|
+
{ id: 'C5', cat: 'Preliminary (15%)', label: 'ERD relationship symbol used',
|
|
68
|
+
auto: () => 'MANUAL', guide: 'Use diamond/line symbols for relationships between entities' },
|
|
69
|
+
{ id: 'C6', cat: 'Preliminary (15%)', label: 'ERD link symbol used',
|
|
70
|
+
auto: () => 'MANUAL', guide: 'Use connecting lines between entities and relationships' },
|
|
71
|
+
{ id: 'C7', cat: 'Preliminary (15%)', label: 'ERD Primary Key rule respected',
|
|
72
|
+
auto: () => 'MANUAL', guide: 'Underline PK attributes in each entity' },
|
|
73
|
+
{ id: 'C8', cat: 'Preliminary (15%)', label: 'ERD Foreign Key rule respected',
|
|
74
|
+
auto: () => 'MANUAL', guide: 'Mark FK attributes with dotted underline or FK notation' },
|
|
75
|
+
{ id: 'C9', cat: 'Preliminary (15%)', label: 'ERD cardinalities indicated',
|
|
76
|
+
auto: () => 'MANUAL', guide: 'Show 1---* or M---N on relationship lines' },
|
|
77
|
+
{ id: 'C10', cat: 'Preliminary (15%)', label: 'Spare_Part-Stock_In relationship drawn',
|
|
78
|
+
auto: () => 'MANUAL', guide: `Connect Spare_Part (1) to Stock_In (*) via SparePartID FK` },
|
|
79
|
+
{ id: 'C11', cat: 'Preliminary (15%)', label: 'Users-Stock_Out relationship drawn',
|
|
80
|
+
auto: () => 'MANUAL', guide: 'Connect Users (1) to Stock_Out (*) via UserID FK' },
|
|
81
|
+
{ id: 'C12', cat: 'Preliminary (15%)', label: 'Spare_Part-Stock_Out relationship drawn',
|
|
82
|
+
auto: () => 'MANUAL', guide: `Connect Spare_Part (1) to Stock_Out (*) via SparePartID FK` },
|
|
83
|
+
{ id: 'C13', cat: 'Preliminary (15%)', label: 'Users PK indicated',
|
|
84
|
+
auto: () => 'MANUAL', guide: 'Underline UserID in Users entity' },
|
|
85
|
+
{ id: 'C14', cat: 'Preliminary (15%)', label: 'Spare_Part PK indicated',
|
|
86
|
+
auto: () => 'MANUAL', guide: 'Underline SparePartID in Spare_Part entity' },
|
|
87
|
+
{ id: 'C15', cat: 'Preliminary (15%)', label: 'Stock_In PK indicated',
|
|
88
|
+
auto: () => 'MANUAL', guide: 'Underline StockInID in Stock_In entity' },
|
|
89
|
+
{ id: 'C16', cat: 'Preliminary (15%)', label: 'Stock_Out PK indicated',
|
|
90
|
+
auto: () => 'MANUAL', guide: 'Underline StockOutID in Stock_Out entity' },
|
|
91
|
+
{ id: 'C17', cat: 'Preliminary (15%)', label: 'Users FK in Stock_Out indicated',
|
|
92
|
+
auto: () => 'MANUAL', guide: 'Mark UserID as FK in Stock_Out entity' },
|
|
93
|
+
{ id: 'C18', cat: 'Preliminary (15%)', label: 'Spare_Part FK in Stock_In indicated',
|
|
94
|
+
auto: () => 'MANUAL', guide: 'Mark SparePartID as FK in Stock_In entity' },
|
|
95
|
+
{ id: 'C19', cat: 'Preliminary (15%)', label: 'Spare_Part FK in Stock_Out indicated',
|
|
96
|
+
auto: () => 'MANUAL', guide: 'Mark SparePartID as FK in Stock_Out entity' },
|
|
97
|
+
|
|
98
|
+
{ id: 'C20', cat: 'Process (50%)', label: 'Project folder named FirstName_LastName_Exam_2025',
|
|
99
|
+
auto: () => !baseDir.includes('node_modules') && baseDir.split('_').length >= 3 ? 'PASS' : 'FAIL',
|
|
100
|
+
guide: 'Rename parent folder to FirstName_LastName_National_Practical_Exam_2025' },
|
|
101
|
+
{ id: 'C21', cat: 'Process (50%)', label: 'Node.js project created (package.json)',
|
|
102
|
+
auto: () => existsSync(be('package.json')) ? 'PASS' : 'FAIL',
|
|
103
|
+
guide: GUIDANCE['backend-package'] },
|
|
104
|
+
{ id: 'C22', cat: 'Process (50%)', label: 'Express.js installed',
|
|
105
|
+
auto: () => existsSync(be('node_modules/express')) ? 'PASS' : 'FAIL',
|
|
106
|
+
guide: GUIDANCE['express'] },
|
|
107
|
+
{ id: 'C23', cat: 'Process (50%)', label: 'Cors installed',
|
|
108
|
+
auto: () => existsSync(be('node_modules/cors')) ? 'PASS' : 'FAIL',
|
|
109
|
+
guide: GUIDANCE['cors'] },
|
|
110
|
+
{ id: 'C24', cat: 'Process (50%)', label: 'Nodemon installed',
|
|
111
|
+
auto: () => existsSync(be('node_modules/nodemon')) ? 'PASS' : 'FAIL',
|
|
112
|
+
guide: GUIDANCE['nodemon'] },
|
|
113
|
+
{ id: 'C25', cat: 'Process (50%)', label: 'MySQL2 installed in Node.js',
|
|
114
|
+
auto: () => existsSync(be('node_modules/mysql2')) ? 'PASS' : 'FAIL',
|
|
115
|
+
guide: GUIDANCE['mysql2'] },
|
|
116
|
+
{ id: 'C26', cat: 'Process (50%)', label: 'React project created (vite)',
|
|
117
|
+
auto: () => existsSync(fe('package.json')) && existsSync(fe('vite.config.js')) ? 'PASS' : 'FAIL',
|
|
118
|
+
guide: GUIDANCE['frontend-package'] },
|
|
119
|
+
{ id: 'C27', cat: 'Process (50%)', label: 'React-router-dom installed',
|
|
120
|
+
auto: () => existsSync(fe('node_modules/react-router-dom')) ? 'PASS' : 'FAIL',
|
|
121
|
+
guide: GUIDANCE['react-router-dom'] },
|
|
122
|
+
{ id: 'C28', cat: 'Process (50%)', label: 'Axios installed',
|
|
123
|
+
auto: () => existsSync(fe('node_modules/axios')) ? 'PASS' : 'FAIL',
|
|
124
|
+
guide: GUIDANCE['axios'] },
|
|
125
|
+
{ id: 'C29', cat: 'Process (50%)', label: `${dbName} database SQL created`,
|
|
126
|
+
auto: () => existsSync(b('database.sql')) && read(b('database.sql')).includes(dbName) ? 'PASS' : 'FAIL',
|
|
127
|
+
guide: `Create database.sql with CREATE DATABASE ${dbName}` },
|
|
128
|
+
{ id: 'C30', cat: 'Process (50%)', label: 'Users table created in SQL',
|
|
129
|
+
auto: () => read(b('database.sql')).includes('CREATE TABLE') && read(b('database.sql')).toLowerCase().includes('user') ? 'PASS' : 'FAIL',
|
|
130
|
+
guide: 'Add CREATE TABLE Users (UserID INT PK, Username, Password)' },
|
|
131
|
+
{ id: 'C31', cat: 'Process (50%)', label: `${projectType === 'SIMS' ? 'Spare_Part' : 'Services'} table created`,
|
|
132
|
+
auto: () => { const s = read(b('database.sql')).toLowerCase(); return projectType === 'SIMS' ? (s.includes('spare_part') ? 'PASS' : 'FAIL') : (s.includes('services') ? 'PASS' : 'FAIL'); },
|
|
133
|
+
guide: projectType === 'SIMS' ? 'Add CREATE TABLE Spare_Part (SparePartID PK, Name, Category, Quantity, UnitPrice, TotalPrice)' : 'Add CREATE TABLE Services (ServiceCode PK, ServiceName, ServicePrice)' },
|
|
134
|
+
{ id: 'C32', cat: 'Process (50%)', label: `${projectType === 'SIMS' ? 'Stock_Out' : 'ServiceRecord'} table created`,
|
|
135
|
+
auto: () => { const s = read(b('database.sql')).toLowerCase(); return projectType === 'SIMS' ? (s.includes('stock_out') ? 'PASS' : 'FAIL') : (s.includes('servicerecord') ? 'PASS' : 'FAIL'); },
|
|
136
|
+
guide: projectType === 'SIMS' ? 'Add CREATE TABLE Stock_Out (StockOutID PK, SparePartID FK, StockOutQuantity, StockOutUnitPrice, StockOutTotalPrice, StockOutDate)' : 'Add CREATE TABLE ServiceRecord (RecordNumber PK, ServiceDate, PlateNumber FK, ServiceCode FK)' },
|
|
137
|
+
{ id: 'C33', cat: 'Process (50%)', label: `${projectType === 'SIMS' ? 'Stock_In' : 'Payment'} table created`,
|
|
138
|
+
auto: () => { const s = read(b('database.sql')).toLowerCase(); return projectType === 'SIMS' ? (s.includes('stock_in') ? 'PASS' : 'FAIL') : (s.includes('payment') ? 'PASS' : 'FAIL'); },
|
|
139
|
+
guide: projectType === 'SIMS' ? 'Add CREATE TABLE Stock_In (StockInID PK, SparePartID FK, StockInQuantity, StockInDate)' : 'Add CREATE TABLE Payment (PaymentNumber PK, AmountPaid, PaymentDate, RecordNumber FK, UserID FK)' },
|
|
140
|
+
{ id: 'C34', cat: 'Process (50%)', label: 'Primary keys applied in all tables',
|
|
141
|
+
auto: () => read(b('database.sql')).includes('PRIMARY KEY') ? 'PASS' : 'FAIL',
|
|
142
|
+
guide: 'Add PRIMARY KEY to each table definition' },
|
|
143
|
+
{ id: 'C35', cat: 'Process (50%)', label: `FKs applied in ${projectType === 'SIMS' ? 'Stock_In' : 'ServiceRecord'}`,
|
|
144
|
+
auto: () => read(b('database.sql')).includes('FOREIGN KEY') ? 'PASS' : 'FAIL',
|
|
145
|
+
guide: `Add FOREIGN KEY references in ${projectType === 'SIMS' ? 'Stock_In (SparePartID -> Spare_Part)' : 'ServiceRecord (PlateNumber -> Car, ServiceCode -> Services)'}` },
|
|
146
|
+
{ id: 'C36', cat: 'Process (50%)', label: `FKs applied in ${projectType === 'SIMS' ? 'Stock_Out' : 'Payment'}`,
|
|
147
|
+
auto: () => { const s = read(b('database.sql')); return s.includes('FOREIGN KEY') ? 'PASS' : 'FAIL'; },
|
|
148
|
+
guide: `Add FOREIGN KEY in ${projectType === 'SIMS' ? 'Stock_Out' : 'Payment'}` },
|
|
149
|
+
{ id: 'C37', cat: 'Process (50%)', label: 'React function component declared',
|
|
150
|
+
auto: () => read(src('App.jsx')).includes('function') || read(src('App.jsx')).includes('=>') || read(src('App.jsx')).includes('export default function') ? 'PASS' : 'FAIL',
|
|
151
|
+
guide: 'Use function App() { ... } in App.jsx' },
|
|
152
|
+
{ id: 'C38', cat: 'Process (50%)', label: 'Return method included in component',
|
|
153
|
+
auto: () => read(src('App.jsx')).includes('return') ? 'PASS' : 'FAIL',
|
|
154
|
+
guide: 'Add return ( <JSX> ) in your component' },
|
|
155
|
+
{ id: 'C39', cat: 'Process (50%)', label: 'Component exported',
|
|
156
|
+
auto: () => read(src('App.jsx')).includes('export default') ? 'PASS' : 'FAIL',
|
|
157
|
+
guide: 'Add "export default App;" at the end' },
|
|
158
|
+
{ id: 'C40', cat: 'Process (50%)', label: 'Component mounted to DOM',
|
|
159
|
+
auto: () => existsSync(src('main.jsx')) && read(src('main.jsx')).includes('ReactDOM') ? 'PASS' : 'FAIL',
|
|
160
|
+
guide: 'In main.jsx: createRoot(document.getElementById("root")).render(<App />)' },
|
|
161
|
+
{ id: 'C41', cat: 'Process (50%)', label: 'JSX used inside return method',
|
|
162
|
+
auto: () => read(src('App.jsx')).includes('return') && (read(src('App.jsx')).includes('<') || read(src('App.jsx')).includes('React.createElement')) ? 'PASS' : 'FAIL',
|
|
163
|
+
guide: 'Add JSX markup inside return()' },
|
|
164
|
+
{ id: 'C42', cat: 'Process (50%)', label: 'Login form created',
|
|
165
|
+
auto: () => existsSync(src('pages/Login.jsx')) ? 'PASS' : 'FAIL',
|
|
166
|
+
guide: 'Create src/pages/Login.jsx with username/password inputs' },
|
|
167
|
+
{ id: 'C43', cat: 'Process (50%)', label: `${projectType === 'SIMS' ? 'Spare_Part' : 'Cars/Services'} form created`,
|
|
168
|
+
auto: () => { const p = inDir(src('pages')); return projectType === 'SIMS' ? (p.some(f => f.toLowerCase().includes('spare')) ? 'PASS' : 'FAIL') : (p.some(f => f.toLowerCase().includes('car')) && p.some(f => f.toLowerCase().includes('service')) ? 'PASS' : 'FAIL'); },
|
|
169
|
+
guide: projectType === 'SIMS' ? 'Create SparePart.jsx with fields: Name, Category, Quantity, UnitPrice' : 'Create Cars.jsx and Services.jsx' },
|
|
170
|
+
{ id: 'C44', cat: 'Process (50%)', label: `${projectType === 'SIMS' ? 'Stock_In' : 'ServiceRecord'} form created`,
|
|
171
|
+
auto: () => inDir(src('pages')).some(f => f.toLowerCase().includes(projectType === 'SIMS' ? 'stockin' : 'record')) ? 'PASS' : 'FAIL',
|
|
172
|
+
guide: projectType === 'SIMS' ? 'Create StockIn.jsx with SparePart selection, quantity, date' : 'Create ServiceRecords.jsx' },
|
|
173
|
+
{ id: 'C45', cat: 'Process (50%)', label: `${projectType === 'SIMS' ? 'Stock_Out' : 'Payment'} form created`,
|
|
174
|
+
auto: () => inDir(src('pages')).some(f => f.toLowerCase().includes(projectType === 'SIMS' ? 'stockout' : 'payment')) ? 'PASS' : 'FAIL',
|
|
175
|
+
guide: projectType === 'SIMS' ? 'Create StockOut.jsx' : 'Create Payments.jsx' },
|
|
176
|
+
{ id: 'C46', cat: 'Process (50%)', label: 'Routes configured in React',
|
|
177
|
+
auto: () => read(src('App.jsx')).includes('Routes') && read(src('App.jsx')).includes('Route') ? 'PASS' : 'FAIL',
|
|
178
|
+
guide: 'Add <Routes><Route path="..." element={...}></Routes> in App.jsx' },
|
|
179
|
+
{ id: 'C47', cat: 'Process (50%)', label: 'Links created between components',
|
|
180
|
+
auto: () => read(src('App.jsx')).includes('Link') || read(src('components/Navbar.jsx')).includes('Link') ? 'PASS' : 'FAIL',
|
|
181
|
+
guide: 'Use <Link to="/path"> in Navbar or components for navigation' },
|
|
182
|
+
{ id: 'C48', cat: 'Process (50%)', label: 'Navigation layout created',
|
|
183
|
+
auto: () => read(src('components/Navbar.jsx')).length > 50 ? 'PASS' : 'FAIL',
|
|
184
|
+
guide: 'Create Navbar.jsx with links to all pages and logout button' },
|
|
185
|
+
{ id: 'C49', cat: 'Process (50%)', label: 'Tailwind CSS installed',
|
|
186
|
+
auto: () => existsSync(fe('node_modules/tailwindcss')) ? 'PASS' : 'FAIL',
|
|
187
|
+
guide: GUIDANCE['tailwindcss'] },
|
|
188
|
+
{ id: 'C50', cat: 'Process (50%)', label: 'Tailwind CSS configured',
|
|
189
|
+
auto: () => (existsSync(fe('tailwind.config.js')) && read(fe('tailwind.config.js')).includes('content')) || read(fe('vite.config.js')).includes('tailwindcss') || read(fe('postcss.config.js')).includes('tailwind') ? 'PASS' : 'FAIL',
|
|
190
|
+
guide: 'Configure tailwind.config.js with content paths or use @tailwindcss/vite plugin' },
|
|
191
|
+
{ id: 'C51', cat: 'Process (50%)', label: 'Server JS file created',
|
|
192
|
+
auto: () => existsSync(be('server.js')) ? 'PASS' : 'FAIL',
|
|
193
|
+
guide: GUIDANCE['server.js'] },
|
|
194
|
+
{ id: 'C52', cat: 'Process (50%)', label: 'Express package imported',
|
|
195
|
+
auto: () => read(be('server.js')).includes('express') ? 'PASS' : 'FAIL',
|
|
196
|
+
guide: 'Add "const express = require(\'express\')" at top of server.js' },
|
|
197
|
+
{ id: 'C53', cat: 'Process (50%)', label: 'Cors package imported',
|
|
198
|
+
auto: () => read(be('server.js')).includes('cors') ? 'PASS' : 'FAIL',
|
|
199
|
+
guide: 'Add "const cors = require(\'cors\')" and app.use(cors(...)) in server.js' },
|
|
200
|
+
{ id: 'C54', cat: 'Process (50%)', label: 'Port number identified',
|
|
201
|
+
auto: () => read(be('server.js')).includes('PORT') ? 'PASS' : 'FAIL',
|
|
202
|
+
guide: 'Define "const PORT = 5000" and app.listen(PORT, ...) in server.js' },
|
|
203
|
+
{ id: 'C55', cat: 'Process (50%)', label: 'Express object created',
|
|
204
|
+
auto: () => read(be('server.js')).includes('express()') ? 'PASS' : 'FAIL',
|
|
205
|
+
guide: 'Add "const app = express()" in server.js' },
|
|
206
|
+
{ id: 'C56', cat: 'Process (50%)', label: 'Listen method applied',
|
|
207
|
+
auto: () => read(be('server.js')).includes('listen') ? 'PASS' : 'FAIL',
|
|
208
|
+
guide: 'Add "app.listen(PORT, () => console.log(...))" in server.js' },
|
|
209
|
+
{ id: 'C57', cat: 'Process (50%)', label: 'MySQL2 package used in backend',
|
|
210
|
+
auto: () => existsSync(be('node_modules/mysql2')) ? 'PASS' : 'FAIL',
|
|
211
|
+
guide: GUIDANCE['mysql2'] },
|
|
212
|
+
{ id: 'C58', cat: 'Process (50%)', label: 'POST endpoint created',
|
|
213
|
+
auto: () => inDir(be('routes')).some(f => read(be('routes/'+f)).includes('router.post')) ? 'PASS' : 'FAIL',
|
|
214
|
+
guide: 'Add router.post(\'/\', async (req, res) => {...}) in route files' },
|
|
215
|
+
{ id: 'C59', cat: 'Process (50%)', label: 'GET endpoint created',
|
|
216
|
+
auto: () => inDir(be('routes')).some(f => read(be('routes/'+f)).includes('router.get')) ? 'PASS' : 'FAIL',
|
|
217
|
+
guide: 'Add router.get(\'/\', async (req, res) => {...}) in route files' },
|
|
218
|
+
{ id: 'C60', cat: 'Process (50%)', label: 'PUT endpoint created',
|
|
219
|
+
auto: () => inDir(be('routes')).some(f => read(be('routes/'+f)).includes('router.put')) ? 'PASS' : 'FAIL',
|
|
220
|
+
guide: 'Add router.put(\'/:id\', async (req, res) => {...}) in route files' },
|
|
221
|
+
{ id: 'C61', cat: 'Process (50%)', label: 'DELETE endpoint created',
|
|
222
|
+
auto: () => inDir(be('routes')).some(f => read(be('routes/'+f)).includes('router.delete')) ? 'PASS' : 'FAIL',
|
|
223
|
+
guide: 'Add router.delete(\'/:id\', async (req, res) => {...}) in route files' },
|
|
224
|
+
{ id: 'C62', cat: 'Process (50%)', label: 'Axios imported/used in frontend',
|
|
225
|
+
auto: () => read(src('App.jsx')).includes('axios') || existsSync(src('api/axios.js')) ? 'PASS' : 'FAIL',
|
|
226
|
+
guide: 'Add "import axios from \'axios\'" and set axios.defaults.baseURL' },
|
|
227
|
+
{ id: 'C63', cat: 'Process (50%)', label: 'Password encrypted (bcryptjs)',
|
|
228
|
+
auto: () => existsSync(be('node_modules/bcryptjs')) ? 'PASS' : 'FAIL',
|
|
229
|
+
guide: GUIDANCE['bcryptjs'] },
|
|
230
|
+
{ id: 'C64', cat: 'Process (50%)', label: 'Express-session installed',
|
|
231
|
+
auto: () => existsSync(be('node_modules/express-session')) ? 'PASS' : 'FAIL',
|
|
232
|
+
guide: 'Run: cd backend-project && npm install express-session' },
|
|
233
|
+
{ id: 'C65', cat: 'Process (50%)', label: 'Session-based login implemented',
|
|
234
|
+
auto: () => read(be('server.js')).includes('session') && read(be('routes/auth.js')).includes('session') ? 'PASS' : 'FAIL',
|
|
235
|
+
guide: 'Use express-session with req.session.user to manage login state' },
|
|
236
|
+
{ id: 'C66', cat: 'Process (50%)', label: 'DB config file exists',
|
|
237
|
+
auto: () => existsSync(be('config/db.js')) ? 'PASS' : 'FAIL',
|
|
238
|
+
guide: 'Create config/db.js with mysql2/promise pool using env variables' },
|
|
239
|
+
{ id: 'C67', cat: 'Process (50%)', label: '.env file configured',
|
|
240
|
+
auto: () => existsSync(be('.env')) || existsSync(be('.env.example')) ? 'PASS' : 'FAIL',
|
|
241
|
+
guide: GUIDANCE['env'] },
|
|
242
|
+
{ id: 'C68', cat: 'Process (50%)', label: 'Tailwind CSS imported in index.css',
|
|
243
|
+
auto: () => read(src('index.css')).includes('tailwind') ? 'PASS' : 'FAIL',
|
|
244
|
+
guide: 'Add @tailwind base; @tailwind components; @tailwind utilities; in index.css' },
|
|
245
|
+
{ id: 'C69', cat: 'Process (50%)', label: 'Vite config for React',
|
|
246
|
+
auto: () => read(fe('vite.config.js')).includes('@vitejs/plugin-react') ? 'PASS' : 'FAIL',
|
|
247
|
+
guide: 'Add @vitejs/plugin-react in vite.config.js plugins array' },
|
|
248
|
+
{ id: 'C70', cat: 'Product (30%)', label: 'Project is presentable',
|
|
249
|
+
auto: () => 'MANUAL', guide: 'Prepare a brief presentation covering: product name, key steps, importance, challenges, solutions' },
|
|
250
|
+
{ id: 'C71', cat: 'Product (30%)', label: 'Input validation prevents invalid data',
|
|
251
|
+
auto: () => { const r = read(be('routes/stockOut.js')) || read(be('routes/payments.js')); return r.includes('return res.status(400)') ? 'PASS' : 'FAIL';
|
|
252
|
+
}, guide: 'Add validation checks: if (!field) return res.status(400).json({error: "Field required"})' },
|
|
253
|
+
{ id: 'C72', cat: 'Product (30%)', label: 'Form submission saves data to DB',
|
|
254
|
+
auto: () => { const r = inDir(be('routes')); return r.some(f => read(be('routes/'+f)).includes('INSERT INTO') || read(be('routes/'+f)).includes('res.status(201)')) ? 'PASS' : 'FAIL'; },
|
|
255
|
+
guide: 'Use INSERT INTO SQL statements in POST route handlers' },
|
|
256
|
+
{ id: 'C73', cat: 'Product (30%)', label: 'Data retrieval displays records',
|
|
257
|
+
auto: () => { const r = inDir(be('routes')); return r.some(f => read(be('routes/'+f)).includes('SELECT') && read(be('routes/'+f)).includes('router.get')) ? 'PASS' : 'FAIL'; },
|
|
258
|
+
guide: 'Use SELECT queries in GET route handlers and return JSON' },
|
|
259
|
+
{ id: 'C74', cat: 'Product (30%)', label: 'Update functionality works (PUT)',
|
|
260
|
+
auto: () => inDir(be('routes')).some(f => read(be('routes/'+f)).includes('UPDATE') && read(be('routes/'+f)).includes('router.put')) ? 'PASS' : 'FAIL',
|
|
261
|
+
guide: 'Use UPDATE SQL in PUT route handlers' },
|
|
262
|
+
{ id: 'C75', cat: 'Product (30%)', label: 'Delete functionality works (DELETE)',
|
|
263
|
+
auto: () => inDir(be('routes')).some(f => read(be('routes/'+f)).includes('DELETE') && read(be('routes/'+f)).includes('router.delete')) ? 'PASS' : 'FAIL',
|
|
264
|
+
guide: 'Use DELETE SQL in DELETE route handlers' },
|
|
265
|
+
{ id: 'C76', cat: 'Product (30%)', label: 'Navigation menus are interactive',
|
|
266
|
+
auto: () => read(src('components/Navbar.jsx')).includes('useState') && read(src('components/Navbar.jsx')).includes('onClick') ? 'PASS' : 'FAIL',
|
|
267
|
+
guide: 'Add onClick handlers and responsive toggle (hamburger menu) in Navbar' },
|
|
268
|
+
{ id: 'C77', cat: 'Product (30%)', label: 'Buttons are interactive',
|
|
269
|
+
auto: () => (read(src('pages/Login.jsx')).includes('onClick') || read(src('pages/Login.jsx')).includes('onSubmit')) ? 'PASS' : 'FAIL',
|
|
270
|
+
guide: 'Add onClick or onSubmit event handlers to buttons' },
|
|
271
|
+
{ id: 'C78', cat: 'Product (30%)', label: 'Responsive design (flexbox/grid)',
|
|
272
|
+
auto: () => (read(src('components/Navbar.jsx')).includes('md:') || read(src('App.jsx')).includes('flex') || read(src('index.css')).includes('grid')) ? 'PASS' : 'FAIL',
|
|
273
|
+
guide: 'Use Tailwind responsive prefixes (sm:, md:, lg:) and flexbox/grid' },
|
|
274
|
+
{ id: 'C79', cat: 'Product (30%)', label: `Daily ${projectType === 'SIMS' ? 'StockOut' : 'Service'} report generated`,
|
|
275
|
+
auto: () => read(be('routes/reports.js')).includes(projectType === 'SIMS' ? 'daily-stockout' : '/daily') ? 'PASS' : 'FAIL',
|
|
276
|
+
guide: `Add GET /api/reports/${projectType === 'SIMS' ? 'daily-stockout' : 'daily'} endpoint in routes/reports.js` },
|
|
277
|
+
{ id: 'C80', cat: 'Product (30%)', label: `Daily Stock Status report generated`,
|
|
278
|
+
auto: () => read(be('routes/reports.js')).includes('stock-status') || read(be('routes/reports.js')).includes('summary') ? 'PASS' : 'FAIL',
|
|
279
|
+
guide: `Add GET /api/reports/stock-status with Stored Quantity, StockOut, Remaining query` },
|
|
280
|
+
{ id: 'C81', cat: 'Product (30%)', label: 'Colors and styling are consistent',
|
|
281
|
+
auto: () => read(src('index.css')).length > 0 && read(fe('tailwind.config.js') || '').length > 0 ? 'PASS' : 'FAIL',
|
|
282
|
+
guide: 'Use a consistent color palette in Tailwind config (primary, secondary, accent)' },
|
|
283
|
+
{ id: 'C82', cat: 'Closing (5%)', label: 'Project folder can be removed after marking',
|
|
284
|
+
auto: () => 'MANUAL', guide: 'After assessor approval, delete the project folder' },
|
|
285
|
+
{ id: 'C83', cat: 'Closing (5%)', label: 'Database can be deleted after marking',
|
|
286
|
+
auto: () => 'MANUAL', guide: 'Run: DROP DATABASE ' + dbName + '; in MySQL' },
|
|
287
|
+
{ id: 'C84', cat: 'Closing (5%)', label: 'Global dependencies removed',
|
|
288
|
+
auto: () => 'MANUAL', guide: 'Uninstall global packages: npm uninstall -g nodemon etc.' },
|
|
289
|
+
];
|
|
290
|
+
return checks;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function showResults(results) {
|
|
294
|
+
const groups = {};
|
|
295
|
+
for (const r of results) {
|
|
296
|
+
if (!groups[r.cat]) groups[r.cat] = [];
|
|
297
|
+
groups[r.cat].push(r);
|
|
113
298
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
299
|
+
|
|
300
|
+
log(chalk.bold('\n═══════════════════════════════════════════════════════════════'));
|
|
301
|
+
log(chalk.bold(' AUTO-ASSESSMENT RESULTS'));
|
|
302
|
+
log(chalk.bold('═══════════════════════════════════════════════════════════════\n'));
|
|
303
|
+
|
|
304
|
+
const total = results.length;
|
|
305
|
+
let passed = 0, failed = 0, manual = 0;
|
|
306
|
+
|
|
307
|
+
for (const [cat, items] of Object.entries(groups)) {
|
|
308
|
+
log(chalk.bold(chalk.bgHex('#1e293b').white(` ${cat} `)));
|
|
309
|
+
log('');
|
|
310
|
+
for (const r of items) {
|
|
311
|
+
if (r.status === 'PASS') { passed++;
|
|
312
|
+
log(` ${chalk.green('✓')} ${r.label.padEnd(58)} ${chalk.green('PASS')}`); }
|
|
313
|
+
else if (r.status === 'FAIL') { failed++;
|
|
314
|
+
log(` ${chalk.red('✗')} ${r.label.padEnd(58)} ${chalk.red('FAIL')}`); }
|
|
315
|
+
else { manual++;
|
|
316
|
+
log(` ${chalk.cyan('?')} ${r.label.padEnd(58)} ${chalk.cyan('MANUAL')}`); }
|
|
127
317
|
}
|
|
318
|
+
log('');
|
|
128
319
|
}
|
|
129
|
-
}
|
|
130
320
|
|
|
131
|
-
|
|
132
|
-
log(chalk.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
321
|
+
log(chalk.bold('───────────────────────────────────────────────────────────────'));
|
|
322
|
+
log(chalk.bold(` ${chalk.green('PASSED')}: ${passed} | ${chalk.red('FAILED')}: ${failed} | ${chalk.cyan('MANUAL')}: ${manual} | TOTAL: ${total}`));
|
|
323
|
+
const autoTotal = total - manual;
|
|
324
|
+
const autoPassed = passed;
|
|
325
|
+
const pct = autoTotal > 0 ? ((autoPassed / autoTotal) * 100).toFixed(1) : 'N/A';
|
|
326
|
+
log(chalk.bold(` Auto-check score: ${pct}% (${autoPassed}/${autoTotal} auto-checkable items)`));
|
|
327
|
+
|
|
328
|
+
if (failed > 0) {
|
|
329
|
+
log(chalk.bold(chalk.yellow('\n─── HOW TO FIX FAILED ITEMS ───\n')));
|
|
330
|
+
for (const r of results) {
|
|
331
|
+
if (r.status === 'FAIL' && r.guide) {
|
|
332
|
+
log(` ${chalk.red('✗')} ${chalk.bold(r.label)}`);
|
|
333
|
+
log(` ${chalk.dim('→')} ${r.guide}\n`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
142
336
|
}
|
|
337
|
+
|
|
338
|
+
log(chalk.bold('═══════════════════════════════════════════════════════════════\n'));
|
|
143
339
|
}
|
|
144
340
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
341
|
+
function generateMermaidDiagrams(projectType) {
|
|
342
|
+
let erd = '', dfd = '';
|
|
343
|
+
if (projectType === 'SIMS') {
|
|
344
|
+
erd = `erDiagram
|
|
345
|
+
Users ||--o{ Stock_Out : "records"
|
|
346
|
+
Spare_Part ||--o{ Stock_In : "receives"
|
|
347
|
+
Spare_Part ||--o{ Stock_Out : "issues"
|
|
348
|
+
|
|
349
|
+
Users {
|
|
350
|
+
int UserID PK "Auto Increment"
|
|
351
|
+
varchar Username "Unique"
|
|
352
|
+
varchar Password "Hashed"
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
Spare_Part {
|
|
356
|
+
int SparePartID PK "Auto Increment"
|
|
357
|
+
varchar Name
|
|
358
|
+
varchar Category
|
|
359
|
+
int Quantity
|
|
360
|
+
decimal UnitPrice
|
|
361
|
+
decimal TotalPrice "Generated: Qty * UnitPrice"
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
Stock_In {
|
|
365
|
+
int StockInID PK "Auto Increment"
|
|
366
|
+
int SparePartID FK
|
|
367
|
+
int StockInQuantity
|
|
368
|
+
date StockInDate
|
|
369
|
+
}
|
|
149
370
|
|
|
150
|
-
|
|
151
|
-
|
|
371
|
+
Stock_Out {
|
|
372
|
+
int StockOutID PK "Auto Increment"
|
|
373
|
+
int SparePartID FK
|
|
374
|
+
int UserID FK
|
|
375
|
+
int StockOutQuantity
|
|
376
|
+
decimal StockOutUnitPrice
|
|
377
|
+
decimal StockOutTotalPrice
|
|
378
|
+
date StockOutDate
|
|
379
|
+
}`;
|
|
380
|
+
|
|
381
|
+
dfd = `graph TD
|
|
382
|
+
subgraph "Context Level 0"
|
|
383
|
+
SM[Stock Manager] -->|Input Data| SIMS[SIMS System]
|
|
384
|
+
SIMS -->|Reports| SM
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
subgraph "Level 1 - Processes"
|
|
388
|
+
P1["1.0 Manage Spare Parts"]
|
|
389
|
+
P2["2.0 Record Stock In"]
|
|
390
|
+
P3["3.0 Record Stock Out"]
|
|
391
|
+
P4["4.0 Generate Reports"]
|
|
392
|
+
P5["5.0 Authentication"]
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
subgraph "Data Stores"
|
|
396
|
+
D1[(Spare_Part)]
|
|
397
|
+
D2[(Stock_In)]
|
|
398
|
+
D3[(Stock_Out)]
|
|
399
|
+
D4[(Users)]
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
SM -->|"Login Credentials"| P5
|
|
403
|
+
P5 --> D4
|
|
404
|
+
SM -->|"Spare Part Details"| P1
|
|
405
|
+
P1 --> D1
|
|
406
|
+
SM -->|"Stock In Data"| P2
|
|
407
|
+
P2 --> D2
|
|
408
|
+
P2 -->|"Update Stock"| D1
|
|
409
|
+
SM -->|"Stock Out Data"| P3
|
|
410
|
+
P3 --> D3
|
|
411
|
+
P3 -->|"Reduce Stock"| D1
|
|
412
|
+
P3 -->|"Record User"| D4
|
|
413
|
+
SM -->|"Request Report"| P4
|
|
414
|
+
P4 --> D1
|
|
415
|
+
P4 --> D2
|
|
416
|
+
P4 --> D3`;
|
|
417
|
+
} else {
|
|
418
|
+
erd = `erDiagram
|
|
419
|
+
Car ||--o{ ServiceRecord : "has"
|
|
420
|
+
Services ||--o{ ServiceRecord : "includes"
|
|
421
|
+
ServiceRecord ||--o{ Payment : "generates"
|
|
422
|
+
User ||--o{ Payment : "processes"
|
|
423
|
+
|
|
424
|
+
Car {
|
|
425
|
+
varchar PlateNumber PK
|
|
426
|
+
varchar type
|
|
427
|
+
varchar Model
|
|
428
|
+
int ManufacturingYear
|
|
429
|
+
varchar DriverPhone
|
|
430
|
+
varchar MechanicName
|
|
431
|
+
}
|
|
152
432
|
|
|
153
|
-
|
|
154
|
-
|
|
433
|
+
Services {
|
|
434
|
+
varchar ServiceCode PK
|
|
435
|
+
varchar ServiceName
|
|
436
|
+
decimal ServicePrice
|
|
437
|
+
}
|
|
155
438
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
439
|
+
ServiceRecord {
|
|
440
|
+
int RecordNumber PK "Auto Increment"
|
|
441
|
+
date ServiceDate
|
|
442
|
+
varchar PlateNumber FK
|
|
443
|
+
varchar ServiceCode FK
|
|
160
444
|
}
|
|
161
445
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
} else {
|
|
169
|
-
const { confirmed } = await inquirer.prompt([{
|
|
170
|
-
type: 'confirm',
|
|
171
|
-
name: 'confirmed',
|
|
172
|
-
message: `${item.label}`,
|
|
173
|
-
default: true
|
|
174
|
-
}]);
|
|
175
|
-
const mark = confirmed ? chalk.green('✓') : chalk.red('✗');
|
|
176
|
-
const status = confirmed ? chalk.green('PASS') : chalk.red('FAIL');
|
|
177
|
-
log(` ${mark} ${item.label.padEnd(55)} ${status}`);
|
|
178
|
-
results.push({ ...item, passed: confirmed, reason: '' });
|
|
446
|
+
Payment {
|
|
447
|
+
int PaymentNumber PK "Auto Increment"
|
|
448
|
+
decimal AmountPaid
|
|
449
|
+
date PaymentDate
|
|
450
|
+
int RecordNumber FK
|
|
451
|
+
int UserID FK
|
|
179
452
|
}
|
|
453
|
+
|
|
454
|
+
User {
|
|
455
|
+
int UserID PK "Auto Increment"
|
|
456
|
+
varchar Username "Unique"
|
|
457
|
+
varchar Password "Hashed"
|
|
458
|
+
varchar FullName
|
|
459
|
+
}`;
|
|
460
|
+
|
|
461
|
+
dfd = `graph TD
|
|
462
|
+
subgraph "Context Level 0"
|
|
463
|
+
CM[Cashier/Mechanic] -->|Input Data| CRPMS[CRPMS System]
|
|
464
|
+
CRPMS -->|Reports & Bills| CM
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
subgraph "Level 1 - Processes"
|
|
468
|
+
P1["1.0 Manage Cars"]
|
|
469
|
+
P2["2.0 Manage Services"]
|
|
470
|
+
P3["3.0 Record Service Records"]
|
|
471
|
+
P4["4.0 Record Payments"]
|
|
472
|
+
P5["5.0 Generate Reports & Bills"]
|
|
473
|
+
P6["6.0 Authentication"]
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
subgraph "Data Stores"
|
|
477
|
+
D1[(Car)]
|
|
478
|
+
D2[(Services)]
|
|
479
|
+
D3[(ServiceRecord)]
|
|
480
|
+
D4[(Payment)]
|
|
481
|
+
D5[(User)]
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
CM -->|"Login"| P6
|
|
485
|
+
P6 --> D5
|
|
486
|
+
CM -->|"Car Details"| P1
|
|
487
|
+
P1 --> D1
|
|
488
|
+
CM -->|"Service Types"| P2
|
|
489
|
+
P2 --> D2
|
|
490
|
+
CM -->|"Service Record"| P3
|
|
491
|
+
P3 --> D1
|
|
492
|
+
P3 --> D2
|
|
493
|
+
P3 --> D3
|
|
494
|
+
CM -->|"Payment Data"| P4
|
|
495
|
+
P4 --> D3
|
|
496
|
+
P4 --> D4
|
|
497
|
+
P4 --> D5
|
|
498
|
+
CM -->|"Request Report/Bill"| P5
|
|
499
|
+
P5 --> D1
|
|
500
|
+
P5 --> D2
|
|
501
|
+
P5 --> D3
|
|
502
|
+
P5 --> D4`;
|
|
180
503
|
}
|
|
504
|
+
return { erd, dfd };
|
|
505
|
+
}
|
|
181
506
|
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
507
|
+
function initMermaidFile(filePath, projectType) {
|
|
508
|
+
const diags = generateMermaidDiagrams(projectType);
|
|
509
|
+
const title = projectType === 'SIMS' ? 'Stock Inventory Management System (SIMS)' : 'Car Repair Payment Management System (CRPMS)';
|
|
510
|
+
const content = `# ${title} - ERD & DFD Diagrams
|
|
511
|
+
## Project: ${projectType}
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## Entity Relationship Diagram (ERD)
|
|
516
|
+
|
|
517
|
+
\`\`\`mermaid
|
|
518
|
+
${diags.erd}
|
|
519
|
+
\`\`\`
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## Data Flow Diagram (DFD)
|
|
524
|
+
|
|
525
|
+
\`\`\`mermaid
|
|
526
|
+
${diags.dfd}
|
|
527
|
+
\`\`\`
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
*Generated by npms-exam-kit on ${new Date().toISOString().split('T')[0]}*
|
|
532
|
+
`;
|
|
533
|
+
writeFileSync(filePath, content, 'utf8');
|
|
534
|
+
return filePath;
|
|
195
535
|
}
|
|
196
536
|
|
|
537
|
+
function log(m) { console.log(m); }
|
|
538
|
+
|
|
197
539
|
async function main() {
|
|
198
540
|
console.clear();
|
|
199
|
-
log(chalk.bold.hex('#00D2FF')('\n
|
|
200
|
-
log(chalk.bold.hex('#00D2FF')(' ║
|
|
201
|
-
log(chalk.bold.hex('#00D2FF')(' ║
|
|
202
|
-
log(chalk.bold.hex('#00D2FF')(' ║
|
|
203
|
-
log(chalk.bold.hex('#00D2FF')('
|
|
541
|
+
log(chalk.bold.hex('#00D2FF')('\n ╔══════════════════════════════════════════════════╗'));
|
|
542
|
+
log(chalk.bold.hex('#00D2FF')(' ║ NESA NATIONAL PRACTICAL EXAM 2024-2025 ║'));
|
|
543
|
+
log(chalk.bold.hex('#00D2FF')(' ║ SECTOR: ICT & Multimedia ║'));
|
|
544
|
+
log(chalk.bold.hex('#00D2FF')(' ║ TRADE: Software Development ║'));
|
|
545
|
+
log(chalk.bold.hex('#00D2FF')(' ╚══════════════════════════════════════════════════╝\n'));
|
|
546
|
+
|
|
547
|
+
const { mode } = await inquirer.prompt([{
|
|
548
|
+
type: 'list',
|
|
549
|
+
name: 'mode',
|
|
550
|
+
message: 'What do you want to do?',
|
|
551
|
+
choices: [
|
|
552
|
+
{ name: 'Install a project + Auto-Assess', value: 'install' },
|
|
553
|
+
{ name: 'Auto-Assess an existing project (already installed)', value: 'assess' },
|
|
554
|
+
{ name: 'Generate ERD & DFD Mermaid diagrams for a project', value: 'mermaid' },
|
|
555
|
+
]
|
|
556
|
+
}]);
|
|
204
557
|
|
|
205
|
-
|
|
558
|
+
const { project } = await inquirer.prompt([{
|
|
559
|
+
type: 'list',
|
|
560
|
+
name: 'project',
|
|
561
|
+
message: 'Select project:',
|
|
562
|
+
choices: [
|
|
563
|
+
{ name: 'SIMS - Stock Inventory Management System (SmartPark)', value: 'SIMS' },
|
|
564
|
+
{ name: 'CRPMS - Car Repair Payment Management System', value: 'CRPMS' },
|
|
565
|
+
]
|
|
566
|
+
}]);
|
|
206
567
|
|
|
207
568
|
const targetDir = join(process.cwd());
|
|
208
569
|
|
|
570
|
+
if (mode === 'mermaid') {
|
|
571
|
+
const filePath = join(targetDir, `${project}_ERD_DFD.md`);
|
|
572
|
+
initMermaidFile(filePath, project);
|
|
573
|
+
log(chalk.green(`\n ✓ Mermaid diagrams saved to: ${chalk.bold(filePath)}\n`));
|
|
574
|
+
log(chalk.dim(' Open this file in any Markdown viewer that supports Mermaid'));
|
|
575
|
+
log(chalk.dim(' (VS Code, GitHub, or https://mermaid.live)\n'));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (mode === 'assess') {
|
|
580
|
+
log(chalk.bold(`\n Running auto-assessment for ${project} in: ${targetDir}\n`));
|
|
581
|
+
const checks = buildChecklist(targetDir, project);
|
|
582
|
+
const results = checks.map(c => {
|
|
583
|
+
let status;
|
|
584
|
+
try {
|
|
585
|
+
const result = c.auto();
|
|
586
|
+
status = result === true || result === 'PASS' ? 'PASS' : (result === 'MANUAL' ? 'MANUAL' : 'FAIL');
|
|
587
|
+
} catch { status = 'MANUAL'; }
|
|
588
|
+
return { ...c, status, guide: c.guide || '' };
|
|
589
|
+
});
|
|
590
|
+
showResults(results);
|
|
591
|
+
const reportDir = join(targetDir, 'checklist_report');
|
|
592
|
+
mkdirSync(reportDir, { recursive: true });
|
|
593
|
+
const reportFile = join(reportDir, `assessment_${project}_${Date.now()}.json`);
|
|
594
|
+
writeFileSync(reportFile, JSON.stringify({
|
|
595
|
+
project, date: new Date().toISOString(),
|
|
596
|
+
total: results.length,
|
|
597
|
+
passed: results.filter(r => r.status === 'PASS').length,
|
|
598
|
+
failed: results.filter(r => r.status === 'FAIL').length,
|
|
599
|
+
manual: results.filter(r => r.status === 'MANUAL').length,
|
|
600
|
+
results
|
|
601
|
+
}, null, 2));
|
|
602
|
+
log(chalk.dim(` Report saved: ${reportFile}\n`));
|
|
603
|
+
|
|
604
|
+
const { genDiag } = await inquirer.prompt([{
|
|
605
|
+
type: 'confirm', name: 'genDiag',
|
|
606
|
+
message: 'Generate ERD & DFD Mermaid diagrams for this project?',
|
|
607
|
+
default: true
|
|
608
|
+
}]);
|
|
609
|
+
if (genDiag) {
|
|
610
|
+
const filePath = join(targetDir, `${project}_ERD_DFD.md`);
|
|
611
|
+
initMermaidFile(filePath, project);
|
|
612
|
+
log(chalk.green(` ✓ Diagrams saved to: ${chalk.bold(filePath)}\n`));
|
|
613
|
+
}
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// INSTALL mode
|
|
209
618
|
const exist = [];
|
|
210
619
|
if (existsSync(join(targetDir, 'backend-project'))) exist.push('backend-project');
|
|
211
620
|
if (existsSync(join(targetDir, 'frontend-project'))) exist.push('frontend-project');
|
|
212
621
|
|
|
213
|
-
let proceed = true;
|
|
214
622
|
if (exist.length > 0) {
|
|
215
|
-
log(chalk.yellow(` Warning: ${exist.join(', ')} already exists
|
|
623
|
+
log(chalk.yellow(` Warning: ${exist.join(', ')} already exists.`));
|
|
216
624
|
const { overwrite } = await inquirer.prompt([{
|
|
217
|
-
type: 'confirm',
|
|
218
|
-
|
|
219
|
-
message: 'Overwrite existing files?',
|
|
220
|
-
default: false
|
|
625
|
+
type: 'confirm', name: 'overwrite',
|
|
626
|
+
message: 'Overwrite existing files?', default: false
|
|
221
627
|
}]);
|
|
222
|
-
if (overwrite) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
for (const f of sqlFiles) { const p = join(targetDir, f); if (existsSync(p)) fs.removeSync(p); }
|
|
227
|
-
} else {
|
|
228
|
-
proceed = false;
|
|
229
|
-
log(chalk.yellow(' Operation cancelled.'));
|
|
230
|
-
process.exit(0);
|
|
628
|
+
if (!overwrite) { log(chalk.yellow(' Cancelled.')); process.exit(0); }
|
|
629
|
+
for (const f of ['backend-project', 'frontend-project', 'database.sql']) {
|
|
630
|
+
const p = join(targetDir, f);
|
|
631
|
+
if (existsSync(p)) fs.removeSync(p);
|
|
231
632
|
}
|
|
232
633
|
}
|
|
233
634
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const { project } = await inquirer.prompt([{
|
|
240
|
-
type: 'list',
|
|
241
|
-
name: 'project',
|
|
242
|
-
message: 'Select project to install:',
|
|
243
|
-
choices: PROJECT_CHOICES.map(p => ({ name: `${p.name} - ${p.description}`, value: p }))
|
|
244
|
-
}]);
|
|
245
|
-
|
|
246
|
-
log(chalk.bold(`\n Installing ${project.name}...\n`));
|
|
635
|
+
const projectDir = project === 'SIMS' ? 'SIMS-master' : 'CRPMS-main';
|
|
636
|
+
const srcDir = join(PROJECTS_DIR, projectDir);
|
|
637
|
+
if (!existsSync(srcDir)) { log(chalk.red(` Error: Source ${srcDir} not found`)); process.exit(1); }
|
|
247
638
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
log(chalk.red(` Error: Project source not found at ${srcProjectDir}`));
|
|
251
|
-
process.exit(1);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const spinnerChars = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
639
|
+
log(chalk.bold(`\n Installing ${project}...`));
|
|
640
|
+
const spinChars = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
255
641
|
let si = 0;
|
|
256
|
-
const spin = setInterval(() => { process.stdout.write(`\r ${chalk.cyan(
|
|
642
|
+
const spin = setInterval(() => { process.stdout.write(`\r ${chalk.cyan(spinChars[si++ % spinChars.length])} Installing...`); }, 80);
|
|
257
643
|
|
|
258
644
|
try {
|
|
259
|
-
const entries = readdirSync(
|
|
645
|
+
const entries = readdirSync(srcDir);
|
|
260
646
|
for (const entry of entries) {
|
|
261
647
|
if (entry === 'node_modules') continue;
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
if (statSync(
|
|
265
|
-
|
|
266
|
-
} else {
|
|
267
|
-
copyFileSync(srcPath, destPath);
|
|
268
|
-
}
|
|
648
|
+
const s = join(srcDir, entry);
|
|
649
|
+
const d = join(targetDir, entry);
|
|
650
|
+
if (statSync(s).isDirectory()) fs.copySync(s, d, { filter: f => !f.includes('node_modules') });
|
|
651
|
+
else copyFileSync(s, d);
|
|
269
652
|
}
|
|
270
|
-
clearInterval(spin);
|
|
271
|
-
process.stdout.write('\r \r');
|
|
272
|
-
log(chalk.green(' ✓ Project files copied\n'));
|
|
273
653
|
|
|
274
|
-
const
|
|
275
|
-
const
|
|
654
|
+
const beDir = join(targetDir, 'backend-project');
|
|
655
|
+
const feDir = join(targetDir, 'frontend-project');
|
|
276
656
|
|
|
277
|
-
if (existsSync(
|
|
278
|
-
|
|
657
|
+
if (existsSync(beDir) && existsSync(join(beDir, 'package.json'))) {
|
|
658
|
+
process.chdir(beDir);
|
|
659
|
+
log(chalk.cyan('\n Installing Backend dependencies...'));
|
|
660
|
+
try { await promisify(exec)('npm install', { timeout: 120000, windowsHide: true }); log(chalk.green(' ✓ Backend done')); } catch (e) { log(chalk.yellow(` ⚠ Backend npm install: ${e.message}`)); }
|
|
279
661
|
}
|
|
280
|
-
if (existsSync(
|
|
281
|
-
|
|
662
|
+
if (existsSync(feDir) && existsSync(join(feDir, 'package.json'))) {
|
|
663
|
+
process.chdir(feDir);
|
|
664
|
+
log(chalk.cyan(' Installing Frontend dependencies...'));
|
|
665
|
+
try { await promisify(exec)('npm install', { timeout: 120000, windowsHide: true }); log(chalk.green(' ✓ Frontend done')); } catch (e) { log(chalk.yellow(` ⚠ Frontend npm install: ${e.message}`)); }
|
|
282
666
|
}
|
|
283
667
|
|
|
284
668
|
process.chdir(targetDir);
|
|
669
|
+
clearInterval(spin);
|
|
670
|
+
process.stdout.write('\r \r');
|
|
671
|
+
log(chalk.green(' ✓ Project installed\n'));
|
|
285
672
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
log(chalk.dim(' Database SQL file found. Import instructions:\n'));
|
|
290
|
-
log(` ${chalk.cyan(' 1) Open MySQL client (MySQL Workbench / phpMyAdmin / CLI')}`);
|
|
291
|
-
log(` ${chalk.cyan(' 2) Execute the SQL script from:')}`);
|
|
292
|
-
log(` ${chalk.dim(dbSqlPath)}`);
|
|
293
|
-
log(` ${chalk.cyan(` 3) This will create the "${project.db}" database and all tables`)}\n`);
|
|
294
|
-
|
|
295
|
-
const { doDb } = await inquirer.prompt([{
|
|
296
|
-
type: 'confirm',
|
|
297
|
-
name: 'doDb',
|
|
298
|
-
message: 'Attempt automatic database import? (requires MySQL CLI)',
|
|
299
|
-
default: false
|
|
300
|
-
}]);
|
|
301
|
-
|
|
302
|
-
if (doDb) {
|
|
303
|
-
const dbSql = readFileSync(dbSqlPath, 'utf8');
|
|
304
|
-
try {
|
|
305
|
-
await execAsync('mysql -u root', { input: dbSql, timeout: 30000, windowsHide: true });
|
|
306
|
-
log(chalk.green(' ✓ Database imported successfully'));
|
|
307
|
-
} catch (err) {
|
|
308
|
-
log(chalk.yellow(` ⚠ Could not auto-import: ${err.message}`));
|
|
309
|
-
log(chalk.dim(' Please import manually using the SQL file.'));
|
|
310
|
-
}
|
|
311
|
-
}
|
|
673
|
+
if (existsSync(join(beDir, '.env.example')) && !existsSync(join(beDir, '.env'))) {
|
|
674
|
+
copyFileSync(join(beDir, '.env.example'), join(beDir, '.env'));
|
|
675
|
+
log(chalk.green(' ✓ .env created'));
|
|
312
676
|
}
|
|
313
677
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
678
|
+
log(chalk.bold('\n ─── QUICK START ───\n'));
|
|
679
|
+
log(` ${chalk.cyan('1. Import database:')} Run database.sql in MySQL`);
|
|
680
|
+
log(` ${chalk.cyan('2. Start backend:')} cd backend-project && npm start (port 5000)`);
|
|
681
|
+
log(` ${chalk.cyan('3. Start frontend:')} cd frontend-project && npm run dev (port 5173)`);
|
|
682
|
+
const dbName = project === 'SIMS' ? 'SIMS' : 'CRPMS';
|
|
683
|
+
log(` ${chalk.cyan('4. Open browser:')} http://localhost:5173`);
|
|
684
|
+
log(` ${chalk.cyan(' Login:')} ${project === 'SIMS' ? 'admin / admin123' : 'admin / Admin@1234'}`);
|
|
685
|
+
log(` ${chalk.cyan(' Database:')} ${dbName}\n`);
|
|
686
|
+
|
|
687
|
+
const { doAssess } = await inquirer.prompt([{
|
|
688
|
+
type: 'confirm', name: 'doAssess',
|
|
689
|
+
message: 'Run auto-assessment checklist now?', default: true
|
|
690
|
+
}]);
|
|
691
|
+
|
|
692
|
+
if (doAssess) {
|
|
693
|
+
const checks = buildChecklist(targetDir, project);
|
|
694
|
+
const results = checks.map(c => {
|
|
695
|
+
let status;
|
|
696
|
+
try {
|
|
697
|
+
const result = c.auto();
|
|
698
|
+
status = result === true || result === 'PASS' ? 'PASS' : (result === 'MANUAL' ? 'MANUAL' : 'FAIL');
|
|
699
|
+
} catch { status = 'MANUAL'; }
|
|
700
|
+
return { ...c, status, guide: c.guide || '' };
|
|
701
|
+
});
|
|
702
|
+
showResults(results);
|
|
703
|
+
const reportDir = join(targetDir, 'checklist_report');
|
|
704
|
+
mkdirSync(reportDir, { recursive: true });
|
|
705
|
+
const reportFile = join(reportDir, `assessment_${project}_${Date.now()}.json`);
|
|
706
|
+
writeFileSync(reportFile, JSON.stringify({
|
|
707
|
+
project, date: new Date().toISOString(),
|
|
708
|
+
total: results.length,
|
|
709
|
+
passed: results.filter(r => r.status === 'PASS').length,
|
|
710
|
+
failed: results.filter(r => r.status === 'FAIL').length,
|
|
711
|
+
manual: results.filter(r => r.status === 'MANUAL').length,
|
|
712
|
+
results
|
|
713
|
+
}, null, 2));
|
|
714
|
+
log(chalk.dim(` Report saved: ${reportFile}\n`));
|
|
320
715
|
}
|
|
321
716
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
log(` ${chalk.cyan('Project:')} ${project.name}`);
|
|
326
|
-
log(` ${chalk.cyan('Database:')} ${project.db}`);
|
|
327
|
-
log(` ${chalk.cyan('Default User:')} ${project.defaultUser}`);
|
|
328
|
-
log(` ${chalk.cyan('Backend:')} cd backend-project && npm start (port 5000)`);
|
|
329
|
-
log(` ${chalk.cyan('Frontend:')} cd frontend-project && npm run dev (port 5173)`);
|
|
330
|
-
log(chalk.dim(`\n ${targetDir}\n`));
|
|
331
|
-
|
|
332
|
-
const { runCheck } = await inquirer.prompt([{
|
|
333
|
-
type: 'confirm',
|
|
334
|
-
name: 'runCheck',
|
|
335
|
-
message: 'Run the NESA assessment checklist now?',
|
|
336
|
-
default: true
|
|
717
|
+
const { genDiag } = await inquirer.prompt([{
|
|
718
|
+
type: 'confirm', name: 'genDiag',
|
|
719
|
+
message: 'Generate ERD & DFD Mermaid diagrams?', default: true
|
|
337
720
|
}]);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
721
|
+
if (genDiag) {
|
|
722
|
+
const filePath = join(targetDir, `${project}_ERD_DFD.md`);
|
|
723
|
+
initMermaidFile(filePath, project);
|
|
724
|
+
log(chalk.green(` ✓ Diagrams saved to: ${chalk.bold(filePath)}\n`));
|
|
341
725
|
}
|
|
342
726
|
|
|
343
|
-
log(chalk.bold.green('\n ✓
|
|
344
|
-
log(`\n ${chalk.dim('To start the backend:')}`);
|
|
345
|
-
log(` ${chalk.cyan(' cd backend-project && npm start')}`);
|
|
346
|
-
log(`\n ${chalk.dim('To start the frontend (new terminal):')}`);
|
|
347
|
-
log(` ${chalk.cyan(' cd frontend-project && npm run dev')}`);
|
|
348
|
-
log(`\n ${chalk.dim('Open:')} ${chalk.cyan('http://localhost:5173')}\n`);
|
|
727
|
+
log(chalk.bold.green('\n ✓ All done!\n'));
|
|
349
728
|
|
|
350
729
|
} catch (err) {
|
|
351
730
|
clearInterval(spin);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "npms-exam-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "NESA National Practical Exam Projects Installer - SIMS & CRPMS",
|
|
5
5
|
"bin": "bin/exam-kit.js",
|
|
6
6
|
"keywords": [
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"chalk": "^5.3.0",
|
|
17
17
|
"inquirer": "^9.2.0",
|
|
18
|
-
"ora": "^8.0.0",
|
|
19
18
|
"fs-extra": "^11.2.0"
|
|
20
19
|
},
|
|
21
20
|
"type": "module",
|
|
@@ -24,7 +23,6 @@
|
|
|
24
23
|
},
|
|
25
24
|
"files": [
|
|
26
25
|
"bin/",
|
|
27
|
-
"src/",
|
|
28
26
|
"projects/"
|
|
29
27
|
]
|
|
30
28
|
}
|