Add in financial overview component with related routes

This commit is contained in:
2025-09-18 12:04:20 -04:00
parent 7da2b304b4
commit c61115f665
5 changed files with 1547 additions and 5 deletions

View File

@@ -410,6 +410,68 @@ router.get('/stats/details', async (req, res) => {
} }
}); });
// Financial performance endpoint
router.get('/financials', async (req, res) => {
let release;
try {
const { timeRange, startDate, endDate } = req.query;
const { connection, release: releaseConn } = await getDbConnection();
release = releaseConn;
const { whereClause, params, dateRange } = getTimeRangeConditions(timeRange, startDate, endDate);
const financialWhere = whereClause.replace(/date_placed/g, 'date_change');
const [totalsRows] = await connection.execute(
buildFinancialTotalsQuery(financialWhere),
params
);
const totals = normalizeFinancialTotals(totalsRows[0]);
const [trendRows] = await connection.execute(
buildFinancialTrendQuery(financialWhere),
params
);
const trend = trendRows.map(normalizeFinancialTrendRow);
let previousTotals = null;
let comparison = null;
const previousRange = getPreviousPeriodRange(timeRange, startDate, endDate);
if (previousRange) {
const prevWhere = previousRange.whereClause.replace(/date_placed/g, 'date_change');
const [previousRows] = await connection.execute(
buildFinancialTotalsQuery(prevWhere),
previousRange.params
);
previousTotals = normalizeFinancialTotals(previousRows[0]);
comparison = {
grossSales: calculateComparison(totals.grossSales, previousTotals.grossSales),
refunds: calculateComparison(totals.refunds, previousTotals.refunds),
taxCollected: calculateComparison(totals.taxCollected, previousTotals.taxCollected),
cogs: calculateComparison(totals.cogs, previousTotals.cogs),
netRevenue: calculateComparison(totals.netRevenue, previousTotals.netRevenue),
profit: calculateComparison(totals.profit, previousTotals.profit),
margin: calculateComparison(totals.margin, previousTotals.margin),
};
}
res.json({
dateRange,
totals,
previousTotals,
comparison,
trend,
});
} catch (error) {
console.error('Error in /financials:', error);
res.status(500).json({ error: error.message });
} finally {
if (release) release();
}
});
// Products endpoint - replaces /api/klaviyo/events/products // Products endpoint - replaces /api/klaviyo/events/products
router.get('/products', async (req, res) => { router.get('/products', async (req, res) => {
let release; let release;
@@ -639,6 +701,132 @@ function calculatePeriodProgress(timeRange) {
} }
} }
function buildFinancialTotalsQuery(whereClause) {
return `
SELECT
COALESCE(SUM(sale_amount), 0) as grossSales,
COALESCE(SUM(refund_amount), 0) as refunds,
COALESCE(SUM(tax_collected_amount), 0) as taxCollected,
COALESCE(SUM(cogs_amount), 0) as cogs
FROM report_sales_data
WHERE ${whereClause}
`;
}
function buildFinancialTrendQuery(whereClause) {
return `
SELECT
DATE(date_change) as date,
SUM(sale_amount) as grossSales,
SUM(refund_amount) as refunds,
SUM(tax_collected_amount) as taxCollected,
SUM(cogs_amount) as cogs
FROM report_sales_data
WHERE ${whereClause}
GROUP BY DATE(date_change)
ORDER BY date ASC
`;
}
function normalizeFinancialTotals(row = {}) {
const grossSales = parseFloat(row.grossSales || 0);
const refunds = parseFloat(row.refunds || 0);
const taxCollected = parseFloat(row.taxCollected || 0);
const cogs = parseFloat(row.cogs || 0);
const netSales = grossSales - refunds;
const netRevenue = netSales - taxCollected;
const profit = netRevenue - cogs;
const margin = netRevenue !== 0 ? (profit / netRevenue) * 100 : 0;
return {
grossSales,
refunds,
taxCollected,
cogs,
netSales,
netRevenue,
profit,
margin,
};
}
function normalizeFinancialTrendRow(row = {}) {
const grossSales = parseFloat(row.grossSales || 0);
const refunds = parseFloat(row.refunds || 0);
const taxCollected = parseFloat(row.taxCollected || 0);
const cogs = parseFloat(row.cogs || 0);
const netSales = grossSales - refunds;
const netRevenue = netSales - taxCollected;
const profit = netRevenue - cogs;
const margin = netRevenue !== 0 ? (profit / netRevenue) * 100 : 0;
let timestamp = null;
if (row.date instanceof Date) {
timestamp = new Date(row.date.getTime()).toISOString();
} else if (typeof row.date === 'string') {
timestamp = new Date(`${row.date}T00:00:00Z`).toISOString();
}
return {
date: row.date,
grossSales,
refunds,
taxCollected,
cogs,
netSales,
netRevenue,
profit,
margin,
timestamp,
};
}
function calculateComparison(currentValue, previousValue) {
if (typeof previousValue !== 'number') {
return { absolute: null, percentage: null };
}
const absolute = typeof currentValue === 'number' ? currentValue - previousValue : null;
const percentage =
absolute !== null && previousValue !== 0
? (absolute / Math.abs(previousValue)) * 100
: null;
return { absolute, percentage };
}
function getPreviousPeriodRange(timeRange, startDate, endDate) {
if (timeRange && timeRange !== 'custom') {
const prevTimeRange = getPreviousTimeRange(timeRange);
if (!prevTimeRange || prevTimeRange === timeRange) {
return null;
}
return getTimeRangeConditions(prevTimeRange);
}
const hasCustomDates = (timeRange === 'custom' || !timeRange) && startDate && endDate;
if (!hasCustomDates) {
return null;
}
const start = new Date(startDate);
const end = new Date(endDate);
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
return null;
}
const duration = end.getTime() - start.getTime();
if (!Number.isFinite(duration) || duration <= 0) {
return null;
}
const prevEnd = new Date(start.getTime() - 1);
const prevStart = new Date(prevEnd.getTime() - duration);
return getTimeRangeConditions('custom', prevStart.toISOString(), prevEnd.toISOString());
}
async function getPreviousPeriodData(connection, timeRange, startDate, endDate) { async function getPreviousPeriodData(connection, timeRange, startDate, endDate) {
// Calculate previous period dates // Calculate previous period dates
let prevWhereClause, prevParams; let prevWhereClause, prevParams;

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ import { ThemeProvider } from "@/components/dashboard/theme/ThemeProvider";
import AircallDashboard from "@/components/dashboard/AircallDashboard"; import AircallDashboard from "@/components/dashboard/AircallDashboard";
import EventFeed from "@/components/dashboard/EventFeed"; import EventFeed from "@/components/dashboard/EventFeed";
import StatCards from "@/components/dashboard/StatCards"; import StatCards from "@/components/dashboard/StatCards";
import FinancialOverview from "@/components/dashboard/FinancialOverview";
import ProductGrid from "@/components/dashboard/ProductGrid"; import ProductGrid from "@/components/dashboard/ProductGrid";
import SalesChart from "@/components/dashboard/SalesChart"; import SalesChart from "@/components/dashboard/SalesChart";
import KlaviyoCampaigns from "@/components/dashboard/KlaviyoCampaigns"; import KlaviyoCampaigns from "@/components/dashboard/KlaviyoCampaigns";
@@ -42,6 +43,11 @@ export function Dashboard() {
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-12 gap-4">
<div id="financial" className="col-span-12">
<FinancialOverview />
</div>
</div>
<div className="grid grid-cols-12 gap-4"> <div className="grid grid-cols-12 gap-4">
<div id="feed" className="col-span-12 lg:col-span-6 xl:col-span-4 h-[600px] lg:h-[740px]"> <div id="feed" className="col-span-12 lg:col-span-6 xl:col-span-4 h-[600px] lg:h-[740px]">
<EventFeed /> <EventFeed />

View File

@@ -157,6 +157,17 @@ export const acotService = {
); );
}, },
// Get financial performance data
getFinancials: async (params) => {
const cacheKey = `financials_${JSON.stringify(params)}`;
return deduplicatedRequest(cacheKey, () =>
retryRequest(async () => {
const response = await acotApi.get('/api/acot/events/financials', { params });
return response.data;
})
);
},
// Get projections - replaces klaviyo events/projection // Get projections - replaces klaviyo events/projection
getProjection: async (params) => { getProjection: async (params) => {
const cacheKey = `projection_${JSON.stringify(params)}`; const cacheKey = `projection_${JSON.stringify(params)}`;

View File

@@ -0,0 +1,71 @@
declare module "@/services/dashboard/acotService" {
const acotService: {
getFinancials: (params: unknown) => Promise<unknown>;
[key: string]: (...args: never[]) => Promise<unknown> | unknown;
};
export { acotService };
export default acotService;
}
declare module "@/services/dashboard/*" {
const value: any;
export default value;
}
declare module "@/lib/dashboard/constants" {
type TimeRangeOption = {
value: string;
label: string;
};
export const TIME_RANGES: TimeRangeOption[];
export const GROUP_BY_OPTIONS: TimeRangeOption[];
export const formatDateForInput: (date: Date | string | number | null) => string;
export const parseDateFromInput: (dateString: string) => Date | null;
}
declare module "@/lib/dashboard/*" {
const value: any;
export default value;
}
declare module "@/components/dashboard/ui/card" {
export const Card: React.ComponentType<any>;
export const CardContent: React.ComponentType<any>;
export const CardDescription: React.ComponentType<any>;
export const CardHeader: React.ComponentType<any>;
export const CardTitle: React.ComponentType<any>;
}
declare module "@/components/dashboard/ui/button" {
export const Button: React.ComponentType<any>;
}
declare module "@/components/dashboard/ui/select" {
export const Select: React.ComponentType<any>;
export const SelectContent: React.ComponentType<any>;
export const SelectItem: React.ComponentType<any>;
export const SelectTrigger: React.ComponentType<any>;
export const SelectValue: React.ComponentType<any>;
}
declare module "@/components/dashboard/ui/alert" {
export const Alert: React.ComponentType<any>;
export const AlertDescription: React.ComponentType<any>;
export const AlertTitle: React.ComponentType<any>;
}
declare module "@/components/dashboard/ui/skeleton" {
export const Skeleton: React.ComponentType<any>;
}
declare module "@/components/dashboard/ui/*" {
const components: any;
export default components;
}
declare module "@/components/dashboard/ui/toggle-group" {
export const ToggleGroup: React.ComponentType<any>;
export const ToggleGroupItem: React.ComponentType<any>;
}