diff --git a/exthernal-accountingwep-api/src/controllers/reportController.js b/exthernal-accountingwep-api/src/controllers/reportController.js new file mode 100644 index 0000000..ef06f6c --- /dev/null +++ b/exthernal-accountingwep-api/src/controllers/reportController.js @@ -0,0 +1,137 @@ +import { ReportService } from '../services/reportService.js' +import { sendError } from '../utils/response.js' +// import { OftenError } from '../utils/oftenError.js' +import { GeneralService } from '../share/generalservice.js'; +import { trim_all_array } from '../utils/trim.js' +import { verifyToken, generateToken } from '../utils/token.js' +import { Interface } from '../interfaces/Interface.js'; + +export class reportController { + + constructor() { + this.generalService = new GeneralService(); + this.reportService = new ReportService(); + this.Interface = new Interface(); + } + + async onNavigate(req, res) { + this.generalService.devhint(1, 'reportController.js', 'onNavigate() start'); + let organization = req.body.organization; + const prommis = await this.onReportController(req, res, organization); + return prommis; + } + + async onReportController(req, res, database) { + let idx = -1 + let aryResult = [] + try { + let token = req.headers.authorization?.split(' ')[1]; + const decoded = verifyToken(token); + + let actnum = req.body.request.actnum; + database = decoded.organization; + + aryResult = await this.reportService.getReportController(database, actnum); + } catch (error) { + idx = 1; + } finally { + if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error'); + if (!aryResult) return sendError('ไม่พบการมีอยู่ของข้อมูล', 'Cannot Find Any Data'); + + try { + // ✅ 1) เตรียม data สำหรับใช้คำนวณ + // ถ้า service คืนมาเป็น { code, message, data: [...] } + const data = Array.isArray(aryResult) + ? aryResult + : Array.isArray(aryResult.data) + ? aryResult.data + : []; + + // ถ้าไม่มีข้อมูลก็ไม่ต้องคำนวณ + if (!data.length) { + return aryResult; + } + + // ✅ 2) แยก income / expense + const incomeList = data.filter(i => i.acttyp === 'i'); + const expenseList = data.filter(e => e.acttyp === 'e'); + + const totalIncome = incomeList.reduce((sum, i) => sum + parseFloat(i.actqty || 0), 0); + const totalExpense = expenseList.reduce((sum, e) => sum + parseFloat(e.actqty || 0), 0); + + const netProfit = totalIncome - totalExpense; + const profitRate = totalIncome > 0 ? (netProfit / totalIncome) * 100 : 0; + const adjustedProfitRate = profitRate + 1.9; + + // ✅ 3) แนบ summary (เหมือนที่เราทำไปก่อนหน้า) + var summary = { + totalIncome: totalIncome.toFixed(2), + totalExpense: totalExpense.toFixed(2), + netProfit: netProfit.toFixed(2), + profitRate: profitRate.toFixed(2) + ' %', + adjustedProfitRate: adjustedProfitRate.toFixed(2) + ' %', + period: '30 วัน' + }; + + // ✅ 4) ดึงสีจาก dtlmst (แนะนำให้เรียกจาก service เพิ่ม) + // ตัวอย่างสมมติ: คุณไป query มาจาก service ก่อนหน้าแล้วได้เป็น object แบบนี้ + // key = ชื่อหมวด (actcatnam หรือ code), value = color + const categoryColorMap = await this.reportService.getCategoryColorMap(database); + // ตัวอย่างที่คาดหวังจาก service: + // { 'ค่าอาหาร': '#FF6384', 'ค่าเดินทาง': '#36A2EB', 'ขายสินค้า': '#4BC0C0', ... } + + // ✅ 5) สรุปยอดตามหมวด แล้วคำนวณ % สำหรับ expense + const expenseAgg = {}; + expenseList.forEach(row => { + const key = row.actcat; // หรือใช้รหัส category ถ้ามี เช่น row.actcatcod + const amount = parseFloat(row.actqty || 0); + expenseAgg[key] = (expenseAgg[key] || 0) + amount; + }); + + const incomeAgg = {}; + incomeList.forEach(row => { + const key = row.actcat; + const amount = parseFloat(row.actqty || 0); + incomeAgg[key] = (incomeAgg[key] || 0) + amount; + }); + + const expensePie = Object.entries(expenseAgg).map(([cat, value]) => { + const percent = totalExpense > 0 ? (value / totalExpense) * 100 : 0; + const color = categoryColorMap[cat] || '#CCCCCC'; // fallback สี default + return { + label: cat, + value: value.toFixed(2), + percent: percent.toFixed(2), + color + }; + }); + + const incomePie = Object.entries(incomeAgg).map(([cat, value]) => { + const percent = totalIncome > 0 ? (value / totalIncome) * 100 : 0; + const color = categoryColorMap[cat] || '#CCCCCC'; + return { + label: cat, + value: value.toFixed(2), + percent: percent.toFixed(2), + color + }; + }); + + // ✅ 6) แนบข้อมูล pie chart เข้า aryResult + var pie = { + expense: expensePie, + income: incomePie + }; + + } catch (err) { + console.error('calculate summary/pie error:', err); + } + let arydiy = { + summary, + pie + } + + return arydiy; + } + } +} diff --git a/exthernal-accountingwep-api/src/interfaces/Interface.js b/exthernal-accountingwep-api/src/interfaces/Interface.js index 21238b6..aea5da1 100644 --- a/exthernal-accountingwep-api/src/interfaces/Interface.js +++ b/exthernal-accountingwep-api/src/interfaces/Interface.js @@ -1,27 +1,34 @@ import jwt from 'jsonwebtoken' import { BdgmstInterface } from './table/bdgmstInterface.js' +import { ActmstInterface } from './table/actmstInterface.js' +import { sendError } from '../utils/response.js' + // import { ActmstInterface } from './actmstInterface.js' +// ------------------------------- +// GLOBAL FILE +// ----------------------------- + export class Interface { constructor() { this.map = { bdgmst: new BdgmstInterface(), - // actmst: new ActmstInterface(), + actmst: new ActmstInterface(), } } // =============================================================== - // 📌 saveInterface → แกะ token เอง และ route ไปยัง interface เฉพาะ table + // saveInterface → แกะ token เอง และ route ไปยัง interface เฉพาะ table // =============================================================== - async saveInterface(tableName, req, data) { + async saveInterface(tableName, data, req) { // ------------------------------ // ✔ 1) จับ Interface ที่ตรงกับ table // ------------------------------ const handler = this.map[tableName.toLowerCase()] if (!handler) { - throw new Error(`Interface not found for table: ${tableName}`) + return new sendError(`Interface not found for table: ${tableName}`) } // ------------------------------ @@ -29,18 +36,18 @@ export class Interface { // ------------------------------ const token = req.headers.authorization?.split(' ')[1] if (!token) { - throw new Error('Missing token in request header') + return new sendError('ไม่พบการยืนยันตัวตน' ,'Missing token in request header') } let decoded try { decoded = jwt.verify(token, process.env.JWT_SECRET) } catch (err) { - throw new Error('Invalid token: ' + err.message) + return new sendError('Invalid token: ' + err.message) } - const schema = decoded.organization // ⭐ ได้ schema ที่ต้องการ - if (!schema) throw new Error("Token missing 'organization' field") + const schema = decoded.organization + if (!schema) return new sendError("Token missing 'organization' field") // ------------------------------ // ✔ 3) ส่งงานไปยัง interface ของ table นั้น ๆ diff --git a/exthernal-accountingwep-api/src/routes/route.js b/exthernal-accountingwep-api/src/routes/route.js index 8362d50..bf6eee9 100644 --- a/exthernal-accountingwep-api/src/routes/route.js +++ b/exthernal-accountingwep-api/src/routes/route.js @@ -3,6 +3,7 @@ import { accountingSetup } from '../controllers/accountingSetupController.js' import { accountingSearch } from '../controllers/accountingSearchController.js' import { accountingSum } from '../controllers/accountingSumController.js' import { accountingAdd } from '../controllers/accountingAddController.js' +import { reportController } from '../controllers/ReportController.js' // import { authMiddleware } from '../middlewares/auth.js' // import { sendResponse } from '../utils/response.js' @@ -12,28 +13,34 @@ const controller_accountingSetup_post = new accountingSetup() const controller_accountingSearch_post = new accountingSearch() const controller_accountingSum_post = new accountingSum() const controller_accountingAdd_post = new accountingAdd() +const controller_report_post = new reportController() -router.post('/accountingSetup', async (req, res) => { +router.post('/accountingsetup', async (req, res) => { const result = await controller_accountingSetup_post.onNavigate(req, res) if (result) return res.json(result) }) -router.post('/accountingSearch', async (req, res) => { +router.post('/accountingsearch', async (req, res) => { const result = await controller_accountingSearch_post.onNavigate(req, res) if (result) return res.json(result) }) -router.post('/accountingSum', async (req, res) => { +router.post('/accountingsum', async (req, res) => { const result = await controller_accountingSum_post.onNavigate(req, res) if (result) return res.json(result) }) -router.post('/accountingAdd', async (req, res) => { +router.post('/accountingadd', async (req, res) => { const result = await controller_accountingAdd_post.onNavigate(req, res) if (result) return res.json(result) }) +router.post('/report', async (req, res) => { + const result = await controller_report_post.onNavigate(req, res) + if (result) return res.json(result) +}) + // // =================================================== // // 🔹 BIOMETRIC LOGIN // // =================================================== diff --git a/exthernal-accountingwep-api/src/services/reportService.js b/exthernal-accountingwep-api/src/services/reportService.js new file mode 100644 index 0000000..ea23ee1 --- /dev/null +++ b/exthernal-accountingwep-api/src/services/reportService.js @@ -0,0 +1,42 @@ +import { GeneralService } from '../share/generalservice.js' + +export class ReportService { + + constructor() { + this.generalService = new GeneralService() + } + + async getReportController(database, number) { + const sql = ` + SELECT + acttyp, + actcat, + actqty, + actcmt, + actacpdtm + FROM ${database}.actmst + WHERE actnum = $1 + ` + + const params = [number] + const result = await this.generalService.executeQueryParam(database, sql, params); + return result + } + + async getCategoryColorMap(database) { + const sql = ` + SELECT dtlcod, dtlnam, dtlmsc as dtlclr + FROM ${database}.dtlmst + WHERE dtltblcod IN ('ACTCAT_INC', 'ACTCAT_EXP') + `; + const params = [] + const rows = await this.generalService.executeQueryParam(database, sql, params); + + const map = {}; + rows.forEach(r => { + map[r.dtlnam] = r.dtlclr; + }); + + return map; + } +} \ No newline at end of file diff --git a/exthernal-accountingwep-api/src/share/generalservice.js b/exthernal-accountingwep-api/src/share/generalservice.js index c2cada3..c543870 100644 --- a/exthernal-accountingwep-api/src/share/generalservice.js +++ b/exthernal-accountingwep-api/src/share/generalservice.js @@ -54,7 +54,9 @@ export class GeneralService { for (const [key, value] of Object.entries(conditions)) { if (value === undefined || value === null || value === '') continue + const match = String(value).match(/^(ILIKE|LIKE)\s+(.+)$/i) + if (match) { const operator = match[1].toUpperCase() const pattern = match[2].trim() @@ -69,6 +71,7 @@ export class GeneralService { let finalQuery = baseQuery if (whereClauses.length > 0) finalQuery += ' AND ' + whereClauses.join(' AND ') + const formattedSQL = finalQuery.replace(/\${database}/g, database) try { @@ -77,15 +80,20 @@ export class GeneralService { sql: formattedSQL, params }) + const result = await connection.query(formattedSQL, params) + this.devhint(2, 'executeQueryConditions', `✅ Query Success (${result.rowCount} rows)`) - return result.rows + + return result.rows + } catch (err) { this.devhint(1, 'executeQueryConditions', `❌ SQL Error`, err.message) console.error('🧨 SQL Error:', err.message) - throw new Error(`SQL_EXECUTION_FAILED::${err.message}`) // ✅ “เจ๊งจริง” ส่งถึง controller แน่นอน + throw new Error(`SQL_EXECUTION_FAILED::${err.message}`) } } + } // =================================================== // Export สำหรับ controller หรืออื่นๆ เรียกใช้ได้ด้วย