server.js Back to Presentation
// Load environment variables FIRST
require('dotenv').config();
process.env.TZ = 'Asia/Bangkok';

const fs = require('fs');
process.on('uncaughtException', (err) => {
    fs.appendFileSync('crash_error.log', `[${new Date().toISOString()}] Uncaught Exception: ${err.stack || err}\n`);
    console.error('Uncaught Exception:', err);
    process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
    fs.appendFileSync('crash_error.log', `[${new Date().toISOString()}] Unhandled Rejection: ${reason.stack || reason}\n`);
    console.error('Unhandled Rejection at:', promise, 'reason:', reason);
    process.exit(1);
});

const express = require('express');
const cors = require('cors');
const sqlite3 = require('sqlite3');
const { open } = require('sqlite');
const multer = require('multer');
const path = require('path');
const https = require('https');
const http = require('http');
const XLSX = require('xlsx');
const ExcelJS = require('exceljs');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const compression = require('compression');

// JWT configuration from .env (persistent across restarts)
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
    console.error('FATAL: JWT_SECRET is not set in .env file. Run the app with a proper .env configuration.');
    process.exit(1);
}
const JWT_EXPIRY = process.env.JWT_EXPIRES_IN || '8h';
const BCRYPT_ROUNDS = 12;

// Password policy
const PASSWORD_MIN_LENGTH = 4;
function validatePassword(password) {
    if (!password || password.length < PASSWORD_MIN_LENGTH) {
        return `Password must be at least ${PASSWORD_MIN_LENGTH} characters`;
    }
    return null;
}

const app = express();
app.set('trust proxy', true);
const PORT = process.env.PORT || 8080;

let excelTemplates = {};

function loadExcelTemplates() {
    try {
        const absPath = path.join(__dirname, 'report1.xlsx');
        if (!fs.existsSync(absPath)) {
            console.log('report1.xlsx not found at', absPath);
            return;
        }
        const workbook = XLSX.readFile(absPath);
        workbook.SheetNames.forEach(sheetName => {
            const sheet = workbook.Sheets[sheetName];
            const rawData = XLSX.utils.sheet_to_json(sheet, { header: 1 });
            const items = [];
            let headerInfo = {
                title: '',
                project: '',
                container_no: '',
                testing_date: '',
                panel_type: '',
                location: '',
                rated: '',
                voltage: '',
                instrument: ''
            };
            
            if (rawData.length > 0) {
                headerInfo.title = String(rawData[0][0] || '').trim();
            }
            
            for (let i = 7; i < rawData.length; i++) {
                const row = rawData[i];
                if (!row || row.length === 0) continue;
                
                const itemNo = String(row[1] || '').trim();
                let title = String(row[2] || '').trim();
                if (title.includes('CICRUIT')) {
                    title = title.replace(/CICRUIT/g, 'CIRCUIT');
                }
                let desc = String(row[3] || row[4] || row[5] || '').trim();
                if (desc.includes('2000')) {
                    desc = desc.replace(/2000[.\s]*/g, '');
                }
                if (desc.includes('25.6')) {
                    desc = desc.replace(/25\.6[.\s]*/g, '');
                }
                
                if (itemNo || title) {
                    let pass = true;
                    let fail = false;
                    
                    items.push({
                        item_no: itemNo,
                        title: title,
                        description: desc,
                        default_pass: pass,
                        default_fail: fail
                    });
                }
            }
            
            excelTemplates[sheetName] = {
                header: headerInfo,
                items: items
            };
        });
        
        // Post-process "AUX DB" template to update items and delete 3.7 (which naturally shifts Tested By to 3.6)
        if (excelTemplates['AUX DB']) {
            const aux = excelTemplates['AUX DB'];
            
            // 1. Update existing items in-place
            aux.items = aux.items.map(item => {
                if (item.item_no === '1') {
                    item.title = 'TEST CONDITION :';
                    item.description = 'FOLLOW TO IEC 61439-1&3  STANDARD';
                } else if (item.item_no === '1.1') {
                    item.title = 'AMBIENT TEMP';
                    item.description = 'AMBIENT TEMP - -10 °C< T AMBIENT< 40 °C      Value :...[XXX]....°C';
                } else if (item.item_no === '1.2') {
                    item.title = 'HUMIDITY';
                    item.description = 'HUMIDITY - < 90 %      Value :...[XXX]....%';
                } else if (item.item_no === '3.1') {
                    item.title = 'INSULATION TEST';
                    item.description = 'INSULATION TEST - PHASE I - GROUND/NEUTRAL [XXX] M Ω';
                } else if (item.item_no === '3.2') {
                    item.title = 'INSULATION TEST';
                    item.description = 'INSULATION TEST - PHASE II - GROUND/NEUTRAL [XXX] M Ω';
                } else if (item.item_no === '3.3') {
                    item.title = 'INSULATION TEST';
                    item.description = 'INSULATION TEST - PHASE III - GROUND/NEUTRAL [XXX] M Ω';
                }
                return item;
            });

            // 2. Remove old 3.4 (LAMP TEST), 3.5 (GROUND TEST connectivity), 3.6 (GROUND TEST resistance)
            // and replace with:
            // - 3.4: GROUND CONTINUITY TEST < 1Ω
            // - 3.5: GROUND RESISTANCE TEST < 1Ω
            const index3_1 = aux.items.findIndex(item => item.item_no === '3.1');
            if (index3_1 !== -1) {
                const before3_4 = aux.items.slice(0, index3_1 + 3);
                const finalRow = aux.items.find(item => item.item_no && item.item_no.toLowerCase().includes('tested'));
                if (finalRow) {
                    finalRow.title = 'FINAL JUDGEMENT PASS FAIL COMMENT.';
                    finalRow.description = '[XXXXXXXXXXXXXX]';
                }
                aux.items = [
                    ...before3_4,
                    {
                        item_no: '3.4',
                        title: 'GROUND CONTINUITY TEST < 1Ω',
                        description: 'Value :...[XXX|XXX]',
                        default_pass: true,
                        default_fail: false
                    },
                    {
                        item_no: '3.5',
                        title: 'GROUND RESISTANCE TEST < 1Ω',
                        description: 'Value :...[XXX|XXX]',
                        default_pass: true,
                        default_fail: false
                    },
                    finalRow
                ].filter(Boolean);
            }
        }
        
        // Copy "AUX DB" template for new systems
        const auxDbTemplate = excelTemplates['AUX DB'];
        if (auxDbTemplate) {
            // New templates mapping from report.pdf
            const pdfTemplates = {
                'ACCESS CONTROL INSPECTION': [
                    { item_no: '1', title: 'Access Control System', description: '' },
                    { item_no: '1.1', title: 'Devices and Equipment Installation Verify', description: '' },
                    { item_no: '1.2', title: 'Fixing Rigid and Routing of Conduit and Wireway Inspection', description: '' },
                    { item_no: '1.3', title: 'Cable Routing Inspection as drawing Inspection', description: '' },
                    { item_no: '1.4', title: 'Labeling/marking of cables as drawing Inspection', description: '' },
                    { item_no: '1.5', title: 'All bolts and nuts are tight down.', description: '' },
                    { item_no: '1.6', title: 'Physical Damage Inspection', description: '' },
                    { item_no: '1.7', title: 'Cleanliness Check.', description: '' },
                    { item_no: '1.8', title: 'All Circuit Function Test', description: '' },
                    { item_no: '1.9', title: 'Function Communication', description: '' }
                ],
                'AIR CONDITION INSPECTION': [
                    { item_no: '1', title: 'Air Condition', description: '' },
                    { item_no: '1.1', title: 'Material Incoming Inspection', description: '' },
                    { item_no: '1.2', title: 'Visual Check and Inspection (Before Installation)', description: '' },
                    { item_no: '1.3', title: 'Check Piping Hangers and Supports', description: '' },
                    { item_no: '1.4', title: 'Check Conduit Hangers and Supports', description: '' },
                    { item_no: '1.5', title: 'Check FCU Controller as per design', description: '' },
                    { item_no: '1.6', title: 'Piping Leakage Test', description: '' },
                    { item_no: '1.7', title: 'Cable Insulation Resistance Test', description: '' },
                    { item_no: '1.8', title: 'Grounding Connection Check', description: '' }
                ],
                'BATTERY RACK INSPECTION': [
                    { item_no: '1', title: 'Battery Cabinet/Rack', description: '' },
                    { item_no: '1.1', title: 'Material Incoming Inspection', description: '' },
                    { item_no: '1.2', title: 'Visual Check and Inspection (Before Installation)', description: '' },
                    { item_no: '1.3', title: 'Cabinet Installation and Alignment Check', description: '' },
                    { item_no: '1.4', title: 'Battery Terminal Tightness (Torque Check)', description: '' },
                    { item_no: '1.5', title: 'Cable Insulation Resistance Test', description: '' },
                    { item_no: '1.6', title: 'Grounding Connection Check', description: '' }
                ],
                'COMMUNICATION SYSTEM': [
                    { item_no: '1', title: 'Communication System', description: '' },
                    { item_no: '1.1', title: 'Devices and Equipment Installation Verify', description: '' },
                    { item_no: '1.2', title: 'Fixing Rigid and Routing of Conduit and Wireway Inspection', description: '' },
                    { item_no: '1.3', title: 'Cable Routing Inspection as drawing Inspection', description: '' },
                    { item_no: '1.4', title: 'Labeling/marking of cables as drawing Inspection', description: '' },
                    { item_no: '1.5', title: 'All bolts and nuts are tight down.', description: '' },
                    { item_no: '1.6', title: 'Physical Damage Inspection', description: '' },
                    { item_no: '1.7', title: 'Cleanliness Check.', description: '' },
                    { item_no: '1.8', title: 'All Circuit Function Test', description: '' },
                    { item_no: '1.9', title: 'Function Communication', description: '' }
                ],
                'CRAC INSPECTION': [
                    { item_no: '1', title: 'CRAC', description: '' },
                    { item_no: '1.1', title: 'Devices and Equipment Installation Verify', description: '' },
                    { item_no: '1.2', title: 'Fixing Rigid and Routing of Conduit and Wireway Inspection', description: '' },
                    { item_no: '1.3', title: 'All bolts and nuts are tight down.', description: '' },
                    { item_no: '1.4', title: 'Physical Damage Inspection', description: '' },
                    { item_no: '1.5', title: 'Cleanliness Check.', description: '' },
                    { item_no: '1.6', title: 'Grounding connection check', description: '' }
                ],
                'GROUNDING CONTINUITY TEST': [
                    { item_no: '1', title: 'Grounding Continuity Test', description: '' },
                    { item_no: '1.1', title: 'Point to Point Continuity and Resistance Test', description: '' }
                ],
                'EXIT LIGHT & EMERGENCY LIGHT': [
                    { item_no: '1', title: 'Visual Inspection:', description: '' },
                    { item_no: '1.1', title: 'Name plate and labeling equipment installed.', description: '' },
                    { item_no: '1.2', title: 'Check fixing rigid of conduit and alignment horizontal, vertical, elevation.', description: '' },
                    { item_no: '1.3', title: 'Cable entries are properly sealed.', description: '' },
                    { item_no: '1.4', title: 'All cables are connected according the connection diagram.', description: '' },
                    { item_no: '1.5', title: 'Verify labeling / marking of cables as per drawing.', description: '' },
                    { item_no: '1.6', title: 'All bolts and nuts are tight down.', description: '' },
                    { item_no: '1.7', title: 'Inspect for any damage', description: '' },
                    { item_no: '1.8', title: 'Corrosion protection has been carried out.', description: '' },
                    { item_no: '1.9', title: 'All Circuit function checked.', description: '' }
                ],
                'FIRE ALARM INSPECTION': [
                    { item_no: '1', title: 'VISUAL INSPECTION :', description: '' },
                    { item_no: '1.1', title: 'TO CHECK THAT THE INSTALLATION IS ACCORDING TO DESIGNED STANDARDS.', description: '' },
                    { item_no: '1.2', title: 'TO CHECK THAT THE SIGNAL AND ELECTRICAL CABLES ARE INSTALLED CORRECTLY.', description: '' },
                    { item_no: '1.3', title: 'TO CHECK THAT THE FIRE EXTINGUISHING GAS TANK IS FILLED WITH GAS AND THAT THE GAS RELEASE SYSTEM HAS BEEN DISCONNECTED.', description: '' },
                    { item_no: '1.4', title: 'TO CHECK THAT THE GAS RELEASE SWITCH IS DISABLED. BY OBSERVING THAT THE DISPLAY LIGHT ON THE LED BULB WILL BE RED.', description: '' },
                    { item_no: '1.5', title: 'ALL PIPING ARE INSTALLED CORRECT SUCH AS SIZING, SUPPORTS, FIXING, VALVE, ETC.', description: '' },
                    { item_no: '1.6', title: 'TO CHECK THAT THE SMOKE SENSORS, FIRE MANUAL SWITCH, BELL, ABORT SWITCH, FLASHER, ETC. HAVE ALL BEEN INSTALLED COMPLETELY.', description: '' },
                    { item_no: '1.7', title: 'ALL SMOKE DETECTOR ARE READY TO USED. (BLINKING LED STATUS)', description: '' },
                    { item_no: '1.8', title: 'TO CHECK THAT END OF LINE (RESISTANT) IS INSTALLED ON THE CIRCUIT.', description: '' },
                    { item_no: '1.9', title: 'TO CHECK THAT ALL SMOKE DETECTORS ARE WORKING PROPERLY. NOTICE THAT THE RED LED WILL BLINK.', description: '' },
                    { item_no: '1.10', title: 'TO CHECK THAT THE FIRE ALARM CONTROL PANEL IS READY TO WORK BY OBSERVING THAT THE LCD SCREEN IS READY TO USE AND DOES NOT DISPLAY ANY ERRORS.', description: '' },
                    { item_no: '1.11', title: 'TO CHECK THAT THE ASD CONTROL PANEL IS IN A READY STATE. NOTE THAT THE STATUS LED ROW IS NOT DISPLAYED.', description: '' },
                    { item_no: '2', title: 'TEST INSPECTION : SMOKE DETECTOR', description: '' },
                    { item_no: '2.1', title: 'TO CHECK THAT ALL SMOKE DETECTORS CAN DETECT SMOKE BY USING A SMOKE SAMPLE. WHEN THE SMOKE DETECTOR DETECTS GAS. THE LED WILL STAY ON WITHOUT BLINKING AT THE SAME TIME , THE FIRE ALARM CONTROL PANEL WILL RECEIVE A SIGNAL AND CAUSE THE ALARM BELL AND BUZZER ON THE FIRE ALARM CONTROL PANEL TO SOUND. AND THE TESTER CAN PRESS THE ACKNOWLEDGE BUTTON TO CANCEL THE SOUND.', description: '' },
                    { item_no: '2.2', title: 'WHILE THE SMOKE DETECTOR ONLY DETECTS SMOKE. THE CONTROL CABINET ONLY RESPONDS TO LOUD NOISES SO THAT THE OPERATOR CAN CHECK FOR ERRORS. WITHOUT ENTERING THE GAS INJECTION PREPARATION MODE BY OBSERVING THAT THE CONTROL CABINET WILL NOT CHANGE THE STATUS TO PREPARING TO INJECT GAS.', description: '' },
                    { item_no: '2.3', title: 'TO RESET THE GAS DETECTOR TO STANDBY STATE BY EMPTYING THE GAS REMAINING IN THE DETECTOR AND THEN RESETTING THE FIRE ALARM CONTROL PANEL TO STANDBY MODE. AND TESTED EVERY GAS DETECTORS UNTIL IT WAS COMPLETE.', description: '' }
                ],
                'FIRE SUPPRESSION INSPECTION': [
                    { item_no: '1', title: 'Fire Suppression Panel', description: '' },
                    { item_no: '1.1', title: 'Panel are properly install according to location drawing', description: '' },
                    { item_no: '1.2', title: 'Checking correct cable sizing as per drawing', description: '' },
                    { item_no: '1.3', title: 'Cable Routing Inspection', description: '' },
                    { item_no: '1.4', title: 'Checking panel LED display when power energizing', description: '' },
                    { item_no: '1.5', title: 'Physical Damage Inspection', description: '' },
                    { item_no: '1.6', title: 'Cleanliness Check.', description: '' }
                ],
                'HEAT DETECTOR': [
                    { item_no: '1', title: 'Heat Detector', description: '' },
                    { item_no: '1.1', title: 'Equipment installation complie per drawing', description: '' },
                    { item_no: '1.2', title: 'Checking correct cable sizing as per drawing', description: '' },
                    { item_no: '1.3', title: 'Cable Routing Inspection', description: '' },
                    { item_no: '1.4', title: 'Physical Damage Inspection', description: '' },
                    { item_no: '1.5', title: 'Testing temperature to simulate system', description: '' },
                    { item_no: '1.6', title: 'Checking system properly working', description: '' },
                    { item_no: '1.7', title: 'Functional test system', description: '' }
                ],
                'LIGHTING AND RECEPTACLE': [
                    { item_no: '1', title: 'Visual Inspection:', description: '' },
                    { item_no: '1.1', title: 'Lighting types are installed correctly and show no damage from installation.', description: '' },
                    { item_no: '1.2', title: 'Lighting circuits are connected correctly as per the design.', description: '' },
                    { item_no: '1.3', title: 'Switches are installed in the correct type.', description: '' },
                    { item_no: '1.4', title: 'Sizing of cables for lighting is installed correctly.', description: '' },
                    { item_no: '1.5', title: 'Outlet types are installed correctly as per the design.', description: '' },
                    { item_no: '1.6', title: 'Sizing of cables for outlets is installed correctly according to the design.', description: '' },
                    { item_no: '2', title: 'Electrical Inspection:', description: '' },
                    { item_no: '2.1', title: 'All lights can be turned on/off correctly and appropriately.', description: '' },
                    { item_no: '2.2', title: 'The lighting circuits have been installed correctly and are suitable for use.', description: '' },
                    { item_no: '2.3', title: 'The lights provide appropriate brightness according', description: '' },
                    { item_no: '2.4', title: 'The outlets have appropriate voltage.', description: '' }
                ],
                'LV CABLE': [
                    { item_no: '1', title: 'LV CABLE', description: '' },
                    { item_no: '1.1', title: 'Material Incoming Inspection and check cable damage', description: '' },
                    { item_no: '1.2', title: 'Cable Fitting Installation Properly Inspection', description: '' },
                    { item_no: '1.3', title: 'Cables Tagging/Phase Marking/Cable Dressing', description: '' },
                    { item_no: '1.4', title: 'Cables Connection Tightness (Torque) Check', description: '' },
                    { item_no: '1.5', title: 'Grounding Connection Check', description: '' },
                    { item_no: '1.6', title: 'Cable After Installation Completion Inspection', description: '' }
                ],
                'LV CABLE TESTING - 3 PHASE': [
                    { item_no: '1', title: 'General Data', description: '' },
                    { item_no: '2', title: 'Test Record', description: '' }
                ],
                'LV CABLE TESTING - SINGLE PHASE': [
                    { item_no: '1', title: 'General Data', description: '' },
                    { item_no: '2', title: 'Test Record', description: '' }
                ],
                'LOW VOLTAGE SWITCHBOARD': [
                    { item_no: '1', title: 'LV Panel', description: '' },
                    { item_no: '1.1', title: 'Visual Check and Inspection (Before Installation)', description: '' },
                    { item_no: '1.2', title: 'Panel Frame Base Installation', description: '' },
                    { item_no: '1.3', title: 'Switchboard Installation and Alignment Check', description: '' },
                    { item_no: '1.4', title: 'Bolt Tightening Check', description: '' },
                    { item_no: '1.5', title: 'Internal Wiring Visual Check and Inspect Damage', description: '' },
                    { item_no: '1.6', title: 'Grounding Connection Check', description: '' }
                ],
                'MANUAL RELEASE': [
                    { item_no: '1', title: 'Manual Release', description: '' },
                    { item_no: '1.1', title: 'Equipment installation as per drawing', description: '' },
                    { item_no: '1.2', title: 'Physical Damage Inspection', description: '' },
                    { item_no: '1.3', title: 'Checking correct cable sizing as per drawing', description: '' },
                    { item_no: '1.4', title: 'Cable Routing Inspection', description: '' },
                    { item_no: '1.5', title: 'Checking system properly working', description: '' },
                    { item_no: '1.6', title: 'Checking Manual Release LED is properly activate', description: '' },
                    { item_no: '1.7', title: 'Function test system', description: '' }
                ],
                'MV CABLE HI POT TEST': [
                    { item_no: '1', title: 'General Data', description: '' },
                    { item_no: '2', title: 'Cable Continuity and Phase Check', description: '' },
                    { item_no: '3', title: 'Cable Insulation Resistance and HI-POT Test', description: '' }
                ],
                'MV CABLE': [
                    { item_no: '1', title: 'MV Cable', description: '' },
                    { item_no: '1.1', title: 'Material Incoming Inspection and check cable damage', description: '' },
                    { item_no: '1.2', title: 'Cable Fitting Installation Properly Inspection', description: '' },
                    { item_no: '1.2a', title: 'Cables Tagging/Phase Marking/Cable Dressing', description: '' },
                    { item_no: '1.3', title: 'Cables Connection Tightness (Torque) Check', description: '' },
                    { item_no: '1.4', title: 'Grounding Connection Check', description: '' },
                    { item_no: '1.5', title: 'Check Cable Termination Kit', description: '' },
                    { item_no: '1.6', title: 'Cable After Installation Completion Inspection', description: '' }
                ],
                'RING MAIN UNIT INSPECTION': [
                    { item_no: '1', title: 'RMU Visual Inspection', description: '' },
                    { item_no: '1.1', title: 'RMU is not damage from transportation and installation', description: '' },
                    { item_no: '1.2', title: 'Gas shall be full fill in tank', description: '' },
                    { item_no: '1.3', title: 'All chamber shall clean and check any risk of condition', description: '' },
                    { item_no: '1.4', title: 'Grounding Connection Check', description: '' }
                ],
                'SMOKE DETECTOR': [
                    { item_no: '1', title: 'Smoke Detector', description: '' },
                    { item_no: '1.1', title: 'Equipment installation complie per drawing', description: '' },
                    { item_no: '1.2', title: 'Checking correct cable sizing as per drawing', description: '' },
                    { item_no: '1.3', title: 'Cable Routing Inspection', description: '' },
                    { item_no: '1.4', title: 'Physical Damage Inspection', description: '' },
                    { item_no: '1.5', title: 'Testing gas to simulate system', description: '' },
                    { item_no: '1.6', title: 'Checking system properly working', description: '' },
                    { item_no: '1.7', title: 'Functional test system', description: '' }
                ],
                'TORQUE CHECK': [
                    { item_no: '1', title: 'Torque Tightening Check', description: '' }
                ],
                'TRANSFORMER': [
                    { item_no: '1', title: 'Transformer Visual Inspection', description: '' },
                    { item_no: '1.1', title: 'Transformer is not damage from transportation and installation', description: '' },
                    { item_no: '1.2', title: 'Transformer type and size is correct as per design', description: '' },
                    { item_no: '1.3', title: 'MV cables are installed correct cable size', description: '' },
                    { item_no: '1.4', title: 'Temperature controller is installed correct', description: '' },
                    { item_no: '1.5', title: 'Ventilation blowers are not damage and install properly', description: '' },
                    { item_no: '1.6', title: 'Grounding Connection Check', description: '' }
                ],
                'VENTILATION SYSTEM': [
                    { item_no: '1', title: 'Ventilation System', description: '' },
                    { item_no: '1.1', title: 'Devices and Equipment Installation Verify', description: '' },
                    { item_no: '1.2', title: 'Fixing Rigid and Routing of Conduit and Wireway Inspection', description: '' },
                    { item_no: '1.3', title: 'Labeling/marking of cables as drawing Inspection', description: '' },
                    { item_no: '1.4', title: 'All bolts and nuts are tight down.', description: '' },
                    { item_no: '1.5', title: 'Insulation Test', description: '' },
                    { item_no: '1.6', title: 'Physical Damage Inspection', description: '' },
                    { item_no: '1.7', title: 'Cleanliness Check.', description: '' },
                    { item_no: '1.8', title: 'All Circuit Function Test', description: '' },
                    { item_no: '1.9', title: 'Function Communication', description: '' }
                ],
                'WATERPROOF IPX5': [
                    { item_no: '1', title: 'Waterproof IPX5 Test Report', description: '' },
                    { item_no: '1.1', title: 'Water Leak Rectification and Testing Report', description: '' }
                ]
            };

            for (const [sysName, items] of Object.entries(pdfTemplates)) {
                excelTemplates[sysName] = {
                    header: {
                        ...JSON.parse(JSON.stringify(auxDbTemplate.header)),
                        title: 'TEST REPORT ' + sysName
                    },
                    items: items.map(it => ({
                        item_no: it.item_no,
                        title: it.title,
                        description: it.description,
                        default_pass: true,
                        default_fail: false
                    }))
                };
            }
            
            // Backwards compatibility names
            excelTemplates['GROUND INSPECTION'] = excelTemplates['GROUNDING CONTINUITY TEST'];
            excelTemplates['GROUNG INSPECTION'] = excelTemplates['GROUNDING CONTINUITY TEST'];
            excelTemplates['POWER CABLE INSPECTION'] = excelTemplates['LV CABLE'];
            excelTemplates['LIGHTNING INSPECTION'] = excelTemplates['GROUNDING CONTINUITY TEST'];
            excelTemplates['USP INSPECTION'] = excelTemplates['BATTERY RACK INSPECTION'];
            excelTemplates['UPS INSPECTION'] = excelTemplates['BATTERY RACK INSPECTION'];
        }

        console.log('Successfully loaded all FAT templates from report1.xlsx');
    } catch (e) {
        console.error('Failed to load excel templates from report1.xlsx', e);
    }
}

loadExcelTemplates();

const PROJECT_TEMPLATES = {
    'E-HOUSE TYPE': [
        { section: 'PTU Room - MV SYSTEM', items: ['RMU', 'EX-FAN'] },
        { section: 'PTU Room - TRANSFORMER', items: ['TRANSFORMER CONTROLLER', 'EX-FAN', 'FCU'] },
        { section: 'PTU Room - LV SYSTEM', items: ['LV POWER LINK', 'AUX DB'] },
        { section: 'PTU Room - LV SWITCHBOARD', items: ['LV SWITCHBOARD'] },
        { section: 'PTU Room - UPS', items: ['UPS'] },
        { section: 'PTU Room - AIRCON SYSTEM', items: ['AIRCON SYSTEM'] },
        { section: 'PTU Room - FIRE ALARM SYSTEM', items: ['FIRE ALARM SYSTEM'] },
        { section: 'PTU Room - VESDA / ASD SYSTEM', items: ['VESDA / ASD SYSTEM'] },
        { section: 'PTU Room - FIRE SUPPRESSION SYSTEM', items: ['FIRE SUPPRESSION SYSTEM'] },
        { section: 'PTU Room - EMS', items: ['EMS'] },
        { section: 'PTU Room - COMMUNICATION SYSTEM', items: ['JB BOX', 'LAN CABLE', 'RS485', 'SENSOR'] },
        { section: 'Battery Room - BATTERY RACK', items: ['BATTERY RACK'] },
        { section: 'Battery Room - AIRCON', items: ['ACU', 'DX AIR CON'] },
        { section: 'Battery Room - FIRE ALARM SYSTEM', items: ['FIRE ALARM SYSTEM'] },
        { section: 'Battery Room - VESDA / ASD SYSTEM', items: ['VESDA / ASD SYSTEM'] },
        { section: 'Battery Room - FIRE SUPPRESSION SYSTEM', items: ['FIRE SUPPRESSION SYSTEM'] },
        { section: 'Battery Room - EMS', items: ['EMS'] },
        { section: 'Battery Room - COMMUNICATION SYSTEM', items: ['LAN CABLE', 'RS485', 'SENSOR'] },
        { section: 'Battery Room - OFF GAS', items: ['OFF GAS'] },
        { section: 'TRANSFORMER ROOM', items: ['TRANSFORMER ROOM'] },
        { section: 'RMU ROOM', items: ['RMU ROOM'] }
    ],
    'GENERATOR TYPE': [
        { section: 'GEN-E-HOUSE - UTILITY SYSTEM', items: ['LIGHTING SYSTEM', 'EMERGENCY', 'EXIT LIGHT', 'OUTLET'] },
        { section: 'GEN-E-HOUSE - LV PANEL', items: ['GCP PANEL', 'AUX DB'] },
        { section: 'GEN-E-HOUSE - CONTROL PANEL', items: ['DAMPER CONTROL PANEL', 'AIR INTAKE DAMPER', 'AIR DISCHARGE DAMPER'] },
        { section: 'GEN-E-HOUSE - DAY TANK', items: ['SENSOR', 'PUMPING CONTROL PANEL', 'OIL PUMP'] }
    ],
    'SOLAR TRACK': [
        { section: 'SOLAR TRACK', items: ['SOLAR TRACK SYSTEM'] }
    ],
    'UNIT SUB STATION': [
        { section: 'UNIT SUB STATION', items: ['UNIT SUB STATION'] }
    ]
};

// Helper function to download files from URLs, supporting redirects
function downloadFile(url) {
    return new Promise((resolve, reject) => {
        const client = url.startsWith('https') ? https : http;
        client.get(url, (res) => {
            if (res.statusCode === 301 || res.statusCode === 302) {
                // Handle redirect
                downloadFile(res.headers.location).then(resolve).catch(reject);
                return;
            }
            if (res.statusCode !== 200) {
                reject(new Error(`Failed to download file: status code ${res.statusCode}`));
                return;
            }
            const chunks = [];
            res.on('data', (chunk) => chunks.push(chunk));
            res.on('end', () => {
                const buffer = Buffer.concat(chunks);
                resolve({
                    buffer,
                    mimeType: res.headers['content-type']
                });
            });
        }).on('error', reject);
    });
}

// Security headers (helmet)
app.use(helmet({
    contentSecurityPolicy: {
        directives: {
            defaultSrc: ["'self'"],
            scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://kit.fontawesome.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net"],
            styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://ka-f.fontawesome.com", "https://cdnjs.cloudflare.com"],
            fontSrc: ["'self'", "https://fonts.gstatic.com", "https://ka-f.fontawesome.com", "https://cdnjs.cloudflare.com"],
            imgSrc: ["'self'", "data:", "blob:", "https://*"],
            connectSrc: ["'self'", "https://ka-f.fontawesome.com", "wss:", "ws:", "https://cdnjs.cloudflare.com"],
            mediaSrc: ["'self'", "data:", "blob:"],
            objectSrc: ["'self'"],
            frameSrc: ["'self'", "data:", "blob:"]
        }
    },
    crossOriginEmbedderPolicy: false,
    crossOriginResourcePolicy: false,
    hsts: false // Disable HSTS to prevent Safari forcing HTTPS on local HTTP/IP connections
}));

// Compression for all responses
app.use(compression());

// Enable CORS and JSON body parser
app.use(cors({
    origin: (origin, callback) => {
        if (process.env.CORS_ORIGIN) {
            const allowedOrigins = process.env.CORS_ORIGIN.split(',').map(o => o.trim());
            if (!origin || allowedOrigins.indexOf(origin) !== -1 || allowedOrigins.includes('*')) {
                callback(null, true);
            } else {
                callback(new Error('Not allowed by CORS'));
            }
        } else {
            callback(null, true);
        }
    },
    credentials: true
}));
app.use(express.json({ limit: '10mb' }));

// Serve static frontend files with no-cache headers for active development
app.use((req, res, next) => {
    res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
    next();
});
// Defense-in-depth: block any request for sensitive file types
app.use((req, res, next) => {
    const blocked = /\.(db|sqlite|sql|env|lock|log|bak)$/i;
    if (blocked.test(req.path) || req.path.startsWith('/backups') || req.path.startsWith('/node_modules')) {
        return res.status(404).send('Not found');
    }
    next();
});
app.use(express.static(path.join(__dirname, 'public')));
app.use('/image', express.static(path.join(__dirname, 'image')));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));

// Database connection pool (SQLite Adapter)
const dbPath = process.env.DB_PATH
    ? path.resolve(__dirname, process.env.DB_PATH)
    : path.join(__dirname, 'shopada_b2b.db');
let dbPromise = null;

function getDb() {
    if (!dbPromise) {
        dbPromise = open({
            filename: dbPath,
            driver: sqlite3.Database
        }).then(async (database) => {
            // Enable foreign keys
            await database.run("PRAGMA foreign_keys = ON;");
            return database;
        });
    }
    return dbPromise;
}

const pool = {
    async query(sql, params = []) {
        const db = await getDb();
        
        let querySql = sql;
        let trimmedSql = sql.trim();
        let upperSql = trimmedSql.toUpperCase();
        
        // 1. Rewrite MySQL-specific table creation options
        if (upperSql.includes('AUTO_INCREMENT')) {
            querySql = querySql.replace(/INT\s+AUTO_INCREMENT\s+PRIMARY\s+KEY/gi, 'INTEGER PRIMARY KEY AUTOINCREMENT');
            querySql = querySql.replace(/INT\s+AUTO_INCREMENT/gi, 'INTEGER PRIMARY KEY AUTOINCREMENT');
        }
        if (querySql.toUpperCase().includes('ENGINE=INNODB')) {
            querySql = querySql.replace(/ENGINE\s*=\s*InnoDB/gi, '');
        }
        if (querySql.toUpperCase().includes('UNIQUE KEY')) {
            querySql = querySql.replace(/UNIQUE KEY\s+\w+\s*\(([^)]+)\)/gi, 'UNIQUE ($1)');
        }
        
        // Refresh trimmed/upper strings
        trimmedSql = querySql.trim();
        upperSql = trimmedSql.toUpperCase();
        
        // 2. Handle SHOW COLUMNS FROM
        if (upperSql.startsWith('SHOW COLUMNS FROM')) {
            const match = querySql.match(/SHOW COLUMNS FROM\s+[`"']?(\w+)[`"']?\s+LIKE\s+['"](\w+)['"]/i);
            if (match) {
                const table = match[1];
                const column = match[2];
                const pragmaRows = await db.all(`PRAGMA table_info(\`${table}\`)`);
                const found = pragmaRows.filter(col => col.name === column);
                const mapped = found.map(col => ({ Field: col.name }));
                return [mapped, null];
            }
        }
        
        // 3. Handle bulk inserts like: VALUES ?
        if (/VALUES\s+\?/i.test(querySql) && Array.isArray(params[0]) && Array.isArray(params[0][0])) {
            const rows = params[0];
            const rowPlaceholder = '(' + rows[0].map(() => '?').join(', ') + ')';
            const placeholders = rows.map(() => rowPlaceholder).join(', ');
            querySql = querySql.replace(/VALUES\s+\?/i, 'VALUES ' + placeholders);
            const flatParams = rows.flat();
            const result = await db.run(querySql, flatParams);
            return [{ insertId: result.lastID, affectedRows: result.changes }, null];
        }
        
        // 4. Run the query
        if (upperSql.startsWith('SELECT') || upperSql.startsWith('SHOW') || upperSql.startsWith('PRAGMA') || upperSql.startsWith('EXPLAIN')) {
            const rows = await db.all(querySql, params);
            return [rows, null];
        } else {
            const result = await db.run(querySql, params);
            return [{ insertId: result.lastID, affectedRows: result.changes }, null];
        }
    },
    
    async getConnection() {
        const db = await getDb();
        return {
            async query(sql, params = []) {
                return pool.query(sql, params);
            },
            async execute(sql, params = []) {
                return pool.query(sql, params);
            },
            async release() {
                // No-op
            },
            async end() {
                // No-op
            },
            async beginTransaction() {
                await db.run('BEGIN TRANSACTION');
            },
            async commit() {
                await db.run('COMMIT');
            },
            async rollback() {
                await db.run('ROLLBACK');
            }
        };
    }
};

// Automatically check and run database migrations on server startup
(async () => {
    try {
        const connection = await pool.getConnection();
        
        // Increase max_allowed_packet to support larger file uploads (up to 1GB)
        await connection.query("SET GLOBAL max_allowed_packet=1073741824;").catch(err => {
            console.warn("Could not set GLOBAL max_allowed_packet:", err.message);
        });
        await connection.query("SET max_allowed_packet=1073741824;").catch(err => {
            console.warn("Could not set SESSION max_allowed_packet:", err.message);
        });

        // 1. Check and add column 'status' to projects
        const [colStatus] = await connection.query("SHOW COLUMNS FROM projects LIKE 'status'");
        if (colStatus.length === 0) {
            console.log("Database Migration: Adding 'status' column to projects table...");
            await connection.query("ALTER TABLE projects ADD COLUMN status VARCHAR(50) DEFAULT 'รอดำเนินการ'");
        }
        
        // Check and add drawing/spec name columns to projects (file bytes now live on disk, see path columns below)
        const [colDrawingName] = await connection.query("SHOW COLUMNS FROM projects LIKE 'drawing_name'");
        if (colDrawingName.length === 0) {
            console.log("Database Migration: Adding drawing and spec name columns to projects table...");
            await connection.query("ALTER TABLE projects ADD COLUMN drawing_name VARCHAR(255) NULL");
            await connection.query("ALTER TABLE projects ADD COLUMN spec_name VARCHAR(255) NULL");
        }

        // Check and add is_deleted column to projects
        const [colIsDeleted] = await connection.query("SHOW COLUMNS FROM projects LIKE 'is_deleted'");
        if (colIsDeleted.length === 0) {
            console.log("Database Migration: Adding 'is_deleted' column to projects table...");
            await connection.query("ALTER TABLE projects ADD COLUMN is_deleted TINYINT(1) DEFAULT 0");
        }

        // Check and add project_type column to projects
        const [colProjectType] = await connection.query("SHOW COLUMNS FROM projects LIKE 'project_type'");
        if (colProjectType.length === 0) {
            console.log("Database Migration: Adding 'project_type' column to projects table...");
            await connection.query("ALTER TABLE projects ADD COLUMN project_type VARCHAR(100) NULL");
        }

        // Check and add external_name column to projects (ชื่อโครงการที่เรียก / External project name)
        const [colExternalName] = await connection.query("SHOW COLUMNS FROM projects LIKE 'external_name'");
        if (colExternalName.length === 0) {
            console.log("Database Migration: Adding 'external_name' column to projects table...");
            await connection.query("ALTER TABLE projects ADD COLUMN external_name VARCHAR(255) NULL");
        }

        // Check and add is_active column to users (account deactivation support)
        const [colUserIsActive] = await connection.query("SHOW COLUMNS FROM users LIKE 'is_active'");
        if (colUserIsActive.length === 0) {
            console.log("Database Migration: Adding 'is_active' column to users table...");
            await connection.query("ALTER TABLE users ADD COLUMN is_active TINYINT(1) DEFAULT 1");
        }

        // Check and add last_active_at column to users (online status support)
        const [colUserLastActive] = await connection.query("SHOW COLUMNS FROM users LIKE 'last_active_at'");
        if (colUserLastActive.length === 0) {
            console.log("Database Migration: Adding 'last_active_at' column to users table...");
            await connection.query("ALTER TABLE users ADD COLUMN last_active_at TIMESTAMP NULL");
        }

        // Check and add plain_password column to users (superadmin view/edit password support)
        const [colPlainPassword] = await connection.query("SHOW COLUMNS FROM users LIKE 'plain_password'");
        if (colPlainPassword.length === 0) {
            console.log("Database Migration: Adding 'plain_password' column to users table...");
            await connection.query("ALTER TABLE users ADD COLUMN plain_password VARCHAR(255) NULL");
        }

        // Create announcements / announcement_acks tables if they don't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS announcements (
                id INT AUTO_INCREMENT PRIMARY KEY,
                title VARCHAR(255) NOT NULL,
                body TEXT NOT NULL,
                created_by INT NULL,
                is_active TINYINT(1) DEFAULT 1,
                type VARCHAR(20) DEFAULT 'general',
                media_path VARCHAR(255) NULL,
                media_type VARCHAR(20) DEFAULT 'none',
                start_at TIMESTAMP NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
            ) ENGINE=InnoDB;
        `);

        // Check and add columns for announcements: type, media_path, media_type, start_at
        const [colType] = await connection.query("SHOW COLUMNS FROM announcements LIKE 'type'");
        if (colType.length === 0) {
            console.log("Database Migration: Adding 'type' column to announcements table...");
            await connection.query("ALTER TABLE announcements ADD COLUMN type VARCHAR(20) DEFAULT 'general'");
        }
        const [colMediaPath] = await connection.query("SHOW COLUMNS FROM announcements LIKE 'media_path'");
        if (colMediaPath.length === 0) {
            console.log("Database Migration: Adding 'media_path' column to announcements table...");
            await connection.query("ALTER TABLE announcements ADD COLUMN media_path VARCHAR(255) NULL");
        }
        const [colMediaType] = await connection.query("SHOW COLUMNS FROM announcements LIKE 'media_type'");
        if (colMediaType.length === 0) {
            console.log("Database Migration: Adding 'media_type' column to announcements table...");
            await connection.query("ALTER TABLE announcements ADD COLUMN media_type VARCHAR(20) DEFAULT 'none'");
        }
        const [colStartAt] = await connection.query("SHOW COLUMNS FROM announcements LIKE 'start_at'");
        if (colStartAt.length === 0) {
            console.log("Database Migration: Adding 'start_at' column to announcements table...");
            await connection.query("ALTER TABLE announcements ADD COLUMN start_at TIMESTAMP NULL");
        }

        await connection.query(`
            CREATE TABLE IF NOT EXISTS announcement_acks (
                id INT AUTO_INCREMENT PRIMARY KEY,
                announcement_id INT NOT NULL,
                user_id INT NOT NULL,
                acked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (announcement_id) REFERENCES announcements(id) ON DELETE CASCADE,
                FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
                UNIQUE KEY unique_ack (announcement_id, user_id)
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: Announcements tables checked/created.");

        // Create internal chat tables if they don't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS chat_conversations (
                id INT AUTO_INCREMENT PRIMARY KEY,
                is_group TINYINT(1) DEFAULT 0,
                name VARCHAR(255) NULL,
                creator_id INT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL
            ) ENGINE=InnoDB;
        `);
        await connection.query(`
            CREATE TABLE IF NOT EXISTS chat_participants (
                id INT AUTO_INCREMENT PRIMARY KEY,
                conversation_id INT NOT NULL,
                user_id INT NOT NULL,
                last_read_at TIMESTAMP NULL,
                FOREIGN KEY (conversation_id) REFERENCES chat_conversations(id) ON DELETE CASCADE,
                FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
                UNIQUE KEY unique_participant (conversation_id, user_id)
            ) ENGINE=InnoDB;
        `);
        await connection.query(`
            CREATE TABLE IF NOT EXISTS chat_messages (
                id INT AUTO_INCREMENT PRIMARY KEY,
                conversation_id INT NOT NULL,
                sender_id INT NOT NULL,
                body TEXT NOT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (conversation_id) REFERENCES chat_conversations(id) ON DELETE CASCADE,
                FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: Chat tables checked/created.");

        // 2. Create suppliers table if it doesn't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS suppliers (
                id INT AUTO_INCREMENT PRIMARY KEY,
                name VARCHAR(255) NOT NULL,
                contact_info TEXT NULL,
                website VARCHAR(255) NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: Suppliers table checked/created.");

        // 2b. Create contractors / contractor_rfqs / contractor_rfq_items tables if they don't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS contractors (
                id INT AUTO_INCREMENT PRIMARY KEY,
                name VARCHAR(255) NOT NULL,
                contact_info TEXT NULL,
                website VARCHAR(255) NULL,
                user_id INT NULL,
                is_deleted TINYINT(1) DEFAULT 0,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
            ) ENGINE=InnoDB;
        `);
        await connection.query(`
            CREATE TABLE IF NOT EXISTS contractor_rfqs (
                id INT AUTO_INCREMENT PRIMARY KEY,
                project_id INT NOT NULL,
                contractor_id INT NOT NULL,
                rfq_number VARCHAR(50) NOT NULL,
                status VARCHAR(50) NOT NULL DEFAULT 'draft',
                issue_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                due_date DATE NULL,
                notes TEXT NULL,
                quote_file_path VARCHAR(255) NULL,
                quote_file_name VARCHAR(255) NULL,
                total_amount DECIMAL(15,2) DEFAULT 0,
                is_deleted TINYINT(1) DEFAULT 0,
                created_by INT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
                FOREIGN KEY (contractor_id) REFERENCES contractors(id),
                UNIQUE KEY unique_rfq_number (rfq_number)
            ) ENGINE=InnoDB;
        `);
        await connection.query(`
            CREATE TABLE IF NOT EXISTS contractor_rfq_items (
                id INT AUTO_INCREMENT PRIMARY KEY,
                rfq_id INT NOT NULL,
                project_item_id INT NULL,
                section_name VARCHAR(255) NULL,
                description TEXT NULL,
                brand VARCHAR(255) NULL,
                qty DECIMAL(15,3) DEFAULT 0,
                unit VARCHAR(50) NULL,
                order_index INT DEFAULT 0,
                is_included TINYINT(1) DEFAULT 1,
                material_rate DECIMAL(15,2) DEFAULT 0,
                labour_rate DECIMAL(15,2) DEFAULT 0,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (rfq_id) REFERENCES contractor_rfqs(id) ON DELETE CASCADE,
                FOREIGN KEY (project_item_id) REFERENCES project_items(id) ON DELETE SET NULL
            ) ENGINE=InnoDB;
        `);
        const [colRfqRevisionNumber] = await connection.query("SHOW COLUMNS FROM contractor_rfqs LIKE 'revision_number'");
        if (colRfqRevisionNumber.length === 0) {
            console.log("Database Migration: Adding revision_number column to contractor_rfqs table...");
            await connection.query("ALTER TABLE contractor_rfqs ADD COLUMN revision_number INT DEFAULT 0");
        }
        console.log("Database Migration: Contractors/RFQ tables checked/created.");

        // 3. Add supplier columns to products
        const [colProductSupplierId] = await connection.query("SHOW COLUMNS FROM products LIKE 'supplier_id'");
        if (colProductSupplierId.length === 0) {
            console.log("Database Migration: Adding supplier columns to products table...");
            await connection.query("ALTER TABLE products ADD COLUMN supplier_id INT NULL");
            await connection.query("ALTER TABLE products ADD COLUMN supplier_price DECIMAL(12,2) DEFAULT 0.00");
            await connection.query("ALTER TABLE products ADD COLUMN supplier_quote_data LONGBLOB NULL");
            await connection.query("ALTER TABLE products ADD COLUMN supplier_quote_name VARCHAR(255) NULL");
            await connection.query("ALTER TABLE products ADD CONSTRAINT fk_products_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL");
        }

        // Add prod_group and prod_system columns to products
        const [colProductGroup] = await connection.query("SHOW COLUMNS FROM products LIKE 'prod_group'");
        if (colProductGroup.length === 0) {
            console.log("Database Migration: Adding prod_group and prod_system columns to products table...");
            await connection.query("ALTER TABLE products ADD COLUMN prod_group VARCHAR(255) NULL");
            await connection.query("ALTER TABLE products ADD COLUMN prod_system VARCHAR(255) NULL");
        }

        // Add lead_time column to products
        const [colLeadTime] = await connection.query("SHOW COLUMNS FROM products LIKE 'lead_time'");
        if (colLeadTime.length === 0) {
            console.log("Database Migration: Adding lead_time column to products table...");
            await connection.query("ALTER TABLE products ADD COLUMN lead_time VARCHAR(255) NULL");
        }

        // Add model column to products
        const [colProductModel] = await connection.query("SHOW COLUMNS FROM products LIKE 'model'");
        if (colProductModel.length === 0) {
            console.log("Database Migration: Adding 'model' column to products table...");
            await connection.query("ALTER TABLE products ADD COLUMN model VARCHAR(255) NULL");
        }

        // Add model column to project_items
        const [colItemModel] = await connection.query("SHOW COLUMNS FROM project_items LIKE 'model'");
        if (colItemModel.length === 0) {
            console.log("Database Migration: Adding 'model' column to project_items table...");
            await connection.query("ALTER TABLE project_items ADD COLUMN model VARCHAR(255) NULL");
        }

        // Add sm_no column to products
        const [colProductSmNo] = await connection.query("SHOW COLUMNS FROM products LIKE 'sm_no'");
        if (colProductSmNo.length === 0) {
            console.log("Database Migration: Adding 'sm_no' column to products table...");
            await connection.query("ALTER TABLE products ADD COLUMN sm_no VARCHAR(100) NULL");
        }

        // Add sm_no column to project_items
        const [colItemSmNo] = await connection.query("SHOW COLUMNS FROM project_items LIKE 'sm_no'");
        if (colItemSmNo.length === 0) {
            console.log("Database Migration: Adding 'sm_no' column to project_items table...");
            await connection.query("ALTER TABLE project_items ADD COLUMN sm_no VARCHAR(100) NULL");
        }
        
        // 4. Add 'is_free_issue' column to project_items
        const [colIsFreeIssue] = await connection.query("SHOW COLUMNS FROM project_items LIKE 'is_free_issue'");
        if (colIsFreeIssue.length === 0) {
            console.log("Database Migration: Adding 'is_free_issue' column to project_items table...");
            await connection.query("ALTER TABLE project_items ADD COLUMN is_free_issue TINYINT(1) DEFAULT 0");
        }

        // Check and add 'prod_group' column to project_items
        const [colItemProdGroup] = await connection.query("SHOW COLUMNS FROM project_items LIKE 'prod_group'");
        if (colItemProdGroup.length === 0) {
            console.log("Database Migration: Adding 'prod_group' column to project_items table...");
            await connection.query("ALTER TABLE project_items ADD COLUMN prod_group VARCHAR(255) NULL");
        }
        // Retroactively populate empty prod_group fields for existing items
        await connection.query("UPDATE project_items SET prod_group = description WHERE prod_group IS NULL OR prod_group = ''");

        // 5. Create project_permissions table if it doesn't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS project_permissions (
                id INT AUTO_INCREMENT PRIMARY KEY,
                project_id INT NOT NULL,
                user_id INT NOT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
                FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
                UNIQUE KEY unique_proj_user (project_id, user_id)
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: project_permissions table checked/created.");

        // Add spec_name column to project_sections (file bytes now live on disk, see path columns below)
        const [colSecSpecName] = await connection.query("SHOW COLUMNS FROM project_sections LIKE 'spec_name'");
        if (colSecSpecName.length === 0) {
            console.log("Database Migration: Adding spec_name column to project_sections table...");
            await connection.query("ALTER TABLE project_sections ADD COLUMN spec_name VARCHAR(255) NULL");
        }

        // 6. Add soft delete columns
        const [colSecDeleted] = await connection.query("SHOW COLUMNS FROM project_sections LIKE 'is_deleted'");
        if (colSecDeleted.length === 0) {
            console.log("Database Migration: Adding 'is_deleted' column to project_sections...");
            await connection.query("ALTER TABLE project_sections ADD COLUMN is_deleted TINYINT(1) DEFAULT 0");
        }

        const [colSecOrderIndex] = await connection.query("SHOW COLUMNS FROM project_sections LIKE 'order_index'");
        if (colSecOrderIndex.length === 0) {
            console.log("Database Migration: Adding 'order_index' column to project_sections...");
            await connection.query("ALTER TABLE project_sections ADD COLUMN order_index INT NOT NULL DEFAULT 0");
        }

        const [colSecProfitPercent] = await connection.query("SHOW COLUMNS FROM project_sections LIKE 'profit_percent'");
        if (colSecProfitPercent.length === 0) {
            console.log("Database Migration: Adding 'profit_percent' column to project_sections...");
            await connection.query("ALTER TABLE project_sections ADD COLUMN profit_percent DECIMAL(5,2) DEFAULT 0.00");
        }

        const [colItemDeleted] = await connection.query("SHOW COLUMNS FROM project_items LIKE 'is_deleted'");
        if (colItemDeleted.length === 0) {
            console.log("Database Migration: Adding 'is_deleted' column to project_items...");
            await connection.query("ALTER TABLE project_items ADD COLUMN is_deleted TINYINT(1) DEFAULT 0");
        }

        const [colItemModified] = await connection.query("SHOW COLUMNS FROM project_items LIKE 'is_modified'");
        if (colItemModified.length === 0) {
            console.log("Database Migration: Adding 'is_modified' column to project_items...");
            await connection.query("ALTER TABLE project_items ADD COLUMN is_modified TINYINT(1) DEFAULT 0");
        }

        const [colItemOrderIndex] = await connection.query("SHOW COLUMNS FROM project_items LIKE 'order_index'");
        if (colItemOrderIndex.length === 0) {
            console.log("Database Migration: Adding 'order_index' column to project_items...");
            await connection.query("ALTER TABLE project_items ADD COLUMN order_index INT NOT NULL DEFAULT 0");
        }

        // 7. Create project_revisions table
        await connection.query(`
            CREATE TABLE IF NOT EXISTS project_revisions (
                id INT AUTO_INCREMENT PRIMARY KEY,
                project_id INT NOT NULL,
                revision_number INT NOT NULL,
                user_id INT NOT NULL,
                snapshot_data LONGTEXT NOT NULL,
                change_summary TEXT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
                FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: project_revisions table checked/created.");

        // Create project_folders table
        await connection.query(`
            CREATE TABLE IF NOT EXISTS project_folders (
                id INT AUTO_INCREMENT PRIMARY KEY,
                project_id INT NOT NULL,
                folder_path VARCHAR(255) NOT NULL,
                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
                UNIQUE KEY unique_proj_folder (project_id, folder_path)
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: project_folders table checked/created.");

        // Create project_documents table
        await connection.query(`
            CREATE TABLE IF NOT EXISTS project_documents (
                id INT AUTO_INCREMENT PRIMARY KEY,
                project_id INT NOT NULL,
                file_name VARCHAR(255) NOT NULL,
                file_path VARCHAR(255) NOT NULL DEFAULT '/',
                file_type VARCHAR(100) NOT NULL,
                file_size INT NOT NULL,
                file_data LONGBLOB NOT NULL,
                uploaded_by VARCHAR(255) NOT NULL,
                uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                doc_level VARCHAR(50) NOT NULL DEFAULT 'internal',
                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
            ) ENGINE=InnoDB;
        `);
        
        const [colDocLevel] = await connection.query("SHOW COLUMNS FROM project_documents LIKE 'doc_level'");
        if (colDocLevel.length === 0) {
            console.log("Database Migration: Adding 'doc_level' column to project_documents...");
            await connection.query("ALTER TABLE project_documents ADD COLUMN doc_level VARCHAR(50) NOT NULL DEFAULT 'internal'");
        }
        console.log("Database Migration: project_documents table checked/created.");

        // 8. Create activity_logs table
        await connection.query(`
            CREATE TABLE IF NOT EXISTS activity_logs (
                id INT AUTO_INCREMENT PRIMARY KEY,
                user_id INT NULL,
                username VARCHAR(100) NULL,
                action VARCHAR(100) NOT NULL,
                details TEXT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: activity_logs table checked/created.");
        
        // 9. Create project_progress table
        await connection.query(`
            CREATE TABLE IF NOT EXISTS project_progress (
                id INT AUTO_INCREMENT PRIMARY KEY,
                project_id INT NOT NULL,
                image_data LONGBLOB NULL,
                image_name VARCHAR(255) NULL,
                image_title VARCHAR(255) NULL,
                timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                progress_desc TEXT NULL,
                issues TEXT NULL,
                solutions TEXT NULL,
                suggestions TEXT NULL,
                system_name VARCHAR(255) NULL,
                created_by INT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
                FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: project_progress table checked/created.");
        
        // 10. Create system_templates table if it doesn't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS system_templates (
                id INT AUTO_INCREMENT PRIMARY KEY,
                system_name VARCHAR(255) NOT NULL UNIQUE,
                items TEXT NOT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: system_templates table checked/created.");

        // 11. Create project_fat_reports table if it doesn't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS project_fat_reports (
                id INT AUTO_INCREMENT PRIMARY KEY,
                project_id INT NOT NULL,
                report_type VARCHAR(100) NOT NULL,
                testing_date VARCHAR(50) NULL,
                container_no VARCHAR(100) NULL,
                panel_type VARCHAR(100) NULL,
                location VARCHAR(100) NULL,
                rated VARCHAR(100) NULL,
                voltage VARCHAR(100) NULL,
                instrument_model VARCHAR(255) NULL,
                instrument_sn VARCHAR(255) NULL,
                instrument_expire VARCHAR(255) NULL,
                is_deleted TINYINT(1) DEFAULT 0,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: project_fat_reports table checked/created.");

        // 12. Create project_fat_items table if it doesn't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS project_fat_items (
                id INT AUTO_INCREMENT PRIMARY KEY,
                fat_report_id INT NOT NULL,
                item_no VARCHAR(50) NULL,
                title VARCHAR(255) NULL,
                description TEXT NULL,
                result_pass TINYINT(1) DEFAULT 0,
                result_fail TINYINT(1) DEFAULT 0,
                photo VARCHAR(255) NULL,
                remarks TEXT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (fat_report_id) REFERENCES project_fat_reports(id) ON DELETE CASCADE
            ) ENGINE=InnoDB;
        `);
        await connection.query("ALTER TABLE project_fat_items MODIFY COLUMN photo LONGBLOB NULL").catch(e => {});
        await connection.query("ALTER TABLE chat_conversations ADD COLUMN name VARCHAR(255) NULL").catch(e => {});
        await connection.query("ALTER TABLE chat_conversations ADD COLUMN creator_id INT NULL").catch(e => {});
        console.log("Database Migration: project_fat_items table checked/created.");

        // 13. Create project_fat_test_script_files table if it doesn't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS project_fat_test_script_files (
                id INT AUTO_INCREMENT PRIMARY KEY,
                project_id INT NOT NULL,
                fat_report_id INT NOT NULL,
                file_name VARCHAR(255) NOT NULL,
                file_type VARCHAR(100) NOT NULL,
                file_size INT NOT NULL,
                file_data LONGBLOB NOT NULL,
                uploaded_by VARCHAR(255) NOT NULL,
                uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
                FOREIGN KEY (fat_report_id) REFERENCES project_fat_reports(id) ON DELETE CASCADE
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: project_fat_test_script_files table checked/created.");

        // 14. Create project_fat_item_photos table if it doesn't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS project_fat_item_photos (
                id INT AUTO_INCREMENT PRIMARY KEY,
                item_id INT NOT NULL,
                photo LONGBLOB NOT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (item_id) REFERENCES project_fat_items(id) ON DELETE CASCADE
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: project_fat_item_photos table checked/created.");

        // 15. Create contractor_extra_works table if it doesn't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS contractor_extra_works (
                id INT AUTO_INCREMENT PRIMARY KEY,
                project_id INT NOT NULL,
                contractor_id INT NOT NULL,
                work_name VARCHAR(255) NOT NULL,
                quantity DECIMAL(15,3) DEFAULT 0,
                estimated_cost DECIMAL(15,2) DEFAULT 0,
                status VARCHAR(50) NOT NULL DEFAULT 'pending',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                is_deleted TINYINT(1) DEFAULT 0,
                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
                FOREIGN KEY (contractor_id) REFERENCES contractors(id)
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: contractor_extra_works table checked/created.");

        // 16. Create contractor_deduct_works table if it doesn't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS contractor_deduct_works (
                id INT AUTO_INCREMENT PRIMARY KEY,
                project_id INT NOT NULL,
                contractor_id INT NOT NULL,
                item_name VARCHAR(255) NOT NULL,
                quantity DECIMAL(15,3) DEFAULT 0,
                deduct_amount DECIMAL(15,2) DEFAULT 0,
                status VARCHAR(50) NOT NULL DEFAULT 'pending',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                is_deleted TINYINT(1) DEFAULT 0,
                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
                FOREIGN KEY (contractor_id) REFERENCES contractors(id)
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: contractor_deduct_works table checked/created.");

        // 17. Create contractor_invoices table if it doesn't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS contractor_invoices (
                id INT AUTO_INCREMENT PRIMARY KEY,
                project_id INT NOT NULL,
                contractor_id INT NOT NULL,
                invoice_no VARCHAR(100) NOT NULL,
                description TEXT NULL,
                amount DECIMAL(15,2) DEFAULT 0,
                status VARCHAR(50) NOT NULL DEFAULT 'pending',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                is_deleted TINYINT(1) DEFAULT 0,
                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
                FOREIGN KEY (contractor_id) REFERENCES contractors(id)
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: contractor_invoices table checked/created.");

        // 18. Create contractor_complaints table if it doesn't exist
        await connection.query(`
            CREATE TABLE IF NOT EXISTS contractor_complaints (
                id INT AUTO_INCREMENT PRIMARY KEY,
                project_id INT NOT NULL,
                contractor_id INT NOT NULL,
                title VARCHAR(255) NOT NULL,
                detail TEXT NULL,
                status VARCHAR(50) NOT NULL DEFAULT 'pending',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                is_deleted TINYINT(1) DEFAULT 0,
                FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
                FOREIGN KEY (contractor_id) REFERENCES contractors(id)
            ) ENGINE=InnoDB;
        `);
        console.log("Database Migration: contractor_complaints table checked/created.");

        // 19. Run Project PR/PO Sourcing and Stock warehouse migrations
        try {
            const sqlSchema = fs.readFileSync(path.join(__dirname, 'pr_po_stock_schema.sql'), 'utf8');
            const sqlQueries = sqlSchema.split(';').map(q => q.trim()).filter(Boolean);
            for (const query of sqlQueries) {
                await connection.query(query);
            }
            console.log("Database Migration: Project PR/PO Sourcing and Stock warehouse tables checked/created.");
        } catch (schemaErr) {
            console.error("Failed to run PR/PO/Stock schema migrations:", schemaErr);
        }

        // Add filesystem path columns so attachments are stored on disk instead of as DB blobs
        const pathColumnsToAdd = [
            ['products', 'image_path', "VARCHAR(255) NULL"],
            ['products', 'pdf_path', "VARCHAR(255) NULL"],
            ['products', 'supplier_quote_path', "VARCHAR(255) NULL"],
            ['projects', 'drawing_path', "VARCHAR(255) NULL"],
            ['projects', 'spec_path', "VARCHAR(255) NULL"],
            ['project_sections', 'spec_path', "VARCHAR(255) NULL"],
            ['project_documents', 'file_disk_path', "VARCHAR(255) NULL"],
            ['project_progress', 'image_path', "VARCHAR(255) NULL"],
            ['project_fat_test_script_files', 'file_path', "VARCHAR(255) NULL"],
            ['activity_logs', 'project_id', "INT NULL"],
            ['project_items', 'production_qty', "DECIMAL(10,2) DEFAULT NULL"],
            ['project_items', 'production_cost_material', "DECIMAL(12,2) DEFAULT NULL"],
            ['project_items', 'production_cost_labour', "DECIMAL(12,2) DEFAULT NULL"],
            ['project_items', 'production_price_material', "DECIMAL(12,2) DEFAULT NULL"],
            ['project_items', 'production_price_labour', "DECIMAL(12,2) DEFAULT NULL"]
        ];
        for (const [table, column, definition] of pathColumnsToAdd) {
            const [colExists] = await connection.query(`SHOW COLUMNS FROM ${table} LIKE '${column}'`);
            if (colExists.length === 0) {
                console.log(`Database Migration: Adding '${column}' column to ${table}...`);
                await connection.query(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
            }
        }

        // Ensure file_data column exists on tables where it is required by legacy INSERT queries
        const legacyDataColumnsToCheck = [
            ['project_documents', 'file_data', "LONGBLOB NOT NULL DEFAULT ''"],
            ['project_fat_test_script_files', 'file_data', "LONGBLOB NOT NULL DEFAULT ''"]
        ];
        for (const [table, column, definition] of legacyDataColumnsToCheck) {
            const [colExists] = await connection.query(`SHOW COLUMNS FROM ${table} LIKE '${column}'`);
            if (colExists.length === 0) {
                console.log(`Database Migration: Adding missing legacy '${column}' column to ${table}...`);
                await connection.query(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
            }
        }


        // Add quotation-style detail columns for admin-ordered extra work / contract-based deduct work
        const extraDeductColumnsToAdd = [
            ['contractor_extra_works', 'section_name', "VARCHAR(255) NULL"],
            ['contractor_extra_works', 'brand', "VARCHAR(255) NULL"],
            ['contractor_extra_works', 'unit', "VARCHAR(50) NULL"],
            ['contractor_extra_works', 'material_rate', "DECIMAL(15,2) DEFAULT 0"],
            ['contractor_extra_works', 'labour_rate', "DECIMAL(15,2) DEFAULT 0"],
            ['contractor_extra_works', 'created_by', "INT NULL"],
            ['contractor_deduct_works', 'rfq_item_id', "INT NULL"],
            ['contractor_deduct_works', 'quoted_qty', "DECIMAL(15,3) DEFAULT 0"],
            ['contractor_deduct_works', 'actual_qty', "DECIMAL(15,3) DEFAULT 0"],
            ['contractor_deduct_works', 'rate', "DECIMAL(15,2) DEFAULT 0"],
            ['contractor_deduct_works', 'created_by', "INT NULL"],
            ['chat_conversations', 'project_id', "INT NULL"]
        ];
        for (const [table, column, definition] of extraDeductColumnsToAdd) {
            const [colExists] = await connection.query(`SHOW COLUMNS FROM ${table} LIKE '${column}'`);
            if (colExists.length === 0) {
                console.log(`Database Migration: Adding '${column}' column to ${table}...`);
                await connection.query(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
            }
        }

        // Backfill project_id for activity_logs if project_id is null
        const [nullProjLogs] = await connection.query("SELECT id, details FROM activity_logs WHERE project_id IS NULL AND details IS NOT NULL");
        for (const log of nullProjLogs) {
            const match = log.details.match(/Project ID\s+(\d+)/i);
            if (match) {
                const pid = parseInt(match[1]);
                await connection.query("UPDATE activity_logs SET project_id = ? WHERE id = ?", [pid, log.id]);
            }
        }

        // Seed default templates if table is empty
        const [templatesRows] = await connection.query("SELECT COUNT(*) as count FROM system_templates");
        if (templatesRows[0].count === 0) {
            console.log("Database Migration: Seeding default system templates...");
            const defaultTemplates = {};

            for (const [sysName, items] of Object.entries(defaultTemplates)) {
                await connection.query(
                    "INSERT INTO system_templates (system_name, items) VALUES (?, ?)",
                    [sysName, JSON.stringify(items)]
                );
            }
        }

        // Migration: convert system_templates items from strings to objects if necessary
        const [allTemplates] = await connection.query("SELECT * FROM system_templates");
        for (const row of allTemplates) {
            try {
                const items = JSON.parse(row.items || '[]');
                let modified = false;
                const converted = items.map(item => {
                    if (typeof item === 'string') {
                        modified = true;
                        return {
                            description: item,
                            qty: 1,
                            brand: '',
                            model: '',
                            type: '',
                            unit: 'Set',
                            prod_group: ''
                        };
                    }
                    return item;
                });
                if (modified) {
                    await connection.query("UPDATE system_templates SET items = ? WHERE id = ?", [JSON.stringify(converted), row.id]);
                    console.log(`Database Migration: Converted templates items to objects for system "${row.system_name}"`);
                }
            } catch (e) {
                console.error('Failed to convert template items:', e);
            }
        }
        
        try {
            await connection.query(`ALTER TABLE project_progress ADD COLUMN system_name VARCHAR(255) NULL`);
            console.log("Database Migration: Added system_name column to project_progress table.");
        } catch (alterErr) {
            // Ignore if column already exists
        }
        
        // Ensure superadmin user exists
        const [superadmins] = await connection.query("SELECT * FROM users WHERE role = 'superadmin'");
        if (superadmins.length === 0) {
            console.log("Database Migration: Seeding 'superadmin' user...");
            const hashedDefaultPwd = await bcrypt.hash('superadmin123', BCRYPT_ROUNDS);
            await connection.query("INSERT INTO users (username, password, role) VALUES ('superadmin', ?, 'superadmin')", [hashedDefaultPwd]).catch(err => {
                console.error("Could not seed superadmin user:", err.message);
            });
        }
        
        // One-time migration: hash all existing plaintext passwords
        const [allUsers] = await connection.query('SELECT id, password FROM users');
        let hashedCount = 0;
        for (const u of allUsers) {
            // Skip already-hashed passwords (bcrypt hashes start with $2b$)
            if (u.password && !u.password.startsWith('$2b$')) {
                const hashed = await bcrypt.hash(u.password, BCRYPT_ROUNDS);
                await connection.query('UPDATE users SET password = ? WHERE id = ?', [hashed, u.id]);
                hashedCount++;
            }
        }
        if (hashedCount > 0) {
            console.log(`Security Migration: Hashed ${hashedCount} plaintext password(s) with bcrypt.`);
        }
        
        connection.release();
    } catch (err) {
        console.error("Database Migration Error:", err);
    }
})();

// Configure Multer for In-Memory Storage with safe limits
const storage = multer.memoryStorage();

// File type validation
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
const ALLOWED_DOC_TYPES = ['application/pdf', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/x-dwg', 'application/x-rar-compressed', 'application/zip', 'application/x-zip-compressed', 'application/octet-stream'];
const ALL_ALLOWED_TYPES = [...ALLOWED_IMAGE_TYPES, ...ALLOWED_DOC_TYPES];

function fileFilter(req, file, cb) {
    if (ALL_ALLOWED_TYPES.includes(file.mimetype)) {
        cb(null, true);
    } else {
        cb(new Error(`File type '${file.mimetype}' is not allowed. Allowed: images, PDF, Excel, Word, DWG, RAR, ZIP`), false);
    }
}

const MAX_FILE_SIZE = parseInt(process.env.MAX_DOC_SIZE) || 50 * 1024 * 1024; // 50MB default
const upload = multer({
    storage: storage,
    fileFilter: fileFilter,
    limits: {
        fileSize: MAX_FILE_SIZE
    }
});


// ==========================================
// AUTHENTICATION MIDDLEWARE & ENDPOINTS
// ==========================================

// Rate limiter for login endpoint
const loginLimiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 10, // 10 attempts per window
    message: { error: 'Too many login attempts, please try again later' },
    standardHeaders: true,
    legacyHeaders: false,
    validate: { trustProxy: false }
});

// Helper function to extract a cookie value from raw Request headers without external middleware dependencies
function getCookie(cookieHeader, name) {
    if (!cookieHeader) return null;
    const cookies = cookieHeader.split(';');
    for (let c of cookies) {
        c = c.trim();
        if (c.startsWith(name + '=')) {
            return c.substring(name.length + 1);
        }
    }
    return null;
}

// JWT authentication middleware
function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    // Plain <img>/<a> tags can't send an Authorization header, so also accept
    // the token as a query string param for those GET-only media-streaming routes.
    let token = (authHeader && authHeader.split(' ')[1]) || req.query.token;

    // Fallback: check HttpOnly cookie
    if (!token && req.headers.cookie) {
        token = getCookie(req.headers.cookie, 'authToken');
    }

    if (!token) {
        return res.status(401).json({ error: 'Authentication required' });
    }
    try {
        const decoded = jwt.verify(token, JWT_SECRET);
        req.user = decoded; // { id, username, role }
        next();
    } catch (err) {
        return res.status(401).json({ error: 'Invalid or expired token' });
    }
}

// Role-check middleware factory
function requireRole(...roles) {
    return (req, res, next) => {
        if (!req.user || !roles.includes(req.user.role)) {
            return res.status(403).json({ error: 'Insufficient permissions' });
        }
        next();
    };
}

// ==========================================
// CENTRALIZED PERMISSION MATRIX (Phase 3)
// ==========================================
// '*' means all authenticated roles can access
const PERMISSIONS = {
    // Projects
    'project:create':    ['superadmin', 'admin', 'supervisor', 'estimate'],
    'project:read':      ['*'],
    'project:update':    ['superadmin', 'admin', 'supervisor', 'estimate'],
    'project:delete':    ['superadmin', 'admin'],
    'project:approve':   ['superadmin', 'admin'],
    'project:clone':     ['superadmin', 'admin', 'supervisor', 'estimate'],

    // Sections & Items
    'section:write':     ['superadmin', 'admin', 'supervisor', 'estimate'],
    'item:write':        ['superadmin', 'admin', 'supervisor', 'estimate'],

    // Products / Catalog
    'catalog:read':      ['*'],
    'catalog:write':     ['superadmin', 'admin', 'supervisor', 'estimate'],

    // FAT Reports
    'fat:create':        ['superadmin', 'admin', 'supervisor', 'estimate', 'qc', 'qa', 'contractor', 'ee_manager', 'me_manager', 'excusive', 'procurment', 'stock'],
    'fat:read':          ['*'],
    'fat:update':        ['superadmin', 'admin', 'supervisor', 'estimate', 'qc', 'qa', 'contractor', 'ee_manager', 'me_manager', 'excusive', 'procurment', 'stock'],

    // Progress
    'progress:create':   ['superadmin', 'admin', 'supervisor', 'estimate', 'production'],
    'progress:read':     ['*'],
    'progress:update':   ['superadmin', 'admin', 'supervisor', 'estimate', 'production'],

    // Documents
    'docs:upload':       ['superadmin', 'admin', 'supervisor', 'estimate', 'design', 'excusive'],
    'docs:read':         ['*'],
    'docs:delete':       ['superadmin', 'admin'],

    // Suppliers
    'supplier:read':     ['*'],
    'supplier:write':    ['superadmin', 'admin', 'procurment'],

    // Contractors
    'contractor:read':   ['*'],
    'contractor:write':  ['superadmin', 'admin', 'procurment', 'procurement_manager'],

    // Contractor RFQs
    'rfq:create':        ['superadmin', 'admin', 'supervisor', 'estimate', 'procurment', 'procurement_manager'],
    'rfq:read':          ['*'],
    'rfq:update':        ['superadmin', 'admin', 'supervisor', 'estimate', 'procurment', 'procurement_manager', 'contractor'],

    // Templates
    'template:write':    ['superadmin', 'admin'],

    // Users
    'user:read':         ['superadmin', 'admin'],
    'user:create':       ['superadmin', 'admin'],
    'user:update':       ['superadmin', 'admin'],
    'user:delete':       ['superadmin', 'admin'],

    // System
    'system:backup':     ['superadmin'],
    'system:restore':    ['superadmin'],
    'system:repair':     ['superadmin'],

    // Revisions
    'revision:create':   ['superadmin', 'admin', 'supervisor', 'estimate'],
    'revision:read':     ['*'],
};

// Roles assignable to a user via user creation / role update (excludes 'superadmin', which is singleton)
const ASSIGNABLE_ROLES = [
    'admin', 'supervisor', 'estimate', 'user', 'sale', 'design', 'production', 'qc', 'qa', 'stock', 'procurment', 'support', 'excusive', 'draft',
    'sale_manager', 'design_manager_mech', 'design_manager_elec', 'consultant_mech', 'consultant_elec', 'factory_manager', 'account_manager', 'procurement_manager',
    'contractor'
];

// Authorization middleware factory using the permission matrix
function authorize(permission) {
    return (req, res, next) => {
        if (!req.user) {
            return res.status(401).json({ error: 'Authentication required' });
        }
        const allowedRoles = PERMISSIONS[permission];
        if (!allowedRoles) {
            console.error(`Unknown permission: ${permission}`);
            return res.status(500).json({ error: 'Server configuration error' });
        }
        if (allowedRoles.includes('*') || allowedRoles.includes(req.user.role)) {
            next();
        } else {
            return res.status(403).json({ error: `Permission denied: ${permission} requires one of [${allowedRoles.join(', ')}]` });
        }
    };
}

// Helper: check if a role has a permission (for use in route handlers)
function hasPermission(role, permission) {
    const allowedRoles = PERMISSIONS[permission];
    if (!allowedRoles) return false;
    return allowedRoles.includes('*') || allowedRoles.includes(role);
}

// Login endpoint
app.post('/api/login', loginLimiter, async (req, res) => {
    let { username, password } = req.body;
    try {
        if (username) username = username.trim();
        if (password) password = password.trim();
        const [users] = await pool.query('SELECT * FROM users WHERE LOWER(username) = LOWER(?)', [username]);
        if (users.length === 0) {
            return res.status(401).json({ error: 'Username หรือ Password ไม่ถูกต้อง' });
        }
        const user = users[0];
        
        // Compare with bcrypt hash
        const validPassword = await bcrypt.compare(password, user.password);
        if (!validPassword) {
            return res.status(401).json({ error: 'Username หรือ Password ไม่ถูกต้อง' });
        }

        if (user.is_active === 0) {
            return res.status(403).json({ error: 'บัญชีผู้ใช้นี้ถูกระงับการใช้งาน กรุณาติดต่อผู้ดูแลระบบ' });
        }

        // Create JWT token
        const token = jwt.sign(
            { id: user.id, username: user.username, role: user.role },
            JWT_SECRET,
            { expiresIn: JWT_EXPIRY }
        );
        
        // Log the successful login
        await pool.query('INSERT INTO activity_logs (user_id, username, action, details) VALUES (?, ?, ?, ?)', [user.id, user.username, 'LOGIN', 'User logged into the system']);
        
        // Set HttpOnly Secure cookie
        res.cookie('authToken', token, {
            httpOnly: true,
            secure: true,
            sameSite: 'strict',
            maxAge: 8 * 60 * 60 * 1000 // 8 hours matching standard token expiry
        });

        res.json({
            token,
            id: user.id,
            username: user.username,
            role: user.role
        });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Login failed' });
    }
});

// Endpoint to view specific codebase source files safely for documentation
app.get('/api/view-code', (req, res) => {
    const filename = req.query.file;
    const allowedFiles = {
        'server.js': './server.js',
        'pr_po_stock_api.js': './pr_po_stock_api.js',
        'public/app.js': './public/app.js',
        'public/index.html': './public/index.html',
        'public/style.css': './public/style.css',
        'public/translations.js': './public/translations.js'
    };
    
    if (!filename || !allowedFiles[filename]) {
        return res.status(403).send('Access Denied or File Not Found');
    }
    
    const fs = require('fs');
    const path = require('path');
    const filePath = path.resolve(allowedFiles[filename]);
    
    fs.readFile(filePath, 'utf8', (err, data) => {
        if (err) {
            return res.status(500).send('Error reading file: ' + err.message);
        }
        
        // Escape HTML to prevent injection
        const escaped = data
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
            
        res.send(`
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="utf-8">
                <title>Code View - ${filename}</title>
                <link rel="preconnect" href="https://fonts.googleapis.com">
                <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
                <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;600&display=swap" rel="stylesheet">
                <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
                <style>
                    body {
                        background-color: #0d1117;
                        color: #c9d1d9;
                        font-family: 'Fira Code', monospace;
                        margin: 0;
                        padding: 20px;
                        line-height: 1.5;
                        font-size: 14px;
                    }
                    .header {
                        font-family: 'Inter', sans-serif;
                        padding: 15px 25px;
                        background: #161b22;
                        border: 1px solid #30363d;
                        border-radius: 8px;
                        margin-bottom: 20px;
                        display: flex;
                        justify-content: space-between;
                        align-items: center;
                    }
                    .filename {
                        font-weight: 600;
                        color: #58a6ff;
                        font-size: 1.1rem;
                        display: flex;
                        align-items: center;
                        gap: 8px;
                    }
                    .back-link {
                        color: #58a6ff;
                        text-decoration: none;
                        font-size: 14px;
                        display: flex;
                        align-items: center;
                        gap: 6px;
                        font-weight: 600;
                        transition: color 0.2s;
                    }
                    .back-link:hover {
                        color: #8cc2ff;
                    }
                    pre {
                        background: #161b22;
                        border: 1px solid #30363d;
                        padding: 20px;
                        border-radius: 8px;
                        overflow-x: auto;
                        white-space: pre;
                    }
                </style>
            </head>
            <body>
                <div class="header">
                    <span class="filename"><i class="fa-solid fa-file-code"></i> ${filename}</span>
                    <a href="/project_presentation.html" class="back-link"><i class="fa-solid fa-arrow-left"></i> Back to Presentation</a>
                </div>
                <pre><code>${escaped}</code></pre>
            </body>
            </html>
        `);
    });
});

// Get user's permissions based on role (for client-side UI enforcement)
app.get('/api/permissions', authenticateToken, (req, res) => {
    const role = req.user.role;
    const userPermissions = {};
    for (const [perm, roles] of Object.entries(PERMISSIONS)) {
        userPermissions[perm] = roles.includes('*') || roles.includes(role);
    }
    res.json({ role, permissions: userPermissions });
});

// Logout endpoint that logs activity and creates project revisions for modified projects
app.post('/api/logout', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const { modifiedProjects } = req.body;

    if (!userId) {
        return res.status(400).json({ error: 'User ID is required for logout' });
    }

    try {
        // Fetch username for logging
        const [users] = await pool.query('SELECT username FROM users WHERE id = ?', [userId]);
        const username = users.length > 0 ? users[0].username : 'Unknown';

        // Log the logout action in activity_logs
        await pool.query('INSERT INTO activity_logs (user_id, username, action, details) VALUES (?, ?, ?, ?)', 
            [userId, username, 'LOGOUT', 'User logged out of the system']);

        if (modifiedProjects && Array.isArray(modifiedProjects)) {
            for (const projectId of modifiedProjects) {
                // Find all changes for this project since the last revision
                const [logs] = await pool.query(`
                    SELECT details FROM activity_logs
                    WHERE user_id = ?
                      AND action IN ('SECTION_CREATE', 'SECTION_DELETE', 'SECTION_UPDATE', 'ITEM_CREATE', 'ITEM_UPDATE', 'ITEM_DELETE')
                      AND created_at > (
                          SELECT COALESCE(MAX(created_at), '1970-01-01')
                          FROM project_revisions
                          WHERE project_id = ?
                      )
                    ORDER BY id ASC
                `, [userId, projectId]);

                if (logs.length > 0) {
                    // Combine change logs into a clean summary
                    const changeDescriptions = logs.map(l => {
                        return l.details.replace(/\s+in\s+Project\s+ID\s+\d+/i, '').replace(/\s+of\s+Project\s+ID\s+\d+/i, '');
                    });
                    const changeSummary = changeDescriptions.join(', ');
                    
                    // Create the revision snapshot
                    await createProjectRevision(projectId, userId, changeSummary);
                }
            }
        }

        res.clearCookie('authToken');
        res.json({ success: true, message: 'Logged out and revisions created successfully' });
    } catch (err) {
        console.error('Logout error:', err);
        res.status(500).json({ error: 'Failed to process logout and revisions' });
    }
});

function getMimeType(filename) {
    const ext = path.extname(filename).toLowerCase();
    if (ext === '.pdf') return 'application/pdf';
    if (ext === '.dwg') return 'image/vnd.dwg';
    if (ext === '.dxf') return 'image/vnd.dxf';
    if (ext === '.xlsx') return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
    if (ext === '.xls') return 'application/vnd.ms-excel';
    if (ext === '.docx') return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
    if (ext === '.doc') return 'application/msword';
    if (ext === '.zip') return 'application/zip';
    if (ext === '.rar') return 'application/vnd.rar';
    return 'application/octet-stream';
}

// Helper functions for storing/serving attachment files on disk instead of as DB blobs
const UPLOADS_ROOT = path.join(__dirname, 'uploads');

function saveBufferToDisk(subdir, buffer, originalName) {
    const dir = path.join(UPLOADS_ROOT, subdir);
    fs.mkdirSync(dir, { recursive: true });
    const ext = path.extname(originalName || '');
    const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
    fs.writeFileSync(path.join(dir, filename), buffer);
    return `uploads/${subdir}/${filename}`;
}

function deleteDiskFile(relativePath) {
    if (!relativePath) return;
    fs.unlink(path.join(__dirname, relativePath), () => {});
}

function copyDiskFile(subdir, relativePath) {
    if (!relativePath) return null;
    const dir = path.join(UPLOADS_ROOT, subdir);
    fs.mkdirSync(dir, { recursive: true });
    const ext = path.extname(relativePath);
    const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
    const destRelative = `uploads/${subdir}/${filename}`;
    fs.copyFileSync(path.join(__dirname, relativePath), path.join(__dirname, destRelative));
    return destRelative;
}

function sendDiskFile(res, relativePath, filename, contentType) {
    res.setHeader('Content-Type', contentType);
    res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(filename)}"`);
    res.sendFile(path.join(__dirname, relativePath), (err) => {
        if (err && !res.headersSent) {
            res.status(404).send('File not found on disk');
        }
    });
}

// Roles that may view Contractor RFQs/quotations across ALL projects, read-only,
// without gaining the broader project write access that 'excusive' has.
function hasGlobalRfqReadAccess(role) {
    return ['superadmin', 'admin', 'sale_manager'].includes(role);
}

// Helper functions for authorization checks
async function checkProjectWriteAccess(projectId, userId, userRole) {
    if (userRole === 'admin' || userRole === 'superadmin' || userRole === 'excusive') return true;
    const [proj] = await pool.query('SELECT created_by FROM projects WHERE id = ?', [projectId]);
    if (proj.length > 0 && proj[0].created_by === parseInt(userId)) {
        return true;
    }
    const [perm] = await pool.query('SELECT 1 FROM project_permissions WHERE project_id = ? AND user_id = ?', [projectId, userId]);
    if (perm.length > 0) return true;

    if (userRole === 'contractor') {
        const [rfq] = await pool.query(`
            SELECT 1 FROM contractor_rfqs cr
            JOIN contractors c ON cr.contractor_id = c.id
            WHERE cr.project_id = ? AND c.user_id = ? AND cr.is_deleted = 0
              AND cr.status IN ('draft', 'sent', 'submitted', 'replied')
        `, [projectId, userId]);
        return rfq.length > 0;
    }

    return false;
}

async function checkSectionWriteAccess(sectionId, userId, userRole) {
    if (userRole === 'admin' || userRole === 'superadmin' || userRole === 'excusive') return true;
    const [sec] = await pool.query('SELECT project_id FROM project_sections WHERE id = ?', [sectionId]);
    if (sec.length > 0) {
        return checkProjectWriteAccess(sec[0].project_id, userId, userRole);
    }
    return false;
}

async function checkItemWriteAccess(itemId, userId, userRole) {
    if (userRole === 'admin' || userRole === 'superadmin' || userRole === 'excusive') return true;
    const [item] = await pool.query(`
        SELECT ps.project_id FROM project_items pi
        JOIN project_sections ps ON pi.section_id = ps.id
        WHERE pi.id = ?
    `, [itemId]);
    if (item.length > 0) {
        return checkProjectWriteAccess(item[0].project_id, userId, userRole);
    }
    return false;
}

// Helper function to log user actions in activity_logs
async function logActivity(userId, username, action, details) {
    let uName = username;
    if (userId && !uName) {
        try {
            const [users] = await pool.query('SELECT username FROM users WHERE id = ?', [userId]);
            if (users.length > 0) uName = users[0].username;
        } catch (dbErr) {
            console.error('Error fetching username for activity log:', dbErr);
        }
    }
    let projectId = null;
    if (details) {
        const match = details.match(/Project ID\s+(\d+)/i);
        if (match) {
            projectId = parseInt(match[1]);
        }
    }
    try {
        await pool.query(
            'INSERT INTO activity_logs (user_id, username, action, details, project_id) VALUES (?, ?, ?, ?, ?)',
            [userId || null, uName || null, action, details, projectId]
        );
    } catch (err) {
        console.error('Error writing activity log:', err);
    }
}

// Helper to snapshot active sections and items for revision history
async function createProjectRevision(projectId, userId, changeSummary = '') {
    try {
        // Get username for log
        const [users] = await pool.query('SELECT username FROM users WHERE id = ?', [userId]);
        const username = users.length > 0 ? users[0].username : 'Unknown';

        // 1. Get current max revision number
        const [revs] = await pool.query('SELECT MAX(revision_number) as max_rev FROM project_revisions WHERE project_id = ?', [projectId]);
        const nextRev = (revs[0].max_rev || 0) + 1;

        // 2. Fetch all active sections for this project
        const [sections] = await pool.query('SELECT * FROM project_sections WHERE project_id = ? AND is_deleted = 0 ORDER BY order_index ASC, id ASC', [projectId]);
        const snapshot = [];
        
        for (let sec of sections) {
            const [items] = await pool.query('SELECT * FROM project_items WHERE section_id = ? AND is_deleted = 0 ORDER BY order_index ASC, id ASC', [sec.id]);
            snapshot.push({
                section_id: sec.id,
                section_name: sec.name,
                items: items.map(item => ({
                    id: item.id,
                    product_id: item.product_id,
                    brand: item.brand,
                    description: item.description,
                    type: item.type,
                    qty: parseFloat(item.qty) || 0,
                    unit: item.unit,
                    cost_material: parseFloat(item.cost_material) || 0,
                    cost_labour: parseFloat(item.cost_labour) || 0,
                    price_material: parseFloat(item.price_material) || 0,
                    price_labour: parseFloat(item.price_labour) || 0,
                    discount: parseFloat(item.discount) || 0,
                    is_free_issue: item.is_free_issue,
                    is_modified: item.is_modified
                }))
            });
        }

        // 3. Save snapshot to project_revisions
        await pool.query(`
            INSERT INTO project_revisions (project_id, revision_number, user_id, snapshot_data, change_summary)
            VALUES (?, ?, ?, ?, ?)
        `, [projectId, nextRev, userId, JSON.stringify(snapshot), changeSummary]);

        // 4. Log the revision change
        await logActivity(userId, username, 'PROJECT_REVISION', `Project ID ${projectId} revision ${nextRev} created: ${changeSummary}`);
        console.log(`Project Revision ${nextRev} created for Project ID ${projectId} by User ${username}`);
    } catch (err) {
        console.error('Error creating project revision snapshot:', err);
    }
}

// ==========================================
// SYSTEM TEMPLATES ENDPOINTS
// ==========================================

// Get all templates
app.get('/api/system-templates', authenticateToken, async (req, res) => {
    try {
        const [rows] = await pool.query('SELECT * FROM system_templates ORDER BY id DESC');
        const parsed = rows.map(r => ({
            ...r,
            items: JSON.parse(r.items || '[]')
        }));
        res.json(parsed);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve system templates' });
    }
});

// Update a system template
app.put('/api/system-templates/:systemName', authenticateToken, async (req, res) => {
    const { systemName } = req.params;
    const { items } = req.body;
    const role = req.user.role;
    const userId = req.user.id;

    if (role !== 'admin' && role !== 'superadmin' && role !== 'supervisor') {
        return res.status(403).json({ error: 'Read-only access' });
    }

    if (!Array.isArray(items)) {
        return res.status(400).json({ error: 'Items must be an array' });
    }

    try {
        const [existing] = await pool.query('SELECT id FROM system_templates WHERE system_name = ?', [systemName]);
        if (existing.length > 0) {
            await pool.query('UPDATE system_templates SET items = ? WHERE system_name = ?', [JSON.stringify(items), systemName]);
        } else {
            await pool.query('INSERT INTO system_templates (system_name, items) VALUES (?, ?)', [systemName, JSON.stringify(items)]);
        }
        await logActivity(userId, null, 'TEMPLATE_UPDATE', `Updated system template for "${systemName}"`);
        res.json({ success: true, system_name: systemName, items });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update system template' });
    }
});

// Update a system template by ID (allows renaming)
app.put('/api/system-templates/id/:id', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const { system_name, items } = req.body;
    const role = req.user.role;
    const userId = req.user.id;

    if (role !== 'admin' && role !== 'superadmin' && role !== 'supervisor') {
        return res.status(403).json({ error: 'Read-only access' });
    }

    if (!system_name) {
        return res.status(400).json({ error: 'System name is required' });
    }
    if (!Array.isArray(items)) {
        return res.status(400).json({ error: 'Items must be an array' });
    }

    try {
        // Check if name already exists for a different ID to prevent duplicate names
        const [duplicate] = await pool.query('SELECT id FROM system_templates WHERE system_name = ? AND id != ?', [system_name, id]);
        if (duplicate.length > 0) {
            return res.status(400).json({ error: 'System name already exists' });
        }
        await pool.query('UPDATE system_templates SET system_name = ?, items = ? WHERE id = ?', [system_name, JSON.stringify(items), id]);
        await logActivity(userId, null, 'TEMPLATE_UPDATE', `Updated system template ID ${id} to "${system_name}"`);
        res.json({ success: true, id, system_name, items });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update system template' });
    }
});

// ==========================================
// 1. PRODUCTS (CATALOG) ENDPOINTS
// ==========================================

// Get all products (sends flags has_image and has_pdf, excludes raw binaries to save bandwidth)
app.get('/api/products', authenticateToken, async (req, res) => {
    try {
        const { q, category, group, system } = req.query;
        let query = `
            SELECT p.id, p.brand, p.description, p.type, p.model, p.lb_cost, p.mt_cost, p.lb_qou, p.mt_qou, p.discount, p.weight, p.unit, p.category, p.prod_group, p.prod_system, p.details,
                   p.supplier_id, p.supplier_price, p.supplier_quote_name, p.lead_time, p.sm_no,
                   (p.image_path IS NOT NULL) AS has_image,
                   (p.pdf_path IS NOT NULL) AS has_pdf,
                   (p.supplier_quote_path IS NOT NULL) AS has_supplier_quote,
                   s.name AS supplier_name, s.website AS supplier_website
            FROM products p
            LEFT JOIN suppliers s ON p.supplier_id = s.id
            WHERE 1=1`;
        const params = [];

        if (category) {
            query += ' AND p.category = ?';
            params.push(category);
        }

        if (group) {
            query += ' AND p.prod_group = ?';
            params.push(group);
        }

        if (system) {
            query += ' AND p.prod_system = ?';
            params.push(system);
        }

        if (q) {
            query += ' AND (p.brand LIKE ? OR p.description LIKE ? OR p.type LIKE ? OR p.model LIKE ? OR p.category LIKE ? OR p.prod_group LIKE ? OR p.prod_system LIKE ? OR p.sm_no LIKE ?)';
            const searchPattern = `%${q}%`;
            params.push(searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern);
        }

        query += ' ORDER BY p.id DESC';
        const [rows] = await pool.query(query, params);
        
        const role = req.user.role;
        if (role !== 'superadmin' && role !== 'excusive' && role !== 'admin') {
            rows.forEach(p => {
                p.lb_cost = 0;
                p.mt_cost = 0;
                p.supplier_price = 0;
            });
        }
        
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve products' });
    }
});

// Stream product image from disk
app.get('/api/products/:id/image', authenticateToken, async (req, res) => {
    try {
        const { id } = req.params;
        const [rows] = await pool.query('SELECT image_path, image_mime FROM products WHERE id = ?', [id]);

        if (rows.length === 0 || !rows[0].image_path) {
            return res.status(404).send('Image not found');
        }

        sendDiskFile(res, rows[0].image_path, 'image', rows[0].image_mime || 'image/jpeg');
    } catch (err) {
        console.error(err);
        res.status(500).send('Error streaming image');
    }
});

// Stream product PDF specifications from disk
app.get('/api/products/:id/pdf', authenticateToken, async (req, res) => {
    try {
        const { id } = req.params;
        const [rows] = await pool.query('SELECT pdf_path, pdf_name FROM products WHERE id = ?', [id]);

        if (rows.length === 0 || !rows[0].pdf_path) {
            return res.status(404).send('PDF datasheet not found');
        }

        sendDiskFile(res, rows[0].pdf_path, rows[0].pdf_name || 'datasheet.pdf', 'application/pdf');
    } catch (err) {
        console.error(err);
        res.status(500).send('Error streaming PDF');
    }
});

// Get unique categories list
app.get('/api/categories', authenticateToken, async (req, res) => {
    try {
        const [rows] = await pool.query('SELECT DISTINCT category FROM products WHERE category IS NOT NULL AND category != "" ORDER BY category');
        res.json(rows.map(r => r.category));
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve categories' });
    }
});

// Get unique groups list
app.get('/api/groups', authenticateToken, async (req, res) => {
    try {
        const [rows] = await pool.query('SELECT DISTINCT prod_group FROM products WHERE prod_group IS NOT NULL AND prod_group != "" ORDER BY prod_group');
        res.json(rows.map(r => r.prod_group));
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve groups' });
    }
});

// Get unique systems list
app.get('/api/systems', authenticateToken, async (req, res) => {
    try {
        const [rows] = await pool.query('SELECT DISTINCT prod_system FROM products WHERE prod_system IS NOT NULL AND prod_system != "" ORDER BY prod_system');
        res.json(rows.map(r => r.prod_system));
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve systems' });
    }
});

// Create product (Only Admin can manage catalog)
app.post('/api/products', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (!PERMISSIONS['catalog:write'].includes(role)) {
        return res.status(403).json({ error: 'Only admins or supervisors can modify the product catalog' });
    }

    try {
        const { brand, description, type, model, sm_no, lb_cost, mt_cost, lb_qou, mt_qou, discount, weight, unit, category, prod_group, prod_system, details, supplier_id, supplier_price, lead_time } = req.body;
        const [result] = await pool.query(`
            INSERT INTO products (brand, description, type, model, sm_no, lb_cost, mt_cost, lb_qou, mt_qou, discount, weight, unit, category, prod_group, prod_system, details, supplier_id, supplier_price, lead_time)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        `, [brand, description, type, model || null, sm_no || null, lb_cost || 0, mt_cost || 0, lb_qou || 0, mt_qou || 0, discount || 0, weight || 0, unit, category || 'Uncategorized', prod_group || '', prod_system || '', details, supplier_id || null, supplier_price || 0, lead_time || null]);
        
        const [newProduct] = await pool.query(`
            SELECT p.id, p.brand, p.description, p.type, p.model, p.lb_cost, p.mt_cost, p.lb_qou, p.mt_qou, p.discount, p.weight, p.unit, p.category, p.prod_group, p.prod_system, p.details,
                   p.supplier_id, p.supplier_price, p.supplier_quote_name, p.lead_time, p.sm_no,
                   (p.image_path IS NOT NULL) AS has_image,
                   (p.pdf_path IS NOT NULL) AS has_pdf,
                   (p.supplier_quote_path IS NOT NULL) AS has_supplier_quote,
                   s.name AS supplier_name, s.website AS supplier_website
            FROM products p
            LEFT JOIN suppliers s ON p.supplier_id = s.id
            WHERE p.id = ?
        `, [result.insertId]);
        res.status(201).json(newProduct[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to create product' });
    }
});

// Update product
app.put('/api/products/:id', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (!PERMISSIONS['catalog:write'].includes(role)) {
        return res.status(403).json({ error: 'Only admins or supervisors can modify the product catalog' });
    }

    try {
        const { id } = req.params;
        const { brand, description, type, model, sm_no, lb_cost, mt_cost, lb_qou, mt_qou, discount, weight, unit, category, prod_group, prod_system, details, supplier_id, supplier_price, lead_time } = req.body;
        
        await pool.query(`
            UPDATE products 
            SET brand=?, description=?, type=?, model=?, sm_no=?, lb_cost=?, mt_cost=?, lb_qou=?, mt_qou=?, discount=?, weight=?, unit=?, category=?, prod_group=?, prod_system=?, details=?, supplier_id=?, supplier_price=?, lead_time=?
            WHERE id=?
        `, [brand, description, type, model || null, sm_no || null, lb_cost || 0, mt_cost || 0, lb_qou || 0, mt_qou || 0, discount || 0, weight || 0, unit, category || 'Uncategorized', prod_group || '', prod_system || '', details, supplier_id || null, supplier_price || 0, lead_time || null, id]);
        
        // Synchronize matching items in project_items table to propagate product updates to active projects
        await pool.query(`
            UPDATE project_items
            SET brand = ?, description = ?, type = ?, model = ?, unit = ?,
                cost_material = ?, cost_labour = ?,
                price_material = ?, price_labour = ?,
                sm_no = ?
            WHERE product_id = ?
        `, [brand, description, type, model || null, unit, mt_cost || 0, lb_cost || 0, mt_qou || 0, lb_qou || 0, sm_no || null, id]);
        
        const [updatedProduct] = await pool.query(`
            SELECT p.id, p.brand, p.description, p.type, p.model, p.lb_cost, p.mt_cost, p.lb_qou, p.mt_qou, p.discount, p.weight, p.unit, p.category, p.prod_group, p.prod_system, p.details,
                   p.supplier_id, p.supplier_price, p.supplier_quote_name, p.lead_time, p.sm_no,
                   (p.image_path IS NOT NULL) AS has_image,
                   (p.pdf_path IS NOT NULL) AS has_pdf,
                   (p.supplier_quote_path IS NOT NULL) AS has_supplier_quote,
                   s.name AS supplier_name, s.website AS supplier_website
            FROM products p
            LEFT JOIN suppliers s ON p.supplier_id = s.id
            WHERE p.id = ?
        `, [id]);
        res.json(updatedProduct[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update product' });
    }
});

// Clone product (Only Admin can manage catalog)
app.post('/api/products/:id/clone', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (!PERMISSIONS['catalog:write'].includes(role)) {
        return res.status(403).json({ error: 'Only admins or supervisors can modify the product catalog' });
    }

    try {
        const { id } = req.params;

        // 1. Get the source product
        const [source] = await pool.query('SELECT * FROM products WHERE id = ?', [id]);
        if (source.length === 0) {
            return res.status(404).json({ error: 'Source product not found' });
        }

        const p = source[0];
        // Append " (Copy)" to the description to distinguish it
        const newDescription = p.description ? `${p.description} (Copy)` : 'New Product Copy';

        const clonedImagePath = copyDiskFile('products/images', p.image_path);
        const clonedPdfPath = copyDiskFile('products/pdfs', p.pdf_path);
        const clonedQuotePath = copyDiskFile('products/quotes', p.supplier_quote_path);

        // 2. Insert duplicated product
        const [result] = await pool.query(`
            INSERT INTO products (
                brand, description, type, model, lb_cost, mt_cost, lb_qou, mt_qou,
                discount, weight, unit, category, prod_group, prod_system, details,
                image_path, image_mime, pdf_path, pdf_name,
                supplier_id, supplier_price, supplier_quote_path, supplier_quote_name, lead_time
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        `, [
            p.brand, newDescription, p.type, p.model, p.lb_cost, p.mt_cost, p.lb_qou, p.mt_qou,
            p.discount, p.weight, p.unit, p.category, p.prod_group, p.prod_system, p.details,
            clonedImagePath, p.image_mime, clonedPdfPath, p.pdf_name,
            p.supplier_id, p.supplier_price, clonedQuotePath, p.supplier_quote_name, p.lead_time
        ]);

        // 3. Log the activity
        const userId = req.user.id;
        await logActivity(userId, null, 'PRODUCT_CLONE', `Cloned product "${p.description}" to new product ID ${result.insertId}`);

        // 4. Retrieve and return the new product
        const [newProduct] = await pool.query(`
            SELECT p.id, p.brand, p.description, p.type, p.model, p.lb_cost, p.mt_cost, p.lb_qou, p.mt_qou, p.discount, p.weight, p.unit, p.category, p.prod_group, p.prod_system, p.details,
                   p.supplier_id, p.supplier_price, p.supplier_quote_name, p.lead_time,
                   (p.image_path IS NOT NULL) AS has_image,
                   (p.pdf_path IS NOT NULL) AS has_pdf,
                   (p.supplier_quote_path IS NOT NULL) AS has_supplier_quote,
                   s.name AS supplier_name, s.website AS supplier_website
            FROM products p
            LEFT JOIN suppliers s ON p.supplier_id = s.id
            WHERE p.id = ?
        `, [result.insertId]);

        res.status(201).json(newProduct[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to clone product' });
    }
});

// Upload image binary directly to product column
app.post('/api/products/:id/upload', authenticateToken, upload.single('image'), async (req, res) => {
    const role = req.user.role;
    if (!PERMISSIONS['catalog:write'].includes(role)) {
        return res.status(403).json({ error: 'Only admins or supervisors can upload catalog files' });
    }
    try {
        const { id } = req.params;
        if (!req.file) {
            return res.status(400).json({ error: 'No image file uploaded' });
        }

        const [existing] = await pool.query('SELECT image_path FROM products WHERE id = ?', [id]);
        const imagePath = saveBufferToDisk('products/images', req.file.buffer, req.file.originalname);
        await pool.query('UPDATE products SET image_path = ?, image_mime = ? WHERE id = ?', [imagePath, req.file.mimetype, id]);
        if (existing.length > 0) deleteDiskFile(existing[0].image_path);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to upload product image' });
    }
});

// Upload PDF binary datasheet directly to product column
app.post('/api/products/:id/upload-pdf', authenticateToken, upload.single('pdf'), async (req, res) => {
    const role = req.user.role;
    if (!PERMISSIONS['catalog:write'].includes(role)) {
        return res.status(403).json({ error: 'Only admins or supervisors can upload catalog files' });
    }
    try {
        const { id } = req.params;
        if (!req.file) {
            return res.status(400).json({ error: 'No PDF datasheet file uploaded' });
        }
        
        const pdfName = Buffer.from(req.file.originalname, 'latin1').toString('utf8');
        const [existing] = await pool.query('SELECT pdf_path FROM products WHERE id = ?', [id]);
        const pdfPath = saveBufferToDisk('products/pdfs', req.file.buffer, pdfName);
        await pool.query('UPDATE products SET pdf_path = ?, pdf_name = ? WHERE id = ?', [pdfPath, pdfName, id]);
        if (existing.length > 0) deleteDiskFile(existing[0].pdf_path);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to upload product PDF' });
    }
});

// Upload image binary via internet URL directly to product column
app.post('/api/products/:id/upload-image-url', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (!PERMISSIONS['catalog:write'].includes(role)) {
        return res.status(403).json({ error: 'Only admins or supervisors can upload catalog files' });
    }
    try {
        const { id } = req.params;
        const { url } = req.body;
        if (!url) {
            return res.status(400).json({ error: 'No image URL provided' });
        }
        
        const { buffer, mimeType } = await downloadFile(url);

        let urlPathname = url;
        try { urlPathname = new URL(url).pathname; } catch (urlErr) { /* keep raw url as fallback */ }

        const [existing] = await pool.query('SELECT image_path FROM products WHERE id = ?', [id]);
        const imagePath = saveBufferToDisk('products/images', buffer, urlPathname);
        await pool.query('UPDATE products SET image_path = ?, image_mime = ? WHERE id = ?', [imagePath, mimeType || 'image/png', id]);
        if (existing.length > 0) deleteDiskFile(existing[0].image_path);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to download and upload image from URL' });
    }
});

// Download PDF manual via internet URL directly to product column
app.post('/api/products/:id/upload-pdf-url', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (!PERMISSIONS['catalog:write'].includes(role)) {
        return res.status(403).json({ error: 'Only admins or supervisors can upload catalog files' });
    }
    try {
        const { id } = req.params;
        const { url } = req.body;
        if (!url) {
            return res.status(400).json({ error: 'No PDF URL provided' });
        }
        
        const { buffer } = await downloadFile(url);
        
        // Get pdf_name from URL path segments
        let pdfName = 'manual.pdf';
        try {
            const parsedUrl = new URL(url);
            const pathSegments = parsedUrl.pathname.split('/');
            const lastSegment = pathSegments[pathSegments.length - 1];
            if (lastSegment && lastSegment.toLowerCase().endsWith('.pdf')) {
                pdfName = decodeURIComponent(lastSegment);
            }
        } catch (urlErr) {
            // Revert to default
        }
        
        const [existing] = await pool.query('SELECT pdf_path FROM products WHERE id = ?', [id]);
        const pdfPath = saveBufferToDisk('products/pdfs', buffer, pdfName);
        await pool.query('UPDATE products SET pdf_path = ?, pdf_name = ? WHERE id = ?', [pdfPath, pdfName, id]);
        if (existing.length > 0) deleteDiskFile(existing[0].pdf_path);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to download and upload PDF from URL' });
    }
});

// Delete PDF datasheet
app.delete('/api/products/:id/pdf', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (!PERMISSIONS['catalog:write'].includes(role)) {
        return res.status(403).json({ error: 'Only admins or supervisors can manage catalog files' });
    }
    try {
        const { id } = req.params;
        const [existing] = await pool.query('SELECT pdf_path FROM products WHERE id = ?', [id]);
        await pool.query('UPDATE products SET pdf_path = NULL, pdf_name = NULL WHERE id = ?', [id]);
        if (existing.length > 0) deleteDiskFile(existing[0].pdf_path);
        res.json({ success: true, message: 'PDF datasheet deleted' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete PDF datasheet' });
    }
});

// Delete product
app.delete('/api/products/:id', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (!PERMISSIONS['catalog:write'].includes(role)) {
        return res.status(403).json({ error: 'Only admins or supervisors can modify the product catalog' });
    }
    try {
        const { id } = req.params;
        await pool.query('DELETE FROM products WHERE id = ?', [id]);
        res.json({ success: true, message: 'Product deleted' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete product' });
    }
});

// ==========================================
// 1.1 SUPPLIERS ENDPOINTS
// ==========================================

// Get all suppliers
app.get('/api/suppliers', authenticateToken, async (req, res) => {
    try {
        const [rows] = await pool.query('SELECT * FROM suppliers ORDER BY name');
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve suppliers' });
    }
});

// Create supplier
app.post('/api/suppliers', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (role !== 'admin' && role !== 'superadmin') {
        return res.status(403).json({ error: 'Only admins can manage suppliers' });
    }
    try {
        const { name, contact_info, website } = req.body;
        const [result] = await pool.query('INSERT INTO suppliers (name, contact_info, website) VALUES (?, ?, ?)', [name, contact_info, website]);
        res.status(201).json({ id: result.insertId, name, contact_info, website });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to create supplier' });
    }
});

// Update supplier
app.put('/api/suppliers/:id', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (role !== 'admin' && role !== 'superadmin') {
        return res.status(403).json({ error: 'Only admins can manage suppliers' });
    }
    try {
        const { id } = req.params;
        const { name, contact_info, website } = req.body;
        await pool.query('UPDATE suppliers SET name = ?, contact_info = ?, website = ? WHERE id = ?', [name, contact_info, website, id]);
        res.json({ id, name, contact_info, website });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update supplier' });
    }
});

// Delete supplier
app.delete('/api/suppliers/:id', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (role !== 'admin' && role !== 'superadmin') {
        return res.status(403).json({ error: 'Only admins can delete suppliers' });
    }
    try {
        const { id } = req.params;
        await pool.query('DELETE FROM suppliers WHERE id = ?', [id]);
        res.json({ success: true, message: 'Supplier deleted' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete supplier' });
    }
});

// Upload supplier quote PDF or image file
app.post('/api/products/:id/upload-supplier-quote', authenticateToken, upload.single('quote'), async (req, res) => {
    const role = req.user.role;
    if (role !== 'admin' && role !== 'superadmin') {
        return res.status(403).json({ error: 'Only admins can upload supplier quotes' });
    }
    try {
        const { id } = req.params;
        if (!req.file) {
            return res.status(400).json({ error: 'No quotation file uploaded' });
        }
        const quoteName = Buffer.from(req.file.originalname, 'latin1').toString('utf8');
        const [existing] = await pool.query('SELECT supplier_quote_path FROM products WHERE id = ?', [id]);
        const quotePath = saveBufferToDisk('products/quotes', req.file.buffer, quoteName);
        await pool.query('UPDATE products SET supplier_quote_path = ?, supplier_quote_name = ? WHERE id = ?', [quotePath, quoteName, id]);
        if (existing.length > 0) deleteDiskFile(existing[0].supplier_quote_path);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to upload supplier quote' });
    }
});

// Stream/view supplier quote document from disk
app.get('/api/products/:id/supplier-quote', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (role !== 'superadmin' && role !== 'excusive') {
        return res.status(403).send('Not authorized to view supplier quotes containing sensitive costs');
    }
    try {
        const { id } = req.params;
        const [rows] = await pool.query('SELECT supplier_quote_path, supplier_quote_name FROM products WHERE id = ?', [id]);

        if (rows.length === 0 || !rows[0].supplier_quote_path) {
            return res.status(404).send('Supplier quote document not found');
        }

        const filename = rows[0].supplier_quote_name || 'quote.pdf';
        let contentType = 'application/pdf';
        if (filename.toLowerCase().endsWith('.jpg') || filename.toLowerCase().endsWith('.jpeg')) {
            contentType = 'image/jpeg';
        } else if (filename.toLowerCase().endsWith('.png')) {
            contentType = 'image/png';
        }

        sendDiskFile(res, rows[0].supplier_quote_path, filename, contentType);
    } catch (err) {
        console.error(err);
        res.status(500).send('Error streaming supplier quote');
    }
});

// Delete supplier quote document
app.delete('/api/products/:id/supplier-quote', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (role !== 'admin' && role !== 'superadmin') {
        return res.status(403).json({ error: 'Only admins can delete supplier quotes' });
    }
    try {
        const { id } = req.params;
        const [existing] = await pool.query('SELECT supplier_quote_path FROM products WHERE id = ?', [id]);
        await pool.query('UPDATE products SET supplier_quote_path = NULL, supplier_quote_name = NULL WHERE id = ?', [id]);
        if (existing.length > 0) deleteDiskFile(existing[0].supplier_quote_path);
        res.json({ success: true, message: 'Supplier quote deleted' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete supplier quote' });
    }
});

// ==========================================
// 1b. CONTRACTORS ENDPOINTS
// ==========================================

// Get all contractors (with optional linked username)
app.get('/api/contractors', authenticateToken, async (req, res) => {
    try {
        const [rows] = await pool.query(`
            SELECT c.*, u.username
            FROM contractors c
            LEFT JOIN users u ON c.user_id = u.id
            WHERE c.is_deleted = 0
            ORDER BY c.name
        `);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve contractors' });
    }
});

// Create contractor
app.post('/api/contractors', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (!PERMISSIONS['contractor:write'].includes(role)) {
        return res.status(403).json({ error: 'Not authorized to manage contractors' });
    }
    try {
        const { name, contact_info, website, user_id } = req.body;
        if (user_id) {
            const [linkedUser] = await pool.query('SELECT role FROM users WHERE id = ?', [user_id]);
            if (linkedUser.length === 0 || linkedUser[0].role !== 'contractor') {
                return res.status(400).json({ error: 'Linked account must be an existing user with role "contractor"' });
            }
        }
        const [result] = await pool.query('INSERT INTO contractors (name, contact_info, website, user_id) VALUES (?, ?, ?, ?)', [name, contact_info, website, user_id || null]);
        res.status(201).json({ id: result.insertId, name, contact_info, website, user_id: user_id || null });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to create contractor' });
    }
});

// Update contractor
app.put('/api/contractors/:id', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (!PERMISSIONS['contractor:write'].includes(role)) {
        return res.status(403).json({ error: 'Not authorized to manage contractors' });
    }
    try {
        const { id } = req.params;
        const { name, contact_info, website, user_id } = req.body;
        if (user_id) {
            const [linkedUser] = await pool.query('SELECT role FROM users WHERE id = ?', [user_id]);
            if (linkedUser.length === 0 || linkedUser[0].role !== 'contractor') {
                return res.status(400).json({ error: 'Linked account must be an existing user with role "contractor"' });
            }
        }
        await pool.query('UPDATE contractors SET name = ?, contact_info = ?, website = ?, user_id = ? WHERE id = ?', [name, contact_info, website, user_id || null, id]);
        res.json({ id, name, contact_info, website, user_id: user_id || null });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update contractor' });
    }
});

// Delete contractor (soft delete — RFQ history references it by FK)
app.delete('/api/contractors/:id', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (!PERMISSIONS['contractor:write'].includes(role)) {
        return res.status(403).json({ error: 'Not authorized to delete contractors' });
    }
    try {
        const { id } = req.params;
        await pool.query('UPDATE contractors SET is_deleted = 1 WHERE id = ?', [id]);
        res.json({ success: true, message: 'Contractor deleted' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete contractor' });
    }
});

// ==========================================
// 2. PROJECTS ENDPOINTS
// ==========================================

// Get all projects
app.get('/api/projects', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const role = req.user.role;

    const showDeleted = req.query.show_deleted === 'true' ? 1 : 0;

    try {
        let query = `
            SELECT p.id, p.name, p.client_name, p.description, p.created_by, p.status, p.created_at, p.updated_at, p.drawing_name, p.spec_name, p.is_deleted, p.project_type, p.external_name,
                   COALESCE((SELECT MAX(revision_number) FROM project_revisions WHERE project_id = p.id), 0) as current_revision
            FROM projects p
            WHERE COALESCE(p.is_deleted, 0) = ?
        `;
        const params = [showDeleted];

        if (role === 'contractor') {
            query = `
                SELECT DISTINCT p.id, p.name, p.client_name, p.description, p.created_by, p.status, p.created_at, p.updated_at, p.drawing_name, p.spec_name, p.is_deleted, p.project_type, p.external_name,
                       COALESCE((SELECT MAX(revision_number) FROM project_revisions WHERE project_id = p.id), 0) as current_revision,
                       EXISTS (
                           SELECT 1 FROM contractor_rfqs cr2 
                           JOIN contractors c2 ON cr2.contractor_id = c2.id 
                           WHERE cr2.project_id = p.id AND c2.user_id = ? AND cr2.is_deleted = 0 AND cr2.status = 'sent'
                       ) AS has_pending_rfq
                FROM projects p
                LEFT JOIN project_permissions pp ON p.id = pp.project_id
                LEFT JOIN contractor_rfqs cr ON p.id = cr.project_id AND cr.is_deleted = 0
                LEFT JOIN contractors c ON cr.contractor_id = c.id
                WHERE COALESCE(p.is_deleted, 0) = ? 
                  AND (p.created_by = ? OR pp.user_id = ? OR (c.user_id = ? AND cr.status IN ('draft', 'sent', 'submitted', 'replied')))
            `;
            params.length = 0;
            params.push(userId, showDeleted, userId, userId, userId);
        } else if (role !== 'admin' && role !== 'superadmin' && role !== 'excusive') {
            query = `
                SELECT DISTINCT p.id, p.name, p.client_name, p.description, p.created_by, p.status, p.created_at, p.updated_at, p.drawing_name, p.spec_name, p.is_deleted, p.project_type, p.external_name,
                       COALESCE((SELECT MAX(revision_number) FROM project_revisions WHERE project_id = p.id), 0) as current_revision
                FROM projects p
                LEFT JOIN project_permissions pp ON p.id = pp.project_id
                WHERE COALESCE(p.is_deleted, 0) = ? AND (p.created_by = ? OR pp.user_id = ?)
            `;
            params.length = 0;
            params.push(showDeleted, userId, userId);
        }
        
        query += ' ORDER BY p.id DESC';
        const [projects] = await pool.query(query, params);
        
        // Enrich projects with high-level totals
        for (let proj of projects) {
            const [items] = await pool.query(`
                SELECT pi.* FROM project_items pi
                JOIN project_sections ps ON pi.section_id = ps.id
                WHERE ps.project_id = ?
            `, [proj.id]);
            
            let totalCost = 0;
            let totalQuote = 0;
            
            items.forEach(item => {
                const qty = parseFloat(item.qty) || 0;
                const isFree = item.is_free_issue === 1;
                const costMat = isFree ? 0 : parseFloat(item.cost_material) || 0;
                const costLab = parseFloat(item.cost_labour) || 0;
                const priceMat = isFree ? 0 : parseFloat(item.price_material) || 0;
                const priceLab = parseFloat(item.price_labour) || 0;
                const discount = parseFloat(item.discount) || 0;

                const itemCost = (costMat + costLab) * qty;
                const itemQuote = (priceMat + priceLab) * qty * (1 - discount / 100);

                totalCost += itemCost;
                totalQuote += itemQuote;
            });

            proj.total_quote = totalQuote;
            if (role !== 'superadmin' && role !== 'excusive') {
                proj.total_cost = 0;
                proj.total_profit = 0;
                proj.profit_percent = 0;
            } else {
                proj.total_cost = totalCost;
                proj.total_profit = totalQuote - totalCost;
                proj.profit_percent = totalCost > 0 ? ((totalQuote - totalCost) / totalCost) * 100 : 0;
            }
        }

        res.json(projects);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve projects' });
    }
});

// Create project
// Default virtual folder structure created automatically for every new project
const DEFAULT_DOCUMENT_FOLDERS = [
    '/Confidential Documents',
    '/General Documents',
    '/Documents requiring NDA',
    '/Internal Documents',
    '/Internal Documents/Approval Documents - Submittals',
    '/Internal Documents/Complaints & Feedback',
    '/Internal Documents/Contractor Quotations',
    '/Internal Documents/Factory Acceptance Test (FAT)',
    '/Internal Documents/Handover Documents',
    '/Internal Documents/Performance Evaluations'
];

app.post('/api/projects', authenticateToken, upload.none(), async (req, res) => {
    const userId = req.user.id;
    const role = req.user.role;
    if (role !== 'admin' && role !== 'superadmin' && role !== 'supervisor') {
        return res.status(403).json({ error: 'Read-only access' });
    }

    try {
        const { name, client_name, description, project_type, external_name } = req.body;

        const [result] = await pool.query(`
            INSERT INTO projects (name, client_name, description, created_by, project_type, external_name) 
            VALUES (?, ?, ?, ?, ?, ?)
        `, [name, client_name, description, userId || null, project_type || null, external_name || null]);
        
        const newProjectId = result.insertId;

        // Create the default document folder structure for the new project
        for (const folderPath of DEFAULT_DOCUMENT_FOLDERS) {
            await pool.query('INSERT INTO project_folders (project_id, folder_path) VALUES (?, ?)', [newProjectId, folderPath]);
        }

        // Auto-populate sections and checklist items if type is supported
        if (project_type && PROJECT_TEMPLATES[project_type]) {
            const template = PROJECT_TEMPLATES[project_type];
            for (const secData of template) {
                const [secResult] = await pool.query(`
                    INSERT INTO project_sections (project_id, name) VALUES (?, ?)
                `, [newProjectId, secData.section]);
                const sectionId = secResult.insertId;
                
                for (const itemName of secData.items) {
                    await pool.query(`
                        INSERT INTO project_items (section_id, brand, description, type, model, qty, unit, cost_material, cost_labour, price_material, price_labour, discount, is_free_issue, prod_group)
                        VALUES (?, '', ?, '', '', 1.00, 'Set', 0.00, 0.00, 0.00, 0.00, 0.00, 0, ?)
                    `, [sectionId, itemName, itemName]);
                }
            }
        }

        const [newProj] = await pool.query('SELECT id, name, client_name, description, created_by, status, created_at, updated_at, drawing_name, spec_name, project_type, external_name FROM projects WHERE id = ?', [newProjectId]);
        res.status(201).json(newProj[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to create project' });
    }
});

// Update project
app.put('/api/projects/:id', authenticateToken, upload.none(), async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    const hasAccess = await checkProjectWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to modify this project' });
    }

    try {
        const { name, client_name, description, project_type, external_name } = req.body;

        let query = 'UPDATE projects SET name = ?, client_name = ?, description = ?, project_type = ?, external_name = ?';
        let params = [name, client_name, description, project_type || null, external_name || null];
        
        query += ' WHERE id = ?';
        params.push(id);
        
        await pool.query(query, params);
        const [updated] = await pool.query('SELECT id, name, client_name, description, created_by, status, created_at, updated_at, drawing_name, spec_name, project_type, external_name FROM projects WHERE id = ?', [id]);
        res.json(updated[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update project' });
    }
});

// Delete project (soft delete)
app.delete('/api/projects/:id', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    const confirmPassword = req.headers['x-confirm-password'];

    if (!confirmPassword) {
        return res.status(400).json({ error: 'Confirmation password is required' });
    }

    try {
        // Verify password using bcrypt
        const [users] = await pool.query('SELECT password FROM users WHERE id = ?', [userId]);
        if (users.length === 0) {
            return res.status(401).json({ error: 'Incorrect password' });
        }
        const validPwd = await bcrypt.compare(confirmPassword, users[0].password);
        if (!validPwd) {
            return res.status(401).json({ error: 'Incorrect password' });
        }

        // Check if user is the creator or admin
        const [proj] = await pool.query('SELECT created_by, name FROM projects WHERE id = ?', [id]);
        if (proj.length === 0) {
            return res.status(404).json({ error: 'Project not found' });
        }
        
        const isCreator = proj[0].created_by === parseInt(userId);
        const isAdmin = role === 'admin';
        if (!isCreator && !isAdmin) {
            return res.status(403).json({ error: 'Only the project creator or administrator can delete this project' });
        }

        // Mark flag in DB
        await pool.query('UPDATE projects SET is_deleted = 1 WHERE id = ?', [id]);
        
        // Log activity
        await pool.query('INSERT INTO activity_logs (user_id, username, action, details) SELECT id, username, ?, ? FROM users WHERE id = ?', 
            ['PROJECT_DELETE', `Project '${proj[0].name}' soft-deleted`, userId]);

        res.json({ success: true, message: 'Project marked as deleted' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete project' });
    }
});

// Restore project
app.post('/api/projects/:id/restore', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    try {
        // Check if user is the creator or admin
        const [proj] = await pool.query('SELECT created_by, name FROM projects WHERE id = ?', [id]);
        if (proj.length === 0) {
            return res.status(404).json({ error: 'Project not found' });
        }

        const isCreator = proj[0].created_by === parseInt(userId);
        const isAdmin = role === 'admin';
        if (!isCreator && !isAdmin) {
            return res.status(403).json({ error: 'Only the project creator or administrator can restore this project' });
        }

        // Unmark flag in DB
        await pool.query('UPDATE projects SET is_deleted = 0 WHERE id = ?', [id]);

        // Log activity
        await pool.query('INSERT INTO activity_logs (user_id, username, action, details) SELECT id, username, ?, ? FROM users WHERE id = ?', 
            ['PROJECT_RESTORE', `Project '${proj[0].name}' restored`, userId]);

        res.json({ success: true, message: 'Project restored successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to restore project' });
    }
});

// Clone project
app.post('/api/projects/:id/clone', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    try {
        // 1. Fetch source project metadata
        const [projects] = await pool.query('SELECT * FROM projects WHERE id = ?', [id]);
        if (projects.length === 0) {
            return res.status(404).json({ error: 'Source project not found' });
        }
        const sourceProj = projects[0];

        // 2. Insert new cloned project
        const clonedName = `[Clone] ${sourceProj.name}`;
        
        const clonedDrawingPath = copyDiskFile('projects/drawings', sourceProj.drawing_path);
        const clonedSpecPath = copyDiskFile('projects/specs', sourceProj.spec_path);

        const [result] = await pool.query(`
            INSERT INTO projects (name, client_name, description, created_by, status, drawing_path, drawing_name, spec_path, spec_name, project_type)
            VALUES (?, ?, ?, ?, 'รอดำเนินการ', ?, ?, ?, ?, ?)
        `, [
            clonedName,
            sourceProj.client_name,
            sourceProj.description,
            userId || sourceProj.created_by,
            clonedDrawingPath,
            sourceProj.drawing_name,
            clonedSpecPath,
            sourceProj.spec_name,
            sourceProj.project_type
        ]);

        const newProjectId = result.insertId;

        // 3. Fetch source sections
        const [sections] = await pool.query('SELECT * FROM project_sections WHERE project_id = ? AND is_deleted = 0 ORDER BY order_index ASC, id ASC', [id]);

        for (let sec of sections) {
            // Insert cloned section
            const clonedSectionSpecPath = copyDiskFile('sections/specs', sec.spec_path);
            const [secResult] = await pool.query(`
                INSERT INTO project_sections (project_id, name, spec_path, spec_name, order_index, profit_percent)
                VALUES (?, ?, ?, ?, ?, ?)
            `, [newProjectId, sec.name, clonedSectionSpecPath, sec.spec_name || null, sec.order_index || 0, sec.profit_percent || 0]);
            
            const newSectionId = secResult.insertId;

            // Fetch items of source section
            const [items] = await pool.query('SELECT * FROM project_items WHERE section_id = ? AND is_deleted = 0 ORDER BY order_index ASC, id ASC', [sec.id]);
            
            for (let item of items) {
                // Insert cloned item
                await pool.query(`
                    INSERT INTO project_items (
                        section_id, product_id, brand, description, type, model, sm_no, qty, unit, 
                        cost_material, cost_labour, price_material, price_labour, discount, is_free_issue, 
                        prod_group, order_index, production_qty, production_cost_material, 
                        production_cost_labour, production_price_material, production_price_labour
                    )
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                `, [
                    newSectionId,
                    item.product_id || null,
                    item.brand,
                    item.description,
                    item.type,
                    item.model || null,
                    item.sm_no || null,
                    item.qty,
                    item.unit,
                    item.cost_material,
                    item.cost_labour,
                    item.price_material,
                    item.price_labour,
                    item.discount,
                    item.is_free_issue,
                    item.prod_group,
                    item.order_index || 0,
                    item.production_qty,
                    item.production_cost_material,
                    item.production_cost_labour,
                    item.production_price_material,
                    item.production_price_labour
                ]);
            }
        }

        // 4. Create baseline revision REV 1 for the cloned project
        await createProjectRevision(newProjectId, userId, 'โคลนโครงการ (Clone baseline revision)');

        // 5. Fetch and return the new cloned project summary details
        const [summaryRows] = await pool.query(`
            SELECT p.id, p.name, p.client_name, p.description, p.created_by, p.status, p.created_at, p.updated_at, p.drawing_name, p.spec_name,
                   COALESCE((SELECT MAX(revision_number) FROM project_revisions WHERE project_id = p.id), 0) as current_revision
            FROM projects p WHERE p.id = ?
        `, [newProjectId]);

        // Get username for activity log
        const [users] = await pool.query('SELECT username FROM users WHERE id = ?', [userId]);
        const username = users.length > 0 ? users[0].username : 'Unknown';
        await logActivity(userId, username, 'PROJECT_CLONE', `Project ID ${id} cloned as Project ID ${newProjectId}`);

        res.json(summaryRows[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to clone project' });
    }
});

// ==========================================
// 3. PROJECT SECTIONS ENDPOINTS
// ==========================================

// Get sections for a project
app.get('/api/projects/:id/sections', authenticateToken, async (req, res) => {
    try {
        const { id } = req.params;
        const role = req.user.role;
        const userId = req.user.id;

        if (role !== 'admin' && role !== 'superadmin' && role !== 'excusive') {
            const hasAccess = await checkProjectWriteAccess(id, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have access to this project' });
            }
        }

        const [rows] = await pool.query('SELECT id, project_id, name, spec_name, (spec_path IS NOT NULL) AS has_spec, is_deleted, profit_percent, created_at FROM project_sections WHERE project_id = ? AND is_deleted = 0 ORDER BY order_index ASC, id ASC', [id]);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve sections' });
    }
});

// Reorder sections in a project
app.put('/api/projects/:id/reorder-sections', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const { sectionIds } = req.body;
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        const hasAccess = await checkProjectWriteAccess(id, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to modify this project' });
        }
        
        for (let i = 0; i < sectionIds.length; i++) {
            await pool.query('UPDATE project_sections SET order_index = ? WHERE id = ? AND project_id = ?', [i, sectionIds[i], id]);
        }
        
        await logActivity(userId, null, 'SECTION_REORDER', `Reordered systems in project ID ${id}`);
        res.json({ message: 'Systems reordered successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to reorder systems' });
    }
});

// Create section for a project
app.post('/api/projects/:id/sections', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    const hasAccess = await checkProjectWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to add sections to this project' });
    }

    try {
        const { name, auto_populate } = req.body;
        const [maxRow] = await pool.query('SELECT COALESCE(MAX(order_index), 0) AS max_order FROM project_sections WHERE project_id = ? AND is_deleted = 0', [id]);
        const orderIndex = maxRow[0].max_order + 1;

        const [result] = await pool.query(`
            INSERT INTO project_sections (project_id, name, order_index) VALUES (?, ?, ?)
        `, [id, name, orderIndex]);

        if (auto_populate && name) {
            let itemsToInsert = [];
            const [dbTemplates] = await pool.query('SELECT system_name, items FROM system_templates');
            const upperName = name.toUpperCase();

            // Prefer an exact match; otherwise fall back to the most specific
            // (longest) partial match so a generic template like "UTILITY" doesn't
            // shadow a more specific one like "Utility_EGAT".
            let matchedRow = dbTemplates.find(tRow => tRow.system_name.toUpperCase() === upperName);
            if (!matchedRow) {
                const partialMatches = dbTemplates
                    .filter(tRow => upperName.includes(tRow.system_name.toUpperCase()))
                    .sort((a, b) => b.system_name.length - a.system_name.length);
                matchedRow = partialMatches[0];
            }

            if (matchedRow) {
                try {
                    itemsToInsert = JSON.parse(matchedRow.items || '[]');
                } catch (e) {
                    console.error('Failed to parse database template items:', e);
                }
            }

            if (itemsToInsert.length > 0) {
                const sectionId = result.insertId;
                for (const item of itemsToInsert) {
                    let desc = '';
                    let brand = '';
                    let model = '';
                    let type = '';
                    let qty = 1.00;
                    let unit = 'Set';
                    let prodGroup = '';
                    
                    if (typeof item === 'string') {
                        desc = item;
                        prodGroup = item;
                    } else if (item && typeof item === 'object') {
                        desc = item.description || '';
                        brand = item.brand || '';
                        model = item.model || '';
                        type = item.type || '';
                        qty = parseFloat(item.qty) || 1.00;
                        unit = item.unit || 'Set';
                        prodGroup = item.prod_group || desc;
                    }

                    // Auto-fill prices and details from products catalog if description match found
                    let costMat = 0.00;
                    let costLab = 0.00;
                    let priceMat = 0.00;
                    let priceLab = 0.00;
                    let discount = 0.00;
                    let productId = null;

                    let queryStr = 'SELECT id, brand, model, type, unit, lb_cost, mt_cost, lb_qou, mt_qou, discount, prod_group FROM products WHERE description = ?';
                    let queryParams = [desc];
                    if (brand) {
                        queryStr += ' AND brand = ?';
                        queryParams.push(brand);
                    }
                    if (model) {
                        queryStr += ' AND model = ?';
                        queryParams.push(model);
                    }
                    const [matchedProd] = await pool.query(queryStr + ' LIMIT 1', queryParams);
                    if (matchedProd.length > 0) {
                        const p = matchedProd[0];
                        productId = p.id;
                        brand = brand || p.brand || '';
                        model = model || p.model || '';
                        type = type || p.type || '';
                        unit = unit || p.unit || 'Set';
                        costMat = p.mt_cost || 0.00;
                        costLab = p.lb_cost || 0.00;
                        priceMat = p.mt_qou || 0.00;
                        priceLab = p.lb_qou || 0.00;
                        discount = p.discount || 0.00;
                        prodGroup = prodGroup || p.prod_group || desc;
                    }
                    
                    await pool.query(`
                        INSERT INTO project_items (section_id, product_id, brand, description, type, model, qty, unit, cost_material, cost_labour, price_material, price_labour, discount, is_free_issue, prod_group)
                        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)
                    `, [sectionId, productId, brand, desc, type, model, qty, unit, costMat, costLab, priceMat, priceLab, discount, prodGroup]);
                }
            }
        }
        
        await logActivity(userId, null, 'SECTION_CREATE', `Created section "${name}" in Project ID ${id}`);
        
        const [newSection] = await pool.query('SELECT * FROM project_sections WHERE id = ?', [result.insertId]);
        res.status(201).json(newSection[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to create section' });
    }
});

// Rename/edit section
app.put('/api/sections/:id', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const { name } = req.body;
    const userId = req.user.id;
    const role = req.user.role;

    if (!name || name.trim() === '') {
        return res.status(400).json({ error: 'Section name is required' });
    }

    const hasAccess = await checkSectionWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to modify this section' });
    }

    try {
        const [oldSec] = await pool.query('SELECT project_id, name FROM project_sections WHERE id = ?', [id]);
        if (oldSec.length === 0) {
            return res.status(404).json({ error: 'Section not found' });
        }
        const projectId = oldSec[0].project_id;
        const oldName = oldSec[0].name;

        await pool.query('UPDATE project_sections SET name = ? WHERE id = ?', [name, id]);
        await logActivity(userId, null, 'SECTION_UPDATE', `Renamed section "${oldName}" to "${name}" in Project ID ${projectId}`);
        res.json({ success: true, message: 'Section renamed successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to rename section' });
    }
});

// Update section profit percentage
app.put('/api/sections/:id/profit', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const { profit_percent } = req.body;
    const userId = req.user.id;
    const role = req.user.role;

    if (profit_percent === undefined || isNaN(parseFloat(profit_percent))) {
        return res.status(400).json({ error: 'Invalid profit percentage' });
    }

    const hasAccess = await checkSectionWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to modify this section' });
    }

    try {
        const [oldSec] = await pool.query('SELECT project_id, name, profit_percent FROM project_sections WHERE id = ?', [id]);
        if (oldSec.length === 0) {
            return res.status(404).json({ error: 'Section not found' });
        }
        const projectId = oldSec[0].project_id;
        const sectionName = oldSec[0].name;
        const oldProfit = oldSec[0].profit_percent || 0;
        const newProfit = parseFloat(profit_percent);

        // Update the section profit percent
        await pool.query('UPDATE project_sections SET profit_percent = ? WHERE id = ?', [newProfit, id]);

        // Fetch all active items in this section to compute totals for proportional adjustment
        const [items] = await pool.query(
            'SELECT id, qty, cost_material, cost_labour, price_material, price_labour, discount, is_free_issue FROM project_items WHERE section_id = ? AND is_deleted = 0',
            [id]
        );

        let cost_total = 0;
        let price_total = 0;
        items.forEach(item => {
            const qty = parseFloat(item.qty) || 0;
            const isFree = item.is_free_issue === 1;
            const costMat = isFree ? 0 : (parseFloat(item.cost_material) || 0) * qty;
            const costLab = (parseFloat(item.cost_labour) || 0) * qty;
            
            const factor = 1 - (parseFloat(item.discount) || 0) / 100;
            const priceMat = isFree ? 0 : (parseFloat(item.price_material) || 0) * qty * factor;
            const priceLab = (parseFloat(item.price_labour) || 0) * qty * factor;

            cost_total += (costMat + costLab);
            price_total += (priceMat + priceLab);
        });

        // Determine proportional factor or fallback if total cost/price is 0
        if (cost_total > 0 && price_total > 0) {
            const new_price_total = cost_total * (1 + newProfit / 100);
            const ratio = new_price_total / price_total;
            for (const item of items) {
                const newPriceMat = (parseFloat(item.price_material) || 0) * ratio;
                const newPriceLab = (parseFloat(item.price_labour) || 0) * ratio;
                await pool.query(
                    'UPDATE project_items SET price_material = ?, price_labour = ?, is_modified = 1 WHERE id = ?',
                    [newPriceMat, newPriceLab, item.id]
                );
            }
        } else {
            // Fallback: standard cost-based pricing if current price_total or cost_total is 0
            for (const item of items) {
                const newPriceMat = (parseFloat(item.cost_material) || 0) * (1 + newProfit / 100);
                const newPriceLab = (parseFloat(item.cost_labour) || 0) * (1 + newProfit / 100);
                await pool.query(
                    'UPDATE project_items SET price_material = ?, price_labour = ?, is_modified = 1 WHERE id = ?',
                    [newPriceMat, newPriceLab, item.id]
                );
            }
        }

        // Log revision and activity
        await createProjectRevision(projectId, userId, `Adjusted profit margin for system "${sectionName}" from ${oldProfit}% to ${newProfit}%`);
        await logActivity(userId, null, 'SECTION_PROFIT_UPDATE', `Adjusted profit margin for system "${sectionName}" from ${oldProfit}% to ${newProfit}% in Project ID ${projectId}`);

        res.json({ success: true, message: 'System profit margin adjusted successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to adjust system profit margin' });
    }
});

// Reset all items price/cost in a section/system from catalog
app.put('/api/sections/:id/reset-prices', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    const hasAccess = await checkSectionWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to modify this system' });
    }

    try {
        const [sec] = await pool.query('SELECT project_id, name FROM project_sections WHERE id = ?', [id]);
        if (sec.length === 0) {
            return res.status(404).json({ error: 'System not found' });
        }
        const projectId = sec[0].project_id;
        const sectionName = sec[0].name;

        // Reset costs and prices in project_items linked to products table
        await pool.query(`
            UPDATE project_items 
            SET cost_material = (SELECT mt_cost FROM products WHERE id = project_items.product_id),
                cost_labour = (SELECT lb_cost FROM products WHERE id = project_items.product_id),
                price_material = (SELECT mt_qou FROM products WHERE id = project_items.product_id),
                price_labour = (SELECT lb_qou FROM products WHERE id = project_items.product_id),
                is_modified = 1
            WHERE section_id = ? AND is_deleted = 0 AND product_id IS NOT NULL
        `, [id]);

        // Log revision and activity
        await createProjectRevision(projectId, userId, `Reset all items pricing in system "${sectionName}" to catalog defaults`);
        await logActivity(userId, null, 'SECTION_RESET_PRICES', `Reset all items pricing in system "${sectionName}" to catalog defaults in Project ID ${projectId}`);

        res.json({ success: true, message: 'System pricing reset from catalog defaults successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to reset system pricing from catalog defaults' });
    }
});

// Delete section
app.delete('/api/sections/:id', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    const hasAccess = await checkSectionWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to delete this section' });
    }

    try {
        const [sec] = await pool.query('SELECT project_id, name FROM project_sections WHERE id = ?', [id]);
        if (sec.length > 0) {
            const projectId = sec[0].project_id;
            const sectionName = sec[0].name;
            
            await pool.query('UPDATE project_sections SET is_deleted = 1 WHERE id = ?', [id]);
            await pool.query('UPDATE project_items SET is_deleted = 1 WHERE section_id = ?', [id]);
            
            await logActivity(userId, null, 'SECTION_DELETE', `Deleted section "${sectionName}" in Project ID ${projectId}`);
        }
        res.json({ success: true, message: 'Section deleted' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete section' });
    }
});

// Upload spec PDF directly to section
app.post('/api/sections/:id/spec', authenticateToken, upload.single('spec'), async (req, res) => {
    const role = req.user.role;
    const userId = req.user.id;
    const { id } = req.params;

    const hasAccess = await checkSectionWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to modify this section' });
    }

    try {
        if (!req.file) {
            return res.status(400).json({ error: 'No PDF file uploaded' });
        }

        const pdfName = Buffer.from(req.file.originalname, 'latin1').toString('utf8');
        const [existing] = await pool.query('SELECT spec_path FROM project_sections WHERE id = ?', [id]);
        const specPath = saveBufferToDisk('sections/specs', req.file.buffer, pdfName);

        await pool.query('UPDATE project_sections SET spec_path = ?, spec_name = ? WHERE id = ?', [specPath, pdfName, id]);
        if (existing.length > 0) deleteDiskFile(existing[0].spec_path);

        // Log the activity
        const [sec] = await pool.query('SELECT project_id, name FROM project_sections WHERE id = ?', [id]);
        if (sec.length > 0) {
            const projectId = sec[0].project_id;
            const secName = sec[0].name;
            await logActivity(userId, null, 'SECTION_SPEC_UPLOAD', `Uploaded spec PDF "${pdfName}" for section "${secName}" in Project ID ${projectId}`);
        }

        res.json({ success: true, spec_name: pdfName });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to upload section spec PDF' });
    }
});

// Download/View section spec PDF
app.get('/api/sections/:id/spec', authenticateToken, async (req, res) => {
    try {
        const { id } = req.params;
        const [rows] = await pool.query('SELECT spec_path, spec_name FROM project_sections WHERE id = ?', [id]);

        if (rows.length === 0 || !rows[0].spec_path) {
            return res.status(404).send('Spec PDF not found for this section');
        }

        sendDiskFile(res, rows[0].spec_path, rows[0].spec_name, 'application/pdf');
    } catch (err) {
        console.error(err);
        res.status(500).send('Error retrieving spec PDF');
    }
});

// Delete section spec PDF
app.delete('/api/sections/:id/spec', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const userId = req.user.id;
    const { id } = req.params;

    const hasAccess = await checkSectionWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to modify this section' });
    }

    try {
        const [existing] = await pool.query('SELECT spec_path FROM project_sections WHERE id = ?', [id]);
        await pool.query('UPDATE project_sections SET spec_path = NULL, spec_name = NULL WHERE id = ?', [id]);
        if (existing.length > 0) deleteDiskFile(existing[0].spec_path);

        // Log the activity
        const [sec] = await pool.query('SELECT project_id, name FROM project_sections WHERE id = ?', [id]);
        if (sec.length > 0) {
            const projectId = sec[0].project_id;
            const secName = sec[0].name;
            await logActivity(userId, null, 'SECTION_SPEC_DELETE', `Deleted spec PDF for section "${secName}" in Project ID ${projectId}`);
        }

        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete section spec PDF' });
    }
});

// ==========================================
// 4. PROJECT ITEMS (TAKEOFF) ENDPOINTS
// ==========================================

// Get items in a section (enriches with product flags showing image/pdf status)
app.get('/api/sections/:id/items', authenticateToken, async (req, res) => {
    try {
        const { id } = req.params;
        const role = req.user.role;
        const userId = req.user.id;

        // Fetch section to get projectId
        const [sec] = await pool.query('SELECT project_id FROM project_sections WHERE id = ? AND is_deleted = 0', [id]);
        if (sec.length === 0) {
            return res.status(404).json({ error: 'Section not found' });
        }
        const projectId = sec[0].project_id;

        if (role !== 'admin' && role !== 'superadmin' && role !== 'excusive') {
            const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have access to this project' });
            }
        }

        const [rows] = await pool.query(`
            SELECT pi.*, 
                   (p.image_path IS NOT NULL) AS has_image,
                   (p.pdf_path IS NOT NULL) AS has_pdf,
                   p.supplier_price
            FROM project_items pi
            LEFT JOIN products p ON pi.product_id = p.id
            WHERE pi.section_id = ? AND pi.is_deleted = 0
            ORDER BY pi.order_index ASC, pi.id ASC
        `, [id]);
        
        if (role !== 'superadmin' && role !== 'excusive' && role !== 'admin') {
            rows.forEach(item => {
                item.cost_material = 0;
                item.cost_labour = 0;
                item.production_cost_material = 0;
                item.production_cost_labour = 0;
            });
        }

        const allowedSupplierPriceRoles = ['superadmin', 'excusive', 'admin', 'estimate', 'procurment', 'procurement_manager'];
        if (!allowedSupplierPriceRoles.includes(role)) {
            rows.forEach(item => {
                item.supplier_price = 0;
            });
        }

        if (role === 'contractor') {
            const [contractorRows] = await pool.query('SELECT id FROM contractors WHERE user_id = ? AND is_deleted = 0', [userId]);
            let contractorId = contractorRows.length > 0 ? contractorRows[0].id : null;

            const [secRows] = await pool.query('SELECT project_id FROM project_sections WHERE id = ?', [id]);
            let projectId = secRows.length > 0 ? secRows[0].project_id : null;

            let latestRfqId = null;
            const rfqIdOverride = req.query.rfq_id ? parseInt(req.query.rfq_id) : null;
            if (projectId && contractorId) {
                if (rfqIdOverride) {
                    const [rfqRows] = await pool.query(
                        'SELECT id FROM contractor_rfqs WHERE id = ? AND contractor_id = ? AND is_deleted = 0',
                        [rfqIdOverride, contractorId]
                    );
                    if (rfqRows.length > 0) {
                        latestRfqId = rfqRows[0].id;
                    }
                }
                
                if (!latestRfqId) {
                    const [rfqRows] = await pool.query(
                        'SELECT id FROM contractor_rfqs WHERE project_id = ? AND contractor_id = ? AND is_deleted = 0 ORDER BY created_at DESC LIMIT 1',
                        [projectId, contractorId]
                    );
                    latestRfqId = rfqRows.length > 0 ? rfqRows[0].id : null;
                }
            }

            let bidMap = new Map();
            if (latestRfqId) {
                const [rfqItems] = await pool.query(
                    'SELECT project_item_id, material_rate, labour_rate FROM contractor_rfq_items WHERE rfq_id = ?',
                    [latestRfqId]
                );

                // Synchronize with active project items that might be missing from this RFQ
                if (projectId) {
                    const [projItems] = await pool.query(`
                        SELECT pi.id, ps.name AS section_name, pi.description, pi.brand, pi.qty, pi.unit, pi.order_index
                        FROM project_items pi
                        JOIN project_sections ps ON pi.section_id = ps.id
                        WHERE ps.project_id = ? AND pi.is_deleted = 0 AND ps.is_deleted = 0
                    `, [projectId]);

                    const existingProjItemIds = new Set(rfqItems.map(ri => ri.project_item_id));
                    for (const pi of projItems) {
                        if (!existingProjItemIds.has(pi.id)) {
                            await pool.query(`
                                INSERT INTO contractor_rfq_items (rfq_id, project_item_id, section_name, description, brand, qty, unit, order_index)
                                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                            `, [latestRfqId, pi.id, pi.section_name, pi.description, pi.brand, pi.qty, pi.unit, pi.order_index]);
                            rfqItems.push({ project_item_id: pi.id, material_rate: 0, labour_rate: 0 });
                        }
                    }
                }

                rfqItems.forEach(ri => {
                    bidMap.set(ri.project_item_id, ri);
                });
            }

            rows.forEach(item => {
                const bid = bidMap.get(item.id);
                if (bid) {
                    item.price_material = bid.material_rate;
                    item.price_labour = bid.labour_rate;
                } else {
                    item.price_material = 0;
                    item.price_labour = 0;
                }
                item.discount = 0;
            });
        }
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve items' });
    }
});

// Reorder items in a section
app.put('/api/sections/:id/reorder-items', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const { itemIds } = req.body;
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        const [sections] = await pool.query('SELECT project_id FROM project_sections WHERE id = ?', [id]);
        if (sections.length === 0) {
            return res.status(404).json({ error: 'Section not found' });
        }
        const projectId = sections[0].project_id;
        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to modify this project' });
        }
        
        for (let i = 0; i < itemIds.length; i++) {
            await pool.query('UPDATE project_items SET order_index = ? WHERE id = ? AND section_id = ?', [i, itemIds[i], id]);
        }
        
        await logActivity(userId, null, 'ITEM_REORDER', `Reordered items in section ID ${id}`);
        res.json({ message: 'Items reordered successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to reorder items' });
    }
});

// Add item to a section
app.post('/api/sections/:id/items', authenticateToken, async (req, res) => {
    const { id } = req.params; // section_id
    const userId = req.user.id;
    const role = req.user.role;

    const hasAccess = await checkSectionWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to add items to this section' });
    }

    try {
        const { product_id, brand, description, type, model, sm_no, qty, unit, cost_material, cost_labour, price_material, price_labour, discount, is_free_issue, prod_group } = req.body;
        
        const [maxRow] = await pool.query('SELECT COALESCE(MAX(order_index), 0) AS max_order FROM project_items WHERE section_id = ? AND is_deleted = 0', [id]);
        const orderIndex = maxRow[0].max_order + 1;

        const [result] = await pool.query(`
            INSERT INTO project_items (section_id, product_id, brand, description, type, model, sm_no, qty, unit, cost_material, cost_labour, price_material, price_labour, discount, is_free_issue, prod_group, order_index)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        `, [id, product_id || null, brand, description, type, model || null, sm_no || null, qty || 1, unit, cost_material || 0, cost_labour || 0, price_material || 0, price_labour || 0, discount || 0, is_free_issue || 0, prod_group || null, orderIndex]);
        
        const [sec] = await pool.query('SELECT project_id, name FROM project_sections WHERE id = ?', [id]);
        if (sec.length > 0) {
            const projectId = sec[0].project_id;
            const secName = sec[0].name;
            await logActivity(userId, null, 'ITEM_CREATE', `Added item "${description}" to section "${secName}" in Project ID ${projectId}`);
        }

        const [newItem] = await pool.query(`
            SELECT pi.*, 
                   (p.image_path IS NOT NULL) AS has_image,
                   (p.pdf_path IS NOT NULL) AS has_pdf
            FROM project_items pi
            LEFT JOIN products p ON pi.product_id = p.id
            WHERE pi.id = ?
        `, [result.insertId]);
        res.status(201).json(newItem[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to add item to section' });
    }
});

// Update item
app.put('/api/items/:id', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    const hasAccess = await checkItemWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to modify this item' });
    }

    try {
        const { qty, cost_material, cost_labour, price_material, price_labour, discount, brand, description, type, model, sm_no, unit, is_free_issue, prod_group,
                production_qty, production_cost_material, production_cost_labour, production_price_material, production_price_labour } = req.body;
        
        const [itemDetails] = await pool.query(`
            SELECT pi.description, ps.project_id, ps.name as section_name FROM project_items pi
            JOIN project_sections ps ON pi.section_id = ps.id
            WHERE pi.id = ?
        `, [id]);
        
        if (role === 'contractor') {
            // Find contractor ID
            const [contractorRows] = await pool.query('SELECT id FROM contractors WHERE user_id = ? AND is_deleted = 0', [userId]);
            if (contractorRows.length === 0) {
                return res.status(403).json({ error: 'Contractor profile not found' });
            }
            const contractorId = contractorRows[0].id;

            // Find project_id of the item
            const projectId = itemDetails.length > 0 ? itemDetails[0].project_id : null;
            if (!projectId) {
                return res.status(404).json({ error: 'Item project not found' });
            }

            // Find the active RFQ for this project and contractor
            const [rfqRows] = await pool.query(`
                SELECT id FROM contractor_rfqs 
                WHERE project_id = ? AND contractor_id = ? AND is_deleted = 0
                ORDER BY created_at DESC LIMIT 1
            `, [projectId, contractorId]);
            if (rfqRows.length === 0) {
                return res.status(400).json({ error: 'No active RFQ found for this project' });
            }
            const rfqId = rfqRows[0].id;

            // Update contractor_rfq_items rate
            await pool.query(`
                UPDATE contractor_rfq_items 
                SET material_rate = ?, labour_rate = ? 
                WHERE project_item_id = ? AND rfq_id = ?
            `, [parseFloat(price_material) || 0, parseFloat(price_labour) || 0, id, rfqId]);
            
            // Recompute RFQ total
            await recomputeRfqTotal(rfqId);

            // Log activity
            if (itemDetails.length > 0) {
                const desc = itemDetails[0].description;
                const secName = itemDetails[0].section_name;
                await logActivity(userId, null, 'RFQ_ITEM_UPDATE', `Contractor updated bid price for item "${desc}" in section "${secName}" of Project ID ${projectId}`);
            }

            // Return updated item details (with overridden prices for client update compatibility)
            const [updated] = await pool.query(`
                SELECT pi.*, 
                       (p.image_path IS NOT NULL) AS has_image,
                       (p.pdf_path IS NOT NULL) AS has_pdf,
                       p.supplier_price
                FROM project_items pi
                LEFT JOIN products p ON pi.product_id = p.id
                WHERE pi.id = ? AND pi.is_deleted = 0
            `, [id]);

            if (updated.length > 0) {
                updated[0].price_material = parseFloat(price_material) || 0;
                updated[0].price_labour = parseFloat(price_labour) || 0;
            }
            return res.json(updated[0]);
        }

        let updateFields = [];
        let queryParams = [];

        if (role === 'supervisor') {
            if (qty !== undefined) { updateFields.push('qty = ?'); queryParams.push(qty); }
            if (discount !== undefined) { updateFields.push('discount = ?'); queryParams.push(discount); }
            if (is_free_issue !== undefined) { updateFields.push('is_free_issue = ?'); queryParams.push(is_free_issue || 0); }
            if (production_qty !== undefined) { updateFields.push('production_qty = ?'); queryParams.push(production_qty); }
        } else {
            // Admin/Superadmin/Executive
            if (qty !== undefined) { updateFields.push('qty = ?'); queryParams.push(qty); }
            if (cost_material !== undefined) { updateFields.push('cost_material = ?'); queryParams.push(cost_material); }
            if (cost_labour !== undefined) { updateFields.push('cost_labour = ?'); queryParams.push(cost_labour); }
            if (price_material !== undefined) { updateFields.push('price_material = ?'); queryParams.push(price_material); }
            if (price_labour !== undefined) { updateFields.push('price_labour = ?'); queryParams.push(price_labour); }
            if (discount !== undefined) { updateFields.push('discount = ?'); queryParams.push(discount); }
            if (brand !== undefined) { updateFields.push('brand = ?'); queryParams.push(brand); }
            if (description !== undefined) { updateFields.push('description = ?'); queryParams.push(description); }
            if (type !== undefined) { updateFields.push('type = ?'); queryParams.push(type); }
            if (model !== undefined) { updateFields.push('model = ?'); queryParams.push(model || null); }
            if (sm_no !== undefined) { updateFields.push('sm_no = ?'); queryParams.push(sm_no || null); }
            if (unit !== undefined) { updateFields.push('unit = ?'); queryParams.push(unit); }
            if (is_free_issue !== undefined) { updateFields.push('is_free_issue = ?'); queryParams.push(is_free_issue || 0); }
            if (prod_group !== undefined) { updateFields.push('prod_group = ?'); queryParams.push(prod_group || null); }

            // Production fields
            if (production_qty !== undefined) { updateFields.push('production_qty = ?'); queryParams.push(production_qty); }
            if (production_cost_material !== undefined) { updateFields.push('production_cost_material = ?'); queryParams.push(production_cost_material); }
            if (production_cost_labour !== undefined) { updateFields.push('production_cost_labour = ?'); queryParams.push(production_cost_labour); }
            if (production_price_material !== undefined) { updateFields.push('production_price_material = ?'); queryParams.push(production_price_material); }
            if (production_price_labour !== undefined) { updateFields.push('production_price_labour = ?'); queryParams.push(production_price_labour); }
        }

        if (updateFields.length > 0) {
            updateFields.push('is_modified = 1');
            queryParams.push(id);
            await pool.query(`
                UPDATE project_items 
                SET ${updateFields.join(', ')}
                WHERE id = ?
            `, queryParams);
        }
        
        if (itemDetails.length > 0) {
            const projectId = itemDetails[0].project_id;
            const desc = itemDetails[0].description;
            const secName = itemDetails[0].section_name;
            await logActivity(userId, null, 'ITEM_UPDATE', `Modified item "${desc}" in section "${secName}" of Project ID ${projectId}`);
        }

        const [updated] = await pool.query(`
            SELECT pi.*, 
                   (p.image_path IS NOT NULL) AS has_image,
                   (p.pdf_path IS NOT NULL) AS has_pdf,
                   p.supplier_price
            FROM project_items pi
            LEFT JOIN products p ON pi.product_id = p.id
            WHERE pi.id = ? AND pi.is_deleted = 0
        `, [id]);
        res.json(updated[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update item' });
    }
});

// Reset item price/cost from catalog
app.put('/api/items/:id/reset-price', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    const hasAccess = await checkItemWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to modify this item' });
    }

    try {
        // Find the item details, project ID and product_id
        const [itemRows] = await pool.query(`
            SELECT pi.product_id, pi.description, ps.project_id, ps.name as section_name FROM project_items pi
            JOIN project_sections ps ON pi.section_id = ps.id
            WHERE pi.id = ? AND pi.is_deleted = 0
        `, [id]);

        if (itemRows.length === 0) {
            return res.status(404).json({ error: 'Item not found' });
        }

        const { product_id, description, project_id, section_name } = itemRows[0];
        if (!product_id) {
            return res.status(400).json({ error: 'This item is not linked to a catalog product' });
        }

        // Fetch original catalog prices from products table
        const [prodRows] = await pool.query(`
            SELECT mt_cost, lb_cost, mt_qou, lb_qou FROM products WHERE id = ?
        `, [product_id]);

        if (prodRows.length === 0) {
            return res.status(404).json({ error: 'Catalog product not found' });
        }

        const { mt_cost, lb_cost, mt_qou, lb_qou } = prodRows[0];

        // Reset costs and prices in project_items
        await pool.query(`
            UPDATE project_items 
            SET cost_material = ?, cost_labour = ?, price_material = ?, price_labour = ?, is_modified = 1
            WHERE id = ?
        `, [mt_cost, lb_cost, mt_qou, lb_qou, id]);

        // Log activity
        await logActivity(userId, null, 'ITEM_RESET_PRICE', `Reset pricing to catalog for item "${description}" in section "${section_name}" of Project ID ${project_id}`);

        // Fetch updated item details
        const [updated] = await pool.query(`
            SELECT pi.*, 
                   (p.image_path IS NOT NULL) AS has_image,
                   (p.pdf_path IS NOT NULL) AS has_pdf
            FROM project_items pi
            LEFT JOIN products p ON pi.product_id = p.id
            WHERE pi.id = ? AND pi.is_deleted = 0
        `, [id]);

        res.json(updated[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to reset item pricing from catalog' });
    }
});

// Delete item
app.delete('/api/items/:id', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    const hasAccess = await checkItemWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to delete this item' });
    }

    try {
        const [itemDetails] = await pool.query(`
            SELECT pi.description, ps.project_id, ps.name as section_name FROM project_items pi
            JOIN project_sections ps ON pi.section_id = ps.id
            WHERE pi.id = ?
        `, [id]);
        
        if (itemDetails.length > 0) {
            const projectId = itemDetails[0].project_id;
            const desc = itemDetails[0].description;
            const secName = itemDetails[0].section_name;
            
            await pool.query('UPDATE project_items SET is_deleted = 1 WHERE id = ?', [id]);
            
            await logActivity(userId, null, 'ITEM_DELETE', `Deleted item "${desc}" in section "${secName}" of Project ID ${projectId}`);
        }
        res.json({ success: true, message: 'Item deleted' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete item' });
    }
});

// ==========================================
// CONTRACTOR RFQ / QUOTATION ENDPOINTS
// ==========================================

const RFQ_STATUSES = ['draft', 'sent', 'submitted', 'approved', 'rejected'];

async function recomputeRfqTotal(rfqId) {
    const [rows] = await pool.query('SELECT COALESCE(SUM(qty * (material_rate + labour_rate)), 0) AS total FROM contractor_rfq_items WHERE rfq_id = ? AND is_included = 1', [rfqId]);
    const total = rows[0].total;
    await pool.query('UPDATE contractor_rfqs SET total_amount = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [total, rfqId]);
    return total;
}

// List RFQs for a project
app.get('/api/projects/:id/contractor-rfqs', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        if (!hasGlobalRfqReadAccess(role) && role !== 'contractor') {
            const hasAccess = await checkProjectWriteAccess(id, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have permission to view RFQs for this project' });
            }
        }
        let queryStr = `
            SELECT r.*, c.name AS contractor_name
            FROM contractor_rfqs r
            JOIN contractors c ON r.contractor_id = c.id
            WHERE r.project_id = ? AND r.is_deleted = 0
        `;
        const queryParams = [id];
        if (role === 'contractor') {
            queryStr += ` AND c.user_id = ?`;
            queryParams.push(userId);
        }
        queryStr += ` ORDER BY r.created_at DESC`;

        const [rows] = await pool.query(queryStr, queryParams);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve contractor RFQs' });
    }
});

// Create a new RFQ for a project (snapshots current project items)
app.post('/api/projects/:id/contractor-rfqs', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    const { contractor_id, due_date, notes } = req.body;

    if (!PERMISSIONS['rfq:create'].includes(role)) {
        return res.status(403).json({ error: 'Not authorized to create RFQs' });
    }
    const hasAccess = await checkProjectWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to modify this project' });
    }
    if (!contractor_id) {
        return res.status(400).json({ error: 'contractor_id is required' });
    }

    try {
        const [contractorRows] = await pool.query('SELECT id FROM contractors WHERE id = ? AND is_deleted = 0', [contractor_id]);
        if (contractorRows.length === 0) {
            return res.status(400).json({ error: 'Contractor not found' });
        }

        const [result] = await pool.query(
            'INSERT INTO contractor_rfqs (project_id, contractor_id, rfq_number, due_date, notes, created_by) VALUES (?, ?, ?, ?, ?, ?)',
            [id, contractor_id, '', due_date || null, notes || null, userId]
        );
        const rfqId = result.insertId;
        const rfqNumber = `RFQ-${String(rfqId).padStart(7, '0')}`;
        await pool.query('UPDATE contractor_rfqs SET rfq_number = ? WHERE id = ?', [rfqNumber, rfqId]);

        // Snapshot current project items (with their section name), skip cost/price fields entirely
        const [items] = await pool.query(`
            SELECT pi.id AS project_item_id, ps.name AS section_name, pi.description, pi.brand, pi.qty, pi.unit, pi.order_index
            FROM project_items pi
            JOIN project_sections ps ON pi.section_id = ps.id
            WHERE ps.project_id = ? AND pi.is_deleted = 0 AND ps.is_deleted = 0
            ORDER BY ps.order_index ASC, pi.order_index ASC, pi.id ASC
        `, [id]);

        for (const item of items) {
            await pool.query(`
                INSERT INTO contractor_rfq_items (rfq_id, project_item_id, section_name, description, brand, qty, unit, order_index)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            `, [rfqId, item.project_item_id, item.section_name, item.description, item.brand, item.qty, item.unit, item.order_index]);
        }

        await logActivity(userId, null, 'RFQ_CREATE', `Created RFQ ${rfqNumber} for Project ID ${id}`);

        const [newRfq] = await pool.query('SELECT * FROM contractor_rfqs WHERE id = ?', [rfqId]);
        res.status(201).json(newRfq[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to create RFQ' });
    }
});

// Get RFQ detail including snapshotted items
app.get('/api/contractor-rfqs/:rfqId', authenticateToken, async (req, res) => {
    const { rfqId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        const [rfqRows] = await pool.query(`
            SELECT r.*, c.name AS contractor_name, c.user_id AS contractor_user_id
            FROM contractor_rfqs r
            JOIN contractors c ON r.contractor_id = c.id
            WHERE r.id = ? AND r.is_deleted = 0
        `, [rfqId]);
        if (rfqRows.length === 0) {
            return res.status(404).json({ error: 'RFQ not found' });
        }
        const rfq = rfqRows[0];

        if (role === 'contractor') {
            if (rfq.contractor_user_id !== userId) {
                return res.status(403).json({ error: 'You do not have permission to view this RFQ' });
            }
        } else if (!hasGlobalRfqReadAccess(role)) {
            const hasAccess = await checkProjectWriteAccess(rfq.project_id, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have permission to view this RFQ' });
            }
        }

        const [items] = await pool.query('SELECT * FROM contractor_rfq_items WHERE rfq_id = ? ORDER BY order_index ASC, id ASC', [rfqId]);
        res.json({ ...rfq, items });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve RFQ' });
    }
});

// Update RFQ (status/due date/notes + item rates), recomputes total_amount
app.put('/api/contractor-rfqs/:rfqId', authenticateToken, async (req, res) => {
    const { rfqId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    const { status, due_date, notes, items } = req.body;

    if (!PERMISSIONS['rfq:update'].includes(role)) {
        return res.status(403).json({ error: 'Not authorized to update RFQs' });
    }
    if (status && !RFQ_STATUSES.includes(status)) {
        return res.status(400).json({ error: `Invalid status. Must be one of: ${RFQ_STATUSES.join(', ')}` });
    }

    try {
        const [rfqRows] = await pool.query(`
            SELECT r.project_id, c.user_id AS contractor_user_id 
            FROM contractor_rfqs r
            JOIN contractors c ON r.contractor_id = c.id
            WHERE r.id = ? AND r.is_deleted = 0
        `, [rfqId]);
        if (rfqRows.length === 0) {
            return res.status(404).json({ error: 'RFQ not found' });
        }
        const rfq = rfqRows[0];

        if (role === 'contractor') {
            if (rfq.contractor_user_id !== userId) {
                return res.status(403).json({ error: 'You do not have permission to modify this RFQ' });
            }
        } else {
            const hasAccess = await checkProjectWriteAccess(rfq.project_id, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have permission to modify this RFQ' });
            }
        }

        if (Array.isArray(items)) {
            for (const item of items) {
                await pool.query(
                    'UPDATE contractor_rfq_items SET is_included = ?, material_rate = ?, labour_rate = ? WHERE id = ? AND rfq_id = ?',
                    [item.is_included ? 1 : 0, parseFloat(item.material_rate) || 0, parseFloat(item.labour_rate) || 0, item.id, rfqId]
                );
            }
        }

        await pool.query(
            'UPDATE contractor_rfqs SET status = COALESCE(?, status), due_date = ?, notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
            [status || null, due_date || null, notes || null, rfqId]
        );
        const totalAmount = await recomputeRfqTotal(rfqId);

        await logActivity(userId, null, 'RFQ_UPDATE', `Updated RFQ ID ${rfqId}`);

        const [updated] = await pool.query('SELECT * FROM contractor_rfqs WHERE id = ?', [rfqId]);
        res.json({ ...updated[0], total_amount: totalAmount });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update RFQ' });
    }
});

// Submit RFQ with password verification and auto revision increment
app.post('/api/contractor-rfqs/:rfqId/submit', authenticateToken, async (req, res) => {
    const { rfqId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    const { password, items } = req.body;

    if (!password) {
        return res.status(400).json({ error: 'Password is required' });
    }

    try {
        // 1. Fetch user to verify password
        const [users] = await pool.query('SELECT password FROM users WHERE id = ?', [userId]);
        if (users.length === 0) {
            return res.status(404).json({ error: 'User not found' });
        }
        const validPassword = await bcrypt.compare(password, users[0].password);
        if (!validPassword) {
            return res.status(400).json({ error: 'Invalid password' });
        }

        // 2. Fetch RFQ
        const [rfqRows] = await pool.query(`
            SELECT r.*, c.user_id AS contractor_user_id 
            FROM contractor_rfqs r
            JOIN contractors c ON r.contractor_id = c.id
            WHERE r.id = ? AND r.is_deleted = 0
        `, [rfqId]);
        if (rfqRows.length === 0) {
            return res.status(404).json({ error: 'RFQ not found' });
        }
        const rfq = rfqRows[0];

        // 3. Authorization check
        if (role === 'contractor') {
            if (rfq.contractor_user_id !== userId) {
                return res.status(403).json({ error: 'You do not have permission to submit this RFQ' });
            }
        } else {
            const hasAccess = await checkProjectWriteAccess(rfq.project_id, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have permission to modify this RFQ' });
            }
        }

        // 4. Update items if provided (just in case they submitted newest rates)
        if (Array.isArray(items)) {
            for (const item of items) {
                await pool.query(
                    'UPDATE contractor_rfq_items SET is_included = ?, material_rate = ?, labour_rate = ? WHERE id = ? AND rfq_id = ?',
                    [item.is_included ? 1 : 0, parseFloat(item.material_rate) || 0, parseFloat(item.labour_rate) || 0, item.id, rfqId]
                );
            }
        }

        // 5. Calculate revision increment
        // If current status is already 'submitted' or 'replied', we increment the revision number!
        let newRevision = rfq.revision_number || 0;
        if (rfq.status === 'submitted' || rfq.status === 'replied') {
            newRevision = (rfq.revision_number || 0) + 1;
        }

        // 6. Update RFQ status to 'submitted' and set revision number
        await pool.query(
            `UPDATE contractor_rfqs SET 
                status = 'submitted', 
                revision_number = ?, 
                updated_at = CURRENT_TIMESTAMP 
             WHERE id = ?`,
            [newRevision, rfqId]
        );

        const totalAmount = await recomputeRfqTotal(rfqId);

        await logActivity(userId, null, 'RFQ_SUBMIT', `Submitted RFQ ID ${rfqId} (Rev ${newRevision})`);

        res.json({ success: true, rfq_number: rfq.rfq_number, revision_number: newRevision, total_amount: totalAmount });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to submit RFQ' });
    }
});

// Delete RFQ (soft delete)
app.delete('/api/contractor-rfqs/:rfqId', authenticateToken, async (req, res) => {
    const { rfqId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        const [rfqRows] = await pool.query('SELECT project_id FROM contractor_rfqs WHERE id = ?', [rfqId]);
        if (rfqRows.length === 0) {
            return res.status(404).json({ error: 'RFQ not found' });
        }
        const hasAccess = await checkProjectWriteAccess(rfqRows[0].project_id, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to delete this RFQ' });
        }
        await pool.query('UPDATE contractor_rfqs SET is_deleted = 1 WHERE id = ?', [rfqId]);
        res.json({ success: true, message: 'RFQ deleted' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete RFQ' });
    }
});

// Upload contractor's quote PDF onto an RFQ
app.post('/api/contractor-rfqs/:rfqId/quote-file', authenticateToken, upload.single('quote'), async (req, res) => {
    const { rfqId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        const [rfqRows] = await pool.query(`
            SELECT r.project_id, r.quote_file_path, c.user_id AS contractor_user_id
            FROM contractor_rfqs r
            JOIN contractors c ON r.contractor_id = c.id
            WHERE r.id = ?
        `, [rfqId]);
        if (rfqRows.length === 0) {
            return res.status(404).json({ error: 'RFQ not found' });
        }
        const rfq = rfqRows[0];
        if (role === 'contractor') {
            if (rfq.contractor_user_id !== userId) {
                return res.status(403).json({ error: 'You do not have permission to modify this RFQ' });
            }
        } else {
            const hasAccess = await checkProjectWriteAccess(rfq.project_id, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have permission to modify this RFQ' });
            }
        }
        if (!req.file) {
            return res.status(400).json({ error: 'No quote file uploaded' });
        }

        const fileName = Buffer.from(req.file.originalname, 'latin1').toString('utf8');
        const filePath = saveBufferToDisk('contractor-rfqs/quotes', req.file.buffer, fileName);
        await pool.query('UPDATE contractor_rfqs SET quote_file_path = ?, quote_file_name = ? WHERE id = ?', [filePath, fileName, rfqId]);
        if (rfq.quote_file_path) deleteDiskFile(rfq.quote_file_path);

        res.json({ success: true, quote_file_name: fileName });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to upload quote file' });
    }
});

// Stream the contractor's quote PDF
app.get('/api/contractor-rfqs/:rfqId/quote-file', authenticateToken, async (req, res) => {
    const { rfqId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        const [rows] = await pool.query(`
            SELECT r.project_id, r.quote_file_path, r.quote_file_name, c.user_id AS contractor_user_id
            FROM contractor_rfqs r
            JOIN contractors c ON r.contractor_id = c.id
            WHERE r.id = ?
        `, [rfqId]);
        if (rows.length === 0 || !rows[0].quote_file_path) {
            return res.status(404).send('Quote file not found');
        }
        const rfq = rows[0];
        if (role === 'contractor') {
            if (rfq.contractor_user_id !== userId) {
                return res.status(403).send('Not authorized to view this quote file');
            }
        } else if (!hasGlobalRfqReadAccess(role)) {
            const hasAccess = await checkProjectWriteAccess(rfq.project_id, userId, role);
            if (!hasAccess) {
                return res.status(403).send('Not authorized to view this quote file');
            }
        }
        sendDiskFile(res, rfq.quote_file_path, rfq.quote_file_name || 'quote.pdf', 'application/pdf');
    } catch (err) {
        console.error(err);
        res.status(500).send('Error streaming quote file');
    }
});

// Delete the contractor's quote PDF from an RFQ
app.delete('/api/contractor-rfqs/:rfqId/quote-file', authenticateToken, async (req, res) => {
    const { rfqId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        const [rows] = await pool.query(`
            SELECT r.project_id, r.quote_file_path, c.user_id AS contractor_user_id
            FROM contractor_rfqs r
            JOIN contractors c ON r.contractor_id = c.id
            WHERE r.id = ?
        `, [rfqId]);
        if (rows.length === 0) {
            return res.status(404).json({ error: 'RFQ not found' });
        }
        const rfq = rows[0];
        if (role === 'contractor') {
            if (rfq.contractor_user_id !== userId) {
                return res.status(403).json({ error: 'You do not have permission to modify this RFQ' });
            }
        } else {
            const hasAccess = await checkProjectWriteAccess(rfq.project_id, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have permission to modify this RFQ' });
            }
        }
        await pool.query('UPDATE contractor_rfqs SET quote_file_path = NULL, quote_file_name = NULL WHERE id = ?', [rfqId]);
        if (rows[0].quote_file_path) deleteDiskFile(rows[0].quote_file_path);
        res.json({ success: true, message: 'Quote file deleted' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete quote file' });
    }
});

// ==========================================
// CONTRACTOR NEW FEATURES (EXTRA/DEDUCT WORK, INVOICES, COMPLAINTS)
// ==========================================

// 1. Extra Works (งานเพิ่ม)
app.get('/api/projects/:projectId/extra-works', authenticateToken, async (req, res) => {
    const { projectId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        let queryStr = `
            SELECT ew.*, c.name AS contractor_name
            FROM contractor_extra_works ew
            JOIN contractors c ON ew.contractor_id = c.id
            WHERE ew.project_id = ? AND ew.is_deleted = 0
        `;
        const queryParams = [projectId];
        if (role === 'contractor') {
            queryStr += ` AND c.user_id = ?`;
            queryParams.push(userId);
        }
        queryStr += ` ORDER BY ew.created_at DESC`;
        const [rows] = await pool.query(queryStr, queryParams);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve extra works' });
    }
});

app.post('/api/projects/:projectId/extra-works', authenticateToken, async (req, res) => {
    const { projectId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        if (role === 'contractor') {
            const { work_name, quantity, estimated_cost } = req.body;
            const [contractors] = await pool.query('SELECT id FROM contractors WHERE user_id = ? AND is_deleted = 0', [userId]);
            if (contractors.length === 0) {
                return res.status(403).json({ error: 'Contractor profile not found' });
            }
            const contractorId = contractors[0].id;

            await pool.query(`
                INSERT INTO contractor_extra_works (project_id, contractor_id, work_name, quantity, estimated_cost, status)
                VALUES (?, ?, ?, ?, ?, 'pending')
            `, [projectId, contractorId, work_name, parseFloat(quantity) || 0, parseFloat(estimated_cost) || 0]);

            return res.json({ success: true, message: 'Extra work requested successfully' });
        }

        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to order extra work for this project' });
        }

        const { contractor_id, work_name, section_name, brand, quantity, unit, material_rate, labour_rate } = req.body;
        if (!contractor_id || !work_name) {
            return res.status(400).json({ error: 'contractor_id and work_name are required' });
        }
        const [contractorRows] = await pool.query('SELECT id FROM contractors WHERE id = ? AND is_deleted = 0', [contractor_id]);
        if (contractorRows.length === 0) {
            return res.status(400).json({ error: 'Contractor not found' });
        }

        const qty = parseFloat(quantity) || 0;
        const matRate = parseFloat(material_rate) || 0;
        const labRate = parseFloat(labour_rate) || 0;
        const estimatedCost = qty * (matRate + labRate);

        await pool.query(`
            INSERT INTO contractor_extra_works (project_id, contractor_id, work_name, section_name, brand, quantity, unit, material_rate, labour_rate, estimated_cost, status, created_by)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)
        `, [projectId, contractor_id, work_name, section_name || null, brand || null, qty, unit || null, matRate, labRate, estimatedCost, userId]);

        res.json({ success: true, message: 'Extra work order recorded successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to request extra work' });
    }
});

// 2. Deduct Works (งานลด)
app.get('/api/projects/:projectId/deduct-works', authenticateToken, async (req, res) => {
    const { projectId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        let queryStr = `
            SELECT dw.*, c.name AS contractor_name
            FROM contractor_deduct_works dw
            JOIN contractors c ON dw.contractor_id = c.id
            WHERE dw.project_id = ? AND dw.is_deleted = 0
        `;
        const queryParams = [projectId];
        if (role === 'contractor') {
            queryStr += ` AND c.user_id = ?`;
            queryParams.push(userId);
        }
        queryStr += ` ORDER BY dw.created_at DESC`;
        const [rows] = await pool.query(queryStr, queryParams);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve deduct works' });
    }
});

app.post('/api/projects/:projectId/deduct-works', authenticateToken, async (req, res) => {
    const { projectId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        if (role === 'contractor') {
            const { item_name, quantity, deduct_amount } = req.body;
            const [contractors] = await pool.query('SELECT id FROM contractors WHERE user_id = ? AND is_deleted = 0', [userId]);
            if (contractors.length === 0) {
                return res.status(403).json({ error: 'Contractor profile not found' });
            }
            const contractorId = contractors[0].id;

            await pool.query(`
                INSERT INTO contractor_deduct_works (project_id, contractor_id, item_name, quantity, deduct_amount, status)
                VALUES (?, ?, ?, ?, ?, 'pending')
            `, [projectId, contractorId, item_name, parseFloat(quantity) || 0, parseFloat(deduct_amount) || 0]);

            return res.json({ success: true, message: 'Deduct work proposed successfully' });
        }

        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to record deduct work for this project' });
        }

        const { contractor_id, rfq_item_id, actual_qty } = req.body;
        if (!contractor_id || !rfq_item_id || actual_qty === undefined || actual_qty === null) {
            return res.status(400).json({ error: 'contractor_id, rfq_item_id and actual_qty are required' });
        }

        const [itemRows] = await pool.query(`
            SELECT ri.id, ri.description, ri.section_name, ri.qty AS quoted_qty, ri.material_rate, ri.labour_rate
            FROM contractor_rfq_items ri
            JOIN contractor_rfqs r ON ri.rfq_id = r.id
            WHERE ri.id = ? AND r.project_id = ? AND r.contractor_id = ? AND r.status = 'approved' AND r.is_deleted = 0
        `, [rfq_item_id, projectId, contractor_id]);
        if (itemRows.length === 0) {
            return res.status(400).json({ error: 'Quotation item not found on an approved contract for this contractor' });
        }
        const item = itemRows[0];
        const quotedQty = parseFloat(item.quoted_qty) || 0;
        const actualQty = parseFloat(actual_qty) || 0;
        const rate = (parseFloat(item.material_rate) || 0) + (parseFloat(item.labour_rate) || 0);
        const varianceQty = quotedQty - actualQty;
        const deductAmount = varianceQty * rate;

        await pool.query(`
            INSERT INTO contractor_deduct_works (project_id, contractor_id, item_name, quantity, deduct_amount, status, rfq_item_id, quoted_qty, actual_qty, rate, created_by)
            VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)
        `, [projectId, contractor_id, item.description, varianceQty, deductAmount, rfq_item_id, quotedQty, actualQty, rate, userId]);

        res.json({ success: true, message: 'Deduct work recorded successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to propose deduct work' });
    }
});

// 2b. Contract quotation lookup (latest approved RFQ items) for a contractor on a project
app.get('/api/projects/:projectId/contract-quotation', authenticateToken, async (req, res) => {
    const { projectId } = req.params;
    const { contractor_id } = req.query;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        if (!contractor_id) {
            return res.status(400).json({ error: 'contractor_id is required' });
        }
        if (role === 'contractor') {
            const [ownContractor] = await pool.query('SELECT id FROM contractors WHERE id = ? AND user_id = ? AND is_deleted = 0', [contractor_id, userId]);
            if (ownContractor.length === 0) {
                return res.status(403).json({ error: 'You do not have permission to view this quotation' });
            }
        } else {
            const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have permission to view this quotation' });
            }
        }

        const [rfqRows] = await pool.query(`
            SELECT id FROM contractor_rfqs
            WHERE project_id = ? AND contractor_id = ? AND status = 'approved' AND is_deleted = 0
            ORDER BY created_at DESC LIMIT 1
        `, [projectId, contractor_id]);
        if (rfqRows.length === 0) {
            return res.json({ rfq_id: null, items: [] });
        }
        const rfqId = rfqRows[0].id;
        const [items] = await pool.query('SELECT * FROM contractor_rfq_items WHERE rfq_id = ? AND is_included = 1 ORDER BY order_index ASC, id ASC', [rfqId]);
        res.json({ rfq_id: rfqId, items });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve contract quotation' });
    }
});

// 3. Billing Invoices (เอกสารวางบิล)
app.get('/api/projects/:projectId/invoices', authenticateToken, async (req, res) => {
    const { projectId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        let queryStr = `
            SELECT inv.*, c.name AS contractor_name
            FROM contractor_invoices inv
            JOIN contractors c ON inv.contractor_id = c.id
            WHERE inv.project_id = ? AND inv.is_deleted = 0
        `;
        const queryParams = [projectId];
        if (role === 'contractor') {
            queryStr += ` AND c.user_id = ?`;
            queryParams.push(userId);
        }
        queryStr += ` ORDER BY inv.created_at DESC`;
        const [rows] = await pool.query(queryStr, queryParams);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve invoices' });
    }
});

app.post('/api/projects/:projectId/invoices', authenticateToken, async (req, res) => {
    const { projectId } = req.params;
    const { invoice_no, description, amount } = req.body;
    const userId = req.user.id;
    try {
        const [contractors] = await pool.query('SELECT id FROM contractors WHERE user_id = ? AND is_deleted = 0', [userId]);
        if (contractors.length === 0) {
            return res.status(403).json({ error: 'Contractor profile not found' });
        }
        const contractorId = contractors[0].id;
        
        await pool.query(`
            INSERT INTO contractor_invoices (project_id, contractor_id, invoice_no, description, amount, status)
            VALUES (?, ?, ?, ?, ?, 'pending')
        `, [projectId, contractorId, invoice_no, description, parseFloat(amount) || 0]);
        
        res.json({ success: true, message: 'Invoice submitted successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to submit invoice' });
    }
});

// 4. Complaints (เรื่องร้องเรียน)
app.get('/api/projects/:projectId/complaints', authenticateToken, async (req, res) => {
    const { projectId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        let queryStr = `
            SELECT cmp.*, c.name AS contractor_name
            FROM contractor_complaints cmp
            JOIN contractors c ON cmp.contractor_id = c.id
            WHERE cmp.project_id = ? AND cmp.is_deleted = 0
        `;
        const queryParams = [projectId];
        if (role === 'contractor') {
            queryStr += ` AND c.user_id = ?`;
            queryParams.push(userId);
        }
        queryStr += ` ORDER BY cmp.created_at DESC`;
        const [rows] = await pool.query(queryStr, queryParams);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve complaints' });
    }
});

app.post('/api/projects/:projectId/complaints', authenticateToken, async (req, res) => {
    const { projectId } = req.params;
    const { title, detail } = req.body;
    const userId = req.user.id;
    try {
        const [contractors] = await pool.query('SELECT id FROM contractors WHERE user_id = ? AND is_deleted = 0', [userId]);
        if (contractors.length === 0) {
            return res.status(403).json({ error: 'Contractor profile not found' });
        }
        const contractorId = contractors[0].id;
        
        await pool.query(`
            INSERT INTO contractor_complaints (project_id, contractor_id, title, detail, status)
            VALUES (?, ?, ?, ?, 'pending')
        `, [projectId, contractorId, title, detail]);
        
        res.json({ success: true, message: 'Complaint submitted successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to submit complaint' });
    }
});

// ==========================================
// FAT (FACTORY ACCEPTANCE TEST) ENDPOINTS
// ==========================================

// Get all FAT reports for a project
app.get('/api/projects/:id/fat-reports', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        const [rows] = await pool.query(`
            SELECT * FROM project_fat_reports 
            WHERE project_id = ? AND is_deleted = 0 
            ORDER BY created_at DESC
        `, [id]);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve FAT reports' });
    }
});

// Create a new FAT report for a project (pre-populates from Excel template)
app.post('/api/projects/:id/fat-reports', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        if (!PERMISSIONS['fat:create'].includes(role)) {
            return res.status(403).json({ error: 'Not authorized to create FAT reports' });
        }

        const hasAccess = await checkProjectWriteAccess(id, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to modify this project' });
        }
        
        const {
            report_type,
            testing_date,
            container_no,
            panel_type,
            location,
            rated,
            voltage,
            instrument_model,
            instrument_sn,
            instrument_expire
        } = req.body;
        
        if (!report_type) {
            return res.status(400).json({ error: 'Report type is required' });
        }
        
        // Insert FAT Report Header
        const [result] = await pool.query(`
            INSERT INTO project_fat_reports 
            (project_id, report_type, testing_date, container_no, panel_type, location, rated, voltage, instrument_model, instrument_sn, instrument_expire)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        `, [
            id,
            report_type,
            testing_date || null,
            container_no || null,
            panel_type || null,
            location || null,
            rated || null,
            voltage || null,
            instrument_model || null,
            instrument_sn || null,
            instrument_expire || null
        ]);
        
        const fatReportId = result.insertId;
        
        // Load items from pre-cached Excel templates
        const template = excelTemplates[report_type];
        if (template && template.items) {
            for (const item of template.items) {
                await pool.query(`
                    INSERT INTO project_fat_items
                    (fat_report_id, item_no, title, description, result_pass, result_fail, photo, remarks)
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                `, [
                    fatReportId,
                    item.item_no || null,
                    item.title || null,
                    item.description || null,
                    (() => {
                        const isHeader = !item.item_no || (!item.item_no.includes('.') && (!item.description || item.description.trim() === ''));
                        return isHeader ? 0 : 1;
                    })(),
                    0,
                    null,
                    null
                ]);
            }
        }
        
        await logActivity(userId, null, 'FAT_CREATE', `Created FAT report "${report_type}" for Project ID ${id}`);
        
        const [newReport] = await pool.query('SELECT * FROM project_fat_reports WHERE id = ?', [fatReportId]);
        res.status(201).json(newReport[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to create FAT report' });
    }
});

// Get all items in a FAT report
app.get('/api/fat-reports/:reportId/items', authenticateToken, async (req, res) => {
    const { reportId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        const [items] = await pool.query(`
            SELECT id, fat_report_id, item_no, title, description, result_pass, result_fail, remarks, created_at,
                   (CASE WHEN photo IS NOT NULL THEN 1 ELSE 0 END) as has_legacy_photo
            FROM project_fat_items 
            WHERE fat_report_id = ? 
            ORDER BY id ASC
        `, [reportId]);
        
        const itemIds = items.map(it => it.id);
        let photosMap = {};
        if (itemIds.length > 0) {
            const [photos] = await pool.query(`
                SELECT id, item_id FROM project_fat_item_photos 
                WHERE item_id IN (${itemIds.join(',')})
            `);
            photos.forEach(p => {
                if (!photosMap[p.item_id]) photosMap[p.item_id] = [];
                photosMap[p.item_id].push(p.id);
            });
        }
        
        items.forEach(it => {
            it.photos = photosMap[it.id] || [];
            if (it.has_legacy_photo) {
                it.photos.unshift(`legacy_${it.id}`);
            }
        });
        
        res.json(items);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve FAT report items' });
    }
});

// Save/update items in a FAT report (bulk save)
app.put('/api/fat-reports/:reportId/items', authenticateToken, async (req, res) => {
    const { reportId } = req.params;
    const { items, header } = req.body;
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        const [reports] = await pool.query('SELECT project_id FROM project_fat_reports WHERE id = ?', [reportId]);
        if (reports.length === 0) {
            return res.status(404).json({ error: 'FAT report not found' });
        }
        if (!PERMISSIONS['fat:update'].includes(role)) {
            return res.status(403).json({ error: 'Not authorized to modify FAT reports' });
        }

        const projectId = reports[0].project_id;
        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to modify this project' });
        }
        
        // Update header details if provided
        if (header) {
            await pool.query(`
                UPDATE project_fat_reports SET
                    testing_date = ?,
                    container_no = ?,
                    panel_type = ?,
                    location = ?,
                    rated = ?,
                    voltage = ?,
                    instrument_model = ?,
                    instrument_sn = ?,
                    instrument_expire = ?
                WHERE id = ?
            `, [
                header.testing_date || null,
                header.container_no || null,
                header.panel_type || null,
                header.location || null,
                header.rated || null,
                header.voltage || null,
                header.instrument_model || null,
                header.instrument_sn || null,
                header.instrument_expire || null,
                reportId
            ]);
        }
        
        // Save items
        if (Array.isArray(items)) {
            for (const item of items) {
                await pool.query(`
                    UPDATE project_fat_items SET
                        result_pass = ?,
                        result_fail = ?,
                        remarks = ?,
                        description = ?
                    WHERE id = ? AND fat_report_id = ?
                `, [
                    item.result_pass ? 1 : 0,
                    item.result_fail ? 1 : 0,
                    item.remarks || null,
                    item.description || null,
                    item.id,
                    reportId
                ]);
            }
        }
        
        await logActivity(userId, null, 'FAT_UPDATE', `Updated FAT report ID ${reportId} items`);
        res.json({ success: true, message: 'FAT report updated successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update FAT report' });
    }
});
// Upload image directly to a FAT report item photo table (Support up to 10 photos)
app.post('/api/fat-reports/:reportId/items/:itemId/upload', authenticateToken, upload.single('image'), async (req, res) => {
    const { reportId, itemId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    
    if (!req.file) {
        return res.status(400).json({ error: 'No image file uploaded' });
    }
    
    try {
        const [reports] = await pool.query('SELECT project_id FROM project_fat_reports WHERE id = ?', [reportId]);
        if (reports.length === 0) {
            return res.status(404).json({ error: 'FAT report not found' });
        }
        if (!PERMISSIONS['fat:update'].includes(role)) {
            return res.status(403).json({ error: 'Not authorized to modify FAT reports' });
        }

        const projectId = reports[0].project_id;
        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to modify this project' });
        }
        
        // Enforce maximum of 10 photos
        const [countRows] = await pool.query('SELECT COUNT(*) as count FROM project_fat_item_photos WHERE item_id = ?', [itemId]);
        const [legacyRow] = await pool.query('SELECT photo FROM project_fat_items WHERE id = ?', [itemId]);
        const totalPhotos = countRows[0].count + (legacyRow[0] && legacyRow[0].photo ? 1 : 0);
        if (totalPhotos >= 10) {
            return res.status(400).json({ error: 'Maximum of 10 photos allowed per item' });
        }
        
        await pool.query(`
            INSERT INTO project_fat_item_photos (item_id, photo)
            VALUES (?, ?)
        `, [itemId, req.file.buffer]);
        
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to upload image' });
    }
});

// Get photo from a FAT report item (Legacy compatibility)
app.get('/api/fat-reports/:reportId/items/:itemId/photo', authenticateToken, async (req, res) => {
    const { itemId } = req.params;
    try {
        const [rows] = await pool.query('SELECT photo FROM project_fat_items WHERE id = ?', [itemId]);
        if (rows.length === 0 || !rows[0].photo) {
            return res.status(404).send('Photo not found');
        }
        res.setHeader('Content-Type', 'image/png');
        res.send(rows[0].photo);
    } catch (err) {
        console.error(err);
        res.status(500).send('Error');
    }
});

// Get a specific photo by photoId (Supports legacy_itemId and new photo database IDs)
app.get('/api/fat-reports/photos/:photoId', authenticateToken, async (req, res) => {
    const { photoId } = req.params;
    try {
        if (photoId.startsWith('legacy_')) {
            const itemId = photoId.replace('legacy_', '');
            const [rows] = await pool.query('SELECT photo FROM project_fat_items WHERE id = ?', [itemId]);
            if (rows.length === 0 || !rows[0].photo) {
                return res.status(404).send('Photo not found');
            }
            res.setHeader('Content-Type', 'image/png');
            return res.send(rows[0].photo);
        } else {
            const [rows] = await pool.query('SELECT photo FROM project_fat_item_photos WHERE id = ?', [photoId]);
            if (rows.length === 0 || !rows[0].photo) {
                return res.status(404).send('Photo not found');
            }
            res.setHeader('Content-Type', 'image/png');
            return res.send(rows[0].photo);
        }
    } catch (err) {
        console.error(err);
        res.status(500).send('Error retrieving photo');
    }
});

// Delete photo from a FAT report item
app.delete('/api/fat-reports/:reportId/items/:itemId/photo', authenticateToken, async (req, res) => {
    const { reportId, itemId } = req.params;
    const { photoId } = req.query; // If specified, delete only this photo. Otherwise, delete all photos for this item.
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        const [reports] = await pool.query('SELECT project_id FROM project_fat_reports WHERE id = ?', [reportId]);
        if (reports.length === 0) {
            return res.status(404).json({ error: 'FAT report not found' });
        }
        if (!PERMISSIONS['fat:update'].includes(role)) {
            return res.status(403).json({ error: 'Not authorized to modify FAT reports' });
        }

        const projectId = reports[0].project_id;
        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission' });
        }
        
        if (photoId) {
            if (photoId.startsWith('legacy_')) {
                await pool.query('UPDATE project_fat_items SET photo = NULL WHERE id = ?', [itemId]);
            } else {
                await pool.query('DELETE FROM project_fat_item_photos WHERE id = ? AND item_id = ?', [photoId, itemId]);
            }
        } else {
            // Delete all photos for this item
            await pool.query('UPDATE project_fat_items SET photo = NULL WHERE id = ?', [itemId]);
            await pool.query('DELETE FROM project_fat_item_photos WHERE item_id = ?', [itemId]);
        }
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete photo' });
    }
});
// Delete a FAT report
app.delete('/api/fat-reports/:reportId', authenticateToken, async (req, res) => {
    const { reportId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        const [reports] = await pool.query('SELECT project_id, report_type FROM project_fat_reports WHERE id = ?', [reportId]);
        if (reports.length === 0) {
            return res.status(404).json({ error: 'FAT report not found' });
        }
        if (!PERMISSIONS['fat:update'].includes(role)) {
            return res.status(403).json({ error: 'Not authorized to modify FAT reports' });
        }

        const projectId = reports[0].project_id;
        const reportType = reports[0].report_type;
        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to modify this project' });
        }
        
        await pool.query('UPDATE project_fat_reports SET is_deleted = 1 WHERE id = ?', [reportId]);
        await logActivity(userId, null, 'FAT_DELETE', `Deleted FAT report "${reportType}" ID ${reportId} of Project ID ${projectId}`);
        res.json({ success: true, message: 'FAT report deleted successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete FAT report' });
    }
});

app.get('/api/projects/:id/items', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    try {
        if (role !== 'admin' && role !== 'superadmin' && role !== 'excusive') {
            const hasAccess = await checkProjectWriteAccess(id, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have access to this project' });
            }
        }

        const [rows] = await pool.query(`
            SELECT pi.*, ps.name AS section_name, p.supplier_price, s.name AS supplier_name
            FROM project_items pi
            JOIN project_sections ps ON pi.section_id = ps.id
            LEFT JOIN products p ON pi.product_id = p.id
            LEFT JOIN suppliers s ON p.supplier_id = s.id
            WHERE ps.project_id = ? AND pi.is_deleted = 0 AND ps.is_deleted = 0
            ORDER BY ps.order_index ASC, pi.order_index ASC, pi.id ASC
        `, [id]);

        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to fetch project items' });
    }
});

// Excel Export Endpoint
app.get('/api/projects/:id/export-excel', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    try {
        if (role !== 'admin' && role !== 'superadmin' && role !== 'excusive') {
            const hasAccess = await checkProjectWriteAccess(id, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have access to this project' });
            }
        }

        const [projRows] = await pool.query('SELECT name, external_name, client_name, description FROM projects WHERE id = ?', [id]);
        if (projRows.length === 0) {
            return res.status(404).json({ error: 'Project not found' });
        }
        const project = projRows[0];
        const projName = project.external_name || project.name;

        const [sections] = await pool.query('SELECT id, name FROM project_sections WHERE project_id = ? AND is_deleted = 0 ORDER BY order_index ASC, id ASC', [id]);

        const workbook = new ExcelJS.Workbook();

        // 1. SUMMARY SHEET
        const summarySheet = workbook.addWorksheet('Summary');
        summarySheet.pageSetup = {
            paperSize: 9, // A4
            orientation: 'portrait',
            fitToPage: true,
            fitToWidth: 1,
            fitToHeight: 0,
            margins: { left: 0.5, right: 0.5, top: 0.5, bottom: 0.5, header: 0.3, footer: 0.3 }
        };

        // Title Block
        summarySheet.addRow(['ใบเสนอราคาสรุปโครงการ (Project Summary Quotation)']).font = { bold: true, size: 16, color: { argb: 'FF1E293B' } };
        summarySheet.addRow([]);
        summarySheet.addRow(['โครงการ / Project:', projName]);
        summarySheet.addRow(['ลูกค้า / Client:', project.client_name || '-']);
        summarySheet.addRow(['เลขที่เอกสาร / Doc No:', `QT-2026-${id.toString().padStart(4, '0')}`]);
        summarySheet.addRow(['วันที่พิมพ์ / Date:', new Date().toLocaleDateString('th-TH')]);
        summarySheet.addRow(['รายละเอียด / Description:', project.description || '-']);
        summarySheet.addRow([]);

        for (let r = 3; r <= 7; r++) {
            summarySheet.getRow(r).getCell(1).font = { bold: true };
        }

        // Check if internal BOM (admin, superadmin, supervisor)
        const isBOM = ['admin', 'superadmin', 'supervisor'].includes(role);
        const summaryHeaders = isBOM 
            ? ['ลำดับ (No.)', 'ระบบงาน (System / Section)', 'เสนอราคาค่าวัสดุ (Material Price)', 'เสนอราคาค่าแรง (Labour Price)', 'เสนอราคารวม (Total Price)', 'ต้นทุนรวม (Total Cost)', 'กำไรสุทธิ (Profit)']
            : ['ลำดับ (No.)', 'ระบบงาน (System / Section)', 'เสนอราคาค่าวัสดุ (Material Price)', 'เสนอราคาค่าแรง (Labour Price)', 'เสนอราคารวม (Total Price)'];

        const sumHeaderRow = summarySheet.addRow(summaryHeaders);
        sumHeaderRow.height = 28;
        sumHeaderRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
        sumHeaderRow.eachCell((cell, colNumber) => {
            cell.fill = {
                type: 'pattern',
                pattern: 'solid',
                fgColor: { argb: 'FF1E293B' }
            };
            cell.alignment = { vertical: 'middle', horizontal: colNumber >= 3 ? 'right' : 'left' };
            cell.border = {
                top: { style: 'thin' },
                left: { style: 'thin' },
                bottom: { style: 'medium' },
                right: { style: 'thin' }
            };
        });

        let index = 1;
        let sumPriceMat = 0, sumPriceLab = 0, sumPriceTotal = 0, sumCostTotal = 0, sumProfit = 0;

        const systemRows = [];
        for (const sec of sections) {
            const [totals] = await pool.query(`
                SELECT 
                    SUM(qty * cost_material) as cost_mat,
                    SUM(qty * cost_labour) as cost_lab,
                    SUM(qty * price_material) as price_mat,
                    SUM(qty * price_labour) as price_lab
                FROM project_items 
                WHERE section_id = ? AND is_deleted = 0
            `, [sec.id]);

            const rowTotals = totals[0] || {};
            const pMat = parseFloat(rowTotals.price_mat) || 0;
            const pLab = parseFloat(rowTotals.price_lab) || 0;
            const pTotal = pMat + pLab;
            const cMat = parseFloat(rowTotals.cost_mat) || 0;
            const cLab = parseFloat(rowTotals.cost_lab) || 0;
            const cTotal = cMat + cLab;
            const profit = pTotal - cTotal;

            sumPriceMat += pMat;
            sumPriceLab += pLab;
            sumPriceTotal += pTotal;
            sumCostTotal += cTotal;
            sumProfit += profit;

            const rowData = isBOM
                ? [index++, sec.name, pMat, pLab, pTotal, cTotal, profit]
                : [index++, sec.name, pMat, pLab, pTotal];

            const row = summarySheet.addRow(rowData);
            row.height = 22;
            row.eachCell((cell, colNumber) => {
                cell.alignment = { vertical: 'middle', horizontal: colNumber >= 3 ? 'right' : 'left' };
                cell.border = {
                    top: { style: 'thin', color: { argb: 'FFE2E8F0' } },
                    left: { style: 'thin', color: { argb: 'FFE2E8F0' } },
                    bottom: { style: 'thin', color: { argb: 'FFE2E8F0' } },
                    right: { style: 'thin', color: { argb: 'FFE2E8F0' } }
                };
                if (colNumber >= 3) {
                    cell.numFmt = '#,##0.00';
                }
            });

            systemRows.push({ sec, pMat, pLab, pTotal, cTotal, profit });
        }

        // Summary Grand Total Row
        const grandTotalData = isBOM
            ? ['', 'ยอดรวมสุทธิทั้งสิ้น (Grand Total)', sumPriceMat, sumPriceLab, sumPriceTotal, sumCostTotal, sumProfit]
            : ['', 'ยอดรวมสุทธิทั้งสิ้น (Grand Total)', sumPriceMat, sumPriceLab, sumPriceTotal];

        const grandRow = summarySheet.addRow(grandTotalData);
        grandRow.height = 28;
        grandRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
        grandRow.eachCell((cell, colNumber) => {
            cell.fill = {
                type: 'pattern',
                pattern: 'solid',
                fgColor: { argb: 'FF0F172A' }
            };
            cell.alignment = { vertical: 'middle', horizontal: colNumber >= 3 ? 'right' : 'left' };
            cell.border = {
                top: { style: 'thin', color: { argb: 'FF000000' } },
                left: { style: 'thin', color: { argb: 'FF000000' } },
                bottom: { style: 'double', color: { argb: 'FF000000' } },
                right: { style: 'thin', color: { argb: 'FF000000' } }
            };
            if (colNumber >= 3) {
                cell.numFmt = '#,##0.00';
            }
        });

        // Signatures Block
        summarySheet.addRow([]);
        summarySheet.addRow([]);
        const sigRow1 = summarySheet.addRow(['', '___________________________', '', '___________________________']);
        const sigRow2 = summarySheet.addRow(['', 'ผู้เตรียมเอกสาร (Preparer)', '', 'ผู้อนุมัติโครงการ (Approver)']);
        sigRow1.font = { bold: true };
        sigRow2.font = { italic: true, size: 9 };
        sigRow1.getCell(2).alignment = { horizontal: 'center' };
        sigRow1.getCell(4).alignment = { horizontal: 'center' };
        sigRow2.getCell(2).alignment = { horizontal: 'center' };
        sigRow2.getCell(4).alignment = { horizontal: 'center' };

        // Auto-fit Summary Sheet Column Widths
        summarySheet.columns.forEach((column, i) => {
            let maxLen = 0;
            column.eachCell({ includeEmpty: true }, (cell) => {
                const len = cell.value ? String(cell.value).length : 0;
                if (len > maxLen) maxLen = len;
            });
            column.width = Math.max(maxLen + 3, i === 1 ? 30 : 12);
        });


        // 2. INDIVIDUAL SYSTEM SHEETS
        let secIndex = 0;
        for (const sys of systemRows) {
            const sec = sys.sec;
            let sheetName = sec.name.replace(/[\\/?*\[\]]/g, '').trim();
            if (sheetName.length > 22) {
                sheetName = sheetName.substring(0, 22);
            }
            sheetName = `${secIndex + 1}. ${sheetName}`;
            secIndex++;

            const ws = workbook.addWorksheet(sheetName);
            ws.pageSetup = {
                paperSize: 9, // A4
                orientation: 'portrait',
                fitToPage: true,
                fitToWidth: 1,
                fitToHeight: 0,
                margins: { left: 0.5, right: 0.5, top: 0.5, bottom: 0.5, header: 0.3, footer: 0.3 }
            };

            // Sheet Header Info
            ws.addRow([`รายละเอียดระบบงาน: ${sec.name}`]).font = { bold: true, size: 14, color: { argb: 'FF1E293B' } };
            ws.addRow(['โครงการ / Project:', projName]).font = { size: 10 };
            ws.addRow([]);

            // Headers for system item table (Image in Column 3 / Column C)
            const itemHeaders = [
                'Item ID', 'System Name', 'Image', 'Brand', 'Description', 'Model/Spec', 'Unit', 'Free Issue (Y/N)',
                'Est Qty', 'Est Material Cost', 'Est Labour Cost', 'Est Material Price', 'Est Labour Price', 'Est Discount (%)',
                'Actual Qty', 'Actual Material Cost', 'Actual Labour Cost', 'Actual Material Price', 'Actual Labour Price'
            ];

            const itemHeaderRow = ws.addRow(itemHeaders);
            itemHeaderRow.height = 25;
            itemHeaderRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
            itemHeaderRow.eachCell((cell, colNumber) => {
                cell.fill = {
                    type: 'pattern',
                    pattern: 'solid',
                    fgColor: { argb: 'FF334155' } // slate-700
                };
                cell.alignment = { 
                    vertical: 'middle', 
                    horizontal: colNumber >= 9 && colNumber <= 19 ? 'right' : 'left',
                    wrapText: true 
                };
                cell.border = {
                    top: { style: 'thin' },
                    left: { style: 'thin' },
                    bottom: { style: 'medium' },
                    right: { style: 'thin' }
                };
            });

            // Fetch items in section
            const [items] = await pool.query(`
                SELECT pi.id, pi.brand, pi.description, pi.type as model, pi.unit, pi.is_free_issue,
                       pi.qty, pi.cost_material, pi.cost_labour, pi.price_material, pi.price_labour, pi.discount,
                       pi.production_qty, pi.production_cost_material, pi.production_cost_labour, pi.production_price_material, pi.production_price_labour,
                       p.image_path
                FROM project_items pi
                LEFT JOIN products p ON pi.product_id = p.id
                WHERE pi.section_id = ? AND pi.is_deleted = 0
                ORDER BY pi.order_index ASC, pi.id ASC
            `, [sec.id]);

            let secCostMat = 0, secCostLab = 0, secPriceMat = 0, secPriceLab = 0;
            let secActCostMat = 0, secActCostLab = 0, secActPriceMat = 0, secActPriceLab = 0;

            for (const item of items) {
                const qty = parseFloat(item.qty) || 0;
                const costMat = parseFloat(item.cost_material) || 0;
                const costLab = parseFloat(item.cost_labour) || 0;
                const priceMat = parseFloat(item.price_material) || 0;
                const priceLab = parseFloat(item.price_labour) || 0;
                const discount = parseFloat(item.discount) || 0;

                const actQty = item.production_qty !== null ? parseFloat(item.production_qty) : null;
                const actCostMat = item.production_cost_material !== null ? parseFloat(item.production_cost_material) : null;
                const actCostLab = item.production_cost_labour !== null ? parseFloat(item.production_cost_labour) : null;
                const actPriceMat = item.production_price_material !== null ? parseFloat(item.production_price_material) : null;
                const actPriceLab = item.production_price_labour !== null ? parseFloat(item.production_price_labour) : null;

                secCostMat += costMat * qty;
                secCostLab += costLab * qty;
                secPriceMat += priceMat * qty;
                secPriceLab += priceLab * qty;

                if (actQty !== null) {
                    secActCostMat += (actCostMat || 0) * actQty;
                    secActCostLab += (actCostLab || 0) * actQty;
                    secActPriceMat += (actPriceMat || 0) * actQty;
                    secActPriceLab += (actPriceLab || 0) * actQty;
                }

                const rowData = [
                    item.id,
                    sec.name,
                    '', // Image placeholder in Column 3 (Column C)
                    item.brand || '',
                    item.description || '',
                    item.model || '',
                    item.unit || '',
                    item.is_free_issue ? 'Y' : 'N',
                    qty,
                    costMat,
                    costLab,
                    priceMat,
                    priceLab,
                    discount,
                    actQty !== null ? actQty : '',
                    actCostMat !== null ? actCostMat : '',
                    actCostLab !== null ? actCostLab : '',
                    actPriceMat !== null ? actPriceMat : '',
                    actPriceLab !== null ? actPriceLab : ''
                ];

                const row = ws.addRow(rowData);
                row.height = item.image_path ? 65 : 20;

                row.eachCell((cell, colNumber) => {
                    cell.alignment = { 
                        vertical: 'middle', 
                        horizontal: colNumber >= 9 && colNumber <= 19 ? 'right' : 'left',
                        wrapText: true 
                    };
                    cell.border = {
                        top: { style: 'thin', color: { argb: 'FFE2E8F0' } },
                        left: { style: 'thin', color: { argb: 'FFE2E8F0' } },
                        bottom: { style: 'thin', color: { argb: 'FFE2E8F0' } },
                        right: { style: 'thin', color: { argb: 'FFE2E8F0' } }
                    };
                    if (colNumber >= 9 && colNumber <= 19 && cell.value !== '') {
                        cell.numFmt = '#,##0.00';
                    }
                });

                if (item.image_path) {
                    const absImagePath = path.join(__dirname, item.image_path);
                    if (fs.existsSync(absImagePath)) {
                        try {
                            const extName = path.extname(item.image_path).toLowerCase().substring(1) || 'jpeg';
                            const imgId = workbook.addImage({
                                filename: absImagePath,
                                extension: extName === 'jpg' ? 'jpeg' : extName
                            });
                            ws.addImage(imgId, {
                                tl: { col: 2, row: row.number - 1 },
                                br: { col: 3, row: row.number },
                                editAs: 'twoCell'
                            });
                        } catch (err) {
                            console.error('Failed to embed excel image', err);
                        }
                    }
                }
            }

            // System Total Row inside System Sheet
            const sysTotalRowData = [
                '',
                `ผลรวมระบบ: ${sec.name}`,
                '', '', '', '', '', '', '',
                secCostMat,
                secCostLab,
                secPriceMat,
                secPriceLab,
                '', // Discount
                '', // Actual Qty
                secActCostMat,
                secActCostLab,
                secActPriceMat,
                secActPriceLab
            ];

            const sumRow = ws.addRow(sysTotalRowData);
            sumRow.height = 24;
            sumRow.font = { bold: true, color: { argb: 'FF1E293B' } };
            sumRow.eachCell((cell, colNumber) => {
                cell.fill = {
                    type: 'pattern',
                    pattern: 'solid',
                    fgColor: { argb: 'FFF1F5F9' }
                };
                cell.alignment = { 
                    vertical: 'middle', 
                    horizontal: colNumber >= 9 && colNumber <= 19 ? 'right' : 'left',
                    wrapText: true 
                };
                cell.border = {
                    top: { style: 'thin', color: { argb: 'FF94A3B8' } },
                    left: { style: 'thin', color: { argb: 'FF94A3B8' } },
                    bottom: { style: 'medium', color: { argb: 'FF1E293B' } },
                    right: { style: 'thin', color: { argb: 'FF94A3B8' } }
                };
                if (colNumber >= 9 && colNumber <= 19 && cell.value !== '') {
                    cell.numFmt = '#,##0.00';
                }
            });

            // Set column widths dynamically for system sheets
            ws.columns.forEach((column, i) => {
                let maxLen = 0;
                column.eachCell({ includeEmpty: true }, (cell) => {
                    const len = cell.value ? String(cell.value).length : 0;
                    if (len > maxLen) maxLen = len;
                });
                column.width = Math.max(maxLen + 3, i === 2 ? 16 : 10);
            });
        }

        const buffer = await workbook.xlsx.writeBuffer();
        res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
        res.setHeader('Content-Disposition', `attachment; filename="Project_${id}_Estimates.xlsx"`);
        res.send(buffer);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to export Excel file' });
    }
});

// Excel Import Endpoint
app.post('/api/projects/:id/import-excel', authenticateToken, upload.single('excel'), async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    const allowedRoles = ['admin', 'superadmin', 'supervisor', 'estimate', 'production'];
    if (!allowedRoles.includes(role)) {
        return res.status(403).json({ error: 'Role unauthorized to import project sheets' });
    }

    try {
        if (role !== 'admin' && role !== 'superadmin' && role !== 'excusive') {
            const hasAccess = await checkProjectWriteAccess(id, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have access to this project' });
            }
        }

        if (!req.file) {
            return res.status(400).json({ error: 'No Excel file uploaded' });
        }

        const workbook = XLSX.read(req.file.buffer, { type: 'buffer' });
        
        await pool.query('BEGIN TRANSACTION');

        try {
            const [existingSections] = await pool.query('SELECT id, name FROM project_sections WHERE project_id = ? AND is_deleted = 0', [id]);
            const sectionMap = new Map();
            existingSections.forEach(s => sectionMap.set(s.name.trim().toLowerCase(), s.id));

            let orderIndex = 0;

            for (const sheetName of workbook.SheetNames) {
                if (sheetName.toLowerCase() === 'summary') {
                    continue;
                }

                const sheet = workbook.Sheets[sheetName];
                const data = XLSX.utils.sheet_to_json(sheet, { header: 1 });

                for (let i = 1; i < data.length; i++) {
                    const row = data[i];
                    if (!row || row.length === 0) continue;

                    const itemIdVal = row[0];
                    const systemName = row[1] ? String(row[1]).trim() : 'General';
                    // Index 2 is Image, which we skip in parsing
                    const brand = row[3] ? String(row[3]).trim() : '';
                    const description = row[4] ? String(row[4]).trim() : '';
                    const model = row[5] ? String(row[5]).trim() : '';
                    const unit = row[6] ? String(row[6]).trim() : '';
                    const isFreeIssue = String(row[7]).trim().toUpperCase() === 'Y';
                    
                    const qty = parseFloat(row[8]) || 0;
                    const costMaterial = parseFloat(row[9]) || 0;
                    const costLabour = parseFloat(row[10]) || 0;
                    const priceMaterial = parseFloat(row[11]) || 0;
                    const priceLabour = parseFloat(row[12]) || 0;
                    const discount = parseFloat(row[13]) || 0;

                    const prodQty = row[14] !== undefined && row[14] !== '' ? parseFloat(row[14]) : null;
                    const prodCostMaterial = row[15] !== undefined && row[15] !== '' ? parseFloat(row[15]) : null;
                    const prodCostLabour = row[16] !== undefined && row[16] !== '' ? parseFloat(row[16]) : null;
                    const prodPriceMaterial = row[17] !== undefined && row[17] !== '' ? parseFloat(row[17]) : null;
                    const prodPriceLabour = row[18] !== undefined && row[18] !== '' ? parseFloat(row[18]) : null;

                    if (!description && !brand) continue;

                    let sectionId = sectionMap.get(systemName.toLowerCase());
                    if (!sectionId) {
                        const [secRes] = await pool.query(
                            'INSERT INTO project_sections (project_id, name, order_index) VALUES (?, ?, ?)',
                            [id, systemName, orderIndex++]
                        );
                        sectionId = secRes.insertId;
                        sectionMap.set(systemName.toLowerCase(), sectionId);
                    }

                    if (itemIdVal) {
                        const itemId = parseInt(itemIdVal);
                        const [itemCheck] = await pool.query(`
                            SELECT 1 FROM project_items pi
                            JOIN project_sections ps ON pi.section_id = ps.id
                            WHERE pi.id = ? AND ps.project_id = ? AND pi.is_deleted = 0
                        `, [itemId, id]);

                        if (itemCheck.length > 0) {
                            await pool.query(`
                                UPDATE project_items
                                SET brand = ?, description = ?, type = ?, unit = ?, is_free_issue = ?,
                                    qty = ?, cost_material = ?, cost_labour = ?, price_material = ?, price_labour = ?, discount = ?,
                                    production_qty = ?, production_cost_material = ?, production_cost_labour = ?, production_price_material = ?, production_price_labour = ?
                                WHERE id = ?
                            `, [
                                brand, description, model, unit, isFreeIssue ? 1 : 0,
                                qty, costMaterial, costLabour, priceMaterial, priceLabour, discount,
                                prodQty, prodCostMaterial, prodCostLabour, prodPriceMaterial, prodPriceLabour,
                                itemId
                            ]);
                        }
                    } else {
                        await pool.query(`
                            INSERT INTO project_items
                            (section_id, brand, description, type, unit, is_free_issue,
                             qty, cost_material, cost_labour, price_material, price_labour, discount,
                             production_qty, production_cost_material, production_cost_labour, production_price_material, production_price_labour,
                             created_by)
                            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                        `, [
                            sectionId, brand, description, model, unit, isFreeIssue ? 1 : 0,
                            qty, costMaterial, costLabour, priceMaterial, priceLabour, discount,
                            prodQty, prodCostMaterial, prodCostLabour, prodPriceMaterial, prodPriceLabour,
                            userId
                        ]);
                    }
                }
            }

            await pool.query('COMMIT');
            await logActivity(userId, req.user.username, 'Import Excel', `Imported/Synced Excel sheet for Project ${id}`);
            res.json({ success: true, message: 'Excel import and synchronization completed successfully' });
        } catch (txnErr) {
            await pool.query('ROLLBACK');
            throw txnErr;
        }
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to parse and import Excel file' });
    }
});

// ==========================================
// 5. SUMMARY & PROFIT ANALYSIS ENDPOINT
// ==========================================
app.get('/api/projects/:id/summary', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    const mode = req.query.mode || 'estimate';

    try {
        // Get project details (exclude blob data)

        const [projects] = await pool.query(`
            SELECT p.id, p.name, p.client_name, p.description, p.created_by, p.status, p.drawing_name, p.spec_name, p.is_deleted, p.project_type, p.external_name,
                   COALESCE((SELECT MAX(revision_number) FROM project_revisions WHERE project_id = p.id), 0) as current_revision
            FROM projects p WHERE p.id = ?
        `, [id]);
        if (projects.length === 0) {
            return res.status(404).json({ error: 'Project not found' });
        }
        const project = projects[0];

        // Access check for restricted roles: check ownership or assignment
        if (role !== 'admin' && role !== 'superadmin' && role !== 'excusive') {
            const hasAccess = await checkProjectWriteAccess(id, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have access to this project' });
            }
        }

        // Get sections
        const [sections] = await pool.query('SELECT id, project_id, name, spec_name, (spec_path IS NOT NULL) AS has_spec, is_deleted, profit_percent, created_at FROM project_sections WHERE project_id = ? AND is_deleted = 0 ORDER BY order_index ASC, id ASC', [id]);
        
        let contractorId = null;
        if (role === 'contractor') {
            const [contractorRows] = await pool.query('SELECT id FROM contractors WHERE user_id = ? AND is_deleted = 0', [userId]);
            if (contractorRows.length > 0) {
                contractorId = contractorRows[0].id;
            }
        }

        let latestRfqId = null;
        let latestRfqNumber = null;
        const rfqIdOverride = req.query.rfq_id ? parseInt(req.query.rfq_id) : null;
        if (contractorId) {
            if (rfqIdOverride) {
                const [rfqRows] = await pool.query(
                    'SELECT id, rfq_number FROM contractor_rfqs WHERE id = ? AND contractor_id = ? AND is_deleted = 0',
                    [rfqIdOverride, contractorId]
                );
                if (rfqRows.length > 0) {
                    latestRfqId = rfqRows[0].id;
                    latestRfqNumber = rfqRows[0].rfq_number;
                }
            }

            if (!latestRfqId) {
                const [rfqRows] = await pool.query(
                    'SELECT id, rfq_number FROM contractor_rfqs WHERE project_id = ? AND contractor_id = ? AND is_deleted = 0 ORDER BY created_at DESC LIMIT 1',
                    [id, contractorId]
                );
                if (rfqRows.length > 0) {
                    latestRfqId = rfqRows[0].id;
                    latestRfqNumber = rfqRows[0].rfq_number;
                }
            }
        }

        let bidMap = new Map();
        if (latestRfqId) {
            const [rfqItems] = await pool.query(
                'SELECT project_item_id, material_rate, labour_rate FROM contractor_rfq_items WHERE rfq_id = ?',
                [latestRfqId]
            );

            // Synchronize with active project items that might be missing from this RFQ
            const [projItems] = await pool.query(`
                SELECT pi.id, ps.name AS section_name, pi.description, pi.brand, pi.qty, pi.unit, pi.order_index
                FROM project_items pi
                JOIN project_sections ps ON pi.section_id = ps.id
                WHERE ps.project_id = ? AND pi.is_deleted = 0 AND ps.is_deleted = 0
            `, [id]);

            const existingProjItemIds = new Set(rfqItems.map(ri => ri.project_item_id));
            for (const pi of projItems) {
                if (!existingProjItemIds.has(pi.id)) {
                    await pool.query(`
                        INSERT INTO contractor_rfq_items (rfq_id, project_item_id, section_name, description, brand, qty, unit, order_index)
                        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                    `, [latestRfqId, pi.id, pi.section_name, pi.description, pi.brand, pi.qty, pi.unit, pi.order_index]);
                    rfqItems.push({ project_item_id: pi.id, material_rate: 0, labour_rate: 0 });
                }
            }

            rfqItems.forEach(ri => {
                bidMap.set(ri.project_item_id, ri);
            });
        }

        let projectSummary = {
            id: project.id,
            name: project.name,
            client_name: project.client_name,
            description: project.description,
            status: project.status || 'รอดำเนินการ',
            project_type: project.project_type || '',
            external_name: project.external_name || '',
            drawing_name: project.drawing_name,
            spec_name: project.spec_name,
            is_deleted: project.is_deleted || 0,
            created_by: project.created_by,
            current_revision: project.current_revision || 0,
            latest_rfq_number: latestRfqNumber,
            latest_rfq_id: latestRfqId,
            cost_material: 0,
            cost_labour: 0,
            cost_total: 0,
            price_material: 0,
            price_labour: 0,
            price_total: 0,
            profit: 0,
            profit_margin_percent: 0,

            // Production fields
            production_cost_material: 0,
            production_cost_labour: 0,
            production_cost_total: 0,
            production_price_material: 0,
            production_price_labour: 0,
            production_price_total: 0,
            production_profit: 0,
            production_profit_margin_percent: 0,

            sections: []
        };

        for (let sec of sections) {
            const [items] = await pool.query('SELECT pi.*, p.supplier_price FROM project_items pi LEFT JOIN products p ON pi.product_id = p.id WHERE pi.section_id = ? AND pi.is_deleted = 0', [sec.id]);
            
            if (role === 'contractor') {
                items.forEach(item => {
                    const bid = bidMap.get(item.id);
                    if (bid) {
                        item.price_material = bid.material_rate;
                        item.price_labour = bid.labour_rate;
                    } else {
                        item.price_material = 0;
                        item.price_labour = 0;
                    }
                    item.discount = 0;
                });
            }

            let secSummary = {
                id: sec.id,
                name: sec.name,
                spec_name: sec.spec_name,
                has_spec: sec.has_spec || 0,
                profit_percent: parseFloat(sec.profit_percent) || 0,
                cost_material: 0,
                cost_labour: 0,
                cost_total: 0,
                price_material: 0,
                price_labour: 0,
                price_total: 0,
                profit: 0,
                profit_margin_percent: 0,

                // Production fields
                production_cost_material: 0,
                production_cost_labour: 0,
                production_cost_total: 0,
                production_price_material: 0,
                production_price_labour: 0,
                production_price_total: 0,
                production_profit: 0,
                production_profit_margin_percent: 0,

                items_count: items.length
            };

            items.forEach(item => {
                const qty = parseFloat(item.qty) || 0;
                const isFree = item.is_free_issue === 1;
                const costMat = isFree ? 0 : (parseFloat(item.cost_material) || 0) * qty;
                const costLab = (parseFloat(item.cost_labour) || 0) * qty;
                
                const factor = 1 - (parseFloat(item.discount) || 0) / 100;
                const priceMat = isFree ? 0 : (parseFloat(item.price_material) || 0) * qty * factor;
                const priceLab = (parseFloat(item.price_labour) || 0) * qty * factor;

                secSummary.cost_material += costMat;
                secSummary.cost_labour += costLab;
                secSummary.price_material += priceMat;
                secSummary.price_labour += priceLab;

                // Production calculations with inheritance fallback
                const prodQty = item.production_qty !== null ? parseFloat(item.production_qty) : qty;
                const prodCostMat = isFree ? 0 : (item.production_cost_material !== null ? parseFloat(item.production_cost_material) : (mode === 'product-supply' ? (parseFloat(item.supplier_price) || 0) : (parseFloat(item.cost_material) || 0))) * prodQty;
                const prodCostLab = (item.production_cost_labour !== null ? parseFloat(item.production_cost_labour) : (parseFloat(item.cost_labour) || 0)) * prodQty;
                
                const prodPriceMat = isFree ? 0 : (item.production_price_material !== null ? parseFloat(item.production_price_material) : (parseFloat(item.price_material) || 0)) * prodQty * factor;
                const prodPriceLab = (item.production_price_labour !== null ? parseFloat(item.production_price_labour) : (parseFloat(item.price_labour) || 0)) * prodQty * factor;

                secSummary.production_cost_material += prodCostMat;
                secSummary.production_cost_labour += prodCostLab;
                secSummary.production_price_material += prodPriceMat;
                secSummary.production_price_labour += prodPriceLab;
            });

            secSummary.cost_total = secSummary.cost_material + secSummary.cost_labour;
            secSummary.price_total = secSummary.price_material + secSummary.price_labour;
            secSummary.profit = secSummary.price_total - secSummary.cost_total;
            secSummary.profit_margin_percent = secSummary.cost_total > 0 ? (secSummary.profit / secSummary.cost_total) * 100 : 0;

            secSummary.production_cost_total = secSummary.production_cost_material + secSummary.production_cost_labour;
            secSummary.production_price_total = secSummary.production_price_material + secSummary.production_price_labour;
            secSummary.production_profit = secSummary.production_price_total - secSummary.production_cost_total;
            secSummary.production_profit_margin_percent = secSummary.production_cost_total > 0 ? (secSummary.production_profit / secSummary.production_cost_total) * 100 : 0;

            projectSummary.cost_material += secSummary.cost_material;
            projectSummary.cost_labour += secSummary.cost_labour;
            projectSummary.price_material += secSummary.price_material;
            projectSummary.price_labour += secSummary.price_labour;

            projectSummary.production_cost_material += secSummary.production_cost_material;
            projectSummary.production_cost_labour += secSummary.production_cost_labour;
            projectSummary.production_price_material += secSummary.production_price_material;
            projectSummary.production_price_labour += secSummary.production_price_labour;

            if (role !== 'superadmin' && role !== 'excusive' && role !== 'admin') {
                secSummary.cost_material = 0;
                secSummary.cost_labour = 0;
                secSummary.cost_total = 0;
                secSummary.profit = 0;
                secSummary.profit_margin_percent = 0;
                secSummary.production_cost_material = 0;
                secSummary.production_cost_labour = 0;
                secSummary.production_cost_total = 0;
                secSummary.production_profit = 0;
                secSummary.production_profit_margin_percent = 0;
            }

            // Map and attach detailed items for frontend reporting
            secSummary.items = items.map(item => {
                const qty = parseFloat(item.qty) || 0;
                const factor = 1 - (parseFloat(item.discount) || 0) / 100;
                const priceMat = item.is_free_issue === 1 ? 0 : (parseFloat(item.price_material) || 0) * qty * factor;
                const priceLab = (parseFloat(item.price_labour) || 0) * qty * factor;
                const priceTotal = priceMat + priceLab;
                return {
                    id: item.id,
                    description: item.description,
                    brand: item.brand,
                    qty: qty,
                    unit: item.unit || 'Pcs',
                    price_material: item.price_material || 0,
                    price_labour: item.price_labour || 0,
                    price_total: priceTotal
                };
            });

            projectSummary.sections.push(secSummary);
        }

        projectSummary.cost_total = projectSummary.cost_material + projectSummary.cost_labour;
        projectSummary.price_total = projectSummary.price_material + projectSummary.price_labour;
        projectSummary.profit = projectSummary.price_total - projectSummary.cost_total;
        projectSummary.profit_margin_percent = projectSummary.cost_total > 0 ? (projectSummary.profit / projectSummary.cost_total) * 100 : 0;

        projectSummary.production_cost_total = projectSummary.production_cost_material + projectSummary.production_cost_labour;
        projectSummary.production_price_total = projectSummary.production_price_material + projectSummary.production_price_labour;
        projectSummary.production_profit = projectSummary.production_price_total - projectSummary.production_cost_total;
        projectSummary.production_profit_margin_percent = projectSummary.production_cost_total > 0 ? (projectSummary.production_profit / projectSummary.production_cost_total) * 100 : 0;

        if (role !== 'superadmin' && role !== 'excusive' && role !== 'admin') {
            projectSummary.cost_material = 0;
            projectSummary.cost_labour = 0;
            projectSummary.cost_total = 0;
            projectSummary.profit = 0;
            projectSummary.profit_margin_percent = 0;
            projectSummary.production_cost_material = 0;
            projectSummary.production_cost_labour = 0;
            projectSummary.production_cost_total = 0;
            projectSummary.production_profit = 0;
            projectSummary.production_profit_margin_percent = 0;
        }

        res.json(projectSummary);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve project summary' });
    }
});

// ==========================================
// 6. PROJECT APPROVAL STATUS ENDPOINTS
// ==========================================

// Approve project status
app.post('/api/projects/:id/approve', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    // Supervisor and admin can approve
    if (role !== 'admin' && role !== 'supervisor' && role !== 'estimate') {
        return res.status(403).json({ error: 'Only admins and supervisors can approve projects' });
    }

    try {
        // For supervisor, check if they own the project
        if (role === 'supervisor' || role === 'estimate') {
            const [projects] = await pool.query('SELECT created_by FROM projects WHERE id = ?', [id]);
            if (projects.length === 0) {
                return res.status(404).json({ error: 'Project not found' });
            }
            if (projects[0].created_by !== parseInt(userId)) {
                return res.status(403).json({ error: 'You do not have permission to approve this project' });
            }
        }

        await pool.query("UPDATE projects SET status = 'อนุมัติแล้ว' WHERE id = ?", [id]);
        res.json({ success: true, message: 'Project approved successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to approve project' });
    }
});

// Set project status to Pending
app.post('/api/projects/:id/pending', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;

    if (role !== 'admin' && role !== 'supervisor' && role !== 'estimate') {
        return res.status(403).json({ error: 'Only admins and supervisors can modify project status' });
    }

    try {
        if (role === 'supervisor' || role === 'estimate') {
            const [projects] = await pool.query('SELECT created_by FROM projects WHERE id = ?', [id]);
            if (projects.length === 0) {
                return res.status(404).json({ error: 'Project not found' });
            }
            if (projects[0].created_by !== parseInt(userId)) {
                return res.status(403).json({ error: 'You do not have permission to modify this project' });
            }
        }

        await pool.query("UPDATE projects SET status = 'รอดำเนินการ' WHERE id = ?", [id]);
        res.json({ success: true, message: 'Project status set to pending' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update project status' });
    }
});

// Change project status directly
app.put('/api/projects/:id/status', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const { status } = req.body;
    const role = req.user.role;

    const allowedRoles = ['admin', 'superadmin', 'supervisor', 'estimate', 'production', 'procurment', 'sale', 'excusive', 'stock'];
    if (!allowedRoles.includes(role)) {
        return res.status(403).json({ error: 'You do not have permission to change the status of this project' });
    }

    try {
        await pool.query("UPDATE projects SET status = ? WHERE id = ?", [status, id]);
        
        // Log activity
        await pool.query('INSERT INTO activity_logs (user_id, action, project_id, details) VALUES (?, ?, ?, ?)',
            [req.user.id, 'update_project_status', id, `Changed project status to: ${status}`]);

        res.json({ success: true, status });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update project status' });
    }
});


// Get project drawing file
app.get('/api/projects/:id/drawing', authenticateToken, async (req, res) => {
    try {
        const { id } = req.params;
        const [rows] = await pool.query('SELECT drawing_path, drawing_name FROM projects WHERE id = ?', [id]);
        if (rows.length === 0 || !rows[0].drawing_path) {
            return res.status(404).send('Drawing file not found');
        }

        const name = rows[0].drawing_name || 'drawing.pdf';
        sendDiskFile(res, rows[0].drawing_path, name, getMimeType(name));
    } catch (err) {
        console.error(err);
        res.status(500).send('Server Error');
    }
});

// Get project spec file
app.get('/api/projects/:id/spec', authenticateToken, async (req, res) => {
    try {
        const { id } = req.params;
        const [rows] = await pool.query('SELECT spec_path, spec_name FROM projects WHERE id = ?', [id]);
        if (rows.length === 0 || !rows[0].spec_path) {
            return res.status(404).send('Specification file not found');
        }

        const name = rows[0].spec_name || 'spec.pdf';
        sendDiskFile(res, rows[0].spec_path, name, getMimeType(name));
    } catch (err) {
        console.error(err);
        res.status(500).send('Server Error');
    }
});

// ==========================================
// 6.5. PROJECT DOCUMENTS & FOLDERS ENDPOINTS
// ==========================================

// Get project folders and documents
app.get('/api/projects/:id/documents', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        const { id } = req.params;
        
        // Retrieve folder paths
        const [folders] = await pool.query('SELECT id, folder_path FROM project_folders WHERE project_id = ? ORDER BY folder_path', [id]);
        
        // Retrieve documents metadata (exclude raw BLOB file_data to save memory/bandwidth)
        const [files] = await pool.query('SELECT id, project_id, file_name, file_path, file_type, file_size, uploaded_by, uploaded_at, doc_level FROM project_documents WHERE project_id = ? ORDER BY id DESC', [id]);
        
        // Fetch username
        const [users] = await pool.query('SELECT username FROM users WHERE id = ?', [userId]);
        const username = users.length > 0 ? users[0].username : '';

        // Filter files based on user access level
        const visibleFiles = files.filter(file => {
            const level = file.doc_level || 'internal';
            if (level === 'public') {
                return true;
            }
            if (level === 'internal') {
                return role !== 'contractor';
            }
            if (level === 'confidential') {
                return ['admin', 'superadmin', 'supervisor', 'excusive', 'estimate'].includes(role) || file.uploaded_by === username;
            }
            if (level === 'executive') {
                return ['superadmin', 'excusive'].includes(role) || file.uploaded_by === username;
            }
            return false;
        });

        res.json({ folders, files: visibleFiles });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve project documents' });
    }
});

// Create project virtual folder
app.post('/api/projects/:id/folders', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const { folder_path } = req.body;
    const userId = req.user.id;
    const role = req.user.role;

    const hasAccess = await checkProjectWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to modify this project' });
    }

    if (!folder_path || folder_path === '/' || !folder_path.startsWith('/')) {
        return res.status(400).json({ error: 'Invalid folder path' });
    }

    try {
        await pool.query('INSERT INTO project_folders (project_id, folder_path) VALUES (?, ?)', [id, folder_path]);
        await logActivity(userId, null, 'FOLDER_CREATE', `Created virtual folder "${folder_path}" in Project ID ${id}`);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to create virtual folder' });
    }
});

// Delete project virtual folder (and all files inside)
app.delete('/api/projects/:id/folders', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const { path } = req.query;
    const userId = req.user.id;
    const role = req.user.role;

    const hasAccess = await checkProjectWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to modify this project' });
    }

    if (!path || path === '/') {
        return res.status(400).json({ error: 'Cannot delete root folder' });
    }

    try {
        // Delete folder record
        await pool.query('DELETE FROM project_folders WHERE project_id = ? AND folder_path = ?', [id, path]);

        // Delete any subfolders that start with this path (e.g. /Drawings/CAD if we delete /Drawings)
        await pool.query('DELETE FROM project_folders WHERE project_id = ? AND folder_path LIKE ?', [id, path + '/%']);

        // Delete all files residing in this folder or subfolders (and their files on disk)
        const [docsToDelete] = await pool.query('SELECT file_disk_path FROM project_documents WHERE project_id = ? AND (file_path = ? OR file_path LIKE ?)', [id, path, path + '/%']);
        await pool.query('DELETE FROM project_documents WHERE project_id = ? AND (file_path = ? OR file_path LIKE ?)', [id, path, path + '/%']);
        docsToDelete.forEach(doc => deleteDiskFile(doc.file_disk_path));

        await logActivity(userId, null, 'FOLDER_DELETE', `Deleted virtual folder "${path}" in Project ID ${id}`);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete folder' });
    }
});

// Upload document to project folder
app.post('/api/projects/:id/documents', authenticateToken, upload.single('document'), async (req, res) => {
    const { id } = req.params;
    const { file_path } = req.body;
    const userId = req.user.id;
    const role = req.user.role;

    const hasAccess = await checkProjectWriteAccess(id, userId, role);
    if (!hasAccess) {
        return res.status(403).json({ error: 'You do not have permission to upload files to this project' });
    }

    try {
        if (!req.file) {
            return res.status(400).json({ error: 'No file uploaded' });
        }

        const fileName = Buffer.from(req.file.originalname, 'latin1').toString('utf8');
        const fileType = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
        const fileSize = req.file.size;
        const targetPath = file_path || '/';
        const fileDiskPath = saveBufferToDisk('documents', req.file.buffer, fileName);

        // Retrieve username for log
        const [users] = await pool.query('SELECT username FROM users WHERE id = ?', [userId]);
        const username = users.length > 0 ? users[0].username : 'Unknown';

        await pool.query(
            'INSERT INTO project_documents (project_id, file_name, file_path, file_type, file_size, file_data, uploaded_by, file_disk_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
            [id, fileName, targetPath, fileType, fileSize, Buffer.alloc(0), username, fileDiskPath]
        );

        await logActivity(userId, null, 'DOCUMENT_UPLOAD', `Uploaded document "${fileName}" to folder "${targetPath}" in Project ID ${id}`);
        
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to upload document' });
    }
});

// Download/View document file
app.get('/api/documents/:id', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        const { id } = req.params;
        const [rows] = await pool.query('SELECT file_name, file_type, file_disk_path, doc_level, uploaded_by FROM project_documents WHERE id = ?', [id]);

        if (rows.length === 0 || !rows[0].file_disk_path) {
            return res.status(404).send('Document not found');
        }

        const doc = rows[0];
        const level = doc.doc_level || 'internal';

        // Fetch username
        const [users] = await pool.query('SELECT username FROM users WHERE id = ?', [userId]);
        const username = users.length > 0 ? users[0].username : '';

        // Check level permission
        let allowed = false;
        if (level === 'public') {
            allowed = true;
        } else if (level === 'internal') {
            allowed = role !== 'contractor';
        } else if (level === 'confidential') {
            allowed = ['admin', 'superadmin', 'supervisor', 'excusive', 'estimate'].includes(role) || doc.uploaded_by === username;
        } else if (level === 'executive') {
            allowed = ['superadmin', 'excusive'].includes(role) || doc.uploaded_by === username;
        }

        if (!allowed) {
            return res.status(403).send('Forbidden: You do not have permission to view or download this document');
        }

        sendDiskFile(res, doc.file_disk_path, doc.file_name, getMimeType(doc.file_name));
    } catch (err) {
        console.error(err);
        res.status(500).send('Error retrieving document');
    }
});

// Update document level classification
app.put('/api/documents/:id/level', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const { doc_level } = req.body;
    const userId = req.user.id;
    const role = req.user.role;

    if (!doc_level) {
        return res.status(400).json({ error: 'doc_level is required' });
    }

    try {
        const [docs] = await pool.query('SELECT uploaded_by, file_name, project_id FROM project_documents WHERE id = ?', [id]);
        if (docs.length === 0) {
            return res.status(404).json({ error: 'Document not found' });
        }

        const doc = docs[0];
        const [users] = await pool.query('SELECT username FROM users WHERE id = ?', [userId]);
        const username = users.length > 0 ? users[0].username : '';

        // Check permission: admin, superadmin, or uploader
        if (role !== 'admin' && role !== 'superadmin' && doc.uploaded_by !== username) {
            return res.status(403).json({ error: 'Only administrators or the uploader can adjust the document level' });
        }

        await pool.query('UPDATE project_documents SET doc_level = ? WHERE id = ?', [doc_level, id]);
        
        await logActivity(userId, null, 'DOCUMENT_LEVEL_UPDATE', `Changed document "${doc.file_name}" level to "${doc_level}" in Project ID ${doc.project_id}`);

        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update document level' });
    }
});

// Delete document file
app.delete('/api/documents/:id', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const userId = req.user.id;
    const { id } = req.params;

    try {
        const [docs] = await pool.query('SELECT project_id, file_name, file_disk_path, uploaded_by FROM project_documents WHERE id = ?', [id]);
        if (docs.length === 0) {
            return res.status(404).json({ error: 'Document not found' });
        }

        const doc = docs[0];
        const projectId = doc.project_id;
        const fileName = doc.file_name;

        // Only allow uploader or admin/superadmin to delete the file
        const isAdmin = ['admin', 'superadmin'].includes(role);
        const isUploader = doc.uploaded_by === req.user.username;

        if (!isAdmin && !isUploader) {
            return res.status(403).json({ error: 'You do not have permission to delete this document' });
        }

        await pool.query('DELETE FROM project_documents WHERE id = ?', [id]);
        deleteDiskFile(docs[0].file_disk_path);
        await logActivity(userId, null, 'DOCUMENT_DELETE', `Deleted document "${fileName}" from Project ID ${projectId}`);
        
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete document' });
    }
});

// ==========================================
// PROJECT FAT TEST SCRIPT ENDPOINTS
// ==========================================

// Get all Test Scripts (linked 1-to-1 to FAT reports) for a project
app.get('/api/projects/:id/test-scripts', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        const [rows] = await pool.query(`
            SELECT r.id, r.report_type, r.created_at,
                   (SELECT COUNT(*) FROM project_fat_test_script_files f WHERE f.fat_report_id = r.id) AS file_count
            FROM project_fat_reports r
            WHERE r.project_id = ? AND r.is_deleted = 0
            ORDER BY r.created_at DESC
        `, [id]);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve Test Scripts' });
    }
});

// Get all files attached to a specific Test Script (FAT report)
app.get('/api/test-scripts/:reportId/files', authenticateToken, async (req, res) => {
    const { reportId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        // Fetch FAT Report to check project_id
        const [reports] = await pool.query('SELECT project_id FROM project_fat_reports WHERE id = ?', [reportId]);
        if (reports.length === 0) {
            return res.status(404).json({ error: 'Test Script / FAT report not found' });
        }
        const projectId = reports[0].project_id;
        
        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to view files' });
        }
        
        const [files] = await pool.query(`
            SELECT id, file_name, file_type, file_size, uploaded_by, uploaded_at 
            FROM project_fat_test_script_files 
            WHERE fat_report_id = ? 
            ORDER BY id DESC
        `, [reportId]);
        res.json(files);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve files' });
    }
});

// Upload file to a Test Script
app.post('/api/test-scripts/:reportId/files', authenticateToken, upload.single('file'), async (req, res) => {
    const { reportId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    
    try {
        if (!req.file) {
            return res.status(400).json({ error: 'No file uploaded' });
        }
        
        // Fetch FAT Report to check project_id and report_type
        const [reports] = await pool.query('SELECT project_id, report_type FROM project_fat_reports WHERE id = ?', [reportId]);
        if (reports.length === 0) {
            return res.status(404).json({ error: 'Test Script / FAT report not found' });
        }
        const { project_id: projectId, report_type: reportType } = reports[0];
        
        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to upload files' });
        }
        
        const fileName = Buffer.from(req.file.originalname, 'latin1').toString('utf8');
        const fileType = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
        const fileSize = req.file.size;
        const filePath = saveBufferToDisk('test-scripts', req.file.buffer, fileName);

        // Retrieve username for log
        const [users] = await pool.query('SELECT username FROM users WHERE id = ?', [userId]);
        const username = users.length > 0 ? users[0].username : 'Unknown';

        await pool.query(`
            INSERT INTO project_fat_test_script_files
            (project_id, fat_report_id, file_name, file_type, file_size, file_data, uploaded_by, file_path)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        `, [projectId, reportId, fileName, fileType, fileSize, Buffer.alloc(0), username, filePath]);
        
        await logActivity(userId, null, 'TEST_SCRIPT_UPLOAD', `Uploaded test script file "${fileName}" for FAT report "${reportType}" in Project ID ${projectId}`);
        
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to upload file' });
    }
});

// Download/View a specific Test Script file
app.get('/api/test-scripts/files/:fileId', authenticateToken, async (req, res) => {
    const { fileId } = req.params;
    
    try {
        const [rows] = await pool.query('SELECT file_name, file_type, file_path FROM project_fat_test_script_files WHERE id = ?', [fileId]);
        if (rows.length === 0 || !rows[0].file_path) {
            return res.status(404).send('File not found');
        }

        const file = rows[0];
        sendDiskFile(res, file.file_path, file.file_name, getMimeType(file.file_name));
    } catch (err) {
        console.error(err);
        res.status(500).send('Error retrieving file');
    }
});

// Delete a specific Test Script file
app.delete('/api/test-scripts/files/:fileId', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const userId = req.user.id;
    const { fileId } = req.params;

    try {
        const [files] = await pool.query('SELECT project_id, fat_report_id, file_name, file_path FROM project_fat_test_script_files WHERE id = ?', [fileId]);
        if (files.length === 0) {
            return res.status(404).json({ error: 'File not found' });
        }

        const { project_id: projectId, file_name: fileName } = files[0];

        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to delete this file' });
        }

        await pool.query('DELETE FROM project_fat_test_script_files WHERE id = ?', [fileId]);
        deleteDiskFile(files[0].file_path);
        await logActivity(userId, null, 'TEST_SCRIPT_DELETE', `Deleted test script file "${fileName}" from Project ID ${projectId}`);
        
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete file' });
    }
});

// ==========================================
// PROJECT PROGRESS UPDATES ENDPOINTS
// ==========================================

// Get all progress updates for a project (excluding binary image_data for performance)
app.get('/api/projects/:projectId/progress', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const userId = req.user.id;
    const { projectId } = req.params;

    try {
        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to view progress for this project' });
        }

        const [rows] = await pool.query(
            `SELECT p.id, p.project_id, p.image_name, p.image_title, p.timestamp, 
                    p.progress_desc, p.issues, p.solutions, p.suggestions, p.system_name, p.created_by, p.created_at,
                    u.username as creator_name
             FROM project_progress p 
             LEFT JOIN users u ON p.created_by = u.id
             WHERE p.project_id = ? 
             ORDER BY p.timestamp DESC`, 
            [projectId]
        );
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve progress updates' });
    }
});

// View/Serve progress update image
app.get('/api/projects/:projectId/progress/:progressId/image', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const userId = req.user.id;
    const { projectId, progressId } = req.params;

    try {
        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to view this project data' });
        }

        const [rows] = await pool.query('SELECT image_path, image_name FROM project_progress WHERE id = ? AND project_id = ?', [progressId, projectId]);
        if (rows.length === 0 || !rows[0].image_path) {
            return res.status(404).send('Image not found');
        }

        const name = rows[0].image_name || 'image.jpg';
        sendDiskFile(res, rows[0].image_path, name, getMimeType(name));
    } catch (err) {
        console.error(err);
        res.status(500).send('Error retrieving image');
    }
});

// Create progress update
app.post('/api/projects/:projectId/progress', authenticateToken, upload.single('image'), async (req, res) => {
    const role = req.user.role;
    const userId = req.user.id;
    const { projectId } = req.params;
    const { image_title, progress_desc, issues, solutions, suggestions, timestamp, system_name } = req.body;

    try {
        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to add progress for this project' });
        }

        let imagePath = null;
        let imageName = null;
        if (req.file) {
            imageName = Buffer.from(req.file.originalname, 'latin1').toString('utf8');
            imagePath = saveBufferToDisk('progress', req.file.buffer, imageName);
        }

        // Parse custom timestamp or default to current date
        const updateTime = timestamp ? new Date(timestamp) : new Date();

        await pool.query(
            `INSERT INTO project_progress (project_id, image_path, image_name, image_title, timestamp, progress_desc, issues, solutions, suggestions, system_name, created_by)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
            [projectId, imagePath, imageName, image_title || null, updateTime, progress_desc || null, issues || null, solutions || null, suggestions || null, system_name || null, userId]
        );

        await logActivity(userId, null, 'PROGRESS_ADD', `Added progress update for Project ID ${projectId}`);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to create progress update' });
    }
});

// Update progress update
app.put('/api/projects/:projectId/progress/:progressId', authenticateToken, upload.single('image'), async (req, res) => {
    const role = req.user.role;
    const userId = req.user.id;
    const { projectId, progressId } = req.params;
    const { image_title, progress_desc, issues, solutions, suggestions, timestamp, system_name } = req.body;

    try {
        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to edit progress for this project' });
        }

        const updateTime = timestamp ? new Date(timestamp) : new Date();

        if (req.file) {
            // If new image uploaded, update everything including image
            const imageName = Buffer.from(req.file.originalname, 'latin1').toString('utf8');
            const [existing] = await pool.query('SELECT image_path FROM project_progress WHERE id = ?', [progressId]);
            const imagePath = saveBufferToDisk('progress', req.file.buffer, imageName);
            await pool.query(
                `UPDATE project_progress
                 SET image_title = ?, image_path = ?, image_name = ?, timestamp = ?, progress_desc = ?, issues = ?, solutions = ?, suggestions = ?, system_name = ?
                 WHERE id = ? AND project_id = ?`,
                [image_title || null, imagePath, imageName, updateTime, progress_desc || null, issues || null, solutions || null, suggestions || null, system_name || null, progressId, projectId]
            );
            if (existing.length > 0) deleteDiskFile(existing[0].image_path);
        } else {
            // Update fields without changing the existing image
            await pool.query(
                `UPDATE project_progress 
                 SET image_title = ?, timestamp = ?, progress_desc = ?, issues = ?, solutions = ?, suggestions = ?, system_name = ?
                 WHERE id = ? AND project_id = ?`,
                [image_title || null, updateTime, progress_desc || null, issues || null, solutions || null, suggestions || null, system_name || null, progressId, projectId]
            );
        }

        await logActivity(userId, null, 'PROGRESS_EDIT', `Edited progress update ID ${progressId} for Project ID ${projectId}`);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update progress' });
    }
});

// Delete progress update
app.delete('/api/projects/:projectId/progress/:progressId', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const userId = req.user.id;
    const { projectId, progressId } = req.params;

    try {
        const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
        if (!hasAccess) {
            return res.status(403).json({ error: 'You do not have permission to delete progress for this project' });
        }

        const [existing] = await pool.query('SELECT image_path FROM project_progress WHERE id = ? AND project_id = ?', [progressId, projectId]);
        await pool.query('DELETE FROM project_progress WHERE id = ? AND project_id = ?', [progressId, projectId]);
        if (existing.length > 0) deleteDiskFile(existing[0].image_path);
        await logActivity(userId, null, 'PROGRESS_DELETE', `Deleted progress update ID ${progressId} from Project ID ${projectId}`);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete progress' });
    }
});

// ==========================================
// 7. ADMIN USER & PERMISSION MANAGEMENT ENDPOINTS
// ==========================================

// Get all users
app.get('/api/users', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (role !== 'admin' && role !== 'superadmin') {
        return res.status(403).json({ error: 'Only administrators can access user management' });
    }
    try {
        let queryStr = 'SELECT id, username, role, is_active, created_at, plain_password FROM users';
        let queryParams = [];
        if (role !== 'superadmin') {
            queryStr += ' WHERE role != ?';
            queryParams.push('superadmin');
        }
        queryStr += ' ORDER BY username ASC';
        const [users] = await pool.query(queryStr, queryParams);
        
        // Enrich users with project access list
        for (let user of users) {
            if (user.role === 'superadmin' || user.role === 'excusive') {
                user.projectAccess = ['All Projects'];
            } else {
                // Find projects created by this user OR shared with them
                const [projects] = await pool.query(`
                    SELECT DISTINCT p.name FROM projects p
                    LEFT JOIN project_permissions pp ON p.id = pp.project_id
                    WHERE p.created_by = ? OR pp.user_id = ?
                `, [user.id, user.id]);
                user.projectAccess = projects.map(p => p.name);
            }
        }
        res.json(users);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve users' });
    }
});

// Create new user
app.post('/api/users', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (role !== 'admin' && role !== 'superadmin') {
        return res.status(403).json({ error: 'Only administrators can create users' });
    }
    const { username, password, userRole } = req.body;
    if (!username || !password || !userRole) {
        return res.status(400).json({ error: 'Please provide all required fields' });
    }
    // Password policy enforcement
    const pwError = validatePassword(password);
    if (pwError) {
        return res.status(400).json({ error: pwError });
    }
    if (!ASSIGNABLE_ROLES.includes(userRole)) {
        return res.status(400).json({ error: 'Invalid role specified' });
    }
    if (userRole === 'superadmin') {
        return res.status(400).json({ error: 'There can only be one Super Admin' });
    }
    try {
        // Check if username exists
        const [existing] = await pool.query('SELECT id FROM users WHERE username = ?', [username]);
        if (existing.length > 0) {
            return res.status(400).json({ error: 'Username already exists' });
        }
        const hashedPassword = await bcrypt.hash(password, BCRYPT_ROUNDS);
        await pool.query('INSERT INTO users (username, password, role, plain_password) VALUES (?, ?, ?, ?)', [username, hashedPassword, userRole, password]);
        res.status(201).json({ success: true, message: 'User created successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to create user' });
    }
});

// Change my own password (self-service)
app.put('/api/users/me/password', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const { currentPassword, newPassword } = req.body;

    const pwError = validatePassword(newPassword);
    if (pwError) {
        return res.status(400).json({ error: pwError });
    }

    try {
        const [rows] = await pool.query('SELECT password FROM users WHERE id = ?', [userId]);
        if (rows.length === 0) {
            return res.status(404).json({ error: 'User not found' });
        }
        const validPassword = await bcrypt.compare(currentPassword || '', rows[0].password);
        if (!validPassword) {
            // Use 400 (not 401) so authFetch's global "session expired" handler doesn't force a logout
            return res.status(400).json({ error: 'Current password is incorrect' });
        }

        const hashedPassword = await bcrypt.hash(newPassword, BCRYPT_ROUNDS);
        await pool.query('UPDATE users SET password = ? WHERE id = ?', [hashedPassword, userId]);
        await logActivity(userId, null, 'PASSWORD_CHANGE', 'User changed their own password');
        res.json({ success: true, message: 'Password updated successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update password' });
    }
});

// Reset user password by Admin/Super Admin
app.put('/api/users/:id/password', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const { id } = req.params;
    const { newPassword } = req.body;

    if (role !== 'admin' && role !== 'superadmin') {
        return res.status(403).json({ error: 'Only administrators can reset user passwords' });
    }
    if (!newPassword) {
        return res.status(400).json({ error: 'Please provide a new password' });
    }
    const pwError = validatePassword(newPassword);
    if (pwError) {
        return res.status(400).json({ error: pwError });
    }

    try {
        const [targetUser] = await pool.query('SELECT role FROM users WHERE id = ?', [id]);
        if (targetUser.length === 0) {
            return res.status(404).json({ error: 'User not found' });
        }
        if (targetUser[0].role === 'superadmin' && req.user.role !== 'superadmin') {
            return res.status(403).json({ error: 'Only Super Admin can reset Super Admin password' });
        }

        const hashedPassword = await bcrypt.hash(newPassword, BCRYPT_ROUNDS);
        await pool.query('UPDATE users SET password = ?, plain_password = ? WHERE id = ?', [hashedPassword, newPassword, id]);
        await logActivity(req.user.id, null, 'USER_PASSWORD_RESET', `Admin reset password for user ID ${id}`);
        res.json({ success: true, message: 'User password reset successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to reset user password' });
    }
});

// Update user's role
app.put('/api/users/:id/role', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const currentUserId = req.user.id;
    const { id } = req.params;
    const { userRole } = req.body;

    if (role !== 'admin' && role !== 'superadmin') {
        return res.status(403).json({ error: 'Only administrators can modify roles' });
    }
    if (parseInt(id) === parseInt(currentUserId)) {
        return res.status(400).json({ error: 'You cannot change your own role to prevent lockout' });
    }
    if (userRole === 'superadmin') {
        return res.status(400).json({ error: 'There can only be one Super Admin' });
    }

    try {
        const [targetUser] = await pool.query('SELECT role FROM users WHERE id = ?', [id]);
        if (targetUser.length > 0 && targetUser[0].role === 'superadmin') {
            return res.status(400).json({ error: 'Super Admin role cannot be modified' });
        }
        
        if (!ASSIGNABLE_ROLES.includes(userRole)) {
            return res.status(400).json({ error: 'Invalid role specified' });
        }

        await pool.query('UPDATE users SET role = ? WHERE id = ?', [userRole, id]);
        res.json({ success: true, message: 'User role updated successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update user role' });
    }
});

// Activate / deactivate a user account
app.put('/api/users/:id/status', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const currentUserId = req.user.id;
    const { id } = req.params;
    const { is_active } = req.body;

    if (role !== 'admin' && role !== 'superadmin') {
        return res.status(403).json({ error: 'Only administrators can modify account status' });
    }
    if (parseInt(id) === parseInt(currentUserId)) {
        return res.status(400).json({ error: 'You cannot deactivate your own account' });
    }

    try {
        const [targetUser] = await pool.query('SELECT role FROM users WHERE id = ?', [id]);
        if (targetUser.length === 0) {
            return res.status(404).json({ error: 'User not found' });
        }
        if (targetUser[0].role === 'superadmin') {
            return res.status(400).json({ error: 'Super Admin status cannot be modified' });
        }

        await pool.query('UPDATE users SET is_active = ? WHERE id = ?', [is_active ? 1 : 0, id]);
        await logActivity(currentUserId, null, 'USER_STATUS_CHANGE', `${is_active ? 'Activated' : 'Deactivated'} user ID ${id}`);
        res.json({ success: true, message: 'User status updated successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update user status' });
    }
});


// Delete user account
app.delete('/api/users/:id', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const currentUserId = req.user.id;
    const { id } = req.params;

    if (role !== 'admin' && role !== 'superadmin') {
        return res.status(403).json({ error: 'Only administrators can delete user accounts' });
    }
    if (parseInt(id) === parseInt(currentUserId)) {
        return res.status(400).json({ error: 'You cannot delete your own account' });
    }

    try {
        const [targetUser] = await pool.query('SELECT role FROM users WHERE id = ?', [id]);
        if (targetUser.length > 0 && targetUser[0].role === 'superadmin') {
            return res.status(400).json({ error: 'Super Admin account cannot be deleted' });
        }
        
        const connection = await pool.getConnection();
        await connection.beginTransaction();
        
        try {
            // Get contractor IDs for the user
            const [contractors] = await connection.query('SELECT id FROM contractors WHERE user_id = ?', [id]);
            const contractorIds = contractors.map(c => c.id);
            
            if (contractorIds.length > 0) {
                // Delete contractor complaints
                await connection.query('DELETE FROM contractor_complaints WHERE contractor_id IN (?)', [contractorIds]);
                
                // Delete contractor invoices
                await connection.query('DELETE FROM contractor_invoices WHERE contractor_id IN (?)', [contractorIds]);
                
                // Delete contractor deduct works
                await connection.query('DELETE FROM contractor_deduct_works WHERE contractor_id IN (?)', [contractorIds]);
                
                // Delete contractor extra works
                await connection.query('DELETE FROM contractor_extra_works WHERE contractor_id IN (?)', [contractorIds]);
                
                // Delete RFQ items
                await connection.query('DELETE FROM contractor_rfq_items WHERE rfq_id IN (SELECT id FROM contractor_rfqs WHERE contractor_id IN (?))', [contractorIds]);
                
                // Delete RFQs
                await connection.query('DELETE FROM contractor_rfqs WHERE contractor_id IN (?)', [contractorIds]);
                
                // Delete contractors
                await connection.query('DELETE FROM contractors WHERE user_id = ?', [id]);
            }
            
            // Delete user project permissions
            await connection.query('DELETE FROM project_permissions WHERE user_id = ?', [id]);
            
            // Delete activity logs
            await connection.query('DELETE FROM activity_logs WHERE user_id = ?', [id]);
            
            // Delete announcement acknowledgements
            await connection.query('DELETE FROM announcement_acks WHERE user_id = ?', [id]);
            
            // Delete chat messages
            await connection.query('DELETE FROM chat_messages WHERE sender_id = ?', [id]);
            
            // Delete chat participants
            await connection.query('DELETE FROM chat_participants WHERE user_id = ?', [id]);
            
            // Delete project progress updates
            await connection.query('DELETE FROM project_progress WHERE created_by = ?', [id]);
            
            // Delete project revisions
            await connection.query('DELETE FROM project_revisions WHERE user_id = ?', [id]);
            
            // Finally delete the user
            await connection.query('DELETE FROM users WHERE id = ?', [id]);
            
            await connection.commit();
            res.json({ success: true, message: 'User account and all related history deleted successfully' });
        } catch (txErr) {
            await connection.rollback();
            throw txErr;
        } finally {
            connection.release();
        }
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete user account' });
    }
});

// Get supervisor user IDs mapped to a project
app.get('/api/project-permissions/:projectId', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const role = req.user.role;
    const { projectId } = req.params;
    try {
        const [proj] = await pool.query('SELECT created_by FROM projects WHERE id = ?', [projectId]);
        if (proj.length === 0) {
            return res.status(404).json({ error: 'Project not found' });
        }
        
        const creatorId = proj[0].created_by;
        if (role !== 'admin' && role !== 'superadmin' && String(creatorId) !== String(userId)) {
            return res.status(403).json({ error: 'Only administrators or the project creator can view project permissions' });
        }

        const [permissions] = await pool.query('SELECT user_id FROM project_permissions WHERE project_id = ?', [projectId]);
        res.json(permissions.map(p => p.user_id));
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve project permissions' });
    }
});

// Overwrite project permissions for supervisors
app.post('/api/project-permissions/:projectId', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const role = req.user.role;
    const { projectId } = req.params;
    const { supervisorIds } = req.body; // Array of user IDs
    
    if (!Array.isArray(supervisorIds)) {
        return res.status(400).json({ error: 'supervisorIds must be an array' });
    }

    try {
        const [proj] = await pool.query('SELECT created_by FROM projects WHERE id = ?', [projectId]);
        if (proj.length === 0) {
            return res.status(404).json({ error: 'Project not found' });
        }
        
        const creatorId = proj[0].created_by;
        if (role !== 'admin' && role !== 'superadmin' && String(creatorId) !== String(userId)) {
            return res.status(403).json({ error: 'Only administrators or the project creator can modify project permissions' });
        }

        // Run in transaction to ensure consistency
        const connection = await pool.getConnection();
        await connection.beginTransaction();

        try {
            // Delete old permissions
            await connection.query('DELETE FROM project_permissions WHERE project_id = ?', [projectId]);
            
            // Insert new permissions
            if (supervisorIds.length > 0) {
                const values = supervisorIds.map(uId => [projectId, uId]);
                await connection.query('INSERT INTO project_permissions (project_id, user_id) VALUES ?', [values]);
            }

            await connection.commit();
            res.json({ success: true, message: 'Project permissions updated successfully' });
        } catch (txErr) {
            await connection.rollback();
            throw txErr;
        } finally {
            connection.release();
        }
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update project permissions' });
    }
});

// Get activity log (login/logout + edit history).
// Admin/superadmin/excusive see everyone's activity; everyone else sees only their own.
app.get('/api/activity-logs', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const role = req.user.role;
    const canViewAll = ['admin', 'superadmin', 'excusive'].includes(role);

    const page = Math.max(parseInt(req.query.page) || 1, 1);
    const limit = Math.min(parseInt(req.query.limit) || 50, 200);
    const offset = (page - 1) * limit;
    const search = (req.query.search || '').trim();
    const dateFrom = (req.query.date_from || '').trim();
    const dateTo = (req.query.date_to || '').trim();

    try {
        const whereClauses = [
            '(project_id IS NULL OR project_id IN (SELECT id FROM projects WHERE is_deleted = 0))'
        ];
        const params = [];

        if (!canViewAll) {
            whereClauses.push('user_id = ?');
            params.push(userId);
        }
        if (search) {
            whereClauses.push('(username LIKE ? OR action LIKE ? OR details LIKE ?)');
            params.push(`%${search}%`, `%${search}%`, `%${search}%`);
        }
        if (dateFrom) {
            whereClauses.push('created_at >= ?');
            params.push(`${dateFrom} 00:00:00`);
        }
        if (dateTo) {
            whereClauses.push('created_at <= ?');
            params.push(`${dateTo} 23:59:59`);
        }
        const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';

        const [countRows] = await pool.query(`SELECT COUNT(*) AS total FROM activity_logs ${whereSql}`, params);
        const total = countRows[0].total;

        const [rows] = await pool.query(
            `SELECT id, user_id, username, action, details, created_at FROM activity_logs ${whereSql} ORDER BY id DESC LIMIT ? OFFSET ?`,
            [...params, limit, offset]
        );

        res.json({ logs: rows, total, page, limit });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve activity logs' });
    }
});

// ==========================================
// ANNOUNCEMENTS ENDPOINTS
// ==========================================

// Get active announcements the current user hasn't acknowledged yet
app.get('/api/announcements/pending', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    try {
        const nowIso = new Date().toISOString().replace('T', ' ').substring(0, 19); // e.g. "2026-07-12 13:30:00"
        const [rows] = await pool.query(`
            SELECT a.id, a.title, a.body, a.type, a.media_path, a.media_type, a.start_at, a.created_at, u.username AS author
            FROM announcements a
            LEFT JOIN users u ON a.created_by = u.id
            WHERE a.is_active = 1
              AND (a.start_at IS NULL OR a.start_at <= ?)
              AND NOT EXISTS (SELECT 1 FROM announcement_acks ak WHERE ak.announcement_id = a.id AND ak.user_id = ?)
            ORDER BY a.created_at ASC
        `, [nowIso, userId]);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve pending announcements' });
    }
});

// Acknowledge an announcement
app.post('/api/announcements/:id/ack', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const { id } = req.params;
    try {
        await pool.query(
            'INSERT INTO announcement_acks (announcement_id, user_id) VALUES (?, ?) ON CONFLICT(announcement_id, user_id) DO NOTHING',
            [id, userId]
        );
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to acknowledge announcement' });
    }
});

// Org-wide announcements may be created/managed by admins and executives ("ผู้บริหาร")
function canManageAnnouncements(role) {
    return ['admin', 'superadmin', 'excusive'].includes(role);
}

// List all announcements (admin management view, includes ack count)
app.get('/api/announcements', authenticateToken, async (req, res) => {
    const role = req.user.role;
    if (!canManageAnnouncements(role)) {
        return res.status(403).json({ error: 'Only administrators or executives can manage announcements' });
    }
    try {
        const [rows] = await pool.query(`
            SELECT a.id, a.title, a.body, a.type, a.media_path, a.media_type, a.start_at, a.is_active, a.created_at, u.username AS created_by_name,
                   (SELECT COUNT(*) FROM announcement_acks ak WHERE ak.announcement_id = a.id) AS ack_count
            FROM announcements a
            LEFT JOIN users u ON a.created_by = u.id
            ORDER BY a.created_at DESC
        `);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve announcements' });
    }
});

// Create announcement
app.post('/api/announcements', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const userId = req.user.id;
    if (!canManageAnnouncements(role)) {
        return res.status(403).json({ error: 'Only administrators or executives can manage announcements' });
    }
    const { title, body, type, media_path, media_type, start_at } = req.body;
    if (!title || !body) {
        return res.status(400).json({ error: 'Title and body are required' });
    }
    try {
        const formattedStartAt = start_at ? start_at.replace('T', ' ').substring(0, 19) : null;
        const [result] = await pool.query(
            'INSERT INTO announcements (title, body, type, media_path, media_type, start_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)', 
            [title, body, type || 'general', media_path || null, media_type || 'none', formattedStartAt, userId]
        );
        await logActivity(userId, null, 'ANNOUNCEMENT_CREATE', `Created announcement "${title}"`);
        res.status(201).json({ id: result.insertId, title, body });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to create announcement' });
    }
});

// Update announcement
app.put('/api/announcements/:id', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const userId = req.user.id;
    if (!canManageAnnouncements(role)) {
        return res.status(403).json({ error: 'Only administrators or executives can manage announcements' });
    }
    const { id } = req.params;
    const { title, body, type, media_path, media_type, start_at, is_active } = req.body;
    try {
        const formattedStartAt = start_at ? start_at.replace('T', ' ').substring(0, 19) : null;
        await pool.query(
            'UPDATE announcements SET title = ?, body = ?, type = ?, media_path = ?, media_type = ?, start_at = ?, is_active = ? WHERE id = ?',
            [title, body, type || 'general', media_path || null, media_type || 'none', formattedStartAt, is_active ? 1 : 0, id]
        );
        await logActivity(userId, null, 'ANNOUNCEMENT_UPDATE', `Updated announcement ID ${id}`);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to update announcement' });
    }
});

// Upload media for announcements (images or video clips)
app.post('/api/announcements/upload', authenticateToken, upload.single('file'), async (req, res) => {
    const role = req.user.role;
    if (!canManageAnnouncements(role)) {
        return res.status(403).json({ error: 'Only administrators or executives can manage announcements' });
    }
    try {
        if (!req.file) {
            return res.status(400).json({ error: 'No media file uploaded' });
        }
        
        // Save the media buffer to disk using built-in saveBufferToDisk helper
        const filePath = saveBufferToDisk('announcements', req.file.buffer, req.file.originalname);
        
        // Detect media type from mimetype
        let mediaType = 'none';
        if (req.file.mimetype.startsWith('image/')) {
            mediaType = 'image';
        } else if (req.file.mimetype.startsWith('video/')) {
            mediaType = 'video';
        }
        
        res.json({ success: true, filePath, mediaType });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to upload media' });
    }
});

// Delete announcement
app.delete('/api/announcements/:id', authenticateToken, async (req, res) => {
    const role = req.user.role;
    const userId = req.user.id;
    if (!canManageAnnouncements(role)) {
        return res.status(403).json({ error: 'Only administrators or executives can manage announcements' });
    }
    const { id } = req.params;
    try {
        await pool.query('DELETE FROM announcements WHERE id = ?', [id]);
        await logActivity(userId, null, 'ANNOUNCEMENT_DELETE', `Deleted announcement ID ${id}`);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to delete announcement' });
    }
});

// ==========================================
// DEPARTMENT REPORTS ENDPOINTS
// ==========================================

const REPORT_ROLES = {
    'electrical-design': ['admin', 'superadmin', 'excusive', 'design', 'design_manager_elec', 'consultant_elec'],
    'mechanical-design': ['admin', 'superadmin', 'excusive', 'design', 'design_manager_mech', 'consultant_mech'],
    'sales':              ['admin', 'superadmin', 'excusive', 'sale', 'sale_manager'],
    'production':         ['admin', 'superadmin', 'excusive', 'production', 'factory_manager'],
    'qc':                 ['admin', 'superadmin', 'excusive', 'qc', 'qa'],
    'planning':           ['admin', 'superadmin', 'excusive', 'supervisor', 'estimate'],
    'procurement':        ['admin', 'superadmin', 'excusive', 'procurment', 'procurement_manager'],
    'accounting':         ['admin', 'superadmin', 'excusive', 'account_manager'],
    'executive':          ['admin', 'superadmin', 'excusive'],
};

const ELECTRICAL_SYSTEM_KEYWORDS = ['MV SYSTEM', 'LV SYSTEM', 'BUSDUCT', 'LV POWER CABLE', 'UTILITY', 'COMMUNICATION', 'ACCESS CONTROL', 'FIRE ALARM', 'SENSOR'];
const MECHANICAL_SYSTEM_KEYWORDS = ['FIRE SUPPRESSION', 'OFF GAS', 'CRAC', 'AIR CONDITION', 'EXAUST', 'EXHAUST', 'DAMPER', 'DAY TANK', 'PUMPING'];

function checkReportAccess(reportKey, role) {
    const allowedRoles = REPORT_ROLES[reportKey];
    return !!allowedRoles && allowedRoles.includes(role);
}

// Which report tabs the current user is allowed to see
app.get('/api/reports/available', authenticateToken, (req, res) => {
    const role = req.user.role;
    const available = Object.keys(REPORT_ROLES).filter(key => checkReportAccess(key, role));
    res.json({ available });
});

async function getDesignReport(keywords) {
    const [sections] = await pool.query(`
        SELECT p.id AS project_id, p.name AS project_name, p.status AS project_status,
               ps.id AS section_id, ps.name AS section_name, (ps.spec_path IS NOT NULL) AS has_spec,
               COUNT(pi.id) AS item_count
        FROM project_sections ps
        JOIN projects p ON ps.project_id = p.id
        LEFT JOIN project_items pi ON pi.section_id = ps.id AND pi.is_deleted = 0
        WHERE ps.is_deleted = 0 AND COALESCE(p.is_deleted, 0) = 0
        GROUP BY ps.id
        ORDER BY p.name, ps.name
    `);
    const upperKeywords = keywords.map(k => k.toUpperCase());
    const matched = sections.filter(s => upperKeywords.some(kw => (s.section_name || '').toUpperCase().includes(kw)));
    return {
        sections: matched,
        summary: {
            total_sections: matched.length,
            total_projects: new Set(matched.map(s => s.project_id)).size,
            sections_missing_spec: matched.filter(s => !s.has_spec).length
        }
    };
}

// 6.1.1 Electrical design report
app.get('/api/reports/electrical-design', authenticateToken, async (req, res) => {
    if (!checkReportAccess('electrical-design', req.user.role)) {
        return res.status(403).json({ error: 'Not authorized to view this report' });
    }
    try {
        res.json(await getDesignReport(ELECTRICAL_SYSTEM_KEYWORDS));
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to generate electrical design report' });
    }
});

// 6.1.2 Mechanical design report
app.get('/api/reports/mechanical-design', authenticateToken, async (req, res) => {
    if (!checkReportAccess('mechanical-design', req.user.role)) {
        return res.status(403).json({ error: 'Not authorized to view this report' });
    }
    try {
        res.json(await getDesignReport(MECHANICAL_SYSTEM_KEYWORDS));
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to generate mechanical design report' });
    }
});

// Shared cost/price/profit rollup per project (portfolio-wide)
async function getProjectFinancialRollup() {
    const [rows] = await pool.query(`
        SELECT p.id, p.name, p.client_name, p.status, p.created_at,
               COALESCE(SUM(CASE WHEN pi.is_free_issue = 1 THEN 0 ELSE pi.qty * pi.cost_material END), 0) AS cost_material,
               COALESCE(SUM(pi.qty * pi.cost_labour), 0) AS cost_labour,
               COALESCE(SUM(CASE WHEN pi.is_free_issue = 1 THEN 0 ELSE pi.qty * pi.price_material * (1 - pi.discount / 100) END), 0) AS price_material,
               COALESCE(SUM(pi.qty * pi.price_labour * (1 - pi.discount / 100)), 0) AS price_labour
        FROM projects p
        LEFT JOIN project_sections ps ON ps.project_id = p.id AND ps.is_deleted = 0
        LEFT JOIN project_items pi ON pi.section_id = ps.id AND pi.is_deleted = 0
        WHERE COALESCE(p.is_deleted, 0) = 0
        GROUP BY p.id
        ORDER BY p.created_at DESC
    `);
    return rows.map(r => {
        const costTotal = r.cost_material + r.cost_labour;
        const priceTotal = r.price_material + r.price_labour;
        const profit = priceTotal - costTotal;
        return {
            ...r,
            cost_total: costTotal,
            price_total: priceTotal,
            profit,
            profit_margin_percent: costTotal > 0 ? (profit / costTotal) * 100 : 0
        };
    });
}

// 6.2 Sales report
app.get('/api/reports/sales', authenticateToken, async (req, res) => {
    if (!checkReportAccess('sales', req.user.role)) {
        return res.status(403).json({ error: 'Not authorized to view this report' });
    }
    try {
        const projects = await getProjectFinancialRollup();
        const byStatus = {};
        projects.forEach(p => {
            byStatus[p.status] = (byStatus[p.status] || 0) + 1;
        });
        const byClient = {};
        projects.forEach(p => {
            const client = p.client_name || (req.query.lang === 'en' ? 'Unspecified' : 'ไม่ระบุ');
            byClient[client] = (byClient[client] || 0) + p.price_total;
        });
        res.json({
            projects: projects.map(p => ({ id: p.id, name: p.name, client_name: p.client_name, status: p.status, created_at: p.created_at, price_total: p.price_total })),
            summary: {
                total_projects: projects.length,
                total_bid_value: projects.reduce((sum, p) => sum + p.price_total, 0),
                by_status: byStatus,
                top_clients: Object.entries(byClient).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([client, value]) => ({ client, value }))
            }
        });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to generate sales report' });
    }
});

// 6.3 Production report
app.get('/api/reports/production', authenticateToken, async (req, res) => {
    if (!checkReportAccess('production', req.user.role)) {
        return res.status(403).json({ error: 'Not authorized to view this report' });
    }
    try {
        const [updates] = await pool.query(`
            SELECT pp.id, pp.project_id, p.name AS project_name, pp.system_name, pp.progress_desc, pp.issues, pp.created_at, u.username AS creator_name
            FROM project_progress pp
            JOIN projects p ON pp.project_id = p.id
            LEFT JOIN users u ON pp.created_by = u.id
            WHERE COALESCE(p.is_deleted, 0) = 0
            ORDER BY pp.created_at DESC
            LIMIT 100
        `);
        const [staleProjects] = await pool.query(`
            SELECT p.id, p.name, MAX(pp.created_at) AS last_update
            FROM projects p
            LEFT JOIN project_progress pp ON pp.project_id = p.id
            WHERE COALESCE(p.is_deleted, 0) = 0
            GROUP BY p.id
            HAVING last_update IS NULL OR last_update < datetime('now', '-14 days')
        `);
        res.json({
            recent_updates: updates,
            open_issues: updates.filter(u => u.issues),
            stale_projects: staleProjects,
            summary: {
                total_updates: updates.length,
                open_issue_count: updates.filter(u => u.issues).length,
                stale_project_count: staleProjects.length
            }
        });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to generate production report' });
    }
});

// 6.4 QC report
app.get('/api/reports/qc', authenticateToken, async (req, res) => {
    if (!checkReportAccess('qc', req.user.role)) {
        return res.status(403).json({ error: 'Not authorized to view this report' });
    }
    try {
        const [reports] = await pool.query(`
            SELECT fr.id, fr.project_id, p.name AS project_name, fr.report_type, fr.testing_date,
                   COUNT(fi.id) AS item_count,
                   SUM(CASE WHEN fi.result_pass = 1 THEN 1 ELSE 0 END) AS pass_count,
                   SUM(CASE WHEN fi.result_fail = 1 THEN 1 ELSE 0 END) AS fail_count
            FROM project_fat_reports fr
            JOIN projects p ON fr.project_id = p.id
            LEFT JOIN project_fat_items fi ON fi.fat_report_id = fr.id
            WHERE COALESCE(p.is_deleted, 0) = 0
            GROUP BY fr.id
            ORDER BY fr.testing_date DESC
        `);
        const [failedItems] = await pool.query(`
            SELECT fi.id, fr.project_id, p.name AS project_name, fr.report_type, fi.item_no, fi.title, fi.remarks
            FROM project_fat_items fi
            JOIN project_fat_reports fr ON fi.fat_report_id = fr.id
            JOIN projects p ON fr.project_id = p.id
            WHERE fi.result_fail = 1 AND COALESCE(p.is_deleted, 0) = 0
            ORDER BY fr.testing_date DESC
            LIMIT 100
        `);
        res.json({
            reports,
            failed_items: failedItems,
            summary: {
                total_reports: reports.length,
                total_pass: reports.reduce((sum, r) => sum + (r.pass_count || 0), 0),
                total_fail: reports.reduce((sum, r) => sum + (r.fail_count || 0), 0)
            }
        });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to generate QC report' });
    }
});

// 6.5 Planning report (limited: no scheduling/capacity data exists yet)
app.get('/api/reports/planning', authenticateToken, async (req, res) => {
    if (!checkReportAccess('planning', req.user.role)) {
        return res.status(403).json({ error: 'Not authorized to view this report' });
    }
    try {
        const [projects] = await pool.query(`
            SELECT p.id, p.name, p.status, p.created_at,
                   COALESCE((SELECT MAX(revision_number) FROM project_revisions WHERE project_id = p.id), 0) AS current_revision
            FROM projects p
            WHERE COALESCE(p.is_deleted, 0) = 0
            ORDER BY p.created_at DESC
        `);
        const [upcomingRfqs] = await pool.query(`
            SELECT r.id, r.rfq_number, r.due_date, r.status, p.name AS project_name, c.name AS contractor_name
            FROM contractor_rfqs r
            JOIN projects p ON r.project_id = p.id
            JOIN contractors c ON r.contractor_id = c.id
            WHERE r.is_deleted = 0 AND r.due_date IS NOT NULL AND r.status NOT IN ('approved', 'rejected')
            ORDER BY r.due_date ASC
            LIMIT 50
        `);
        const byStatus = {};
        projects.forEach(p => { byStatus[p.status] = (byStatus[p.status] || 0) + 1; });
        res.json({
            projects,
            upcoming_rfqs: upcomingRfqs,
            summary: { total_projects: projects.length, by_status: byStatus },
            note: 'Limited data: no formal scheduling/capacity-planning workflow exists yet, so this uses project status and RFQ due dates as the closest available proxy.'
        });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to generate planning report' });
    }
});

// 6.6 Procurement report
app.get('/api/reports/procurement', authenticateToken, async (req, res) => {
    if (!checkReportAccess('procurement', req.user.role)) {
        return res.status(403).json({ error: 'Not authorized to view this report' });
    }
    try {
        const [rfqs] = await pool.query(`
            SELECT r.id, r.rfq_number, r.status, r.total_amount, r.due_date, p.name AS project_name, c.name AS contractor_name
            FROM contractor_rfqs r
            JOIN projects p ON r.project_id = p.id
            JOIN contractors c ON r.contractor_id = c.id
            WHERE r.is_deleted = 0
            ORDER BY r.created_at DESC
        `);
        const bySpend = {};
        rfqs.forEach(r => { bySpend[r.contractor_name] = (bySpend[r.contractor_name] || 0) + (r.total_amount || 0); });
        const byStatus = {};
        rfqs.forEach(r => { byStatus[r.status] = (byStatus[r.status] || 0) + 1; });
        const [supplierCount] = await pool.query('SELECT COUNT(*) AS total FROM suppliers');
        res.json({
            rfqs,
            summary: {
                total_rfqs: rfqs.length,
                by_status: byStatus,
                spend_by_contractor: Object.entries(bySpend).sort((a, b) => b[1] - a[1]).map(([contractor, total]) => ({ contractor, total })),
                total_suppliers: supplierCount[0].total
            }
        });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to generate procurement report' });
    }
});

// 6.7 Accounting report (portfolio-wide P&L)
app.get('/api/reports/accounting', authenticateToken, async (req, res) => {
    if (!checkReportAccess('accounting', req.user.role)) {
        return res.status(403).json({ error: 'Not authorized to view this report' });
    }
    try {
        const projects = await getProjectFinancialRollup();
        res.json({
            projects,
            summary: {
                total_projects: projects.length,
                total_cost: projects.reduce((sum, p) => sum + p.cost_total, 0),
                total_price: projects.reduce((sum, p) => sum + p.price_total, 0),
                total_profit: projects.reduce((sum, p) => sum + p.profit, 0)
            }
        });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to generate accounting report' });
    }
});

// 6.8 Executive report (top-level KPI rollup)
app.get('/api/reports/executive', authenticateToken, async (req, res) => {
    if (!checkReportAccess('executive', req.user.role)) {
        return res.status(403).json({ error: 'Not authorized to view this report' });
    }
    try {
        const projects = await getProjectFinancialRollup();
        const totalCost = projects.reduce((sum, p) => sum + p.cost_total, 0);
        const totalPrice = projects.reduce((sum, p) => sum + p.price_total, 0);
        const totalProfit = totalPrice - totalCost;
        const [recentActivity] = await pool.query('SELECT id, username, action, details, created_at FROM activity_logs ORDER BY id DESC LIMIT 10');
        const byStatus = {};
        projects.forEach(p => { byStatus[p.status] = (byStatus[p.status] || 0) + 1; });
        res.json({
            summary: {
                total_projects: projects.length,
                total_portfolio_value: totalPrice,
                total_cost: totalCost,
                total_profit: totalProfit,
                overall_margin_percent: totalCost > 0 ? (totalProfit / totalCost) * 100 : 0,
                by_status: byStatus
            },
            recent_activity: recentActivity
        });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to generate executive report' });
    }
});

// ==========================================
// INTERNAL CHAT ENDPOINTS
// ==========================================

// Trimmed user directory - any authenticated user can pick a colleague to message
app.get('/api/users/directory', authenticateToken, async (req, res) => {
    try {
        let queryStr = `SELECT id, username, role, last_active_at,
                    CASE WHEN last_active_at >= datetime('now', '-15 seconds') THEN 1 ELSE 0 END AS is_online
             FROM users WHERE id != ? AND COALESCE(is_active, 1) = 1`;
        let params = [req.user.id];
        if (req.user.role !== 'superadmin') {
            queryStr += " AND role != 'superadmin'";
        }
        queryStr += " ORDER BY username ASC";
        const [users] = await pool.query(queryStr, params);
        res.json(users);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve user directory' });
    }
});

async function checkChatParticipant(conversationId, userId) {
    const [rows] = await pool.query(
        'SELECT 1 FROM chat_participants WHERE conversation_id = ? AND user_id = ?',
        [conversationId, userId]
    );
    return rows.length > 0;
}

// Contractors may only read/send messages in conversations they were added to
// (via the project-chat endpoint below); they cannot start arbitrary 1:1/group
// chats or manage group participants.
app.use('/api/chat', authenticateToken, (req, res, next) => {
    if (req.user && req.user.role === 'contractor') {
        const path = req.path;
        const isAllowed =
            (req.method === 'GET' && path === '/conversations') ||
            (req.method === 'GET' && path === '/unread-count') ||
            (req.method === 'GET' && /^\/conversations\/\d+\/messages$/.test(path)) ||
            (req.method === 'POST' && /^\/conversations\/\d+\/messages$/.test(path)) ||
            (req.method === 'PUT' && /^\/conversations\/\d+\/read$/.test(path)) ||
            (req.method === 'POST' && path === '/upload') ||
            (req.method === 'POST' && path === '/upload-audio');
        if (!isAllowed) {
            return res.status(403).json({ error: 'Contractors are not authorized to use the internal chat' });
        }
    }
    next();
});

// Find-or-create the project chat thread between a contractor and the project's admins
app.post('/api/projects/:projectId/chat/open', authenticateToken, async (req, res) => {
    const { projectId } = req.params;
    const userId = req.user.id;
    const role = req.user.role;
    try {
        let contractorUserId;
        if (role === 'contractor') {
            const [c] = await pool.query('SELECT id FROM contractors WHERE user_id = ? AND is_deleted = 0', [userId]);
            if (c.length === 0) {
                return res.status(403).json({ error: 'Contractor profile not found' });
            }
            const [rfq] = await pool.query('SELECT 1 FROM contractor_rfqs WHERE project_id = ? AND contractor_id = ? AND is_deleted = 0 LIMIT 1', [projectId, c[0].id]);
            if (rfq.length === 0) {
                return res.status(403).json({ error: 'You do not have a relationship with this project' });
            }
            contractorUserId = userId;
        } else {
            const hasAccess = await checkProjectWriteAccess(projectId, userId, role);
            if (!hasAccess) {
                return res.status(403).json({ error: 'You do not have permission to chat about this project' });
            }
            const { contractor_id } = req.body;
            if (!contractor_id) {
                return res.status(400).json({ error: 'contractor_id is required' });
            }
            const [c] = await pool.query('SELECT user_id FROM contractors WHERE id = ? AND is_deleted = 0', [contractor_id]);
            if (c.length === 0 || !c[0].user_id) {
                return res.status(400).json({ error: 'This contractor has no linked user account to chat with' });
            }
            contractorUserId = c[0].user_id;
        }

        let conversationId;
        const [existing] = await pool.query(`
            SELECT cc.id FROM chat_conversations cc
            JOIN chat_participants cp ON cp.conversation_id = cc.id AND cp.user_id = ?
            WHERE cc.project_id = ?
        `, [contractorUserId, projectId]);

        const [projRows] = await pool.query('SELECT name, created_by FROM projects WHERE id = ?', [projectId]);
        const projectName = projRows[0]?.name || `#${projectId}`;

        if (existing.length > 0) {
            conversationId = existing[0].id;
        } else {
            const [result] = await pool.query(
                'INSERT INTO chat_conversations (is_group, name, creator_id, project_id) VALUES (1, ?, ?, ?)',
                [`โครงการ: ${projectName}`, contractorUserId, projectId]
            );
            conversationId = result.insertId;
            await pool.query('INSERT INTO chat_participants (conversation_id, user_id) VALUES (?, ?)', [conversationId, contractorUserId]);
        }

        // Sync current admins/permitted users as participants (covers newly-granted access too)
        const adminIds = new Set();
        if (projRows[0]?.created_by) adminIds.add(projRows[0].created_by);
        const [globalAdmins] = await pool.query(`SELECT id FROM users WHERE role IN ('admin', 'superadmin', 'excusive')`);
        globalAdmins.forEach(u => adminIds.add(u.id));
        const [permitted] = await pool.query('SELECT user_id FROM project_permissions WHERE project_id = ?', [projectId]);
        permitted.forEach(p => adminIds.add(p.user_id));

        for (const adminId of adminIds) {
            const [already] = await pool.query('SELECT 1 FROM chat_participants WHERE conversation_id = ? AND user_id = ?', [conversationId, adminId]);
            if (already.length === 0) {
                await pool.query('INSERT INTO chat_participants (conversation_id, user_id) VALUES (?, ?)', [conversationId, adminId]);
            }
        }

        res.json({ conversation_id: conversationId, name: `โครงการ: ${projectName}` });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to open project chat' });
    }
});

// List my conversations with last message preview + unread count
app.get('/api/chat/conversations', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    try {
        // Update user's online activity timestamp
        await pool.query('UPDATE users SET last_active_at = CURRENT_TIMESTAMP WHERE id = ?', [userId]).catch(err => console.error("Active timestamp update failed:", err));

        const [rows] = await pool.query(`
            SELECT cc.id, cc.is_group, cc.name, cc.creator_id, cc.created_at, my_part.last_read_at,
                   (SELECT body FROM chat_messages cm WHERE cm.conversation_id = cc.id ORDER BY cm.id DESC LIMIT 1) AS last_message,
                   (SELECT created_at FROM chat_messages cm WHERE cm.conversation_id = cc.id ORDER BY cm.id DESC LIMIT 1) AS last_message_at,
                   (SELECT COUNT(*) FROM chat_messages cm WHERE cm.conversation_id = cc.id AND cm.sender_id != ? AND cm.created_at > COALESCE(my_part.last_read_at, '1970-01-01 00:00:00')) AS unread_count,
                   (SELECT u.id FROM chat_participants cp2 JOIN users u ON cp2.user_id = u.id WHERE cp2.conversation_id = cc.id AND cp2.user_id != ? LIMIT 1) AS other_user_id,
                   (SELECT u.username FROM chat_participants cp2 JOIN users u ON cp2.user_id = u.id WHERE cp2.conversation_id = cc.id AND cp2.user_id != ? LIMIT 1) AS other_username,
                   (SELECT CASE WHEN u.last_active_at >= datetime('now', '-15 seconds') THEN 1 ELSE 0 END FROM chat_participants cp2 JOIN users u ON cp2.user_id = u.id WHERE cp2.conversation_id = cc.id AND cp2.user_id != ? LIMIT 1) AS other_is_online
            FROM chat_conversations cc
            JOIN chat_participants my_part ON my_part.conversation_id = cc.id AND my_part.user_id = ?
            ORDER BY last_message_at DESC
        `, [userId, userId, userId, userId, userId]);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve conversations' });
    }
});

// Lightweight total-unread count for the header badge
app.get('/api/chat/unread-count', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    try {
        // Update user's online activity timestamp
        await pool.query('UPDATE users SET last_active_at = CURRENT_TIMESTAMP WHERE id = ?', [userId]).catch(err => console.error("Active timestamp update failed:", err));

        const [rows] = await pool.query(`
            SELECT COALESCE(SUM(
                (SELECT COUNT(*) FROM chat_messages cm WHERE cm.conversation_id = cp.conversation_id AND cm.sender_id != ? AND cm.created_at > COALESCE(cp.last_read_at, '1970-01-01 00:00:00'))
            ), 0) AS total_unread
            FROM chat_participants cp
            WHERE cp.user_id = ?
        `, [userId, userId]);
        res.json({ total_unread: rows[0].total_unread });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve unread count' });
    }
});

// Start (or find existing) a 1:1 conversation with another user
app.post('/api/chat/conversations', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const { userId: targetUserId } = req.body;

    if (!targetUserId || parseInt(targetUserId) === parseInt(userId)) {
        return res.status(400).json({ error: 'A valid target user is required' });
    }

    try {
        const [targetUser] = await pool.query('SELECT id FROM users WHERE id = ?', [targetUserId]);
        if (targetUser.length === 0) {
            return res.status(404).json({ error: 'User not found' });
        }

        const [existing] = await pool.query(`
            SELECT cc.id FROM chat_conversations cc
            WHERE cc.is_group = 0
              AND EXISTS (SELECT 1 FROM chat_participants cp1 WHERE cp1.conversation_id = cc.id AND cp1.user_id = ?)
              AND EXISTS (SELECT 1 FROM chat_participants cp2 WHERE cp2.conversation_id = cc.id AND cp2.user_id = ?)
              AND (SELECT COUNT(*) FROM chat_participants cp WHERE cp.conversation_id = cc.id) = 2
        `, [userId, targetUserId]);

        if (existing.length > 0) {
            return res.json({ id: existing[0].id, created: false });
        }

        const [result] = await pool.query('INSERT INTO chat_conversations (is_group) VALUES (0)');
        const conversationId = result.insertId;
        await pool.query('INSERT INTO chat_participants (conversation_id, user_id) VALUES (?, ?)', [conversationId, userId]);
        await pool.query('INSERT INTO chat_participants (conversation_id, user_id) VALUES (?, ?)', [conversationId, targetUserId]);

        res.status(201).json({ id: conversationId, created: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to start conversation' });
    }
});

// Start a group conversation
app.post('/api/chat/conversations/group', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const { name, userIds } = req.body;
    
    if (!name || !userIds || !Array.isArray(userIds) || userIds.length === 0) {
        return res.status(400).json({ error: 'Group name and participant user IDs are required' });
    }
    
    try {
        // Insert group conversation header
        const [result] = await pool.query('INSERT INTO chat_conversations (is_group, name, creator_id) VALUES (1, ?, ?)', [name, userId]);
        const conversationId = result.insertId;
        
        // Include the creator in the participants list
        const allParticipants = Array.from(new Set([userId, ...userIds.map(id => parseInt(id))]));
        
        for (const pId of allParticipants) {
            await pool.query('INSERT INTO chat_participants (conversation_id, user_id) VALUES (?, ?)', [conversationId, pId]);
        }
        
        res.status(201).json({ id: conversationId, name, is_group: 1, creator_id: userId });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to start group conversation' });
    }
});

// Get participants of a group conversation
app.get('/api/chat/conversations/:id/participants', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const { id } = req.params;

    try {
        const isParticipant = await checkChatParticipant(id, userId);
        if (!isParticipant) {
            return res.status(403).json({ error: 'You are not part of this conversation' });
        }

        const [participants] = await pool.query(`
            SELECT u.id, u.username, u.role, u.last_active_at,
                   CASE WHEN u.last_active_at >= datetime('now', '-15 seconds') THEN 1 ELSE 0 END AS is_online
            FROM chat_participants cp
            JOIN users u ON cp.user_id = u.id
            WHERE cp.conversation_id = ?
            ORDER BY u.username ASC
        `, [id]);

        res.json(participants);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve group participants' });
    }
});

// Invite a user to a group conversation
app.post('/api/chat/conversations/:id/participants', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const { id } = req.params;
    const { targetUserId } = req.body;

    if (!targetUserId) {
        return res.status(400).json({ error: 'targetUserId is required' });
    }

    try {
        // Fetch group details to confirm it is a group and check the creator
        const [convs] = await pool.query('SELECT is_group, creator_id FROM chat_conversations WHERE id = ?', [id]);
        if (convs.length === 0) {
            return res.status(404).json({ error: 'Conversation not found' });
        }

        const conv = convs[0];
        if (conv.is_group !== 1) {
            return res.status(400).json({ error: 'This is not a group conversation' });
        }

        if (parseInt(conv.creator_id) !== parseInt(userId)) {
            return res.status(403).json({ error: 'Only the group creator can invite members' });
        }

        // Add to participants list (ensure no duplicates)
        const [exists] = await pool.query('SELECT 1 FROM chat_participants WHERE conversation_id = ? AND user_id = ?', [id, targetUserId]);
        if (exists.length === 0) {
            await pool.query('INSERT INTO chat_participants (conversation_id, user_id) VALUES (?, ?)', [id, targetUserId]);
        }
        res.json({ success: true, message: 'Member invited successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to invite group member' });
    }
});

// Kick/Exclude a user from a group conversation
app.delete('/api/chat/conversations/:id/participants/:targetUserId', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const { id, targetUserId } = req.params;

    try {
        // Fetch group details to confirm it is a group and check the creator
        const [convs] = await pool.query('SELECT is_group, creator_id FROM chat_conversations WHERE id = ?', [id]);
        if (convs.length === 0) {
            return res.status(404).json({ error: 'Conversation not found' });
        }

        const conv = convs[0];
        if (conv.is_group !== 1) {
            return res.status(400).json({ error: 'This is not a group conversation' });
        }

        if (parseInt(conv.creator_id) !== parseInt(userId)) {
            return res.status(403).json({ error: 'Only the group creator can kick members' });
        }

        if (parseInt(targetUserId) === parseInt(userId)) {
            return res.status(400).json({ error: 'You cannot kick yourself from the group' });
        }

        // Delete from participants list
        await pool.query('DELETE FROM chat_participants WHERE conversation_id = ? AND user_id = ?', [id, targetUserId]);
        res.json({ success: true, message: 'Member kicked successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to kick group member' });
    }
});

// Get messages in a conversation (most recent N, chronological order)
app.get('/api/chat/conversations/:id/messages', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const { id } = req.params;
    const limit = Math.min(parseInt(req.query.limit) || 100, 300);

    try {
        const isParticipant = await checkChatParticipant(id, userId);
        if (!isParticipant) {
            return res.status(403).json({ error: 'You are not part of this conversation' });
        }

        const [rows] = await pool.query(`
            SELECT cm.id, cm.conversation_id, cm.sender_id, cm.body, cm.created_at, u.username AS sender_name
            FROM chat_messages cm
            JOIN users u ON cm.sender_id = u.id
            WHERE cm.conversation_id = ?
            ORDER BY cm.id DESC
            LIMIT ?
        `, [id, limit]);

        res.json(rows.reverse());
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve messages' });
    }
});

// Send a message in a conversation
app.post('/api/chat/conversations/:id/messages', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const { id } = req.params;
    const { body } = req.body;

    if (!body || !body.trim()) {
        return res.status(400).json({ error: 'Message body is required' });
    }

    try {
        const isParticipant = await checkChatParticipant(id, userId);
        if (!isParticipant) {
            return res.status(403).json({ error: 'You are not part of this conversation' });
        }

        const [result] = await pool.query(
            'INSERT INTO chat_messages (conversation_id, sender_id, body) VALUES (?, ?, ?)',
            [id, userId, body.trim()]
        );
        // Sending also counts as having read up to now
        await pool.query('UPDATE chat_participants SET last_read_at = CURRENT_TIMESTAMP WHERE conversation_id = ? AND user_id = ?', [id, userId]);

        const [newMessage] = await pool.query(`
            SELECT cm.id, cm.conversation_id, cm.sender_id, cm.body, cm.created_at, u.username AS sender_name
            FROM chat_messages cm JOIN users u ON cm.sender_id = u.id WHERE cm.id = ?
        `, [result.insertId]);
        res.status(201).json(newMessage[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to send message' });
    }
});

// Upload image for chat
app.post('/api/chat/upload', authenticateToken, upload.single('image'), async (req, res) => {
    try {
        if (!req.file) {
            return res.status(400).json({ error: 'No image file uploaded' });
        }
        // Save the image buffer to disk
        const filePath = saveBufferToDisk('chat', req.file.buffer, req.file.originalname);
        res.json({ success: true, filePath });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to upload image' });
    }
});

// Multer instance for audio clips
const audioUpload = multer({
    storage: multer.memoryStorage(),
    limits: { fileSize: 15 * 1024 * 1024 }, // 15MB is plenty for voice messages
    fileFilter: (req, file, cb) => {
        if (file.mimetype.startsWith('audio/') || file.mimetype === 'application/octet-stream') {
            cb(null, true);
        } else {
            cb(new Error('Only audio files are allowed'), false);
        }
    }
});

// Upload audio clip for chat
app.post('/api/chat/upload-audio', authenticateToken, audioUpload.single('audio'), async (req, res) => {
    try {
        if (!req.file) {
            return res.status(400).json({ error: 'No audio file uploaded' });
        }
        // Save the audio buffer to disk
        const filePath = saveBufferToDisk('chat', req.file.buffer, req.file.originalname || 'voice_clip.webm');
        res.json({ success: true, filePath });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to upload audio clip' });
    }
});

// Mark a conversation as read
app.put('/api/chat/conversations/:id/read', authenticateToken, async (req, res) => {
    const userId = req.user.id;
    const { id } = req.params;
    try {
        const isParticipant = await checkChatParticipant(id, userId);
        if (!isParticipant) {
            return res.status(403).json({ error: 'You are not part of this conversation' });
        }
        await pool.query('UPDATE chat_participants SET last_read_at = CURRENT_TIMESTAMP WHERE conversation_id = ? AND user_id = ?', [id, userId]);
        res.json({ success: true });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to mark conversation as read' });
    }
});

// ==========================================
// 8. PROJECT REVISION ENDPOINTS
// ==========================================

// Get revision history for a project
app.get('/api/projects/:id/revisions', authenticateToken, async (req, res) => {
    const { id } = req.params;
    const role = req.user.role;
    const userId = req.user.id;
    
    if (role === 'supervisor' || role === 'estimate') {
        const hasAccess = await checkProjectWriteAccess(id, userId, role);
        if (!hasAccess) return res.status(403).json({ error: 'Access Denied' });
    }
    
    try {
        const [rows] = await pool.query(`
            SELECT pr.id, pr.revision_number, pr.change_summary, pr.created_at, u.username 
            FROM project_revisions pr
            LEFT JOIN users u ON pr.user_id = u.id
            WHERE pr.project_id = ?
            ORDER BY pr.revision_number DESC
        `, [id]);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve project revisions' });
    }
});

// Compare two revisions and return diff list
app.get('/api/projects/:id/revisions/:revA/compare/:revB', authenticateToken, async (req, res) => {
    const { id, revA, revB } = req.params;
    const role = req.user.role;
    const userId = req.user.id;
    
    if (role === 'supervisor' || role === 'estimate') {
        const hasAccess = await checkProjectWriteAccess(id, userId, role);
        if (!hasAccess) return res.status(403).json({ error: 'Access Denied' });
    }

    try {
        const [rowsA] = await pool.query('SELECT snapshot_data, revision_number FROM project_revisions WHERE project_id = ? AND revision_number = ?', [id, revA]);
        const [rowsB] = await pool.query('SELECT snapshot_data, revision_number FROM project_revisions WHERE project_id = ? AND revision_number = ?', [id, revB]);

        if (rowsA.length === 0 || rowsB.length === 0) {
            return res.status(404).json({ error: 'One or both revisions not found' });
        }

        const snapA = JSON.parse(rowsA[0].snapshot_data || '[]');
        const snapB = JSON.parse(rowsB[0].snapshot_data || '[]');

        const changes = [];
        const itemsA = [];
        snapA.forEach(sec => {
            sec.items.forEach(item => {
                itemsA.push({ ...item, section_name: sec.section_name });
            });
        });

        const itemsB = [];
        snapB.forEach(sec => {
            sec.items.forEach(item => {
                itemsB.push({ ...item, section_name: sec.section_name });
            });
        });

        // Find removed or modified items (present in A but not in B, or modified in B)
        itemsA.forEach(itemA => {
            const itemB = itemsB.find(b => b.id === itemA.id || (b.product_id === itemA.product_id && b.section_name === itemA.section_name && b.description === itemA.description));
            if (!itemB) {
                changes.push({
                    section_name: itemA.section_name,
                    description: itemA.description,
                    type: 'removed',
                    details: `Qty: ${itemA.qty} -> Removed`
                });
            } else {
                const diffs = [];
                if (itemA.qty !== itemB.qty) {
                    diffs.push(`Qty: ${itemA.qty} -> ${itemB.qty}`);
                }
                if (itemA.price_material !== itemB.price_material) {
                    diffs.push(`Price Material: ${itemA.price_material} -> ${itemB.price_material}`);
                }
                if (itemA.price_labour !== itemB.price_labour) {
                    diffs.push(`Price Labor: ${itemA.price_labour} -> ${itemB.price_labour}`);
                }
                if (itemA.discount !== itemB.discount) {
                    diffs.push(`Discount: ${itemA.discount}% -> ${itemB.discount}%`);
                }
                if (itemA.is_free_issue !== itemB.is_free_issue) {
                    diffs.push(`Free Issue: ${itemA.is_free_issue ? 'Yes' : 'No'} -> ${itemB.is_free_issue ? 'Yes' : 'No'}`);
                }

                if (diffs.length > 0) {
                    changes.push({
                        section_name: itemA.section_name,
                        description: itemA.description,
                        type: 'modified',
                        details: diffs.join(', ')
                    });
                }
            }
        });

        // Find added items (present in B but not in A)
        itemsB.forEach(itemB => {
            const itemA = itemsA.find(a => a.id === itemB.id || (a.product_id === itemB.product_id && a.section_name === itemB.section_name && a.description === itemB.description));
            if (!itemA) {
                changes.push({
                    section_name: itemB.section_name,
                    description: itemB.description,
                    type: 'added',
                    details: `Added (Qty: ${itemB.qty}, Price Material: ${itemB.price_material}, Price Labor: ${itemB.price_labour})`
                });
            }
        });

        res.json({
            revisionA: revA,
            revisionB: revB,
            changes
        });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to compare revisions' });
    }
});

// ==========================================================================
// 12. SYSTEM / SUPER ADMIN ENDPOINTS
// ==========================================================================

const backupDir = process.env.BACKUP_DIR
    ? path.resolve(__dirname, process.env.BACKUP_DIR)
    : path.join(__dirname, 'backups');
if (!fs.existsSync(backupDir)) {
    fs.mkdirSync(backupDir, { recursive: true });
    console.log(`Backup directory created: ${backupDir}`);
}

const configPath = path.join(__dirname, 'backup_config.json');

function loadBackupConfig() {
    if (!fs.existsSync(configPath)) {
        return { schedule: 'disabled', last_backup: null };
    }
    try {
        return JSON.parse(fs.readFileSync(configPath, 'utf8'));
    } catch (e) {
        return { schedule: 'disabled', last_backup: null };
    }
}

function saveBackupConfig(config) {
    fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
}

// Helper middleware to verify Super Admin role
function requireSuperAdmin(req, res, next) {
    if (!req.user || req.user.role !== 'superadmin') {
        return res.status(403).json({ error: 'Access denied: Super Admin role required' });
    }
    next();
}

async function closeDb() {
    if (dbPromise) {
        try {
            const db = await dbPromise;
            await db.close();
        } catch (e) {
            console.error("Error closing database:", e);
        }
        dbPromise = null;
    }
}

async function performBackup() {
    const timestamp = new Date().toISOString().replace(/T/, '_').replace(/\..+/, '').replace(/:/g, '');
    const filename = `shopada_b2b_backup_${timestamp}.db`;
    const destPath = path.join(backupDir, filename);
    
    const db = await getDb();
    await db.run(`VACUUM INTO ?`, [destPath]);
    return { filename, timestamp };
}

async function checkBackupSchedule() {
    const config = loadBackupConfig();
    if (config.schedule === 'disabled') return;
    
    const now = new Date();
    let shouldBackup = false;
    
    if (!config.last_backup) {
        shouldBackup = true;
    } else {
        const lastBackupDate = new Date(config.last_backup);
        const diffMs = now - lastBackupDate;
        const diffDays = diffMs / (1000 * 60 * 60 * 24);
        
        if (config.schedule === 'daily' && diffDays >= 1) {
            shouldBackup = true;
        } else if (config.schedule === 'weekly' && diffDays >= 7) {
            shouldBackup = true;
        }
    }
    
    if (shouldBackup) {
        try {
            console.log(`Auto-Backup: Triggering scheduled ${config.schedule} backup...`);
            const backupInfo = await performBackup();
            config.last_backup = now.toISOString();
            saveBackupConfig(config);
            console.log(`Auto-Backup: Completed successfully. File: ${backupInfo.filename}`);
        } catch (err) {
            console.error('Auto-Backup failed:', err.message);
        }
    }
}

// Retention policy: keep 7 daily, 4 weekly, 3 monthly
function applyRetentionPolicy() {
    try {
        if (!fs.existsSync(backupDir)) return;
        const files = fs.readdirSync(backupDir)
            .filter(f => f.startsWith('shopada_b2b_backup_') && f.endsWith('.db'))
            .map(f => ({ name: f, time: fs.statSync(path.join(backupDir, f)).mtime }))
            .sort((a, b) => b.time - a.time); // newest first

        const now = new Date();
        const DAY = 86400000;
        const keep = new Set();

        // Keep 7 most recent daily backups
        let dailyCount = 0;
        for (const f of files) {
            if (dailyCount >= 7) break;
            if ((now - f.time) < 7 * DAY) { keep.add(f.name); dailyCount++; }
        }
        // Keep 4 weekly (oldest per week)
        let weeklyCount = 0;
        for (let w = 1; w <= 4; w++) {
            const weekFiles = files.filter(f => {
                const age = (now - f.time) / DAY;
                return age >= w * 7 && age < (w + 1) * 7;
            });
            if (weekFiles.length > 0) { keep.add(weekFiles[weekFiles.length - 1].name); weeklyCount++; }
        }
        // Keep 3 monthly (oldest per month)
        for (let m = 1; m <= 3; m++) {
            const monthFiles = files.filter(f => {
                const age = (now - f.time) / DAY;
                return age >= m * 30 && age < (m + 1) * 30;
            });
            if (monthFiles.length > 0) { keep.add(monthFiles[monthFiles.length - 1].name); }
        }

        // Delete files not in keep set
        let deleted = 0;
        for (const f of files) {
            if (!keep.has(f.name)) {
                fs.unlinkSync(path.join(backupDir, f.name));
                deleted++;
            }
        }
        if (deleted > 0) console.log(`Retention policy: deleted ${deleted} old backup(s)`);
    } catch (e) {
        console.error('Retention policy error:', e.message);
    }
}

// Background scheduling timer
setInterval(checkBackupSchedule, 10 * 60 * 1000);
setTimeout(checkBackupSchedule, 5000);

// 1. Get backup schedule configuration
app.get('/api/system/backups/schedule', authenticateToken, requireSuperAdmin, (req, res) => {
    res.json(loadBackupConfig());
});

// 2. Configure backup schedule
app.post('/api/system/backups/schedule', authenticateToken, requireSuperAdmin, (req, res) => {
    const { schedule } = req.body;
    if (!['disabled', 'daily', 'weekly'].includes(schedule)) {
        return res.status(400).json({ error: 'Invalid schedule value' });
    }
    const config = loadBackupConfig();
    config.schedule = schedule;
    saveBackupConfig(config);
    res.json({ success: true, message: 'Backup schedule updated', config });
});

// 3. Get all backups versions
app.get('/api/system/backups', authenticateToken, requireSuperAdmin, (req, res) => {
    try {
        if (!fs.existsSync(backupDir)) {
            return res.json([]);
        }
        const files = fs.readdirSync(backupDir)
            .filter(f => f.startsWith('shopada_b2b_backup_') && f.endsWith('.db'))
            .map(f => {
                const stats = fs.statSync(path.join(backupDir, f));
                const parts = f.replace('shopada_b2b_backup_', '').replace('.db', '').split('_');
                const dateStr = parts[0];
                const timeStr = parts[1] || '';
                const formattedTime = timeStr ? `${timeStr.substr(0,2)}:${timeStr.substr(2,2)}:${timeStr.substr(4,2)}` : '';
                return {
                    filename: f,
                    version: `Version ${dateStr} ${formattedTime}`.trim(),
                    size: stats.size,
                    created_at: stats.mtime
                };
            })
            .sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
        res.json(files);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to retrieve backups' });
    }
});

// 4. Create manual backup
app.post('/api/system/backups', authenticateToken, requireSuperAdmin, async (req, res) => {
    try {
        const backupInfo = await performBackup();
        res.json({ success: true, message: 'Backup created successfully', backupInfo });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Backup failed' });
    }
});

// 5. Restore from backup version (auto-backup before restore)
app.post('/api/system/backups/:filename/restore', authenticateToken, requireSuperAdmin, async (req, res) => {
    const { filename } = req.params;
    if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
        return res.status(400).json({ error: 'Invalid filename' });
    }
    const filePath = path.join(backupDir, filename);
    if (!fs.existsSync(filePath)) {
        return res.status(404).json({ error: 'Backup version file not found' });
    }
    
    try {
        // Auto-backup current database before restore
        console.log('Auto-backup before restore...');
        const preRestoreBackup = await performBackup();
        console.log(`Pre-restore backup saved: ${preRestoreBackup.filename}`);

        await closeDb();
        fs.copyFileSync(filePath, dbPath);
        await getDb(); // Reopen database
        res.json({ success: true, message: `System restored to version ${filename} successfully. Pre-restore backup: ${preRestoreBackup.filename}` });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to restore database backup' });
    }
});

// 6. Repair database (Integrity check, orphaned cleanup, vacuum)
app.post('/api/system/repair', authenticateToken, requireSuperAdmin, async (req, res) => {
    try {
        const db = await getDb();
        const integrity = await db.all('PRAGMA integrity_check;');
        const isHealthy = integrity.length > 0 && integrity[0].integrity_check === 'ok';
        
        // Cleanup orphaned rows
        await db.run('DELETE FROM project_items WHERE section_id NOT IN (SELECT id FROM project_sections);');
        await db.run('DELETE FROM project_sections WHERE project_id NOT IN (SELECT id FROM projects);');
        await db.run('DELETE FROM project_progress WHERE project_id NOT IN (SELECT id FROM projects);');
        await db.run('DELETE FROM project_documents WHERE folder_id NOT IN (SELECT id FROM project_folders);');
        await db.run('DELETE FROM project_folders WHERE project_id NOT IN (SELECT id FROM projects);');
        
        // Compact database
        await db.run('VACUUM;');
        
        res.json({ 
            success: true, 
            message: 'Database check and repairs completed successfully',
            health: isHealthy ? 'Healthy' : 'Corrupted',
            details: integrity.map(i => i.integrity_check).join(', ')
        });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to run database repairs' });
    }
});

// 7. Restore from uploaded file (auto-backup before restore)
app.post('/api/system/restore-file', authenticateToken, upload.single('db_file'), requireSuperAdmin, async (req, res) => {
    if (!req.file) {
        return res.status(400).json({ error: 'No database file uploaded' });
    }
    
    try {
        // Auto-backup current database before restore
        console.log('Auto-backup before file restore...');
        const preRestoreBackup = await performBackup();
        console.log(`Pre-restore backup saved: ${preRestoreBackup.filename}`);

        await closeDb();
        fs.writeFileSync(dbPath, req.file.buffer);
        await getDb(); // Reopen
        res.json({ success: true, message: `Database restored from uploaded file successfully. Pre-restore backup: ${preRestoreBackup.filename}` });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to restore database from uploaded file' });
    }
});

// 8. Secure download backup file
app.get('/api/system/backups/:filename/download', authenticateToken, requireSuperAdmin, (req, res) => {
    const { filename } = req.params;
    if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
        return res.status(400).json({ error: 'Invalid filename' });
    }
    const filePath = path.join(backupDir, filename);
    if (!fs.existsSync(filePath)) {
        return res.status(404).json({ error: 'Backup file not found' });
    }
    res.download(filePath, filename);
});

// Helper to find local IPv4 network address
function getLocalIpAddress() {
    const os = require('os');
    const interfaces = os.networkInterfaces();
    for (const name of Object.keys(interfaces)) {
        for (const net of interfaces[name]) {
            if (net.family === 'IPv4' && !net.internal) {
                return net.address;
            }
        }
    }
    return '127.0.0.1';
}

// Register PR, PO, and Stock Management APIs
require('./pr_po_stock_api')(app, pool);

// Start server
const useSSL = process.env.USE_SSL === 'true';
const sslKeyPath = path.join(__dirname, 'key.pem');
const sslCertPath = path.join(__dirname, 'cert.pem');

if (useSSL && fs.existsSync(sslKeyPath) && fs.existsSync(sslCertPath)) {
    const httpsOptions = {
        key: fs.readFileSync(sslKeyPath),
        cert: fs.readFileSync(sslCertPath)
    };
    https.createServer(httpsOptions, app).listen(PORT, '0.0.0.0', () => {
        const localIp = getLocalIpAddress();
        console.log(`Server is running with SSL (HTTPS)!`);
        console.log(`  - Local:   https://localhost:${PORT}`);
        console.log(`  - Network: https://${localIp}:${PORT} (Use this link on your iPad Safari)`);
    });

    // Start HTTP redirect server on port 80 if SSL is on 443
    if (PORT === 443 || PORT === '443') {
        http.createServer((req, res) => {
            const host = req.headers.host || '';
            res.writeHead(301, { "Location": `https://${host}${req.url}` });
            res.end();
        }).listen(80, '0.0.0.0', () => {
            console.log(`HTTP redirect server is running on port 80 (redirects to HTTPS)`);
        });
    }
} else {
    app.listen(PORT, '0.0.0.0', () => {
        const localIp = getLocalIpAddress();
        console.log(`Server is running!`);
        console.log(`  - Local:   http://localhost:${PORT}`);
        console.log(`  - Network: http://${localIp}:${PORT} (Use this link on your iPad Safari)`);
    });
}