mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-02-05 08:16:31 -05:00
feat: Implement a new dashboard shell with animated background, refactor dashboard data fetching into a dedicated API route, and introduce new UI components.**
This commit is contained in:
@@ -3,6 +3,7 @@ import { businessesRouter } from "~/server/api/routers/businesses";
|
||||
import { invoicesRouter } from "~/server/api/routers/invoices";
|
||||
import { settingsRouter } from "~/server/api/routers/settings";
|
||||
import { emailRouter } from "~/server/api/routers/email";
|
||||
import { dashboardRouter } from "~/server/api/routers/dashboard";
|
||||
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
|
||||
|
||||
/**
|
||||
@@ -16,6 +17,7 @@ export const appRouter = createTRPCRouter({
|
||||
invoices: invoicesRouter,
|
||||
settings: settingsRouter,
|
||||
email: emailRouter,
|
||||
dashboard: dashboardRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
126
src/server/api/routers/dashboard.ts
Normal file
126
src/server/api/routers/dashboard.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
import { invoices, clients } from "~/server/db/schema";
|
||||
import { eq, and, desc, sql, gte, lt } from "drizzle-orm";
|
||||
|
||||
export const dashboardRouter = createTRPCRouter({
|
||||
getStats: protectedProcedure.query(async ({ ctx }) => {
|
||||
const userId = ctx.session.user.id;
|
||||
const now = new Date();
|
||||
const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
|
||||
// 1. Fetch all invoices for the user to calculate stats
|
||||
// Note: For very large datasets, we should use separate count/sum queries,
|
||||
// but for typical usage, fetching fields is fine and allows flexible JS calculation
|
||||
// where SQL complexity might be high (e.g. dynamic status).
|
||||
// However, let's try to be efficient with SQL where possible.
|
||||
|
||||
const userInvoices = await ctx.db.query.invoices.findMany({
|
||||
where: eq(invoices.createdById, userId),
|
||||
columns: {
|
||||
id: true,
|
||||
totalAmount: true,
|
||||
status: true,
|
||||
dueDate: true,
|
||||
issueDate: true,
|
||||
},
|
||||
});
|
||||
|
||||
const userClientsCount = await ctx.db.$count(
|
||||
clients,
|
||||
eq(clients.createdById, userId),
|
||||
);
|
||||
|
||||
// Helper to check status
|
||||
const getStatus = (inv: typeof userInvoices[0]) => {
|
||||
if (inv.status === "paid") return "paid";
|
||||
if (inv.status === "draft") return "draft";
|
||||
if (new Date(inv.dueDate) < now && inv.status !== "paid") return "overdue";
|
||||
return "sent";
|
||||
};
|
||||
|
||||
// Calculate Stats
|
||||
let totalRevenue = 0;
|
||||
let pendingAmount = 0;
|
||||
let overdueCount = 0;
|
||||
|
||||
let currentMonthRevenue = 0;
|
||||
let lastMonthRevenue = 0;
|
||||
|
||||
for (const inv of userInvoices) {
|
||||
const status = getStatus(inv);
|
||||
const amount = inv.totalAmount;
|
||||
const issueDate = new Date(inv.issueDate);
|
||||
|
||||
if (status === "paid") {
|
||||
totalRevenue += amount;
|
||||
|
||||
if (issueDate >= currentMonthStart) {
|
||||
currentMonthRevenue += amount;
|
||||
} else if (issueDate >= lastMonthStart && issueDate < currentMonthStart) {
|
||||
lastMonthRevenue += amount;
|
||||
}
|
||||
} else if (status === "sent" || status === "overdue") {
|
||||
pendingAmount += amount;
|
||||
}
|
||||
|
||||
if (status === "overdue") {
|
||||
overdueCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Revenue Trend (Last 6 months)
|
||||
const revenueByMonth: Record<string, number> = {};
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
||||
revenueByMonth[key] = 0;
|
||||
}
|
||||
|
||||
for (const inv of userInvoices) {
|
||||
if (getStatus(inv) === "paid") {
|
||||
const d = new Date(inv.issueDate);
|
||||
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
||||
if (revenueByMonth[key] !== undefined) {
|
||||
revenueByMonth[key] += inv.totalAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const revenueChartData = Object.entries(revenueByMonth)
|
||||
.map(([month, revenue]) => ({
|
||||
month,
|
||||
revenue,
|
||||
monthLabel: new Date(month + "-01").toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
year: "2-digit",
|
||||
}),
|
||||
}))
|
||||
.sort((a, b) => a.month.localeCompare(b.month));
|
||||
|
||||
// Recent Activity
|
||||
const recentInvoices = await ctx.db.query.invoices.findMany({
|
||||
where: eq(invoices.createdById, userId),
|
||||
orderBy: [desc(invoices.issueDate)],
|
||||
limit: 5,
|
||||
with: {
|
||||
client: {
|
||||
columns: { name: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
totalRevenue,
|
||||
pendingAmount,
|
||||
overdueCount,
|
||||
totalClients: userClientsCount,
|
||||
revenueChange: lastMonthRevenue > 0
|
||||
? ((currentMonthRevenue - lastMonthRevenue) / lastMonthRevenue) * 100
|
||||
: 0,
|
||||
revenueChartData,
|
||||
recentInvoices,
|
||||
};
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user