Compare commits
2 Commits
7da2b304b4
...
5833779c10
| Author | SHA1 | Date | |
|---|---|---|---|
| 5833779c10 | |||
| c61115f665 |
@@ -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
|
||||
router.get('/products', async (req, res) => {
|
||||
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) {
|
||||
// Calculate previous period dates
|
||||
let prevWhereClause, prevParams;
|
||||
|
||||
1488
inventory/src/components/dashboard/FinancialOverview.tsx
Normal file
1488
inventory/src/components/dashboard/FinancialOverview.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import { ThemeProvider } from "@/components/dashboard/theme/ThemeProvider";
|
||||
import AircallDashboard from "@/components/dashboard/AircallDashboard";
|
||||
import EventFeed from "@/components/dashboard/EventFeed";
|
||||
import StatCards from "@/components/dashboard/StatCards";
|
||||
import FinancialOverview from "@/components/dashboard/FinancialOverview";
|
||||
import ProductGrid from "@/components/dashboard/ProductGrid";
|
||||
import SalesChart from "@/components/dashboard/SalesChart";
|
||||
import KlaviyoCampaigns from "@/components/dashboard/KlaviyoCampaigns";
|
||||
@@ -42,6 +43,11 @@ export function Dashboard() {
|
||||
</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 id="feed" className="col-span-12 lg:col-span-6 xl:col-span-4 h-[600px] lg:h-[740px]">
|
||||
<EventFeed />
|
||||
|
||||
@@ -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
|
||||
getProjection: async (params) => {
|
||||
const cacheKey = `projection_${JSON.stringify(params)}`;
|
||||
|
||||
71
inventory/src/types/dashboard-shims.d.ts
vendored
Normal file
71
inventory/src/types/dashboard-shims.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user