NODE JS introduction: socket polling

This commit is contained in:
Thanakorn S
2026-05-22 17:01:59 +07:00
parent 2410f7bade
commit 9c275eb358
17 changed files with 1520 additions and 2135 deletions
@@ -234,25 +234,50 @@ class FinancialReports
// ── Monthly Trend ─────────────────────────────────────────────────────────
// Revenue vs expense per period for the last N months (defaults to 6).
// Returns rows: [{period, revenue, expense}]
// Reads from etl_gl_summary; falls back to td_gl_item for periods not yet in ETL.
public function getMonthlyTrend(int $months = 6): array
{
$from = date('Y-m', strtotime('-' . ($months - 1) . ' months'));
$sth = $this->pdo->prepare("
SELECT
g.period,
SUM(CASE WHEN a.account_type = 'revenue' THEN i.credit - i.debit ELSE 0 END) AS revenue,
SUM(CASE WHEN a.account_type = 'expense' THEN i.debit - i.credit ELSE 0 END) AS expense
FROM td_gl g
JOIN td_gl_item i ON i.gl_id = g.id AND i.company_id = g.company_id
JOIN md_account a ON a.company_id = i.company_id AND a.account_code = i.account_code
WHERE g.company_id = :cid AND g.period >= :from_period
GROUP BY g.period
ORDER BY g.period ASC
$sth = $this->pdo->prepare("
SELECT period, acc_code, SUM(debit) AS d, SUM(credit) AS c
FROM etl_gl_summary
WHERE company_id = :cid AND acc_code >= 4000 AND period >= :from
GROUP BY period, acc_code
UNION ALL
SELECT g.period, i.account_code AS acc_code, SUM(i.debit) AS d, SUM(i.credit) AS c
FROM td_gl_item i
JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id
LEFT JOIN (
SELECT DISTINCT period FROM etl_gl_summary WHERE company_id = :cid2
) etl ON etl.period = g.period
WHERE i.company_id = :cid3
AND i.account_code >= 4000
AND g.source_type != 'voided'
AND g.period >= :from2
AND etl.period IS NULL
GROUP BY g.period, i.account_code
");
$sth->execute([':cid' => $this->companyId, ':from_period' => $from]);
return $sth->fetchAll(PDO::FETCH_ASSOC);
$sth->execute([
':cid' => $this->companyId, ':from' => $from,
':cid2' => $this->companyId,
':cid3' => $this->companyId, ':from2' => $from,
]);
$by_period = [];
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
$p = $row['period'];
$code = (int)$row['acc_code'];
if (!isset($by_period[$p])) $by_period[$p] = ['period' => $p, 'revenue' => 0.0, 'expense' => 0.0];
if ($code >= 4000 && $code < 5000) $by_period[$p]['revenue'] += (float)$row['c'] - (float)$row['d'];
else $by_period[$p]['expense'] += (float)$row['d'] - (float)$row['c'];
}
ksort($by_period);
return array_values($by_period);
}
// ── Dashboard focused methods ─────────────────────────────────────────────
@@ -260,29 +285,48 @@ class FinancialReports
public function getDashboardPL(): array
{
// ETL first — all periods present in etl_gl_summary
// Fallback — periods in td_gl but missing from ETL (gap fill)
$sth = $this->pdo->prepare("
SELECT
a.account_type,
COALESCE(SUM(i.debit), 0) AS total_debit,
COALESCE(SUM(i.credit), 0) AS total_credit
FROM md_account a
JOIN td_gl_item i ON i.company_id = a.company_id AND i.account_code = a.account_code
JOIN td_gl g ON g.company_id = i.company_id AND g.id = i.gl_id
WHERE a.company_id = :cid
AND a.account_type IN ('revenue', 'expense')
AND a.is_posting = 1
GROUP BY a.account_type
SELECT acc_code, SUM(debit) AS d, SUM(credit) AS c
FROM etl_gl_summary
WHERE company_id = :cid AND acc_code >= 4000
GROUP BY acc_code
UNION ALL
SELECT i.account_code AS acc_code, SUM(i.debit) AS d, SUM(i.credit) AS c
FROM td_gl_item i
JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id
LEFT JOIN (
SELECT DISTINCT period FROM etl_gl_summary WHERE company_id = :cid2
) etl ON etl.period = g.period
WHERE i.company_id = :cid3
AND i.account_code >= 4000
AND g.source_type != 'voided'
AND etl.period IS NULL
GROUP BY i.account_code
");
$sth->execute([':cid' => $this->companyId]);
$sth->execute([
':cid' => $this->companyId,
':cid2' => $this->companyId,
':cid3' => $this->companyId,
]);
$revenue = 0.0;
$expense = 0.0;
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
if ($row['account_type'] === 'revenue') $revenue += (float)$row['total_credit'] - (float)$row['total_debit'];
if ($row['account_type'] === 'expense') $expense += (float)$row['total_debit'] - (float)$row['total_credit'];
$code = (int)$row['acc_code'];
if ($code >= 4000 && $code < 5000) $revenue += (float)$row['c'] - (float)$row['d'];
else $expense += (float)$row['d'] - (float)$row['c'];
}
return ['revenue' => $revenue, 'expense' => $expense, 'net_profit' => $revenue - $expense];
return [
'revenue' => $revenue,
'expense' => $expense,
'net_profit' => $revenue - $expense,
'etl_gaps' => $this->getEtlGaps(),
];
}
public function getDashboardJournalCount(): int
@@ -404,6 +448,30 @@ class FinancialReports
];
}
// Returns periods that exist in td_gl but are missing from etl_gl_summary.
// Used by getDashboardPL() to signal which periods need a rebuild queue job.
private function getEtlGaps(string $from_period = ''): array
{
$sql = "
SELECT DISTINCT g.period
FROM td_gl g
LEFT JOIN (
SELECT DISTINCT period FROM etl_gl_summary WHERE company_id = :cid2
) etl ON etl.period = g.period
WHERE g.company_id = :cid
AND g.source_type NOT IN ('voided', 'reversal')
AND etl.period IS NULL
" . ($from_period ? "AND g.period >= :from_period" : "") . "
ORDER BY g.period
";
$params = [':cid' => $this->companyId, ':cid2' => $this->companyId];
if ($from_period) $params[':from_period'] = $from_period;
$sth = $this->pdo->prepare($sql);
$sth->execute($params);
return $sth->fetchAll(PDO::FETCH_COLUMN);
}
// ── GL Movement ───────────────────────────────────────────────────────────
// Per-account ledger: opening balance + chronological lines with running balance.