feat: convert financials template to EJS and add database queries
ARCHITECTURE FIX (per Gemini consultation):
Old JavaScript template literals converted to proper EJS
CHANGES:
1. Converted admin/financials/index.ejs from JavaScript to EJS
- Changed ${variable} to <%= variable %>
- Removed let bodyContent wrapper
- Added EJS loops for tier breakdown table
2. Created FinancialsService.js with PostgreSQL queries:
- Active subscribers count
- Recognized MRR calculation
- At-risk subscribers (grace period)
- Lifetime revenue (Awakened + Sovereign)
- Fire vs Frost path breakdown
- Tier-level performance metrics
3. Updated admin.js financials route:
- Import FinancialsService
- Fetch real data from database
- Pass metrics, paths, tierBreakdown to template
TESTING:
Visit /admin/financials - should show real subscription data
Credit: Gemini consultation - template architecture fix
Signed-off-by: Claude (Chronicler #57) <claude@firefrostgaming.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getRoleMappings, saveRoleMappings } = require('../utils/roleMappings');
|
||||
const { getFinancialMetrics } = require('../services/FinancialsService');
|
||||
|
||||
const isAdmin = (req, res, next) => {
|
||||
if (req.isAuthenticated()) {
|
||||
@@ -107,11 +108,17 @@ router.get('/audit', isAdmin, async (req, res) => {
|
||||
// Financials Module
|
||||
router.get('/financials', isAdmin, async (req, res) => {
|
||||
try {
|
||||
// Fetch real financial data from PostgreSQL
|
||||
const financialData = await getFinancialMetrics();
|
||||
|
||||
res.render('admin/financials/index', {
|
||||
title: 'Financials',
|
||||
adminUser: req.user,
|
||||
csrfToken: req.csrfToken(),
|
||||
currentPath: '/financials'
|
||||
currentPath: '/financials',
|
||||
metrics: financialData.metrics,
|
||||
paths: financialData.paths,
|
||||
tierBreakdown: financialData.tierBreakdown
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Financials error:', error);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// Build the body content as a string variable
|
||||
let bodyContent = `
|
||||
<div class="mb-6 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold dark:text-white">💰 Revenue Analytics</h1>
|
||||
@@ -12,39 +10,37 @@ let bodyContent = `
|
||||
<!-- Active Subscribers -->
|
||||
<div class="bg-white dark:bg-darkcard rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">Active Subscribers</div>
|
||||
<div class="text-2xl font-bold dark:text-white">${metrics.activeSubs}</div>
|
||||
<div class="text-2xl font-bold dark:text-white"><%= metrics.activeSubs %></div>
|
||||
</div>
|
||||
|
||||
<!-- Recognized MRR -->
|
||||
<div class="bg-white dark:bg-darkcard rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">Monthly Revenue</div>
|
||||
<div class="text-2xl font-bold text-green-600 dark:text-green-400">$${metrics.recognizedMrr.toFixed(2)}</div>
|
||||
<div class="text-2xl font-bold text-green-600 dark:text-green-400">$<%= metrics.recognizedMrr.toFixed(2) %></div>
|
||||
</div>
|
||||
|
||||
<!-- ARR -->
|
||||
<div class="bg-white dark:bg-darkcard rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">Annual Run Rate</div>
|
||||
<div class="text-2xl font-bold dark:text-white">$${metrics.arr}</div>
|
||||
<div class="text-2xl font-bold dark:text-white">$<%= (metrics.recognizedMrr * 12).toFixed(2) %></div>
|
||||
</div>
|
||||
|
||||
<!-- At Risk -->
|
||||
<div class="bg-white dark:bg-darkcard rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">At Risk</div>
|
||||
<div class="text-2xl font-bold text-yellow-600 dark:text-yellow-400">${metrics.atRiskSubs}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">$${metrics.atRiskMrr.toFixed(2)} MRR</div>
|
||||
<div class="text-2xl font-bold text-yellow-600 dark:text-yellow-400"><%= metrics.atRiskSubs %></div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">$<%= metrics.atRiskMrr.toFixed(2) %> MRR</div>
|
||||
</div>
|
||||
|
||||
<!-- Lifetime Revenue -->
|
||||
<div class="bg-white dark:bg-darkcard rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">Lifetime Revenue</div>
|
||||
<div class="text-2xl font-bold text-purple-600 dark:text-purple-400">$${metrics.lifetimeRevenue.toFixed(2)}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">${metrics.lifetimeSubs} Sovereign</div>
|
||||
<div class="text-2xl font-bold text-purple-600 dark:text-purple-400">$<%= metrics.lifetimeRevenue.toFixed(2) %></div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400"><%= metrics.lifetimeSubs %> Sovereign</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Fire vs Frost Path Comparison
|
||||
bodyContent += `
|
||||
<!-- Fire vs Frost Path Comparison -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Fire Path -->
|
||||
<div class="bg-gradient-to-br from-orange-50 to-red-50 dark:from-gray-800 dark:to-gray-800 rounded-lg p-6 border-2 border-orange-500">
|
||||
@@ -58,11 +54,11 @@ bodyContent += `
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-700 dark:text-gray-300">Subscribers:</span>
|
||||
<span class="font-bold dark:text-white">${paths.fire.subs}</span>
|
||||
<span class="font-bold dark:text-white"><%= paths.fire.subs %></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-700 dark:text-gray-300">Monthly Revenue:</span>
|
||||
<span class="font-bold text-green-600 dark:text-green-400">$${paths.fire.mrr.toFixed(2)}</span>
|
||||
<span class="font-bold text-green-600 dark:text-green-400">$<%= paths.fire.mrr.toFixed(2) %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,19 +75,17 @@ bodyContent += `
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-700 dark:text-gray-300">Subscribers:</span>
|
||||
<span class="font-bold dark:text-white">${paths.frost.subs}</span>
|
||||
<span class="font-bold dark:text-white"><%= paths.frost.subs %></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-700 dark:text-gray-300">Monthly Revenue:</span>
|
||||
<span class="font-bold text-green-600 dark:text-green-400">$${paths.frost.mrr.toFixed(2)}</span>
|
||||
<span class="font-bold text-green-600 dark:text-green-400">$<%= paths.frost.mrr.toFixed(2) %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Tier Breakdown Table
|
||||
bodyContent += `
|
||||
<!-- Tier Breakdown Table -->
|
||||
<div class="bg-white dark:bg-darkcard rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-xl font-bold dark:text-white">Tier Performance</h2>
|
||||
@@ -109,41 +103,36 @@ bodyContent += `
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
`;
|
||||
|
||||
// Loop through tiers and add rows
|
||||
Object.keys(tierBreakdown).sort((a, b) => parseInt(b) - parseInt(a)).forEach(tierLevel => {
|
||||
const tier = tierBreakdown[tierLevel];
|
||||
const pathColor = tier.path === 'fire' ? 'text-orange-600 dark:text-orange-400' :
|
||||
tier.path === 'frost' ? 'text-cyan-600 dark:text-cyan-400' :
|
||||
'text-purple-600 dark:text-purple-400';
|
||||
|
||||
bodyContent += `
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium dark:text-white">${tier.name}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm font-medium ${pathColor}">${tier.path.charAt(0).toUpperCase() + tier.path.slice(1)}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<span class="text-sm dark:text-white">${tier.activeCount}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<span class="text-sm text-yellow-600 dark:text-yellow-400">${tier.graceCount}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<span class="text-sm font-medium text-green-600 dark:text-green-400">$${tier.totalMrr.toFixed(2)}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
bodyContent += `
|
||||
<% if (tierBreakdown && Object.keys(tierBreakdown).length > 0) { %>
|
||||
<% Object.keys(tierBreakdown).sort((a, b) => parseInt(b) - parseInt(a)).forEach(tierLevel => { %>
|
||||
<% const tier = tierBreakdown[tierLevel]; %>
|
||||
<% const pathColor = tier.path === 'fire' ? 'text-orange-600 dark:text-orange-400' :
|
||||
tier.path === 'frost' ? 'text-cyan-600 dark:text-cyan-400' :
|
||||
'text-purple-600 dark:text-purple-400'; %>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium dark:text-white"><%= tier.name %></div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm font-medium <%= pathColor %>"><%= tier.path.charAt(0).toUpperCase() + tier.path.slice(1) %></span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<span class="text-sm dark:text-white"><%= tier.activeCount %></span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<span class="text-sm text-yellow-600 dark:text-yellow-400"><%= tier.graceCount %></span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<span class="text-sm font-medium text-green-600 dark:text-green-400">$<%= tier.totalMrr.toFixed(2) %></span>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
<% } else { %>
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">No subscription data available</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
%>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user