Changeset 6c7cfa6
- Timestamp:
- 09/21/26 00:41:03 (9 days ago)
- Branches:
- main
- Children:
- 2d1ec46
- Parents:
- 6149556
- git-author:
- Klimentina Efremova <klimentina08642@…> (09/21/26 00:24:09)
- git-committer:
- Klimentina Efremova <klimentina08642@…> (09/21/26 00:41:03)
- Files:
-
- 1 added
- 11 edited
-
database.js (modified) (9 diffs)
-
node_modules/.package-lock.json (modified) (1 diff)
-
node_modules/semver/README.md (modified) (4 diffs)
-
node_modules/semver/classes/range.js (modified) (8 diffs)
-
node_modules/semver/classes/semver.js (modified) (2 diffs)
-
node_modules/semver/functions/truncate.js (added)
-
node_modules/semver/index.js (modified) (2 diffs)
-
node_modules/semver/internal/re.js (modified) (1 diff)
-
node_modules/semver/package.json (modified) (3 diffs)
-
node_modules/semver/range.bnf (modified) (1 diff)
-
node_modules/semver/ranges/subset.js (modified) (2 diffs)
-
server.js (modified) (37 diffs)
Legend:
- Unmodified
- Added
- Removed
-
database.js
r6149556 r6c7cfa6 1383 1383 1384 1384 query( 1385 `SELECT * 1386 FROM report 1387 WHERE store_id = $1 1388 ORDER BY generated_at DESC`, 1385 `SELECT 1386 r.date, 1387 r.store_id, 1388 r.overall_profit, 1389 r.sales_trend, 1390 r.marketing_growth, 1391 r.owner_signature 1392 FROM report r 1393 WHERE r.store_id = $1 1394 ORDER BY r.date DESC`, 1389 1395 [storeId], 1390 1396 (err, result) => { 1391 1392 1397 callback( 1393 1398 err, … … 1404 1409 1405 1410 query( 1406 `SELECT COUNT( *) AS total_products1407 FROM product1408 WHERE s tore_id = $1`,1411 `SELECT COUNT(DISTINCT se.product_code) AS total_products 1412 FROM sells se 1413 WHERE se.store_id = $1`, 1409 1414 [storeId], 1410 1415 (err, result) => { … … 1415 1420 } 1416 1421 1417 stats.total_products = 1418 result.rows[0] 1419 ? Number(result.rows[0].total_products) 1420 : 0; 1422 stats.total_products = Number( 1423 result.rows[0]?.total_products || 0 1424 ); 1421 1425 1422 1426 query( 1423 `SELECT COUNT(*) AS total_orders 1424 FROM "order" 1425 WHERE store_id = $1`, 1427 `SELECT COUNT(DISTINCT o.order_num) AS total_orders 1428 FROM sells se 1429 JOIN includes i 1430 ON i.product_code = se.product_code 1431 JOIN "order" o 1432 ON o.order_num = i.order_num 1433 WHERE se.store_id = $1`, 1426 1434 [storeId], 1427 1435 (err, result) => { … … 1432 1440 } 1433 1441 1434 stats.total_orders = 1435 result.rows[0] 1436 ? Number(result.rows[0].total_orders) 1437 : 0; 1442 stats.total_orders = Number( 1443 result.rows[0]?.total_orders || 0 1444 ); 1438 1445 1439 1446 query( … … 1441 1448 COALESCE( 1442 1449 SUM( 1443 oi.price * oi.quantity 1450 p.price * i.quantity 1451 * (1 - COALESCE(o.discount, 0) / 100.0) 1444 1452 ), 1445 1453 0 1446 1454 ) AS total_revenue 1447 FROM order_items oi 1455 FROM sells se 1456 JOIN product p 1457 ON p.code = se.product_code 1458 JOIN includes i 1459 ON i.product_code = se.product_code 1448 1460 JOIN "order" o 1449 ON oi.order_num = 1450 o.order_num 1451 WHERE o.store_id = $1`, 1461 ON o.order_num = i.order_num 1462 WHERE se.store_id = $1`, 1452 1463 [storeId], 1453 1464 (err, result) => { … … 1458 1469 } 1459 1470 1460 stats.total_revenue = 1461 Number( 1462 result.rows[0] 1463 .total_revenue || 0 1464 ); 1471 stats.total_revenue = Number( 1472 result.rows[0]?.total_revenue || 0 1473 ); 1465 1474 1466 1475 query( 1467 `SELECT 1468 COALESCE( 1469 AVG(r.rating), 1470 0 1471 ) AS avg_rating 1472 FROM review r 1473 JOIN product p 1474 ON r.product_code = 1475 p.code 1476 WHERE p.store_id = $1`, 1476 `WITH store_reviews AS ( 1477 SELECT DISTINCT 1478 r.order_num, 1479 r.rating 1480 FROM review r 1481 JOIN includes i 1482 ON i.order_num = r.order_num 1483 JOIN sells se 1484 ON se.product_code = i.product_code 1485 WHERE se.store_id = $1 1486 ) 1487 SELECT COALESCE(AVG(rating), 0) AS avg_rating 1488 FROM store_reviews`, 1477 1489 [storeId], 1478 1490 (err, result) => { … … 1483 1495 } 1484 1496 1485 stats.avg_rating = 1486 Number( 1487 result.rows[0] 1488 .avg_rating || 0 1489 ); 1490 1491 callback( 1492 null, 1493 stats 1497 stats.avg_rating = Number( 1498 result.rows[0]?.avg_rating || 0 1494 1499 ); 1500 1501 callback(null, stats); 1495 1502 } 1496 1503 ); … … 2293 2300 2294 2301 2302 2303 2304 /* 2305 * ============================================================ 2306 * ADVANCED REPORTS 2307 * ============================================================ 2308 */ 2309 2310 2311 const REPORT_FUNCTIONS_SQL = String.raw` 2312 -- ============================================================ 2313 -- HANDCRAFT MARKETPLACE REPORT FUNCTIONS 2314 -- PostgreSQL / exact project schema 2315 -- ============================================================ 2316 2317 CREATE OR REPLACE FUNCTION get_orders_by_total() 2318 RETURNS TABLE ( 2319 order_num VARCHAR(11), 2320 client_id INTEGER, 2321 client_name TEXT, 2322 order_quantity BIGINT, 2323 order_status VARCHAR(20), 2324 payment_method VARCHAR(250), 2325 discount NUMERIC, 2326 order_total NUMERIC 2327 ) 2328 LANGUAGE sql 2329 AS $$ 2330 SELECT 2331 o.order_num, 2332 o.client_id, 2333 CONCAT_WS(' ', c.first_name, c.last_name) AS client_name, 2334 COALESCE(SUM(i.quantity), 0)::BIGINT AS order_quantity, 2335 o.status, 2336 o.payment_method, 2337 COALESCE(o.discount, 0)::NUMERIC AS discount, 2338 ROUND( 2339 COALESCE(SUM(p.price * i.quantity), 0) 2340 * (1 - COALESCE(o.discount, 0) / 100.0), 2341 2 2342 ) AS order_total 2343 FROM "order" o 2344 LEFT JOIN client c ON c.client_id = o.client_id 2345 LEFT JOIN includes i ON i.order_num = o.order_num 2346 LEFT JOIN product p ON p.code = i.product_code 2347 GROUP BY 2348 o.order_num, o.client_id, c.first_name, c.last_name, 2349 o.status, o.payment_method, o.discount 2350 ORDER BY order_total DESC, o.order_num; 2351 $$; 2352 2353 CREATE OR REPLACE FUNCTION get_products_by_total_sales() 2354 RETURNS TABLE ( 2355 product_code VARCHAR(8), 2356 product_description VARCHAR(500), 2357 product_price NUMERIC, 2358 number_of_orders BIGINT, 2359 total_quantity_sold BIGINT, 2360 total_revenue NUMERIC 2361 ) 2362 LANGUAGE sql 2363 AS $$ 2364 SELECT 2365 p.code, 2366 p.description, 2367 p.price::NUMERIC, 2368 COUNT(DISTINCT i.order_num) AS number_of_orders, 2369 COALESCE(SUM(i.quantity), 0)::BIGINT AS total_quantity_sold, 2370 ROUND( 2371 COALESCE( 2372 SUM( 2373 p.price * i.quantity 2374 * (1 - COALESCE(o.discount, 0) / 100.0) 2375 ), 2376 0 2377 ), 2378 2 2379 ) AS total_revenue 2380 FROM product p 2381 LEFT JOIN includes i ON i.product_code = p.code 2382 LEFT JOIN "order" o ON o.order_num = i.order_num 2383 GROUP BY p.code, p.description, p.price 2384 ORDER BY number_of_orders DESC, total_quantity_sold DESC, total_revenue DESC; 2385 $$; 2386 2387 CREATE OR REPLACE FUNCTION get_low_stock_high_demand_products( 2388 p_stock_threshold INTEGER, 2389 p_demand_threshold INTEGER 2390 ) 2391 RETURNS TABLE ( 2392 product_code VARCHAR(8), 2393 product_description VARCHAR(500), 2394 current_stock INTEGER, 2395 number_of_orders BIGINT, 2396 total_quantity_sold BIGINT 2397 ) 2398 LANGUAGE sql 2399 AS $$ 2400 SELECT 2401 p.code, 2402 p.description, 2403 p.availability, 2404 COUNT(DISTINCT i.order_num), 2405 COALESCE(SUM(i.quantity), 0)::BIGINT 2406 FROM product p 2407 JOIN includes i ON i.product_code = p.code 2408 GROUP BY p.code, p.description, p.availability 2409 HAVING 2410 p.availability < p_stock_threshold 2411 AND COUNT(DISTINCT i.order_num) >= p_demand_threshold 2412 ORDER BY number_of_orders DESC, total_quantity_sold DESC, current_stock ASC; 2413 $$; 2414 2415 CREATE OR REPLACE FUNCTION get_products_monthly_sales() 2416 RETURNS TABLE ( 2417 product_code VARCHAR(8), 2418 product_description VARCHAR(500), 2419 year INTEGER, 2420 month INTEGER, 2421 number_of_orders BIGINT, 2422 total_quantity_sold BIGINT, 2423 total_revenue NUMERIC 2424 ) 2425 LANGUAGE sql 2426 AS $$ 2427 SELECT 2428 p.code, 2429 p.description, 2430 EXTRACT(YEAR FROM o.last_date_mod)::INTEGER, 2431 EXTRACT(MONTH FROM o.last_date_mod)::INTEGER, 2432 COUNT(DISTINCT o.order_num), 2433 COALESCE(SUM(i.quantity), 0)::BIGINT, 2434 ROUND( 2435 COALESCE( 2436 SUM( 2437 p.price * i.quantity 2438 * (1 - COALESCE(o.discount, 0) / 100.0) 2439 ), 2440 0 2441 ), 2442 2 2443 ) 2444 FROM product p 2445 JOIN includes i ON i.product_code = p.code 2446 JOIN "order" o ON o.order_num = i.order_num 2447 GROUP BY 2448 p.code, p.description, 2449 EXTRACT(YEAR FROM o.last_date_mod), 2450 EXTRACT(MONTH FROM o.last_date_mod) 2451 ORDER BY year DESC, month DESC, total_revenue DESC; 2452 $$; 2453 2454 CREATE OR REPLACE FUNCTION get_stores_by_last_calendar_year_revenue() 2455 RETURNS TABLE ( 2456 store_id VARCHAR(3), 2457 store_name VARCHAR(50), 2458 number_of_orders BIGINT, 2459 total_quantity_sold BIGINT, 2460 total_revenue NUMERIC 2461 ) 2462 LANGUAGE sql 2463 AS $$ 2464 SELECT 2465 s.store_id, 2466 s.name, 2467 COUNT(DISTINCT o.order_num), 2468 COALESCE(SUM(i.quantity), 0)::BIGINT, 2469 ROUND( 2470 COALESCE( 2471 SUM( 2472 p.price * i.quantity 2473 * (1 - COALESCE(o.discount, 0) / 100.0) 2474 ), 2475 0 2476 ), 2477 2 2478 ) 2479 FROM store s 2480 LEFT JOIN sells se ON se.store_id = s.store_id 2481 LEFT JOIN product p ON p.code = se.product_code 2482 LEFT JOIN includes i ON i.product_code = p.code 2483 LEFT JOIN "order" o 2484 ON o.order_num = i.order_num 2485 AND o.last_date_mod >= DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '1 year' 2486 AND o.last_date_mod < DATE_TRUNC('year', CURRENT_DATE) 2487 GROUP BY s.store_id, s.name 2488 ORDER BY total_revenue DESC, s.store_id; 2489 $$; 2490 2491 CREATE OR REPLACE FUNCTION get_products_never_ordered() 2492 RETURNS TABLE ( 2493 product_code VARCHAR(8), 2494 product_description VARCHAR(500), 2495 product_price NUMERIC, 2496 current_stock INTEGER 2497 ) 2498 LANGUAGE sql 2499 AS $$ 2500 SELECT p.code, p.description, p.price::NUMERIC, p.availability 2501 FROM product p 2502 WHERE NOT EXISTS ( 2503 SELECT 1 2504 FROM includes i 2505 WHERE i.product_code = p.code 2506 ) 2507 ORDER BY p.code; 2508 $$; 2509 2510 CREATE OR REPLACE FUNCTION get_products_by_number_of_orders() 2511 RETURNS TABLE ( 2512 product_code VARCHAR(8), 2513 product_description VARCHAR(500), 2514 product_price NUMERIC, 2515 number_of_orders BIGINT 2516 ) 2517 LANGUAGE sql 2518 AS $$ 2519 SELECT 2520 p.code, 2521 p.description, 2522 p.price::NUMERIC, 2523 COUNT(DISTINCT i.order_num) 2524 FROM product p 2525 JOIN includes i ON i.product_code = p.code 2526 GROUP BY p.code, p.description, p.price 2527 ORDER BY number_of_orders DESC, p.code; 2528 $$; 2529 2530 CREATE OR REPLACE FUNCTION get_stores_by_average_review() 2531 RETURNS TABLE ( 2532 store_id VARCHAR(3), 2533 store_name VARCHAR(50), 2534 average_review NUMERIC, 2535 number_of_reviews BIGINT 2536 ) 2537 LANGUAGE sql 2538 AS $$ 2539 WITH store_reviews AS ( 2540 SELECT DISTINCT 2541 s.store_id, 2542 s.name AS store_name, 2543 r.order_num, 2544 r.rating 2545 FROM store s 2546 JOIN sells se ON se.store_id = s.store_id 2547 JOIN includes i ON i.product_code = se.product_code 2548 JOIN review r ON r.order_num = i.order_num 2549 ) 2550 SELECT 2551 s.store_id, 2552 s.name, 2553 COALESCE(ROUND(AVG(sr.rating), 2), 0)::NUMERIC, 2554 COUNT(sr.order_num) 2555 FROM store s 2556 LEFT JOIN store_reviews sr ON sr.store_id = s.store_id 2557 GROUP BY s.store_id, s.name 2558 ORDER BY average_review DESC, s.store_id; 2559 $$; 2560 2561 CREATE OR REPLACE FUNCTION get_store_with_highest_revenue_growth() 2562 RETURNS TABLE ( 2563 store_id VARCHAR(3), 2564 store_name VARCHAR(50), 2565 previous_year_revenue NUMERIC, 2566 last_year_revenue NUMERIC, 2567 revenue_growth NUMERIC 2568 ) 2569 LANGUAGE sql 2570 AS $$ 2571 WITH store_years AS ( 2572 SELECT 2573 s.store_id, 2574 s.name AS store_name, 2575 EXTRACT(YEAR FROM o.last_date_mod)::INTEGER AS sales_year, 2576 SUM( 2577 p.price * i.quantity 2578 * (1 - COALESCE(o.discount, 0) / 100.0) 2579 ) AS revenue 2580 FROM store s 2581 JOIN sells se ON se.store_id = s.store_id 2582 JOIN includes i ON i.product_code = se.product_code 2583 JOIN product p ON p.code = i.product_code 2584 JOIN "order" o ON o.order_num = i.order_num 2585 WHERE o.last_date_mod >= DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '2 years' 2586 AND o.last_date_mod < DATE_TRUNC('year', CURRENT_DATE) 2587 GROUP BY s.store_id, s.name, EXTRACT(YEAR FROM o.last_date_mod) 2588 ), 2589 comparison AS ( 2590 SELECT 2591 s.store_id, 2592 s.name AS store_name, 2593 COALESCE(MAX(CASE 2594 WHEN sy.sales_year = EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2 2595 THEN sy.revenue ELSE 0 END), 0) AS previous_year_revenue, 2596 COALESCE(MAX(CASE 2597 WHEN sy.sales_year = EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1 2598 THEN sy.revenue ELSE 0 END), 0) AS last_year_revenue 2599 FROM store s 2600 LEFT JOIN store_years sy ON sy.store_id = s.store_id 2601 GROUP BY s.store_id, s.name 2602 ) 2603 SELECT 2604 store_id, 2605 store_name, 2606 ROUND(previous_year_revenue, 2), 2607 ROUND(last_year_revenue, 2), 2608 ROUND(last_year_revenue - previous_year_revenue, 2) 2609 FROM comparison 2610 ORDER BY revenue_growth DESC, store_id 2611 LIMIT 1; 2612 $$; 2613 2614 CREATE OR REPLACE FUNCTION get_clients_by_number_of_orders() 2615 RETURNS TABLE ( 2616 client_id INTEGER, 2617 client_name TEXT, 2618 number_of_orders BIGINT 2619 ) 2620 LANGUAGE sql 2621 AS $$ 2622 SELECT 2623 c.client_id, 2624 CONCAT_WS(' ', c.first_name, c.last_name), 2625 COUNT(o.order_num) 2626 FROM client c 2627 JOIN "order" o ON o.client_id = c.client_id 2628 GROUP BY c.client_id, c.first_name, c.last_name 2629 ORDER BY number_of_orders DESC, c.client_id; 2630 $$; 2631 2632 CREATE OR REPLACE FUNCTION get_approximate_orders_per_client() 2633 RETURNS TABLE ( 2634 total_clients BIGINT, 2635 total_orders BIGINT, 2636 approximate_orders_per_client NUMERIC 2637 ) 2638 LANGUAGE sql 2639 AS $$ 2640 SELECT 2641 (SELECT COUNT(*) FROM client), 2642 (SELECT COUNT(*) FROM "order"), 2643 ROUND( 2644 (SELECT COUNT(*)::NUMERIC FROM "order") 2645 / NULLIF((SELECT COUNT(*) FROM client), 0), 2646 2 2647 ); 2648 $$; 2649 2650 CREATE OR REPLACE FUNCTION get_clients_without_orders() 2651 RETURNS TABLE ( 2652 client_id INTEGER, 2653 client_name TEXT, 2654 email VARCHAR(50) 2655 ) 2656 LANGUAGE sql 2657 AS $$ 2658 SELECT 2659 c.client_id, 2660 CONCAT_WS(' ', c.first_name, c.last_name), 2661 c.email 2662 FROM client c 2663 WHERE NOT EXISTS ( 2664 SELECT 1 FROM "order" o WHERE o.client_id = c.client_id 2665 ) 2666 ORDER BY c.client_id; 2667 $$; 2668 2669 CREATE OR REPLACE FUNCTION get_store_request_statistics() 2670 RETURNS TABLE ( 2671 store_id VARCHAR(3), 2672 store_name VARCHAR(50), 2673 total_requests BIGINT, 2674 solved_requests BIGINT, 2675 requests_in_progress BIGINT 2676 ) 2677 LANGUAGE sql 2678 AS $$ 2679 SELECT 2680 s.store_id, 2681 s.name, 2682 COUNT(r.request_num), 2683 COUNT(r.request_num) FILTER (WHERE r.customer_satisfaction > 0), 2684 COUNT(r.request_num) FILTER (WHERE r.customer_satisfaction <= 0) 2685 FROM store s 2686 LEFT JOIN for_store fs ON fs.store_id = s.store_id 2687 LEFT JOIN request r ON r.request_num = fs.request_num 2688 GROUP BY s.store_id, s.name 2689 ORDER BY total_requests DESC, s.store_id; 2690 $$; 2691 2692 CREATE OR REPLACE FUNCTION get_top_10_employees_by_requests_last_month() 2693 RETURNS TABLE ( 2694 employee_id VARCHAR(10), 2695 employee_name TEXT, 2696 number_of_requests BIGINT 2697 ) 2698 LANGUAGE sql 2699 AS $$ 2700 SELECT 2701 e.employee_id, 2702 CONCAT_WS(' ', p.first_name, p.last_name), 2703 COUNT(DISTINCT a.request_num) 2704 FROM employees e 2705 JOIN personal p ON p.id = e.employee_id 2706 JOIN answers a ON a.personal_id = e.employee_id 2707 JOIN request r ON r.request_num = a.request_num 2708 WHERE r.date_and_time >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month' 2709 AND r.date_and_time < DATE_TRUNC('month', CURRENT_DATE) 2710 GROUP BY e.employee_id, p.first_name, p.last_name 2711 ORDER BY number_of_requests DESC, e.employee_id 2712 LIMIT 10; 2713 $$; 2714 2715 CREATE OR REPLACE FUNCTION get_employees_by_hours_and_pay() 2716 RETURNS TABLE ( 2717 employee_id VARCHAR(10), 2718 employee_name TEXT, 2719 total_hours_worked NUMERIC, 2720 total_pay NUMERIC 2721 ) 2722 LANGUAGE sql 2723 AS $$ 2724 SELECT 2725 e.employee_id, 2726 CONCAT_WS(' ', p.first_name, p.last_name), 2727 COALESCE(SUM(w.total_hours), 0), 2728 COALESCE(SUM(w.wage * w.total_hours), 0) 2729 FROM employees e 2730 JOIN personal p ON p.id = e.employee_id 2731 LEFT JOIN worked w ON w.personal_id = e.employee_id 2732 GROUP BY e.employee_id, p.first_name, p.last_name 2733 ORDER BY total_hours_worked DESC, total_pay DESC, e.employee_id; 2734 $$; 2735 2736 CREATE OR REPLACE FUNCTION get_stores_average_pay() 2737 RETURNS TABLE ( 2738 store_id VARCHAR(3), 2739 store_name VARCHAR(50), 2740 average_pay NUMERIC 2741 ) 2742 LANGUAGE sql 2743 AS $$ 2744 SELECT 2745 s.store_id, 2746 s.name, 2747 COALESCE(ROUND(AVG(w.wage), 2), 0) 2748 FROM store s 2749 LEFT JOIN worked w ON w.store_id = s.store_id 2750 GROUP BY s.store_id, s.name 2751 ORDER BY average_pay DESC, s.store_id; 2752 $$; 2753 2754 CREATE OR REPLACE FUNCTION get_employee_product_changes_last_month() 2755 RETURNS TABLE ( 2756 employee_id VARCHAR(10), 2757 employee_name TEXT, 2758 number_of_product_changes BIGINT 2759 ) 2760 LANGUAGE sql 2761 AS $$ 2762 SELECT 2763 e.employee_id, 2764 CONCAT_WS(' ', p.first_name, p.last_name), 2765 COUNT(mc.change_date_time) 2766 FROM employees e 2767 JOIN personal p ON p.id = e.employee_id 2768 LEFT JOIN makes_change mc 2769 ON mc.personal_id = e.employee_id 2770 AND mc.change_date_time >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month' 2771 AND mc.change_date_time < DATE_TRUNC('month', CURRENT_DATE) 2772 GROUP BY e.employee_id, p.first_name, p.last_name 2773 ORDER BY number_of_product_changes DESC, e.employee_id; 2774 $$; 2775 2776 CREATE OR REPLACE FUNCTION get_stores_by_monthly_profit_and_revenue_growth() 2777 RETURNS TABLE ( 2778 store_id VARCHAR(3), 2779 store_name VARCHAR(50), 2780 month_and_year TEXT, 2781 monthly_profit NUMERIC, 2782 previous_month_revenue NUMERIC, 2783 current_month_revenue NUMERIC, 2784 revenue_growth NUMERIC 2785 ) 2786 LANGUAGE sql 2787 AS $$ 2788 WITH monthly_revenue AS ( 2789 SELECT 2790 s.store_id, 2791 s.name AS store_name, 2792 DATE_TRUNC('month', o.last_date_mod)::DATE AS month_date, 2793 SUM( 2794 p.price * i.quantity 2795 * (1 - COALESCE(o.discount, 0) / 100.0) 2796 ) AS revenue 2797 FROM store s 2798 JOIN sells se ON se.store_id = s.store_id 2799 JOIN includes i ON i.product_code = se.product_code 2800 JOIN product p ON p.code = i.product_code 2801 JOIN "order" o ON o.order_num = i.order_num 2802 GROUP BY s.store_id, s.name, DATE_TRUNC('month', o.last_date_mod) 2803 ), 2804 with_previous AS ( 2805 SELECT 2806 store_id, 2807 store_name, 2808 month_date, 2809 revenue, 2810 LAG(revenue) OVER ( 2811 PARTITION BY store_id 2812 ORDER BY month_date 2813 ) AS previous_revenue 2814 FROM monthly_revenue 2815 ) 2816 SELECT 2817 store_id, 2818 store_name, 2819 TO_CHAR(month_date, 'YYYY-MM'), 2820 ROUND(revenue, 2), 2821 ROUND(COALESCE(previous_revenue, 0), 2), 2822 ROUND(revenue, 2), 2823 ROUND(revenue - COALESCE(previous_revenue, 0), 2) 2824 FROM with_previous 2825 ORDER BY month_date DESC, monthly_profit DESC, store_id; 2826 $$; 2827 2828 CREATE OR REPLACE FUNCTION get_unapproved_reports() 2829 RETURNS TABLE ( 2830 report_date TIMESTAMP, 2831 store_id VARCHAR(3), 2832 overall_profit NUMERIC, 2833 sales_trend VARCHAR(100), 2834 marketing_growth VARCHAR(100), 2835 owner_signature VARCHAR(50) 2836 ) 2837 LANGUAGE sql 2838 AS $$ 2839 SELECT 2840 r.date, 2841 r.store_id, 2842 r.overall_profit, 2843 r.sales_trend, 2844 r.marketing_growth, 2845 r.owner_signature 2846 FROM report r 2847 LEFT JOIN approves a 2848 ON a.report_date = r.date 2849 AND a.store_id = r.store_id 2850 WHERE a.report_date IS NULL 2851 ORDER BY r.date DESC, r.store_id; 2852 $$; 2853 `; 2854 2855 function installReportFunctions(callback) { 2856 2857 pool.query(REPORT_FUNCTIONS_SQL) 2858 .then(() => { 2859 console.log('✅ PostgreSQL report functions installed'); 2860 if (callback) callback(null); 2861 }) 2862 .catch(err => { 2863 console.error('❌ Failed to install PostgreSQL report functions:', err); 2864 if (callback) callback(err); 2865 }); 2866 } 2867 2868 const REPORT_FUNCTION_NAMES = new Set([ 2869 'get_orders_by_total', 2870 'get_products_by_total_sales', 2871 'get_low_stock_high_demand_products', 2872 'get_products_monthly_sales', 2873 'get_stores_by_last_calendar_year_revenue', 2874 'get_products_never_ordered', 2875 'get_products_by_number_of_orders', 2876 'get_stores_by_average_review', 2877 'get_store_with_highest_revenue_growth', 2878 'get_clients_by_number_of_orders', 2879 'get_approximate_orders_per_client', 2880 'get_clients_without_orders', 2881 'get_store_request_statistics', 2882 'get_top_10_employees_by_requests_last_month', 2883 'get_employees_by_hours_and_pay', 2884 'get_stores_average_pay', 2885 'get_employee_product_changes_last_month', 2886 'get_stores_by_monthly_profit_and_revenue_growth', 2887 'get_unapproved_reports' 2888 ]); 2889 2890 function runReport(reportName, params, callback) { 2891 2892 if (!REPORT_FUNCTION_NAMES.has(reportName)) { 2893 callback(new Error('Unknown report: ' + reportName), null); 2894 return; 2895 } 2896 2897 const values = Array.isArray(params) ? params : []; 2898 2899 const placeholders = values.map( 2900 (_, index) => '$' + (index + 1) 2901 ).join(', '); 2902 2903 query( 2904 `SELECT * FROM ${reportName}(${placeholders})`, 2905 values, 2906 (err, result) => { 2907 callback( 2908 err, 2909 result ? result.rows : [] 2910 ); 2911 } 2912 ); 2913 } 2914 2915 function generateStoreReport( 2916 storeId, 2917 startDate, 2918 endDate, 2919 type, 2920 period, 2921 ownerSignature, 2922 callback 2923 ) { 2924 2925 query( 2926 `WITH sales AS ( 2927 SELECT 2928 COALESCE( 2929 SUM( 2930 p.price * i.quantity 2931 * (1 - COALESCE(o.discount, 0) / 100.0) 2932 ), 2933 0 2934 ) AS revenue 2935 FROM sells se 2936 JOIN product p 2937 ON p.code = se.product_code 2938 JOIN includes i 2939 ON i.product_code = se.product_code 2940 JOIN "order" o 2941 ON o.order_num = i.order_num 2942 WHERE se.store_id = $1 2943 AND o.last_date_mod >= $2::timestamp 2944 AND o.last_date_mod < ($3::timestamp + INTERVAL '1 day') 2945 ), 2946 refunds AS ( 2947 SELECT 2948 COALESCE(SUM(rf.amount), 0) AS refund_total 2949 FROM refund rf 2950 JOIN "order" o 2951 ON o.order_num = rf.order_num 2952 WHERE LEFT(o.order_num, 3) = $1 2953 AND o.last_date_mod >= $2::timestamp 2954 AND o.last_date_mod < ($3::timestamp + INTERVAL '1 day') 2955 AND rf.status IN ('approved', 'processed') 2956 ) 2957 SELECT 2958 sales.revenue, 2959 refunds.refund_total, 2960 sales.revenue - refunds.refund_total AS net_profit 2961 FROM sales CROSS JOIN refunds`, 2962 [storeId, startDate, endDate], 2963 (err, result) => { 2964 2965 if (err) { 2966 callback(err, null); 2967 return; 2968 } 2969 2970 const row = result.rows[0] || {}; 2971 const revenue = Number(row.revenue || 0); 2972 const refundTotal = Number(row.refund_total || 0); 2973 const netProfit = Number(row.net_profit || 0); 2974 2975 const salesTrend = 2976 `Revenue ${revenue.toFixed(2)}; Refunds ${refundTotal.toFixed(2)}`; 2977 2978 query( 2979 `SELECT 2980 COALESCE(SUM( 2981 p.price * i.quantity 2982 * (1 - COALESCE(o.discount, 0) / 100.0) 2983 ), 0) AS revenue 2984 FROM sells se 2985 JOIN product p 2986 ON p.code = se.product_code 2987 JOIN includes i 2988 ON i.product_code = se.product_code 2989 JOIN "order" o 2990 ON o.order_num = i.order_num 2991 WHERE se.store_id = $1 2992 AND o.last_date_mod >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month' 2993 AND o.last_date_mod < DATE_TRUNC('month', CURRENT_DATE)`, 2994 [storeId], 2995 (previousErr, previousResult) => { 2996 2997 if (previousErr) { 2998 callback(previousErr, null); 2999 return; 3000 } 3001 3002 const previousRevenue = 3003 Number(previousResult.rows[0]?.revenue || 0); 3004 3005 const growth = 3006 previousRevenue === 0 3007 ? (revenue > 0 ? 100 : 0) 3008 : ((revenue - previousRevenue) / previousRevenue) * 100; 3009 3010 const marketingGrowth = 3011 `${growth.toFixed(2)}%`; 3012 3013 query( 3014 `INSERT INTO report 3015 ( 3016 date, 3017 store_id, 3018 overall_profit, 3019 sales_trend, 3020 marketing_growth, 3021 owner_signature 3022 ) 3023 VALUES 3024 ( 3025 CURRENT_TIMESTAMP, 3026 $1, 3027 $2, 3028 $3, 3029 $4, 3030 $5 3031 ) 3032 RETURNING 3033 date, 3034 store_id, 3035 overall_profit, 3036 sales_trend, 3037 marketing_growth, 3038 owner_signature`, 3039 [ 3040 storeId, 3041 Math.max(0, netProfit), 3042 salesTrend.slice(0, 100), 3043 marketingGrowth.slice(0, 100), 3044 ownerSignature || 'Not signed yet' 3045 ], 3046 (insertErr, insertResult) => { 3047 3048 if (insertErr) { 3049 callback(insertErr, null); 3050 return; 3051 } 3052 3053 const report = insertResult.rows[0]; 3054 3055 /* 3056 * monthly_profit has a composite primary key of 3057 * (report_date, store_id), so it can contain one 3058 * summary row per generated report without changing 3059 * the project database structure. 3060 */ 3061 query( 3062 `INSERT INTO monthly_profit 3063 ( 3064 report_date, 3065 store_id, 3066 month_and_year, 3067 profit 3068 ) 3069 VALUES 3070 ( 3071 $1, 3072 $2, 3073 DATE_TRUNC('month', $3::timestamp)::DATE, 3074 $4 3075 ) 3076 ON CONFLICT (report_date, store_id) 3077 DO UPDATE SET 3078 month_and_year = EXCLUDED.month_and_year, 3079 profit = EXCLUDED.profit`, 3080 [ 3081 report.date, 3082 storeId, 3083 endDate, 3084 Math.max(0, netProfit) 3085 ], 3086 (monthlyErr) => { 3087 3088 if (monthlyErr) { 3089 console.error( 3090 'Warning inserting monthly profit:', 3091 monthlyErr 3092 ); 3093 } 3094 3095 query( 3096 `INSERT INTO exchanges_data 3097 ( 3098 report_date, 3099 store_id, 3100 monthly_profit, 3101 date, 3102 sales, 3103 damages 3104 ) 3105 VALUES 3106 ( 3107 $1, 3108 $2, 3109 $3, 3110 CURRENT_TIMESTAMP, 3111 $4, 3112 $5 3113 ) 3114 ON CONFLICT (report_date, store_id) 3115 DO UPDATE SET 3116 monthly_profit = EXCLUDED.monthly_profit, 3117 date = EXCLUDED.date, 3118 sales = EXCLUDED.sales, 3119 damages = EXCLUDED.damages`, 3120 [ 3121 report.date, 3122 storeId, 3123 Math.max(0, netProfit), 3124 revenue, 3125 -refundTotal 3126 ], 3127 (exchangeErr) => { 3128 3129 if (exchangeErr) { 3130 console.error( 3131 'Warning inserting exchange data:', 3132 exchangeErr 3133 ); 3134 } 3135 3136 callback( 3137 null, 3138 report 3139 ); 3140 } 3141 ); 3142 } 3143 ); 3144 } 3145 ); 3146 } 3147 ); 3148 } 3149 ); 3150 } 2295 3151 /* 2296 3152 * ============================================================ … … 2340 3196 getStoreStats, 2341 3197 3198 installReportFunctions, 3199 runReport, 3200 generateStoreReport, 3201 2342 3202 createOrderNew, 2343 3203 getOrdersByClient, -
node_modules/.package-lock.json
r6149556 r6c7cfa6 468 468 }, 469 469 "node_modules/semver": { 470 "version": "7. 7.4",471 "resolved": "https://registry.npmjs.org/semver/-/semver-7. 7.4.tgz",472 "integrity": "sha512- vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",470 "version": "7.8.5", 471 "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", 472 "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", 473 473 "dev": true, 474 474 "license": "ISC", -
node_modules/semver/README.md
r6149556 r6c7cfa6 57 57 const semverSort = require('semver/functions/sort') 58 58 const semverRsort = require('semver/functions/rsort') 59 const semverTruncate = require('semver/functions/truncate') 59 60 60 61 // low-level comparators between versions … … 400 401 caret ::= '^' partial 401 402 qualifier ::= ( '-' pre )? ( '+' build )? 402 pre ::= parts 403 build ::= parts 404 parts ::= part ( '.' part ) * 405 part ::= nr | [-0-9A-Za-z]+ 406 ``` 403 pre ::= prepart ( '.' prepart ) * 404 prepart ::= nr | alphanumid 405 build ::= buildid ( '.' buildid ) * 406 alphanumid ::= ( ['0'-'9'] ) * [-A-Za-z] [-0-9A-Za-z] * 407 buildid ::= [-0-9A-Za-z]+ 408 ``` 409 410 Note: Prerelease identifiers (`pre`) use `nr` for numeric parts, which 411 disallows leading zeros (e.g., `1.2.3-00` is invalid). Build metadata 412 identifiers (`build`) allow any alphanumeric string including leading 413 zeros (e.g., `1.2.3+00` is valid). This matches the 414 [SemVer 2.0.0 specification](https://semver.org/#spec-item-9). 407 415 408 416 ## Functions … … 450 458 * `parse(v)`: Attempt to parse a string as a semantic version, returning either 451 459 a `SemVer` object or `null`. 460 * `truncate(v, releaseType)`: Return the version with components _lower_ 461 than `releaseType` dropped off, e.g.: 462 * `major` removes build & prerelease info and sets minor & patch to 0. 463 * `minor` removes build & prerelease info, and sets patch to 0 464 * `patch` removes build & prerelease info 465 * All prerelease types remove build info only 452 466 453 467 ### Comparison … … 651 665 * `require('semver/functions/satisfies')` 652 666 * `require('semver/functions/sort')` 667 * `require('semver/functions/truncate')` 653 668 * `require('semver/functions/valid')` 654 669 * `require('semver/ranges/gtr')` -
node_modules/semver/classes/range.js
r6149556 r6c7cfa6 99 99 100 100 parseRange (range) { 101 // strip build metadata so it can't bleed into the version 102 range = range.replace(BUILDSTRIPRE, '') 103 101 104 // memoize range parsing for performance. 102 105 // this is a very hot path, and fully deterministic. … … 224 227 const { 225 228 safeRe: re, 229 src, 226 230 t, 227 231 comparatorTrimReplace, … … 230 234 } = require('../internal/re') 231 235 const { FLAG_INCLUDE_PRERELEASE, FLAG_LOOSE } = require('../internal/constants') 236 237 // unbounded global build-metadata stripper used by parseRange 238 const BUILDSTRIPRE = new RegExp(src[t.BUILD], 'g') 232 239 233 240 const isNullSet = c => c.value === '<0.0.0-0' … … 271 278 const isX = id => !id || id.toLowerCase() === 'x' || id === '*' 272 279 280 const invalidXRangeOrder = (M, m, p) => ( 281 (isX(M) && !isX(m)) || 282 (isX(m) && p && !isX(p)) 283 ) 284 273 285 // ~, ~> --> * (any, kinda silly) 274 286 // ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0-0 … … 288 300 const replaceTilde = (comp, options) => { 289 301 const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE] 302 // if we're including prereleases in the match, then the lower bound is 303 // -0, the lowest possible prerelease value, just like x-ranges and carets. 304 // this keeps `~1.2` equivalent to the `1.2.x` x-range it's documented as. 305 const z = options.includePrerelease ? '-0' : '' 290 306 return comp.replace(r, (_, M, m, p, pr) => { 291 307 debug('tilde', comp, _, M, m, p, pr) … … 295 311 ret = '' 296 312 } else if (isX(m)) { 297 ret = `>=${M}.0.0 <${+M + 1}.0.0-0`313 ret = `>=${M}.0.0${z} <${+M + 1}.0.0-0` 298 314 } else if (isX(p)) { 299 315 // ~1.2 == >=1.2.0 <1.3.0-0 300 ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0`316 ret = `>=${M}.${m}.0${z} <${M}.${+m + 1}.0-0` 301 317 } else if (pr) { 302 318 debug('replaceTilde pr', pr) … … 367 383 if (m === '0') { 368 384 ret = `>=${M}.${m}.${p 369 } ${z}<${M}.${m}.${+p + 1}-0`385 } <${M}.${m}.${+p + 1}-0` 370 386 } else { 371 387 ret = `>=${M}.${m}.${p 372 } ${z}<${M}.${+m + 1}.0-0`388 } <${M}.${+m + 1}.0-0` 373 389 } 374 390 } else { … … 396 412 return comp.replace(r, (ret, gtlt, M, m, p, pr) => { 397 413 debug('xRange', comp, ret, gtlt, M, m, p, pr) 414 if (invalidXRangeOrder(M, m, p)) { 415 return comp 416 } 417 398 418 const xM = isX(M) 399 419 const xm = xM || isX(m) -
node_modules/semver/classes/semver.js
r6149556 r6c7cfa6 7 7 const parseOptions = require('../internal/parse-options') 8 8 const { compareIdentifiers } = require('../internal/identifiers') 9 10 const isPrereleaseIdentifier = (prerelease, identifier) => { 11 const identifiers = identifier.split('.') 12 if (identifiers.length > prerelease.length) { 13 return false 14 } 15 16 for (let i = 0; i < identifiers.length; i++) { 17 if (compareIdentifiers(prerelease[i], identifiers[i]) !== 0) { 18 return false 19 } 20 } 21 22 return true 23 } 24 9 25 class SemVer { 10 26 constructor (version, options) { … … 310 326 prerelease = [identifier] 311 327 } 312 if (compareIdentifiers(this.prerelease[0], identifier) === 0) { 313 if (isNaN(this.prerelease[1])) { 328 if (isPrereleaseIdentifier(this.prerelease, identifier)) { 329 const prereleaseBase = this.prerelease[identifier.split('.').length] 330 if (isNaN(prereleaseBase)) { 314 331 this.prerelease = prerelease 315 332 } -
node_modules/semver/index.js
r6149556 r6c7cfa6 29 29 const cmp = require('./functions/cmp') 30 30 const coerce = require('./functions/coerce') 31 const truncate = require('./functions/truncate') 31 32 const Comparator = require('./classes/comparator') 32 33 const Range = require('./classes/range') … … 67 68 cmp, 68 69 coerce, 70 truncate, 69 71 Comparator, 70 72 Range, -
node_modules/semver/internal/re.js
r6149556 r6c7cfa6 137 137 138 138 // Something like "2.*" or "1.2.x". 139 // Note that "x.x" is a valid xRange identif er, meaning "any version"139 // Note that "x.x" is a valid xRange identifier, meaning "any version" 140 140 // Only the first item is strictly required. 141 141 createToken('XRANGEIDENTIFIERLOOSE', `${src[t.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`) -
node_modules/semver/package.json
r6149556 r6c7cfa6 1 1 { 2 2 "name": "semver", 3 "version": "7. 7.4",3 "version": "7.8.5", 4 4 "description": "The semantic version parser used by npm.", 5 5 "main": "index.js", … … 15 15 }, 16 16 "devDependencies": { 17 "@npmcli/eslint-config": "^ 6.0.0",18 "@npmcli/template-oss": " 4.29.0",17 "@npmcli/eslint-config": "^7.0.0", 18 "@npmcli/template-oss": "5.0.0", 19 19 "benchmark": "^2.1.4", 20 20 "tap": "^16.0.0" … … 53 53 "templateOSS": { 54 54 "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", 55 "version": " 4.29.0",55 "version": "5.0.0", 56 56 "engines": ">=10", 57 57 "distPaths": [ -
node_modules/semver/range.bnf
r6149556 r6c7cfa6 11 11 caret ::= '^' partial 12 12 qualifier ::= ( '-' pre )? ( '+' build )? 13 pre ::= parts 14 build ::= parts 15 parts ::= part ( '.' part ) * 16 part ::= nr | [-0-9A-Za-z]+ 13 pre ::= prepart ( '.' prepart ) * 14 prepart ::= nr | alphanumid 15 build ::= buildid ( '.' buildid ) * 16 alphanumid ::= ( [0-9] ) * [A-Za-z-] [-0-9A-Za-z] * 17 buildid ::= [-0-9A-Za-z]+ -
node_modules/semver/ranges/subset.js
r6149556 r6c7cfa6 175 175 return false 176 176 } 177 } else if (gt.operator === '>=' && ! satisfies(gt.semver, String(c), options)) {177 } else if (gt.operator === '>=' && !c.test(gt.semver)) { 178 178 return false 179 179 } … … 193 193 return false 194 194 } 195 } else if (lt.operator === '<=' && ! satisfies(lt.semver, String(c), options)) {195 } else if (lt.operator === '<=' && !c.test(lt.semver)) { 196 196 return false 197 197 } -
server.js
r6149556 r6c7cfa6 7 7 const nodemailer = require('nodemailer'); 8 8 const bcrypt = require('bcryptjs'); 9 const { AsyncLocalStorage } = require('async_hooks'); 9 10 require('dotenv').config(); 10 11 … … 356 357 }); 357 358 358 let transactionClient = null;359 const transactionStorage = new AsyncLocalStorage(); 359 360 360 361 function dbQuery(sql, params = [], callback) { 361 const client = transactionClient || pool; 362 const client = transactionStorage.getStore() || pool; 363 362 364 client.query(sql, params) 363 365 .then(result => callback(null, result)) 364 366 .catch(err => callback(err)); 365 367 } 368 369 const REPORT_FUNCTIONS_SQL = String.raw` 370 -- ============================================================ 371 -- HANDCRAFT MARKETPLACE REPORT FUNCTIONS 372 -- PostgreSQL / exact project schema 373 -- ============================================================ 374 375 CREATE OR REPLACE FUNCTION get_orders_by_total() 376 RETURNS 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 ) 386 LANGUAGE sql 387 AS $$ 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 411 CREATE OR REPLACE FUNCTION get_products_by_total_sales() 412 RETURNS 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 ) 420 LANGUAGE sql 421 AS $$ 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 445 CREATE OR REPLACE FUNCTION get_low_stock_high_demand_products( 446 p_stock_threshold INTEGER, 447 p_demand_threshold INTEGER 448 ) 449 RETURNS 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 ) 456 LANGUAGE sql 457 AS $$ 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 473 CREATE OR REPLACE FUNCTION get_products_monthly_sales() 474 RETURNS 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 ) 483 LANGUAGE sql 484 AS $$ 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 512 CREATE OR REPLACE FUNCTION get_stores_by_last_calendar_year_revenue() 513 RETURNS 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 ) 520 LANGUAGE sql 521 AS $$ 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 549 CREATE OR REPLACE FUNCTION get_products_never_ordered() 550 RETURNS TABLE ( 551 product_code VARCHAR(8), 552 product_description VARCHAR(500), 553 product_price NUMERIC, 554 current_stock INTEGER 555 ) 556 LANGUAGE sql 557 AS $$ 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 568 CREATE OR REPLACE FUNCTION get_products_by_number_of_orders() 569 RETURNS TABLE ( 570 product_code VARCHAR(8), 571 product_description VARCHAR(500), 572 product_price NUMERIC, 573 number_of_orders BIGINT 574 ) 575 LANGUAGE sql 576 AS $$ 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 588 CREATE OR REPLACE FUNCTION get_stores_by_average_review() 589 RETURNS TABLE ( 590 store_id VARCHAR(3), 591 store_name VARCHAR(50), 592 average_review NUMERIC, 593 number_of_reviews BIGINT 594 ) 595 LANGUAGE sql 596 AS $$ 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 619 CREATE OR REPLACE FUNCTION get_store_with_highest_revenue_growth() 620 RETURNS 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 ) 627 LANGUAGE sql 628 AS $$ 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 672 CREATE OR REPLACE FUNCTION get_clients_by_number_of_orders() 673 RETURNS TABLE ( 674 client_id INTEGER, 675 client_name TEXT, 676 number_of_orders BIGINT 677 ) 678 LANGUAGE sql 679 AS $$ 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 690 CREATE OR REPLACE FUNCTION get_approximate_orders_per_client() 691 RETURNS TABLE ( 692 total_clients BIGINT, 693 total_orders BIGINT, 694 approximate_orders_per_client NUMERIC 695 ) 696 LANGUAGE sql 697 AS $$ 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 708 CREATE OR REPLACE FUNCTION get_clients_without_orders() 709 RETURNS TABLE ( 710 client_id INTEGER, 711 client_name TEXT, 712 email VARCHAR(50) 713 ) 714 LANGUAGE sql 715 AS $$ 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 727 CREATE OR REPLACE FUNCTION get_store_request_statistics() 728 RETURNS 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 ) 735 LANGUAGE sql 736 AS $$ 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 750 CREATE OR REPLACE FUNCTION get_top_10_employees_by_requests_last_month() 751 RETURNS TABLE ( 752 employee_id VARCHAR(10), 753 employee_name TEXT, 754 number_of_requests BIGINT 755 ) 756 LANGUAGE sql 757 AS $$ 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 773 CREATE OR REPLACE FUNCTION get_employees_by_hours_and_pay() 774 RETURNS TABLE ( 775 employee_id VARCHAR(10), 776 employee_name TEXT, 777 total_hours_worked NUMERIC, 778 total_pay NUMERIC 779 ) 780 LANGUAGE sql 781 AS $$ 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 794 CREATE OR REPLACE FUNCTION get_stores_average_pay() 795 RETURNS TABLE ( 796 store_id VARCHAR(3), 797 store_name VARCHAR(50), 798 average_pay NUMERIC 799 ) 800 LANGUAGE sql 801 AS $$ 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 812 CREATE OR REPLACE FUNCTION get_employee_product_changes_last_month() 813 RETURNS TABLE ( 814 employee_id VARCHAR(10), 815 employee_name TEXT, 816 number_of_product_changes BIGINT 817 ) 818 LANGUAGE sql 819 AS $$ 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 834 CREATE OR REPLACE FUNCTION get_stores_by_monthly_profit_and_revenue_growth() 835 RETURNS 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 ) 844 LANGUAGE sql 845 AS $$ 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 886 CREATE OR REPLACE FUNCTION get_unapproved_reports() 887 RETURNS 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 ) 895 LANGUAGE sql 896 AS $$ 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 `; 366 912 367 913 const database = { … … 393 939 394 940 if (normalized === 'BEGIN TRANSACTION' || normalized === 'BEGIN') { 395 if (transactionClient) { 941 const existingClient = transactionStorage.getStore(); 942 943 // A transaction is already active in this async execution context. 944 // Do not create a second transaction on the same request. 945 if (existingClient) { 396 946 callback?.(null); 397 947 return; 398 948 } 399 pool.connect().then(client => { 400 transactionClient = client; 401 return client.query('BEGIN'); 402 }).then(() => callback?.(null)) 949 950 pool.connect() 951 .then(client => { 952 return client.query('BEGIN') 953 .then(() => { 954 // Everything scheduled by the callback now inherits 955 // this client through AsyncLocalStorage. Other 956 // concurrent requests get their own transaction. 957 transactionStorage.run(client, () => { 958 callback?.(null); 959 }); 960 }) 961 .catch(err => { 962 client.release(); 963 callback?.(err); 964 }); 965 }) 403 966 .catch(err => { 404 if (transactionClient) transactionClient.release();405 transactionClient = null;406 967 callback?.(err); 407 968 }); 969 408 970 return; 409 971 } 410 972 411 973 if (normalized === 'COMMIT') { 412 if (!transactionClient) { 974 const client = transactionStorage.getStore(); 975 976 if (!client) { 413 977 callback?.(null); 414 978 return; 415 979 } 416 const client = transactionClient; 980 417 981 client.query('COMMIT') 418 982 .then(() => { 419 transactionClient = null;420 983 client.release(); 421 984 callback?.(null); 422 985 }) 423 986 .catch(err => { 424 transactionClient = null; 425 client.release(); 426 callback?.(err); 987 // COMMIT may fail before the transaction is completed. 988 // Roll back before releasing the client when possible. 989 client.query('ROLLBACK') 990 .catch(() => {}) 991 .then(() => { 992 client.release(); 993 callback?.(err); 994 }); 427 995 }); 996 428 997 return; 429 998 } 430 999 431 1000 if (normalized === 'ROLLBACK') { 432 if (!transactionClient) { 1001 const client = transactionStorage.getStore(); 1002 1003 if (!client) { 433 1004 callback?.(null); 434 1005 return; 435 1006 } 436 const client = transactionClient; 1007 437 1008 client.query('ROLLBACK') 438 1009 .then(() => { 439 transactionClient = null;440 1010 client.release(); 441 1011 callback?.(null); 442 1012 }) 443 1013 .catch(err => { 444 transactionClient = null;445 1014 client.release(); 446 1015 callback?.(err); 447 1016 }); 1017 448 1018 return; 449 1019 } … … 458 1028 }); 459 1029 } 1030 }, 1031 1032 async installReportFunctions() { 1033 await pool.query(REPORT_FUNCTIONS_SQL); 1034 console.log('✅ PostgreSQL report functions installed'); 1035 }, 1036 1037 runReport(reportName, params, callback) { 1038 const allowed = new Set([ 1039 'get_orders_by_total', 1040 'get_products_by_total_sales', 1041 'get_low_stock_high_demand_products', 1042 'get_products_monthly_sales', 1043 'get_stores_by_last_calendar_year_revenue', 1044 'get_products_never_ordered', 1045 'get_products_by_number_of_orders', 1046 'get_stores_by_average_review', 1047 'get_store_with_highest_revenue_growth', 1048 'get_clients_by_number_of_orders', 1049 'get_approximate_orders_per_client', 1050 'get_clients_without_orders', 1051 'get_store_request_statistics', 1052 'get_top_10_employees_by_requests_last_month', 1053 'get_employees_by_hours_and_pay', 1054 'get_stores_average_pay', 1055 'get_employee_product_changes_last_month', 1056 'get_stores_by_monthly_profit_and_revenue_growth', 1057 'get_unapproved_reports' 1058 ]); 1059 1060 if (!allowed.has(reportName)) { 1061 callback(new Error('Unknown report: ' + reportName), null); 1062 return; 1063 } 1064 1065 const values = Array.isArray(params) ? params : []; 1066 const placeholders = values.map((_, index) => '$' + (index + 1)).join(', '); 1067 1068 dbQuery( 1069 `SELECT * FROM ${reportName}(${placeholders})`, 1070 values, 1071 (err, result) => callback(err, result?.rows || []) 1072 ); 460 1073 }, 461 1074 … … 520 1133 const schema = ` 521 1134 CREATE TABLE IF NOT EXISTS category ( 522 id SERIAL PRIMARY KEY,523 name VARCHAR(50) NOT NULL,1135 id SERIAL PRIMARY KEY, 1136 name VARCHAR(50) NOT NULL, 524 1137 parent_category_id INTEGER REFERENCES category(id) NOT NULL 525 );1138 ); 526 1139 527 1140 CREATE TABLE IF NOT EXISTS product ( 528 code VARCHAR(8) PRIMARY KEY DEFAULT '-1',1141 code VARCHAR(8) PRIMARY KEY DEFAULT '-1', 529 1142 price DECIMAL(10,2) NOT NULL CHECK (price >= 0.0), 530 1143 availability INTEGER NOT NULL, … … 534 1147 description VARCHAR(500) NOT NULL, 535 1148 category_id INTEGER NOT NULL REFERENCES category(id) ON DELETE SET DEFAULT 536 );1149 ); 537 1150 538 1151 CREATE TABLE IF NOT EXISTS image ( 539 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE,1152 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE, 540 1153 image VARCHAR NOT NULL DEFAULT 'Image NOT found!' 541 );1154 ); 542 1155 543 1156 CREATE TABLE IF NOT EXISTS color ( 544 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE,1157 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE, 545 1158 color VARCHAR(50) 546 );1159 ); 547 1160 548 1161 CREATE TABLE IF NOT EXISTS store ( 549 store_ID VARCHAR(3) PRIMARY KEY,1162 store_ID VARCHAR(3) PRIMARY KEY, 550 1163 name VARCHAR(50) UNIQUE NOT NULL, 551 1164 date_of_founding DATE NOT NULL, … … 553 1166 store_email VARCHAR(40) UNIQUE NOT NULL CHECK (store_email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'), 554 1167 rating DECIMAL(2,1) NOT NULL DEFAULT 0 CHECK (rating>=0.0 AND rating<=5.0) 555 );1168 ); 556 1169 557 1170 CREATE TABLE IF NOT EXISTS personal ( 558 id VARCHAR(10) PRIMARY KEY,1171 id VARCHAR(10) PRIMARY KEY, 559 1172 first_name VARCHAR(20) NOT NULL, 560 1173 last_name VARCHAR(20) NOT NULL, … … 562 1175 email VARCHAR(50) UNIQUE NOT NULL CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'), 563 1176 password VARCHAR NOT NULL 564 );1177 ); 565 1178 566 1179 CREATE TABLE IF NOT EXISTS permissions ( 567 personal_isVARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE,1180 personal_id VARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE, 568 1181 type VARCHAR(50) NOT NULL, 569 1182 authorisation VARCHAR(50) NOT NULL 570 );1183 ); 571 1184 572 1185 CREATE TABLE IF NOT EXISTS boss ( 573 boss_id VARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE574 );1186 boss_id VARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE 1187 ); 575 1188 576 1189 CREATE TABLE IF NOT EXISTS employees ( 577 employee_id VARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE,1190 employee_id VARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE, 578 1191 date_of_hire DATE NOT NULL 579 );1192 ); 580 1193 581 1194 CREATE TABLE IF NOT EXISTS client ( 582 client_ID SERIAL PRIMARY KEY,583 first_name VARCHAR(50) NOT NULL,1195 client_ID SERIAL PRIMARY KEY, 1196 first_name VARCHAR(50) NOT NULL, 584 1197 last_name VARCHAR(50) NOT NULL, 585 1198 email VARCHAR(50) UNIQUE NOT NULL CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'), 586 1199 password VARCHAR NOT NULL 587 );1200 ); 588 1201 589 1202 CREATE TABLE IF NOT EXISTS delivery_address ( 590 client_ID INTEGER PRIMARY KEY REFERENCES client(client_ID) ON DELETE CASCADE,1203 client_ID INTEGER PRIMARY KEY REFERENCES client(client_ID) ON DELETE CASCADE, 591 1204 address VARCHAR(200) NOT NULL, 592 1205 city VARCHAR(30) NOT NULL, … … 594 1207 country VARCHAR(40) NOT NULL, 595 1208 is_default BOOLEAN DEFAULT TRUE 596 );1209 ); 597 1210 598 1211 CREATE TABLE IF NOT EXISTS "order" ( 599 order_num VARCHAR(11) PRIMARY KEY,1212 order_num VARCHAR(11) PRIMARY KEY, 600 1213 client_ID INTEGER REFERENCES client(client_ID) ON DELETE CASCADE, 601 1214 status VARCHAR(20) NOT NULL DEFAULT 'placed order', … … 604 1217 discount DECIMAL(5,2) DEFAULT 0.0 CHECK(discount>=0.0 AND discount<=100.00), 605 1218 CONSTRAINT check_status CHECK (status IN ('placed order', 'being processed', 'shipping', 'delivered', 'canceled')) 606 );1219 ); 607 1220 608 1221 CREATE TABLE IF NOT EXISTS review ( 609 order_num VARCHAR(11) PRIMARY KEY REFERENCES "order"(order_num) ON DELETE CASCADE,1222 order_num VARCHAR(11) PRIMARY KEY REFERENCES "order"(order_num) ON DELETE CASCADE, 610 1223 comment VARCHAR(300), 611 1224 rating DECIMAL(2,1) NOT NULL CHECK(rating>=0.0 AND rating<=5.0), 612 1225 last_mod_date TIMESTAMP NOT NULL 613 );1226 ); 614 1227 615 1228 CREATE TABLE IF NOT EXISTS refund ( 616 refund_id SERIAL PRIMARY KEY,617 order_num VARCHAR(11) REFERENCES "order"(order_num) ON DELETE CASCADE,1229 refund_id SERIAL PRIMARY KEY, 1230 order_num VARCHAR(11) REFERENCES "order"(order_num) ON DELETE CASCADE, 618 1231 reason VARCHAR(300), 619 1232 amount DECIMAL(5,2) NOT NULL, 620 1233 status VARCHAR(100) NOT NULL DEFAULT 'requested refund', 621 CONSTRAINT check_refund_status CHECK (status IN ('requested refund', 'being revie d', 'approved', 'not approved', 'processed'))622 );1234 CONSTRAINT check_refund_status CHECK (status IN ('requested refund', 'being reviewed', 'approved', 'not approved', 'processed')) 1235 ); 623 1236 624 1237 CREATE TABLE IF NOT EXISTS report ( 625 date TIMESTAMP NOT NULL,626 store_ID VARCHAR(3) NOT NULL REFERENCES store(store_ID) ON DELETE CASCADE,1238 date TIMESTAMP NOT NULL, 1239 store_ID VARCHAR(3) NOT NULL REFERENCES store(store_ID) ON DELETE CASCADE, 627 1240 overall_profit NUMERIC NOT NULL DEFAULT 0.0 CHECK(overall_profit>=0), 628 1241 sales_trend VARCHAR(100) NOT NULL, … … 630 1243 owner_signature VARCHAR(50) NOT NULL DEFAULT 'Not signed yet', 631 1244 PRIMARY KEY (date, store_ID) 632 );1245 ); 633 1246 634 1247 CREATE TABLE IF NOT EXISTS monthly_profit ( 635 report_date TIMESTAMP NOT NULL,636 store_ID VARCHAR(3) NOT NULL,1248 report_date TIMESTAMP NOT NULL, 1249 store_ID VARCHAR(3) NOT NULL, 637 1250 month_and_year DATE NOT NULL, 638 1251 profit NUMERIC NOT NULL DEFAULT 0.0, 639 1252 PRIMARY KEY(report_date, store_ID), 640 1253 FOREIGN KEY (report_date, store_ID) REFERENCES report(date, store_ID) ON DELETE CASCADE 641 );1254 ); 642 1255 643 1256 CREATE TABLE IF NOT EXISTS exchanges_data ( 644 report_date TIMESTAMP NOT NULL,645 store_ID VARCHAR(3) NOT NULL,1257 report_date TIMESTAMP NOT NULL, 1258 store_ID VARCHAR(3) NOT NULL, 646 1259 monthly_profit NUMERIC NOT NULL DEFAULT 0.0, 647 1260 date TIMESTAMP NOT NULL, … … 650 1263 PRIMARY KEY (report_date, store_ID), 651 1264 FOREIGN KEY (report_date, store_ID) REFERENCES report(date, store_ID) ON DELETE CASCADE 652 );1265 ); 653 1266 654 1267 CREATE TABLE IF NOT EXISTS request ( 655 request_num VARCHAR(14) PRIMARY KEY,1268 request_num VARCHAR(14) PRIMARY KEY, 656 1269 date_and_time TIMESTAMP NOT NULL, 657 1270 problem VARCHAR(300) NOT NULL, 658 1271 notes_of_communication VARCHAR, 659 1272 customer_satisfaction NUMERIC NOT NULL 660 );1273 ); 661 1274 662 1275 CREATE TABLE IF NOT EXISTS makes_request ( 663 client_ID INTEGER NOT NULL REFERENCES client(client_ID) ON DELETE CASCADE,1276 client_ID INTEGER NOT NULL REFERENCES client(client_ID) ON DELETE CASCADE, 664 1277 order_num VARCHAR(11) UNIQUE NOT NULL REFERENCES "order"(order_num) ON DELETE CASCADE, 665 1278 PRIMARY KEY(client_ID, order_num) 666 );1279 ); 667 1280 668 1281 CREATE TABLE IF NOT EXISTS answers ( 669 request_num VARCHAR(14) REFERENCES request(request_num) ON DELETE CASCADE,1282 request_num VARCHAR(14) REFERENCES request(request_num) ON DELETE CASCADE, 670 1283 personal_id VARCHAR(10) NOT NULL REFERENCES personal(id) ON DELETE CASCADE, 671 1284 PRIMARY KEY(request_num, personal_id) 672 );1285 ); 673 1286 674 1287 CREATE TABLE IF NOT EXISTS for_store ( 675 request_num VARCHAR(14) REFERENCES request(request_num) ON DELETE CASCADE,1288 request_num VARCHAR(14) REFERENCES request(request_num) ON DELETE CASCADE, 676 1289 store_ID VARCHAR(3) REFERENCES store(store_ID) ON DELETE CASCADE, 677 1290 PRIMARY KEY(request_num, store_ID) 678 );1291 ); 679 1292 680 1293 CREATE TABLE IF NOT EXISTS "change" ( 681 date_and_time TIMESTAMP NOT NULL,682 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE,1294 date_and_time TIMESTAMP NOT NULL, 1295 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE, 683 1296 changes VARCHAR NOT NULL, 684 1297 PRIMARY KEY (date_and_time, product_code) 685 );1298 ); 686 1299 687 1300 CREATE TABLE IF NOT EXISTS makes_change ( 688 personal_id VARCHAR(10) REFERENCES personal(id) ON DELETE CASCADE,1301 personal_id VARCHAR(10) REFERENCES personal(id) ON DELETE CASCADE, 689 1302 change_date_time TIMESTAMP, 690 1303 product_code VARCHAR(8), 691 1304 PRIMARY KEY(personal_id, change_date_time, product_code), 692 1305 FOREIGN KEY(change_date_time, product_code) REFERENCES "change"(date_and_time, product_code) ON DELETE CASCADE 693 );1306 ); 694 1307 695 1308 CREATE TABLE IF NOT EXISTS works_in_store ( 696 personal_id VARCHAR(10) REFERENCES personal(id) ON DELETE CASCADE,1309 personal_id VARCHAR(10) REFERENCES personal(id) ON DELETE CASCADE, 697 1310 store_ID VARCHAR(3) REFERENCES store(store_ID) ON DELETE CASCADE, 698 1311 PRIMARY KEY(personal_id, store_ID) 699 );1312 ); 700 1313 701 1314 CREATE TABLE IF NOT EXISTS worked ( 702 personal_id VARCHAR(10) REFERENCES personal(id) ON DELETE CASCADE,1315 personal_id VARCHAR(10) REFERENCES personal(id) ON DELETE CASCADE, 703 1316 report_date TIMESTAMP, 704 1317 store_ID VARCHAR(3), … … 710 1323 FOREIGN KEY (report_date, store_ID) REFERENCES report(date, store_ID) ON DELETE CASCADE, 711 1324 CONSTRAINT check_pay_method CHECK (pay_method IN ('full_time', 'part-time', 'custom')) 712 );1325 ); 713 1326 714 1327 CREATE TABLE IF NOT EXISTS sells ( 715 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE,1328 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE, 716 1329 store_ID VARCHAR(3) REFERENCES store(store_ID) ON DELETE CASCADE, 717 1330 discount NUMERIC NOT NULL DEFAULT 0.0, 718 1331 PRIMARY KEY (product_code, store_ID) 719 );1332 ); 720 1333 721 1334 CREATE TABLE IF NOT EXISTS includes ( 722 order_num VARCHAR(11) REFERENCES "order"(order_num) ON DELETE CASCADE,1335 order_num VARCHAR(11) REFERENCES "order"(order_num) ON DELETE CASCADE, 723 1336 product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE, 724 1337 quantity INTEGER NOT NULL CHECK(quantity>=0), 725 1338 PRIMARY KEY (order_num, product_code) 726 );1339 ); 727 1340 728 1341 CREATE TABLE IF NOT EXISTS approves ( 729 boss_id VARCHAR(10) REFERENCES boss(boss_id) ON DELETE CASCADE,1342 boss_id VARCHAR(10) REFERENCES boss(boss_id) ON DELETE CASCADE, 730 1343 report_date TIMESTAMP, 731 1344 store_ID VARCHAR(3), … … 733 1346 PRIMARY KEY (boss_id, report_date, store_ID), 734 1347 FOREIGN KEY (report_date, store_ID) REFERENCES report(date, store_ID) ON DELETE CASCADE 735 );1348 ); 736 1349 737 1350 -- These four small tables are application authentication/audit storage. 738 1351 -- They do not modify any of the project tables above. 739 1352 CREATE TABLE IF NOT EXISTS users ( 740 id VARCHAR(50) PRIMARY KEY,1353 id VARCHAR(50) PRIMARY KEY, 741 1354 username VARCHAR(100) UNIQUE NOT NULL, 742 1355 email VARCHAR(255) UNIQUE NOT NULL, … … 745 1358 force_password_change BOOLEAN DEFAULT FALSE, 746 1359 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 747 );1360 ); 748 1361 749 1362 CREATE TABLE IF NOT EXISTS roles ( 750 role_id SERIAL PRIMARY KEY,751 name VARCHAR(50) UNIQUE NOT NULL,1363 role_id SERIAL PRIMARY KEY, 1364 name VARCHAR(50) UNIQUE NOT NULL, 752 1365 description TEXT 753 );1366 ); 754 1367 755 1368 CREATE TABLE IF NOT EXISTS user_roles ( 756 user_id VARCHAR(50) REFERENCES users(id) ON DELETE CASCADE,1369 user_id VARCHAR(50) REFERENCES users(id) ON DELETE CASCADE, 757 1370 role_id INTEGER REFERENCES roles(role_id) ON DELETE CASCADE, 758 1371 PRIMARY KEY(user_id, role_id) 759 );1372 ); 760 1373 761 1374 CREATE TABLE IF NOT EXISTS audit_log ( 762 log_id BIGSERIAL PRIMARY KEY,763 user_id VARCHAR(50),1375 log_id BIGSERIAL PRIMARY KEY, 1376 user_id VARCHAR(50), 764 1377 action VARCHAR(100) NOT NULL, 765 1378 resource_type VARCHAR(50), … … 768 1381 ip_address VARCHAR(45), 769 1382 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 770 );1383 ); 771 1384 772 1385 CREATE INDEX IF NOT EXISTS idx_product_category ON product(category_id); … … 783 1396 784 1397 await pool.query(schema); 1398 1399 // ------------------------------------------------------------ 1400 // Compatibility migration for older PostgreSQL databases. 1401 // 1402 // Some existing project databases contain a permissions table 1403 // created by an older version of the schema with a typo such as 1404 // personal_is instead of personal_id. CREATE TABLE IF NOT EXISTS cannot add 1405 // missing columns to an existing table, so the admin bootstrap 1406 // INSERT would otherwise fail with PostgreSQL error 42703. 1407 // 1408 // The migration is intentionally non-destructive: it keeps all 1409 // existing rows, renames the typo when possible, and migrates the old typo column into the expected schema without deleting 1410 // permission values. 1411 // ------------------------------------------------------------ 1412 await pool.query(` 1413 DO $$ 1414 BEGIN 1415 -- Some older versions of the database used the typo 1416 -- personal_is instead of personal_id. Rename it rather 1417 -- than adding a second column: the old column may be NOT NULL 1418 -- and would otherwise make the admin bootstrap INSERT fail. 1419 IF EXISTS ( 1420 SELECT 1 1421 FROM information_schema.columns 1422 WHERE table_schema = 'public' 1423 AND table_name = 'permissions' 1424 AND column_name = 'personal_is' 1425 ) AND NOT EXISTS ( 1426 SELECT 1 1427 FROM information_schema.columns 1428 WHERE table_schema = 'public' 1429 AND table_name = 'permissions' 1430 AND column_name = 'personal_id' 1431 ) THEN 1432 ALTER TABLE permissions 1433 RENAME COLUMN personal_is TO personal_id; 1434 END IF; 1435 1436 -- If neither spelling exists, add the expected column. 1437 IF NOT EXISTS ( 1438 SELECT 1 1439 FROM information_schema.columns 1440 WHERE table_schema = 'public' 1441 AND table_name = 'permissions' 1442 AND column_name = 'personal_id' 1443 ) THEN 1444 ALTER TABLE permissions 1445 ADD COLUMN personal_id VARCHAR(10); 1446 END IF; 1447 1448 -- Some databases were already partially migrated and therefore 1449 -- contain BOTH personal_is and personal_id. If the old typo 1450 -- column participates in the primary key, PostgreSQL will not 1451 -- allow us to drop its NOT NULL requirement. Migrate the 1452 -- primary-key data to personal_id first, then remove the old 1453 -- typo column from the key and drop it. 1454 IF EXISTS ( 1455 SELECT 1 1456 FROM information_schema.columns 1457 WHERE table_schema = 'public' 1458 AND table_name = 'permissions' 1459 AND column_name = 'personal_is' 1460 ) AND EXISTS ( 1461 SELECT 1 1462 FROM information_schema.columns 1463 WHERE table_schema = 'public' 1464 AND table_name = 'permissions' 1465 AND column_name = 'personal_id' 1466 ) THEN 1467 -- Copy old primary-key values into the new column where 1468 -- the new column is currently empty. 1469 UPDATE permissions 1470 SET personal_id = personal_is 1471 WHERE personal_id IS NULL 1472 AND personal_is IS NOT NULL; 1473 1474 -- Remove the old typo column from the primary key. 1475 DO $drop_old_permission_pk$ 1476 DECLARE 1477 pk_name TEXT; 1478 BEGIN 1479 SELECT tc.constraint_name 1480 INTO pk_name 1481 FROM information_schema.table_constraints tc 1482 JOIN information_schema.key_column_usage kcu 1483 ON kcu.constraint_name = tc.constraint_name 1484 AND kcu.table_schema = tc.table_schema 1485 AND kcu.table_name = tc.table_name 1486 WHERE tc.table_schema = 'public' 1487 AND tc.table_name = 'permissions' 1488 AND tc.constraint_type = 'PRIMARY KEY' 1489 AND kcu.column_name = 'personal_is' 1490 LIMIT 1; 1491 1492 IF pk_name IS NOT NULL THEN 1493 EXECUTE format( 1494 'ALTER TABLE permissions DROP CONSTRAINT %I', 1495 pk_name 1496 ); 1497 END IF; 1498 END 1499 $drop_old_permission_pk$; 1500 1501 -- The application uses personal_id as the primary key. 1502 -- Drop the obsolete typo column after preserving its data. 1503 ALTER TABLE permissions 1504 DROP COLUMN personal_is; 1505 1506 -- Recreate the primary key on the correct column if one 1507 -- was removed above and no primary key currently exists. 1508 IF NOT EXISTS ( 1509 SELECT 1 1510 FROM information_schema.table_constraints 1511 WHERE table_schema = 'public' 1512 AND table_name = 'permissions' 1513 AND constraint_type = 'PRIMARY KEY' 1514 ) THEN 1515 ALTER TABLE permissions 1516 ADD PRIMARY KEY (personal_id); 1517 END IF; 1518 END IF; 1519 1520 IF NOT EXISTS ( 1521 SELECT 1 1522 FROM information_schema.columns 1523 WHERE table_schema = 'public' 1524 AND table_name = 'permissions' 1525 AND column_name = 'type' 1526 ) THEN 1527 ALTER TABLE permissions 1528 ADD COLUMN type VARCHAR(50); 1529 END IF; 1530 1531 IF NOT EXISTS ( 1532 SELECT 1 1533 FROM information_schema.columns 1534 WHERE table_schema = 'public' 1535 AND table_name = 'permissions' 1536 AND column_name = 'authorisation' 1537 ) THEN 1538 ALTER TABLE permissions 1539 ADD COLUMN authorisation VARCHAR(50); 1540 END IF; 1541 END 1542 $$; 1543 `); 1544 1545 // ON CONFLICT(personal_id) requires a unique/exclusion constraint 1546 // that PostgreSQL can use for conflict inference. A unique index 1547 // permits multiple NULL values, so this remains safe for any legacy 1548 // permission rows that do not have a personal_id yet. 1549 await pool.query(` 1550 CREATE UNIQUE INDEX IF NOT EXISTS 1551 permissions_personal_id_unique 1552 ON permissions(personal_id) 1553 `); 785 1554 786 1555 const roles = [ … … 812 1581 await pool.query(`INSERT INTO boss(boss_id) VALUES('000000') ON CONFLICT(boss_id) DO NOTHING`); 813 1582 await pool.query( 814 `INSERT INTO permissions(personal_i s,type,authorisation)815 VALUES('000000','ADMIN','full_access') ON CONFLICT(personal_i s) DO NOTHING`1583 `INSERT INTO permissions(personal_id,type,authorisation) 1584 VALUES('000000','ADMIN','full_access') ON CONFLICT(personal_id) DO NOTHING` 816 1585 ); 817 1586 await pool.query( … … 832 1601 FILTER (WHERE r.role_id IS NOT NULL), '[]') AS roles 833 1602 FROM users u 834 LEFT JOIN user_roles ur ON ur.user_id=u.id835 LEFT JOIN roles r ON r.role_id=ur.role_id1603 LEFT JOIN user_roles ur ON ur.user_id=u.id 1604 LEFT JOIN roles r ON r.role_id=ur.role_id 836 1605 WHERE u.id=$1 837 1606 GROUP BY u.id`, … … 847 1616 FILTER (WHERE r.role_id IS NOT NULL), '[]') AS roles 848 1617 FROM users u 849 LEFT JOIN user_roles ur ON ur.user_id=u.id850 LEFT JOIN roles r ON r.role_id=ur.role_id1618 LEFT JOIN user_roles ur ON ur.user_id=u.id 1619 LEFT JOIN roles r ON r.role_id=ur.role_id 851 1620 WHERE u.username=$1 OR u.email=$1 852 1621 GROUP BY u.id 853 LIMIT 1`,1622 LIMIT 1`, 854 1623 [username], 855 1624 (err, result) => callback(err, result?.rows?.[0]) … … 937 1706 const sql = `SELECT p.*, c.name AS category_name, LEFT(p.code,3) AS store_id 938 1707 FROM product p 939 LEFT JOIN category c ON c.id=p.category_id940 ${where.length ? 'WHERE ' + where.join(' AND ') : ''}1708 LEFT JOIN category c ON c.id=p.category_id 1709 ${where.length ? 'WHERE ' + where.join(' AND ') : ''} 941 1710 ORDER BY p.code`; 942 1711 dbQuery(sql, params, (err, result) => callback(err, result?.rows || [])); … … 947 1716 `SELECT p.*, c.name AS category_name, LEFT(p.code,3) AS store_id 948 1717 FROM product p 949 LEFT JOIN category c ON c.id=p.category_id1718 LEFT JOIN category c ON c.id=p.category_id 950 1719 WHERE p.code=$1 LIMIT 1`, 951 1720 [String(id)], … … 958 1727 `SELECT p.*, c.name AS category_name, LEFT(p.code,3) AS store_id 959 1728 FROM product p 960 LEFT JOIN category c ON c.id=p.category_id1729 LEFT JOIN category c ON c.id=p.category_id 961 1730 WHERE p.code=$1`, 962 1731 [code], … … 985 1754 `INSERT INTO sells(product_code,store_ID,discount) 986 1755 VALUES($1,$2,$3) 987 ON CONFLICT(product_code,store_ID)1756 ON CONFLICT(product_code,store_ID) 988 1757 DO UPDATE SET discount=EXCLUDED.discount`, 989 1758 [data.code,storeId,data.discount || 0], … … 1092 1861 dbQuery( 1093 1862 `SELECT o.*, LEFT(o.order_num,3) AS store_id, 1094 o.last_date_mod AS order_date,1095 COALESCE(json_agg(json_build_object('product_code',i.product_code,'quantity',i.quantity,'price',p.price))1096 FILTER(WHERE i.product_code IS NOT NULL),'[]') AS items1863 o.last_date_mod AS order_date, 1864 COALESCE(json_agg(json_build_object('product_code',i.product_code,'quantity',i.quantity,'price',p.price)) 1865 FILTER(WHERE i.product_code IS NOT NULL),'[]') AS items 1097 1866 FROM "order" o 1098 LEFT JOIN includes i ON i.order_num=o.order_num1099 LEFT JOIN product p ON p.code=i.product_code1867 LEFT JOIN includes i ON i.order_num=o.order_num 1868 LEFT JOIN product p ON p.code=i.product_code 1100 1869 WHERE o.client_ID=$1 1101 1870 GROUP BY o.order_num … … 1109 1878 `INSERT INTO review(order_num,comment,rating,last_mod_date) 1110 1879 VALUES($1,$2,$3,CURRENT_TIMESTAMP) 1111 RETURNING order_num`,1880 RETURNING order_num`, 1112 1881 [data.order_num,data.comment||null,data.rating], 1113 1882 (err,result)=>callback(err,result?.rows?.[0]?.order_num) … … 1162 1931 getAllOrders(callback) { 1163 1932 dbQuery(`SELECT o.*, LEFT(o.order_num,3) AS store_id, o.last_date_mod AS order_date, 1164 c.first_name,c.last_name,c.email1933 c.first_name,c.last_name,c.email 1165 1934 FROM "order" o 1166 LEFT JOIN client c ON c.client_id=o.client_ID1935 LEFT JOIN client c ON c.client_id=o.client_ID 1167 1936 ORDER BY o.last_date_mod DESC`, 1168 1937 [],(err,result)=>callback(err,result?.rows||[])); … … 1172 1941 dbQuery(`SELECT p.*,c.name AS category_name,LEFT(p.code,3) AS store_id,s.discount 1173 1942 FROM product p 1174 LEFT JOIN category c ON c.id=p.category_id1175 LEFT JOIN sells s ON s.product_code=p.code AND s.store_ID=$11943 LEFT JOIN category c ON c.id=p.category_id 1944 LEFT JOIN sells s ON s.product_code=p.code AND s.store_ID=$1 1176 1945 WHERE LEFT(p.code,3)=$1 OR EXISTS(SELECT 1 FROM sells sx WHERE sx.product_code=p.code AND sx.store_ID=$1) 1177 1946 ORDER BY p.code`,[storeId],(err,result)=>callback(err,result?.rows||[])); … … 1181 1950 dbQuery(`SELECT o.*,LEFT(o.order_num,3) AS store_id,o.last_date_mod AS order_date,c.first_name,c.last_name 1182 1951 FROM "order" o 1183 LEFT JOIN client c ON c.client_id=o.client_ID1952 LEFT JOIN client c ON c.client_id=o.client_ID 1184 1953 WHERE LEFT(o.order_num,3)=$1 ORDER BY o.last_date_mod DESC`,[storeId], 1185 1954 (err,result)=>callback(err,result?.rows||[])); … … 1189 1958 dbQuery(`SELECT p.*,e.date_of_hire,per.type,per.authorisation 1190 1959 FROM personal p JOIN works_in_store w ON w.personal_id=p.id 1191 LEFT JOIN employees e ON e.employee_id=p.id1192 LEFT JOIN permissions per ON per.personal_is=p.id1960 LEFT JOIN employees e ON e.employee_id=p.id 1961 LEFT JOIN permissions per ON per.personal_id=p.id 1193 1962 WHERE w.store_ID=$1 ORDER BY p.last_name,p.first_name`, 1194 1963 [storeId],(err,result)=>callback(err,result?.rows||[])); … … 1196 1965 1197 1966 getStoreReports(storeId, callback) { 1198 dbQuery(`SELECT * FROM report WHERE store_ID=$1 ORDER BY date DESC`,[storeId], 1199 (err,result)=>callback(err,result?.rows||[])); 1967 dbQuery( 1968 `SELECT date, store_id, overall_profit, sales_trend, marketing_growth, owner_signature 1969 FROM report 1970 WHERE store_id = $1 1971 ORDER BY date DESC`, 1972 [storeId], 1973 (err, result) => callback(err, result?.rows || []) 1974 ); 1200 1975 }, 1201 1976 1202 1977 getStoreStats(storeId, callback) { 1203 const sql=`SELECT 1204 (SELECT COUNT(*) FROM product WHERE LEFT(code,3)=$1)::int AS product_count, 1205 (SELECT COUNT(*) FROM "order" WHERE LEFT(order_num,3)=$1)::int AS order_count, 1206 (SELECT COALESCE(SUM(i.quantity*p.price*(1-o.discount/100)),0) 1207 FROM "order" o JOIN includes i ON i.order_num=o.order_num JOIN product p ON p.code=i.product_code 1208 WHERE LEFT(o.order_num,3)=$1) AS revenue, 1209 (SELECT COUNT(*) FROM works_in_store WHERE store_ID=$1)::int AS employee_count, 1210 (SELECT COUNT(*) FROM for_store WHERE store_ID=$1)::int AS request_count, 1211 (SELECT COUNT(*) FROM refund r JOIN "order" o ON o.order_num=r.order_num WHERE LEFT(o.order_num,3)=$1)::int AS refund_count`; 1212 dbQuery(sql,[storeId],(err,result)=>callback(err,result?.rows?.[0]||{})); 1978 const sql = ` 1979 SELECT 1980 (SELECT COUNT(DISTINCT product_code) 1981 FROM sells 1982 WHERE store_ID = $1)::int AS product_count, 1983 1984 (SELECT COUNT(DISTINCT o.order_num) 1985 FROM sells se 1986 JOIN includes i ON i.product_code = se.product_code 1987 JOIN "order" o ON o.order_num = i.order_num 1988 WHERE se.store_ID = $1)::int AS order_count, 1989 1990 (SELECT COALESCE(SUM( 1991 i.quantity * p.price 1992 * (1 - COALESCE(o.discount, 0) / 100.0) 1993 ), 0) 1994 FROM sells se 1995 JOIN includes i ON i.product_code = se.product_code 1996 JOIN "order" o ON o.order_num = i.order_num 1997 JOIN product p ON p.code = i.product_code 1998 WHERE se.store_ID = $1) AS revenue, 1999 2000 (SELECT COUNT(*) 2001 FROM works_in_store 2002 WHERE store_ID = $1)::int AS employee_count, 2003 2004 (SELECT COUNT(*) 2005 FROM for_store 2006 WHERE store_ID = $1)::int AS request_count, 2007 2008 (SELECT COUNT(*) 2009 FROM refund r 2010 JOIN "order" o ON o.order_num = r.order_num 2011 WHERE LEFT(o.order_num, 3) = $1)::int AS refund_count 2012 `; 2013 2014 dbQuery(sql, [storeId], (err, result) => { 2015 callback(err, result?.rows?.[0] || {}); 2016 }); 2017 }, 2018 2019 generateStoreReport(storeId, startDate, endDate, type, period, ownerSignature, callback) { 2020 dbQuery( 2021 `WITH sales AS ( 2022 SELECT COALESCE(SUM( 2023 p.price * i.quantity 2024 * (1 - COALESCE(o.discount, 0) / 100.0) 2025 ), 0) AS revenue 2026 FROM sells se 2027 JOIN product p ON p.code = se.product_code 2028 JOIN includes i ON i.product_code = se.product_code 2029 JOIN "order" o ON o.order_num = i.order_num 2030 WHERE se.store_ID = $1 2031 AND o.last_date_mod >= $2::timestamp 2032 AND o.last_date_mod < ($3::timestamp + INTERVAL '1 day') 2033 ), 2034 refunds AS ( 2035 SELECT COALESCE(SUM(rf.amount), 0) AS refund_total 2036 FROM refund rf 2037 JOIN "order" o ON o.order_num = rf.order_num 2038 WHERE LEFT(o.order_num, 3) = $1 2039 AND o.last_date_mod >= $2::timestamp 2040 AND o.last_date_mod < ($3::timestamp + INTERVAL '1 day') 2041 AND rf.status IN ('approved', 'processed') 2042 ) 2043 SELECT sales.revenue, refunds.refund_total, 2044 sales.revenue - refunds.refund_total AS net_profit 2045 FROM sales CROSS JOIN refunds`, 2046 [storeId, startDate, endDate], 2047 (err, result) => { 2048 if (err) return callback(err); 2049 2050 const row = result.rows[0] || {}; 2051 const revenue = Number(row.revenue || 0); 2052 const refundTotal = Number(row.refund_total || 0); 2053 const netProfit = Number(row.net_profit || 0); 2054 2055 dbQuery( 2056 `SELECT COALESCE(SUM( 2057 p.price * i.quantity 2058 * (1 - COALESCE(o.discount, 0) / 100.0) 2059 ), 0) AS previous_revenue 2060 FROM sells se 2061 JOIN product p ON p.code = se.product_code 2062 JOIN includes i ON i.product_code = se.product_code 2063 JOIN "order" o ON o.order_num = i.order_num 2064 WHERE se.store_ID = $1 2065 AND o.last_date_mod >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month' 2066 AND o.last_date_mod < DATE_TRUNC('month', CURRENT_DATE)`, 2067 [storeId], 2068 (previousErr, previousResult) => { 2069 if (previousErr) return callback(previousErr); 2070 2071 const previousRevenue = Number(previousResult.rows[0]?.previous_revenue || 0); 2072 const growth = previousRevenue === 0 2073 ? (revenue > 0 ? 100 : 0) 2074 : ((revenue - previousRevenue) / previousRevenue) * 100; 2075 2076 dbQuery( 2077 `INSERT INTO report 2078 (date, store_ID, overall_profit, sales_trend, marketing_growth, owner_signature) 2079 VALUES 2080 (CURRENT_TIMESTAMP, $1, $2, $3, $4, $5) 2081 RETURNING date, store_ID, overall_profit, sales_trend, marketing_growth, owner_signature`, 2082 [ 2083 storeId, 2084 Math.max(0, netProfit), 2085 `Revenue ${revenue.toFixed(2)}; Refunds ${refundTotal.toFixed(2)}`.slice(0, 100), 2086 `${growth.toFixed(2)}%`, 2087 ownerSignature || 'Not signed yet' 2088 ], 2089 (insertErr, insertResult) => { 2090 if (insertErr) return callback(insertErr); 2091 2092 const report = insertResult.rows[0]; 2093 2094 dbQuery( 2095 `INSERT INTO monthly_profit 2096 (report_date, store_ID, month_and_year, profit) 2097 VALUES 2098 ($1, $2, DATE_TRUNC('month', $3::timestamp)::DATE, $4) 2099 ON CONFLICT (report_date, store_ID) 2100 DO UPDATE SET 2101 month_and_year = EXCLUDED.month_and_year, 2102 profit = EXCLUDED.profit`, 2103 [report.date, storeId, endDate, Math.max(0, netProfit)], 2104 (monthlyErr) => { 2105 if (monthlyErr) console.error('Warning inserting monthly profit:', monthlyErr); 2106 2107 dbQuery( 2108 `INSERT INTO exchanges_data 2109 (report_date, store_ID, monthly_profit, date, sales, damages) 2110 VALUES ($1, $2, $3, CURRENT_TIMESTAMP, $4, $5) 2111 ON CONFLICT (report_date, store_ID) 2112 DO UPDATE SET 2113 monthly_profit = EXCLUDED.monthly_profit, 2114 date = EXCLUDED.date, 2115 sales = EXCLUDED.sales, 2116 damages = EXCLUDED.damages`, 2117 [report.date, storeId, Math.max(0, netProfit), revenue, -refundTotal], 2118 (exchangeErr) => { 2119 if (exchangeErr) console.error('Warning inserting exchange data:', exchangeErr); 2120 callback(null, report); 2121 } 2122 ); 2123 } 2124 ); 2125 } 2126 ); 2127 } 2128 ); 2129 } 2130 ); 1213 2131 }, 1214 2132 … … 1216 2134 dbQuery(`SELECT r.*,a.personal_id AS answered_by 1217 2135 FROM request r 1218 JOIN for_store fs ON fs.request_num=r.request_num1219 LEFT JOIN answers a ON a.request_num=r.request_num2136 JOIN for_store fs ON fs.request_num=r.request_num 2137 LEFT JOIN answers a ON a.request_num=r.request_num 1220 2138 WHERE fs.store_ID=$1 AND (a.personal_id=$2 OR a.personal_id IS NULL) 1221 2139 ORDER BY r.date_and_time DESC`, … … 1225 2143 getClientStats(clientId, callback) { 1226 2144 dbQuery(`SELECT 1227 (SELECT COUNT(*) FROM "order" WHERE client_ID=$1)::int AS order_count,1228 (SELECT COUNT(*) FROM review r JOIN "order" o ON o.order_num=r.order_num WHERE o.client_ID=$1)::int AS review_count,1229 (SELECT COUNT(*) FROM makes_request WHERE client_ID=$1)::int AS request_count,1230 (SELECT COUNT(*) FROM refund r JOIN "order" o ON o.order_num=r.order_num WHERE o.client_ID=$1)::int AS refund_count`,2145 (SELECT COUNT(*) FROM "order" WHERE client_ID=$1)::int AS order_count, 2146 (SELECT COUNT(*) FROM review r JOIN "order" o ON o.order_num=r.order_num WHERE o.client_ID=$1)::int AS review_count, 2147 (SELECT COUNT(*) FROM makes_request WHERE client_ID=$1)::int AS request_count, 2148 (SELECT COUNT(*) FROM refund r JOIN "order" o ON o.order_num=r.order_num WHERE o.client_ID=$1)::int AS refund_count`, 1231 2149 [clientId],(err,result)=>callback(err,result?.rows?.[0]||{})); 1232 2150 } … … 1243 2161 try { 1244 2162 await database.initializeDatabase(); 2163 await database.installReportFunctions(); 1245 2164 console.log('✅ Database initialization completed'); 1246 2165 } catch (err) { … … 4768 5687 } 4769 5688 5689 else if (pathname === '/api/advanced-reports' && req.method === 'GET') { 5690 requireRole('admin')(req, res, () => { 5691 const reportName = parsedUrl.query.report; 5692 5693 if (!reportName) { 5694 res.writeHead(400, { 'Content-Type': 'application/json' }); 5695 res.end(JSON.stringify({ 5696 success: false, 5697 message: 'The report query parameter is required' 5698 })); 5699 return; 5700 } 5701 5702 let params = []; 5703 5704 if (reportName === 'get_low_stock_high_demand_products') { 5705 const stockThreshold = Number(parsedUrl.query.stockThreshold ?? 5); 5706 const demandThreshold = Number(parsedUrl.query.demandThreshold ?? 5); 5707 5708 if (!Number.isInteger(stockThreshold) || !Number.isInteger(demandThreshold)) { 5709 res.writeHead(400, { 'Content-Type': 'application/json' }); 5710 res.end(JSON.stringify({ 5711 success: false, 5712 message: 'stockThreshold and demandThreshold must be integers' 5713 })); 5714 return; 5715 } 5716 5717 params = [stockThreshold, demandThreshold]; 5718 } 5719 5720 database.runReport(reportName, params, (err, rows) => { 5721 if (err) { 5722 console.error('Error executing report:', err); 5723 res.writeHead(500, { 'Content-Type': 'application/json' }); 5724 res.end(JSON.stringify({ 5725 success: false, 5726 message: 'Error executing report: ' + err.message 5727 })); 5728 return; 5729 } 5730 5731 res.writeHead(200, { 'Content-Type': 'application/json' }); 5732 res.end(JSON.stringify({ 5733 success: true, 5734 report: reportName, 5735 rows 5736 })); 5737 }); 5738 }); 5739 } 5740 4770 5741 else if (pathname === '/api/employee-tasks' && req.method === 'GET') { 4771 5742 requireAuth(req, res, (userId) => { … … 4902 5873 requireStoreOwner()(req, res, (personalId) => { 4903 5874 let body = ''; 5875 4904 5876 req.on('data', chunk => { 4905 5877 body += chunk.toString(); 4906 5878 }); 5879 4907 5880 req.on('end', () => { 4908 const { storeId, period, startDate, endDate, type } = JSON.parse(body); 5881 let data; 5882 5883 try { 5884 data = JSON.parse(body); 5885 } catch (err) { 5886 res.writeHead(400, { 'Content-Type': 'application/json' }); 5887 res.end(JSON.stringify({ 5888 success: false, 5889 message: 'Invalid JSON request body' 5890 })); 5891 return; 5892 } 5893 5894 const { 5895 storeId, 5896 period, 5897 startDate, 5898 endDate, 5899 type, 5900 ownerSignature 5901 } = data; 4909 5902 4910 5903 if (!storeId || !period || !startDate || !endDate || !type) { 4911 5904 res.writeHead(400, { 'Content-Type': 'application/json' }); 4912 res.end(JSON.stringify({ success: false, message: 'All fields are required' })); 5905 res.end(JSON.stringify({ 5906 success: false, 5907 message: 'All fields are required' 5908 })); 4913 5909 return; 4914 5910 } … … 4920 5916 if (err || !ownsStore) { 4921 5917 res.writeHead(403, { 'Content-Type': 'application/json' }); 4922 res.end(JSON.stringify({ success: false, message: 'You are not authorized to generate reports for this store' })); 5918 res.end(JSON.stringify({ 5919 success: false, 5920 message: 'You are not authorized to generate reports for this store' 5921 })); 4923 5922 return; 4924 5923 } 4925 5924 4926 const reportId = 'RPT' + Date.now().toString().slice(-6); 4927 4928 database.database.run( 4929 'INSERT INTO report (date, store_ID, overall_profit, sales_trend, marketing_growth, owner_signature) VALUES (CURRENT_TIMESTAMP, $1, 0, $2, $3, $4)', 4930 [storeId, period, type, 'Not signed yet'], 4931 function(err) { 4932 if (err) { 4933 console.error('Error generating report:', err); 5925 database.generateStoreReport( 5926 storeId, 5927 startDate, 5928 endDate, 5929 type, 5930 period, 5931 ownerSignature, 5932 (reportErr, report) => { 5933 if (reportErr) { 5934 console.error('Error generating report:', reportErr); 4934 5935 res.writeHead(500, { 'Content-Type': 'application/json' }); 4935 res.end(JSON.stringify({ success: false, message: 'Error generating report: ' + err.message }));4936 } else {4937 database.logAudit(personalId, 'REPORT_GENERATED', 'report', reportId, `Report generated: ${type} for ${period}`, ipAddress);4938 4939 res.writeHead(200, { 'Content-Type': 'application/json' });4940 5936 res.end(JSON.stringify({ 4941 success: true, 4942 message: 'Report generated successfully', 4943 reportId: reportId, 4944 report: { 4945 id: reportId, 4946 storeId: storeId, 4947 period: period, 4948 startDate: startDate, 4949 endDate: endDate, 4950 type: type, 4951 generatedBy: personalId, 4952 generatedAt: new Date().toISOString() 4953 } 5937 success: false, 5938 message: 'Error generating report: ' + reportErr.message 4954 5939 })); 5940 return; 4955 5941 } 5942 5943 const reportId = 5944 'RPT' + 5945 new Date(report.date).getTime().toString().slice(-6); 5946 5947 database.logAudit( 5948 personalId, 5949 'REPORT_GENERATED', 5950 'report', 5951 reportId, 5952 `Report generated: ${type} for ${period}`, 5953 ipAddress 5954 ); 5955 5956 res.writeHead(200, { 'Content-Type': 'application/json' }); 5957 res.end(JSON.stringify({ 5958 success: true, 5959 message: 'Report generated successfully', 5960 reportId, 5961 report: { 5962 id: reportId, 5963 storeId: report.store_id, 5964 period, 5965 startDate, 5966 endDate, 5967 type, 5968 generatedBy: personalId, 5969 generatedAt: report.date, 5970 overallProfit: report.overall_profit, 5971 salesTrend: report.sales_trend, 5972 marketingGrowth: report.marketing_growth, 5973 ownerSignature: report.owner_signature 5974 } 5975 })); 4956 5976 } 4957 5977 );
Note:
See TracChangeset
for help on using the changeset viewer.
