npms-exam-kit 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/exam-kit.js +440 -620
  2. package/package.json +1 -1
package/bin/exam-kit.js CHANGED
@@ -4,7 +4,7 @@ import { dirname, join } from 'path';
4
4
  import fs from 'fs-extra';
5
5
  import chalk from 'chalk';
6
6
  import inquirer from 'inquirer';
7
- import { execSync, exec } from 'child_process';
7
+ import { exec } from 'child_process';
8
8
  import { promisify } from 'util';
9
9
  import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync, readdirSync, statSync } from 'fs';
10
10
 
@@ -16,527 +16,448 @@ const execAsync = promisify(exec);
16
16
 
17
17
  function read(p) { try { return existsSync(p) ? readFileSync(p, 'utf8') : ''; } catch { return ''; } }
18
18
  function inDir(p) { try { return existsSync(p) ? readdirSync(p) : []; } catch { return []; } }
19
+ function log(m) { console.log(m); }
19
20
 
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'];
21
+ const SIMS_PAGES = ['Login.jsx','SparePart.jsx','StockIn.jsx','StockOut.jsx','Reports.jsx'];
22
+ const CRPMS_PAGES = ['Login.jsx','Cars.jsx','Services.jsx','ServiceRecords.jsx','Payments.jsx','Reports.jsx','Bill.jsx'];
23
+
24
+ const SIMS_ENDPOINTS = [
25
+ { method:'POST', path:'/api/auth/login', body:'{"username":"admin","password":"admin123"}', desc:'Login' },
26
+ { method:'POST', path:'/api/auth/logout', body:'{}', desc:'Logout' },
27
+ { method:'GET', path:'/api/auth/check', body:'', desc:'Check auth session' },
28
+ { method:'GET', path:'/api/spare-parts', body:'', desc:'List spare parts' },
29
+ { method:'POST', path:'/api/spare-parts', body:'{"Name":"Brake Pad","Category":"Brakes","Quantity":50,"UnitPrice":15000}', desc:'Add spare part' },
30
+ { method:'GET', path:'/api/stock-in', body:'', desc:'List stock in records' },
31
+ { method:'POST', path:'/api/stock-in', body:'{"SparePartID":1,"StockInQuantity":20,"StockInDate":"2025-05-31"}', desc:'Record stock in' },
32
+ { method:'GET', path:'/api/stock-out', body:'', desc:'List stock out records' },
33
+ { method:'POST', path:'/api/stock-out', body:'{"SparePartID":1,"StockOutQuantity":5,"StockOutUnitPrice":18000,"StockOutDate":"2025-05-31"}', desc:'Record stock out' },
34
+ { method:'PUT', path:'/api/stock-out/1', body:'{"SparePartID":1,"StockOutQuantity":3,"StockOutUnitPrice":18000,"StockOutDate":"2025-05-31"}', desc:'Update stock out' },
35
+ { method:'DELETE', path:'/api/stock-out/1', body:'', desc:'Delete stock out' },
36
+ { method:'GET', path:'/api/reports/daily-stockout?date=2025-05-31', body:'', desc:'Daily stock out report' },
37
+ { method:'GET', path:'/api/reports/stock-status?date=2025-05-31', body:'', desc:'Stock status report' },
38
+ ];
39
+
40
+ const CRPMS_ENDPOINTS = [
41
+ { method:'POST', path:'/api/auth/login', body:'{"username":"admin","password":"Admin@1234"}', desc:'Login' },
42
+ { method:'POST', path:'/api/auth/logout', body:'{}', desc:'Logout' },
43
+ { method:'GET', path:'/api/auth/me', body:'', desc:'Check auth session' },
44
+ { method:'GET', path:'/api/cars', body:'', desc:'List cars' },
45
+ { method:'POST', path:'/api/cars', body:'{"PlateNumber":"RAB123A","type":"Sedan","Model":"Toyota","ManufacturingYear":2020,"DriverPhone":"0788123456","MechanicName":"John"}', desc:'Add car' },
46
+ { method:'GET', path:'/api/services', body:'', desc:'List services' },
47
+ { method:'POST', path:'/api/services', body:'{"ServiceCode":"SRV007","ServiceName":"AC Repair","ServicePrice":75000}', desc:'Add service' },
48
+ { method:'GET', path:'/api/service-records', body:'', desc:'List service records' },
49
+ { method:'POST', path:'/api/service-records', body:'{"ServiceDate":"2025-05-31","PlateNumber":"RAB123A","ServiceCode":"SRV001"}', desc:'Create service record' },
50
+ { method:'GET', path:'/api/service-records/1', body:'', desc:'Get one service record' },
51
+ { method:'PUT', path:'/api/service-records/1', body:'{"ServiceDate":"2025-05-31","PlateNumber":"RAB123A","ServiceCode":"SRV002"}', desc:'Update service record' },
52
+ { method:'DELETE', path:'/api/service-records/1', body:'', desc:'Delete service record' },
53
+ { method:'GET', path:'/api/payments', body:'', desc:'List payments' },
54
+ { method:'POST', path:'/api/payments', body:'{"AmountPaid":150000,"PaymentDate":"2025-05-31","RecordNumber":1}', desc:'Record payment' },
55
+ { method:'GET', path:'/api/payments/1/bill', body:'', desc:'Get payment bill' },
56
+ { method:'GET', path:'/api/reports/daily?date=2025-05-31', body:'', desc:'Daily report' },
57
+ { method:'GET', path:'/api/reports/summary', body:'', desc:'Summary report' },
58
+ ];
49
59
 
50
60
  function buildChecklist(baseDir, projectType) {
51
61
  const b = p => join(baseDir, p);
52
62
  const fe = p => join(baseDir, 'frontend-project', p);
53
63
  const be = p => join(baseDir, 'backend-project', p);
54
64
  const src = p => join(baseDir, 'frontend-project', 'src', p);
55
- const pages = projectType === 'SIMS' ? SIMS_PAGES : CRPMS_PAGES;
56
65
  const dbName = projectType === 'SIMS' ? 'SIMS' : 'CRPMS';
57
-
58
66
  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') : 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') : 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') : 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')) : (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.' },
67
+ { id:'C1', cat:'Preliminary (15%)', label:'ERD Spare_Part entity drawn',
68
+ auto:()=>'MANUAL', guide:'Draw Spare_Part entity on paper with entity symbol' },
69
+ { id:'C2', cat:'Preliminary (15%)', label:'ERD Stock_In entity drawn',
70
+ auto:()=>'MANUAL', guide:'Draw Stock_In entity on paper' },
71
+ { id:'C3', cat:'Preliminary (15%)', label:'ERD Stock_Out entity drawn',
72
+ auto:()=>'MANUAL', guide:'Draw Stock_Out entity on paper' },
73
+ { id:'C4', cat:'Preliminary (15%)', label:'ERD entity symbol used',
74
+ auto:()=>'MANUAL', guide:'Use rectangle/box symbol for each entity' },
75
+ { id:'C5', cat:'Preliminary (15%)', label:'ERD relationship symbol used',
76
+ auto:()=>'MANUAL', guide:'Use diamond/line symbols for relationships' },
77
+ { id:'C6', cat:'Preliminary (15%)', label:'ERD link symbol used',
78
+ auto:()=>'MANUAL', guide:'Use connecting lines between entities and relationships' },
79
+ { id:'C7', cat:'Preliminary (15%)', label:'ERD Primary Key rule respected',
80
+ auto:()=>'MANUAL', guide:'Underline PK attributes in each entity' },
81
+ { id:'C8', cat:'Preliminary (15%)', label:'ERD Foreign Key rule respected',
82
+ auto:()=>'MANUAL', guide:'Mark FK attributes with dotted underline' },
83
+ { id:'C9', cat:'Preliminary (15%)', label:'ERD cardinalities indicated',
84
+ auto:()=>'MANUAL', guide:'Show 1---* or M---N on relationship lines' },
85
+ { id:'C10', cat:'Preliminary (15%)', label:'Spare_Part-Stock_In relationship drawn',
86
+ auto:()=>'MANUAL', guide:'Connect Spare_Part (1) to Stock_In (*) via SparePartID FK' },
87
+ { id:'C11', cat:'Preliminary (15%)', label:'Users-Stock_Out relationship drawn',
88
+ auto:()=>'MANUAL', guide:'Connect Users (1) to Stock_Out (*) via UserID FK' },
89
+ { id:'C12', cat:'Preliminary (15%)', label:'Spare_Part-Stock_Out relationship drawn',
90
+ auto:()=>'MANUAL', guide:'Connect Spare_Part (1) to Stock_Out (*) via SparePartID FK' },
91
+ { id:'C13', cat:'Preliminary (15%)', label:'Users PK indicated',
92
+ auto:()=>'MANUAL', guide:'Underline UserID in Users entity' },
93
+ { id:'C14', cat:'Preliminary (15%)', label:'Spare_Part PK indicated',
94
+ auto:()=>'MANUAL', guide:'Underline SparePartID in Spare_Part' },
95
+ { id:'C15', cat:'Preliminary (15%)', label:'Stock_In PK indicated',
96
+ auto:()=>'MANUAL', guide:'Underline StockInID in Stock_In' },
97
+ { id:'C16', cat:'Preliminary (15%)', label:'Stock_Out PK indicated',
98
+ auto:()=>'MANUAL', guide:'Underline StockOutID in Stock_Out' },
99
+ { id:'C17', cat:'Preliminary (15%)', label:'Users FK in Stock_Out indicated',
100
+ auto:()=>'MANUAL', guide:'Mark UserID as FK in Stock_Out' },
101
+ { id:'C18', cat:'Preliminary (15%)', label:'Spare_Part FK in Stock_In indicated',
102
+ auto:()=>'MANUAL', guide:'Mark SparePartID as FK in Stock_In' },
103
+ { id:'C19', cat:'Preliminary (15%)', label:'Spare_Part FK in Stock_Out indicated',
104
+ auto:()=>'MANUAL', guide:'Mark SparePartID as FK in Stock_Out' },
105
+ { id:'C20', cat:'Process (50%)', label:'Project folder named FirstName_LastName_Exam_2025',
106
+ auto:()=>!baseDir.includes('node_modules')&&baseDir.split('_').length>=3?'PASS':'FAIL',
107
+ guide:'Rename parent folder to FirstName_LastName_National_Practical_Exam_2025' },
108
+ { id:'C21', cat:'Process (50%)', label:'Node.js project created (package.json)',
109
+ auto:()=>existsSync(be('package.json'))?'PASS':'FAIL', guide:'Run: cd backend-project && npm init -y' },
110
+ { id:'C22', cat:'Process (50%)', label:'Express.js installed',
111
+ auto:()=>existsSync(be('node_modules/express'))?'PASS':'FAIL', guide:'Run: cd backend-project && npm install express' },
112
+ { id:'C23', cat:'Process (50%)', label:'Cors installed',
113
+ auto:()=>existsSync(be('node_modules/cors'))?'PASS':'FAIL', guide:'Run: cd backend-project && npm install cors' },
114
+ { id:'C24', cat:'Process (50%)', label:'Nodemon installed',
115
+ auto:()=>existsSync(be('node_modules/nodemon'))?'PASS':'FAIL', guide:'Run: cd backend-project && npm install --save-dev nodemon' },
116
+ { id:'C25', cat:'Process (50%)', label:'MySQL2 installed',
117
+ auto:()=>existsSync(be('node_modules/mysql2'))?'PASS':'FAIL', guide:'Run: cd backend-project && npm install mysql2' },
118
+ { id:'C26', cat:'Process (50%)', label:'React project created (vite)',
119
+ auto:()=>existsSync(fe('package.json'))&&existsSync(fe('vite.config.js'))?'PASS':'FAIL', guide:'Run: npx create-vite frontend-project --template react' },
120
+ { id:'C27', cat:'Process (50%)', label:'React-router-dom installed',
121
+ auto:()=>existsSync(fe('node_modules/react-router-dom'))?'PASS':'FAIL', guide:'Run: cd frontend-project && npm install react-router-dom' },
122
+ { id:'C28', cat:'Process (50%)', label:'Axios installed',
123
+ auto:()=>existsSync(fe('node_modules/axios'))?'PASS':'FAIL', guide:'Run: cd frontend-project && npm install axios' },
124
+ { id:'C29', cat:'Process (50%)', label:`${dbName} database SQL created`,
125
+ auto:()=>existsSync(b('database.sql'))&&read(b('database.sql')).includes(dbName)?'PASS':'FAIL', guide:'Create database.sql with CREATE DATABASE' },
126
+ { id:'C30', cat:'Process (50%)', label:'Users table created in SQL',
127
+ auto:()=>read(b('database.sql')).includes('CREATE TABLE')&&read(b('database.sql')).toLowerCase().includes('user')?'PASS':'FAIL',
128
+ guide:'Add CREATE TABLE Users (UserID INT PK, Username, Password)' },
129
+ { id:'C31', cat:'Process (50%)', label:`${projectType==='SIMS'?'Spare_Part':'Services'} table created`,
130
+ auto:()=>{const s=read(b('database.sql')).toLowerCase();return projectType==='SIMS'?(s.includes('spare_part')?'PASS':'FAIL'):(s.includes('services')?'PASS':'FAIL');},
131
+ guide:projectType==='SIMS'?'Add CREATE TABLE Spare_Part (SparePartID PK, Name, Category, Quantity, UnitPrice, TotalPrice)':'Add CREATE TABLE Services (ServiceCode PK, ServiceName, ServicePrice)' },
132
+ { id:'C32', cat:'Process (50%)', label:`${projectType==='SIMS'?'Stock_Out':'ServiceRecord'} table created`,
133
+ auto:()=>{const s=read(b('database.sql')).toLowerCase();return projectType==='SIMS'?(s.includes('stock_out')?'PASS':'FAIL'):(s.includes('servicerecord')?'PASS':'FAIL');},
134
+ guide:projectType==='SIMS'?'Add CREATE TABLE Stock_Out':'Add CREATE TABLE ServiceRecord' },
135
+ { id:'C33', cat:'Process (50%)', label:`${projectType==='SIMS'?'Stock_In':'Payment'} table created`,
136
+ auto:()=>{const s=read(b('database.sql')).toLowerCase();return projectType==='SIMS'?(s.includes('stock_in')?'PASS':'FAIL'):(s.includes('payment')?'PASS':'FAIL');},
137
+ guide:projectType==='SIMS'?'Add CREATE TABLE Stock_In':'Add CREATE TABLE Payment' },
138
+ { id:'C34', cat:'Process (50%)', label:'Primary keys applied in all tables',
139
+ auto:()=>read(b('database.sql')).includes('PRIMARY KEY')?'PASS':'FAIL', guide:'Add PRIMARY KEY to each table definition' },
140
+ { id:'C35', cat:'Process (50%)', label:'FKs applied in related tables',
141
+ auto:()=>read(b('database.sql')).includes('FOREIGN KEY')?'PASS':'FAIL', guide:'Add FOREIGN KEY references in child tables' },
142
+ { id:'C36', cat:'Process (50%)', label:'React function component declared',
143
+ auto:()=>read(src('App.jsx')).includes('function')||read(src('App.jsx')).includes('=>')||read(src('App.jsx')).includes('export default function')?'PASS':'FAIL',
144
+ guide:'Use function App() { ... } in App.jsx' },
145
+ { id:'C37', cat:'Process (50%)', label:'Return method included',
146
+ auto:()=>read(src('App.jsx')).includes('return')?'PASS':'FAIL', guide:'Add return( <JSX> ) in component' },
147
+ { id:'C38', cat:'Process (50%)', label:'Component exported',
148
+ auto:()=>read(src('App.jsx')).includes('export default')?'PASS':'FAIL', guide:'Add "export default App;"' },
149
+ { id:'C39', cat:'Process (50%)', label:'Component mounted to DOM',
150
+ auto:()=>existsSync(src('main.jsx'))&&read(src('main.jsx')).includes('ReactDOM')?'PASS':'FAIL',
151
+ guide:'In main.jsx: createRoot().render(<App />)' },
152
+ { id:'C40', cat:'Process (50%)', label:'JSX used inside return',
153
+ auto:()=>read(src('App.jsx')).includes('return')&&(read(src('App.jsx')).includes('<')||read(src('App.jsx')).includes('React.createElement'))?'PASS':'FAIL',
154
+ guide:'Add JSX markup inside return()' },
155
+ { id:'C41', cat:'Process (50%)', label:'Login form created',
156
+ auto:()=>existsSync(src('pages/Login.jsx'))?'PASS':'FAIL', guide:'Create src/pages/Login.jsx' },
157
+ { id:'C42', cat:'Process (50%)', label:`${projectType==='SIMS'?'Spare_Part':'Cars/Services'} form created`,
158
+ 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');},
159
+ guide:projectType==='SIMS'?'Create SparePart.jsx':'Create Cars.jsx and Services.jsx' },
160
+ { id:'C43', cat:'Process (50%)', label:`${projectType==='SIMS'?'Stock_In':'ServiceRecord'} form created`,
161
+ auto:()=>inDir(src('pages')).some(f=>f.toLowerCase().includes(projectType==='SIMS'?'stockin':'record'))?'PASS':'FAIL',
162
+ guide:projectType==='SIMS'?'Create StockIn.jsx':'Create ServiceRecords.jsx' },
163
+ { id:'C44', cat:'Process (50%)', label:`${projectType==='SIMS'?'Stock_Out':'Payment'} form created`,
164
+ auto:()=>inDir(src('pages')).some(f=>f.toLowerCase().includes(projectType==='SIMS'?'stockout':'payment'))?'PASS':'FAIL',
165
+ guide:projectType==='SIMS'?'Create StockOut.jsx':'Create Payments.jsx' },
166
+ { id:'C45', cat:'Process (50%)', label:'Routes configured in React',
167
+ auto:()=>read(src('App.jsx')).includes('Routes')&&read(src('App.jsx')).includes('Route')?'PASS':'FAIL',
168
+ guide:'Add <Routes><Route ...></Routes> in App.jsx' },
169
+ { id:'C46', cat:'Process (50%)', label:'Links created between components',
170
+ auto:()=>read(src('App.jsx')).includes('Link')||read(src('components/Navbar.jsx')).includes('Link')?'PASS':'FAIL',
171
+ guide:'Use <Link to="/path"> for navigation' },
172
+ { id:'C47', cat:'Process (50%)', label:'Navigation layout created',
173
+ auto:()=>read(src('components/Navbar.jsx')).length>50?'PASS':'FAIL', guide:'Create Navbar.jsx' },
174
+ { id:'C48', cat:'Process (50%)', label:'Tailwind CSS installed',
175
+ auto:()=>existsSync(fe('node_modules/tailwindcss'))?'PASS':'FAIL', guide:'Run: npm install -D tailwindcss' },
176
+ { id:'C49', cat:'Process (50%)', label:'Tailwind CSS configured',
177
+ 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',
178
+ guide:'Configure tailwind.config.js or use @tailwindcss/vite plugin' },
179
+ { id:'C50', cat:'Process (50%)', label:'Server JS file created',
180
+ auto:()=>existsSync(be('server.js'))?'PASS':'FAIL', guide:'Create server.js with express app' },
181
+ { id:'C51', cat:'Process (50%)', label:'Express package imported',
182
+ auto:()=>read(be('server.js')).includes('express')?'PASS':'FAIL', guide:'const express = require(\'express\')' },
183
+ { id:'C52', cat:'Process (50%)', label:'Cors package imported',
184
+ auto:()=>read(be('server.js')).includes('cors')?'PASS':'FAIL', guide:'const cors = require(\'cors\')' },
185
+ { id:'C53', cat:'Process (50%)', label:'Port number identified',
186
+ auto:()=>read(be('server.js')).includes('PORT')?'PASS':'FAIL', guide:'const PORT = 5000' },
187
+ { id:'C54', cat:'Process (50%)', label:'Express object created',
188
+ auto:()=>read(be('server.js')).includes('express()')?'PASS':'FAIL', guide:'const app = express()' },
189
+ { id:'C55', cat:'Process (50%)', label:'Listen method applied',
190
+ auto:()=>read(be('server.js')).includes('listen')?'PASS':'FAIL', guide:'app.listen(PORT, callback)' },
191
+ { id:'C56', cat:'Process (50%)', label:'POST endpoint created',
192
+ auto:()=>inDir(be('routes')).some(f=>read(be('routes/'+f)).includes('router.post'))?'PASS':'FAIL', guide:'Add router.post() in route files' },
193
+ { id:'C57', cat:'Process (50%)', label:'GET endpoint created',
194
+ auto:()=>inDir(be('routes')).some(f=>read(be('routes/'+f)).includes('router.get'))?'PASS':'FAIL', guide:'Add router.get() in route files' },
195
+ { id:'C58', cat:'Process (50%)', label:'PUT endpoint created',
196
+ auto:()=>inDir(be('routes')).some(f=>read(be('routes/'+f)).includes('router.put'))?'PASS':'FAIL', guide:'Add router.put() in route files' },
197
+ { id:'C59', cat:'Process (50%)', label:'DELETE endpoint created',
198
+ auto:()=>inDir(be('routes')).some(f=>read(be('routes/'+f)).includes('router.delete'))?'PASS':'FAIL', guide:'Add router.delete() in route files' },
199
+ { id:'C60', cat:'Process (50%)', label:'Axios imported in frontend',
200
+ auto:()=>read(src('App.jsx')).includes('axios')||existsSync(src('api/axios.js'))?'PASS':'FAIL', guide:'import axios from \'axios\'' },
201
+ { id:'C61', cat:'Process (50%)', label:'bcryptjs installed (password encryption)',
202
+ auto:()=>existsSync(be('node_modules/bcryptjs'))?'PASS':'FAIL', guide:'npm install bcryptjs' },
203
+ { id:'C62', cat:'Process (50%)', label:'Express-session installed',
204
+ auto:()=>existsSync(be('node_modules/express-session'))?'PASS':'FAIL', guide:'npm install express-session' },
205
+ { id:'C63', cat:'Process (50%)', label:'Session-based login implemented',
206
+ auto:()=>read(be('server.js')).includes('session')&&read(be('routes/auth.js')).includes('session')?'PASS':'FAIL',
207
+ guide:'Use express-session with req.session.user' },
208
+ { id:'C64', cat:'Process (50%)', label:'DB config file exists',
209
+ auto:()=>existsSync(be('config/db.js'))?'PASS':'FAIL', guide:'Create config/db.js with mysql2 pool' },
210
+ { id:'C65', cat:'Process (50%)', label:'.env file configured',
211
+ auto:()=>existsSync(be('.env'))||existsSync(be('.env.example'))?'PASS':'FAIL', guide:'Create .env with DB_HOST, DB_USER, DB_PASSWORD, DB_NAME' },
212
+ { id:'C66', cat:'Process (50%)', label:'Tailwind imported in index.css',
213
+ auto:()=>read(src('index.css')).includes('tailwind')?'PASS':'FAIL', guide:'@tailwind base; @tailwind components; @tailwind utilities;' },
214
+ { id:'C67', cat:'Process (50%)', label:'Vite React plugin configured',
215
+ auto:()=>read(fe('vite.config.js')).includes('@vitejs/plugin-react')?'PASS':'FAIL', guide:'Add @vitejs/plugin-react in plugins' },
216
+ { id:'C68', cat:'Product (30%)', label:'Project presentable',
217
+ auto:()=>'MANUAL', guide:'Prepare presentation: product name, key steps, importance, challenges' },
218
+ { id:'C69', cat:'Product (30%)', label:'Input validation',
219
+ auto:()=>{const r=read(be('routes/stockOut.js'))||read(be('routes/payments.js'));return r.includes('return res.status(400)')?'PASS':'FAIL';},
220
+ guide:'Add if(!field) return res.status(400).json(...)' },
221
+ { id:'C70', cat:'Product (30%)', label:'Form saves data to DB',
222
+ 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';},
223
+ guide:'Use INSERT INTO in POST handlers' },
224
+ { id:'C71', cat:'Product (30%)', label:'Data retrieval works (GET)',
225
+ 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';},
226
+ guide:'Use SELECT in GET handlers and return JSON' },
227
+ { id:'C72', cat:'Product (30%)', label:'Update works (PUT)',
228
+ auto:()=>inDir(be('routes')).some(f=>read(be('routes/'+f)).includes('UPDATE')&&read(be('routes/'+f)).includes('router.put'))?'PASS':'FAIL',
229
+ guide:'Use UPDATE in PUT handlers' },
230
+ { id:'C73', cat:'Product (30%)', label:'Delete works (DELETE)',
231
+ auto:()=>inDir(be('routes')).some(f=>read(be('routes/'+f)).includes('DELETE')&&read(be('routes/'+f)).includes('router.delete'))?'PASS':'FAIL',
232
+ guide:'Use DELETE in DELETE handlers' },
233
+ { id:'C74', cat:'Product (30%)', label:'Navigation menus interactive',
234
+ auto:()=>read(src('components/Navbar.jsx')).includes('useState')&&read(src('components/Navbar.jsx')).includes('onClick')?'PASS':'FAIL',
235
+ guide:'Add onClick handlers and hamburger menu' },
236
+ { id:'C75', cat:'Product (30%)', label:'Buttons interactive',
237
+ auto:()=>(read(src('pages/Login.jsx')).includes('onClick')||read(src('pages/Login.jsx')).includes('onSubmit'))?'PASS':'FAIL',
238
+ guide:'Add onClick or onSubmit to buttons' },
239
+ { id:'C76', cat:'Product (30%)', label:'Responsive design (flexbox/grid)',
240
+ auto:()=>(read(src('components/Navbar.jsx')).includes('md:')||read(src('App.jsx')).includes('flex')||read(src('index.css')).includes('grid'))?'PASS':'FAIL',
241
+ guide:'Use Tailwind responsive prefixes (sm:, md:, lg:)' },
242
+ { id:'C77', cat:'Product (30%)', label:`Daily ${projectType==='SIMS'?'StockOut':'Service'} report`,
243
+ auto:()=>read(be('routes/reports.js')).includes(projectType==='SIMS'?'daily-stockout':'/daily')?'PASS':'FAIL',
244
+ guide:`Add GET /api/reports/${projectType==='SIMS'?'daily-stockout':'daily'} endpoint` },
245
+ { id:'C78', cat:'Product (30%)', label:'Stock Status / Summary report',
246
+ auto:()=>read(be('routes/reports.js')).includes('stock-status')||read(be('routes/reports.js')).includes('summary')?'PASS':'FAIL',
247
+ guide:'Add GET /api/reports/stock-status or /api/reports/summary' },
248
+ { id:'C79', cat:'Product (30%)', label:'Consistent styling',
249
+ auto:()=>read(src('index.css')).length>0&&(read(fe('tailwind.config.js')||'')).length>0?'PASS':'FAIL',
250
+ guide:'Use consistent color palette in Tailwind config' },
251
+ { id:'C80', cat:'Closing (5%)', label:'Can remove project folder after marking',
252
+ auto:()=>'MANUAL', guide:'After assessor approval, delete project folder' },
253
+ { id:'C81', cat:'Closing (5%)', label:'Can delete database after marking',
254
+ auto:()=>'MANUAL', guide:'Run: DROP DATABASE '+dbName+';' },
255
+ { id:'C82', cat:'Closing (5%)', label:'Global dependencies removable',
256
+ auto:()=>'MANUAL', guide:'npm uninstall -g nodemon etc.' },
289
257
  ];
290
258
  return checks;
291
259
  }
292
260
 
293
261
  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);
298
- }
299
-
262
+ const groups={};
263
+ for(const r of results){if(!groups[r.cat])groups[r.cat]=[];groups[r.cat].push(r);}
300
264
  log(chalk.bold('\n═══════════════════════════════════════════════════════════════'));
301
265
  log(chalk.bold(' AUTO-ASSESSMENT RESULTS'));
302
266
  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')}`); }
317
- }
318
- log('');
267
+ let total=results.length,passed=0,failed=0,manual=0;
268
+ for(const[cat,items]of Object.entries(groups)){
269
+ log(chalk.bold(chalk.bgHex('#1e293b').white(` ${cat} `)));log('');
270
+ for(const r of items){
271
+ if(r.status==='PASS'){passed++;log(` ${chalk.green('✓')} ${r.label.padEnd(55)} ${chalk.green('PASS')}`);}
272
+ else if(r.status==='FAIL'){failed++;log(` ${chalk.red('')} ${r.label.padEnd(55)} ${chalk.red('FAIL')}`);}
273
+ else{manual++;log(` ${chalk.cyan('?')} ${r.label.padEnd(55)} ${chalk.cyan('MANUAL')}`);}
274
+ }log('');
319
275
  }
320
-
321
276
  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
- }
277
+ log(chalk.bold(` ${chalk.green('PASSED')}:${passed} ${chalk.red('FAILED')}:${failed} ${chalk.cyan('MANUAL')}:${manual} TOTAL:${total}`));
278
+ const autoTotal=total-manual,autoPassed=passed;
279
+ log(chalk.bold(` Auto-check score: ${autoTotal>0?((autoPassed/autoTotal)*100).toFixed(1):'N/A'}%`));
280
+ if(failed>0){
281
+ log(chalk.bold(chalk.yellow('\n─── FIX FAILED ITEMS ───\n')));
282
+ for(const r of results){if(r.status==='FAIL'&&r.guide)log(` ${chalk.red('✗')} ${chalk.bold(r.label)}\n ${chalk.dim('→')} ${r.guide}\n`);}
336
283
  }
337
-
338
284
  log(chalk.bold('═══════════════════════════════════════════════════════════════\n'));
339
285
  }
340
286
 
341
- function generateMermaidDiagrams(projectType) {
342
- let erd = '', dfd = '';
343
- if (projectType === 'SIMS') {
344
- erd = `erDiagram
287
+ function generateMermaidDiagrams(projectType){
288
+ if(projectType==='SIMS'){
289
+ return {
290
+ erd:`erDiagram
345
291
  Users ||--o{ Stock_Out : "records"
346
292
  Spare_Part ||--o{ Stock_In : "receives"
347
293
  Spare_Part ||--o{ Stock_Out : "issues"
348
-
349
- Users {
350
- int UserID PK "Auto Increment"
294
+ Users { int UserID PK "Auto Increment"
351
295
  varchar Username "Unique"
352
- varchar Password "Hashed"
353
- }
354
-
355
- Spare_Part {
356
- int SparePartID PK "Auto Increment"
296
+ varchar Password "Hashed" }
297
+ Spare_Part { int SparePartID PK "Auto Increment"
357
298
  varchar Name
358
299
  varchar Category
359
300
  int Quantity
360
301
  decimal UnitPrice
361
- decimal TotalPrice "Generated: Qty * UnitPrice"
362
- }
363
-
364
- Stock_In {
365
- int StockInID PK "Auto Increment"
302
+ decimal TotalPrice "Qty * UnitPrice" }
303
+ Stock_In { int StockInID PK "Auto Increment"
366
304
  int SparePartID FK
367
305
  int StockInQuantity
368
- date StockInDate
369
- }
370
-
371
- Stock_Out {
372
- int StockOutID PK "Auto Increment"
306
+ date StockInDate }
307
+ Stock_Out { int StockOutID PK "Auto Increment"
373
308
  int SparePartID FK
374
309
  int UserID FK
375
310
  int StockOutQuantity
376
311
  decimal StockOutUnitPrice
377
312
  decimal StockOutTotalPrice
378
- date StockOutDate
379
- }`;
380
-
381
- dfd = `graph TD
313
+ date StockOutDate }`,
314
+ dfd:`graph TD
382
315
  subgraph "Context Level 0"
383
316
  SM[Stock Manager] -->|Input Data| SIMS[SIMS System]
384
- SIMS -->|Reports| SM
385
- end
386
-
387
- subgraph "Level 1 - Processes"
317
+ SIMS -->|Reports| SM end
318
+ subgraph "Processes"
388
319
  P1["1.0 Manage Spare Parts"]
389
320
  P2["2.0 Record Stock In"]
390
321
  P3["3.0 Record Stock Out"]
391
322
  P4["4.0 Generate Reports"]
392
- P5["5.0 Authentication"]
393
- end
394
-
323
+ P5["5.0 Authentication"] end
395
324
  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
325
+ D1[(Spare_Part)] D2[(Stock_In)] D3[(Stock_Out)] D4[(Users)] end
326
+ SM-->|"Login"|P5 P5-->D4
327
+ SM-->|"Spare Part"|P1 P1-->D1
328
+ SM-->|"Stock In"|P2 P2-->D2 P2-->|"Update"|D1
329
+ SM-->|"Stock Out"|P3 P3-->D3 P3-->|"Reduce"|D1 P3-->|"User"|D4
330
+ SM-->|"Report"|P4 P4-->D1 P4-->D2 P4-->D3`};
331
+ }
332
+ return {
333
+ erd:`erDiagram
419
334
  Car ||--o{ ServiceRecord : "has"
420
335
  Services ||--o{ ServiceRecord : "includes"
421
336
  ServiceRecord ||--o{ Payment : "generates"
422
337
  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
- }
432
-
433
- Services {
434
- varchar ServiceCode PK
435
- varchar ServiceName
436
- decimal ServicePrice
437
- }
438
-
439
- ServiceRecord {
440
- int RecordNumber PK "Auto Increment"
441
- date ServiceDate
442
- varchar PlateNumber FK
443
- varchar ServiceCode FK
444
- }
445
-
446
- Payment {
447
- int PaymentNumber PK "Auto Increment"
448
- decimal AmountPaid
449
- date PaymentDate
450
- int RecordNumber FK
451
- int UserID FK
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
338
+ Car { varchar PlateNumber PK
339
+ varchar type varchar Model int ManufacturingYear
340
+ varchar DriverPhone varchar MechanicName }
341
+ Services { varchar ServiceCode PK
342
+ varchar ServiceName decimal ServicePrice }
343
+ ServiceRecord { int RecordNumber PK "Auto Increment"
344
+ date ServiceDate varchar PlateNumber FK varchar ServiceCode FK }
345
+ Payment { int PaymentNumber PK "Auto Increment"
346
+ decimal AmountPaid date PaymentDate
347
+ int RecordNumber FK int UserID FK }
348
+ User { int UserID PK "Auto Increment"
349
+ varchar Username "Unique" varchar Password "Hashed" varchar FullName }`,
350
+ dfd:`graph TD
462
351
  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
-
352
+ CM[Cashier] -->|Input| CRPMS[CRPMS System]
353
+ CRPMS -->|Reports & Bills| CM end
354
+ subgraph "Processes"
355
+ P1["1.0 Manage Cars"] P2["2.0 Manage Services"]
356
+ P3["3.0 Service Records"] P4["4.0 Payments"]
357
+ P5["5.0 Reports & Bills"] P6["6.0 Authentication"] end
476
358
  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`;
503
- }
504
- return { erd, dfd };
359
+ D1[(Car)] D2[(Services)] D3[(ServiceRecord)] D4[(Payment)] D5[(User)] end
360
+ CM-->|"Login"|P6 P6-->D5
361
+ CM-->|"Car"|P1 P1-->D1
362
+ CM-->|"Service"|P2 P2-->D2
363
+ CM-->|"Record"|P3 P3-->D1 P3-->D2 P3-->D3
364
+ CM-->|"Payment"|P4 P4-->D3 P4-->D4 P4-->D5
365
+ CM-->|"Report"|P5 P5-->D1 P5-->D2 P5-->D3 P5-->D4`};
505
366
  }
506
367
 
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
- ---
368
+ function initMermaidFile(fp, pt){
369
+ const d=generateMermaidDiagrams(pt);
370
+ writeFileSync(fp,`# ${pt==='SIMS'?'Stock Inventory Management System (SIMS)':'Car Repair Payment Management System (CRPMS)'} - ERD & DFD
514
371
 
515
372
  ## Entity Relationship Diagram (ERD)
516
-
517
- \`\`\`mermaid
518
- ${diags.erd}
519
- \`\`\`
520
-
521
- ---
373
+ \`\`\`mermaid\n${d.erd}\n\`\`\`
522
374
 
523
375
  ## Data Flow Diagram (DFD)
376
+ \`\`\`mermaid\n${d.dfd}\n\`\`\`
524
377
 
525
- \`\`\`mermaid
526
- ${diags.dfd}
527
- \`\`\`
378
+ *Generated by npms-exam-kit on ${new Date().toISOString().split('T')[0]}*`,'utf8');
379
+ return fp;
380
+ }
528
381
 
529
- ---
382
+ function showNextSteps(project, targetDir){
383
+ const dbName=project==='SIMS'?'SIMS':'CRPMS';
384
+ const dbUser=project==='SIMS'?'admin / admin123':'admin / Admin@1234';
385
+ const endpoints=project==='SIMS'?SIMS_ENDPOINTS:CRPMS_ENDPOINTS;
386
+ const pTitle=project==='SIMS'?'Spare Part':'Car Repair Payment';
387
+
388
+ log(chalk.bold.hex('#00D2FF')('\n═══════════════════════════════════════════════════════════════'));
389
+ log(chalk.bold.hex('#00D2FF')(' NEXT STEPS - HOW TO RUN EVERYTHING'));
390
+ log(chalk.bold.hex('#00D2FF')('═══════════════════════════════════════════════════════════════\n'));
391
+
392
+ log(chalk.bold(chalk.bgGreen(' STEP 1: IMPORT DATABASE VIA XAMPP ')));
393
+ log(`\n ${chalk.cyan('A)')} Open XAMPP Control Panel → Start ${chalk.bold('Apache')} & ${chalk.bold('MySQL')}`);
394
+ log(` ${chalk.cyan('B)')} Open browser → ${chalk.bold('http://localhost/phpmyadmin')}`);
395
+ log(` ${chalk.cyan('C)')} Click ${chalk.bold('"New"')} in left sidebar`);
396
+ log(` ${chalk.cyan('D)')} Database name: ${chalk.bold(dbName)} → Click ${chalk.bold('"Create"')}`);
397
+ log(` ${chalk.cyan('E)')} Click ${chalk.bold('"SQL"')} tab`);
398
+ log(` ${chalk.cyan('F)')} Open this file in Notepad:`);
399
+ log(` ${chalk.dim(join(targetDir, 'database.sql'))}`);
400
+ log(` ${chalk.cyan('G)')} ${chalk.bold('Copy ALL the SQL code')} and paste into phpMyAdmin SQL box`);
401
+ log(` ${chalk.cyan('H)')} Click ${chalk.bold('"Go"')} to execute`);
402
+ log(` ${chalk.green(' ✓ Database and tables will be created automatically')}\n`);
403
+
404
+ log(chalk.bold(chalk.bgGreen(' STEP 2: CONFIGURE BACKEND .ENV ')));
405
+ log(`\n Open ${chalk.bold(join(targetDir, 'backend-project', '.env'))} and verify:`);
406
+ log(` ${chalk.dim('DB_HOST=localhost')}`);
407
+ log(` ${chalk.dim('DB_USER=root')}`);
408
+ log(` ${chalk.dim('DB_PASSWORD= (leave empty if no password)')}`);
409
+ log(` ${chalk.dim('DB_NAME='+dbName)}`);
410
+ log(` ${chalk.dim('PORT=5000')}\n`);
411
+
412
+ log(chalk.bold(chalk.bgGreen(' STEP 3: START BACKEND SERVER ')));
413
+ log(`\n Open a terminal and run:`);
414
+ log(` ${chalk.cyan(' cd "' + join(targetDir, 'backend-project') + '"')}`);
415
+ log(` ${chalk.cyan(' npm start')}`);
416
+ log(`\n ${chalk.green(' ✓ Backend will run on:')} ${chalk.bold('http://localhost:5000')}\n`);
417
+
418
+ log(chalk.bold(chalk.bgGreen(' STEP 4: START FRONTEND SERVER ')));
419
+ log(`\n Open a SECOND terminal and run:`);
420
+ log(` ${chalk.cyan(' cd "' + join(targetDir, 'frontend-project') + '"')}`);
421
+ log(` ${chalk.cyan(' npm run dev')}`);
422
+ log(`\n ${chalk.green(' ✓ Frontend will run on:')} ${chalk.bold('http://localhost:5173')}\n`);
423
+
424
+ log(chalk.bold(chalk.bgGreen(' STEP 5: LOGIN ')));
425
+ log(`\n Open ${chalk.bold('http://localhost:5173')} in your browser`);
426
+ log(` Login with: ${chalk.bold(dbUser)}\n`);
427
+
428
+ log(chalk.bold(chalk.bgGreen(' STEP 6: TEST API ENDPOINTS WITH POSTMAN ')));
429
+ log(`\n First, make sure the backend is running (Step 3).`);
430
+ log(` Then open Postman and test these endpoints:\n`);
431
+
432
+ for(const ep of endpoints){
433
+ const mc=ep.method==='POST'?chalk.yellow:ep.method==='PUT'?chalk.blue:ep.method==='DELETE'?chalk.red:chalk.green;
434
+ log(` ${mc(ep.method.padEnd(6))} ${chalk.bold('http://localhost:5000'+ep.path)}`);
435
+ log(` ${chalk.dim(ep.desc)}`);
436
+ if(ep.body) log(` ${chalk.dim('Body (raw JSON):')} ${ep.body}`);
437
+ log('');
438
+ }
530
439
 
531
- *Generated by npms-exam-kit on ${new Date().toISOString().split('T')[0]}*
532
- `;
533
- writeFileSync(filePath, content, 'utf8');
534
- return filePath;
440
+ log(chalk.bold(chalk.bgGreen(' STEP 7: POSTMAN TESTING TIPS ')));
441
+ log(`\n ${chalk.cyan('•')} For POST/PUT requests: select ${chalk.bold('Body → raw → JSON')}`);
442
+ log(` ${chalk.cyan('')} The session cookie is handled automatically by Postman`);
443
+ log(` ${chalk.cyan('•')} Test login first, then other endpoints`);
444
+ log(` ${chalk.cyan('•')} For GET requests: you can paste the URL directly in browser too`);
445
+
446
+ log(chalk.bold('\n───────────────────────────────────────────────────────────────'));
447
+ log(chalk.bold(` PROJECT: ${project==='SIMS'?'SIMS - Stock Inventory Mgmt':'CRPMS - Car Repair Payment Mgmt'}`));
448
+ log(chalk.bold(` LOCATION: ${chalk.dim(targetDir)}`));
449
+ log(chalk.bold(` DATABASE: ${chalk.dim(dbName)}`));
450
+ log(chalk.bold(` FRONTEND: ${chalk.dim('http://localhost:5173')}`));
451
+ log(chalk.bold(` BACKEND: ${chalk.dim('http://localhost:5000')}`));
452
+ log(chalk.bold(` Mermaid diagrams saved: ${chalk.dim(join(targetDir, project+'_ERD_DFD.md'))}`));
453
+ log(chalk.bold(` Assessment report saved: ${chalk.dim(join(targetDir, 'checklist_report'))}`));
454
+ log(chalk.bold('───────────────────────────────────────────────────────────────\n'));
455
+
456
+ log(chalk.dim(' NOTE: Close XAMPP MySQL before running "npm start" if using XAMPP,'));
457
+ log(chalk.dim(' or change the port in XAMPP config to avoid port conflicts.\n'));
535
458
  }
536
459
 
537
- function log(m) { console.log(m); }
538
-
539
- async function main() {
460
+ async function main(){
540
461
  console.clear();
541
462
  log(chalk.bold.hex('#00D2FF')('\n ╔══════════════════════════════════════════════════╗'));
542
463
  log(chalk.bold.hex('#00D2FF')(' ║ NESA NATIONAL PRACTICAL EXAM 2024-2025 ║'));
@@ -544,189 +465,88 @@ async function main() {
544
465
  log(chalk.bold.hex('#00D2FF')(' ║ TRADE: Software Development ║'));
545
466
  log(chalk.bold.hex('#00D2FF')(' ╚══════════════════════════════════════════════════╝\n'));
546
467
 
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
- }]);
557
-
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
- }]);
567
-
568
- const targetDir = join(process.cwd());
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
618
- const exist = [];
619
- if (existsSync(join(targetDir, 'backend-project'))) exist.push('backend-project');
620
- if (existsSync(join(targetDir, 'frontend-project'))) exist.push('frontend-project');
621
-
622
- if (exist.length > 0) {
623
- log(chalk.yellow(` Warning: ${exist.join(', ')} already exists.`));
624
- const { overwrite } = await inquirer.prompt([{
625
- type: 'confirm', name: 'overwrite',
626
- message: 'Overwrite existing files?', default: false
627
- }]);
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);
632
- }
468
+ const {project}=await inquirer.prompt([{type:'list',name:'project',message:'Select project:',
469
+ choices:[
470
+ {name:'SIMS - Stock Inventory Management System (SmartPark)',value:'SIMS'},
471
+ {name:'CRPMS - Car Repair Payment Management System',value:'CRPMS'},
472
+ ]}]);
473
+
474
+ const targetDir=join(process.cwd());
475
+
476
+ // Check if project already installed
477
+ const exist=[];
478
+ if(existsSync(join(targetDir,'backend-project')))exist.push('backend-project');
479
+ if(existsSync(join(targetDir,'frontend-project')))exist.push('frontend-project');
480
+ if(exist.length>0){
481
+ log(chalk.yellow(` Warning: ${exist.join(', ')} already exists here.`));
482
+ const {overwrite}=await inquirer.prompt([{type:'confirm',name:'overwrite',message:'Overwrite?',default:false}]);
483
+ if(!overwrite){log(chalk.yellow(' Cancelled.'));process.exit(0);}
484
+ for(const f of['backend-project','frontend-project','database.sql']){const p=join(targetDir,f);if(existsSync(p))fs.removeSync(p);}
633
485
  }
634
486
 
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); }
487
+ const srcDir=join(PROJECTS_DIR,project==='SIMS'?'SIMS-master':'CRPMS-main');
488
+ if(!existsSync(srcDir)){log(chalk.red(' Error: source not found'));process.exit(1);}
638
489
 
490
+ // ── INSTALL ──
639
491
  log(chalk.bold(`\n Installing ${project}...`));
640
- const spinChars = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
641
- let si = 0;
642
- const spin = setInterval(() => { process.stdout.write(`\r ${chalk.cyan(spinChars[si++ % spinChars.length])} Installing...`); }, 80);
643
-
644
- try {
645
- const entries = readdirSync(srcDir);
646
- for (const entry of entries) {
647
- if (entry === 'node_modules') continue;
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);
492
+ const spinChars=['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
493
+ let si=0;
494
+ const spin=setInterval(()=>{process.stdout.write(`\r ${chalk.cyan(spinChars[si++%spinChars.length])} Installing...`);},80);
495
+
496
+ try{
497
+ const entries=readdirSync(srcDir);
498
+ for(const entry of entries){
499
+ if(entry==='node_modules')continue;
500
+ const s=join(srcDir,entry),d=join(targetDir,entry);
501
+ if(statSync(s).isDirectory())fs.copySync(s,d,{filter:f=>!f.includes('node_modules')});
502
+ else copyFileSync(s,d);
652
503
  }
653
504
 
654
- const beDir = join(targetDir, 'backend-project');
655
- const feDir = join(targetDir, 'frontend-project');
505
+ const beDir=join(targetDir,'backend-project'),feDir=join(targetDir,'frontend-project');
656
506
 
657
- if (existsSync(beDir) && existsSync(join(beDir, 'package.json'))) {
507
+ if(existsSync(beDir)&&existsSync(join(beDir,'package.json'))){
658
508
  process.chdir(beDir);
659
509
  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}`)); }
510
+ try{await execAsync('npm install',{timeout:120000,windowsHide:true});log(chalk.green(' ✓ Backend dependencies installed'));}catch(e){log(chalk.yellow(` ⚠ Backend: ${e.message}`));}
661
511
  }
662
- if (existsSync(feDir) && existsSync(join(feDir, 'package.json'))) {
512
+ if(existsSync(feDir)&&existsSync(join(feDir,'package.json'))){
663
513
  process.chdir(feDir);
664
514
  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}`)); }
515
+ try{await execAsync('npm install',{timeout:120000,windowsHide:true});log(chalk.green(' ✓ Frontend dependencies installed'));}catch(e){log(chalk.yellow(` ⚠ Frontend: ${e.message}`));}
666
516
  }
667
517
 
668
518
  process.chdir(targetDir);
669
519
  clearInterval(spin);
670
520
  process.stdout.write('\r \r');
671
- log(chalk.green(' ✓ Project installed\n'));
521
+ log(chalk.green(' ✓ Project files installed\n'));
672
522
 
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'));
523
+ // Create .env
524
+ if(existsSync(join(beDir,'.env.example'))&&!existsSync(join(beDir,'.env'))){
525
+ copyFileSync(join(beDir,'.env.example'),join(beDir,'.env'));
526
+ log(chalk.green(' ✓ .env file created'));
676
527
  }
677
528
 
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`));
715
- }
529
+ // ── AUTO-ASSESS ──
530
+ log(chalk.bold(' Running auto-assessment...\n'));
531
+ const checks=buildChecklist(targetDir,project);
532
+ const results=checks.map(c=>{let s;try{const r=c.auto();s=r===true||r==='PASS'?'PASS':(r==='MANUAL'?'MANUAL':'FAIL');}catch{s='MANUAL';}return{...c,status:s,guide:c.guide||''};});
533
+ showResults(results);
534
+ const rd=join(targetDir,'checklist_report');mkdirSync(rd,{recursive:true});
535
+ writeFileSync(join(rd,`assessment_${project}_${Date.now()}.json`),JSON.stringify({project,date:new Date().toISOString(),results},null,2));
536
+ log(chalk.dim(` Report: ${join(rd)}\n`));
716
537
 
717
- const { genDiag } = await inquirer.prompt([{
718
- type: 'confirm', name: 'genDiag',
719
- message: 'Generate ERD & DFD Mermaid diagrams?', default: true
720
- }]);
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`));
725
- }
538
+ // ── MERMAID DIAGRAMS ──
539
+ const diagPath=join(targetDir,`${project}_ERD_DFD.md`);
540
+ initMermaidFile(diagPath,project);
541
+ log(chalk.green(` ✓ Mermaid ERD & DFD diagrams saved to:`));
542
+ log(` ${chalk.bold(diagPath)}\n`);
543
+
544
+ // ── NEXT STEPS ──
545
+ showNextSteps(project, targetDir);
726
546
 
727
- log(chalk.bold.green('\n ✓ All done!\n'));
547
+ log(chalk.bold.green('\n ✓ All done! Follow the steps above to run your project.\n'));
728
548
 
729
- } catch (err) {
549
+ }catch(err){
730
550
  clearInterval(spin);
731
551
  log(chalk.red(`\n ✗ Error: ${err.message}`));
732
552
  process.exit(1);