source: server.js

main
Last change on this file was 2d1ec46, checked in by Klimentina Efremova <klimentina08642@…>, 8 days ago

Implemented Triggers and Views

  • Property mode set to 100644
File size: 305.2 KB
RevLine 
[69f2a41]1const http = require('http');
2const url = require('url');
[33517cc]3const { Pool } = require('pg');
[69f2a41]4const fs = require('fs');
5const path = require('path');
6const crypto = require('crypto');
7const nodemailer = require('nodemailer');
8const bcrypt = require('bcryptjs');
[6c7cfa6]9const { AsyncLocalStorage } = require('async_hooks');
[69f2a41]10require('dotenv').config();
11
12const port = process.env.PORT || 3000;
[79fff4f]13
[69f2a41]14const sessions = new Map();
15const verificationCodes = new Map();
16const tempUsers = new Map();
17const tempAdminSessions = new Map();
18const tempStoreRegistrations = new Map();
19
20console.log('🔧 Starting Handcraft Marketplace Server...');
21console.log('🎨 Colors: Royal Blue & Pink Theme');
22
23let emailTransporter;
24
25if (process.env.SMTP_USER && process.env.SMTP_PASS) {
26 const emailConfig = {
27 host: process.env.SMTP_HOST || 'smtp.gmail.com',
[06ebe74]28 port: (() => {
29 const configuredPort = parseInt(process.env.SMTP_PORT, 10);
30 if (process.env.SMTP_HOST === 'smtp.ethereal.email' && configuredPort === 3000) return 587;
31 return configuredPort || 587;
32 })(),
[69f2a41]33 secure: false,
34 auth: {
35 user: process.env.SMTP_USER,
36 pass: process.env.SMTP_PASS
37 }
38 };
[79fff4f]39
[69f2a41]40 emailTransporter = nodemailer.createTransport(emailConfig);
[79fff4f]41
[69f2a41]42 emailTransporter.verify(function(error, success) {
43 if (error) {
44 console.log('❌ Email configuration failed:', error.message);
45 console.log('📧 Falling back to console display for verification codes');
46 emailTransporter = createMockTransporter();
47 } else {
48 console.log('✅ Email server is ready to send real emails!');
49 }
50 });
51} else {
52 console.log('📧 No email credentials found. Verification codes will be shown in console.');
53 emailTransporter = createMockTransporter();
54}
55
56function createMockTransporter() {
57 return {
58 sendMail: function(mailOptions) {
59 return new Promise((resolve, reject) => {
60 const codeMatch = mailOptions.html.match(/\b\d{6}\b/);
61 const code = codeMatch ? codeMatch[0] : 'unknown';
[79fff4f]62
[69f2a41]63 console.log('');
64 console.log('🎯 ===== VERIFICATION CODE =====');
65 console.log('📧 For:', mailOptions.to);
66 console.log('🔐 CODE:', code);
67 console.log('⏰ Expires in: 30 seconds');
68 console.log('📝 Use this code to continue');
69 console.log('================================');
70 console.log('');
[79fff4f]71
[69f2a41]72 resolve({ messageId: 'dev-' + Date.now() });
73 });
74 }
75 };
76}
77
78function sendVerificationEmail(toEmail, code) {
79 const mailOptions = {
80 from: process.env.SMTP_USER || 'noreply@handcraft-marketplace.com',
81 to: toEmail,
82 subject: 'Your Verification Code - Handcraft Marketplace',
83 html: `
[4dff800]84 <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; background: linear-gradient(135deg, #4169E1 0%, #FF69B4 100%); color: white; padding: 20px; border-radius: 10px;">
85 <h2 style="text-align: center;">🎨 Handcraft Marketplace</h2>
86 <div style="background-color: white; color: #333; padding: 20px; border-radius: 8px; margin: 20px 0;">
87 <h3 style="color: #4169E1;">Account Verification</h3>
88 <p>Your verification code is:</p>
89 <div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; text-align: center; font-size: 24px; font-weight: bold; letter-spacing: 5px; margin: 20px 0; color: #4169E1;">
90 ${code}
91 </div>
92 <p style="color: #e74c3c; font-weight: bold;">⚠️ This code will expire in 30 seconds</p>
93 <p style="color: #666; font-size: 12px;">If you didn't request this verification, please ignore this email.</p>
94 </div>
95 </div>`
[69f2a41]96 };
97
98 console.log('');
99 console.log('🎯 ===== VERIFICATION CODE FOR TESTING =====');
100 console.log('📧 Email:', toEmail);
101 console.log('🔐 CODE:', code);
102 console.log('⏰ Expires in: 30 seconds');
103 console.log('==========================================');
104 console.log('');
105
106 return emailTransporter.sendMail(mailOptions);
107}
108
109function send2FACode(toEmail, code) {
110 const mailOptions = {
111 from: process.env.SMTP_USER || 'noreply@handcraft-marketplace.com',
112 to: toEmail,
113 subject: 'Your 2FA Code - Handcraft Marketplace',
114 html: `
[4dff800]115 <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; background: linear-gradient(135deg, #4169E1 0%, #FF69B4 100%); color: white; padding: 20px; border-radius: 10px;">
116 <h2 style="text-align: center;">🎨 Handcraft Marketplace</h2>
117 <div style="background-color: white; color: #333; padding: 20px; border-radius: 8px; margin: 20px 0;">
118 <h3 style="color: #4169E1;">Two-Factor Authentication</h3>
119 <p>Your login verification code is:</p>
120 <div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; text-align: center; font-size: 24px; font-weight: bold; letter-spacing: 5px; margin: 20px 0; color: #4169E1;">
121 ${code}
122 </div>
123 <p style="color: #e74c3c; font-weight: bold;">⚠️ This code will expire in 30 seconds</p>
124 <p style="color: #666; font-size: 12px;">If you're not trying to login, please secure your account immediately.</p>
125 </div>
126 </div>`
[69f2a41]127 };
128
129 console.log('');
130 console.log('🎯 ===== 2FA CODE FOR TESTING =====');
131 console.log('📧 Email:', toEmail);
132 console.log('🔐 CODE:', code);
133 console.log('⏰ Expires in: 30 seconds');
134 console.log('==================================');
135 console.log('');
136
137 return emailTransporter.sendMail(mailOptions);
138}
139
140function sendStoreRegistrationEmail(toEmail, code, storeName) {
141 const mailOptions = {
142 from: process.env.SMTP_USER || 'noreply@handcraft-marketplace.com',
143 to: toEmail,
144 subject: 'Store Registration Verification - Handcraft Marketplace',
145 html: `
[4dff800]146 <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; background: linear-gradient(135deg, #4169E1 0%, #FF69B4 100%); color: white; padding: 20px; border-radius: 10px;">
147 <h2 style="text-align: center;">🎨 Handcraft Marketplace</h2>
148 <div style="background-color: white; color: #333; padding: 20px; border-radius: 8px; margin: 20px 0;">
149 <h3 style="color: #4169E1;">Store Registration Verification</h3>
150 <p>Thank you for registering your store "<strong>${storeName}</strong>" on Handcraft Marketplace!</p>
151 <p>Your verification code is:</p>
152 <div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; text-align: center; font-size: 24px; font-weight: bold; letter-spacing: 5px; margin: 20px 0; color: #4169E1;">
153 ${code}
154 </div>
155 <p style="color: #e74c3c; font-weight: bold;">⚠️ This code will expire in 30 seconds</p>
156 <p style="color: #666; font-size: 12px;">If you didn't request this verification, please ignore this email.</p>
157 </div>
158 </div>`
[69f2a41]159 };
160
161 console.log('');
162 console.log('🎯 ===== STORE REGISTRATION VERIFICATION CODE =====');
163 console.log('📧 For:', toEmail);
164 console.log('🏪 Store:', storeName);
165 console.log('🔐 CODE:', code);
166 console.log('⏰ Expires in: 30 seconds');
167 console.log('==================================================');
168 console.log('');
169
170 return emailTransporter.sendMail(mailOptions);
171}
172
173function generateVerificationCode() {
174 let code = '';
175 for(let i = 0; i < 6; i++) {
[06ebe74]176 code += crypto.randomInt(0, 10);
[69f2a41]177 }
178 return code;
179}
180
181function generateSessionId() {
182 return crypto.randomBytes(32).toString('hex');
183}
184
185function serveStaticFile(res, filePath, contentType) {
186 const fullPath = path.join(__dirname, 'interfejs', filePath);
187 fs.readFile(fullPath, (err, data) => {
188 if (err) {
189 console.error('File not found:', fullPath, err);
190 res.writeHead(404, { 'Content-Type': 'text/plain' });
191 res.end('File not found');
192 } else {
193 res.writeHead(200, { 'Content-Type': contentType });
194 res.end(data);
195 }
196 });
197}
198
199function parseCookies(req) {
200 const cookieHeader = req.headers.cookie;
201 const cookies = {};
202 if (cookieHeader) {
203 cookieHeader.split(';').forEach(cookie => {
204 const parts = cookie.split('=');
205 cookies[parts[0].trim()] = parts[1]?.trim();
206 });
207 }
208 return cookies;
209}
210
211function getClientIp(req) {
212 return req.headers['x-forwarded-for'] ||
213 req.connection.remoteAddress ||
214 req.socket.remoteAddress ||
215 (req.connection.socket ? req.connection.socket.remoteAddress : null);
216}
217
218function requireAuth(req, res, callback) {
219 const cookies = parseCookies(req);
220 const sessionId = cookies.sessionId;
221
[33517cc]222 if (!sessionId || !sessions.has(sessionId)) {
[69f2a41]223 res.writeHead(302, { 'Location': '/login.html' });
224 res.end();
225 return;
226 }
227
[33517cc]228 const userId = sessions.get(sessionId);
[69f2a41]229
230 if (tempAdminSessions.has(sessionId)) {
[33517cc]231 if (!req.url.includes('/change-password') && !req.url.includes('/api/force-change-password')) {
[69f2a41]232 res.writeHead(302, { 'Location': '/change-password.html?forced=true' });
233 res.end();
234 return;
235 }
236 }
237
[33517cc]238 callback(userId);
[69f2a41]239}
240
241function requireRole(roleName) {
242 return function(req, res, callback) {
243 requireAuth(req, res, (userId) => {
244 database.getUserById(userId, (err, user) => {
245 if (err || !user) {
246 res.writeHead(403, { 'Content-Type': 'application/json' });
247 res.end(JSON.stringify({ success: false, message: 'Access denied' }));
248 return;
249 }
250
251 const hasRole = user.roles && user.roles.some(role => role.name === roleName);
252
253 if (!hasRole) {
254 res.writeHead(403, { 'Content-Type': 'application/json' });
255 res.end(JSON.stringify({ success: false, message: 'Insufficient permissions' }));
256 return;
257 }
258
259 callback(userId, user);
260 });
261 });
262 };
263}
264
265function validateEmail(email) {
266 const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
267 return emailRegex.test(email);
268}
269
270function validatePassword(password) {
271 const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
272 return passwordRegex.test(password);
273}
274
275function cleanupExpiredCodes() {
276 const now = Date.now();
277 let cleanedCount = 0;
278
279 for (const [key, data] of verificationCodes.entries()) {
280 if (now - data.timestamp > 30 * 1000) {
281 verificationCodes.delete(key);
282 cleanedCount++;
283 }
284 }
285
286 for (const [key, data] of tempUsers.entries()) {
287 if (now - data.timestamp > 30 * 1000) {
288 tempUsers.delete(key);
289 cleanedCount++;
290 }
291 }
292
293 for (const [key, data] of tempStoreRegistrations.entries()) {
294 if (now - data.timestamp > 30 * 1000) {
295 tempStoreRegistrations.delete(key);
296 cleanedCount++;
297 }
298 }
299
300 if (cleanedCount > 0) {
301 console.log(`🧹 Cleaned ${cleanedCount} expired verification codes`);
302 }
303}
304
305setInterval(cleanupExpiredCodes, 10 * 1000);
306
307function requireStoreOwner() {
308 return function(req, res, callback) {
309 requireAuth(req, res, (userId) => {
310 const userIdStr = String(userId);
311
[4dff800]312 // Check if this is the admin user (ID 000000)
313 if (userIdStr === '000000') {
314 // Admin is not a store owner
315 res.writeHead(403, { 'Content-Type': 'application/json' });
316 res.end(JSON.stringify({ success: false, message: 'Access denied - not a store owner' }));
317 return;
318 }
[69f2a41]319
[4dff800]320 // Check if it's a personal user
321 if (userIdStr.startsWith('personal_')) {
322 const personalId = userIdStr.replace('personal_', '');
323
324 database.database.get(
[33517cc]325 'SELECT boss_id FROM boss WHERE boss_id = $1',
[4dff800]326 [personalId],
327 (err, boss) => {
328 if (err || !boss) {
329 res.writeHead(403, { 'Content-Type': 'application/json' });
330 res.end(JSON.stringify({ success: false, message: 'Access denied - not a store owner' }));
331 return;
332 }
333
334 callback(personalId);
335 }
336 );
337 } else {
338 // Not a personal user, so not a store owner
339 res.writeHead(403, { 'Content-Type': 'application/json' });
340 res.end(JSON.stringify({ success: false, message: 'Access denied - not a store owner' }));
341 }
[69f2a41]342 });
343 };
344}
345
346
347
[33517cc]348const pool = new Pool({
349 connectionString: process.env.DATABASE_URL,
350 host: process.env.PGHOST || process.env.DB_HOST || 'localhost',
351 port: Number(process.env.PGPORT || process.env.DB_PORT || 5432),
352 user: process.env.PGUSER || process.env.DB_USER || 'postgres',
353 password: process.env.PGPASSWORD || process.env.DB_PASSWORD || '',
354 database: process.env.PGDATABASE || process.env.DB_NAME || 'handcraft_marketplace',
355 max: Number(process.env.PG_POOL_MAX || 10),
356 idleTimeoutMillis: 30000
357});
[69f2a41]358
[6c7cfa6]359const transactionStorage = new AsyncLocalStorage();
[69f2a41]360
[33517cc]361function dbQuery(sql, params = [], callback) {
[6c7cfa6]362 const client = transactionStorage.getStore() || pool;
363
[33517cc]364 client.query(sql, params)
365 .then(result => callback(null, result))
366 .catch(err => callback(err));
[69f2a41]367}
368
[6c7cfa6]369const REPORT_FUNCTIONS_SQL = String.raw`
370-- ============================================================
371-- HANDCRAFT MARKETPLACE REPORT FUNCTIONS
372-- PostgreSQL / exact project schema
373-- ============================================================
374
375CREATE OR REPLACE FUNCTION get_orders_by_total()
376RETURNS TABLE (
377 order_num VARCHAR(11),
378 client_id INTEGER,
379 client_name TEXT,
380 order_quantity BIGINT,
381 order_status VARCHAR(20),
382 payment_method VARCHAR(250),
383 discount NUMERIC,
384 order_total NUMERIC
385)
386LANGUAGE sql
387AS $$
388 SELECT
389 o.order_num,
390 o.client_id,
391 CONCAT_WS(' ', c.first_name, c.last_name) AS client_name,
392 COALESCE(SUM(i.quantity), 0)::BIGINT AS order_quantity,
393 o.status,
394 o.payment_method,
395 COALESCE(o.discount, 0)::NUMERIC AS discount,
396 ROUND(
397 COALESCE(SUM(p.price * i.quantity), 0)
398 * (1 - COALESCE(o.discount, 0) / 100.0),
399 2
400 ) AS order_total
401 FROM "order" o
402 LEFT JOIN client c ON c.client_id = o.client_id
403 LEFT JOIN includes i ON i.order_num = o.order_num
404 LEFT JOIN product p ON p.code = i.product_code
405 GROUP BY
406 o.order_num, o.client_id, c.first_name, c.last_name,
407 o.status, o.payment_method, o.discount
408 ORDER BY 8 DESC, o.order_num;
409$$;
410
411CREATE OR REPLACE FUNCTION get_products_by_total_sales()
412RETURNS TABLE (
413 product_code VARCHAR(8),
414 product_description VARCHAR(500),
415 product_price NUMERIC,
416 number_of_orders BIGINT,
417 total_quantity_sold BIGINT,
418 total_revenue NUMERIC
419)
420LANGUAGE sql
421AS $$
422 SELECT
423 p.code,
424 p.description,
425 p.price::NUMERIC,
426 COUNT(DISTINCT i.order_num) AS number_of_orders,
427 COALESCE(SUM(i.quantity), 0)::BIGINT AS total_quantity_sold,
428 ROUND(
429 COALESCE(
430 SUM(
431 p.price * i.quantity
432 * (1 - COALESCE(o.discount, 0) / 100.0)
433 ),
434 0
435 ),
436 2
437 ) AS total_revenue
438 FROM product p
439 LEFT JOIN includes i ON i.product_code = p.code
440 LEFT JOIN "order" o ON o.order_num = i.order_num
441 GROUP BY p.code, p.description, p.price
442 ORDER BY 4 DESC, 5 DESC, 6 DESC;
443$$;
444
445CREATE OR REPLACE FUNCTION get_low_stock_high_demand_products(
446 p_stock_threshold INTEGER,
447 p_demand_threshold INTEGER
448)
449RETURNS TABLE (
450 product_code VARCHAR(8),
451 product_description VARCHAR(500),
452 current_stock INTEGER,
453 number_of_orders BIGINT,
454 total_quantity_sold BIGINT
455)
456LANGUAGE sql
457AS $$
458 SELECT
459 p.code,
460 p.description,
461 p.availability,
462 COUNT(DISTINCT i.order_num),
463 COALESCE(SUM(i.quantity), 0)::BIGINT
464 FROM product p
465 JOIN includes i ON i.product_code = p.code
466 GROUP BY p.code, p.description, p.availability
467 HAVING
468 p.availability < p_stock_threshold
469 AND COUNT(DISTINCT i.order_num) >= p_demand_threshold
470 ORDER BY 4 DESC, 5 DESC, 3 ASC;
471$$;
472
473CREATE OR REPLACE FUNCTION get_products_monthly_sales()
474RETURNS TABLE (
475 product_code VARCHAR(8),
476 product_description VARCHAR(500),
477 year INTEGER,
478 month INTEGER,
479 number_of_orders BIGINT,
480 total_quantity_sold BIGINT,
481 total_revenue NUMERIC
482)
483LANGUAGE sql
484AS $$
485 SELECT
486 p.code,
487 p.description,
488 EXTRACT(YEAR FROM o.last_date_mod)::INTEGER,
489 EXTRACT(MONTH FROM o.last_date_mod)::INTEGER,
490 COUNT(DISTINCT o.order_num),
491 COALESCE(SUM(i.quantity), 0)::BIGINT,
492 ROUND(
493 COALESCE(
494 SUM(
495 p.price * i.quantity
496 * (1 - COALESCE(o.discount, 0) / 100.0)
497 ),
498 0
499 ),
500 2
501 )
502 FROM product p
503 JOIN includes i ON i.product_code = p.code
504 JOIN "order" o ON o.order_num = i.order_num
505 GROUP BY
506 p.code, p.description,
507 EXTRACT(YEAR FROM o.last_date_mod),
508 EXTRACT(MONTH FROM o.last_date_mod)
509 ORDER BY 3 DESC, 4 DESC, 7 DESC;
510$$;
511
512CREATE OR REPLACE FUNCTION get_stores_by_last_calendar_year_revenue()
513RETURNS TABLE (
514 store_id VARCHAR(3),
515 store_name VARCHAR(50),
516 number_of_orders BIGINT,
517 total_quantity_sold BIGINT,
518 total_revenue NUMERIC
519)
520LANGUAGE sql
521AS $$
522 SELECT
523 s.store_id,
524 s.name,
525 COUNT(DISTINCT o.order_num),
526 COALESCE(SUM(i.quantity), 0)::BIGINT,
527 ROUND(
528 COALESCE(
529 SUM(
530 p.price * i.quantity
531 * (1 - COALESCE(o.discount, 0) / 100.0)
532 ),
533 0
534 ),
535 2
536 )
537 FROM store s
538 LEFT JOIN sells se ON se.store_id = s.store_id
539 LEFT JOIN product p ON p.code = se.product_code
540 LEFT JOIN includes i ON i.product_code = p.code
541 LEFT JOIN "order" o
542 ON o.order_num = i.order_num
543 AND o.last_date_mod >= DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '1 year'
544 AND o.last_date_mod < DATE_TRUNC('year', CURRENT_DATE)
545 GROUP BY s.store_id, s.name
546 ORDER BY 5 DESC, s.store_id;
547$$;
548
549CREATE OR REPLACE FUNCTION get_products_never_ordered()
550RETURNS TABLE (
551 product_code VARCHAR(8),
552 product_description VARCHAR(500),
553 product_price NUMERIC,
554 current_stock INTEGER
555)
556LANGUAGE sql
557AS $$
558 SELECT p.code, p.description, p.price::NUMERIC, p.availability
559 FROM product p
560 WHERE NOT EXISTS (
561 SELECT 1
562 FROM includes i
563 WHERE i.product_code = p.code
564 )
565 ORDER BY p.code;
566$$;
567
568CREATE OR REPLACE FUNCTION get_products_by_number_of_orders()
569RETURNS TABLE (
570 product_code VARCHAR(8),
571 product_description VARCHAR(500),
572 product_price NUMERIC,
573 number_of_orders BIGINT
574)
575LANGUAGE sql
576AS $$
577 SELECT
578 p.code,
579 p.description,
580 p.price::NUMERIC,
581 COUNT(DISTINCT i.order_num)
582 FROM product p
583 JOIN includes i ON i.product_code = p.code
584 GROUP BY p.code, p.description, p.price
585 ORDER BY 4 DESC, p.code;
586$$;
587
588CREATE OR REPLACE FUNCTION get_stores_by_average_review()
589RETURNS TABLE (
590 store_id VARCHAR(3),
591 store_name VARCHAR(50),
592 average_review NUMERIC,
593 number_of_reviews BIGINT
594)
595LANGUAGE sql
596AS $$
597 WITH store_reviews AS (
598 SELECT DISTINCT
599 s.store_id,
600 s.name AS store_name,
601 r.order_num,
602 r.rating
603 FROM store s
604 JOIN sells se ON se.store_id = s.store_id
605 JOIN includes i ON i.product_code = se.product_code
606 JOIN review r ON r.order_num = i.order_num
607 )
608 SELECT
609 s.store_id,
610 s.name,
611 COALESCE(ROUND(AVG(sr.rating), 2), 0)::NUMERIC,
612 COUNT(sr.order_num)
613 FROM store s
614 LEFT JOIN store_reviews sr ON sr.store_id = s.store_id
615 GROUP BY s.store_id, s.name
616 ORDER BY 3 DESC, s.store_id;
617$$;
618
619CREATE OR REPLACE FUNCTION get_store_with_highest_revenue_growth()
620RETURNS TABLE (
621 store_id VARCHAR(3),
622 store_name VARCHAR(50),
623 previous_year_revenue NUMERIC,
624 last_year_revenue NUMERIC,
625 revenue_growth NUMERIC
626)
627LANGUAGE sql
628AS $$
629 WITH store_years AS (
630 SELECT
631 s.store_id,
632 s.name AS store_name,
633 EXTRACT(YEAR FROM o.last_date_mod)::INTEGER AS sales_year,
634 SUM(
635 p.price * i.quantity
636 * (1 - COALESCE(o.discount, 0) / 100.0)
637 ) AS revenue
638 FROM store s
639 JOIN sells se ON se.store_id = s.store_id
640 JOIN includes i ON i.product_code = se.product_code
641 JOIN product p ON p.code = i.product_code
642 JOIN "order" o ON o.order_num = i.order_num
643 WHERE o.last_date_mod >= DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '2 years'
644 AND o.last_date_mod < DATE_TRUNC('year', CURRENT_DATE)
645 GROUP BY s.store_id, s.name, EXTRACT(YEAR FROM o.last_date_mod)
646 ),
647 comparison AS (
648 SELECT
649 s.store_id,
650 s.name AS store_name,
651 COALESCE(MAX(CASE
652 WHEN sy.sales_year = EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2
653 THEN sy.revenue ELSE 0 END), 0) AS previous_year_revenue,
654 COALESCE(MAX(CASE
655 WHEN sy.sales_year = EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1
656 THEN sy.revenue ELSE 0 END), 0) AS last_year_revenue
657 FROM store s
658 LEFT JOIN store_years sy ON sy.store_id = s.store_id
659 GROUP BY s.store_id, s.name
660 )
661 SELECT
662 store_id,
663 store_name,
664 ROUND(previous_year_revenue, 2),
665 ROUND(last_year_revenue, 2),
666 ROUND(last_year_revenue - previous_year_revenue, 2)
667 FROM comparison
668 ORDER BY 5 DESC, store_id
669 LIMIT 1;
670$$;
671
672CREATE OR REPLACE FUNCTION get_clients_by_number_of_orders()
673RETURNS TABLE (
674 client_id INTEGER,
675 client_name TEXT,
676 number_of_orders BIGINT
677)
678LANGUAGE sql
679AS $$
680 SELECT
681 c.client_id,
682 CONCAT_WS(' ', c.first_name, c.last_name),
683 COUNT(o.order_num)
684 FROM client c
685 JOIN "order" o ON o.client_id = c.client_id
686 GROUP BY c.client_id, c.first_name, c.last_name
687 ORDER BY 3 DESC, c.client_id;
688$$;
689
690CREATE OR REPLACE FUNCTION get_approximate_orders_per_client()
691RETURNS TABLE (
692 total_clients BIGINT,
693 total_orders BIGINT,
694 approximate_orders_per_client NUMERIC
695)
696LANGUAGE sql
697AS $$
698 SELECT
699 (SELECT COUNT(*) FROM client),
700 (SELECT COUNT(*) FROM "order"),
701 ROUND(
702 (SELECT COUNT(*)::NUMERIC FROM "order")
703 / NULLIF((SELECT COUNT(*) FROM client), 0),
704 2
705 );
706$$;
707
708CREATE OR REPLACE FUNCTION get_clients_without_orders()
709RETURNS TABLE (
710 client_id INTEGER,
711 client_name TEXT,
712 email VARCHAR(50)
713)
714LANGUAGE sql
715AS $$
716 SELECT
717 c.client_id,
718 CONCAT_WS(' ', c.first_name, c.last_name),
719 c.email
720 FROM client c
721 WHERE NOT EXISTS (
722 SELECT 1 FROM "order" o WHERE o.client_id = c.client_id
723 )
724 ORDER BY c.client_id;
725$$;
726
727CREATE OR REPLACE FUNCTION get_store_request_statistics()
728RETURNS TABLE (
729 store_id VARCHAR(3),
730 store_name VARCHAR(50),
731 total_requests BIGINT,
732 solved_requests BIGINT,
733 requests_in_progress BIGINT
734)
735LANGUAGE sql
736AS $$
737 SELECT
738 s.store_id,
739 s.name,
740 COUNT(r.request_num),
741 COUNT(r.request_num) FILTER (WHERE r.customer_satisfaction > 0),
742 COUNT(r.request_num) FILTER (WHERE r.customer_satisfaction <= 0)
743 FROM store s
744 LEFT JOIN for_store fs ON fs.store_id = s.store_id
745 LEFT JOIN request r ON r.request_num = fs.request_num
746 GROUP BY s.store_id, s.name
747 ORDER BY 3 DESC, s.store_id;
748$$;
749
750CREATE OR REPLACE FUNCTION get_top_10_employees_by_requests_last_month()
751RETURNS TABLE (
752 employee_id VARCHAR(10),
753 employee_name TEXT,
754 number_of_requests BIGINT
755)
756LANGUAGE sql
757AS $$
758 SELECT
759 e.employee_id,
760 CONCAT_WS(' ', p.first_name, p.last_name),
761 COUNT(DISTINCT a.request_num)
762 FROM employees e
763 JOIN personal p ON p.id = e.employee_id
764 JOIN answers a ON a.personal_id = e.employee_id
765 JOIN request r ON r.request_num = a.request_num
766 WHERE r.date_and_time >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month'
767 AND r.date_and_time < DATE_TRUNC('month', CURRENT_DATE)
768 GROUP BY e.employee_id, p.first_name, p.last_name
769 ORDER BY 3 DESC, e.employee_id
770 LIMIT 10;
771$$;
772
773CREATE OR REPLACE FUNCTION get_employees_by_hours_and_pay()
774RETURNS TABLE (
775 employee_id VARCHAR(10),
776 employee_name TEXT,
777 total_hours_worked NUMERIC,
778 total_pay NUMERIC
779)
780LANGUAGE sql
781AS $$
782 SELECT
783 e.employee_id,
784 CONCAT_WS(' ', p.first_name, p.last_name),
785 COALESCE(SUM(w.total_hours), 0),
786 COALESCE(SUM(w.wage * w.total_hours), 0)
787 FROM employees e
788 JOIN personal p ON p.id = e.employee_id
789 LEFT JOIN worked w ON w.personal_id = e.employee_id
790 GROUP BY e.employee_id, p.first_name, p.last_name
791 ORDER BY 3 DESC, 4 DESC, e.employee_id;
792$$;
793
794CREATE OR REPLACE FUNCTION get_stores_average_pay()
795RETURNS TABLE (
796 store_id VARCHAR(3),
797 store_name VARCHAR(50),
798 average_pay NUMERIC
799)
800LANGUAGE sql
801AS $$
802 SELECT
803 s.store_id,
804 s.name,
805 COALESCE(ROUND(AVG(w.wage), 2), 0)
806 FROM store s
807 LEFT JOIN worked w ON w.store_id = s.store_id
808 GROUP BY s.store_id, s.name
809 ORDER BY 3 DESC, s.store_id;
810$$;
811
812CREATE OR REPLACE FUNCTION get_employee_product_changes_last_month()
813RETURNS TABLE (
814 employee_id VARCHAR(10),
815 employee_name TEXT,
816 number_of_product_changes BIGINT
817)
818LANGUAGE sql
819AS $$
820 SELECT
821 e.employee_id,
822 CONCAT_WS(' ', p.first_name, p.last_name),
823 COUNT(mc.change_date_time)
824 FROM employees e
825 JOIN personal p ON p.id = e.employee_id
826 LEFT JOIN makes_change mc
827 ON mc.personal_id = e.employee_id
828 AND mc.change_date_time >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month'
829 AND mc.change_date_time < DATE_TRUNC('month', CURRENT_DATE)
830 GROUP BY e.employee_id, p.first_name, p.last_name
831 ORDER BY 3 DESC, e.employee_id;
832$$;
833
834CREATE OR REPLACE FUNCTION get_stores_by_monthly_profit_and_revenue_growth()
835RETURNS TABLE (
836 store_id VARCHAR(3),
837 store_name VARCHAR(50),
838 month_and_year TEXT,
839 monthly_profit NUMERIC,
840 previous_month_revenue NUMERIC,
841 current_month_revenue NUMERIC,
842 revenue_growth NUMERIC
843)
844LANGUAGE sql
845AS $$
846 WITH monthly_revenue AS (
847 SELECT
848 s.store_id,
849 s.name AS store_name,
850 DATE_TRUNC('month', o.last_date_mod)::DATE AS month_date,
851 SUM(
852 p.price * i.quantity
853 * (1 - COALESCE(o.discount, 0) / 100.0)
854 ) AS revenue
855 FROM store s
856 JOIN sells se ON se.store_id = s.store_id
857 JOIN includes i ON i.product_code = se.product_code
858 JOIN product p ON p.code = i.product_code
859 JOIN "order" o ON o.order_num = i.order_num
860 GROUP BY s.store_id, s.name, DATE_TRUNC('month', o.last_date_mod)
861 ),
862 with_previous AS (
863 SELECT
864 store_id,
865 store_name,
866 month_date,
867 revenue,
868 LAG(revenue) OVER (
869 PARTITION BY store_id
870 ORDER BY month_date
871 ) AS previous_revenue
872 FROM monthly_revenue
873 )
874 SELECT
875 store_id,
876 store_name,
877 TO_CHAR(month_date, 'YYYY-MM'),
878 ROUND(revenue, 2),
879 ROUND(COALESCE(previous_revenue, 0), 2),
880 ROUND(revenue, 2),
881 ROUND(revenue - COALESCE(previous_revenue, 0), 2)
882 FROM with_previous
883 ORDER BY month_date DESC, 4 DESC, store_id;
884$$;
885
886CREATE OR REPLACE FUNCTION get_unapproved_reports()
887RETURNS TABLE (
888 report_date TIMESTAMP,
889 store_id VARCHAR(3),
890 overall_profit NUMERIC,
891 sales_trend VARCHAR(100),
892 marketing_growth VARCHAR(100),
893 owner_signature VARCHAR(50)
894)
895LANGUAGE sql
896AS $$
897 SELECT
898 r.date,
899 r.store_id,
900 r.overall_profit,
901 r.sales_trend,
902 r.marketing_growth,
903 r.owner_signature
904 FROM report r
905 LEFT JOIN approves a
906 ON a.report_date = r.date
907 AND a.store_id = r.store_id
908 WHERE a.report_date IS NULL
909 ORDER BY r.date DESC, r.store_id;
910$$;
911`;
912
[2d1ec46]913
914const TRIGGERS_SQL = String.raw`
915-- ============================================================
916-- HANDCRAFT MARKETPLACE TRIGGERS
917-- Compatible with the supplied project schema.
918-- ============================================================
919
920CREATE OR REPLACE FUNCTION update_store_rating()
921RETURNS TRIGGER
922LANGUAGE plpgsql
923AS $$
924DECLARE
925 v_order_num VARCHAR(11);
926 v_store_id VARCHAR(3);
927BEGIN
928 IF TG_OP = 'DELETE' THEN
929 v_order_num := OLD.order_num;
930 ELSE
931 v_order_num := NEW.order_num;
932 END IF;
933
934 v_store_id := LEFT(v_order_num, 3);
935
936 UPDATE store s
937 SET rating = COALESCE(
938 (
939 SELECT ROUND(AVG(r.rating), 1)
940 FROM review r
941 JOIN includes i
942 ON i.order_num = r.order_num
943 JOIN sells sl
944 ON sl.product_code = i.product_code
945 AND sl.store_ID = v_store_id
946 JOIN "order" o
947 ON o.order_num = r.order_num
948 WHERE LEFT(o.order_num, 3) = v_store_id
949 ),
950 0
951 )
952 WHERE s.store_ID = v_store_id;
953
954 RETURN NULL;
955END;
956$$;
957
958DROP TRIGGER IF EXISTS trg_update_store_rating ON review;
959
960CREATE TRIGGER trg_update_store_rating
961AFTER INSERT OR UPDATE OR DELETE
962ON review
963FOR EACH ROW
964EXECUTE FUNCTION update_store_rating();
965
966
967CREATE OR REPLACE FUNCTION check_product_availability()
968RETURNS TRIGGER
969LANGUAGE plpgsql
970AS $$
971DECLARE
972 v_available_quantity INTEGER;
973BEGIN
974 SELECT p.availability
975 INTO v_available_quantity
976 FROM product p
977 WHERE p.code = NEW.product_code
978 FOR UPDATE;
979
980 IF v_available_quantity IS NULL THEN
981 RAISE EXCEPTION
982 'Product % does not exist or is not available.',
983 NEW.product_code;
984 END IF;
985
986 IF TG_OP = 'INSERT' THEN
987 IF NEW.quantity > v_available_quantity THEN
988 RAISE EXCEPTION
989 'Insufficient stock for product %. Available: %, requested: %.',
990 NEW.product_code,
991 v_available_quantity,
992 NEW.quantity;
993 END IF;
994
995 ELSIF TG_OP = 'UPDATE' THEN
996 IF NEW.product_code = OLD.product_code THEN
997 IF NEW.quantity > OLD.quantity
998 AND (NEW.quantity - OLD.quantity) > v_available_quantity THEN
999 RAISE EXCEPTION
1000 'Insufficient stock for product %. Available: %, additional requested: %.',
1001 NEW.product_code,
1002 v_available_quantity,
1003 NEW.quantity - OLD.quantity;
1004 END IF;
1005 ELSE
1006 IF NEW.quantity > v_available_quantity THEN
1007 RAISE EXCEPTION
1008 'Insufficient stock for product %. Available: %, requested: %.',
1009 NEW.product_code,
1010 v_available_quantity,
1011 NEW.quantity;
1012 END IF;
1013 END IF;
1014 END IF;
1015
1016 RETURN NEW;
1017END;
1018$$;
1019
1020DROP TRIGGER IF EXISTS trg_check_product_availability ON includes;
1021
1022CREATE TRIGGER trg_check_product_availability
1023BEFORE INSERT OR UPDATE
1024ON includes
1025FOR EACH ROW
1026EXECUTE FUNCTION check_product_availability();
1027
1028
1029CREATE OR REPLACE FUNCTION update_product_availability()
1030RETURNS TRIGGER
1031LANGUAGE plpgsql
1032AS $$
1033BEGIN
1034 IF TG_OP = 'INSERT' THEN
1035
1036 UPDATE product
1037 SET availability = availability - NEW.quantity
1038 WHERE code = NEW.product_code;
1039
1040 ELSIF TG_OP = 'UPDATE' THEN
1041
1042 IF NEW.product_code = OLD.product_code THEN
1043
1044 UPDATE product
1045 SET availability = availability - (NEW.quantity - OLD.quantity)
1046 WHERE code = NEW.product_code;
1047
1048 ELSE
1049
1050 UPDATE product
1051 SET availability = availability + OLD.quantity
1052 WHERE code = OLD.product_code;
1053
1054 UPDATE product
1055 SET availability = availability - NEW.quantity
1056 WHERE code = NEW.product_code;
1057
1058 END IF;
1059
1060 ELSIF TG_OP = 'DELETE' THEN
1061
1062 UPDATE product
1063 SET availability = availability + OLD.quantity
1064 WHERE code = OLD.product_code;
1065
1066 END IF;
1067
1068 RETURN NULL;
1069END;
1070$$;
1071
1072DROP TRIGGER IF EXISTS trg_update_product_availability ON includes;
1073
1074CREATE TRIGGER trg_update_product_availability
1075AFTER INSERT OR UPDATE OR DELETE
1076ON includes
1077FOR EACH ROW
1078EXECUTE FUNCTION update_product_availability();
1079
1080
1081CREATE OR REPLACE FUNCTION delete_product_changes()
1082RETURNS TRIGGER
1083LANGUAGE plpgsql
1084AS $$
1085BEGIN
1086 DELETE FROM "change"
1087 WHERE product_code = OLD.code;
1088
1089 RETURN OLD;
1090END;
1091$$;
1092
1093DROP TRIGGER IF EXISTS trg_delete_product_changes ON product;
1094
1095CREATE TRIGGER trg_delete_product_changes
1096BEFORE DELETE
1097ON product
1098FOR EACH ROW
1099EXECUTE FUNCTION delete_product_changes();
1100
1101
1102CREATE OR REPLACE FUNCTION prevent_store_deletion()
1103RETURNS TRIGGER
1104LANGUAGE plpgsql
1105AS $$
1106BEGIN
1107 IF EXISTS (
1108 SELECT 1
1109 FROM report
1110 WHERE store_ID = OLD.store_ID
1111 ) THEN
1112 RAISE EXCEPTION
1113 'Store % cannot be deleted because it has existing reports.',
1114 OLD.store_ID;
1115 END IF;
1116
1117 IF EXISTS (
1118 SELECT 1
1119 FROM sells
1120 WHERE store_ID = OLD.store_ID
1121 ) THEN
1122 RAISE EXCEPTION
1123 'Store % cannot be deleted because it has existing product records.',
1124 OLD.store_ID;
1125 END IF;
1126
1127 IF EXISTS (
1128 SELECT 1
1129 FROM "order"
1130 WHERE LEFT(order_num, 3) = OLD.store_ID
1131 ) THEN
1132 RAISE EXCEPTION
1133 'Store % cannot be deleted because it has existing orders.',
1134 OLD.store_ID;
1135 END IF;
1136
1137 RETURN OLD;
1138END;
1139$$;
1140
1141DROP TRIGGER IF EXISTS trg_prevent_store_deletion ON store;
1142
1143CREATE TRIGGER trg_prevent_store_deletion
1144BEFORE DELETE
1145ON store
1146FOR EACH ROW
1147EXECUTE FUNCTION prevent_store_deletion();
1148
1149
1150CREATE OR REPLACE FUNCTION update_order_modified_date()
1151RETURNS TRIGGER
1152LANGUAGE plpgsql
1153AS $$
1154BEGIN
1155 NEW.last_date_mod := CURRENT_TIMESTAMP;
1156 RETURN NEW;
1157END;
1158$$;
1159
1160DROP TRIGGER IF EXISTS trg_update_order_modified_date ON "order";
1161
1162CREATE TRIGGER trg_update_order_modified_date
1163BEFORE UPDATE
1164ON "order"
1165FOR EACH ROW
1166EXECUTE FUNCTION update_order_modified_date();
1167
1168
1169CREATE OR REPLACE FUNCTION validate_employee_authorization()
1170RETURNS TRIGGER
1171LANGUAGE plpgsql
1172AS $$
1173DECLARE
1174 v_authorisation TEXT;
1175BEGIN
1176 SELECT p.authorisation
1177 INTO v_authorisation
1178 FROM permissions p
1179 WHERE p.personal_id = NEW.personal_id;
1180
1181 IF v_authorisation IS NULL THEN
1182 RAISE EXCEPTION
1183 'Employee % does not have valid authorization.',
1184 NEW.personal_id;
1185 END IF;
1186
1187 -- makes_change has no authorisation column in the supplied schema.
1188 -- The employee's permission row is therefore the source of truth.
1189 RETURN NEW;
1190END;
1191$$;
1192
1193DROP TRIGGER IF EXISTS trg_validate_employee_authorization ON makes_change;
1194
1195CREATE TRIGGER trg_validate_employee_authorization
1196BEFORE INSERT OR UPDATE
1197ON makes_change
1198FOR EACH ROW
1199EXECUTE FUNCTION validate_employee_authorization();
1200
1201
1202CREATE OR REPLACE FUNCTION initialize_employee_statistics()
1203RETURNS TRIGGER
1204LANGUAGE plpgsql
1205AS $$
1206BEGIN
1207 /*
1208 * The schema creates an employee before store/report assignment in the
1209 * normal application flow. If an assignment and a report already exist,
1210 * create a zero-hours starting record; otherwise there is nothing to
1211 * initialize yet.
1212 */
1213 INSERT INTO worked (
1214 personal_id,
1215 report_date,
1216 store_ID,
1217 wage,
1218 pay_method,
1219 total_hours,
1220 week
1221 )
1222 SELECT
1223 NEW.employee_id,
1224 r.date,
1225 wis.store_ID,
1226 0,
1227 'full_time',
1228 0,
1229 TO_CHAR(CURRENT_DATE - INTERVAL '6 days', 'DD.MM.YYYY')
1230 || ' - ' ||
1231 TO_CHAR(CURRENT_DATE, 'DD.MM.YYYY')
1232 FROM works_in_store wis
1233 JOIN LATERAL (
1234 SELECT r2.date
1235 FROM report r2
1236 WHERE r2.store_ID = wis.store_ID
1237 ORDER BY r2.date DESC
1238 LIMIT 1
1239 ) r ON TRUE
1240 WHERE wis.personal_id = NEW.employee_id
1241 ON CONFLICT (personal_id, report_date, store_ID) DO NOTHING;
1242
1243 RETURN NEW;
1244END;
1245$$;
1246
1247DROP TRIGGER IF EXISTS trg_initialize_employee_statistics ON employees;
1248
1249CREATE TRIGGER trg_initialize_employee_statistics
1250AFTER INSERT
1251ON employees
1252FOR EACH ROW
1253EXECUTE FUNCTION initialize_employee_statistics();
1254`;
1255
1256
1257const VIEWS_SQL = String.raw`
1258-- ============================================================
1259-- HANDCRAFT MARKETPLACE VIEWS
1260-- Compatible with the supplied project schema.
1261-- ============================================================
1262
1263DROP VIEW IF EXISTS vw_product_store_overview;
1264CREATE VIEW vw_product_store_overview AS
1265SELECT
1266 p.code AS product_code,
1267 p.description,
1268 p.price,
1269 p.availability,
1270 p.weight,
1271 p.width_x_length_x_depth,
1272 p.aprox_production_time,
1273 s.store_ID,
1274 s.name AS store_name,
1275 s.physical_address,
1276 s.rating,
1277 sl.discount
1278FROM product p
1279JOIN sells sl
1280 ON sl.product_code = p.code
1281JOIN store s
1282 ON s.store_ID = sl.store_ID;
1283
1284
1285DROP VIEW IF EXISTS vw_customer_order_overview;
1286CREATE VIEW vw_customer_order_overview AS
1287SELECT
1288 o.order_num,
1289 i.quantity,
1290 o.status,
1291 o.last_date_mod,
1292 o.payment_method,
1293 o.discount,
1294 c.client_ID,
1295 c.first_name,
1296 c.last_name,
1297 c.email,
1298 p.code AS product_code,
1299 p.description AS product_description,
1300 p.price
1301FROM "order" o
1302JOIN makes_request mr
1303 ON mr.order_num = o.order_num
1304JOIN client c
1305 ON c.client_ID = mr.client_ID
1306JOIN includes i
1307 ON i.order_num = o.order_num
1308JOIN product p
1309 ON p.code = i.product_code;
1310
1311
1312DROP VIEW IF EXISTS vw_monthly_sales_profit;
1313CREATE VIEW vw_monthly_sales_profit AS
1314SELECT
1315 s.store_ID,
1316 s.name AS store_name,
1317 r.date AS report_date,
1318 mp.month_and_year,
1319 mp.profit,
1320 r.overall_profit,
1321 ed.monthly_profit,
1322 ed.sales,
1323 ed.damages
1324FROM store s
1325JOIN report r
1326 ON r.store_ID = s.store_ID
1327LEFT JOIN monthly_profit mp
1328 ON mp.report_date = r.date
1329 AND mp.store_ID = r.store_ID
1330LEFT JOIN exchanges_data ed
1331 ON ed.report_date = r.date
1332 AND ed.store_ID = r.store_ID;
1333
1334
1335DROP VIEW IF EXISTS vw_employee_workload_salary;
1336CREATE VIEW vw_employee_workload_salary AS
1337SELECT
1338 e.employee_id,
1339 p.first_name,
1340 p.last_name,
1341 p.email,
1342 e.date_of_hire,
1343 s.store_ID,
1344 s.name AS store_name,
1345 w.week,
1346 w.total_hours,
1347 w.wage,
1348 w.pay_method,
1349 COALESCE(w.wage * w.total_hours, 0) AS total_pay
1350FROM employees e
1351JOIN personal p
1352 ON p.id = e.employee_id
1353JOIN works_in_store wis
1354 ON wis.personal_id = e.employee_id
1355JOIN store s
1356 ON s.store_ID = wis.store_ID
1357LEFT JOIN worked w
1358 ON w.personal_id = e.employee_id
1359 AND w.store_ID = wis.store_ID;
1360
1361
1362DROP VIEW IF EXISTS vw_customer_request_response;
1363CREATE VIEW vw_customer_request_response AS
1364SELECT
1365 r.request_num,
1366 r.date_and_time,
1367 r.problem,
1368 r.notes_of_communication,
1369 r.customer_satisfaction,
1370 c.client_ID,
1371 c.first_name AS client_first_name,
1372 c.last_name AS client_last_name,
1373 c.email AS client_email,
1374 p.id AS employee_id,
1375 p.first_name AS employee_first_name,
1376 p.last_name AS employee_last_name
1377FROM request r
1378LEFT JOIN client c
1379 ON c.client_ID = CASE
1380 WHEN SUBSTRING(r.request_num FROM 9 FOR 4) ~ '^[0-9]{4}$'
1381 THEN SUBSTRING(r.request_num FROM 9 FOR 4)::INTEGER
1382 ELSE NULL
1383 END
1384LEFT JOIN answers a
1385 ON a.request_num = r.request_num
1386LEFT JOIN personal p
1387 ON p.id = a.personal_id;
1388
1389
1390DROP VIEW IF EXISTS vw_store_inventory;
1391CREATE VIEW vw_store_inventory AS
1392SELECT
1393 s.store_ID,
1394 s.name AS store_name,
1395 p.code AS product_code,
1396 p.description,
1397 p.price,
1398 p.availability,
1399 p.aprox_production_time,
1400 sl.discount
1401FROM store s
1402JOIN sells sl
1403 ON sl.store_ID = s.store_ID
1404JOIN product p
1405 ON p.code = sl.product_code;
1406
1407
1408DROP VIEW IF EXISTS vw_customer_order_history;
1409CREATE VIEW vw_customer_order_history AS
1410SELECT
1411 c.client_ID,
1412 c.first_name,
1413 c.last_name,
1414 c.email,
1415 o.order_num,
1416 i.quantity,
1417 o.status,
1418 o.last_date_mod,
1419 o.payment_method,
1420 o.discount,
1421 p.code AS product_code,
1422 p.description AS product_description,
1423 p.price
1424FROM client c
1425JOIN makes_request mr
1426 ON mr.client_ID = c.client_ID
1427JOIN "order" o
1428 ON o.order_num = mr.order_num
1429JOIN includes i
1430 ON i.order_num = o.order_num
1431JOIN product p
1432 ON p.code = i.product_code;
1433
1434
1435DROP VIEW IF EXISTS vw_store_performance;
1436CREATE VIEW vw_store_performance AS
1437SELECT
1438 s.store_ID,
1439 s.name AS store_name,
1440 s.date_of_founding,
1441 s.rating,
1442 COUNT(DISTINCT r.date) AS number_of_reports,
1443 COALESCE(SUM(mp.profit), 0) AS total_reported_profit,
1444 COALESCE(MAX(r.overall_profit), 0) AS overall_profit,
1445 COALESCE(SUM(ed.sales), 0) AS total_sales,
1446 COALESCE(SUM(ed.damages), 0) AS total_damages,
1447 COALESCE(SUM(ed.monthly_profit), 0) AS total_monthly_profit
1448FROM store s
1449LEFT JOIN report r
1450 ON r.store_ID = s.store_ID
1451LEFT JOIN monthly_profit mp
1452 ON mp.report_date = r.date
1453 AND mp.store_ID = r.store_ID
1454LEFT JOIN exchanges_data ed
1455 ON ed.report_date = r.date
1456 AND ed.store_ID = r.store_ID
1457GROUP BY
1458 s.store_ID,
1459 s.name,
1460 s.date_of_founding,
1461 s.rating;
1462`;
1463
1464
[33517cc]1465const database = {
1466 database: {
1467 get(sql, params, callback) {
1468 if (typeof params === 'function') {
1469 callback = params;
1470 params = [];
1471 }
1472 dbQuery(sql, params || [], (err, result) => {
1473 callback(err, result && result.rows ? result.rows[0] : undefined);
1474 });
1475 },
1476 all(sql, params, callback) {
1477 if (typeof params === 'function') {
1478 callback = params;
1479 params = [];
1480 }
1481 dbQuery(sql, params || [], (err, result) => {
1482 callback(err, result ? result.rows : []);
1483 });
1484 },
1485 run(sql, params, callback) {
1486 if (typeof params === 'function') {
1487 callback = params;
1488 params = [];
1489 }
[06ebe74]1490 const normalized = String(sql).trim().replace(/;\s*$/, '').toUpperCase();
1491
[33517cc]1492 if (normalized === 'BEGIN TRANSACTION' || normalized === 'BEGIN') {
[6c7cfa6]1493 const existingClient = transactionStorage.getStore();
1494
1495 // A transaction is already active in this async execution context.
1496 // Do not create a second transaction on the same request.
1497 if (existingClient) {
[33517cc]1498 callback?.(null);
[4dff800]1499 return;
1500 }
[6c7cfa6]1501
1502 pool.connect()
1503 .then(client => {
1504 return client.query('BEGIN')
1505 .then(() => {
1506 // Everything scheduled by the callback now inherits
1507 // this client through AsyncLocalStorage. Other
1508 // concurrent requests get their own transaction.
1509 transactionStorage.run(client, () => {
1510 callback?.(null);
1511 });
1512 })
1513 .catch(err => {
1514 client.release();
1515 callback?.(err);
1516 });
1517 })
[33517cc]1518 .catch(err => {
1519 callback?.(err);
[4dff800]1520 });
[6c7cfa6]1521
[33517cc]1522 return;
1523 }
[06ebe74]1524
[33517cc]1525 if (normalized === 'COMMIT') {
[6c7cfa6]1526 const client = transactionStorage.getStore();
1527
1528 if (!client) {
[33517cc]1529 callback?.(null);
1530 return;
[4dff800]1531 }
[6c7cfa6]1532
[33517cc]1533 client.query('COMMIT')
1534 .then(() => {
1535 client.release();
1536 callback?.(null);
1537 })
1538 .catch(err => {
[6c7cfa6]1539 // COMMIT may fail before the transaction is completed.
1540 // Roll back before releasing the client when possible.
1541 client.query('ROLLBACK')
1542 .catch(() => {})
1543 .then(() => {
1544 client.release();
1545 callback?.(err);
1546 });
[33517cc]1547 });
[6c7cfa6]1548
[33517cc]1549 return;
[4dff800]1550 }
[06ebe74]1551
[33517cc]1552 if (normalized === 'ROLLBACK') {
[6c7cfa6]1553 const client = transactionStorage.getStore();
1554
1555 if (!client) {
[33517cc]1556 callback?.(null);
1557 return;
1558 }
[6c7cfa6]1559
[33517cc]1560 client.query('ROLLBACK')
1561 .then(() => {
1562 client.release();
1563 callback?.(null);
1564 })
1565 .catch(err => {
1566 client.release();
1567 callback?.(err);
1568 });
[6c7cfa6]1569
[69f2a41]1570 return;
1571 }
[06ebe74]1572
[33517cc]1573 dbQuery(sql, params || [], (err, result) => {
1574 if (callback) {
1575 callback.call(
1576 { changes: result ? result.rowCount : 0, lastID: result?.rows?.[0]?.id },
1577 err
1578 );
[69f2a41]1579 }
1580 });
1581 }
[33517cc]1582 },
[69f2a41]1583
[6c7cfa6]1584 async installReportFunctions() {
1585 await pool.query(REPORT_FUNCTIONS_SQL);
1586 console.log('✅ PostgreSQL report functions installed');
1587 },
1588
[2d1ec46]1589 async installTriggersAndViews() {
1590 await pool.query(TRIGGERS_SQL);
1591 console.log('✅ PostgreSQL triggers installed');
1592
1593 await pool.query(VIEWS_SQL);
1594 console.log('✅ PostgreSQL views installed');
1595 },
1596
[6c7cfa6]1597 runReport(reportName, params, callback) {
1598 const allowed = new Set([
1599 'get_orders_by_total',
1600 'get_products_by_total_sales',
1601 'get_low_stock_high_demand_products',
1602 'get_products_monthly_sales',
1603 'get_stores_by_last_calendar_year_revenue',
1604 'get_products_never_ordered',
1605 'get_products_by_number_of_orders',
1606 'get_stores_by_average_review',
1607 'get_store_with_highest_revenue_growth',
1608 'get_clients_by_number_of_orders',
1609 'get_approximate_orders_per_client',
1610 'get_clients_without_orders',
1611 'get_store_request_statistics',
1612 'get_top_10_employees_by_requests_last_month',
1613 'get_employees_by_hours_and_pay',
1614 'get_stores_average_pay',
1615 'get_employee_product_changes_last_month',
1616 'get_stores_by_monthly_profit_and_revenue_growth',
1617 'get_unapproved_reports'
1618 ]);
1619
1620 if (!allowed.has(reportName)) {
1621 callback(new Error('Unknown report: ' + reportName), null);
1622 return;
1623 }
1624
1625 const values = Array.isArray(params) ? params : [];
1626 const placeholders = values.map((_, index) => '$' + (index + 1)).join(', ');
1627
1628 dbQuery(
1629 `SELECT * FROM ${reportName}(${placeholders})`,
1630 values,
1631 (err, result) => callback(err, result?.rows || [])
1632 );
1633 },
1634
[33517cc]1635 async initializeDatabase() {
[06ebe74]1636 // The database supplied by the project is authoritative. Existing tables
1637 // are removed before recreation so an old incompatible schema can never
1638 // survive a restart and cause CREATE TABLE IF NOT EXISTS to skip columns.
1639 const schemaCompatibility = await pool.query(`
1640 SELECT
1641 EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name='product') AS product_exists,
1642 EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='product' AND column_name='category_id') AS product_has_category,
1643 EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='product' AND column_name='store_id') AS product_has_store,
1644 EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='order' AND column_name='store_id') AS order_has_store,
1645 EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='request' AND column_name='store_id') AS request_has_store,
1646 EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='review' AND column_name='review_id') AS review_has_review_id,
1647 EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='refund' AND column_name='request_date') AS refund_has_request_date
1648 `);
1649
1650 const c = schemaCompatibility.rows[0];
1651 const forceReset = String(process.env.RESET_DATABASE || '').toLowerCase() === 'true' || process.env.RESET_DATABASE === '1';
1652 const schemaMismatch = !c.product_exists || !c.product_has_category || c.product_has_store || c.order_has_store || c.request_has_store || c.review_has_review_id || c.refund_has_request_date;
1653 const resetDatabase = forceReset || schemaMismatch;
1654
1655 if (resetDatabase) {
1656 console.log('🧹 Existing PostgreSQL schema is incompatible with the project schema. Recreating project tables...');
1657 await pool.query(`
1658 DROP TABLE IF EXISTS audit_log CASCADE;
1659 DROP TABLE IF EXISTS user_roles CASCADE;
1660 DROP TABLE IF EXISTS roles CASCADE;
1661 DROP TABLE IF EXISTS users CASCADE;
1662 DROP TABLE IF EXISTS approves CASCADE;
1663 DROP TABLE IF EXISTS includes CASCADE;
1664 DROP TABLE IF EXISTS sells CASCADE;
1665 DROP TABLE IF EXISTS worked CASCADE;
1666 DROP TABLE IF EXISTS works_in_store CASCADE;
1667 DROP TABLE IF EXISTS makes_change CASCADE;
1668 DROP TABLE IF EXISTS "change" CASCADE;
1669 DROP TABLE IF EXISTS for_store CASCADE;
1670 DROP TABLE IF EXISTS answers CASCADE;
1671 DROP TABLE IF EXISTS makes_request CASCADE;
1672 DROP TABLE IF EXISTS request CASCADE;
1673 DROP TABLE IF EXISTS exchanges_data CASCADE;
1674 DROP TABLE IF EXISTS monthly_profit CASCADE;
1675 DROP TABLE IF EXISTS report CASCADE;
1676 DROP TABLE IF EXISTS refund CASCADE;
1677 DROP TABLE IF EXISTS review CASCADE;
1678 DROP TABLE IF EXISTS "order" CASCADE;
1679 DROP TABLE IF EXISTS delivery_address CASCADE;
1680 DROP TABLE IF EXISTS client CASCADE;
1681 DROP TABLE IF EXISTS employees CASCADE;
1682 DROP TABLE IF EXISTS boss CASCADE;
1683 DROP TABLE IF EXISTS permissions CASCADE;
1684 DROP TABLE IF EXISTS personal CASCADE;
1685 DROP TABLE IF EXISTS color CASCADE;
1686 DROP TABLE IF EXISTS image CASCADE;
1687 DROP TABLE IF EXISTS product CASCADE;
1688 DROP TABLE IF EXISTS store CASCADE;
1689 DROP TABLE IF EXISTS category CASCADE;
1690 `);
1691 }
[69f2a41]1692
[06ebe74]1693 const schema = `
[33517cc]1694 CREATE TABLE IF NOT EXISTS category (
[6c7cfa6]1695 id SERIAL PRIMARY KEY,
1696 name VARCHAR(50) NOT NULL,
[06ebe74]1697 parent_category_id INTEGER REFERENCES category(id) NOT NULL
[6c7cfa6]1698 );
[06ebe74]1699
1700 CREATE TABLE IF NOT EXISTS product (
[6c7cfa6]1701 code VARCHAR(8) PRIMARY KEY DEFAULT '-1',
[06ebe74]1702 price DECIMAL(10,2) NOT NULL CHECK (price >= 0.0),
1703 availability INTEGER NOT NULL,
1704 weight DECIMAL(5,2) NOT NULL CHECK (weight > 0),
1705 width_x_length_x_depth VARCHAR(20) NOT NULL,
1706 aprox_production_time INTEGER NOT NULL,
1707 description VARCHAR(500) NOT NULL,
1708 category_id INTEGER NOT NULL REFERENCES category(id) ON DELETE SET DEFAULT
[6c7cfa6]1709 );
[06ebe74]1710
1711 CREATE TABLE IF NOT EXISTS image (
[6c7cfa6]1712 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE,
[06ebe74]1713 image VARCHAR NOT NULL DEFAULT 'Image NOT found!'
[6c7cfa6]1714 );
[06ebe74]1715
1716 CREATE TABLE IF NOT EXISTS color (
[6c7cfa6]1717 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE,
[06ebe74]1718 color VARCHAR(50)
[6c7cfa6]1719 );
[06ebe74]1720
[33517cc]1721 CREATE TABLE IF NOT EXISTS store (
[6c7cfa6]1722 store_ID VARCHAR(3) PRIMARY KEY,
[33517cc]1723 name VARCHAR(50) UNIQUE NOT NULL,
[4dff800]1724 date_of_founding DATE NOT NULL,
[33517cc]1725 physical_address VARCHAR(100) NOT NULL,
1726 store_email VARCHAR(40) UNIQUE NOT NULL CHECK (store_email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'),
[06ebe74]1727 rating DECIMAL(2,1) NOT NULL DEFAULT 0 CHECK (rating>=0.0 AND rating<=5.0)
[6c7cfa6]1728 );
[06ebe74]1729
[33517cc]1730 CREATE TABLE IF NOT EXISTS personal (
[6c7cfa6]1731 id VARCHAR(10) PRIMARY KEY,
[33517cc]1732 first_name VARCHAR(20) NOT NULL,
1733 last_name VARCHAR(20) NOT NULL,
[06ebe74]1734 ssn VARCHAR(13) NOT NULL CHECK (ssn ~ '^[0-9]{13}$'),
[33517cc]1735 email VARCHAR(50) UNIQUE NOT NULL CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'),
1736 password VARCHAR NOT NULL
[6c7cfa6]1737 );
[06ebe74]1738
[33517cc]1739 CREATE TABLE IF NOT EXISTS permissions (
[6c7cfa6]1740 personal_id VARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE,
[4dff800]1741 type VARCHAR(50) NOT NULL,
[33517cc]1742 authorisation VARCHAR(50) NOT NULL
[6c7cfa6]1743 );
[06ebe74]1744
[33517cc]1745 CREATE TABLE IF NOT EXISTS boss (
[6c7cfa6]1746 boss_id VARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE
1747 );
[06ebe74]1748
[33517cc]1749 CREATE TABLE IF NOT EXISTS employees (
[6c7cfa6]1750 employee_id VARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE,
[33517cc]1751 date_of_hire DATE NOT NULL
[6c7cfa6]1752 );
[06ebe74]1753
[33517cc]1754 CREATE TABLE IF NOT EXISTS client (
[6c7cfa6]1755 client_ID SERIAL PRIMARY KEY,
1756 first_name VARCHAR(50) NOT NULL,
[33517cc]1757 last_name VARCHAR(50) NOT NULL,
1758 email VARCHAR(50) UNIQUE NOT NULL CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'),
1759 password VARCHAR NOT NULL
[6c7cfa6]1760 );
[06ebe74]1761
[33517cc]1762 CREATE TABLE IF NOT EXISTS delivery_address (
[6c7cfa6]1763 client_ID INTEGER PRIMARY KEY REFERENCES client(client_ID) ON DELETE CASCADE,
[33517cc]1764 address VARCHAR(200) NOT NULL,
1765 city VARCHAR(30) NOT NULL,
1766 postcode VARCHAR(20) NOT NULL,
1767 country VARCHAR(40) NOT NULL,
1768 is_default BOOLEAN DEFAULT TRUE
[6c7cfa6]1769 );
[06ebe74]1770
[33517cc]1771 CREATE TABLE IF NOT EXISTS "order" (
[6c7cfa6]1772 order_num VARCHAR(11) PRIMARY KEY,
[06ebe74]1773 client_ID INTEGER REFERENCES client(client_ID) ON DELETE CASCADE,
[33517cc]1774 status VARCHAR(20) NOT NULL DEFAULT 'placed order',
[06ebe74]1775 last_date_mod TIMESTAMP NOT NULL,
[33517cc]1776 payment_method VARCHAR(250) NOT NULL,
[06ebe74]1777 discount DECIMAL(5,2) DEFAULT 0.0 CHECK(discount>=0.0 AND discount<=100.00),
1778 CONSTRAINT check_status CHECK (status IN ('placed order', 'being processed', 'shipping', 'delivered', 'canceled'))
[6c7cfa6]1779 );
[06ebe74]1780
[33517cc]1781 CREATE TABLE IF NOT EXISTS review (
[6c7cfa6]1782 order_num VARCHAR(11) PRIMARY KEY REFERENCES "order"(order_num) ON DELETE CASCADE,
[33517cc]1783 comment VARCHAR(300),
[06ebe74]1784 rating DECIMAL(2,1) NOT NULL CHECK(rating>=0.0 AND rating<=5.0),
1785 last_mod_date TIMESTAMP NOT NULL
[6c7cfa6]1786 );
[06ebe74]1787
[33517cc]1788 CREATE TABLE IF NOT EXISTS refund (
[6c7cfa6]1789 refund_id SERIAL PRIMARY KEY,
1790 order_num VARCHAR(11) REFERENCES "order"(order_num) ON DELETE CASCADE,
[33517cc]1791 reason VARCHAR(300),
[06ebe74]1792 amount DECIMAL(5,2) NOT NULL,
[33517cc]1793 status VARCHAR(100) NOT NULL DEFAULT 'requested refund',
[6c7cfa6]1794 CONSTRAINT check_refund_status CHECK (status IN ('requested refund', 'being reviewed', 'approved', 'not approved', 'processed'))
1795 );
[06ebe74]1796
[33517cc]1797 CREATE TABLE IF NOT EXISTS report (
[6c7cfa6]1798 date TIMESTAMP NOT NULL,
1799 store_ID VARCHAR(3) NOT NULL REFERENCES store(store_ID) ON DELETE CASCADE,
[06ebe74]1800 overall_profit NUMERIC NOT NULL DEFAULT 0.0 CHECK(overall_profit>=0),
1801 sales_trend VARCHAR(100) NOT NULL,
1802 marketing_growth VARCHAR(100) NOT NULL,
[33517cc]1803 owner_signature VARCHAR(50) NOT NULL DEFAULT 'Not signed yet',
[06ebe74]1804 PRIMARY KEY (date, store_ID)
[6c7cfa6]1805 );
[06ebe74]1806
[33517cc]1807 CREATE TABLE IF NOT EXISTS monthly_profit (
[6c7cfa6]1808 report_date TIMESTAMP NOT NULL,
1809 store_ID VARCHAR(3) NOT NULL,
[33517cc]1810 month_and_year DATE NOT NULL,
[06ebe74]1811 profit NUMERIC NOT NULL DEFAULT 0.0,
1812 PRIMARY KEY(report_date, store_ID),
1813 FOREIGN KEY (report_date, store_ID) REFERENCES report(date, store_ID) ON DELETE CASCADE
[6c7cfa6]1814 );
[06ebe74]1815
[33517cc]1816 CREATE TABLE IF NOT EXISTS exchanges_data (
[6c7cfa6]1817 report_date TIMESTAMP NOT NULL,
1818 store_ID VARCHAR(3) NOT NULL,
[06ebe74]1819 monthly_profit NUMERIC NOT NULL DEFAULT 0.0,
[33517cc]1820 date TIMESTAMP NOT NULL,
[06ebe74]1821 sales NUMERIC NOT NULL DEFAULT 0.0,
1822 damages NUMERIC NOT NULL DEFAULT 0.0 CHECK (damages<=0),
1823 PRIMARY KEY (report_date, store_ID),
1824 FOREIGN KEY (report_date, store_ID) REFERENCES report(date, store_ID) ON DELETE CASCADE
[6c7cfa6]1825 );
[06ebe74]1826
[33517cc]1827 CREATE TABLE IF NOT EXISTS request (
[6c7cfa6]1828 request_num VARCHAR(14) PRIMARY KEY,
[33517cc]1829 date_and_time TIMESTAMP NOT NULL,
1830 problem VARCHAR(300) NOT NULL,
1831 notes_of_communication VARCHAR,
[06ebe74]1832 customer_satisfaction NUMERIC NOT NULL
[6c7cfa6]1833 );
[06ebe74]1834
[33517cc]1835 CREATE TABLE IF NOT EXISTS makes_request (
[6c7cfa6]1836 client_ID INTEGER NOT NULL REFERENCES client(client_ID) ON DELETE CASCADE,
[33517cc]1837 order_num VARCHAR(11) UNIQUE NOT NULL REFERENCES "order"(order_num) ON DELETE CASCADE,
[06ebe74]1838 PRIMARY KEY(client_ID, order_num)
[6c7cfa6]1839 );
[06ebe74]1840
[33517cc]1841 CREATE TABLE IF NOT EXISTS answers (
[6c7cfa6]1842 request_num VARCHAR(14) REFERENCES request(request_num) ON DELETE CASCADE,
[33517cc]1843 personal_id VARCHAR(10) NOT NULL REFERENCES personal(id) ON DELETE CASCADE,
1844 PRIMARY KEY(request_num, personal_id)
[6c7cfa6]1845 );
[06ebe74]1846
[33517cc]1847 CREATE TABLE IF NOT EXISTS for_store (
[6c7cfa6]1848 request_num VARCHAR(14) REFERENCES request(request_num) ON DELETE CASCADE,
[06ebe74]1849 store_ID VARCHAR(3) REFERENCES store(store_ID) ON DELETE CASCADE,
1850 PRIMARY KEY(request_num, store_ID)
[6c7cfa6]1851 );
[06ebe74]1852
[33517cc]1853 CREATE TABLE IF NOT EXISTS "change" (
[6c7cfa6]1854 date_and_time TIMESTAMP NOT NULL,
1855 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE,
[33517cc]1856 changes VARCHAR NOT NULL,
[06ebe74]1857 PRIMARY KEY (date_and_time, product_code)
[6c7cfa6]1858 );
[06ebe74]1859
[33517cc]1860 CREATE TABLE IF NOT EXISTS makes_change (
[6c7cfa6]1861 personal_id VARCHAR(10) REFERENCES personal(id) ON DELETE CASCADE,
[33517cc]1862 change_date_time TIMESTAMP,
1863 product_code VARCHAR(8),
1864 PRIMARY KEY(personal_id, change_date_time, product_code),
1865 FOREIGN KEY(change_date_time, product_code) REFERENCES "change"(date_and_time, product_code) ON DELETE CASCADE
[6c7cfa6]1866 );
[06ebe74]1867
[33517cc]1868 CREATE TABLE IF NOT EXISTS works_in_store (
[6c7cfa6]1869 personal_id VARCHAR(10) REFERENCES personal(id) ON DELETE CASCADE,
[06ebe74]1870 store_ID VARCHAR(3) REFERENCES store(store_ID) ON DELETE CASCADE,
1871 PRIMARY KEY(personal_id, store_ID)
[6c7cfa6]1872 );
[06ebe74]1873
[33517cc]1874 CREATE TABLE IF NOT EXISTS worked (
[6c7cfa6]1875 personal_id VARCHAR(10) REFERENCES personal(id) ON DELETE CASCADE,
[33517cc]1876 report_date TIMESTAMP,
[06ebe74]1877 store_ID VARCHAR(3),
1878 wage NUMERIC NOT NULL CHECK (wage>=0),
1879 pay_method VARCHAR DEFAULT 'full-time',
[33517cc]1880 total_hours NUMERIC NOT NULL,
1881 week VARCHAR(23) NOT NULL,
[06ebe74]1882 PRIMARY KEY (personal_id, report_date, store_ID),
1883 FOREIGN KEY (report_date, store_ID) REFERENCES report(date, store_ID) ON DELETE CASCADE,
1884 CONSTRAINT check_pay_method CHECK (pay_method IN ('full_time', 'part-time', 'custom'))
[6c7cfa6]1885 );
[06ebe74]1886
[33517cc]1887 CREATE TABLE IF NOT EXISTS sells (
[6c7cfa6]1888 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE,
[06ebe74]1889 store_ID VARCHAR(3) REFERENCES store(store_ID) ON DELETE CASCADE,
1890 discount NUMERIC NOT NULL DEFAULT 0.0,
1891 PRIMARY KEY (product_code, store_ID)
[6c7cfa6]1892 );
[06ebe74]1893
[33517cc]1894 CREATE TABLE IF NOT EXISTS includes (
[6c7cfa6]1895 order_num VARCHAR(11) REFERENCES "order"(order_num) ON DELETE CASCADE,
[33517cc]1896 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE,
[06ebe74]1897 quantity INTEGER NOT NULL CHECK(quantity>=0),
1898 PRIMARY KEY (order_num, product_code)
[6c7cfa6]1899 );
[06ebe74]1900
[33517cc]1901 CREATE TABLE IF NOT EXISTS approves (
[6c7cfa6]1902 boss_id VARCHAR(10) REFERENCES boss(boss_id) ON DELETE CASCADE,
[33517cc]1903 report_date TIMESTAMP,
[06ebe74]1904 store_ID VARCHAR(3),
[33517cc]1905 owner_signature VARCHAR NOT NULL,
[06ebe74]1906 PRIMARY KEY (boss_id, report_date, store_ID),
1907 FOREIGN KEY (report_date, store_ID) REFERENCES report(date, store_ID) ON DELETE CASCADE
[6c7cfa6]1908 );
[69f2a41]1909
[06ebe74]1910 -- These four small tables are application authentication/audit storage.
1911 -- They do not modify any of the project tables above.
[33517cc]1912 CREATE TABLE IF NOT EXISTS users (
[6c7cfa6]1913 id VARCHAR(50) PRIMARY KEY,
[33517cc]1914 username VARCHAR(100) UNIQUE NOT NULL,
1915 email VARCHAR(255) UNIQUE NOT NULL,
1916 password VARCHAR(255) NOT NULL,
1917 user_type VARCHAR(50) NOT NULL,
1918 force_password_change BOOLEAN DEFAULT FALSE,
1919 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
[6c7cfa6]1920 );
[06ebe74]1921
[33517cc]1922 CREATE TABLE IF NOT EXISTS roles (
[6c7cfa6]1923 role_id SERIAL PRIMARY KEY,
1924 name VARCHAR(50) UNIQUE NOT NULL,
[33517cc]1925 description TEXT
[6c7cfa6]1926 );
[06ebe74]1927
[33517cc]1928 CREATE TABLE IF NOT EXISTS user_roles (
[6c7cfa6]1929 user_id VARCHAR(50) REFERENCES users(id) ON DELETE CASCADE,
[33517cc]1930 role_id INTEGER REFERENCES roles(role_id) ON DELETE CASCADE,
1931 PRIMARY KEY(user_id, role_id)
[6c7cfa6]1932 );
[06ebe74]1933
[33517cc]1934 CREATE TABLE IF NOT EXISTS audit_log (
[6c7cfa6]1935 log_id BIGSERIAL PRIMARY KEY,
1936 user_id VARCHAR(50),
[4dff800]1937 action VARCHAR(100) NOT NULL,
1938 resource_type VARCHAR(50),
1939 resource_id VARCHAR(50),
1940 details TEXT,
1941 ip_address VARCHAR(45),
1942 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
[6c7cfa6]1943 );
[06ebe74]1944
[33517cc]1945 CREATE INDEX IF NOT EXISTS idx_product_category ON product(category_id);
[06ebe74]1946 CREATE INDEX IF NOT EXISTS idx_order_client ON "order"(client_ID);
1947 CREATE INDEX IF NOT EXISTS idx_review_order ON review(order_num);
1948 CREATE INDEX IF NOT EXISTS idx_request_date ON request(date_and_time);
[33517cc]1949 CREATE INDEX IF NOT EXISTS idx_refund_order ON refund(order_num);
1950 CREATE INDEX IF NOT EXISTS idx_personal_email ON personal(email);
1951 CREATE INDEX IF NOT EXISTS idx_client_email ON client(email);
1952 CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
1953 CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
1954 CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id);
1955 `;
[06ebe74]1956
[33517cc]1957 await pool.query(schema);
[06ebe74]1958
[6c7cfa6]1959 // ------------------------------------------------------------
1960 // Compatibility migration for older PostgreSQL databases.
1961 //
1962 // Some existing project databases contain a permissions table
1963 // created by an older version of the schema with a typo such as
1964 // personal_is instead of personal_id. CREATE TABLE IF NOT EXISTS cannot add
1965 // missing columns to an existing table, so the admin bootstrap
1966 // INSERT would otherwise fail with PostgreSQL error 42703.
1967 //
1968 // The migration is intentionally non-destructive: it keeps all
1969 // existing rows, renames the typo when possible, and migrates the old typo column into the expected schema without deleting
1970 // permission values.
1971 // ------------------------------------------------------------
1972 await pool.query(`
1973 DO $$
1974 BEGIN
1975 -- Some older versions of the database used the typo
1976 -- personal_is instead of personal_id. Rename it rather
1977 -- than adding a second column: the old column may be NOT NULL
1978 -- and would otherwise make the admin bootstrap INSERT fail.
1979 IF EXISTS (
1980 SELECT 1
1981 FROM information_schema.columns
1982 WHERE table_schema = 'public'
1983 AND table_name = 'permissions'
1984 AND column_name = 'personal_is'
1985 ) AND NOT EXISTS (
1986 SELECT 1
1987 FROM information_schema.columns
1988 WHERE table_schema = 'public'
1989 AND table_name = 'permissions'
1990 AND column_name = 'personal_id'
1991 ) THEN
1992 ALTER TABLE permissions
1993 RENAME COLUMN personal_is TO personal_id;
1994 END IF;
1995
1996 -- If neither spelling exists, add the expected column.
1997 IF NOT EXISTS (
1998 SELECT 1
1999 FROM information_schema.columns
2000 WHERE table_schema = 'public'
2001 AND table_name = 'permissions'
2002 AND column_name = 'personal_id'
2003 ) THEN
2004 ALTER TABLE permissions
2005 ADD COLUMN personal_id VARCHAR(10);
2006 END IF;
2007
2008 -- Some databases were already partially migrated and therefore
2009 -- contain BOTH personal_is and personal_id. If the old typo
2010 -- column participates in the primary key, PostgreSQL will not
2011 -- allow us to drop its NOT NULL requirement. Migrate the
2012 -- primary-key data to personal_id first, then remove the old
2013 -- typo column from the key and drop it.
2014 IF EXISTS (
2015 SELECT 1
2016 FROM information_schema.columns
2017 WHERE table_schema = 'public'
2018 AND table_name = 'permissions'
2019 AND column_name = 'personal_is'
2020 ) AND EXISTS (
2021 SELECT 1
2022 FROM information_schema.columns
2023 WHERE table_schema = 'public'
2024 AND table_name = 'permissions'
2025 AND column_name = 'personal_id'
2026 ) THEN
2027 -- Copy old primary-key values into the new column where
2028 -- the new column is currently empty.
2029 UPDATE permissions
2030 SET personal_id = personal_is
2031 WHERE personal_id IS NULL
2032 AND personal_is IS NOT NULL;
2033
2034 -- Remove the old typo column from the primary key.
2035 DO $drop_old_permission_pk$
2036 DECLARE
2037 pk_name TEXT;
2038 BEGIN
2039 SELECT tc.constraint_name
2040 INTO pk_name
2041 FROM information_schema.table_constraints tc
2042 JOIN information_schema.key_column_usage kcu
2043 ON kcu.constraint_name = tc.constraint_name
2044 AND kcu.table_schema = tc.table_schema
2045 AND kcu.table_name = tc.table_name
2046 WHERE tc.table_schema = 'public'
2047 AND tc.table_name = 'permissions'
2048 AND tc.constraint_type = 'PRIMARY KEY'
2049 AND kcu.column_name = 'personal_is'
2050 LIMIT 1;
2051
2052 IF pk_name IS NOT NULL THEN
2053 EXECUTE format(
2054 'ALTER TABLE permissions DROP CONSTRAINT %I',
2055 pk_name
2056 );
2057 END IF;
2058 END
2059 $drop_old_permission_pk$;
2060
2061 -- The application uses personal_id as the primary key.
2062 -- Drop the obsolete typo column after preserving its data.
2063 ALTER TABLE permissions
2064 DROP COLUMN personal_is;
2065
2066 -- Recreate the primary key on the correct column if one
2067 -- was removed above and no primary key currently exists.
2068 IF NOT EXISTS (
2069 SELECT 1
2070 FROM information_schema.table_constraints
2071 WHERE table_schema = 'public'
2072 AND table_name = 'permissions'
2073 AND constraint_type = 'PRIMARY KEY'
2074 ) THEN
2075 ALTER TABLE permissions
2076 ADD PRIMARY KEY (personal_id);
2077 END IF;
2078 END IF;
2079
2080 IF NOT EXISTS (
2081 SELECT 1
2082 FROM information_schema.columns
2083 WHERE table_schema = 'public'
2084 AND table_name = 'permissions'
2085 AND column_name = 'type'
2086 ) THEN
2087 ALTER TABLE permissions
2088 ADD COLUMN type VARCHAR(50);
2089 END IF;
2090
2091 IF NOT EXISTS (
2092 SELECT 1
2093 FROM information_schema.columns
2094 WHERE table_schema = 'public'
2095 AND table_name = 'permissions'
2096 AND column_name = 'authorisation'
2097 ) THEN
2098 ALTER TABLE permissions
2099 ADD COLUMN authorisation VARCHAR(50);
2100 END IF;
2101 END
2102 $$;
2103 `);
2104
2105 // ON CONFLICT(personal_id) requires a unique/exclusion constraint
2106 // that PostgreSQL can use for conflict inference. A unique index
2107 // permits multiple NULL values, so this remains safe for any legacy
2108 // permission rows that do not have a personal_id yet.
2109 await pool.query(`
2110 CREATE UNIQUE INDEX IF NOT EXISTS
2111 permissions_personal_id_unique
2112 ON permissions(personal_id)
2113 `);
2114
[33517cc]2115 const roles = [
2116 ['admin', 'System administrator'],
2117 ['store_owner', 'Store owner'],
2118 ['store_employee', 'Store employee'],
2119 ['client', 'Registered client'],
2120 ['guest', 'Unregistered guest']
[69f2a41]2121 ];
[06ebe74]2122
[33517cc]2123 for (const [name, description] of roles) {
2124 await pool.query(
2125 'INSERT INTO roles(name, description) VALUES($1,$2) ON CONFLICT(name) DO NOTHING',
2126 [name, description]
2127 );
2128 }
[69f2a41]2129
[33517cc]2130 const hash = bcrypt.hashSync('Admin123!', 10);
2131 await pool.query(
2132 `INSERT INTO users(id, username, email, password, user_type, force_password_change)
[06ebe74]2133 VALUES('000000','admin','admin@handcraft.com',$1,'admin',TRUE) ON CONFLICT(id) DO NOTHING`,
[33517cc]2134 [hash]
2135 );
2136 await pool.query(
2137 `INSERT INTO personal(id, first_name, last_name, ssn, email, password)
[06ebe74]2138 VALUES('000000','Admin','User','0000000000000','admin@handcraft.com',$1) ON CONFLICT(id) DO NOTHING`,
[33517cc]2139 [hash]
2140 );
[06ebe74]2141 await pool.query(`INSERT INTO boss(boss_id) VALUES('000000') ON CONFLICT(boss_id) DO NOTHING`);
[33517cc]2142 await pool.query(
[6c7cfa6]2143 `INSERT INTO permissions(personal_id,type,authorisation)
2144 VALUES('000000','ADMIN','full_access') ON CONFLICT(personal_id) DO NOTHING`
[33517cc]2145 );
2146 await pool.query(
2147 `INSERT INTO user_roles(user_id,role_id)
[06ebe74]2148 SELECT '000000', role_id FROM roles WHERE name='admin' ON CONFLICT DO NOTHING`
[33517cc]2149 );
[06ebe74]2150 console.log('✅ PostgreSQL project schema was recreated successfully');
[33517cc]2151 },
2152
2153 close() {
2154 return pool.end();
2155 },
2156
2157 getUserById(id, callback) {
2158 dbQuery(
2159 `SELECT u.*,
2160 COALESCE(json_agg(json_build_object('name',r.name,'description',r.description))
2161 FILTER (WHERE r.role_id IS NOT NULL), '[]') AS roles
2162 FROM users u
[6c7cfa6]2163 LEFT JOIN user_roles ur ON ur.user_id=u.id
2164 LEFT JOIN roles r ON r.role_id=ur.role_id
[33517cc]2165 WHERE u.id=$1
2166 GROUP BY u.id`,
2167 [String(id)],
2168 (err, result) => callback(err, result?.rows?.[0])
2169 );
2170 },
2171
2172 getUserByUsername(username, callback) {
2173 dbQuery(
2174 `SELECT u.*,
2175 COALESCE(json_agg(json_build_object('name',r.name,'description',r.description))
2176 FILTER (WHERE r.role_id IS NOT NULL), '[]') AS roles
2177 FROM users u
[6c7cfa6]2178 LEFT JOIN user_roles ur ON ur.user_id=u.id
2179 LEFT JOIN roles r ON r.role_id=ur.role_id
[33517cc]2180 WHERE u.username=$1 OR u.email=$1
2181 GROUP BY u.id
[6c7cfa6]2182 LIMIT 1`,
[33517cc]2183 [username],
2184 (err, result) => callback(err, result?.rows?.[0])
2185 );
2186 },
2187
2188 createUser(id, username, email, password, userType, callback) {
2189 dbQuery(
2190 `INSERT INTO users(id,username,email,password,user_type,force_password_change)
2191 VALUES($1,$2,$3,$4,$5,FALSE) RETURNING id`,
2192 [String(id), username, email, password, userType],
2193 (err, result) => {
2194 if (err) return callback(err);
2195 const roleName = userType === 'client' ? 'client' :
2196 userType === 'store_owner' ? 'store_owner' :
2197 userType === 'store_employee' ? 'store_employee' : 'guest';
2198 dbQuery(
2199 `INSERT INTO user_roles(user_id,role_id)
[06ebe74]2200 SELECT $1, role_id FROM roles WHERE name=$2`,
[33517cc]2201 [String(id), roleName],
2202 roleErr => callback(roleErr, String(id))
2203 );
[69f2a41]2204 }
[33517cc]2205 );
2206 },
2207
2208 createClient(data, callback) {
2209 dbQuery(
2210 `INSERT INTO client(first_name,last_name,email,password)
2211 VALUES($1,$2,$3,$4) RETURNING client_id`,
[06ebe74]2212 [data.firstName || data.first_name || '', data.lastName || data.last_name || '', data.email, data.password],
[33517cc]2213 (err, result) => callback(err, result?.rows?.[0]?.client_id)
2214 );
2215 },
2216
2217 getClientByEmail(email, callback) {
2218 dbQuery('SELECT * FROM client WHERE email=$1', [email],
2219 (err, result) => callback(err, result?.rows?.[0]));
2220 },
2221
2222 getClientById(id, callback) {
2223 dbQuery('SELECT * FROM client WHERE client_id=$1', [id],
2224 (err, result) => callback(err, result?.rows?.[0]));
2225 },
2226
2227 getPersonalByEmail(email, callback) {
2228 dbQuery('SELECT * FROM personal WHERE email=$1', [email],
2229 (err, result) => callback(err, result?.rows?.[0]));
2230 },
2231
2232 getPersonalById(id, callback) {
2233 dbQuery('SELECT * FROM personal WHERE id=$1', [String(id)],
2234 (err, result) => callback(err, result?.rows?.[0]));
2235 },
2236
2237 verifyPassword(password, hash) {
2238 try { return bcrypt.compareSync(password, hash); } catch { return false; }
2239 },
2240
2241 verifyClientPassword(password, hash, callback) {
[06ebe74]2242 bcrypt.compare(password, hash, callback);
[33517cc]2243 },
2244
2245 logAudit(userId, action, resourceType, resourceId, details, ipAddress) {
2246 dbQuery(
2247 `INSERT INTO audit_log(user_id,action,resource_type,resource_id,details,ip_address)
2248 VALUES($1,$2,$3,$4,$5,$6)`,
[06ebe74]2249 [userId == null ? null : String(userId), action, resourceType,
2250 resourceId == null ? null : String(resourceId), details, ipAddress],
[33517cc]2251 () => {}
2252 );
2253 },
2254
2255 getProducts(categoryId, searchTerm, callback) {
2256 const params = [];
2257 const where = [];
[06ebe74]2258 if (categoryId) {
2259 params.push(categoryId);
2260 where.push(`p.category_id=$${params.length}`);
2261 }
[33517cc]2262 if (searchTerm) {
2263 params.push(`%${searchTerm}%`);
2264 where.push(`(p.description ILIKE $${params.length} OR p.code ILIKE $${params.length})`);
[69f2a41]2265 }
[06ebe74]2266 const sql = `SELECT p.*, c.name AS category_name, LEFT(p.code,3) AS store_id
2267 FROM product p
[6c7cfa6]2268 LEFT JOIN category c ON c.id=p.category_id
2269 ${where.length ? 'WHERE ' + where.join(' AND ') : ''}
[33517cc]2270 ORDER BY p.code`;
2271 dbQuery(sql, params, (err, result) => callback(err, result?.rows || []));
2272 },
2273
2274 getProductById(id, callback) {
2275 dbQuery(
[06ebe74]2276 `SELECT p.*, c.name AS category_name, LEFT(p.code,3) AS store_id
2277 FROM product p
[6c7cfa6]2278 LEFT JOIN category c ON c.id=p.category_id
[06ebe74]2279 WHERE p.code=$1 LIMIT 1`,
[33517cc]2280 [String(id)],
2281 (err,result)=>callback(err,result?.rows?.[0])
2282 );
2283 },
2284
2285 getProductByCode(code, callback) {
2286 dbQuery(
[06ebe74]2287 `SELECT p.*, c.name AS category_name, LEFT(p.code,3) AS store_id
2288 FROM product p
[6c7cfa6]2289 LEFT JOIN category c ON c.id=p.category_id
[06ebe74]2290 WHERE p.code=$1`,
[33517cc]2291 [code],
2292 (err,result)=>callback(err,result?.rows?.[0])
2293 );
2294 },
2295
2296 addProduct(personalId, data, callback) {
[06ebe74]2297 const storeId = data.store_id || data.storeId || String(data.code || '').slice(0,3);
2298 if (!data.category_id) {
2299 return callback(new Error('category_id is required because product.category_id is NOT NULL'));
2300 }
[33517cc]2301 dbQuery(
2302 `INSERT INTO product(code,price,availability,weight,width_x_length_x_depth,
[06ebe74]2303 aprox_production_time,description,category_id)
2304 VALUES($1,$2,$3,$4,$5,$6,$7,$8) RETURNING code`,
[33517cc]2305 [
2306 data.code, data.price, data.availability ?? 0, data.weight,
2307 data.width_x_length_x_depth || data.dimensions || '',
[06ebe74]2308 data.aprox_production_time ?? data.production_time ?? 0,
2309 data.description, data.category_id
[33517cc]2310 ],
2311 (err,result)=>{
2312 if (err) return callback(err);
2313 dbQuery(
[06ebe74]2314 `INSERT INTO sells(product_code,store_ID,discount)
2315 VALUES($1,$2,$3)
[6c7cfa6]2316 ON CONFLICT(product_code,store_ID)
[06ebe74]2317 DO UPDATE SET discount=EXCLUDED.discount`,
[33517cc]2318 [data.code,storeId,data.discount || 0],
2319 e => callback(e, data.code)
2320 );
2321 }
2322 );
2323 },
2324
2325 updateProduct(personalId, data, callback) {
2326 const fields = [];
2327 const params = [];
2328 const allowed = [
[06ebe74]2329 ['price','price'], ['availability','availability'], ['weight','weight'],
[33517cc]2330 ['width_x_length_x_depth','width_x_length_x_depth'],
2331 ['dimensions','width_x_length_x_depth'],
2332 ['aprox_production_time','aprox_production_time'],
2333 ['production_time','aprox_production_time'],
[06ebe74]2334 ['description','description'], ['category_id','category_id']
[69f2a41]2335 ];
[33517cc]2336 for (const [input,col] of allowed) {
2337 if (data[input] !== undefined) {
2338 params.push(data[input]);
2339 fields.push(`${col}=$${params.length}`);
[69f2a41]2340 }
2341 }
[33517cc]2342 if (!fields.length) return callback(null,0);
2343 params.push(data.code);
2344 dbQuery(`UPDATE product SET ${fields.join(', ')} WHERE code=$${params.length}`, params,
2345 (err,result)=>callback(err,result?.rowCount || 0));
2346 },
2347
2348 deleteProduct(productCode, storeId, personalId, callback) {
[06ebe74]2349 dbQuery('DELETE FROM product WHERE code=$1 AND LEFT(code,3)=$2', [productCode,storeId],
[33517cc]2350 (err)=>callback(err));
2351 },
2352
2353 createCategory(data, callback) {
[06ebe74]2354 const parent = data.parent_category_id ?? data.parentCategoryId ?? data.parent_id;
2355 if (parent === undefined || parent === null || parent === '') {
2356 return callback(new Error('parent_category_id is required by the project schema'));
2357 }
[33517cc]2358 dbQuery(
2359 `INSERT INTO category(name,parent_category_id) VALUES($1,$2) RETURNING id,name,parent_category_id`,
[06ebe74]2360 [data.name, parent],
[33517cc]2361 (err,result)=>callback(err,result?.rows?.[0])
2362 );
2363 },
2364
2365 getCategories(callback) {
2366 dbQuery('SELECT * FROM category ORDER BY name', [], (err,result)=>callback(err,result?.rows||[]));
2367 },
2368
2369 getCategoriesWithParents(callback) {
2370 dbQuery(
2371 `SELECT c.*,p.name AS parent_name
2372 FROM category c LEFT JOIN category p ON p.id=c.parent_category_id
2373 ORDER BY c.name`,
2374 [], (err,result)=>callback(err,result?.rows||[])
2375 );
2376 },
2377
2378 getStores(callback) {
2379 dbQuery('SELECT * FROM store ORDER BY name', [], (err,result)=>callback(err,result?.rows||[]));
2380 },
2381
2382 createOrderNew(data, callback) {
2383 const items = data.items || data.products || data.order_items || [];
2384 const storeId = data.store_id || data.storeId || (items[0]?.code ? String(items[0].code).slice(0,3) : null);
[06ebe74]2385 if (!storeId) return callback(new Error('Store ID is required'));
2386 const year = String(new Date().getFullYear()).slice(-3);
2387
2388 dbQuery(
2389 `SELECT COUNT(*)::int AS n
2390 FROM "order"
2391 WHERE LEFT(order_num,3)=$1
2392 AND SUBSTRING(order_num FROM 4 FOR 3)=$2`,
2393 [storeId, year],
2394 (countErr,countResult)=>{
2395 if (countErr) return callback(countErr);
2396 const seq=Number(countResult.rows[0].n)+1;
2397 const orderNum=`${storeId}${year}${String(seq).padStart(5,'0')}`;
2398 dbQuery(
2399 `INSERT INTO "order"(order_num,client_ID,status,last_date_mod,payment_method,discount)
2400 VALUES($1,$2,$3,CURRENT_TIMESTAMP,$4,$5) RETURNING order_num`,
2401 [orderNum,data.client_id,data.status||'placed order',data.payment_method||'cash',data.discount||0],
2402 (err,result)=>{
2403 if(err) return callback(err);
2404 let pending=items.length;
2405 if(!pending) return callback(null,orderNum);
2406 let firstErr=null;
2407 for(const item of items){
2408 dbQuery(
2409 `INSERT INTO includes(order_num,product_code,quantity) VALUES($1,$2,$3)`,
2410 [orderNum,item.product_code||item.code,item.quantity||1],
2411 e=>{ if(e && !firstErr) firstErr=e; if(--pending===0) callback(firstErr,firstErr?undefined:orderNum); }
2412 );
[33517cc]2413 }
[06ebe74]2414 }
2415 );
2416 }
2417 );
[33517cc]2418 },
2419
2420 getOrdersByClient(clientId, callback) {
2421 dbQuery(
[06ebe74]2422 `SELECT o.*, LEFT(o.order_num,3) AS store_id,
[6c7cfa6]2423 o.last_date_mod AS order_date,
2424 COALESCE(json_agg(json_build_object('product_code',i.product_code,'quantity',i.quantity,'price',p.price))
2425 FILTER(WHERE i.product_code IS NOT NULL),'[]') AS items
[33517cc]2426 FROM "order" o
[6c7cfa6]2427 LEFT JOIN includes i ON i.order_num=o.order_num
2428 LEFT JOIN product p ON p.code=i.product_code
[06ebe74]2429 WHERE o.client_ID=$1
2430 GROUP BY o.order_num
2431 ORDER BY o.last_date_mod DESC`,
[33517cc]2432 [clientId],(err,result)=>callback(err,result?.rows||[])
2433 );
2434 },
2435
2436 createReviewNew(data, callback) {
2437 dbQuery(
[06ebe74]2438 `INSERT INTO review(order_num,comment,rating,last_mod_date)
2439 VALUES($1,$2,$3,CURRENT_TIMESTAMP)
[6c7cfa6]2440 RETURNING order_num`,
[06ebe74]2441 [data.order_num,data.comment||null,data.rating],
2442 (err,result)=>callback(err,result?.rows?.[0]?.order_num)
[33517cc]2443 );
2444 },
2445
2446 createRequest(data, callback) {
2447 dbQuery(
[06ebe74]2448 `INSERT INTO request(request_num,date_and_time,problem,notes_of_communication,customer_satisfaction)
2449 VALUES($1,$2,$3,$4,0) RETURNING request_num`,
2450 [data.request_num,data.date_and_time,data.problem,data.notes_of_communication||null],
2451 (err,result)=>{
2452 if (err) return callback(err);
2453 dbQuery(
2454 `INSERT INTO for_store(request_num,store_ID) VALUES($1,$2)`,
2455 [data.request_num,data.store_id],
2456 storeErr=>{
2457 if (storeErr) return callback(storeErr);
2458 if (data.order_num) {
2459 dbQuery(
2460 `INSERT INTO makes_request(client_ID,order_num) VALUES($1,$2)`,
2461 [data.client_id,data.order_num],
2462 e=>callback(e,data.request_num)
2463 );
2464 } else {
2465 callback(null,data.request_num);
2466 }
2467 }
2468 );
2469 }
[33517cc]2470 );
2471 },
2472
2473 createRefund(data, callback) {
[06ebe74]2474 const suppliedId = data.refund_id;
2475 const query = suppliedId
2476 ? `INSERT INTO refund(refund_id,order_num,reason,amount,status) VALUES($1,$2,$3,$4,$5) RETURNING refund_id`
2477 : `INSERT INTO refund(order_num,reason,amount,status) VALUES($1,$2,$3,$4) RETURNING refund_id`;
2478 const params = suppliedId
2479 ? [Number.parseInt(String(suppliedId),10) || undefined,data.order_num,data.reason||null,data.amount,data.status||'requested refund']
2480 : [data.order_num,data.reason||null,data.amount,data.status||'requested refund'];
2481 dbQuery(query,params,(err,result)=>callback(err,result?.rows?.[0]?.refund_id));
[33517cc]2482 },
2483
2484 getAllUsers(callback) {
2485 dbQuery(`SELECT u.*,COALESCE(json_agg(r.name) FILTER(WHERE r.role_id IS NOT NULL),'[]') roles
2486 FROM users u LEFT JOIN user_roles ur ON ur.user_id=u.id
2487 LEFT JOIN roles r ON r.role_id=ur.role_id GROUP BY u.id ORDER BY u.created_at DESC`,
2488 [],(err,result)=>callback(err,result?.rows||[]));
2489 },
2490
2491 getAllOrders(callback) {
[06ebe74]2492 dbQuery(`SELECT o.*, LEFT(o.order_num,3) AS store_id, o.last_date_mod AS order_date,
[6c7cfa6]2493 c.first_name,c.last_name,c.email
[06ebe74]2494 FROM "order" o
[6c7cfa6]2495 LEFT JOIN client c ON c.client_id=o.client_ID
[06ebe74]2496 ORDER BY o.last_date_mod DESC`,
[33517cc]2497 [],(err,result)=>callback(err,result?.rows||[]));
2498 },
2499
2500 getStoreProducts(storeId, callback) {
[06ebe74]2501 dbQuery(`SELECT p.*,c.name AS category_name,LEFT(p.code,3) AS store_id,s.discount
2502 FROM product p
[6c7cfa6]2503 LEFT JOIN category c ON c.id=p.category_id
2504 LEFT JOIN sells s ON s.product_code=p.code AND s.store_ID=$1
[06ebe74]2505 WHERE LEFT(p.code,3)=$1 OR EXISTS(SELECT 1 FROM sells sx WHERE sx.product_code=p.code AND sx.store_ID=$1)
[33517cc]2506 ORDER BY p.code`,[storeId],(err,result)=>callback(err,result?.rows||[]));
2507 },
2508
2509 getStoreOrders(storeId, callback) {
[06ebe74]2510 dbQuery(`SELECT o.*,LEFT(o.order_num,3) AS store_id,o.last_date_mod AS order_date,c.first_name,c.last_name
2511 FROM "order" o
[6c7cfa6]2512 LEFT JOIN client c ON c.client_id=o.client_ID
[06ebe74]2513 WHERE LEFT(o.order_num,3)=$1 ORDER BY o.last_date_mod DESC`,[storeId],
[33517cc]2514 (err,result)=>callback(err,result?.rows||[]));
2515 },
2516
2517 getStoreEmployees(storeId, callback) {
2518 dbQuery(`SELECT p.*,e.date_of_hire,per.type,per.authorisation
2519 FROM personal p JOIN works_in_store w ON w.personal_id=p.id
[6c7cfa6]2520 LEFT JOIN employees e ON e.employee_id=p.id
2521 LEFT JOIN permissions per ON per.personal_id=p.id
[06ebe74]2522 WHERE w.store_ID=$1 ORDER BY p.last_name,p.first_name`,
[33517cc]2523 [storeId],(err,result)=>callback(err,result?.rows||[]));
2524 },
2525
2526 getStoreReports(storeId, callback) {
[6c7cfa6]2527 dbQuery(
2528 `SELECT date, store_id, overall_profit, sales_trend, marketing_growth, owner_signature
2529 FROM report
2530 WHERE store_id = $1
2531 ORDER BY date DESC`,
2532 [storeId],
2533 (err, result) => callback(err, result?.rows || [])
2534 );
[33517cc]2535 },
2536
2537 getStoreStats(storeId, callback) {
[6c7cfa6]2538 const sql = `
2539 SELECT
2540 (SELECT COUNT(DISTINCT product_code)
2541 FROM sells
2542 WHERE store_ID = $1)::int AS product_count,
2543
2544 (SELECT COUNT(DISTINCT o.order_num)
2545 FROM sells se
2546 JOIN includes i ON i.product_code = se.product_code
2547 JOIN "order" o ON o.order_num = i.order_num
2548 WHERE se.store_ID = $1)::int AS order_count,
2549
2550 (SELECT COALESCE(SUM(
2551 i.quantity * p.price
2552 * (1 - COALESCE(o.discount, 0) / 100.0)
2553 ), 0)
2554 FROM sells se
2555 JOIN includes i ON i.product_code = se.product_code
2556 JOIN "order" o ON o.order_num = i.order_num
2557 JOIN product p ON p.code = i.product_code
2558 WHERE se.store_ID = $1) AS revenue,
2559
2560 (SELECT COUNT(*)
2561 FROM works_in_store
2562 WHERE store_ID = $1)::int AS employee_count,
2563
2564 (SELECT COUNT(*)
2565 FROM for_store
2566 WHERE store_ID = $1)::int AS request_count,
2567
2568 (SELECT COUNT(*)
2569 FROM refund r
2570 JOIN "order" o ON o.order_num = r.order_num
2571 WHERE LEFT(o.order_num, 3) = $1)::int AS refund_count
2572 `;
2573
2574 dbQuery(sql, [storeId], (err, result) => {
2575 callback(err, result?.rows?.[0] || {});
2576 });
2577 },
2578
2579 generateStoreReport(storeId, startDate, endDate, type, period, ownerSignature, callback) {
2580 dbQuery(
2581 `WITH sales AS (
2582 SELECT COALESCE(SUM(
2583 p.price * i.quantity
2584 * (1 - COALESCE(o.discount, 0) / 100.0)
2585 ), 0) AS revenue
2586 FROM sells se
2587 JOIN product p ON p.code = se.product_code
2588 JOIN includes i ON i.product_code = se.product_code
2589 JOIN "order" o ON o.order_num = i.order_num
2590 WHERE se.store_ID = $1
2591 AND o.last_date_mod >= $2::timestamp
2592 AND o.last_date_mod < ($3::timestamp + INTERVAL '1 day')
2593 ),
2594 refunds AS (
2595 SELECT COALESCE(SUM(rf.amount), 0) AS refund_total
2596 FROM refund rf
2597 JOIN "order" o ON o.order_num = rf.order_num
2598 WHERE LEFT(o.order_num, 3) = $1
2599 AND o.last_date_mod >= $2::timestamp
2600 AND o.last_date_mod < ($3::timestamp + INTERVAL '1 day')
2601 AND rf.status IN ('approved', 'processed')
2602 )
2603 SELECT sales.revenue, refunds.refund_total,
2604 sales.revenue - refunds.refund_total AS net_profit
2605 FROM sales CROSS JOIN refunds`,
2606 [storeId, startDate, endDate],
2607 (err, result) => {
2608 if (err) return callback(err);
2609
2610 const row = result.rows[0] || {};
2611 const revenue = Number(row.revenue || 0);
2612 const refundTotal = Number(row.refund_total || 0);
2613 const netProfit = Number(row.net_profit || 0);
2614
2615 dbQuery(
2616 `SELECT COALESCE(SUM(
2617 p.price * i.quantity
2618 * (1 - COALESCE(o.discount, 0) / 100.0)
2619 ), 0) AS previous_revenue
2620 FROM sells se
2621 JOIN product p ON p.code = se.product_code
2622 JOIN includes i ON i.product_code = se.product_code
2623 JOIN "order" o ON o.order_num = i.order_num
2624 WHERE se.store_ID = $1
2625 AND o.last_date_mod >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month'
2626 AND o.last_date_mod < DATE_TRUNC('month', CURRENT_DATE)`,
2627 [storeId],
2628 (previousErr, previousResult) => {
2629 if (previousErr) return callback(previousErr);
2630
2631 const previousRevenue = Number(previousResult.rows[0]?.previous_revenue || 0);
2632 const growth = previousRevenue === 0
2633 ? (revenue > 0 ? 100 : 0)
2634 : ((revenue - previousRevenue) / previousRevenue) * 100;
2635
2636 dbQuery(
2637 `INSERT INTO report
2638 (date, store_ID, overall_profit, sales_trend, marketing_growth, owner_signature)
2639 VALUES
2640 (CURRENT_TIMESTAMP, $1, $2, $3, $4, $5)
2641 RETURNING date, store_ID, overall_profit, sales_trend, marketing_growth, owner_signature`,
2642 [
2643 storeId,
2644 Math.max(0, netProfit),
2645 `Revenue ${revenue.toFixed(2)}; Refunds ${refundTotal.toFixed(2)}`.slice(0, 100),
2646 `${growth.toFixed(2)}%`,
2647 ownerSignature || 'Not signed yet'
2648 ],
2649 (insertErr, insertResult) => {
2650 if (insertErr) return callback(insertErr);
2651
2652 const report = insertResult.rows[0];
2653
2654 dbQuery(
2655 `INSERT INTO monthly_profit
2656 (report_date, store_ID, month_and_year, profit)
2657 VALUES
2658 ($1, $2, DATE_TRUNC('month', $3::timestamp)::DATE, $4)
2659 ON CONFLICT (report_date, store_ID)
2660 DO UPDATE SET
2661 month_and_year = EXCLUDED.month_and_year,
2662 profit = EXCLUDED.profit`,
2663 [report.date, storeId, endDate, Math.max(0, netProfit)],
2664 (monthlyErr) => {
2665 if (monthlyErr) console.error('Warning inserting monthly profit:', monthlyErr);
2666
2667 dbQuery(
2668 `INSERT INTO exchanges_data
2669 (report_date, store_ID, monthly_profit, date, sales, damages)
2670 VALUES ($1, $2, $3, CURRENT_TIMESTAMP, $4, $5)
2671 ON CONFLICT (report_date, store_ID)
2672 DO UPDATE SET
2673 monthly_profit = EXCLUDED.monthly_profit,
2674 date = EXCLUDED.date,
2675 sales = EXCLUDED.sales,
2676 damages = EXCLUDED.damages`,
2677 [report.date, storeId, Math.max(0, netProfit), revenue, -refundTotal],
2678 (exchangeErr) => {
2679 if (exchangeErr) console.error('Warning inserting exchange data:', exchangeErr);
2680 callback(null, report);
2681 }
2682 );
2683 }
2684 );
2685 }
2686 );
2687 }
2688 );
2689 }
2690 );
[33517cc]2691 },
2692
2693 getEmployeeTasks(personalId, storeId, callback) {
2694 dbQuery(`SELECT r.*,a.personal_id AS answered_by
2695 FROM request r
[6c7cfa6]2696 JOIN for_store fs ON fs.request_num=r.request_num
2697 LEFT JOIN answers a ON a.request_num=r.request_num
[06ebe74]2698 WHERE fs.store_ID=$1 AND (a.personal_id=$2 OR a.personal_id IS NULL)
[33517cc]2699 ORDER BY r.date_and_time DESC`,
2700 [storeId,personalId],(err,result)=>callback(err,result?.rows||[]));
2701 },
2702
2703 getClientStats(clientId, callback) {
2704 dbQuery(`SELECT
[6c7cfa6]2705 (SELECT COUNT(*) FROM "order" WHERE client_ID=$1)::int AS order_count,
2706 (SELECT COUNT(*) FROM review r JOIN "order" o ON o.order_num=r.order_num WHERE o.client_ID=$1)::int AS review_count,
2707 (SELECT COUNT(*) FROM makes_request WHERE client_ID=$1)::int AS request_count,
2708 (SELECT COUNT(*) FROM refund r JOIN "order" o ON o.order_num=r.order_num WHERE o.client_ID=$1)::int AS refund_count`,
[33517cc]2709 [clientId],(err,result)=>callback(err,result?.rows?.[0]||{}));
2710 }
2711};
[4dff800]2712
2713
2714
[33517cc]2715// PostgreSQL schema initialization.
2716// The schema is based on the supplied Pasted markdown.md. A few invalid PostgreSQL
2717// declarations in the original paste are corrected here (for example DECIMMAL,
2718// PARTIAL KEY, and composite-key column-name typos). The existing HTTP layer also
[06ebe74]2719// uses only the project schema plus the four authentication/audit support tables.
[33517cc]2720(async () => {
[69f2a41]2721 try {
[33517cc]2722 await database.initializeDatabase();
[6c7cfa6]2723 await database.installReportFunctions();
[2d1ec46]2724 await database.installTriggersAndViews();
[69f2a41]2725 console.log('✅ Database initialization completed');
2726 } catch (err) {
2727 console.error('❌ Database initialization failed:', err);
[33517cc]2728 process.exitCode = 1;
[69f2a41]2729 }
2730})();
2731
2732const server = http.createServer((req, res) => {
2733 const parsedUrl = url.parse(req.url, true);
2734 const pathname = parsedUrl.pathname;
2735 const ipAddress = getClientIp(req);
2736
2737 console.log('Request:', req.method, pathname);
2738
2739 res.setHeader('Access-Control-Allow-Origin', '*');
2740 res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
2741 res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
2742
2743 if (req.method === 'OPTIONS') {
2744 res.writeHead(200);
2745 res.end();
2746 return;
2747 }
2748
2749 if (pathname === '/' || pathname === '/index.html') {
2750 serveStaticFile(res, 'index.html', 'text/html');
2751 } else if (pathname === '/login.html') {
2752 serveStaticFile(res, 'login.html', 'text/html');
2753 } else if (pathname === '/register.html') {
2754 serveStaticFile(res, 'register.html', 'text/html');
2755 } else if (pathname === '/register-store.html') {
2756 serveStaticFile(res, 'register-store.html', 'text/html');
2757 } else if (pathname === '/dashboard.html') {
2758 const cookies = parseCookies(req);
2759 const sessionId = cookies.sessionId;
2760
[33517cc]2761 if (!sessionId || !sessions.has(sessionId)) {
[69f2a41]2762 res.writeHead(302, { 'Location': '/login.html' });
2763 res.end();
2764 return;
2765 }
2766
2767 if (tempAdminSessions.has(sessionId)) {
2768 res.writeHead(302, { 'Location': '/change-password.html?forced=true' });
2769 res.end();
2770 return;
2771 }
2772
2773 serveStaticFile(res, 'dashboard.html', 'text/html');
2774 } else if (pathname === '/verify-email.html') {
2775 serveStaticFile(res, 'verify-email.html', 'text/html');
2776 } else if (pathname === '/verify-2fa.html') {
2777 serveStaticFile(res, 'verify-2fa.html', 'text/html');
2778 } else if (pathname === '/admin.html') {
[4dff800]2779 // Check if user is authenticated
2780 const cookies = parseCookies(req);
2781 const sessionId = cookies.sessionId;
2782
[33517cc]2783 if (!sessionId || !sessions.has(sessionId)) {
[4dff800]2784 res.writeHead(302, { 'Location': '/login.html' });
2785 res.end();
2786 return;
2787 }
2788
2789 // Get user from session
2790 const userId = sessions.get(sessionId);
2791
2792 // Check if this is the admin user
2793 if (userId !== '000000') {
2794 // Not admin, redirect to appropriate dashboard
2795 if (userId.startsWith('client_')) {
2796 res.writeHead(302, { 'Location': '/client-dashboard.html' });
2797 } else if (userId.startsWith('personal_')) {
2798 // Check if store owner or employee
2799 const personalId = userId.replace('personal_', '');
[79fff4f]2800
[4dff800]2801 database.database.get(
[33517cc]2802 'SELECT boss_id FROM boss WHERE boss_id = $1',
[4dff800]2803 [personalId],
2804 (err, boss) => {
2805 if (boss) {
2806 res.writeHead(302, { 'Location': '/store-owner.html' });
2807 } else {
2808 res.writeHead(302, { 'Location': '/store-employee.html' });
2809 }
2810 res.end();
2811 }
2812 );
2813 return;
2814 } else {
2815 res.writeHead(302, { 'Location': '/dashboard.html' });
2816 }
2817 res.end();
2818 return;
2819 }
2820
[69f2a41]2821 serveStaticFile(res, 'admin.html', 'text/html');
2822 } else if (pathname === '/store-owner.html') {
[33517cc]2823 serveStaticFile(res, 'store-owner.html', 'text/html');
[69f2a41]2824 } else if (pathname === '/store-employee.html') {
[33517cc]2825 serveStaticFile(res, 'store-employee.html', 'text/html');
[69f2a41]2826 } else if (pathname === '/client-dashboard.html') {
[33517cc]2827 serveStaticFile(res, 'client-dashboard.html', 'text/html');
[69f2a41]2828 } else if (pathname === '/products.html') {
2829 serveStaticFile(res, 'products.html', 'text/html');
2830 } else if (pathname === '/product-detail.html') {
2831 serveStaticFile(res, 'product-detail.html', 'text/html');
2832 } else if (pathname === '/checkout.html') {
2833 serveStaticFile(res, 'checkout.html', 'text/html');
2834 } else if (pathname === '/orders.html') {
2835 serveStaticFile(res, 'orders.html', 'text/html');
2836 } else if (pathname === '/reviews.html') {
2837 serveStaticFile(res, 'reviews.html', 'text/html');
2838 } else if (pathname === '/change-password.html') {
2839 serveStaticFile(res, 'change-password.html', 'text/html');
2840 } else if (pathname === '/style.css') {
2841 serveStaticFile(res, 'style.css', 'text/css');
2842 } else if (pathname === '/script.js') {
2843 serveStaticFile(res, 'script.js', 'application/javascript');
2844 }
2845
2846 else if (pathname === '/api/register' && req.method === 'POST') {
2847 let body = '';
2848 req.on('data', chunk => {
2849 body += chunk.toString();
2850 });
2851 req.on('end', () => {
2852 const { username, email, password, userType, firstName, lastName } = JSON.parse(body);
2853
2854 if (!username || !email || !password || !userType) {
2855 res.writeHead(400, { 'Content-Type': 'application/json' });
2856 res.end(JSON.stringify({ success: false, message: 'All fields are required' }));
2857 return;
2858 }
2859
2860 if (!validateEmail(email)) {
2861 res.writeHead(400, { 'Content-Type': 'application/json' });
2862 res.end(JSON.stringify({ success: false, message: 'Email is not valid' }));
2863 return;
2864 }
2865
2866 if (!validatePassword(password)) {
2867 res.writeHead(400, { 'Content-Type': 'application/json' });
2868 res.end(JSON.stringify({
2869 success: false,
2870 message: 'Password must have at least 8 characters, including uppercase, lowercase, number and special character'
2871 }));
2872 return;
2873 }
2874
2875 database.getUserByUsername(username, (err, existingUser) => {
2876 if (err) {
2877 console.error('Error checking user:', err);
2878 res.writeHead(500, { 'Content-Type': 'application/json' });
2879 res.end(JSON.stringify({ success: false, message: 'Server error checking user' }));
2880 return;
2881 }
2882
2883 database.getClientByEmail(email, (err, existingClient) => {
2884 if (err) {
2885 console.error('Error checking client:', err);
2886 }
2887
2888 if (existingUser || existingClient) {
2889 res.writeHead(400, { 'Content-Type': 'application/json' });
2890 res.end(JSON.stringify({ success: false, message: 'Username or email is already in use' }));
2891 return;
2892 }
2893
2894 const verificationCode = generateVerificationCode();
2895
2896 const tempUserData = {
2897 username,
2898 email,
2899 password,
2900 timestamp: Date.now(),
2901 userType: userType,
2902 firstName: firstName || '',
2903 lastName: lastName || ''
2904 };
2905
2906 tempUsers.set(verificationCode, tempUserData);
2907 verificationCodes.set(email, { code: verificationCode, timestamp: Date.now() });
2908
2909 console.log(`⏰ Generated verification code for ${email}, expires in 30 seconds`);
2910
2911 sendVerificationEmail(email, verificationCode)
2912 .then(() => {
2913 console.log('✅ Verification email sent to:', email);
2914 database.logAudit(null, 'REGISTER_ATTEMPT', 'user', null, `Registration attempt for ${email} as ${userType}`, ipAddress);
2915 res.writeHead(200, { 'Content-Type': 'application/json' });
2916 res.end(JSON.stringify({
2917 success: true,
2918 message: 'Verification code sent to your email (expires in 30 seconds)',
2919 email: email
2920 }));
2921 })
2922 .catch(error => {
2923 console.error('Error sending email:', error.message);
2924 res.writeHead(200, { 'Content-Type': 'application/json' });
2925 res.end(JSON.stringify({
2926 success: true,
2927 message: 'Verification code generated (check console, expires in 30 seconds)',
2928 email: email,
2929 developmentCode: verificationCode
2930 }));
2931 });
2932 });
2933 });
2934 });
2935 }
2936
2937 else if (pathname === '/api/register-store' && req.method === 'POST') {
2938 let body = '';
2939 req.on('data', chunk => {
2940 body += chunk.toString();
2941 });
2942 req.on('end', () => {
2943 const formData = JSON.parse(body);
2944
2945 const requiredFields = [
2946 'ownerFirstName', 'ownerLastName', 'ownerSSN', 'ownerEmail',
2947 'storeName', 'storeAddress', 'storeEmail', 'storeFoundingDate',
2948 'password', 'confirmPassword', 'signature'
2949 ];
2950
2951 for (const field of requiredFields) {
2952 if (!formData[field]) {
2953 res.writeHead(400, { 'Content-Type': 'application/json' });
2954 res.end(JSON.stringify({
2955 success: false,
2956 message: `Field ${field} is required`
2957 }));
2958 return;
2959 }
2960 }
2961
2962 if (!/^\d{13}$/.test(formData.ownerSSN)) {
2963 res.writeHead(400, { 'Content-Type': 'application/json' });
2964 res.end(JSON.stringify({
2965 success: false,
2966 message: 'SSN must be exactly 13 digits'
2967 }));
2968 return;
2969 }
2970
2971 const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
[79fff4f]2972
[69f2a41]2973 if (!emailRegex.test(formData.ownerEmail)) {
2974 res.writeHead(400, { 'Content-Type': 'application/json' });
2975 res.end(JSON.stringify({
2976 success: false,
2977 message: 'Please enter a valid personal email address'
2978 }));
2979 return;
2980 }
2981
2982 if (!emailRegex.test(formData.storeEmail)) {
2983 res.writeHead(400, { 'Content-Type': 'application/json' });
2984 res.end(JSON.stringify({
2985 success: false,
2986 message: 'Please enter a valid store email address'
2987 }));
2988 return;
2989 }
2990
2991 if (formData.password !== formData.confirmPassword) {
2992 res.writeHead(400, { 'Content-Type': 'application/json' });
2993 res.end(JSON.stringify({
2994 success: false,
2995 message: 'Passwords do not match'
2996 }));
2997 return;
2998 }
2999
3000 if (!validatePassword(formData.password)) {
3001 res.writeHead(400, { 'Content-Type': 'application/json' });
3002 res.end(JSON.stringify({
3003 success: false,
3004 message: 'Password must have at least 8 characters, including uppercase, lowercase, number and special character'
3005 }));
3006 return;
3007 }
3008
3009 database.getPersonalByEmail(formData.ownerEmail, (err, existingPersonal) => {
3010 if (err) {
3011 console.error('Error checking personal:', err);
3012 res.writeHead(500, { 'Content-Type': 'application/json' });
3013 res.end(JSON.stringify({ success: false, message: 'Server error checking personal' }));
3014 return;
3015 }
3016
3017 if (existingPersonal) {
3018 res.writeHead(400, { 'Content-Type': 'application/json' });
3019 res.end(JSON.stringify({ success: false, message: 'Personal email is already registered' }));
3020 return;
3021 }
3022
3023 database.database.get(
[33517cc]3024 'SELECT store_id FROM store WHERE store_email = $1',
[69f2a41]3025 [formData.storeEmail],
3026 (err, existingStore) => {
3027 if (err) {
3028 console.error('Error checking store:', err);
3029 res.writeHead(500, { 'Content-Type': 'application/json' });
3030 res.end(JSON.stringify({ success: false, message: 'Server error checking store' }));
3031 return;
3032 }
3033
3034 if (existingStore) {
3035 res.writeHead(400, { 'Content-Type': 'application/json' });
3036 res.end(JSON.stringify({ success: false, message: 'Store email is already registered' }));
3037 return;
3038 }
3039
3040 // Get the maximum store_id to determine the next store ID
3041 database.database.get(
3042 'SELECT MAX(store_id) as max_store_num FROM store',
3043 [],
3044 (err, result) => {
3045 if (err) {
3046 console.error('Error getting max store ID:', err);
3047 res.writeHead(500, { 'Content-Type': 'application/json' });
3048 res.end(JSON.stringify({ success: false, message: 'Server error generating store ID' }));
3049 return;
3050 }
3051
3052 // Next store number is max + 1, starting from 1 if no stores exist
3053 let nextStoreNumber = 1;
[79fff4f]3054
[69f2a41]3055 if (result && result.max_store_num) {
3056 // Extract numeric part from store_id (format: XXX)
3057 const maxNum = parseInt(result.max_store_num, 10);
3058 if (!isNaN(maxNum)) {
3059 nextStoreNumber = maxNum + 1;
3060 }
3061 }
3062
3063 if (nextStoreNumber > 999) {
3064 res.writeHead(400, { 'Content-Type': 'application/json' });
3065 res.end(JSON.stringify({ success: false, message: 'Maximum store limit reached (999)' }));
3066 return;
3067 }
3068
3069 // Store ID is padded to 3 digits (VARCHAR)
3070 const storeIdPadded = nextStoreNumber.toString().padStart(3, '0');
3071
3072 // Personal ID is storeId + '001' (as string for display)
3073 const personalId = storeIdPadded + '001';
3074
3075 const verificationCode = generateVerificationCode();
3076
3077 const tempStoreData = {
3078 personalId: personalId, // VARCHAR for personal table
3079 ownerFirstName: formData.ownerFirstName,
3080 ownerLastName: formData.ownerLastName,
3081 ownerSSN: formData.ownerSSN,
3082 ownerEmail: formData.ownerEmail,
3083 storeId: storeIdPadded, // VARCHAR for store table
3084 storeIdPadded: storeIdPadded,
3085 storeName: formData.storeName,
3086 storeAddress: formData.storeAddress,
3087 storeEmail: formData.storeEmail,
3088 storeFoundingDate: formData.storeFoundingDate,
3089 storeDescription: formData.storeDescription || '',
3090 password: formData.password,
3091 signature: formData.signature,
3092 timestamp: Date.now()
3093 };
3094
3095 tempStoreRegistrations.set(verificationCode, tempStoreData);
3096 verificationCodes.set(formData.ownerEmail, {
3097 code: verificationCode,
3098 timestamp: Date.now(),
3099 storeRegistration: true
3100 });
3101
3102 console.log(`⏰ Generated store registration verification code for ${formData.ownerEmail}, expires in 30 seconds`);
3103 console.log(`🏪 Store ID will be: ${storeIdPadded}`);
3104 console.log(`👤 Personal ID will be: ${personalId}`);
3105
3106 sendStoreRegistrationEmail(formData.ownerEmail, verificationCode, formData.storeName)
3107 .then(() => {
3108 console.log('✅ Store registration email sent to:', formData.ownerEmail);
3109 database.logAudit(null, 'STORE_REGISTER_ATTEMPT', 'store', null, `Store registration attempt: ${formData.storeName}`, ipAddress);
3110 res.writeHead(200, { 'Content-Type': 'application/json' });
3111 res.end(JSON.stringify({
3112 success: true,
3113 message: 'Verification code sent to your email (expires in 30 seconds)',
3114 email: formData.ownerEmail,
3115 storeName: formData.storeName
3116 }));
3117 })
3118 .catch(error => {
3119 console.error('Error sending store registration email:', error.message);
3120 res.writeHead(200, { 'Content-Type': 'application/json' });
3121 res.end(JSON.stringify({
3122 success: true,
3123 message: 'Verification code generated (check console, expires in 30 seconds)',
3124 email: formData.ownerEmail,
3125 storeName: formData.storeName,
3126 developmentCode: verificationCode
3127 }));
3128 });
3129 }
3130 );
3131 }
3132 );
3133 });
3134 });
3135 }
3136
3137 else if (pathname === '/api/client-register' && req.method === 'POST') {
3138 let body = '';
3139 req.on('data', chunk => {
3140 body += chunk.toString();
3141 });
3142 req.on('end', () => {
3143 const { firstName, lastName, email, password, address, city, postcode, country, isDefaultAddress } = JSON.parse(body);
3144
3145 if (!firstName || !lastName || !email || !password) {
3146 res.writeHead(400, { 'Content-Type': 'application/json' });
3147 res.end(JSON.stringify({ success: false, message: 'First name, last name, email and password are required' }));
3148 return;
3149 }
3150
3151 if (!validateEmail(email)) {
3152 res.writeHead(400, { 'Content-Type': 'application/json' });
3153 res.end(JSON.stringify({ success: false, message: 'Email is not valid' }));
3154 return;
3155 }
3156
3157 if (!validatePassword(password)) {
3158 res.writeHead(400, { 'Content-Type': 'application/json' });
3159 res.end(JSON.stringify({
3160 success: false,
3161 message: 'Password must have at least 8 characters, including uppercase, lowercase, number and special character'
3162 }));
3163 return;
3164 }
3165
3166 database.getClientByEmail(email, (err, existingClient) => {
3167 if (err) {
3168 console.error('Error checking client:', err);
3169 res.writeHead(500, { 'Content-Type': 'application/json' });
3170 res.end(JSON.stringify({ success: false, message: 'Server error checking client' }));
3171 return;
3172 }
3173
3174 if (existingClient) {
3175 res.writeHead(400, { 'Content-Type': 'application/json' });
3176 res.end(JSON.stringify({ success: false, message: 'Email is already registered' }));
3177 return;
3178 }
3179
3180 const verificationCode = generateVerificationCode();
3181
3182 const tempUserData = {
3183 username: `${firstName} ${lastName}`,
3184 email,
3185 password,
3186 timestamp: Date.now(),
3187 userType: 'client',
3188 firstName: firstName,
3189 lastName: lastName,
3190 address: address || null,
3191 city: city || null,
3192 postcode: postcode || null,
3193 country: country || null,
3194 isDefaultAddress: isDefaultAddress || false
3195 };
3196
3197 tempUsers.set(verificationCode, tempUserData);
3198 verificationCodes.set(email, { code: verificationCode, timestamp: Date.now() });
3199
3200 console.log(`⏰ Generated verification code for client ${email}, expires in 30 seconds`);
3201
3202 sendVerificationEmail(email, verificationCode)
3203 .then(() => {
3204 console.log('✅ Verification email sent to:', email);
3205 database.logAudit(null, 'CLIENT_REGISTER_ATTEMPT', 'client', null, `Client registration attempt for ${email}`, ipAddress);
3206 res.writeHead(200, { 'Content-Type': 'application/json' });
3207 res.end(JSON.stringify({
3208 success: true,
3209 message: 'Verification code sent to your email (expires in 30 seconds)',
3210 email: email
3211 }));
3212 })
3213 .catch(error => {
3214 console.error('Error sending email:', error.message);
3215 res.writeHead(200, { 'Content-Type': 'application/json' });
3216 res.end(JSON.stringify({
3217 success: true,
3218 message: 'Verification code generated (check console, expires in 30 seconds)',
3219 email: email,
3220 developmentCode: verificationCode
3221 }));
3222 });
3223 });
3224 });
3225 }
3226
3227 else if (pathname === '/api/resend-verification' && req.method === 'POST') {
3228 let body = '';
3229 req.on('data', chunk => {
3230 body += chunk.toString();
3231 });
3232 req.on('end', () => {
3233 const { email } = JSON.parse(body);
3234
3235 if (!email) {
3236 res.writeHead(400, { 'Content-Type': 'application/json' });
3237 res.end(JSON.stringify({ success: false, message: 'Email is required' }));
3238 return;
3239 }
3240
3241 const existingTempUser = Array.from(tempUsers.values()).find(user => user.email === email);
3242
3243 if (existingTempUser) {
3244 const newVerificationCode = generateVerificationCode();
3245
3246 const tempUserData = {
3247 username: existingTempUser.username,
3248 email: existingTempUser.email,
3249 password: existingTempUser.password,
3250 timestamp: Date.now(),
3251 userType: existingTempUser.userType,
3252 firstName: existingTempUser.firstName || '',
3253 lastName: existingTempUser.lastName || '',
3254 address: existingTempUser.address || null,
3255 city: existingTempUser.city || null,
3256 postcode: existingTempUser.postcode || null,
3257 country: existingTempUser.country || null,
3258 isDefaultAddress: existingTempUser.isDefaultAddress || false
3259 };
3260
3261 tempUsers.forEach((value, key) => {
3262 if (value.email === email) {
3263 tempUsers.delete(key);
3264 }
3265 });
3266
3267 tempUsers.set(newVerificationCode, tempUserData);
3268 verificationCodes.set(email, { code: newVerificationCode, timestamp: Date.now() });
3269
3270 console.log(`🔄 Resent verification code for ${email}, expires in 30 seconds`);
3271
3272 sendVerificationEmail(email, newVerificationCode)
3273 .then(() => {
3274 res.writeHead(200, { 'Content-Type': 'application/json' });
3275 res.end(JSON.stringify({
3276 success: true,
3277 message: 'New verification code sent to your email (expires in 30 seconds)',
3278 email: email
3279 }));
3280 })
3281 .catch(error => {
3282 console.error('Error sending email:', error.message);
3283 res.writeHead(200, { 'Content-Type': 'application/json' });
3284 res.end(JSON.stringify({
3285 success: true,
3286 message: 'New verification code generated (check console, expires in 30 seconds)',
3287 email: email,
3288 developmentCode: newVerificationCode
3289 }));
3290 });
3291
3292 return;
3293 }
3294
3295 const existingTempStore = Array.from(tempStoreRegistrations.values()).find(store => store.ownerEmail === email);
3296
3297 if (existingTempStore) {
3298 const newVerificationCode = generateVerificationCode();
3299
3300 const tempStoreData = {
3301 personalId: existingTempStore.personalId,
3302 ownerFirstName: existingTempStore.ownerFirstName,
3303 ownerLastName: existingTempStore.ownerLastName,
3304 ownerSSN: existingTempStore.ownerSSN,
3305 ownerEmail: existingTempStore.ownerEmail,
3306 storeId: existingTempStore.storeId,
3307 storeIdPadded: existingTempStore.storeIdPadded,
3308 storeName: existingTempStore.storeName,
3309 storeAddress: existingTempStore.storeAddress,
3310 storeEmail: existingTempStore.storeEmail,
3311 storeFoundingDate: existingTempStore.storeFoundingDate,
3312 storeDescription: existingTempStore.storeDescription,
3313 password: existingTempStore.password,
3314 signature: existingTempStore.signature,
3315 timestamp: Date.now()
3316 };
3317
3318 tempStoreRegistrations.forEach((value, key) => {
3319 if (value.ownerEmail === email) {
3320 tempStoreRegistrations.delete(key);
3321 }
3322 });
3323
3324 tempStoreRegistrations.set(newVerificationCode, tempStoreData);
3325 verificationCodes.set(email, {
3326 code: newVerificationCode,
3327 timestamp: Date.now(),
3328 storeRegistration: true
3329 });
3330
3331 console.log(`🔄 Resent store registration verification code for ${email}, expires in 30 seconds`);
3332
3333 sendStoreRegistrationEmail(email, newVerificationCode, existingTempStore.storeName)
3334 .then(() => {
3335 res.writeHead(200, { 'Content-Type': 'application/json' });
3336 res.end(JSON.stringify({
3337 success: true,
3338 message: 'New verification code sent to your email (expires in 30 seconds)',
3339 email: email
3340 }));
3341 })
3342 .catch(error => {
3343 console.error('Error sending store registration email:', error.message);
3344 res.writeHead(200, { 'Content-Type': 'application/json' });
3345 res.end(JSON.stringify({
3346 success: true,
3347 message: 'New verification code generated (check console, expires in 30 seconds)',
3348 email: email,
3349 developmentCode: newVerificationCode
3350 }));
3351 });
3352
3353 return;
3354 }
3355
3356 res.writeHead(400, { 'Content-Type': 'application/json' });
3357 res.end(JSON.stringify({ success: false, message: 'No pending registration found for this email' }));
3358 });
3359 }
3360
3361 else if (pathname === '/api/verify-email' && req.method === 'POST') {
3362 let body = '';
3363 req.on('data', chunk => {
3364 body += chunk.toString();
3365 });
3366 req.on('end', () => {
3367 const { email, code } = JSON.parse(body);
3368
3369 if (!email || !code) {
3370 res.writeHead(400, { 'Content-Type': 'application/json' });
3371 res.end(JSON.stringify({ success: false, message: 'All fields are required' }));
3372 return;
3373 }
3374
3375 const verificationData = verificationCodes.get(email);
3376
3377 if (verificationData && verificationData.storeRegistration) {
3378 const tempStoreData = tempStoreRegistrations.get(code);
3379
3380 if (!tempStoreData || tempStoreData.ownerEmail !== email) {
3381 res.writeHead(400, { 'Content-Type': 'application/json' });
3382 res.end(JSON.stringify({ success: false, message: 'Invalid verification code' }));
3383 return;
3384 }
3385
3386 if (Date.now() - tempStoreData.timestamp > 30 * 1000) {
3387 tempStoreRegistrations.delete(code);
3388 verificationCodes.delete(email);
3389 res.writeHead(400, { 'Content-Type': 'application/json' });
3390 res.end(JSON.stringify({ success: false, message: 'Verification code has expired. Please request a new one.' }));
3391 return;
3392 }
3393
3394 database.database.run('BEGIN TRANSACTION', (err) => {
3395 if (err) {
3396 console.error('Error beginning transaction:', err);
3397 res.writeHead(500, { 'Content-Type': 'application/json' });
3398 res.end(JSON.stringify({ success: false, message: 'Server error during registration' }));
3399 return;
3400 }
3401
3402 // Insert into store table (store_id is VARCHAR)
3403 database.database.run(
[33517cc]3404 'INSERT INTO store (store_id, name, date_of_founding, physical_address, store_email, rating) VALUES ($1, $2, $3, $4, $5, $6)',
[69f2a41]3405 [
3406 tempStoreData.storeId,
3407 tempStoreData.storeName,
3408 tempStoreData.storeFoundingDate,
3409 tempStoreData.storeAddress,
3410 tempStoreData.storeEmail,
3411 0.0
3412 ],
3413 function(err) {
3414 if (err) {
3415 database.database.run('ROLLBACK');
3416 console.error('Error inserting store:', err);
3417 res.writeHead(400, { 'Content-Type': 'application/json' });
3418 res.end(JSON.stringify({ success: false, message: 'Error registering store' }));
3419 return;
3420 }
3421
3422 // Insert into personal table (id is VARCHAR)
3423 database.database.run(
[33517cc]3424 'INSERT INTO personal (id, first_name, last_name, ssn, email, password) VALUES ($1, $2, $3, $4, $5, $6)',
[69f2a41]3425 [
3426 tempStoreData.personalId,
3427 tempStoreData.ownerFirstName,
3428 tempStoreData.ownerLastName,
3429 tempStoreData.ownerSSN,
3430 tempStoreData.ownerEmail,
3431 bcrypt.hashSync(tempStoreData.password, 10)
3432 ],
3433 function(err) {
3434 if (err) {
3435 database.database.run('ROLLBACK');
3436 console.error('Error inserting personal:', err);
[79fff4f]3437
[69f2a41]3438 if (err.code === '23505') {
3439 res.writeHead(400, { 'Content-Type': 'application/json' });
3440 res.end(JSON.stringify({
3441 success: false,
3442 message: 'This personal ID is already taken. Please try again.'
3443 }));
3444 } else {
3445 res.writeHead(400, { 'Content-Type': 'application/json' });
3446 res.end(JSON.stringify({ success: false, message: 'Error registering personal information' }));
3447 }
3448 return;
3449 }
3450
3451 // Insert into boss table (boss_id is VARCHAR, references personal.id)
3452 database.database.run(
[06ebe74]3453 'INSERT INTO boss (boss_id) VALUES ($1)',
3454 [tempStoreData.personalId],
[69f2a41]3455 (err) => {
3456 if (err) {
3457 database.database.run('ROLLBACK');
3458 console.error('Error inserting boss:', err);
3459 res.writeHead(400, { 'Content-Type': 'application/json' });
3460 res.end(JSON.stringify({ success: false, message: 'Error registering as boss' }));
3461 return;
3462 }
3463
3464 // Insert into works_in_store table (personal_id is VARCHAR, store_id is VARCHAR)
3465 database.database.run(
[33517cc]3466 'INSERT INTO works_in_store (personal_id, store_id) VALUES ($1, $2)',
[69f2a41]3467 [tempStoreData.personalId, tempStoreData.storeId],
3468 (err) => {
3469 if (err) {
3470 database.database.run('ROLLBACK');
3471 console.error('Error inserting works_in_store:', err);
3472 res.writeHead(400, { 'Content-Type': 'application/json' });
3473 res.end(JSON.stringify({ success: false, message: 'Error assigning to store' }));
3474 return;
3475 }
3476
3477 // Insert into permissions table (personal_id is VARCHAR)
3478 database.database.run(
[33517cc]3479 'INSERT INTO permissions (personal_id, type, authorisation) VALUES ($1, $2, $3)',
[69f2a41]3480 [tempStoreData.personalId, 'BOSS', 'full_access'],
3481 (err) => {
3482 if (err) {
3483 console.error('Error inserting permissions:', err);
3484 }
3485
[79fff4f]3486 // Also create entry in users table for login with force_password_change = 1
3487 database.database.run(
[33517cc]3488 'INSERT INTO users (id, username, email, password, user_type, force_password_change) VALUES ($1, $2, $3, $4, $5, $6)',
[79fff4f]3489 [
3490 tempStoreData.personalId,
3491 `${tempStoreData.ownerFirstName} ${tempStoreData.ownerLastName}`,
3492 tempStoreData.ownerEmail,
3493 bcrypt.hashSync(tempStoreData.password, 10),
3494 'store_owner',
3495 1
3496 ],
3497 (err) => {
3498 if (err) {
3499 console.error('Error creating user entry for store owner:', err);
3500 }
3501
3502 database.database.run('COMMIT', (commitErr) => {
3503 if (commitErr) {
3504 console.error('Error committing transaction:', commitErr);
3505 database.database.run('ROLLBACK');
3506 res.writeHead(500, { 'Content-Type': 'application/json' });
3507 res.end(JSON.stringify({ success: false, message: 'Error completing registration' }));
3508 return;
3509 }
[69f2a41]3510
[79fff4f]3511 tempStoreRegistrations.delete(code);
3512 verificationCodes.delete(email);
[69f2a41]3513
[79fff4f]3514 console.log(`✅ Store registration completed successfully:`);
3515 console.log(` Store ID: ${tempStoreData.storeId}`);
3516 console.log(` Store Name: ${tempStoreData.storeName}`);
3517 console.log(` Personal ID: ${tempStoreData.personalId}`);
3518 console.log(` Owner: ${tempStoreData.ownerFirstName} ${tempStoreData.ownerLastName}`);
[69f2a41]3519
[79fff4f]3520 database.logAudit(tempStoreData.personalId, 'STORE_REGISTER_SUCCESS', 'store', tempStoreData.storeId, `Store registered: ${tempStoreData.storeName}`, ipAddress);
[69f2a41]3521
[79fff4f]3522 res.writeHead(200, { 'Content-Type': 'application/json' });
3523 res.end(JSON.stringify({
3524 success: true,
3525 message: 'Store registration successful! You can now login.',
3526 storeId: tempStoreData.storeId,
3527 storeIdPadded: tempStoreData.storeIdPadded,
3528 storeName: tempStoreData.storeName,
3529 personalId: tempStoreData.personalId,
3530 userType: 'store_owner',
3531 redirectTo: 'login.html'
3532 }));
3533 });
3534 }
3535 );
[69f2a41]3536 }
3537 );
3538 }
3539 );
3540 }
3541 );
3542 }
3543 );
3544 }
3545 );
3546 });
3547
3548 return;
3549 }
3550
3551 const tempUserData = tempUsers.get(code);
3552
3553 if (!tempUserData || tempUserData.email !== email) {
3554 res.writeHead(400, { 'Content-Type': 'application/json' });
3555 res.end(JSON.stringify({ success: false, message: 'Invalid verification code' }));
3556 return;
3557 }
3558
3559 if (Date.now() - tempUserData.timestamp > 30 * 1000) {
3560 tempUsers.delete(code);
3561 verificationCodes.delete(email);
3562 res.writeHead(400, { 'Content-Type': 'application/json' });
3563 res.end(JSON.stringify({ success: false, message: 'Verification code has expired. Please request a new one.' }));
3564 return;
3565 }
3566
3567 if (tempUserData.userType === 'client') {
3568 database.createClient({
3569 first_name: tempUserData.firstName || tempUserData.username.split(' ')[0] || '',
3570 last_name: tempUserData.lastName || tempUserData.username.split(' ')[1] || '',
3571 email: tempUserData.email,
3572 password: tempUserData.password
3573 }, (err, clientId) => {
3574 if (err) {
3575 console.error('Error creating client:', err);
3576 res.writeHead(400, { 'Content-Type': 'application/json' });
3577 res.end(JSON.stringify({ success: false, message: 'Registration failed' }));
3578 } else {
3579 if (tempUserData.address && tempUserData.city && tempUserData.postcode && tempUserData.country) {
3580 database.database.run(
[33517cc]3581 'INSERT INTO delivery_address (client_id, address, city, postcode, country, is_default) VALUES ($1, $2, $3, $4, $5, $6)',
[69f2a41]3582 [
3583 clientId,
3584 tempUserData.address,
3585 tempUserData.city,
3586 tempUserData.postcode,
3587 tempUserData.country,
3588 tempUserData.isDefaultAddress ? 1 : 0
3589 ],
3590 (err) => {
3591 if (err) {
3592 console.error('Error saving delivery address:', err);
3593 }
3594 }
3595 );
3596 }
3597
3598 tempUsers.delete(code);
3599 verificationCodes.delete(email);
3600
3601 database.logAudit(clientId, 'REGISTER_SUCCESS', 'client', clientId.toString(), 'Client registered', ipAddress);
3602
3603 res.writeHead(200, { 'Content-Type': 'application/json' });
3604 res.end(JSON.stringify({
3605 success: true,
3606 message: 'Successfully registered! You can now login.',
3607 userId: clientId,
3608 userType: 'client',
3609 redirectTo: 'login.html'
3610 }));
3611 }
3612 });
3613 } else {
3614 const userId = 'user_' + Date.now().toString().slice(-8);
[79fff4f]3615
[69f2a41]3616 database.createUser(userId, tempUserData.username, tempUserData.email, tempUserData.password, tempUserData.userType, (err, userId) => {
3617 if (err) {
3618 console.error('Error creating user:', err);
3619 res.writeHead(400, { 'Content-Type': 'application/json' });
3620 res.end(JSON.stringify({ success: false, message: 'Registration failed' }));
3621 } else {
3622 tempUsers.delete(code);
3623 verificationCodes.delete(email);
3624
3625 database.logAudit(userId, 'REGISTER_SUCCESS', 'user', userId.toString(), `User registered as ${tempUserData.userType}`, ipAddress);
3626
3627 res.writeHead(200, { 'Content-Type': 'application/json' });
3628 res.end(JSON.stringify({
3629 success: true,
3630 message: 'Successfully registered! You can now login.',
3631 userId: userId,
3632 userType: tempUserData.userType,
3633 redirectTo: 'login.html'
3634 }));
3635 }
3636 });
3637 }
3638 });
3639 }
3640
3641 else if (pathname === '/api/login' && req.method === 'POST') {
3642 let body = '';
3643 req.on('data', chunk => {
3644 body += chunk.toString();
3645 });
3646 req.on('end', () => {
3647 const { email, password } = JSON.parse(body);
[591278c]3648
[69f2a41]3649 console.log(`🔍 Login attempt for email: ${email}`);
3650
[4dff800]3651 // First check if it's the admin user (special case)
3652 if (email === 'admin@handcraft.com') {
3653 database.getUserByUsername('admin', (err, adminUser) => {
3654 if (err || !adminUser) {
3655 console.error('Admin user not found');
3656 database.logAudit(null, 'LOGIN_FAILED', 'auth', null, `Admin login failed - user not found`, ipAddress);
3657 res.writeHead(401, { 'Content-Type': 'application/json' });
3658 res.end(JSON.stringify({ success: false, message: 'Invalid email or password' }));
3659 return;
3660 }
3661
3662 if (database.verifyPassword(password, adminUser.password)) {
3663 const isFirstTimeLogin = adminUser.force_password_change === 1;
3664
3665 const twoFACode = generateVerificationCode();
3666 verificationCodes.set(adminUser.email, {
3667 code: twoFACode,
3668 timestamp: Date.now(),
3669 userId: adminUser.id,
3670 isFirstTimeLogin: isFirstTimeLogin,
3671 userType: 'admin',
3672 needsPasswordChange: isFirstTimeLogin
3673 });
3674
3675 console.log(`⏰ Generated 2FA code for admin ${adminUser.email}`);
3676
3677 send2FACode(adminUser.email, twoFACode)
3678 .then(() => {
3679 res.writeHead(200, { 'Content-Type': 'application/json' });
3680 res.end(JSON.stringify({
3681 success: true,
3682 message: 'Two-factor authentication code sent to your email',
3683 requires2FA: true,
3684 email: adminUser.email,
3685 username: adminUser.username,
3686 isFirstTimeLogin: isFirstTimeLogin,
3687 userType: 'admin'
3688 }));
3689 })
3690 .catch(error => {
3691 console.error('Error sending 2FA email:', error);
3692 res.writeHead(200, { 'Content-Type': 'application/json' });
3693 res.end(JSON.stringify({
3694 success: true,
3695 message: 'Two-factor authentication required',
3696 requires2FA: true,
3697 email: adminUser.email,
3698 username: adminUser.username,
3699 isFirstTimeLogin: isFirstTimeLogin,
3700 userType: 'admin',
3701 developmentCode: twoFACode
3702 }));
3703 });
3704 } else {
3705 database.logAudit(adminUser.id, 'LOGIN_FAILED', 'auth', adminUser.id.toString(), 'Invalid password for admin', ipAddress);
3706 res.writeHead(401, { 'Content-Type': 'application/json' });
3707 res.end(JSON.stringify({ success: false, message: 'Invalid email or password' }));
3708 }
3709 });
[79fff4f]3710
[4dff800]3711 return;
3712 }
3713
[69f2a41]3714 // First check if it's a client
3715 database.getClientByEmail(email, (err, client) => {
3716 if (err) {
3717 console.error('Error checking client:', err);
3718 }
3719
3720 if (client) {
3721 console.log(`🔍 Found client: ${client.email}`);
[591278c]3722
[69f2a41]3723 if (!client.password) {
3724 console.log('❌ Client has no password set');
3725 database.logAudit(client.client_ID, 'LOGIN_FAILED', 'auth', client.client_ID?.toString() || 'unknown', 'Client has no password', ipAddress);
3726 res.writeHead(401, { 'Content-Type': 'application/json' });
3727 res.end(JSON.stringify({ success: false, message: 'Invalid email or password' }));
3728 return;
3729 }
3730
3731 database.verifyClientPassword(password, client.password, (err, isValid) => {
3732 if (err || !isValid) {
3733 const clientId = client.client_ID || 'unknown';
3734 database.logAudit(clientId, 'LOGIN_FAILED', 'auth',
3735 typeof clientId === 'string' ? clientId : String(clientId),
3736 'Invalid password for client', ipAddress);
3737 res.writeHead(401, { 'Content-Type': 'application/json' });
3738 res.end(JSON.stringify({ success: false, message: 'Invalid email or password' }));
3739 return;
3740 }
3741
3742 // Clients go directly to dashboard (no 2FA)
3743 const sessionId = generateSessionId();
3744 const clientId = client.client_ID;
3745 sessions.set(sessionId, `client_${clientId}`);
3746
3747 console.log(`✅ Client login successful. Session: ${sessionId}, User: client_${clientId}`);
[591278c]3748
[69f2a41]3749 database.logAudit(clientId, 'LOGIN_SUCCESS', 'auth',
3750 typeof clientId === 'string' ? clientId : String(clientId),
3751 'Client logged in successfully', ipAddress);
3752
3753 res.writeHead(200, {
3754 'Content-Type': 'application/json',
[33517cc]3755 'Set-Cookie': `sessionId=${sessionId}; HttpOnly; Path=/; Max-Age=3600; SameSite=Strict`
[69f2a41]3756 });
[79fff4f]3757
[69f2a41]3758 res.end(JSON.stringify({
3759 success: true,
3760 message: 'Successfully logged in',
3761 user: {
3762 id: clientId,
3763 firstName: client.first_name,
3764 lastName: client.last_name,
3765 email: client.email,
3766 userType: 'client'
3767 },
3768 redirectTo: 'client-dashboard.html'
3769 }));
3770 });
[4dff800]3771
[69f2a41]3772 return;
3773 }
3774
3775 // If not client, check personal table
3776 database.getPersonalByEmail(email, (err, personal) => {
3777 if (err) {
3778 console.error('Error checking personal:', err);
3779 }
3780
3781 if (personal) {
3782 console.log(`🔍 Found personal user: ${personal.email}`);
[591278c]3783
[69f2a41]3784 if (!personal.password) {
3785 console.log('❌ Personal has no password set');
3786 database.logAudit(personal.id, 'LOGIN_FAILED', 'auth', personal.id, 'Personal has no password', ipAddress);
3787 res.writeHead(401, { 'Content-Type': 'application/json' });
3788 res.end(JSON.stringify({ success: false, message: 'Invalid email or password' }));
3789 return;
3790 }
3791
3792 database.verifyClientPassword(password, personal.password, (err, isValid) => {
3793 if (err || !isValid) {
3794 database.logAudit(personal.id, 'LOGIN_FAILED', 'auth', personal.id, 'Invalid password for personal', ipAddress);
3795 res.writeHead(401, { 'Content-Type': 'application/json' });
3796 res.end(JSON.stringify({ success: false, message: 'Invalid email or password' }));
3797 return;
3798 }
3799
3800 // Check if this is a boss (store owner)
3801 database.database.get(
[33517cc]3802 'SELECT boss_id FROM boss WHERE boss_id = $1',
[69f2a41]3803 [personal.id],
3804 (err, boss) => {
3805 if (err) {
3806 console.error('Error checking boss status:', err);
3807 }
3808
3809 if (boss) {
3810 // This is a store owner
3811 // Check if first time login from users table
3812 database.database.get(
[33517cc]3813 'SELECT force_password_change FROM users WHERE email = $1',
[69f2a41]3814 [email],
3815 (err, user) => {
3816 const isFirstTimeLogin = user && user.force_password_change === 1;
3817
3818 const twoFACode = generateVerificationCode();
3819 verificationCodes.set(personal.email, {
3820 code: twoFACode,
3821 timestamp: Date.now(),
3822 userId: personal.id,
3823 isFirstTimeLogin: isFirstTimeLogin,
3824 userType: 'store_owner',
3825 needsPasswordChange: isFirstTimeLogin
3826 });
3827
3828 console.log(`⏰ Generated 2FA code for store owner ${personal.email}`);
3829
3830 send2FACode(personal.email, twoFACode)
3831 .then(() => {
3832 res.writeHead(200, { 'Content-Type': 'application/json' });
3833 res.end(JSON.stringify({
3834 success: true,
3835 message: 'Two-factor authentication code sent to your email',
3836 requires2FA: true,
3837 email: personal.email,
3838 isFirstTimeLogin: isFirstTimeLogin,
3839 userType: 'store_owner'
3840 }));
3841 })
3842 .catch(error => {
3843 console.error('Error sending 2FA email:', error);
3844 res.writeHead(200, { 'Content-Type': 'application/json' });
3845 res.end(JSON.stringify({
3846 success: true,
3847 message: 'Two-factor authentication required',
3848 requires2FA: true,
3849 email: personal.email,
3850 isFirstTimeLogin: isFirstTimeLogin,
3851 userType: 'store_owner',
3852 developmentCode: twoFACode
3853 }));
3854 });
3855 }
3856 );
[4dff800]3857
[69f2a41]3858 return;
3859 }
3860
3861 // Check if this is an employee
3862 database.database.get(
[33517cc]3863 'SELECT employee_id FROM employees WHERE employee_id = $1',
[69f2a41]3864 [personal.id],
3865 (err, employee) => {
3866 if (err) {
3867 console.error('Error checking employee status:', err);
3868 }
3869
3870 if (employee) {
3871 // This is an employee
3872 database.database.get(
[33517cc]3873 'SELECT force_password_change FROM users WHERE email = $1',
[69f2a41]3874 [email],
3875 (err, user) => {
3876 const isFirstTimeLogin = user && user.force_password_change === 1;
3877
3878 const twoFACode = generateVerificationCode();
3879 verificationCodes.set(personal.email, {
3880 code: twoFACode,
3881 timestamp: Date.now(),
3882 userId: personal.id,
3883 isFirstTimeLogin: isFirstTimeLogin,
3884 userType: 'store_employee',
3885 needsPasswordChange: isFirstTimeLogin
3886 });
3887
3888 console.log(`⏰ Generated 2FA code for employee ${personal.email}`);
3889
3890 send2FACode(personal.email, twoFACode)
3891 .then(() => {
3892 res.writeHead(200, { 'Content-Type': 'application/json' });
3893 res.end(JSON.stringify({
3894 success: true,
3895 message: 'Two-factor authentication code sent to your email',
3896 requires2FA: true,
3897 email: personal.email,
3898 isFirstTimeLogin: isFirstTimeLogin,
3899 userType: 'store_employee'
3900 }));
3901 })
3902 .catch(error => {
3903 console.error('Error sending 2FA email:', error);
3904 res.writeHead(200, { 'Content-Type': 'application/json' });
3905 res.end(JSON.stringify({
3906 success: true,
3907 message: 'Two-factor authentication required',
3908 requires2FA: true,
3909 email: personal.email,
3910 isFirstTimeLogin: isFirstTimeLogin,
3911 userType: 'store_employee',
3912 developmentCode: twoFACode
3913 }));
3914 });
3915 }
3916 );
[4dff800]3917
[69f2a41]3918 return;
3919 }
3920
3921 // If we get here, it's a personal record without boss/employee status
3922 // Treat as regular user
3923 database.database.get(
[33517cc]3924 'SELECT * FROM users WHERE email = $1',
[69f2a41]3925 [email],
3926 (err, user) => {
3927 if (err || !user) {
3928 database.getUserByUsername(email, (err, userByUsername) => {
3929 if (err || !userByUsername) {
3930 database.logAudit(null, 'LOGIN_FAILED', 'auth', null, `Failed login attempt for email: ${email}`, ipAddress);
3931 res.writeHead(401, { 'Content-Type': 'application/json' });
3932 res.end(JSON.stringify({ success: false, message: 'Invalid email or password' }));
3933 return;
3934 }
3935
3936 if (database.verifyPassword(password, userByUsername.password)) {
[4dff800]3937 const isFirstTimeLogin = userByUsername.force_password_change === 1;
[69f2a41]3938
3939 const twoFACode = generateVerificationCode();
3940 verificationCodes.set(userByUsername.email, {
3941 code: twoFACode,
3942 timestamp: Date.now(),
3943 userId: userByUsername.id,
3944 isFirstTimeLogin: isFirstTimeLogin,
[4dff800]3945 userType: userByUsername.user_type,
[69f2a41]3946 needsPasswordChange: isFirstTimeLogin
3947 });
3948
3949 send2FACode(userByUsername.email, twoFACode)
3950 .then(() => {
3951 res.writeHead(200, { 'Content-Type': 'application/json' });
3952 res.end(JSON.stringify({
3953 success: true,
3954 message: 'Two-factor authentication code sent to your email',
3955 requires2FA: true,
3956 email: userByUsername.email,
3957 username: userByUsername.username,
3958 isFirstTimeLogin: isFirstTimeLogin,
[4dff800]3959 userType: userByUsername.user_type
[69f2a41]3960 }));
3961 })
3962 .catch(error => {
3963 console.error('Error sending 2FA email:', error);
3964 res.writeHead(200, { 'Content-Type': 'application/json' });
3965 res.end(JSON.stringify({
3966 success: true,
3967 message: 'Two-factor authentication required',
3968 requires2FA: true,
3969 email: userByUsername.email,
3970 username: userByUsername.username,
3971 isFirstTimeLogin: isFirstTimeLogin,
[4dff800]3972 userType: userByUsername.user_type,
[69f2a41]3973 developmentCode: twoFACode
3974 }));
3975 });
3976 } else {
3977 database.logAudit(userByUsername.id, 'LOGIN_FAILED', 'auth', userByUsername.id.toString(), 'Invalid password', ipAddress);
3978 res.writeHead(401, { 'Content-Type': 'application/json' });
3979 res.end(JSON.stringify({ success: false, message: 'Invalid email or password' }));
3980 }
3981 });
[4dff800]3982
[69f2a41]3983 return;
3984 }
3985
3986 if (database.verifyPassword(password, user.password)) {
[4dff800]3987 const isFirstTimeLogin = user.force_password_change === 1;
[69f2a41]3988
3989 const twoFACode = generateVerificationCode();
3990 verificationCodes.set(user.email, {
3991 code: twoFACode,
3992 timestamp: Date.now(),
3993 userId: user.id,
3994 isFirstTimeLogin: isFirstTimeLogin,
[4dff800]3995 userType: user.user_type,
[69f2a41]3996 needsPasswordChange: isFirstTimeLogin
3997 });
3998
3999 send2FACode(user.email, twoFACode)
4000 .then(() => {
4001 res.writeHead(200, { 'Content-Type': 'application/json' });
4002 res.end(JSON.stringify({
4003 success: true,
4004 message: 'Two-factor authentication code sent to your email',
4005 requires2FA: true,
4006 email: user.email,
4007 username: user.username,
4008 isFirstTimeLogin: isFirstTimeLogin,
[4dff800]4009 userType: user.user_type
[69f2a41]4010 }));
4011 })
4012 .catch(error => {
4013 console.error('Error sending 2FA email:', error);
4014 res.writeHead(200, { 'Content-Type': 'application/json' });
4015 res.end(JSON.stringify({
4016 success: true,
4017 message: 'Two-factor authentication required',
4018 requires2FA: true,
4019 email: user.email,
4020 username: user.username,
4021 isFirstTimeLogin: isFirstTimeLogin,
[4dff800]4022 userType: user.user_type,
[69f2a41]4023 developmentCode: twoFACode
4024 }));
4025 });
4026 } else {
4027 database.logAudit(user.id, 'LOGIN_FAILED', 'auth', user.id.toString(), 'Invalid password', ipAddress);
4028 res.writeHead(401, { 'Content-Type': 'application/json' });
4029 res.end(JSON.stringify({ success: false, message: 'Invalid email or password' }));
4030 }
4031 }
4032 );
4033 }
4034 );
4035 }
4036 );
4037 });
[4dff800]4038
[69f2a41]4039 return;
4040 }
4041
4042 // No user found in any table
4043 database.logAudit(null, 'LOGIN_FAILED', 'auth', null, `Failed login attempt for email: ${email}`, ipAddress);
4044 res.writeHead(401, { 'Content-Type': 'application/json' });
4045 res.end(JSON.stringify({ success: false, message: 'Invalid email or password' }));
4046 });
4047 });
4048 });
4049 }
4050
4051 else if (pathname === '/api/resend-2fa' && req.method === 'POST') {
4052 let body = '';
4053 req.on('data', chunk => {
4054 body += chunk.toString();
4055 });
4056 req.on('end', () => {
4057 const { email } = JSON.parse(body);
4058
4059 if (!email) {
4060 res.writeHead(400, { 'Content-Type': 'application/json' });
4061 res.end(JSON.stringify({ success: false, message: 'Email is required' }));
4062 return;
4063 }
4064
4065 database.database.get(
[33517cc]4066 'SELECT * FROM users WHERE email = $1',
[69f2a41]4067 [email],
4068 (err, user) => {
4069 if (err || !user) {
4070 database.getUserByUsername(email, (err, userByUsername) => {
4071 if (err || !userByUsername) {
4072 res.writeHead(400, { 'Content-Type': 'application/json' });
4073 res.end(JSON.stringify({ success: false, message: 'User not found' }));
4074 return;
4075 }
4076
4077 const newTwoFACode = generateVerificationCode();
4078 verificationCodes.set(userByUsername.email, {
4079 code: newTwoFACode,
4080 timestamp: Date.now(),
4081 userId: userByUsername.id,
[4dff800]4082 isFirstTimeLogin: userByUsername.force_password_change === 1,
4083 needsPasswordChange: userByUsername.force_password_change === 1,
[69f2a41]4084 userType: userByUsername.user_type
4085 });
4086
4087 console.log(`🔄 Resent 2FA code for ${userByUsername.email}, expires in 30 seconds`);
4088
4089 send2FACode(userByUsername.email, newTwoFACode)
4090 .then(() => {
4091 res.writeHead(200, { 'Content-Type': 'application/json' });
4092 res.end(JSON.stringify({
4093 success: true,
4094 message: 'New two-factor authentication code sent to your email (expires in 30 seconds)',
4095 email: userByUsername.email
4096 }));
4097 })
4098 .catch(error => {
4099 console.error('Error sending 2FA email:', error.message);
4100 res.writeHead(200, { 'Content-Type': 'application/json' });
4101 res.end(JSON.stringify({
4102 success: true,
4103 message: 'New two-factor authentication code generated (check console, expires in 30 seconds)',
4104 email: userByUsername.email,
4105 developmentCode: newTwoFACode
4106 }));
4107 });
4108 });
[4dff800]4109
[69f2a41]4110 return;
4111 }
4112
4113 const newTwoFACode = generateVerificationCode();
4114 verificationCodes.set(user.email, {
4115 code: newTwoFACode,
4116 timestamp: Date.now(),
4117 userId: user.id,
[4dff800]4118 isFirstTimeLogin: user.force_password_change === 1,
4119 needsPasswordChange: user.force_password_change === 1,
[69f2a41]4120 userType: user.user_type
4121 });
4122
4123 console.log(`🔄 Resent 2FA code for ${user.email}, expires in 30 seconds`);
4124
4125 send2FACode(user.email, newTwoFACode)
4126 .then(() => {
4127 res.writeHead(200, { 'Content-Type': 'application/json' });
4128 res.end(JSON.stringify({
4129 success: true,
4130 message: 'New two-factor authentication code sent to your email (expires in 30 seconds)',
4131 email: user.email
4132 }));
4133 })
4134 .catch(error => {
4135 console.error('Error sending 2FA email:', error.message);
4136 res.writeHead(200, { 'Content-Type': 'application/json' });
4137 res.end(JSON.stringify({
4138 success: true,
4139 message: 'New two-factor authentication code generated (check console, expires in 30 seconds)',
4140 email: user.email,
4141 developmentCode: newTwoFACode
4142 }));
4143 });
4144 }
4145 );
4146 });
4147 }
4148
4149 else if (pathname === '/api/verify-2fa' && req.method === 'POST') {
4150 let body = '';
4151 req.on('data', chunk => {
4152 body += chunk.toString();
4153 });
4154 req.on('end', () => {
4155 const { email, code } = JSON.parse(body);
4156
4157 if (!email || !code) {
4158 res.writeHead(400, { 'Content-Type': 'application/json' });
4159 res.end(JSON.stringify({ success: false, message: 'All fields are required' }));
4160 return;
4161 }
4162
4163 const verificationData = verificationCodes.get(email);
4164
4165 if (!verificationData || verificationData.code !== code) {
4166 res.writeHead(400, { 'Content-Type': 'application/json' });
4167 res.end(JSON.stringify({ success: false, message: 'Invalid two-factor authentication code' }));
4168 return;
4169 }
4170
4171 if (Date.now() - verificationData.timestamp > 30 * 1000) {
4172 verificationCodes.delete(email);
4173 res.writeHead(400, { 'Content-Type': 'application/json' });
4174 res.end(JSON.stringify({ success: false, message: 'Two-factor authentication code has expired. Please request a new one.' }));
4175 return;
4176 }
4177
4178 // Check if this is a first-time login that requires password change
4179 if (verificationData.needsPasswordChange) {
4180 const tempSessionId = generateSessionId();
4181 tempAdminSessions.set(tempSessionId, verificationData.userId);
4182
4183 database.logAudit(verificationData.userId, 'LOGIN_2FA_SUCCESS_PASSWORD_CHANGE_REQUIRED', 'auth', verificationData.userId.toString(),
4184 `${verificationData.userType} first login, password change required`, ipAddress);
4185
4186 verificationCodes.delete(email);
4187
[33517cc]4188 // Determine redirect based on user type
[79fff4f]4189 let redirectTo = 'change-password.html?forced=true';
4190 if (verificationData.userType === 'store_owner') {
4191 redirectTo = 'change-password.html?forced=true&redirect=store-owner.html';
4192 } else if (verificationData.userType === 'store_employee') {
4193 redirectTo = 'change-password.html?forced=true&redirect=store-employee.html';
4194 } else if (verificationData.userType === 'admin') {
4195 redirectTo = 'change-password.html?forced=true&redirect=admin.html';
4196 } else if (verificationData.userType === 'client') {
4197 redirectTo = 'change-password.html?forced=true&redirect=client-dashboard.html';
4198 }
4199
[69f2a41]4200 res.writeHead(200, {
4201 'Content-Type': 'application/json',
4202 'Set-Cookie': `sessionId=${tempSessionId}; HttpOnly; Path=/; Max-Age=3600; SameSite=Strict`
4203 });
[79fff4f]4204
[69f2a41]4205 res.end(JSON.stringify({
4206 success: true,
4207 message: 'Two-factor authentication successful. Password change required.',
4208 requiresPasswordChange: true,
4209 userType: verificationData.userType,
[79fff4f]4210 redirectTo: redirectTo
[69f2a41]4211 }));
[4dff800]4212
[69f2a41]4213 return;
4214 }
4215
4216 // Regular login - create session and redirect based on user type
4217 const sessionId = generateSessionId();
4218
4219 // Determine how to store the user ID in session
4220 if (verificationData.userType === 'client') {
4221 sessions.set(sessionId, `client_${verificationData.userId}`);
4222 } else if (verificationData.userType === 'store_owner' || verificationData.userType === 'store_employee') {
4223 sessions.set(sessionId, `personal_${verificationData.userId}`);
4224 } else {
4225 sessions.set(sessionId, verificationData.userId.toString());
4226 }
4227
4228 verificationCodes.delete(email);
4229
4230 database.logAudit(verificationData.userId, 'LOGIN_SUCCESS', 'auth', verificationData.userId.toString(),
4231 `${verificationData.userType} logged in successfully`, ipAddress);
4232
4233 // Determine redirect based on user type
4234 let redirectTo = '';
[591278c]4235
[69f2a41]4236 switch(verificationData.userType) {
4237 case 'client':
4238 redirectTo = 'client-dashboard.html';
4239 break;
4240 case 'store_owner':
4241 redirectTo = 'store-owner.html';
4242 break;
4243 case 'store_employee':
4244 redirectTo = 'store-employee.html';
4245 break;
4246 case 'admin':
4247 redirectTo = 'admin.html';
4248 break;
4249 default:
4250 redirectTo = 'dashboard.html';
4251 }
4252
[33517cc]4253 console.log(`✅ ${verificationData.userType} login successful. Redirecting to: ${redirectTo}`);
[69f2a41]4254
4255 res.writeHead(200, {
4256 'Content-Type': 'application/json',
[33517cc]4257 'Set-Cookie': `sessionId=${sessionId}; HttpOnly; Path=/; Max-Age=3600; SameSite=Strict`
[69f2a41]4258 });
[79fff4f]4259
[69f2a41]4260 res.end(JSON.stringify({
4261 success: true,
4262 message: 'Successfully logged in',
4263 userType: verificationData.userType,
4264 redirectTo: redirectTo
4265 }));
4266 });
4267 }
4268
4269 else if (pathname === '/api/logout' && req.method === 'POST') {
4270 const cookies = parseCookies(req);
4271 const sessionId = cookies.sessionId;
4272
4273 if (sessionId) {
[33517cc]4274 const userId = sessions.get(sessionId);
[69f2a41]4275 if (userId) {
4276 database.logAudit(userId, 'LOGOUT', 'auth', userId.toString(), 'User logged out', ipAddress);
4277 }
4278 sessions.delete(sessionId);
4279 tempAdminSessions.delete(sessionId);
4280 }
4281
4282 res.writeHead(200, {
4283 'Content-Type': 'application/json',
4284 'Set-Cookie': 'sessionId=; HttpOnly; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Strict'
4285 });
[79fff4f]4286
[69f2a41]4287 res.end(JSON.stringify({ success: true, message: 'Successfully logged out' }));
4288 }
4289
4290 else if (pathname === '/api/user' && req.method === 'GET') {
4291 requireAuth(req, res, (userId) => {
4292 const cookies = parseCookies(req);
4293 const sessionId = cookies.sessionId;
4294
4295 if (tempAdminSessions.has(sessionId)) {
[4dff800]4296 // This is a temporary session (password change required)
4297 // Get user info to determine type
4298 database.getUserById(userId, (err, user) => {
4299 if (err || !user) {
4300 // Check if it's a personal user
4301 database.getPersonalById(userId, (err, personal) => {
4302 if (err || !personal) {
4303 res.writeHead(200, { 'Content-Type': 'application/json' });
4304 res.end(JSON.stringify({
4305 success: true,
4306 user: {
4307 id: userId,
4308 username: 'admin',
4309 userType: 'admin',
4310 needsPasswordChange: true
4311 },
4312 isTempSession: true
4313 }));
4314 } else {
4315 // Personal user (store owner/employee)
4316 database.database.get(
[33517cc]4317 'SELECT boss_id FROM boss WHERE boss_id = $1',
[4dff800]4318 [userId],
4319 (err, boss) => {
4320 let userType = 'store_employee';
4321 if (boss) {
4322 userType = 'store_owner';
4323 }
4324
4325 res.writeHead(200, { 'Content-Type': 'application/json' });
4326 res.end(JSON.stringify({
4327 success: true,
4328 user: {
4329 id: personal.id,
4330 firstName: personal.first_name,
4331 lastName: personal.last_name,
4332 email: personal.email,
4333 userType: userType,
4334 needsPasswordChange: true
4335 },
4336 isTempSession: true
4337 }));
4338 }
4339 );
4340 }
4341 });
4342 } else {
4343 // Regular user (admin)
4344 res.writeHead(200, { 'Content-Type': 'application/json' });
4345 res.end(JSON.stringify({
4346 success: true,
4347 user: {
4348 id: user.id,
4349 username: user.username,
4350 email: user.email,
4351 userType: user.user_type || 'admin',
4352 needsPasswordChange: true
4353 },
4354 isTempSession: true
4355 }));
4356 }
4357 });
4358
[69f2a41]4359 return;
4360 }
4361
[4dff800]4362 // Regular session
[69f2a41]4363 const userIdStr = String(userId);
4364
[4dff800]4365 if (userIdStr === '000000') {
4366 // Admin user
4367 database.getUserById(userIdStr, (err, user) => {
4368 if (err || !user) {
4369 res.writeHead(404, { 'Content-Type': 'application/json' });
4370 res.end(JSON.stringify({ success: false, message: 'User not found' }));
4371 } else {
4372 res.writeHead(200, { 'Content-Type': 'application/json' });
4373 res.end(JSON.stringify({
4374 success: true,
4375 user: {
4376 id: user.id,
4377 username: user.username,
4378 email: user.email,
4379 userType: 'admin'
4380 }
4381 }));
4382 }
4383 });
4384 }
4385 else if (userIdStr.startsWith('client_')) {
[69f2a41]4386 const clientId = parseInt(userIdStr.replace('client_', ''));
[591278c]4387
[69f2a41]4388 database.getClientById(clientId, (err, client) => {
4389 if (err || !client) {
4390 res.writeHead(404, { 'Content-Type': 'application/json' });
4391 res.end(JSON.stringify({ success: false, message: 'User not found' }));
4392 } else {
4393 res.writeHead(200, { 'Content-Type': 'application/json' });
4394 res.end(JSON.stringify({
4395 success: true,
4396 user: {
4397 id: client.client_ID,
4398 firstName: client.first_name,
4399 lastName: client.last_name,
4400 email: client.email,
4401 userType: 'client'
4402 }
4403 }));
4404 }
4405 });
4406 }
4407 else if (userIdStr.startsWith('personal_')) {
4408 const personalId = userIdStr.replace('personal_', '');
[591278c]4409
[69f2a41]4410 database.getPersonalById(personalId, (err, personal) => {
4411 if (err || !personal) {
4412 res.writeHead(404, { 'Content-Type': 'application/json' });
4413 res.end(JSON.stringify({ success: false, message: 'User not found' }));
4414 return;
4415 }
4416
4417 database.database.get(
[33517cc]4418 'SELECT boss_id FROM boss WHERE boss_id = $1',
[69f2a41]4419 [personalId],
4420 (err, boss) => {
4421 if (err) {
4422 console.error('Error checking boss:', err);
4423 }
4424
4425 if (boss) {
4426 database.database.all(
4427 `SELECT s.* FROM store s
[33517cc]4428 JOIN works_in_store w ON s.store_id = w.store_id
4429 WHERE w.personal_id = $1`,
[69f2a41]4430 [personalId],
4431 (err, stores) => {
4432 if (err) {
4433 console.error('Error getting stores:', err);
4434 stores = [];
4435 }
4436
4437 res.writeHead(200, { 'Content-Type': 'application/json' });
4438 res.end(JSON.stringify({
4439 success: true,
4440 user: {
4441 id: personal.id,
4442 firstName: personal.first_name,
4443 lastName: personal.last_name,
4444 email: personal.email,
4445 userType: 'store_owner',
4446 stores: stores
4447 }
4448 }));
4449 }
4450 );
4451 } else {
4452 database.database.get(
[33517cc]4453 'SELECT employee_id FROM employees WHERE employee_id = $1',
[69f2a41]4454 [personalId],
4455 (err, employee) => {
4456 if (err) {
4457 console.error('Error checking employee:', err);
4458 }
4459
4460 if (employee) {
4461 database.database.all(
4462 `SELECT s.* FROM store s
[33517cc]4463 JOIN works_in_store w ON s.store_id = w.store_id
4464 WHERE w.personal_id = $1`,
[69f2a41]4465 [personalId],
4466 (err, stores) => {
4467 if (err) {
4468 console.error('Error getting stores:', err);
4469 stores = [];
4470 }
4471
4472 res.writeHead(200, { 'Content-Type': 'application/json' });
4473 res.end(JSON.stringify({
4474 success: true,
4475 user: {
4476 id: personal.id,
4477 firstName: personal.first_name,
4478 lastName: personal.last_name,
4479 email: personal.email,
4480 userType: 'store_employee',
4481 stores: stores
4482 }
4483 }));
4484 }
4485 );
4486 } else {
4487 res.writeHead(404, { 'Content-Type': 'application/json' });
4488 res.end(JSON.stringify({ success: false, message: 'User type not recognized' }));
4489 }
4490 }
4491 );
4492 }
4493 }
4494 );
4495 });
4496 } else {
4497 database.getUserById(userIdStr, (err, user) => {
4498 if (err || !user) {
4499 res.writeHead(404, { 'Content-Type': 'application/json' });
4500 res.end(JSON.stringify({ success: false, message: 'User not found' }));
4501 } else {
4502 res.writeHead(200, { 'Content-Type': 'application/json' });
4503 res.end(JSON.stringify({ success: true, user }));
4504 }
4505 });
4506 }
4507 });
4508 }
4509
4510 else if (pathname === '/api/products' && req.method === 'GET') {
4511 const query = parsedUrl.query;
4512 const categoryId = query.category;
4513 const searchTerm = query.search;
4514
4515 database.getProducts(categoryId, searchTerm, (err, products) => {
4516 if (err) {
4517 res.writeHead(500, { 'Content-Type': 'application/json' });
4518 res.end(JSON.stringify({ success: false, message: 'Error fetching products' }));
4519 } else {
4520 res.writeHead(200, { 'Content-Type': 'application/json' });
4521 res.end(JSON.stringify({ success: true, products }));
4522 }
4523 });
4524 }
4525
4526 else if (pathname === '/api/product' && req.method === 'GET') {
4527 const productId = parsedUrl.query.id;
4528
4529 if (!productId) {
4530 res.writeHead(400, { 'Content-Type': 'application/json' });
4531 res.end(JSON.stringify({ success: false, message: 'Product ID is required' }));
4532 return;
4533 }
4534
4535 database.getProductById(productId, (err, product) => {
4536 if (err) {
4537 res.writeHead(500, { 'Content-Type': 'application/json' });
4538 res.end(JSON.stringify({ success: false, message: 'Error fetching product' }));
4539 } else if (!product) {
4540 res.writeHead(404, { 'Content-Type': 'application/json' });
4541 res.end(JSON.stringify({ success: false, message: 'Product not found' }));
4542 } else {
4543 res.writeHead(200, { 'Content-Type': 'application/json' });
4544 res.end(JSON.stringify({ success: true, product }));
4545 }
4546 });
4547 }
4548
4549 else if (pathname === '/api/create-category' && req.method === 'POST') {
4550 requireStoreOwner()(req, res, (personalId) => {
4551 let body = '';
4552 req.on('data', chunk => {
4553 body += chunk.toString();
4554 });
4555 req.on('end', () => {
4556 const categoryData = JSON.parse(body);
4557
4558 if (!categoryData.name || !categoryData.name.trim()) {
4559 res.writeHead(400, { 'Content-Type': 'application/json' });
4560 res.end(JSON.stringify({ success: false, message: 'Category name is required' }));
4561 return;
4562 }
4563
4564 const dbCategoryData = {
4565 name: categoryData.name.trim(),
4566 description: (categoryData.description || '').trim(),
4567 parent_id: categoryData.parentId ? parseInt(categoryData.parentId) : null
4568 };
4569
4570 database.createCategory(dbCategoryData, (err, category) => {
4571 if (err) {
4572 console.error('Error creating category:', err);
4573 res.writeHead(500, { 'Content-Type': 'application/json' });
4574 res.end(JSON.stringify({ success: false, message: 'Error creating category: ' + err.message }));
4575 } else if (!category) {
4576 res.writeHead(500, { 'Content-Type': 'application/json' });
4577 res.end(JSON.stringify({ success: false, message: 'Failed to create category' }));
4578 } else {
4579 database.logAudit(personalId, 'CATEGORY_CREATED', 'category', category.id.toString(), `New category created: ${category.name}`, ipAddress);
[591278c]4580
[69f2a41]4581 res.writeHead(200, { 'Content-Type': 'application/json' });
4582 res.end(JSON.stringify({
4583 success: true,
4584 message: 'Category created successfully',
4585 category: {
4586 id: category.id,
4587 name: category.name,
[06ebe74]4588 parent_id: category.parent_category_id,
[69f2a41]4589 description: category.description
4590 }
4591 }));
4592 }
4593 });
4594 });
4595 });
4596 }
4597
4598 else if (pathname === '/api/categories' && req.method === 'GET') {
4599 database.getCategoriesWithParents((err, categories) => {
4600 if (err) {
4601 console.error('Error fetching categories:', err);
4602 database.getCategories((err, categories) => {
4603 if (err) {
4604 console.error('Error fetching categories (fallback):', err);
4605 res.writeHead(500, { 'Content-Type': 'application/json' });
4606 res.end(JSON.stringify({ success: false, message: 'Error fetching categories' }));
4607 } else {
4608 res.writeHead(200, { 'Content-Type': 'application/json' });
4609 res.end(JSON.stringify({ success: true, categories: categories || [] }));
4610 }
4611 });
4612 } else {
4613 res.writeHead(200, { 'Content-Type': 'application/json' });
4614 res.end(JSON.stringify({ success: true, categories: categories || [] }));
4615 }
4616 });
4617 }
4618
4619 else if (pathname === '/api/stores' && req.method === 'GET') {
4620 database.getStores((err, stores) => {
4621 if (err) {
4622 res.writeHead(500, { 'Content-Type': 'application/json' });
4623 res.end(JSON.stringify({ success: false, message: 'Error fetching stores' }));
4624 } else {
4625 res.writeHead(200, { 'Content-Type': 'application/json' });
4626 res.end(JSON.stringify({ success: true, stores }));
4627 }
4628 });
4629 }
4630
4631 else if (pathname === '/api/create-order' && req.method === 'POST') {
4632 requireAuth(req, res, (userId) => {
4633 let body = '';
4634 req.on('data', chunk => {
4635 body += chunk.toString();
4636 });
4637 req.on('end', () => {
4638 const orderData = JSON.parse(body);
4639 const userIdStr = String(userId);
4640
4641 if (userIdStr.startsWith('client_')) {
4642 const clientId = parseInt(userIdStr.replace('client_', ''));
4643 const storeId = orderData.storeId;
4644
4645 if (!storeId) {
4646 res.writeHead(400, { 'Content-Type': 'application/json' });
4647 res.end(JSON.stringify({ success: false, message: 'Store ID is required' }));
4648 return;
4649 }
4650
4651 const year = new Date().getFullYear().toString().slice(-3);
4652
4653 database.database.get(
[06ebe74]4654 'SELECT COUNT(*) AS order_count FROM "order" WHERE LEFT(order_num,3) = $1 AND SUBSTRING(order_num FROM 4 FOR 3)::INTEGER = ($2::INTEGER % 1000)',
[4dff800]4655 [storeId, new Date().getFullYear().toString()],
[69f2a41]4656 (err, result) => {
4657 if (err) {
4658 console.error('Error counting orders:', err);
4659 res.writeHead(500, { 'Content-Type': 'application/json' });
4660 res.end(JSON.stringify({ success: false, message: 'Error generating order ID' }));
4661 return;
4662 }
4663
[4dff800]4664 const orderCount = result ? result.order_count + 1 : 1;
[69f2a41]4665 const orderNumPadded = orderCount.toString().padStart(5, '0');
4666
4667 // Format order number: storeId + year (3 digits) + orderNum (5 digits)
4668 const orderNum = storeId + year + orderNumPadded;
4669
4670 const newOrderData = {
4671 order_num: orderNum,
4672 client_id: clientId,
4673 store_id: storeId,
4674 quantity: orderData.items.reduce((sum, item) => sum + item.quantity, 0),
4675 payment_method: orderData.paymentMethod || 'credit card',
4676 discount: orderData.discount || 0,
4677 delivery_address: orderData.deliveryAddress || 'Not specified',
4678 items: orderData.items.map(item => ({
4679 product_code: item.productCode,
4680 quantity: item.quantity,
4681 price: item.price
4682 }))
4683 };
4684
4685 database.createOrderNew(newOrderData, (err, orderId) => {
4686 if (err) {
4687 res.writeHead(500, { 'Content-Type': 'application/json' });
4688 res.end(JSON.stringify({ success: false, message: 'Error creating order' }));
4689 } else {
4690 database.logAudit(clientId, 'ORDER_CREATED', 'order', orderId.toString(), 'New order created', ipAddress);
4691 res.writeHead(200, { 'Content-Type': 'application/json' });
4692 res.end(JSON.stringify({ success: true, orderId, message: 'Order created successfully' }));
4693 }
4694 });
4695 }
4696 );
4697 } else {
4698 res.writeHead(403, { 'Content-Type': 'application/json' });
4699 res.end(JSON.stringify({ success: false, message: 'Only clients can create orders' }));
4700 }
4701 });
4702 });
4703 }
4704
4705 else if (pathname === '/api/user-orders' && req.method === 'GET') {
4706 requireAuth(req, res, (userId) => {
4707 const userIdStr = String(userId);
4708
4709 if (userIdStr.startsWith('client_')) {
4710 const clientId = parseInt(userIdStr.replace('client_', ''));
[591278c]4711
[69f2a41]4712 database.getOrdersByClient(clientId, (err, orders) => {
4713 if (err) {
4714 res.writeHead(500, { 'Content-Type': 'application/json' });
4715 res.end(JSON.stringify({ success: false, message: 'Error fetching orders' }));
4716 } else {
4717 res.writeHead(200, { 'Content-Type': 'application/json' });
4718 res.end(JSON.stringify({ success: true, orders }));
4719 }
4720 });
4721 } else {
4722 res.writeHead(403, { 'Content-Type': 'application/json' });
4723 res.end(JSON.stringify({ success: false, message: 'Only clients can view orders' }));
4724 }
4725 });
4726 }
4727
4728 else if (pathname === '/api/create-review' && req.method === 'POST') {
4729 requireAuth(req, res, (userId) => {
4730 let body = '';
4731 req.on('data', chunk => {
4732 body += chunk.toString();
4733 });
4734 req.on('end', () => {
4735 const reviewData = JSON.parse(body);
4736 const userIdStr = String(userId);
4737
4738 if (userIdStr.startsWith('client_')) {
4739 const clientId = parseInt(userIdStr.replace('client_', ''));
4740 reviewData.client_id = clientId;
4741
4742 database.createReviewNew(reviewData, (err, reviewId) => {
4743 if (err) {
4744 res.writeHead(500, { 'Content-Type': 'application/json' });
4745 res.end(JSON.stringify({ success: false, message: 'Error creating review' }));
4746 } else {
4747 database.logAudit(clientId, 'REVIEW_CREATED', 'review', reviewId.toString(), 'New review created', ipAddress);
4748 res.writeHead(200, { 'Content-Type': 'application/json' });
4749 res.end(JSON.stringify({ success: true, reviewId, message: 'Review created successfully' }));
4750 }
4751 });
4752 } else {
4753 res.writeHead(403, { 'Content-Type': 'application/json' });
4754 res.end(JSON.stringify({ success: false, message: 'Only clients can create reviews' }));
4755 }
4756 });
4757 });
4758 }
4759
4760 else if (pathname === '/api/create-request' && req.method === 'POST') {
4761 requireAuth(req, res, (userId) => {
4762 let body = '';
4763 req.on('data', chunk => {
4764 body += chunk.toString();
4765 });
4766 req.on('end', () => {
4767 const requestData = JSON.parse(body);
4768 const userIdStr = String(userId);
4769
4770 if (userIdStr.startsWith('client_')) {
4771 const clientId = parseInt(userIdStr.replace('client_', ''));
4772 const storeId = requestData.storeId;
4773
4774 if (!storeId) {
4775 res.writeHead(400, { 'Content-Type': 'application/json' });
4776 res.end(JSON.stringify({ success: false, message: 'Store ID is required' }));
4777 return;
4778 }
4779
4780 const now = new Date();
4781 const month = (now.getMonth() + 1).toString().padStart(2, '0');
4782 const year = now.getFullYear().toString().slice(-3);
4783
4784 database.database.get(
[06ebe74]4785 'SELECT COUNT(*)::int AS request_count FROM request r JOIN for_store fs ON fs.request_num=r.request_num WHERE fs.store_ID = $1 AND EXTRACT(YEAR FROM r.date_and_time)::int = $2::int AND EXTRACT(MONTH FROM r.date_and_time)::int = $3::int',
[4dff800]4786 [storeId, now.getFullYear().toString(), (now.getMonth() + 1).toString().padStart(2, '0')],
[69f2a41]4787 (err, result) => {
4788 if (err) {
4789 console.error('Error counting requests:', err);
4790 res.writeHead(500, { 'Content-Type': 'application/json' });
4791 res.end(JSON.stringify({ success: false, message: 'Error generating request ID' }));
4792 return;
4793 }
4794
[4dff800]4795 const requestCount = result ? result.request_count + 1 : 1;
[69f2a41]4796 const requestSeqPadded = requestCount.toString().padStart(2, '0');
4797
4798 // Format request number: storeId + month (2 digits) + year (3 digits) + clientId + seq (2 digits)
4799 const requestNum = storeId + month + year + clientId + requestSeqPadded;
4800
4801 const newRequestData = {
4802 request_num: requestNum,
4803 date_and_time: now.toISOString(),
4804 problem: requestData.problem,
4805 client_id: clientId,
4806 store_id: storeId
4807 };
4808
4809 database.createRequest(newRequestData, (err, requestId) => {
4810 if (err) {
4811 res.writeHead(500, { 'Content-Type': 'application/json' });
4812 res.end(JSON.stringify({ success: false, message: 'Error creating request' }));
4813 } else {
4814 database.logAudit(clientId, 'REQUEST_CREATED', 'request', requestId.toString(), 'New request created', ipAddress);
4815 res.writeHead(200, { 'Content-Type': 'application/json' });
4816 res.end(JSON.stringify({ success: true, requestId, message: 'Request created successfully' }));
4817 }
4818 });
4819 }
4820 );
4821 } else {
4822 res.writeHead(403, { 'Content-Type': 'application/json' });
4823 res.end(JSON.stringify({ success: false, message: 'Only clients can create requests' }));
4824 }
4825 });
4826 });
4827 }
4828
4829 else if (pathname === '/api/create-refund' && req.method === 'POST') {
4830 requireAuth(req, res, (userId) => {
4831 let body = '';
4832 req.on('data', chunk => {
4833 body += chunk.toString();
4834 });
4835 req.on('end', () => {
4836 const refundData = JSON.parse(body);
4837 const userIdStr = String(userId);
4838
4839 if (userIdStr.startsWith('client_')) {
4840 const clientId = parseInt(userIdStr.replace('client_', ''));
4841
4842 database.database.get(
[06ebe74]4843 'SELECT LEFT(order_num,3) AS store_id FROM "order" WHERE order_num = $1',
[69f2a41]4844 [refundData.order_num],
4845 (err, result) => {
[4dff800]4846 if (err || !result) {
[69f2a41]4847 res.writeHead(404, { 'Content-Type': 'application/json' });
4848 res.end(JSON.stringify({ success: false, message: 'Order not found' }));
4849 return;
4850 }
4851
[4dff800]4852 const storeId = result.store_id;
[69f2a41]4853 const now = new Date();
4854 const month = (now.getMonth() + 1).toString().padStart(2, '0');
4855 const year = now.getFullYear().toString().slice(-3);
4856
4857 database.database.get(
[06ebe74]4858 'SELECT COUNT(*)::int AS refund_count FROM refund WHERE SUBSTRING(refund_id::text FROM 4 FOR 2)::int = $2::int AND SUBSTRING(refund_id::text FROM 6 FOR 3)::int = ($1::int % 1000)',
[4dff800]4859 [now.getFullYear().toString(), (now.getMonth() + 1).toString().padStart(2, '0')],
[69f2a41]4860 (err, result) => {
4861 if (err) {
4862 console.error('Error counting refunds:', err);
4863 res.writeHead(500, { 'Content-Type': 'application/json' });
4864 res.end(JSON.stringify({ success: false, message: 'Error generating refund ID' }));
4865 return;
4866 }
4867
[4dff800]4868 const refundCount = result ? result.refund_count + 1 : 1;
[69f2a41]4869 const refundSeqPadded = refundCount.toString().padStart(2, '0');
4870
4871 // Format refund ID: storeId + month (2 digits) + year (3 digits) + seq (2 digits)
4872 const refundId = storeId + month + year + refundSeqPadded;
4873
4874 refundData.refund_id = refundId;
4875
4876 database.createRefund(refundData, (err, refundId) => {
4877 if (err) {
4878 res.writeHead(500, { 'Content-Type': 'application/json' });
4879 res.end(JSON.stringify({ success: false, message: 'Error creating refund' }));
4880 } else {
4881 database.logAudit(clientId, 'REFUND_CREATED', 'refund', refundId.toString(), 'New refund requested', ipAddress);
4882 res.writeHead(200, { 'Content-Type': 'application/json' });
4883 res.end(JSON.stringify({ success: true, refundId, message: 'Refund requested successfully' }));
4884 }
4885 });
4886 }
4887 );
4888 }
4889 );
4890 } else {
4891 res.writeHead(403, { 'Content-Type': 'application/json' });
4892 res.end(JSON.stringify({ success: false, message: 'Only clients can request refunds' }));
4893 }
4894 });
4895 });
4896 }
4897
4898 else if (pathname === '/api/add-product' && req.method === 'POST') {
4899 requireStoreOwner()(req, res, (personalId) => {
4900 let body = '';
4901 req.on('data', chunk => {
4902 body += chunk.toString();
4903 });
4904 req.on('end', () => {
4905 const productData = JSON.parse(body);
4906
4907 database.database.get(
[33517cc]4908 'SELECT store_id FROM works_in_store WHERE personal_id = $1',
[69f2a41]4909 [personalId],
4910 (err, bossStore) => {
4911 if (err || !bossStore) {
4912 res.writeHead(403, { 'Content-Type': 'application/json' });
4913 res.end(JSON.stringify({ success: false, message: 'Store not found for this owner' }));
4914 return;
4915 }
4916
4917 const storeId = productData.storeId || bossStore.store_id;
4918
4919 if (!storeId) {
4920 res.writeHead(400, { 'Content-Type': 'application/json' });
4921 res.end(JSON.stringify({ success: false, message: 'Store ID is required' }));
4922 return;
4923 }
4924
4925 database.database.get(
[33517cc]4926 'SELECT store_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]4927 [personalId, storeId],
4928 (err, ownsStore) => {
4929 if (err || !ownsStore) {
4930 res.writeHead(403, { 'Content-Type': 'application/json' });
4931 res.end(JSON.stringify({ success: false, message: 'You are not authorized to add products to this store' }));
4932 return;
4933 }
4934
[33517cc]4935 // PostgreSQL: extract the numeric product sequence from the store-prefixed product code
[69f2a41]4936 database.database.get(
[06ebe74]4937 'SELECT MAX(CAST(SUBSTRING(code FROM 4) AS INTEGER)) AS max_product_num FROM product WHERE LEFT(code,3) = $1',
[69f2a41]4938 [storeId],
4939 (err, result) => {
4940 if (err) {
4941 console.error('Error getting max product number:', err);
4942 res.writeHead(500, { 'Content-Type': 'application/json' });
4943 res.end(JSON.stringify({ success: false, message: 'Error generating product code' }));
4944 return;
4945 }
4946
4947 const maxProductNum = result?.max_product_num || 0;
4948 let nextProductNum = maxProductNum + 1;
4949
4950 // Ensure product number doesn't end with 0000
4951 while (nextProductNum % 10000 === 0) {
4952 nextProductNum++;
4953 }
4954
4955 // Format product code: storeId + productNum (4 digits, padded)
4956 const productNumPadded = nextProductNum.toString().padStart(4, '0');
4957 productData.code = storeId + productNumPadded;
4958 productData.store_id = storeId;
4959
4960 database.addProduct(personalId, productData, (err, productId) => {
4961 if (err) {
4962 console.error('Error adding product:', err);
4963 res.writeHead(500, { 'Content-Type': 'application/json' });
4964 res.end(JSON.stringify({
4965 success: false,
4966 message: 'Error adding product: ' + (err.message || 'Unknown error'),
4967 details: err.toString()
4968 }));
4969 } else {
4970 database.logAudit(personalId, 'PRODUCT_ADDED', 'product', productId.toString(), 'New product added', ipAddress);
4971 res.writeHead(200, { 'Content-Type': 'application/json' });
4972 res.end(JSON.stringify({
4973 success: true,
4974 productId,
4975 message: 'Product added successfully',
4976 productCode: productData.code
4977 }));
4978 }
4979 });
4980 }
4981 );
4982 }
4983 );
4984 }
4985 );
4986 });
4987 });
4988 }
4989
4990 else if (pathname === '/api/update-product' && req.method === 'POST') {
4991 requireStoreOwner()(req, res, (personalId) => {
4992 let body = '';
4993 req.on('data', chunk => {
4994 body += chunk.toString();
4995 });
4996 req.on('end', () => {
4997 const productData = JSON.parse(body);
4998
4999 if (!productData.code) {
5000 res.writeHead(400, { 'Content-Type': 'application/json' });
5001 res.end(JSON.stringify({ success: false, message: 'Product code is required' }));
5002 return;
5003 }
5004
5005 database.database.get(
[06ebe74]5006 'SELECT LEFT(code,3) AS store_id FROM product WHERE code = $1',
[69f2a41]5007 [productData.code],
5008 (err, product) => {
5009 if (err || !product) {
5010 res.writeHead(404, { 'Content-Type': 'application/json' });
5011 res.end(JSON.stringify({ success: false, message: 'Product not found' }));
5012 return;
5013 }
5014
5015 database.database.get(
[33517cc]5016 'SELECT store_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]5017 [personalId, product.store_id],
5018 (err, ownsStore) => {
5019 if (err || !ownsStore) {
5020 res.writeHead(403, { 'Content-Type': 'application/json' });
5021 res.end(JSON.stringify({ success: false, message: 'You are not authorized to update products in this store' }));
5022 return;
5023 }
5024
5025 database.updateProduct(personalId, productData, (err, changes) => {
5026 if (err) {
5027 console.error('Error updating product:', err);
5028 res.writeHead(500, { 'Content-Type': 'application/json' });
5029 res.end(JSON.stringify({ success: false, message: 'Error updating product: ' + err.message }));
5030 } else if (changes === 0) {
5031 res.writeHead(404, { 'Content-Type': 'application/json' });
5032 res.end(JSON.stringify({ success: false, message: 'Product not found or no changes made' }));
5033 } else {
5034 database.logAudit(personalId, 'PRODUCT_UPDATED', 'product', productData.code, 'Product updated', ipAddress);
5035 res.writeHead(200, { 'Content-Type': 'application/json' });
5036 res.end(JSON.stringify({ success: true, message: 'Product updated successfully' }));
5037 }
5038 });
5039 }
5040 );
5041 }
5042 );
5043 });
5044 });
5045 }
5046
5047 else if (pathname === '/api/store-reports' && req.method === 'GET') {
5048 requireRole('store_owner')(req, res, (userId, user) => {
5049 database.getStoreReports(userId, (err, reports) => {
5050 if (err) {
5051 res.writeHead(500, { 'Content-Type': 'application/json' });
5052 res.end(JSON.stringify({ success: false, message: 'Error fetching reports' }));
5053 } else {
5054 res.writeHead(200, { 'Content-Type': 'application/json' });
5055 res.end(JSON.stringify({ success: true, reports }));
5056 }
5057 });
5058 });
5059 }
5060
5061 else if (pathname === '/api/all-users' && req.method === 'GET') {
5062 requireRole('admin')(req, res, (userId, user) => {
5063 database.getAllUsers((err, users) => {
5064 if (err) {
5065 res.writeHead(500, { 'Content-Type': 'application/json' });
5066 res.end(JSON.stringify({ success: false, message: 'Error fetching users' }));
5067 } else {
5068 res.writeHead(200, { 'Content-Type': 'application/json' });
5069 res.end(JSON.stringify({ success: true, users }));
5070 }
5071 });
5072 });
5073 }
5074
5075 else if (pathname === '/api/all-orders' && req.method === 'GET') {
5076 requireRole('admin')(req, res, (userId, user) => {
5077 database.getAllOrders((err, orders) => {
5078 if (err) {
5079 res.writeHead(500, { 'Content-Type': 'application/json' });
5080 res.end(JSON.stringify({ success: false, message: 'Error fetching orders' }));
5081 } else {
5082 res.writeHead(200, { 'Content-Type': 'application/json' });
5083 res.end(JSON.stringify({ success: true, orders }));
5084 }
5085 });
5086 });
5087 }
5088
[79fff4f]5089 // Updated /api/force-change-password endpoint with redirect handling
[69f2a41]5090 else if (pathname === '/api/force-change-password' && req.method === 'POST') {
5091 const cookies = parseCookies(req);
5092 const sessionId = cookies.sessionId;
5093 const userId = tempAdminSessions.get(sessionId);
5094
5095 if (!userId) {
5096 res.writeHead(401, { 'Content-Type': 'application/json' });
5097 res.end(JSON.stringify({ success: false, message: 'Not authenticated or invalid session' }));
5098 return;
5099 }
5100
5101 let body = '';
5102 req.on('data', chunk => {
5103 body += chunk.toString();
5104 });
5105 req.on('end', () => {
[4dff800]5106 try {
[79fff4f]5107 const { newPassword, confirmPassword, redirectTo } = JSON.parse(body);
[69f2a41]5108
[4dff800]5109 if (!newPassword || !confirmPassword) {
5110 res.writeHead(400, { 'Content-Type': 'application/json' });
5111 res.end(JSON.stringify({ success: false, message: 'All fields are required' }));
5112 return;
5113 }
[69f2a41]5114
[4dff800]5115 if (newPassword !== confirmPassword) {
5116 res.writeHead(400, { 'Content-Type': 'application/json' });
5117 res.end(JSON.stringify({ success: false, message: 'New passwords do not match' }));
5118 return;
5119 }
[69f2a41]5120
[4dff800]5121 if (!validatePassword(newPassword)) {
5122 res.writeHead(400, { 'Content-Type': 'application/json' });
5123 res.end(JSON.stringify({
5124 success: false,
5125 message: 'Password must have at least 8 characters, including uppercase, lowercase, number and special character'
5126 }));
[69f2a41]5127 return;
5128 }
5129
[4dff800]5130 // First, try to find the user in the users table (for admin)
5131 database.getUserById(userId, (err, user) => {
5132 if (err) {
5133 console.error('Error finding user by ID:', err);
[69f2a41]5134 }
5135
[4dff800]5136 if (user) {
5137 // Found in users table (admin or regular user)
5138 console.log('Found user in users table:', user);
[69f2a41]5139
[4dff800]5140 const hashedPassword = bcrypt.hashSync(newPassword, 10);
[69f2a41]5141
[4dff800]5142 database.database.run(
[33517cc]5143 'UPDATE users SET password = $1, force_password_change = FALSE WHERE id = $2',
[4dff800]5144 [hashedPassword, userId],
5145 function(err) {
5146 if (err) {
5147 console.error('Error updating password:', err);
5148 res.writeHead(500, { 'Content-Type': 'application/json' });
5149 res.end(JSON.stringify({ success: false, message: 'Failed to update password' }));
5150 return;
5151 }
[69f2a41]5152
[4dff800]5153 // Also update password in personal table if it exists (for admin)
5154 database.database.run(
[33517cc]5155 'UPDATE personal SET password = $1 WHERE id = $2',
[4dff800]5156 [hashedPassword, userId],
5157 function(err) {
5158 if (err) {
5159 console.log('No personal record to update for ID:', userId);
5160 }
5161 }
5162 );
5163
5164 // Clear temp session
5165 tempAdminSessions.delete(sessionId);
5166
5167 // Create new permanent session
5168 const newSessionId = generateSessionId();
[79fff4f]5169
5170 // Determine how to store the user ID based on user type
5171 let sessionUserId = String(userId);
5172
5173 if (user.user_type === 'store_owner' || user.user_type === 'store_employee') {
5174 sessionUserId = `personal_${userId}`;
5175 }
5176
5177 sessions.set(newSessionId, sessionUserId);
5178
5179 // Determine redirect based on user type or provided redirectTo
5180 let finalRedirect = redirectTo || 'dashboard.html';
5181
5182 if (!redirectTo) {
5183 if (user.username === 'admin' || user.user_type === 'admin') {
5184 finalRedirect = 'admin.html';
5185 } else if (user.user_type === 'store_owner') {
5186 finalRedirect = 'store-owner.html';
5187 } else if (user.user_type === 'store_employee') {
5188 finalRedirect = 'store-employee.html';
5189 } else if (user.user_type === 'client') {
5190 finalRedirect = 'client-dashboard.html';
5191 }
[4dff800]5192 }
5193
[33517cc]5194 console.log(`Password changed successfully for user ${userId}, redirecting to ${finalRedirect}`);
[4dff800]5195
5196 database.logAudit(userId, 'FORCED_PASSWORD_CHANGE', 'auth', userId.toString(),
5197 `${user.user_type || 'user'} forced password change completed`, ipAddress);
5198
[33517cc]5199 // Set the cookie with proper options
[4dff800]5200 res.writeHead(200, {
5201 'Content-Type': 'application/json',
[33517cc]5202 'Set-Cookie': `sessionId=${newSessionId}; HttpOnly; Path=/; Max-Age=86400; SameSite=Strict` // Extended to 24 hours
[4dff800]5203 });
[79fff4f]5204
[4dff800]5205 res.end(JSON.stringify({
5206 success: true,
5207 message: 'Password changed successfully.',
[79fff4f]5208 redirectTo: finalRedirect,
[4dff800]5209 userType: user.user_type || 'user'
5210 }));
5211 }
5212 );
5213 } else {
5214 // Not found in users table, check personal table (for store owners/employees)
5215 console.log('User not found in users table, checking personal table for ID:', userId);
5216
5217 database.getPersonalById(userId, (err, personal) => {
5218 if (err) {
5219 console.error('Error finding personal by ID:', err);
5220 }
5221
5222 if (personal) {
5223 console.log('Found user in personal table:', personal);
5224
5225 // Update password in personal table
5226 const hashedPassword = bcrypt.hashSync(newPassword, 10);
5227
5228 database.database.run(
[33517cc]5229 'UPDATE personal SET password = $1 WHERE id = $2',
[4dff800]5230 [hashedPassword, userId],
5231 function(err) {
5232 if (err) {
5233 console.error('Error updating personal password:', err);
5234 res.writeHead(500, { 'Content-Type': 'application/json' });
5235 res.end(JSON.stringify({ success: false, message: 'Failed to update password' }));
5236 return;
5237 }
5238
5239 // Also update in users table if exists
5240 database.database.run(
[33517cc]5241 'UPDATE users SET password = $1, force_password_change = FALSE WHERE email = $2',
[4dff800]5242 [hashedPassword, personal.email],
5243 function(err) {
5244 if (err) {
5245 console.log('No users record to update for email:', personal.email);
5246 }
5247 }
5248 );
5249
5250 // Determine user type (boss/owner or employee)
5251 database.database.get(
[33517cc]5252 'SELECT boss_id FROM boss WHERE boss_id = $1',
[4dff800]5253 [userId],
5254 (err, boss) => {
5255 let userType = 'store_employee';
[79fff4f]5256 let finalRedirect = redirectTo || 'store-employee.html';
[4dff800]5257
5258 if (boss) {
5259 userType = 'store_owner';
[79fff4f]5260 finalRedirect = redirectTo || 'store-owner.html';
[4dff800]5261 }
5262
5263 // Clear temp session
5264 tempAdminSessions.delete(sessionId);
5265
[79fff4f]5266 // Create new permanent session with personal_ prefix
[4dff800]5267 const newSessionId = generateSessionId();
5268 sessions.set(newSessionId, `personal_${userId}`);
5269
[33517cc]5270 console.log(`Password changed successfully for ${userType} ${userId}, redirecting to ${finalRedirect}`);
[4dff800]5271
5272 database.logAudit(userId, 'FORCED_PASSWORD_CHANGE', 'auth', userId.toString(),
5273 `${userType} forced password change completed`, ipAddress);
5274
[79fff4f]5275 // Set the cookie with proper options - extended to 24 hours
[4dff800]5276 res.writeHead(200, {
5277 'Content-Type': 'application/json',
[79fff4f]5278 'Set-Cookie': `sessionId=${newSessionId}; HttpOnly; Path=/; Max-Age=86400; SameSite=Strict`
[4dff800]5279 });
[79fff4f]5280
[4dff800]5281 res.end(JSON.stringify({
5282 success: true,
5283 message: 'Password changed successfully.',
[79fff4f]5284 redirectTo: finalRedirect,
[4dff800]5285 userType: userType
5286 }));
5287 }
5288 );
5289 }
5290 );
5291 } else {
5292 // User not found in any table
5293 console.error('User not found in any table with ID:', userId);
5294 res.writeHead(404, { 'Content-Type': 'application/json' });
5295 res.end(JSON.stringify({ success: false, message: 'User not found' }));
5296 }
5297 });
5298 }
[69f2a41]5299 });
[4dff800]5300 } catch (parseError) {
5301 console.error('JSON parse error:', parseError);
5302 res.writeHead(400, { 'Content-Type': 'application/json' });
5303 res.end(JSON.stringify({ success: false, message: 'Invalid request format' }));
5304 }
[69f2a41]5305 });
5306 }
5307
5308 else if (pathname === '/api/register-employee' && req.method === 'POST') {
5309 requireAuth(req, res, (userId) => {
5310 const userIdStr = String(userId);
5311
[4dff800]5312 // Check if this is the admin user
5313 if (userIdStr === '000000') {
5314 res.writeHead(403, { 'Content-Type': 'application/json' });
5315 res.end(JSON.stringify({ success: false, message: 'Only store owners can register employees' }));
5316 return;
5317 }
5318
[69f2a41]5319 if (!userIdStr.startsWith('personal_')) {
5320 res.writeHead(403, { 'Content-Type': 'application/json' });
5321 res.end(JSON.stringify({ success: false, message: 'Only store owners can register employees' }));
5322 return;
5323 }
5324
5325 const personalId = userIdStr.replace('personal_', '');
5326
5327 database.database.get(
[33517cc]5328 'SELECT boss_id FROM boss WHERE boss_id = $1',
[69f2a41]5329 [personalId],
5330 (err, boss) => {
5331 if (err || !boss) {
5332 res.writeHead(403, { 'Content-Type': 'application/json' });
5333 res.end(JSON.stringify({ success: false, message: 'Only store owners can register employees' }));
5334 return;
5335 }
5336
5337 let body = '';
5338 req.on('data', chunk => {
5339 body += chunk.toString();
5340 });
5341 req.on('end', () => {
5342 const { firstName, lastName, ssn, email, password, storeId, dateOfHire } = JSON.parse(body);
5343
5344 if (!firstName || !lastName || !ssn || !email || !password || !storeId || !dateOfHire) {
5345 res.writeHead(400, { 'Content-Type': 'application/json' });
5346 res.end(JSON.stringify({ success: false, message: 'All fields are required' }));
5347 return;
5348 }
5349
5350 if (!/^\d{13}$/.test(ssn)) {
5351 res.writeHead(400, { 'Content-Type': 'application/json' });
5352 res.end(JSON.stringify({ success: false, message: 'SSN must be exactly 13 digits' }));
5353 return;
5354 }
5355
5356 if (!validateEmail(email)) {
5357 res.writeHead(400, { 'Content-Type': 'application/json' });
5358 res.end(JSON.stringify({ success: false, message: 'Invalid email format' }));
5359 return;
5360 }
5361
5362 if (!validatePassword(password)) {
5363 res.writeHead(400, { 'Content-Type': 'application/json' });
5364 res.end(JSON.stringify({
5365 success: false,
5366 message: 'Password must have at least 8 characters, including uppercase, lowercase, number and special character'
5367 }));
5368 return;
5369 }
5370
5371 database.getPersonalByEmail(email, (err, existingPersonal) => {
5372 if (err) {
5373 console.error('Error checking personal:', err);
5374 res.writeHead(500, { 'Content-Type': 'application/json' });
5375 res.end(JSON.stringify({ success: false, message: 'Server error checking personal' }));
5376 return;
5377 }
5378
5379 if (existingPersonal) {
5380 res.writeHead(400, { 'Content-Type': 'application/json' });
5381 res.end(JSON.stringify({ success: false, message: 'Email is already registered' }));
5382 return;
5383 }
5384
5385 // Find the next available employee number for this store
5386 database.database.all(
5387 "SELECT id FROM personal WHERE id LIKE '" + storeId + "%' ORDER BY id",
5388 [],
5389 (err, existingEmployees) => {
5390 if (err) {
5391 console.error('Error getting employees:', err);
5392 res.writeHead(500, { 'Content-Type': 'application/json' });
5393 res.end(JSON.stringify({ success: false, message: 'Server error generating employee ID' }));
5394 return;
5395 }
5396
5397 // Find the first available employee number from 001 to 999
5398 let nextEmployeeNum = 1;
5399 const existingNumbers = (existingEmployees || [])
5400 .map(e => {
5401 const num = e.id.substring(3);
5402 return parseInt(num, 10);
5403 })
5404 .filter(num => !isNaN(num));
5405
5406 existingNumbers.sort((a, b) => a - b);
5407
5408 // Find the first gap in the sequence
5409 for (let i = 1; i <= 999; i++) {
5410 if (!existingNumbers.includes(i)) {
5411 nextEmployeeNum = i;
5412 break;
5413 }
5414 }
5415
5416 if (nextEmployeeNum > 999) {
5417 res.writeHead(400, { 'Content-Type': 'application/json' });
5418 res.end(JSON.stringify({ success: false, message: 'Maximum employees reached for this store' }));
5419 return;
5420 }
5421
5422 const employeeNumPadded = nextEmployeeNum.toString().padStart(3, '0');
5423 const newPersonalId = storeId + employeeNumPadded;
5424
5425 database.database.run('BEGIN TRANSACTION', (err) => {
5426 if (err) {
5427 console.error('Error beginning transaction:', err);
5428 res.writeHead(500, { 'Content-Type': 'application/json' });
5429 res.end(JSON.stringify({ success: false, message: 'Server error during registration' }));
5430 return;
5431 }
5432
5433 database.database.run(
[33517cc]5434 'INSERT INTO personal (id, first_name, last_name, ssn, email, password) VALUES ($1, $2, $3, $4, $5, $6)',
[69f2a41]5435 [
5436 newPersonalId,
5437 firstName,
5438 lastName,
5439 ssn,
5440 email,
5441 bcrypt.hashSync(password, 10)
5442 ],
5443 function(err) {
5444 if (err) {
5445 database.database.run('ROLLBACK');
5446 console.error('Error inserting personal:', err);
[79fff4f]5447
[69f2a41]5448 if (err.code === '23505') {
5449 res.writeHead(400, { 'Content-Type': 'application/json' });
[79fff4f]5450 res.end(JSON.stringify({
5451 success: false,
5452 message: 'This personal ID is already taken. Please try again.'
5453 }));
[69f2a41]5454 } else {
5455 res.writeHead(400, { 'Content-Type': 'application/json' });
5456 res.end(JSON.stringify({ success: false, message: 'Error registering employee' }));
5457 }
5458 return;
5459 }
5460
5461 database.database.run(
[33517cc]5462 'INSERT INTO employees (employee_id, date_of_hire) VALUES ($1, $2)',
[69f2a41]5463 [newPersonalId, dateOfHire],
5464 (err) => {
5465 if (err) {
5466 database.database.run('ROLLBACK');
5467 console.error('Error inserting employee:', err);
5468 res.writeHead(400, { 'Content-Type': 'application/json' });
5469 res.end(JSON.stringify({ success: false, message: 'Error registering as employee' }));
5470 return;
5471 }
5472
5473 database.database.run(
[33517cc]5474 'INSERT INTO works_in_store (personal_id, store_id) VALUES ($1, $2)',
[69f2a41]5475 [newPersonalId, storeId],
5476 (err) => {
5477 if (err) {
5478 database.database.run('ROLLBACK');
5479 console.error('Error inserting works_in_store:', err);
5480 res.writeHead(400, { 'Content-Type': 'application/json' });
5481 res.end(JSON.stringify({ success: false, message: 'Error assigning employee to store' }));
5482 return;
5483 }
5484
5485 database.database.run(
[33517cc]5486 'INSERT INTO permissions (personal_id, type, authorisation) VALUES ($1, $2, $3)',
[69f2a41]5487 [newPersonalId, 'EMPLOYEE', 'limited_access'],
5488 (err) => {
5489 if (err) {
5490 console.error('Error inserting permissions:', err);
5491 }
5492
[79fff4f]5493 // Also create entry in users table for login with force_password_change = 1
5494 database.database.run(
[33517cc]5495 'INSERT INTO users (id, username, email, password, user_type, force_password_change) VALUES ($1, $2, $3, $4, $5, $6)',
[79fff4f]5496 [
5497 newPersonalId,
5498 `${firstName} ${lastName}`,
5499 email,
5500 bcrypt.hashSync(password, 10),
5501 'store_employee',
5502 1
5503 ],
5504 (err) => {
5505 if (err) {
5506 console.error('Error creating user entry for employee:', err);
5507 }
[69f2a41]5508
[79fff4f]5509 database.database.run('COMMIT', (err) => {
5510 if (err) {
5511 database.database.run('ROLLBACK');
5512 console.error('Error committing transaction:', err);
5513 res.writeHead(500, { 'Content-Type': 'application/json' });
5514 res.end(JSON.stringify({ success: false, message: 'Error completing registration' }));
5515 return;
5516 }
[69f2a41]5517
[79fff4f]5518 database.logAudit(personalId, 'EMPLOYEE_REGISTERED', 'employee', newPersonalId, `Employee registered: ${firstName} ${lastName}`, ipAddress);
5519
5520 res.writeHead(200, { 'Content-Type': 'application/json' });
5521 res.end(JSON.stringify({
5522 success: true,
5523 message: 'Employee registered successfully!',
5524 employeeId: newPersonalId,
5525 name: `${firstName} ${lastName}`
5526 }));
5527 });
5528 }
5529 );
[69f2a41]5530 }
5531 );
5532 }
5533 );
5534 }
5535 );
5536 }
5537 );
5538 });
5539 }
5540 );
5541 });
5542 });
5543 }
5544 );
5545 });
5546 }
5547
5548 else if (pathname === '/api/delete-employee' && req.method === 'POST') {
5549 requireAuth(req, res, (userId) => {
5550 const userIdStr = String(userId);
5551
[4dff800]5552 // Check if this is the admin user
5553 if (userIdStr === '000000') {
5554 res.writeHead(403, { 'Content-Type': 'application/json' });
5555 res.end(JSON.stringify({ success: false, message: 'Only store owners can delete employees' }));
5556 return;
5557 }
5558
[69f2a41]5559 if (!userIdStr.startsWith('personal_')) {
5560 res.writeHead(403, { 'Content-Type': 'application/json' });
5561 res.end(JSON.stringify({ success: false, message: 'Only store owners can delete employees' }));
5562 return;
5563 }
5564
5565 const personalId = userIdStr.replace('personal_', '');
5566
5567 database.database.get(
[33517cc]5568 'SELECT boss_id FROM boss WHERE boss_id = $1',
[69f2a41]5569 [personalId],
5570 (err, boss) => {
5571 if (err || !boss) {
5572 res.writeHead(403, { 'Content-Type': 'application/json' });
5573 res.end(JSON.stringify({ success: false, message: 'Only store owners can delete employees' }));
5574 return;
5575 }
5576
5577 let body = '';
5578 req.on('data', chunk => {
5579 body += chunk.toString();
5580 });
5581 req.on('end', () => {
5582 const { employeeId, storeId } = JSON.parse(body);
5583
5584 if (!employeeId || !storeId) {
5585 res.writeHead(400, { 'Content-Type': 'application/json' });
5586 res.end(JSON.stringify({ success: false, message: 'Employee ID and Store ID are required' }));
5587 return;
5588 }
5589
5590 database.database.get(
[33517cc]5591 'SELECT store_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]5592 [personalId, storeId],
5593 (err, bossStore) => {
5594 if (err || !bossStore) {
5595 res.writeHead(403, { 'Content-Type': 'application/json' });
5596 res.end(JSON.stringify({ success: false, message: 'You are not authorized to manage employees in this store' }));
5597 return;
5598 }
5599
5600 database.database.get(
[33517cc]5601 'SELECT personal_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]5602 [employeeId, storeId],
5603 (err, employeeStore) => {
5604 if (err || !employeeStore) {
5605 res.writeHead(404, { 'Content-Type': 'application/json' });
5606 res.end(JSON.stringify({ success: false, message: 'Employee not found in this store' }));
5607 return;
5608 }
5609
5610 database.database.get(
[33517cc]5611 'SELECT boss_id FROM boss WHERE boss_id = $1',
[69f2a41]5612 [employeeId],
5613 (err, isBoss) => {
5614 if (err) {
5615 console.error('Error checking if employee is boss:', err);
5616 }
5617
5618 if (isBoss) {
5619 res.writeHead(403, { 'Content-Type': 'application/json' });
5620 res.end(JSON.stringify({ success: false, message: 'Cannot delete store owners' }));
5621 return;
5622 }
5623
5624 database.database.run('BEGIN TRANSACTION', (err) => {
5625 if (err) {
5626 console.error('Error beginning transaction:', err);
5627 res.writeHead(500, { 'Content-Type': 'application/json' });
5628 res.end(JSON.stringify({ success: false, message: 'Server error during deletion' }));
5629 return;
5630 }
5631
5632 database.database.run(
[33517cc]5633 'DELETE FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]5634 [employeeId, storeId],
5635 (err) => {
5636 if (err) {
5637 database.database.run('ROLLBACK');
5638 console.error('Error deleting from works_in_store:', err);
5639 res.writeHead(500, { 'Content-Type': 'application/json' });
5640 res.end(JSON.stringify({ success: false, message: 'Error removing employee from store' }));
5641 return;
5642 }
5643
5644 database.database.run(
[33517cc]5645 'DELETE FROM employees WHERE employee_id = $1',
[69f2a41]5646 [employeeId],
5647 (err) => {
5648 if (err) {
5649 console.error('Error deleting from employees:', err);
5650 }
5651
5652 database.database.run(
[33517cc]5653 'DELETE FROM permissions WHERE personal_id = $1',
[69f2a41]5654 [employeeId],
5655 (err) => {
5656 if (err) {
5657 console.error('Error deleting from permissions:', err);
5658 }
5659
5660 database.database.run(
[33517cc]5661 'DELETE FROM personal WHERE id = $1',
[69f2a41]5662 [employeeId],
5663 (err) => {
5664 if (err) {
5665 console.error('Error deleting from personal:', err);
5666 }
5667
[79fff4f]5668 // Also delete from users table
5669 database.database.run(
[33517cc]5670 'DELETE FROM users WHERE id = $1',
[79fff4f]5671 [employeeId],
5672 (err) => {
5673 if (err) {
5674 console.error('Error deleting from users:', err);
5675 }
5676
5677 database.database.run('COMMIT', (commitErr) => {
5678 if (commitErr) {
5679 database.database.run('ROLLBACK');
5680 console.error('Error committing transaction:', commitErr);
5681 res.writeHead(500, { 'Content-Type': 'application/json' });
5682 res.end(JSON.stringify({ success: false, message: 'Error completing deletion' }));
5683 return;
5684 }
5685
5686 database.logAudit(personalId, 'EMPLOYEE_DELETED', 'employee', employeeId, `Employee deleted from store ${storeId}`, ipAddress);
5687
5688 res.writeHead(200, { 'Content-Type': 'application/json' });
5689 res.end(JSON.stringify({
5690 success: true,
5691 message: 'Employee deleted successfully'
5692 }));
5693 });
[69f2a41]5694 }
[79fff4f]5695 );
[69f2a41]5696 }
5697 );
5698 }
5699 );
5700 }
5701 );
5702 }
5703 );
5704 });
5705 }
5706 );
5707 }
5708 );
5709 }
5710 );
5711 });
5712 }
5713 );
5714 });
5715 }
5716
5717 else if (pathname === '/api/update-employee-status' && req.method === 'POST') {
5718 requireAuth(req, res, (userId) => {
5719 const userIdStr = String(userId);
5720
[4dff800]5721 // Check if this is the admin user
5722 if (userIdStr === '000000') {
5723 res.writeHead(403, { 'Content-Type': 'application/json' });
5724 res.end(JSON.stringify({ success: false, message: 'Only store owners can update employee status' }));
5725 return;
5726 }
5727
[69f2a41]5728 if (!userIdStr.startsWith('personal_')) {
5729 res.writeHead(403, { 'Content-Type': 'application/json' });
5730 res.end(JSON.stringify({ success: false, message: 'Only store owners can update employee status' }));
5731 return;
5732 }
5733
5734 const personalId = userIdStr.replace('personal_', '');
5735
5736 database.database.get(
[33517cc]5737 'SELECT boss_id FROM boss WHERE boss_id = $1',
[69f2a41]5738 [personalId],
5739 (err, boss) => {
5740 if (err || !boss) {
5741 res.writeHead(403, { 'Content-Type': 'application/json' });
5742 res.end(JSON.stringify({ success: false, message: 'Only store owners can update employee status' }));
5743 return;
5744 }
5745
5746 let body = '';
5747 req.on('data', chunk => {
5748 body += chunk.toString();
5749 });
5750 req.on('end', () => {
5751 const { employeeId, storeId, status } = JSON.parse(body);
5752
5753 if (!employeeId || !storeId || !status) {
5754 res.writeHead(400, { 'Content-Type': 'application/json' });
5755 res.end(JSON.stringify({ success: false, message: 'Employee ID, Store ID and Status are required' }));
5756 return;
5757 }
5758
5759 database.database.get(
[33517cc]5760 'SELECT store_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]5761 [personalId, storeId],
5762 (err, bossStore) => {
5763 if (err || !bossStore) {
5764 res.writeHead(403, { 'Content-Type': 'application/json' });
5765 res.end(JSON.stringify({ success: false, message: 'You are not authorized to manage employees in this store' }));
5766 return;
5767 }
5768
5769 database.database.get(
[33517cc]5770 'SELECT personal_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]5771 [employeeId, storeId],
5772 (err, employeeStore) => {
5773 if (err || !employeeStore) {
5774 res.writeHead(404, { 'Content-Type': 'application/json' });
5775 res.end(JSON.stringify({ success: false, message: 'Employee not found in this store' }));
5776 return;
5777 }
5778
5779 let permissionType = 'EMPLOYEE';
5780 let authorization = 'limited_access';
5781
5782 if (status === 'promoted') {
5783 permissionType = 'MANAGER';
5784 authorization = 'extended_access';
5785 } else if (status === 'suspended') {
5786 permissionType = 'SUSPENDED';
5787 authorization = 'no_access';
5788 } else if (status === 'active') {
5789 permissionType = 'EMPLOYEE';
5790 authorization = 'limited_access';
5791 }
5792
5793 database.database.run(
[33517cc]5794 'UPDATE permissions SET type = $1, authorisation = $2 WHERE personal_id = $3',
[69f2a41]5795 [permissionType, authorization, employeeId],
5796 function(err) {
5797 if (err) {
5798 console.error('Error updating employee status:', err);
5799 res.writeHead(500, { 'Content-Type': 'application/json' });
5800 res.end(JSON.stringify({ success: false, message: 'Error updating employee status' }));
5801 return;
5802 }
5803
5804 database.logAudit(personalId, 'EMPLOYEE_STATUS_UPDATED', 'employee', employeeId, `Employee status updated to: ${status}`, ipAddress);
5805
5806 res.writeHead(200, { 'Content-Type': 'application/json' });
5807 res.end(JSON.stringify({
5808 success: true,
5809 message: `Employee status updated to ${status} successfully`
5810 }));
5811 }
5812 );
5813 }
5814 );
5815 }
5816 );
5817 });
5818 }
5819 );
5820 });
5821 }
5822
5823 else if (pathname === '/api/update-employee' && req.method === 'POST') {
5824 requireAuth(req, res, (userId) => {
5825 const userIdStr = String(userId);
5826
[4dff800]5827 // Check if this is the admin user
5828 if (userIdStr === '000000') {
5829 res.writeHead(403, { 'Content-Type': 'application/json' });
5830 res.end(JSON.stringify({ success: false, message: 'Only store owners can update employees' }));
5831 return;
5832 }
5833
[69f2a41]5834 if (!userIdStr.startsWith('personal_')) {
5835 res.writeHead(403, { 'Content-Type': 'application/json' });
5836 res.end(JSON.stringify({ success: false, message: 'Only store owners can update employees' }));
5837 return;
5838 }
5839
5840 const personalId = userIdStr.replace('personal_', '');
5841
5842 database.database.get(
[33517cc]5843 'SELECT boss_id FROM boss WHERE boss_id = $1',
[69f2a41]5844 [personalId],
5845 (err, boss) => {
5846 if (err || !boss) {
5847 res.writeHead(403, { 'Content-Type': 'application/json' });
5848 res.end(JSON.stringify({ success: false, message: 'Only store owners can update employees' }));
5849 return;
5850 }
5851
5852 let body = '';
5853 req.on('data', chunk => {
5854 body += chunk.toString();
5855 });
5856 req.on('end', () => {
5857 const { employeeId, storeId, firstName, lastName, email } = JSON.parse(body);
5858
5859 if (!employeeId || !storeId) {
5860 res.writeHead(400, { 'Content-Type': 'application/json' });
5861 res.end(JSON.stringify({ success: false, message: 'Employee ID and Store ID are required' }));
5862 return;
5863 }
5864
5865 database.database.get(
[33517cc]5866 'SELECT store_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]5867 [personalId, storeId],
5868 (err, bossStore) => {
5869 if (err || !bossStore) {
5870 res.writeHead(403, { 'Content-Type': 'application/json' });
5871 res.end(JSON.stringify({ success: false, message: 'You are not authorized to manage employees in this store' }));
5872 return;
5873 }
5874
5875 database.database.get(
[33517cc]5876 'SELECT personal_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]5877 [employeeId, storeId],
5878 (err, employeeStore) => {
5879 if (err || !employeeStore) {
5880 res.writeHead(404, { 'Content-Type': 'application/json' });
5881 res.end(JSON.stringify({ success: false, message: 'Employee not found in this store' }));
5882 return;
5883 }
5884
5885 const updates = [];
5886 const params = [];
5887
5888 if (firstName) {
[33517cc]5889 updates.push(`first_name = $${params.length + 1}`);
[69f2a41]5890 params.push(firstName);
5891 }
5892
5893 if (lastName) {
[33517cc]5894 updates.push(`last_name = $${params.length + 1}`);
[69f2a41]5895 params.push(lastName);
5896 }
5897
5898 if (email) {
5899 if (!validateEmail(email)) {
5900 res.writeHead(400, { 'Content-Type': 'application/json' });
5901 res.end(JSON.stringify({ success: false, message: 'Invalid email format' }));
5902 return;
5903 }
[33517cc]5904 updates.push(`email = $${params.length + 1}`);
[69f2a41]5905 params.push(email);
5906 }
5907
5908 if (updates.length === 0) {
5909 res.writeHead(400, { 'Content-Type': 'application/json' });
5910 res.end(JSON.stringify({ success: false, message: 'No fields to update' }));
5911 return;
5912 }
5913
5914 params.push(employeeId);
5915
5916 database.database.run(
[33517cc]5917 `UPDATE personal SET ${updates.join(', ')} WHERE id = $${params.length}`,
[69f2a41]5918 params,
5919 function(err) {
5920 if (err) {
5921 console.error('Error updating employee:', err);
5922 res.writeHead(500, { 'Content-Type': 'application/json' });
5923 res.end(JSON.stringify({ success: false, message: 'Error updating employee information' }));
5924 return;
5925 }
5926
[79fff4f]5927 // Also update in users table if email was changed
5928 if (email) {
5929 database.database.run(
[33517cc]5930 'UPDATE users SET email = $1 WHERE id = $2',
[79fff4f]5931 [email, employeeId],
5932 (err) => {
5933 if (err) {
5934 console.error('Error updating user email:', err);
5935 }
5936 }
5937 );
5938 }
5939
5940 if (firstName || lastName) {
5941 database.database.get(
[33517cc]5942 'SELECT first_name, last_name FROM personal WHERE id = $1',
[79fff4f]5943 [employeeId],
5944 (err, personal) => {
5945 if (!err && personal) {
5946 const newUsername = `${personal.first_name} ${personal.last_name}`;
5947 database.database.run(
[33517cc]5948 'UPDATE users SET username = $1 WHERE id = $2',
[79fff4f]5949 [newUsername, employeeId],
5950 (err) => {
5951 if (err) {
5952 console.error('Error updating user username:', err);
5953 }
5954 }
5955 );
5956 }
5957 }
5958 );
5959 }
5960
[69f2a41]5961 database.logAudit(personalId, 'EMPLOYEE_UPDATED', 'employee', employeeId, `Employee information updated`, ipAddress);
5962
5963 res.writeHead(200, { 'Content-Type': 'application/json' });
5964 res.end(JSON.stringify({
5965 success: true,
5966 message: 'Employee information updated successfully'
5967 }));
5968 }
5969 );
5970 }
5971 );
5972 }
5973 );
5974 });
5975 }
5976 );
5977 });
5978 }
5979
5980 else if (pathname === '/api/store-products' && req.method === 'GET') {
5981 requireStoreOwner()(req, res, (personalId) => {
5982 const storeId = parsedUrl.query.storeId;
5983
5984 if (!storeId) {
5985 database.database.get(
[33517cc]5986 'SELECT store_id FROM works_in_store WHERE personal_id = $1 LIMIT 1',
[69f2a41]5987 [personalId],
5988 (err, store) => {
5989 if (err || !store) {
5990 res.writeHead(400, { 'Content-Type': 'application/json' });
5991 res.end(JSON.stringify({ success: false, message: 'Store ID is required' }));
5992 return;
5993 }
5994
5995 database.getStoreProducts(store.store_id, (err, products) => {
5996 if (err) {
5997 res.writeHead(500, { 'Content-Type': 'application/json' });
5998 res.end(JSON.stringify({ success: false, message: 'Error fetching store products' }));
5999 } else {
6000 res.writeHead(200, { 'Content-Type': 'application/json' });
6001 res.end(JSON.stringify({ success: true, products }));
6002 }
6003 });
6004 }
6005 );
[4dff800]6006
[69f2a41]6007 return;
6008 }
6009
6010 database.database.get(
[33517cc]6011 'SELECT store_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]6012 [personalId, storeId],
6013 (err, ownsStore) => {
6014 if (err || !ownsStore) {
6015 res.writeHead(403, { 'Content-Type': 'application/json' });
6016 res.end(JSON.stringify({ success: false, message: 'You are not authorized to view products in this store' }));
6017 return;
6018 }
6019
6020 database.getStoreProducts(storeId, (err, products) => {
6021 if (err) {
6022 res.writeHead(500, { 'Content-Type': 'application/json' });
6023 res.end(JSON.stringify({ success: false, message: 'Error fetching store products' }));
6024 } else {
6025 res.writeHead(200, { 'Content-Type': 'application/json' });
6026 res.end(JSON.stringify({ success: true, products }));
6027 }
6028 });
6029 }
6030 );
6031 });
6032 }
6033
6034 else if (pathname === '/api/store-orders' && req.method === 'GET') {
6035 requireStoreOwner()(req, res, (personalId) => {
6036 const storeId = parsedUrl.query.storeId;
6037
6038 if (!storeId) {
6039 database.database.get(
[33517cc]6040 'SELECT store_id FROM works_in_store WHERE personal_id = $1 LIMIT 1',
[69f2a41]6041 [personalId],
6042 (err, store) => {
6043 if (err || !store) {
6044 res.writeHead(400, { 'Content-Type': 'application/json' });
6045 res.end(JSON.stringify({ success: false, message: 'Store ID is required' }));
6046 return;
6047 }
6048
6049 database.getStoreOrders(store.store_id, (err, orders) => {
6050 if (err) {
6051 res.writeHead(500, { 'Content-Type': 'application/json' });
6052 res.end(JSON.stringify({ success: false, message: 'Error fetching store orders' }));
6053 } else {
6054 res.writeHead(200, { 'Content-Type': 'application/json' });
6055 res.end(JSON.stringify({ success: true, orders }));
6056 }
6057 });
6058 }
6059 );
[4dff800]6060
[69f2a41]6061 return;
6062 }
6063
6064 database.database.get(
[33517cc]6065 'SELECT store_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]6066 [personalId, storeId],
6067 (err, ownsStore) => {
6068 if (err || !ownsStore) {
6069 res.writeHead(403, { 'Content-Type': 'application/json' });
6070 res.end(JSON.stringify({ success: false, message: 'You are not authorized to view orders in this store' }));
6071 return;
6072 }
6073
6074 database.getStoreOrders(storeId, (err, orders) => {
6075 if (err) {
6076 res.writeHead(500, { 'Content-Type': 'application/json' });
6077 res.end(JSON.stringify({ success: false, message: 'Error fetching store orders' }));
6078 } else {
6079 res.writeHead(200, { 'Content-Type': 'application/json' });
6080 res.end(JSON.stringify({ success: true, orders }));
6081 }
6082 });
6083 }
6084 );
6085 });
6086 }
6087
6088 else if (pathname === '/api/store-employees' && req.method === 'GET') {
6089 requireStoreOwner()(req, res, (personalId) => {
6090 const storeId = parsedUrl.query.storeId;
6091
6092 if (!storeId) {
6093 database.database.get(
[33517cc]6094 'SELECT store_id FROM works_in_store WHERE personal_id = $1 LIMIT 1',
[69f2a41]6095 [personalId],
6096 (err, store) => {
6097 if (err || !store) {
6098 res.writeHead(400, { 'Content-Type': 'application/json' });
6099 res.end(JSON.stringify({ success: false, message: 'Store ID is required' }));
6100 return;
6101 }
6102
6103 database.getStoreEmployees(store.store_id, (err, employees) => {
6104 if (err) {
6105 res.writeHead(500, { 'Content-Type': 'application/json' });
6106 res.end(JSON.stringify({ success: false, message: 'Error fetching store employees' }));
6107 } else {
6108 res.writeHead(200, { 'Content-Type': 'application/json' });
6109 res.end(JSON.stringify({ success: true, employees }));
6110 }
6111 });
6112 }
6113 );
[4dff800]6114
[69f2a41]6115 return;
6116 }
6117
6118 database.database.get(
[33517cc]6119 'SELECT store_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]6120 [personalId, storeId],
6121 (err, ownsStore) => {
6122 if (err || !ownsStore) {
6123 res.writeHead(403, { 'Content-Type': 'application/json' });
6124 res.end(JSON.stringify({ success: false, message: 'You are not authorized to view employees in this store' }));
6125 return;
6126 }
6127
6128 database.getStoreEmployees(storeId, (err, employees) => {
6129 if (err) {
6130 res.writeHead(500, { 'Content-Type': 'application/json' });
6131 res.end(JSON.stringify({ success: false, message: 'Error fetching store employees' }));
6132 } else {
6133 res.writeHead(200, { 'Content-Type': 'application/json' });
6134 res.end(JSON.stringify({ success: true, employees }));
6135 }
6136 });
6137 }
6138 );
6139 });
6140 }
6141
6142 else if (pathname === '/api/store-reports' && req.method === 'GET') {
6143 requireStoreOwner()(req, res, (personalId) => {
6144 const storeId = parsedUrl.query.storeId;
6145
6146 if (!storeId) {
6147 database.database.get(
[33517cc]6148 'SELECT store_id FROM works_in_store WHERE personal_id = $1 LIMIT 1',
[69f2a41]6149 [personalId],
6150 (err, store) => {
6151 if (err || !store) {
6152 res.writeHead(400, { 'Content-Type': 'application/json' });
6153 res.end(JSON.stringify({ success: false, message: 'Store ID is required' }));
6154 return;
6155 }
6156
6157 database.getStoreReports(store.store_id, (err, reports) => {
6158 if (err) {
6159 res.writeHead(500, { 'Content-Type': 'application/json' });
6160 res.end(JSON.stringify({ success: false, message: 'Error fetching store reports' }));
6161 } else {
6162 res.writeHead(200, { 'Content-Type': 'application/json' });
6163 res.end(JSON.stringify({ success: true, reports }));
6164 }
6165 });
6166 }
6167 );
[4dff800]6168
[69f2a41]6169 return;
6170 }
6171
6172 database.database.get(
[33517cc]6173 'SELECT store_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]6174 [personalId, storeId],
6175 (err, ownsStore) => {
6176 if (err || !ownsStore) {
6177 res.writeHead(403, { 'Content-Type': 'application/json' });
6178 res.end(JSON.stringify({ success: false, message: 'You are not authorized to view reports in this store' }));
6179 return;
6180 }
6181
6182 database.getStoreReports(storeId, (err, reports) => {
6183 if (err) {
6184 res.writeHead(500, { 'Content-Type': 'application/json' });
6185 res.end(JSON.stringify({ success: false, message: 'Error fetching store reports' }));
6186 } else {
6187 res.writeHead(200, { 'Content-Type': 'application/json' });
6188 res.end(JSON.stringify({ success: true, reports }));
6189 }
6190 });
6191 }
6192 );
6193 });
6194 }
6195
6196 else if (pathname === '/api/store-stats' && req.method === 'GET') {
6197 requireStoreOwner()(req, res, (personalId) => {
6198 const storeId = parsedUrl.query.storeId;
6199
6200 if (!storeId) {
6201 database.database.get(
[33517cc]6202 'SELECT store_id FROM works_in_store WHERE personal_id = $1 LIMIT 1',
[69f2a41]6203 [personalId],
6204 (err, store) => {
6205 if (err || !store) {
6206 res.writeHead(400, { 'Content-Type': 'application/json' });
6207 res.end(JSON.stringify({ success: false, message: 'Store ID is required' }));
6208 return;
6209 }
6210
6211 database.getStoreStats(store.store_id, (err, stats) => {
6212 if (err) {
6213 res.writeHead(500, { 'Content-Type': 'application/json' });
6214 res.end(JSON.stringify({ success: false, message: 'Error fetching store statistics' }));
6215 } else {
6216 res.writeHead(200, { 'Content-Type': 'application/json' });
6217 res.end(JSON.stringify({ success: true, stats }));
6218 }
6219 });
6220 }
6221 );
[4dff800]6222
[69f2a41]6223 return;
6224 }
6225
6226 database.database.get(
[33517cc]6227 'SELECT store_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]6228 [personalId, storeId],
6229 (err, ownsStore) => {
6230 if (err || !ownsStore) {
6231 res.writeHead(403, { 'Content-Type': 'application/json' });
6232 res.end(JSON.stringify({ success: false, message: 'You are not authorized to view statistics in this store' }));
6233 return;
6234 }
6235
6236 database.getStoreStats(storeId, (err, stats) => {
6237 if (err) {
6238 res.writeHead(500, { 'Content-Type': 'application/json' });
6239 res.end(JSON.stringify({ success: false, message: 'Error fetching store statistics' }));
6240 } else {
6241 res.writeHead(200, { 'Content-Type': 'application/json' });
6242 res.end(JSON.stringify({ success: true, stats }));
6243 }
6244 });
6245 }
6246 );
6247 });
6248 }
6249
[6c7cfa6]6250 else if (pathname === '/api/advanced-reports' && req.method === 'GET') {
6251 requireRole('admin')(req, res, () => {
6252 const reportName = parsedUrl.query.report;
6253
6254 if (!reportName) {
6255 res.writeHead(400, { 'Content-Type': 'application/json' });
6256 res.end(JSON.stringify({
6257 success: false,
6258 message: 'The report query parameter is required'
6259 }));
6260 return;
6261 }
6262
6263 let params = [];
6264
6265 if (reportName === 'get_low_stock_high_demand_products') {
6266 const stockThreshold = Number(parsedUrl.query.stockThreshold ?? 5);
6267 const demandThreshold = Number(parsedUrl.query.demandThreshold ?? 5);
6268
6269 if (!Number.isInteger(stockThreshold) || !Number.isInteger(demandThreshold)) {
6270 res.writeHead(400, { 'Content-Type': 'application/json' });
6271 res.end(JSON.stringify({
6272 success: false,
6273 message: 'stockThreshold and demandThreshold must be integers'
6274 }));
6275 return;
6276 }
6277
6278 params = [stockThreshold, demandThreshold];
6279 }
6280
6281 database.runReport(reportName, params, (err, rows) => {
6282 if (err) {
6283 console.error('Error executing report:', err);
6284 res.writeHead(500, { 'Content-Type': 'application/json' });
6285 res.end(JSON.stringify({
6286 success: false,
6287 message: 'Error executing report: ' + err.message
6288 }));
6289 return;
6290 }
6291
6292 res.writeHead(200, { 'Content-Type': 'application/json' });
6293 res.end(JSON.stringify({
6294 success: true,
6295 report: reportName,
6296 rows
6297 }));
6298 });
6299 });
6300 }
6301
[69f2a41]6302 else if (pathname === '/api/employee-tasks' && req.method === 'GET') {
6303 requireAuth(req, res, (userId) => {
6304 const userIdStr = String(userId);
6305
[4dff800]6306 // Check if this is the admin user
6307 if (userIdStr === '000000') {
6308 res.writeHead(403, { 'Content-Type': 'application/json' });
6309 res.end(JSON.stringify({ success: false, message: 'Only store employees can access this endpoint' }));
6310 return;
6311 }
6312
[69f2a41]6313 if (!userIdStr.startsWith('personal_')) {
6314 res.writeHead(403, { 'Content-Type': 'application/json' });
6315 res.end(JSON.stringify({ success: false, message: 'Only store employees can access this endpoint' }));
6316 return;
6317 }
6318
6319 const personalId = userIdStr.replace('personal_', '');
6320 const storeId = parsedUrl.query.storeId;
6321
6322 if (!storeId) {
6323 res.writeHead(400, { 'Content-Type': 'application/json' });
6324 res.end(JSON.stringify({ success: false, message: 'Store ID is required' }));
6325 return;
6326 }
6327
6328 database.getEmployeeTasks(personalId, storeId, (err, tasks) => {
6329 if (err) {
6330 res.writeHead(500, { 'Content-Type': 'application/json' });
6331 res.end(JSON.stringify({ success: false, message: 'Error fetching employee tasks' }));
6332 } else {
6333 res.writeHead(200, { 'Content-Type': 'application/json' });
6334 res.end(JSON.stringify({ success: true, tasks }));
6335 }
6336 });
6337 });
6338 }
6339
6340 else if (pathname === '/api/client-stats' && req.method === 'GET') {
6341 requireAuth(req, res, (userId) => {
6342 const userIdStr = String(userId);
6343
6344 if (!userIdStr.startsWith('client_')) {
6345 res.writeHead(403, { 'Content-Type': 'application/json' });
6346 res.end(JSON.stringify({ success: false, message: 'Only clients can access this endpoint' }));
6347 return;
6348 }
6349
6350 const clientId = parseInt(userIdStr.replace('client_', ''));
6351
6352 database.getClientStats(clientId, (err, stats) => {
6353 if (err) {
6354 res.writeHead(500, { 'Content-Type': 'application/json' });
6355 res.end(JSON.stringify({ success: false, message: 'Error fetching client statistics' }));
6356 } else {
6357 res.writeHead(200, { 'Content-Type': 'application/json' });
6358 res.end(JSON.stringify({ success: true, stats }));
6359 }
6360 });
6361 });
6362 }
6363
6364 else if (pathname === '/api/delete-product' && req.method === 'POST') {
6365 requireStoreOwner()(req, res, (personalId) => {
6366 let body = '';
6367 req.on('data', chunk => {
6368 body += chunk.toString();
6369 });
6370 req.on('end', () => {
6371 const { productCode, storeId } = JSON.parse(body);
6372
6373 if (!productCode || !storeId) {
6374 res.writeHead(400, { 'Content-Type': 'application/json' });
6375 res.end(JSON.stringify({ success: false, message: 'Product code and store ID are required' }));
6376 return;
6377 }
6378
6379 database.database.get(
[33517cc]6380 'SELECT store_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]6381 [personalId, storeId],
6382 (err, ownsStore) => {
6383 if (err || !ownsStore) {
6384 res.writeHead(403, { 'Content-Type': 'application/json' });
6385 res.end(JSON.stringify({ success: false, message: 'You are not authorized to delete products from this store' }));
6386 return;
6387 }
6388
6389 database.deleteProduct(productCode, storeId, personalId, (err) => {
6390 if (err) {
6391 console.error('Error deleting product:', err);
6392 res.writeHead(500, { 'Content-Type': 'application/json' });
6393 res.end(JSON.stringify({ success: false, message: 'Error deleting product: ' + err.message }));
6394 } else {
6395 database.logAudit(personalId, 'PRODUCT_DELETED', 'product', productCode, 'Product deleted', ipAddress);
6396 res.writeHead(200, { 'Content-Type': 'application/json' });
6397 res.end(JSON.stringify({ success: true, message: 'Product deleted successfully' }));
6398 }
6399 });
6400 }
6401 );
6402 });
6403 });
6404 }
6405
6406 else if (pathname === '/api/product-by-code' && req.method === 'GET') {
6407 requireAuth(req, res, (userId) => {
6408 const parsedUrl = url.parse(req.url, true);
6409 const productCode = parsedUrl.query.code;
6410
6411 if (!productCode) {
6412 res.writeHead(400, { 'Content-Type': 'application/json' });
6413 res.end(JSON.stringify({ success: false, message: 'Product code is required' }));
6414 return;
6415 }
6416
6417 database.getProductByCode(productCode, (err, product) => {
6418 if (err) {
6419 console.error('Error fetching product:', err);
6420 res.writeHead(500, { 'Content-Type': 'application/json' });
6421 res.end(JSON.stringify({ success: false, message: 'Error fetching product' }));
6422 } else if (!product) {
6423 res.writeHead(404, { 'Content-Type': 'application/json' });
6424 res.end(JSON.stringify({ success: false, message: 'Product not found' }));
6425 } else {
6426 res.writeHead(200, { 'Content-Type': 'application/json' });
6427 res.end(JSON.stringify({ success: true, product }));
6428 }
6429 });
6430 });
6431 }
6432
6433 else if (pathname === '/api/generate-report' && req.method === 'POST') {
6434 requireStoreOwner()(req, res, (personalId) => {
6435 let body = '';
[6c7cfa6]6436
[69f2a41]6437 req.on('data', chunk => {
6438 body += chunk.toString();
6439 });
[6c7cfa6]6440
[69f2a41]6441 req.on('end', () => {
[6c7cfa6]6442 let data;
6443
6444 try {
6445 data = JSON.parse(body);
6446 } catch (err) {
6447 res.writeHead(400, { 'Content-Type': 'application/json' });
6448 res.end(JSON.stringify({
6449 success: false,
6450 message: 'Invalid JSON request body'
6451 }));
6452 return;
6453 }
6454
6455 const {
6456 storeId,
6457 period,
6458 startDate,
6459 endDate,
6460 type,
6461 ownerSignature
6462 } = data;
[69f2a41]6463
6464 if (!storeId || !period || !startDate || !endDate || !type) {
6465 res.writeHead(400, { 'Content-Type': 'application/json' });
[6c7cfa6]6466 res.end(JSON.stringify({
6467 success: false,
6468 message: 'All fields are required'
6469 }));
[69f2a41]6470 return;
6471 }
6472
6473 database.database.get(
[33517cc]6474 'SELECT store_id FROM works_in_store WHERE personal_id = $1 AND store_id = $2',
[69f2a41]6475 [personalId, storeId],
6476 (err, ownsStore) => {
6477 if (err || !ownsStore) {
6478 res.writeHead(403, { 'Content-Type': 'application/json' });
[6c7cfa6]6479 res.end(JSON.stringify({
6480 success: false,
6481 message: 'You are not authorized to generate reports for this store'
6482 }));
[69f2a41]6483 return;
6484 }
6485
[6c7cfa6]6486 database.generateStoreReport(
6487 storeId,
6488 startDate,
6489 endDate,
6490 type,
6491 period,
6492 ownerSignature,
6493 (reportErr, report) => {
6494 if (reportErr) {
6495 console.error('Error generating report:', reportErr);
[69f2a41]6496 res.writeHead(500, { 'Content-Type': 'application/json' });
6497 res.end(JSON.stringify({
[6c7cfa6]6498 success: false,
6499 message: 'Error generating report: ' + reportErr.message
[69f2a41]6500 }));
[6c7cfa6]6501 return;
[69f2a41]6502 }
[6c7cfa6]6503
6504 const reportId =
6505 'RPT' +
6506 new Date(report.date).getTime().toString().slice(-6);
6507
6508 database.logAudit(
6509 personalId,
6510 'REPORT_GENERATED',
6511 'report',
6512 reportId,
6513 `Report generated: ${type} for ${period}`,
6514 ipAddress
6515 );
6516
6517 res.writeHead(200, { 'Content-Type': 'application/json' });
6518 res.end(JSON.stringify({
6519 success: true,
6520 message: 'Report generated successfully',
6521 reportId,
6522 report: {
6523 id: reportId,
6524 storeId: report.store_id,
6525 period,
6526 startDate,
6527 endDate,
6528 type,
6529 generatedBy: personalId,
6530 generatedAt: report.date,
6531 overallProfit: report.overall_profit,
6532 salesTrend: report.sales_trend,
6533 marketingGrowth: report.marketing_growth,
6534 ownerSignature: report.owner_signature
6535 }
6536 }));
[69f2a41]6537 }
6538 );
6539 }
6540 );
6541 });
6542 });
6543 }
6544
6545 else {
6546 res.writeHead(404, { 'Content-Type': 'text/plain' });
6547 res.end('Page not found');
6548 }
6549});
6550
6551server.listen(port, () => {
6552 console.log(`🎨 Handcraft Marketplace running at http://localhost:${port}`);
6553 console.log('👥 Roles: Admin, Store Owner, Store Employee, Registered Client, Unregistered Guest');
6554 console.log('🎯 Features: Product browsing, ordering, reviews, store management');
6555 console.log('🏪 Store Registration: Available at /register-store.html');
6556 console.log('👤 Client Registration: Available at /register.html');
[591278c]6557});
Note: See TracBrowser for help on using the repository browser.