report*: initial (broken)

This commit is contained in:
2025-11-19 08:41:07 +07:00
parent b48a241c26
commit a705281ad7
5 changed files with 215 additions and 14 deletions

View File

@@ -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;
}
}
}

View File

@@ -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 นั้น ๆ

View File

@@ -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
// // ===================================================

View File

@@ -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;
}
}

View File

@@ -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 หรืออื่นๆ เรียกใช้ได้ด้วย