- 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)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
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.
