Changeset 6c7cfa6 for server.js


Ignore:
Timestamp:
09/21/26 00:41:03 (9 days ago)
Author:
Klimentina Efremova <klimentina08642@…>
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)
Message:

Implemented Advaced database reports

File:
1 edited

Legend:

Unmodified
Added
Removed
  • server.js

    r6149556 r6c7cfa6  
    77const nodemailer = require('nodemailer');
    88const bcrypt = require('bcryptjs');
     9const { AsyncLocalStorage } = require('async_hooks');
    910require('dotenv').config();
    1011
    … …  
    356357});
    357358
    358 let transactionClient = null;
     359const transactionStorage = new AsyncLocalStorage();
    359360
    360361function dbQuery(sql, params = [], callback) {
    361     const client = transactionClient || pool;
     362    const client = transactionStorage.getStore() || pool;
     363
    362364    client.query(sql, params)
    363365        .then(result => callback(null, result))
    364366        .catch(err => callback(err));
    365367}
     368
     369const REPORT_FUNCTIONS_SQL = String.raw`
     370-- ============================================================
     371-- HANDCRAFT MARKETPLACE REPORT FUNCTIONS
     372-- PostgreSQL / exact project schema
     373-- ============================================================
     374
     375CREATE OR REPLACE FUNCTION get_orders_by_total()
     376RETURNS TABLE (
     377    order_num VARCHAR(11),
     378    client_id INTEGER,
     379    client_name TEXT,
     380    order_quantity BIGINT,
     381    order_status VARCHAR(20),
     382    payment_method VARCHAR(250),
     383    discount NUMERIC,
     384    order_total NUMERIC
     385)
     386LANGUAGE sql
     387AS $$
     388    SELECT
     389        o.order_num,
     390        o.client_id,
     391        CONCAT_WS(' ', c.first_name, c.last_name) AS client_name,
     392        COALESCE(SUM(i.quantity), 0)::BIGINT AS order_quantity,
     393        o.status,
     394        o.payment_method,
     395        COALESCE(o.discount, 0)::NUMERIC AS discount,
     396        ROUND(
     397            COALESCE(SUM(p.price * i.quantity), 0)
     398            * (1 - COALESCE(o.discount, 0) / 100.0),
     399            2
     400        ) AS order_total
     401    FROM "order" o
     402    LEFT JOIN client c ON c.client_id = o.client_id
     403    LEFT JOIN includes i ON i.order_num = o.order_num
     404    LEFT JOIN product p ON p.code = i.product_code
     405    GROUP BY
     406        o.order_num, o.client_id, c.first_name, c.last_name,
     407        o.status, o.payment_method, o.discount
     408    ORDER BY 8 DESC, o.order_num;
     409$$;
     410
     411CREATE OR REPLACE FUNCTION get_products_by_total_sales()
     412RETURNS TABLE (
     413    product_code VARCHAR(8),
     414    product_description VARCHAR(500),
     415    product_price NUMERIC,
     416    number_of_orders BIGINT,
     417    total_quantity_sold BIGINT,
     418    total_revenue NUMERIC
     419)
     420LANGUAGE sql
     421AS $$
     422    SELECT
     423        p.code,
     424        p.description,
     425        p.price::NUMERIC,
     426        COUNT(DISTINCT i.order_num) AS number_of_orders,
     427        COALESCE(SUM(i.quantity), 0)::BIGINT AS total_quantity_sold,
     428        ROUND(
     429            COALESCE(
     430                SUM(
     431                    p.price * i.quantity
     432                    * (1 - COALESCE(o.discount, 0) / 100.0)
     433                ),
     434                0
     435            ),
     436            2
     437        ) AS total_revenue
     438    FROM product p
     439    LEFT JOIN includes i ON i.product_code = p.code
     440    LEFT JOIN "order" o ON o.order_num = i.order_num
     441    GROUP BY p.code, p.description, p.price
     442    ORDER BY 4 DESC, 5 DESC, 6 DESC;
     443$$;
     444
     445CREATE OR REPLACE FUNCTION get_low_stock_high_demand_products(
     446    p_stock_threshold INTEGER,
     447    p_demand_threshold INTEGER
     448)
     449RETURNS TABLE (
     450    product_code VARCHAR(8),
     451    product_description VARCHAR(500),
     452    current_stock INTEGER,
     453    number_of_orders BIGINT,
     454    total_quantity_sold BIGINT
     455)
     456LANGUAGE sql
     457AS $$
     458    SELECT
     459        p.code,
     460        p.description,
     461        p.availability,
     462        COUNT(DISTINCT i.order_num),
     463        COALESCE(SUM(i.quantity), 0)::BIGINT
     464    FROM product p
     465    JOIN includes i ON i.product_code = p.code
     466    GROUP BY p.code, p.description, p.availability
     467    HAVING
     468        p.availability < p_stock_threshold
     469        AND COUNT(DISTINCT i.order_num) >= p_demand_threshold
     470    ORDER BY 4 DESC, 5 DESC, 3 ASC;
     471$$;
     472
     473CREATE OR REPLACE FUNCTION get_products_monthly_sales()
     474RETURNS TABLE (
     475    product_code VARCHAR(8),
     476    product_description VARCHAR(500),
     477    year INTEGER,
     478    month INTEGER,
     479    number_of_orders BIGINT,
     480    total_quantity_sold BIGINT,
     481    total_revenue NUMERIC
     482)
     483LANGUAGE sql
     484AS $$
     485    SELECT
     486        p.code,
     487        p.description,
     488        EXTRACT(YEAR FROM o.last_date_mod)::INTEGER,
     489        EXTRACT(MONTH FROM o.last_date_mod)::INTEGER,
     490        COUNT(DISTINCT o.order_num),
     491        COALESCE(SUM(i.quantity), 0)::BIGINT,
     492        ROUND(
     493            COALESCE(
     494                SUM(
     495                    p.price * i.quantity
     496                    * (1 - COALESCE(o.discount, 0) / 100.0)
     497                ),
     498                0
     499            ),
     500            2
     501        )
     502    FROM product p
     503    JOIN includes i ON i.product_code = p.code
     504    JOIN "order" o ON o.order_num = i.order_num
     505    GROUP BY
     506        p.code, p.description,
     507        EXTRACT(YEAR FROM o.last_date_mod),
     508        EXTRACT(MONTH FROM o.last_date_mod)
     509    ORDER BY 3 DESC, 4 DESC, 7 DESC;
     510$$;
     511
     512CREATE OR REPLACE FUNCTION get_stores_by_last_calendar_year_revenue()
     513RETURNS TABLE (
     514    store_id VARCHAR(3),
     515    store_name VARCHAR(50),
     516    number_of_orders BIGINT,
     517    total_quantity_sold BIGINT,
     518    total_revenue NUMERIC
     519)
     520LANGUAGE sql
     521AS $$
     522    SELECT
     523        s.store_id,
     524        s.name,
     525        COUNT(DISTINCT o.order_num),
     526        COALESCE(SUM(i.quantity), 0)::BIGINT,
     527        ROUND(
     528            COALESCE(
     529                SUM(
     530                    p.price * i.quantity
     531                    * (1 - COALESCE(o.discount, 0) / 100.0)
     532                ),
     533                0
     534            ),
     535            2
     536        )
     537    FROM store s
     538    LEFT JOIN sells se ON se.store_id = s.store_id
     539    LEFT JOIN product p ON p.code = se.product_code
     540    LEFT JOIN includes i ON i.product_code = p.code
     541    LEFT JOIN "order" o
     542        ON o.order_num = i.order_num
     543       AND o.last_date_mod >= DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '1 year'
     544       AND o.last_date_mod < DATE_TRUNC('year', CURRENT_DATE)
     545    GROUP BY s.store_id, s.name
     546    ORDER BY 5 DESC, s.store_id;
     547$$;
     548
     549CREATE OR REPLACE FUNCTION get_products_never_ordered()
     550RETURNS TABLE (
     551    product_code VARCHAR(8),
     552    product_description VARCHAR(500),
     553    product_price NUMERIC,
     554    current_stock INTEGER
     555)
     556LANGUAGE sql
     557AS $$
     558    SELECT p.code, p.description, p.price::NUMERIC, p.availability
     559    FROM product p
     560    WHERE NOT EXISTS (
     561        SELECT 1
     562        FROM includes i
     563        WHERE i.product_code = p.code
     564    )
     565    ORDER BY p.code;
     566$$;
     567
     568CREATE OR REPLACE FUNCTION get_products_by_number_of_orders()
     569RETURNS TABLE (
     570    product_code VARCHAR(8),
     571    product_description VARCHAR(500),
     572    product_price NUMERIC,
     573    number_of_orders BIGINT
     574)
     575LANGUAGE sql
     576AS $$
     577    SELECT
     578        p.code,
     579        p.description,
     580        p.price::NUMERIC,
     581        COUNT(DISTINCT i.order_num)
     582    FROM product p
     583    JOIN includes i ON i.product_code = p.code
     584    GROUP BY p.code, p.description, p.price
     585    ORDER BY 4 DESC, p.code;
     586$$;
     587
     588CREATE OR REPLACE FUNCTION get_stores_by_average_review()
     589RETURNS TABLE (
     590    store_id VARCHAR(3),
     591    store_name VARCHAR(50),
     592    average_review NUMERIC,
     593    number_of_reviews BIGINT
     594)
     595LANGUAGE sql
     596AS $$
     597    WITH store_reviews AS (
     598        SELECT DISTINCT
     599            s.store_id,
     600            s.name AS store_name,
     601            r.order_num,
     602            r.rating
     603        FROM store s
     604        JOIN sells se ON se.store_id = s.store_id
     605        JOIN includes i ON i.product_code = se.product_code
     606        JOIN review r ON r.order_num = i.order_num
     607    )
     608    SELECT
     609        s.store_id,
     610        s.name,
     611        COALESCE(ROUND(AVG(sr.rating), 2), 0)::NUMERIC,
     612        COUNT(sr.order_num)
     613    FROM store s
     614    LEFT JOIN store_reviews sr ON sr.store_id = s.store_id
     615    GROUP BY s.store_id, s.name
     616    ORDER BY 3 DESC, s.store_id;
     617$$;
     618
     619CREATE OR REPLACE FUNCTION get_store_with_highest_revenue_growth()
     620RETURNS TABLE (
     621    store_id VARCHAR(3),
     622    store_name VARCHAR(50),
     623    previous_year_revenue NUMERIC,
     624    last_year_revenue NUMERIC,
     625    revenue_growth NUMERIC
     626)
     627LANGUAGE sql
     628AS $$
     629    WITH store_years AS (
     630        SELECT
     631            s.store_id,
     632            s.name AS store_name,
     633            EXTRACT(YEAR FROM o.last_date_mod)::INTEGER AS sales_year,
     634            SUM(
     635                p.price * i.quantity
     636                * (1 - COALESCE(o.discount, 0) / 100.0)
     637            ) AS revenue
     638        FROM store s
     639        JOIN sells se ON se.store_id = s.store_id
     640        JOIN includes i ON i.product_code = se.product_code
     641        JOIN product p ON p.code = i.product_code
     642        JOIN "order" o ON o.order_num = i.order_num
     643        WHERE o.last_date_mod >= DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '2 years'
     644          AND o.last_date_mod < DATE_TRUNC('year', CURRENT_DATE)
     645        GROUP BY s.store_id, s.name, EXTRACT(YEAR FROM o.last_date_mod)
     646    ),
     647    comparison AS (
     648        SELECT
     649            s.store_id,
     650            s.name AS store_name,
     651            COALESCE(MAX(CASE
     652                WHEN sy.sales_year = EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 2
     653                THEN sy.revenue ELSE 0 END), 0) AS previous_year_revenue,
     654            COALESCE(MAX(CASE
     655                WHEN sy.sales_year = EXTRACT(YEAR FROM CURRENT_DATE)::INTEGER - 1
     656                THEN sy.revenue ELSE 0 END), 0) AS last_year_revenue
     657        FROM store s
     658        LEFT JOIN store_years sy ON sy.store_id = s.store_id
     659        GROUP BY s.store_id, s.name
     660    )
     661    SELECT
     662        store_id,
     663        store_name,
     664        ROUND(previous_year_revenue, 2),
     665        ROUND(last_year_revenue, 2),
     666        ROUND(last_year_revenue - previous_year_revenue, 2)
     667    FROM comparison
     668    ORDER BY 5 DESC, store_id
     669    LIMIT 1;
     670$$;
     671
     672CREATE OR REPLACE FUNCTION get_clients_by_number_of_orders()
     673RETURNS TABLE (
     674    client_id INTEGER,
     675    client_name TEXT,
     676    number_of_orders BIGINT
     677)
     678LANGUAGE sql
     679AS $$
     680    SELECT
     681        c.client_id,
     682        CONCAT_WS(' ', c.first_name, c.last_name),
     683        COUNT(o.order_num)
     684    FROM client c
     685    JOIN "order" o ON o.client_id = c.client_id
     686    GROUP BY c.client_id, c.first_name, c.last_name
     687    ORDER BY 3 DESC, c.client_id;
     688$$;
     689
     690CREATE OR REPLACE FUNCTION get_approximate_orders_per_client()
     691RETURNS TABLE (
     692    total_clients BIGINT,
     693    total_orders BIGINT,
     694    approximate_orders_per_client NUMERIC
     695)
     696LANGUAGE sql
     697AS $$
     698    SELECT
     699        (SELECT COUNT(*) FROM client),
     700        (SELECT COUNT(*) FROM "order"),
     701        ROUND(
     702            (SELECT COUNT(*)::NUMERIC FROM "order")
     703            / NULLIF((SELECT COUNT(*) FROM client), 0),
     704            2
     705        );
     706$$;
     707
     708CREATE OR REPLACE FUNCTION get_clients_without_orders()
     709RETURNS TABLE (
     710    client_id INTEGER,
     711    client_name TEXT,
     712    email VARCHAR(50)
     713)
     714LANGUAGE sql
     715AS $$
     716    SELECT
     717        c.client_id,
     718        CONCAT_WS(' ', c.first_name, c.last_name),
     719        c.email
     720    FROM client c
     721    WHERE NOT EXISTS (
     722        SELECT 1 FROM "order" o WHERE o.client_id = c.client_id
     723    )
     724    ORDER BY c.client_id;
     725$$;
     726
     727CREATE OR REPLACE FUNCTION get_store_request_statistics()
     728RETURNS TABLE (
     729    store_id VARCHAR(3),
     730    store_name VARCHAR(50),
     731    total_requests BIGINT,
     732    solved_requests BIGINT,
     733    requests_in_progress BIGINT
     734)
     735LANGUAGE sql
     736AS $$
     737    SELECT
     738        s.store_id,
     739        s.name,
     740        COUNT(r.request_num),
     741        COUNT(r.request_num) FILTER (WHERE r.customer_satisfaction > 0),
     742        COUNT(r.request_num) FILTER (WHERE r.customer_satisfaction <= 0)
     743    FROM store s
     744    LEFT JOIN for_store fs ON fs.store_id = s.store_id
     745    LEFT JOIN request r ON r.request_num = fs.request_num
     746    GROUP BY s.store_id, s.name
     747    ORDER BY 3 DESC, s.store_id;
     748$$;
     749
     750CREATE OR REPLACE FUNCTION get_top_10_employees_by_requests_last_month()
     751RETURNS TABLE (
     752    employee_id VARCHAR(10),
     753    employee_name TEXT,
     754    number_of_requests BIGINT
     755)
     756LANGUAGE sql
     757AS $$
     758    SELECT
     759        e.employee_id,
     760        CONCAT_WS(' ', p.first_name, p.last_name),
     761        COUNT(DISTINCT a.request_num)
     762    FROM employees e
     763    JOIN personal p ON p.id = e.employee_id
     764    JOIN answers a ON a.personal_id = e.employee_id
     765    JOIN request r ON r.request_num = a.request_num
     766    WHERE r.date_and_time >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month'
     767      AND r.date_and_time < DATE_TRUNC('month', CURRENT_DATE)
     768    GROUP BY e.employee_id, p.first_name, p.last_name
     769    ORDER BY 3 DESC, e.employee_id
     770    LIMIT 10;
     771$$;
     772
     773CREATE OR REPLACE FUNCTION get_employees_by_hours_and_pay()
     774RETURNS TABLE (
     775    employee_id VARCHAR(10),
     776    employee_name TEXT,
     777    total_hours_worked NUMERIC,
     778    total_pay NUMERIC
     779)
     780LANGUAGE sql
     781AS $$
     782    SELECT
     783        e.employee_id,
     784        CONCAT_WS(' ', p.first_name, p.last_name),
     785        COALESCE(SUM(w.total_hours), 0),
     786        COALESCE(SUM(w.wage * w.total_hours), 0)
     787    FROM employees e
     788    JOIN personal p ON p.id = e.employee_id
     789    LEFT JOIN worked w ON w.personal_id = e.employee_id
     790    GROUP BY e.employee_id, p.first_name, p.last_name
     791    ORDER BY 3 DESC, 4 DESC, e.employee_id;
     792$$;
     793
     794CREATE OR REPLACE FUNCTION get_stores_average_pay()
     795RETURNS TABLE (
     796    store_id VARCHAR(3),
     797    store_name VARCHAR(50),
     798    average_pay NUMERIC
     799)
     800LANGUAGE sql
     801AS $$
     802    SELECT
     803        s.store_id,
     804        s.name,
     805        COALESCE(ROUND(AVG(w.wage), 2), 0)
     806    FROM store s
     807    LEFT JOIN worked w ON w.store_id = s.store_id
     808    GROUP BY s.store_id, s.name
     809    ORDER BY 3 DESC, s.store_id;
     810$$;
     811
     812CREATE OR REPLACE FUNCTION get_employee_product_changes_last_month()
     813RETURNS TABLE (
     814    employee_id VARCHAR(10),
     815    employee_name TEXT,
     816    number_of_product_changes BIGINT
     817)
     818LANGUAGE sql
     819AS $$
     820    SELECT
     821        e.employee_id,
     822        CONCAT_WS(' ', p.first_name, p.last_name),
     823        COUNT(mc.change_date_time)
     824    FROM employees e
     825    JOIN personal p ON p.id = e.employee_id
     826    LEFT JOIN makes_change mc
     827        ON mc.personal_id = e.employee_id
     828       AND mc.change_date_time >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month'
     829       AND mc.change_date_time < DATE_TRUNC('month', CURRENT_DATE)
     830    GROUP BY e.employee_id, p.first_name, p.last_name
     831    ORDER BY 3 DESC, e.employee_id;
     832$$;
     833
     834CREATE OR REPLACE FUNCTION get_stores_by_monthly_profit_and_revenue_growth()
     835RETURNS TABLE (
     836    store_id VARCHAR(3),
     837    store_name VARCHAR(50),
     838    month_and_year TEXT,
     839    monthly_profit NUMERIC,
     840    previous_month_revenue NUMERIC,
     841    current_month_revenue NUMERIC,
     842    revenue_growth NUMERIC
     843)
     844LANGUAGE sql
     845AS $$
     846    WITH monthly_revenue AS (
     847        SELECT
     848            s.store_id,
     849            s.name AS store_name,
     850            DATE_TRUNC('month', o.last_date_mod)::DATE AS month_date,
     851            SUM(
     852                p.price * i.quantity
     853                * (1 - COALESCE(o.discount, 0) / 100.0)
     854            ) AS revenue
     855        FROM store s
     856        JOIN sells se ON se.store_id = s.store_id
     857        JOIN includes i ON i.product_code = se.product_code
     858        JOIN product p ON p.code = i.product_code
     859        JOIN "order" o ON o.order_num = i.order_num
     860        GROUP BY s.store_id, s.name, DATE_TRUNC('month', o.last_date_mod)
     861    ),
     862    with_previous AS (
     863        SELECT
     864            store_id,
     865            store_name,
     866            month_date,
     867            revenue,
     868            LAG(revenue) OVER (
     869                PARTITION BY store_id
     870                ORDER BY month_date
     871            ) AS previous_revenue
     872        FROM monthly_revenue
     873    )
     874    SELECT
     875        store_id,
     876        store_name,
     877        TO_CHAR(month_date, 'YYYY-MM'),
     878        ROUND(revenue, 2),
     879        ROUND(COALESCE(previous_revenue, 0), 2),
     880        ROUND(revenue, 2),
     881        ROUND(revenue - COALESCE(previous_revenue, 0), 2)
     882    FROM with_previous
     883    ORDER BY month_date DESC, 4 DESC, store_id;
     884$$;
     885
     886CREATE OR REPLACE FUNCTION get_unapproved_reports()
     887RETURNS TABLE (
     888    report_date TIMESTAMP,
     889    store_id VARCHAR(3),
     890    overall_profit NUMERIC,
     891    sales_trend VARCHAR(100),
     892    marketing_growth VARCHAR(100),
     893    owner_signature VARCHAR(50)
     894)
     895LANGUAGE sql
     896AS $$
     897    SELECT
     898        r.date,
     899        r.store_id,
     900        r.overall_profit,
     901        r.sales_trend,
     902        r.marketing_growth,
     903        r.owner_signature
     904    FROM report r
     905    LEFT JOIN approves a
     906        ON a.report_date = r.date
     907       AND a.store_id = r.store_id
     908    WHERE a.report_date IS NULL
     909    ORDER BY r.date DESC, r.store_id;
     910$$;
     911`;
    366912
    367913const database = {
    … …  
    393939
    394940            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) {
    396946                    callback?.(null);
    397947                    return;
    398948                }
    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                    })
    403966                    .catch(err => {
    404                         if (transactionClient) transactionClient.release();
    405                         transactionClient = null;
    406967                        callback?.(err);
    407968                    });
     969
    408970                return;
    409971            }
    410972
    411973            if (normalized === 'COMMIT') {
    412                 if (!transactionClient) {
     974                const client = transactionStorage.getStore();
     975
     976                if (!client) {
    413977                    callback?.(null);
    414978                    return;
    415979                }
    416                 const client = transactionClient;
     980
    417981                client.query('COMMIT')
    418982                    .then(() => {
    419                         transactionClient = null;
    420983                        client.release();
    421984                        callback?.(null);
    422985                    })
    423986                    .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                            });
    427995                    });
     996
    428997                return;
    429998            }
    430999
    4311000            if (normalized === 'ROLLBACK') {
    432                 if (!transactionClient) {
     1001                const client = transactionStorage.getStore();
     1002
     1003                if (!client) {
    4331004                    callback?.(null);
    4341005                    return;
    4351006                }
    436                 const client = transactionClient;
     1007
    4371008                client.query('ROLLBACK')
    4381009                    .then(() => {
    439                         transactionClient = null;
    4401010                        client.release();
    4411011                        callback?.(null);
    4421012                    })
    4431013                    .catch(err => {
    444                         transactionClient = null;
    4451014                        client.release();
    4461015                        callback?.(err);
    4471016                    });
     1017
    4481018                return;
    4491019            }
    … …  
    4581028            });
    4591029        }
     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        );
    4601073    },
    4611074
    … …  
    5201133        const schema = `
    5211134            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,
    5241137                parent_category_id INTEGER REFERENCES category(id) NOT NULL
    525             );
     1138                );
    5261139
    5271140            CREATE TABLE IF NOT EXISTS product (
    528                 code VARCHAR(8) PRIMARY KEY DEFAULT '-1',
     1141                                                   code VARCHAR(8) PRIMARY KEY DEFAULT '-1',
    5291142                price DECIMAL(10,2) NOT NULL CHECK (price >= 0.0),
    5301143                availability INTEGER NOT NULL,
    … …  
    5341147                description VARCHAR(500) NOT NULL,
    5351148                category_id INTEGER NOT NULL REFERENCES category(id) ON DELETE SET DEFAULT
    536             );
     1149                );
    5371150
    5381151            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,
    5401153                image VARCHAR NOT NULL DEFAULT 'Image NOT found!'
    541             );
     1154                );
    5421155
    5431156            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,
    5451158                color VARCHAR(50)
    546             );
     1159                );
    5471160
    5481161            CREATE TABLE IF NOT EXISTS store (
    549                 store_ID VARCHAR(3) PRIMARY KEY,
     1162                                                 store_ID VARCHAR(3) PRIMARY KEY,
    5501163                name VARCHAR(50) UNIQUE NOT NULL,
    5511164                date_of_founding DATE NOT NULL,
    … …  
    5531166                store_email VARCHAR(40) UNIQUE NOT NULL CHECK (store_email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'),
    5541167                rating DECIMAL(2,1) NOT NULL DEFAULT 0 CHECK (rating>=0.0 AND rating<=5.0)
    555             );
     1168                );
    5561169
    5571170            CREATE TABLE IF NOT EXISTS personal (
    558                 id VARCHAR(10) PRIMARY KEY,
     1171                                                    id VARCHAR(10) PRIMARY KEY,
    5591172                first_name VARCHAR(20) NOT NULL,
    5601173                last_name VARCHAR(20) NOT NULL,
    … …  
    5621175                email VARCHAR(50) UNIQUE NOT NULL CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'),
    5631176                password VARCHAR NOT NULL
    564             );
     1177                );
    5651178
    5661179            CREATE TABLE IF NOT EXISTS permissions (
    567                 personal_is VARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE,
     1180                                                       personal_id VARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE,
    5681181                type VARCHAR(50) NOT NULL,
    5691182                authorisation VARCHAR(50) NOT NULL
    570             );
     1183                );
    5711184
    5721185            CREATE TABLE IF NOT EXISTS boss (
    573                 boss_id VARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE
    574             );
     1186                                                boss_id VARCHAR(10) PRIMARY KEY REFERENCES personal(id) ON DELETE CASCADE
     1187                );
    5751188
    5761189            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,
    5781191                date_of_hire DATE NOT NULL
    579             );
     1192                );
    5801193
    5811194            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,
    5841197                last_name VARCHAR(50) NOT NULL,
    5851198                email VARCHAR(50) UNIQUE NOT NULL CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'),
    5861199                password VARCHAR NOT NULL
    587             );
     1200                );
    5881201
    5891202            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,
    5911204                address VARCHAR(200) NOT NULL,
    5921205                city VARCHAR(30) NOT NULL,
    … …  
    5941207                country VARCHAR(40) NOT NULL,
    5951208                is_default BOOLEAN DEFAULT TRUE
    596             );
     1209                );
    5971210
    5981211            CREATE TABLE IF NOT EXISTS "order" (
    599                 order_num VARCHAR(11) PRIMARY KEY,
     1212                                                   order_num VARCHAR(11) PRIMARY KEY,
    6001213                client_ID INTEGER REFERENCES client(client_ID) ON DELETE CASCADE,
    6011214                status VARCHAR(20) NOT NULL DEFAULT 'placed order',
    … …  
    6041217                discount DECIMAL(5,2) DEFAULT 0.0 CHECK(discount>=0.0 AND discount<=100.00),
    6051218                CONSTRAINT check_status CHECK (status IN ('placed order', 'being processed', 'shipping', 'delivered', 'canceled'))
    606             );
     1219                );
    6071220
    6081221            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,
    6101223                comment VARCHAR(300),
    6111224                rating DECIMAL(2,1) NOT NULL CHECK(rating>=0.0 AND rating<=5.0),
    6121225                last_mod_date TIMESTAMP NOT NULL
    613             );
     1226                );
    6141227
    6151228            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,
    6181231                reason VARCHAR(300),
    6191232                amount DECIMAL(5,2) NOT NULL,
    6201233                status VARCHAR(100) NOT NULL DEFAULT 'requested refund',
    621                 CONSTRAINT check_refund_status CHECK (status IN ('requested refund', 'being revied', 'approved', 'not approved', 'processed'))
    622             );
     1234                CONSTRAINT check_refund_status CHECK (status IN ('requested refund', 'being reviewed', 'approved', 'not approved', 'processed'))
     1235                );
    6231236
    6241237            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,
    6271240                overall_profit NUMERIC NOT NULL DEFAULT 0.0 CHECK(overall_profit>=0),
    6281241                sales_trend VARCHAR(100) NOT NULL,
    … …  
    6301243                owner_signature VARCHAR(50) NOT NULL DEFAULT 'Not signed yet',
    6311244                PRIMARY KEY (date, store_ID)
    632             );
     1245                );
    6331246
    6341247            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,
    6371250                month_and_year DATE NOT NULL,
    6381251                profit NUMERIC NOT NULL DEFAULT 0.0,
    6391252                PRIMARY KEY(report_date, store_ID),
    6401253                FOREIGN KEY (report_date, store_ID) REFERENCES report(date, store_ID) ON DELETE CASCADE
    641             );
     1254                );
    6421255
    6431256            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,
    6461259                monthly_profit NUMERIC NOT NULL DEFAULT 0.0,
    6471260                date TIMESTAMP NOT NULL,
    … …  
    6501263                PRIMARY KEY (report_date, store_ID),
    6511264                FOREIGN KEY (report_date, store_ID) REFERENCES report(date, store_ID) ON DELETE CASCADE
    652             );
     1265                );
    6531266
    6541267            CREATE TABLE IF NOT EXISTS request (
    655                 request_num VARCHAR(14) PRIMARY KEY,
     1268                                                   request_num VARCHAR(14) PRIMARY KEY,
    6561269                date_and_time TIMESTAMP NOT NULL,
    6571270                problem VARCHAR(300) NOT NULL,
    6581271                notes_of_communication VARCHAR,
    6591272                customer_satisfaction NUMERIC NOT NULL
    660             );
     1273                );
    6611274
    6621275            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,
    6641277                order_num VARCHAR(11) UNIQUE NOT NULL REFERENCES "order"(order_num) ON DELETE CASCADE,
    6651278                PRIMARY KEY(client_ID, order_num)
    666             );
     1279                );
    6671280
    6681281            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,
    6701283                personal_id VARCHAR(10) NOT NULL REFERENCES personal(id) ON DELETE CASCADE,
    6711284                PRIMARY KEY(request_num, personal_id)
    672             );
     1285                );
    6731286
    6741287            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,
    6761289                store_ID VARCHAR(3) REFERENCES store(store_ID) ON DELETE CASCADE,
    6771290                PRIMARY KEY(request_num, store_ID)
    678             );
     1291                );
    6791292
    6801293            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,
    6831296                changes VARCHAR NOT NULL,
    6841297                PRIMARY KEY (date_and_time, product_code)
    685             );
     1298                );
    6861299
    6871300            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,
    6891302                change_date_time TIMESTAMP,
    6901303                product_code VARCHAR(8),
    6911304                PRIMARY KEY(personal_id, change_date_time, product_code),
    6921305                FOREIGN KEY(change_date_time, product_code) REFERENCES "change"(date_and_time, product_code) ON DELETE CASCADE
    693             );
     1306                );
    6941307
    6951308            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,
    6971310                store_ID VARCHAR(3) REFERENCES store(store_ID) ON DELETE CASCADE,
    6981311                PRIMARY KEY(personal_id, store_ID)
    699             );
     1312                );
    7001313
    7011314            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,
    7031316                report_date TIMESTAMP,
    7041317                store_ID VARCHAR(3),
    … …  
    7101323                FOREIGN KEY (report_date, store_ID) REFERENCES report(date, store_ID) ON DELETE CASCADE,
    7111324                CONSTRAINT check_pay_method CHECK (pay_method IN ('full_time', 'part-time', 'custom'))
    712             );
     1325                );
    7131326
    7141327            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,
    7161329                store_ID VARCHAR(3) REFERENCES store(store_ID) ON DELETE CASCADE,
    7171330                discount NUMERIC NOT NULL DEFAULT 0.0,
    7181331                PRIMARY KEY (product_code, store_ID)
    719             );
     1332                );
    7201333
    7211334            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,
    7231336                product_code VARCHAR(8) REFERENCES product(code) ON DELETE CASCADE,
    7241337                quantity INTEGER NOT NULL CHECK(quantity>=0),
    7251338                PRIMARY KEY (order_num, product_code)
    726             );
     1339                );
    7271340
    7281341            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,
    7301343                report_date TIMESTAMP,
    7311344                store_ID VARCHAR(3),
    … …  
    7331346                PRIMARY KEY (boss_id, report_date, store_ID),
    7341347                FOREIGN KEY (report_date, store_ID) REFERENCES report(date, store_ID) ON DELETE CASCADE
    735             );
     1348                );
    7361349
    7371350            -- These four small tables are application authentication/audit storage.
    7381351            -- They do not modify any of the project tables above.
    7391352            CREATE TABLE IF NOT EXISTS users (
    740                 id VARCHAR(50) PRIMARY KEY,
     1353                                                 id VARCHAR(50) PRIMARY KEY,
    7411354                username VARCHAR(100) UNIQUE NOT NULL,
    7421355                email VARCHAR(255) UNIQUE NOT NULL,
    … …  
    7451358                force_password_change BOOLEAN DEFAULT FALSE,
    7461359                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    747             );
     1360                );
    7481361
    7491362            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,
    7521365                description TEXT
    753             );
     1366                );
    7541367
    7551368            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,
    7571370                role_id INTEGER REFERENCES roles(role_id) ON DELETE CASCADE,
    7581371                PRIMARY KEY(user_id, role_id)
    759             );
     1372                );
    7601373
    7611374            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),
    7641377                action VARCHAR(100) NOT NULL,
    7651378                resource_type VARCHAR(50),
    … …  
    7681381                ip_address VARCHAR(45),
    7691382                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    770             );
     1383                );
    7711384
    7721385            CREATE INDEX IF NOT EXISTS idx_product_category ON product(category_id);
    … …  
    7831396
    7841397        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        `);
    7851554
    7861555        const roles = [
    … …  
    8121581        await pool.query(`INSERT INTO boss(boss_id) VALUES('000000') ON CONFLICT(boss_id) DO NOTHING`);
    8131582        await pool.query(
    814             `INSERT INTO permissions(personal_is,type,authorisation)
    815              VALUES('000000','ADMIN','full_access') ON CONFLICT(personal_is) DO NOTHING`
     1583            `INSERT INTO permissions(personal_id,type,authorisation)
     1584             VALUES('000000','ADMIN','full_access') ON CONFLICT(personal_id) DO NOTHING`
    8161585        );
    8171586        await pool.query(
    … …  
    8321601                             FILTER (WHERE r.role_id IS NOT NULL), '[]') AS roles
    8331602             FROM users u
    834              LEFT JOIN user_roles ur ON ur.user_id=u.id
    835              LEFT JOIN roles r ON r.role_id=ur.role_id
     1603                      LEFT JOIN user_roles ur ON ur.user_id=u.id
     1604                      LEFT JOIN roles r ON r.role_id=ur.role_id
    8361605             WHERE u.id=$1
    8371606             GROUP BY u.id`,
    … …  
    8471616                             FILTER (WHERE r.role_id IS NOT NULL), '[]') AS roles
    8481617             FROM users u
    849              LEFT JOIN user_roles ur ON ur.user_id=u.id
    850              LEFT JOIN roles r ON r.role_id=ur.role_id
     1618                      LEFT JOIN user_roles ur ON ur.user_id=u.id
     1619                      LEFT JOIN roles r ON r.role_id=ur.role_id
    8511620             WHERE u.username=$1 OR u.email=$1
    8521621             GROUP BY u.id
    853              LIMIT 1`,
     1622                 LIMIT 1`,
    8541623            [username],
    8551624            (err, result) => callback(err, result?.rows?.[0])
    … …  
    9371706        const sql = `SELECT p.*, c.name AS category_name, LEFT(p.code,3) AS store_id
    9381707                     FROM product p
    939                      LEFT JOIN category c ON c.id=p.category_id
    940                      ${where.length ? 'WHERE ' + where.join(' AND ') : ''}
     1708                         LEFT JOIN category c ON c.id=p.category_id
     1709                         ${where.length ? 'WHERE ' + where.join(' AND ') : ''}
    9411710                     ORDER BY p.code`;
    9421711        dbQuery(sql, params, (err, result) => callback(err, result?.rows || []));
    … …  
    9471716            `SELECT p.*, c.name AS category_name, LEFT(p.code,3) AS store_id
    9481717             FROM product p
    949              LEFT JOIN category c ON c.id=p.category_id
     1718                 LEFT JOIN category c ON c.id=p.category_id
    9501719             WHERE p.code=$1 LIMIT 1`,
    9511720            [String(id)],
    … …  
    9581727            `SELECT p.*, c.name AS category_name, LEFT(p.code,3) AS store_id
    9591728             FROM product p
    960              LEFT JOIN category c ON c.id=p.category_id
     1729                 LEFT JOIN category c ON c.id=p.category_id
    9611730             WHERE p.code=$1`,
    9621731            [code],
    … …  
    9851754                    `INSERT INTO sells(product_code,store_ID,discount)
    9861755                     VALUES($1,$2,$3)
    987                      ON CONFLICT(product_code,store_ID)
     1756                         ON CONFLICT(product_code,store_ID)
    9881757                     DO UPDATE SET discount=EXCLUDED.discount`,
    9891758                    [data.code,storeId,data.discount || 0],
    … …  
    10921861        dbQuery(
    10931862            `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 items
     1863                 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
    10971866             FROM "order" o
    1098              LEFT JOIN includes i ON i.order_num=o.order_num
    1099              LEFT JOIN product p ON p.code=i.product_code
     1867                 LEFT JOIN includes i ON i.order_num=o.order_num
     1868                 LEFT JOIN product p ON p.code=i.product_code
    11001869             WHERE o.client_ID=$1
    11011870             GROUP BY o.order_num
    … …  
    11091878            `INSERT INTO review(order_num,comment,rating,last_mod_date)
    11101879             VALUES($1,$2,$3,CURRENT_TIMESTAMP)
    1111              RETURNING order_num`,
     1880                 RETURNING order_num`,
    11121881            [data.order_num,data.comment||null,data.rating],
    11131882            (err,result)=>callback(err,result?.rows?.[0]?.order_num)
    … …  
    11621931    getAllOrders(callback) {
    11631932        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.email
     1933                     c.first_name,c.last_name,c.email
    11651934                 FROM "order" o
    1166                  LEFT JOIN client c ON c.client_id=o.client_ID
     1935                     LEFT JOIN client c ON c.client_id=o.client_ID
    11671936                 ORDER BY o.last_date_mod DESC`,
    11681937            [],(err,result)=>callback(err,result?.rows||[]));
    … …  
    11721941        dbQuery(`SELECT p.*,c.name AS category_name,LEFT(p.code,3) AS store_id,s.discount
    11731942                 FROM product p
    1174                  LEFT JOIN category c ON c.id=p.category_id
    1175                  LEFT JOIN sells s ON s.product_code=p.code AND s.store_ID=$1
     1943                     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
    11761945                 WHERE LEFT(p.code,3)=$1 OR EXISTS(SELECT 1 FROM sells sx WHERE sx.product_code=p.code AND sx.store_ID=$1)
    11771946                 ORDER BY p.code`,[storeId],(err,result)=>callback(err,result?.rows||[]));
    … …  
    11811950        dbQuery(`SELECT o.*,LEFT(o.order_num,3) AS store_id,o.last_date_mod AS order_date,c.first_name,c.last_name
    11821951                 FROM "order" o
    1183                  LEFT JOIN client c ON c.client_id=o.client_ID
     1952                     LEFT JOIN client c ON c.client_id=o.client_ID
    11841953                 WHERE LEFT(o.order_num,3)=$1 ORDER BY o.last_date_mod DESC`,[storeId],
    11851954            (err,result)=>callback(err,result?.rows||[]));
    … …  
    11891958        dbQuery(`SELECT p.*,e.date_of_hire,per.type,per.authorisation
    11901959                 FROM personal p JOIN works_in_store w ON w.personal_id=p.id
    1191                  LEFT JOIN employees e ON e.employee_id=p.id
    1192                  LEFT JOIN permissions per ON per.personal_is=p.id
     1960                                 LEFT JOIN employees e ON e.employee_id=p.id
     1961                                 LEFT JOIN permissions per ON per.personal_id=p.id
    11931962                 WHERE w.store_ID=$1 ORDER BY p.last_name,p.first_name`,
    11941963            [storeId],(err,result)=>callback(err,result?.rows||[]));
    … …  
    11961965
    11971966    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        );
    12001975    },
    12011976
    12021977    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        );
    12132131    },
    12142132
    … …  
    12162134        dbQuery(`SELECT r.*,a.personal_id AS answered_by
    12172135                 FROM request r
    1218                  JOIN for_store fs ON fs.request_num=r.request_num
    1219                  LEFT JOIN answers a ON a.request_num=r.request_num
     2136                          JOIN for_store fs ON fs.request_num=r.request_num
     2137                          LEFT JOIN answers a ON a.request_num=r.request_num
    12202138                 WHERE fs.store_ID=$1 AND (a.personal_id=$2 OR a.personal_id IS NULL)
    12212139                 ORDER BY r.date_and_time DESC`,
    … …  
    12252143    getClientStats(clientId, callback) {
    12262144        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`,
    12312149            [clientId],(err,result)=>callback(err,result?.rows?.[0]||{}));
    12322150    }
    … …  
    12432161    try {
    12442162        await database.initializeDatabase();
     2163        await database.installReportFunctions();
    12452164        console.log('✅ Database initialization completed');
    12462165    } catch (err) {
    … …  
    47685687    }
    47695688
     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
    47705741    else if (pathname === '/api/employee-tasks' && req.method === 'GET') {
    47715742        requireAuth(req, res, (userId) => {
    … …  
    49025873        requireStoreOwner()(req, res, (personalId) => {
    49035874            let body = '';
     5875
    49045876            req.on('data', chunk => {
    49055877                body += chunk.toString();
    49065878            });
     5879
    49075880            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;
    49095902
    49105903                if (!storeId || !period || !startDate || !endDate || !type) {
    49115904                    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                    }));
    49135909                    return;
    49145910                }
    … …  
    49205916                        if (err || !ownsStore) {
    49215917                            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                            }));
    49235922                            return;
    49245923                        }
    49255924
    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);
    49345935                                    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' });
    49405936                                    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
    49545939                                    }));
     5940                                    return;
    49555941                                }
     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                                }));
    49565976                            }
    49575977                        );
Note: See TracChangeset for help on using the changeset viewer.