NODE JS introduction: socket polling
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ class GlManager
|
||||
':updated_at' => $now,
|
||||
]);
|
||||
$this->insertLines((int)$this->pdo->lastInsertId(), $lines);
|
||||
$this->upsertEtl($lines, $period);
|
||||
}
|
||||
|
||||
public function replaceManual(int $gl_id, string $reference, string $description, string $journal_date, string $period, array $lines): void
|
||||
@@ -99,6 +100,9 @@ class GlManager
|
||||
'lines' => $this->getLines($gl_id),
|
||||
];
|
||||
|
||||
$old_lines = $history[count($history) - 1]['lines']; // captured just above
|
||||
$old_period = $gl['period'];
|
||||
|
||||
$this->pdo->prepare(
|
||||
"DELETE FROM td_gl_item WHERE company_id = :cid AND gl_id = :gl_id"
|
||||
)->execute([':cid' => $this->companyId, ':gl_id' => $gl_id]);
|
||||
@@ -122,6 +126,8 @@ class GlManager
|
||||
]);
|
||||
|
||||
$this->insertLines($gl_id, $lines);
|
||||
$this->upsertEtl($old_lines, $old_period, -1);
|
||||
$this->upsertEtl($lines, $period);
|
||||
}
|
||||
|
||||
public function postManual(string $reference, string $description, string $journal_date, string $period, array $lines): int
|
||||
@@ -148,6 +154,7 @@ class GlManager
|
||||
]);
|
||||
$gl_id = (int)$this->pdo->lastInsertId();
|
||||
$this->insertLines($gl_id, $lines);
|
||||
$this->upsertEtl($lines, $period);
|
||||
return $gl_id;
|
||||
}
|
||||
|
||||
@@ -179,11 +186,14 @@ class GlManager
|
||||
$history = json_decode($gl['history'], true) ?: [];
|
||||
|
||||
// Snapshot current lines into history
|
||||
$old_lines = $this->getLines($gl_id);
|
||||
$old_period = $gl['period'];
|
||||
|
||||
$history[] = [
|
||||
'version' => $version,
|
||||
'formula_id' => (int)$gl['formula_id'],
|
||||
'replaced_at' => $now,
|
||||
'lines' => $this->getLines($gl_id),
|
||||
'lines' => $old_lines,
|
||||
];
|
||||
|
||||
$this->pdo->prepare(
|
||||
@@ -210,6 +220,8 @@ class GlManager
|
||||
]);
|
||||
|
||||
$this->insertLines($gl_id, $lines);
|
||||
$this->upsertEtl($old_lines, $old_period, -1);
|
||||
$this->upsertEtl($lines, $period);
|
||||
}
|
||||
|
||||
public function delete(string $source_type, int $source_id): void
|
||||
@@ -264,7 +276,9 @@ class GlManager
|
||||
':created_at' => $now,
|
||||
':updated_at' => $now,
|
||||
]);
|
||||
$this->insertLines((int)$this->pdo->lastInsertId(), $reversal_lines);
|
||||
$reversal_gl_id = (int)$this->pdo->lastInsertId();
|
||||
$this->insertLines($reversal_gl_id, $reversal_lines);
|
||||
$this->upsertEtl($reversal_lines, $gl['period']);
|
||||
}
|
||||
|
||||
// Mark original as voided — keeps audit trail; getBySource() won't match 'voided' source_type
|
||||
@@ -326,4 +340,32 @@ class GlManager
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// sign=1 to add, sign=-1 to subtract (used when replacing old lines)
|
||||
private function upsertEtl(array $lines, string $period, int $sign = 1): void
|
||||
{
|
||||
if (empty($lines) || empty($period)) return;
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"INSERT INTO etl_gl_summary
|
||||
(company_id, acc_code, period, debit, credit, count, updated_at)
|
||||
VALUES
|
||||
(:cid, :acc, :period, :debit, :credit, :cnt, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
debit = debit + VALUES(debit),
|
||||
credit = credit + VALUES(credit),
|
||||
count = count + VALUES(count),
|
||||
updated_at = NOW()"
|
||||
);
|
||||
foreach ($lines as $line) {
|
||||
$sth->execute([
|
||||
':cid' => $this->companyId,
|
||||
':acc' => (int)($line['account_code'] ?? 0),
|
||||
':period' => $period,
|
||||
':debit' => (float)($line['debit'] ?? 0) * $sign,
|
||||
':credit' => (float)($line['credit'] ?? 0) * $sign,
|
||||
':cnt' => $sign,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user