diff --git a/apps/backend/src/lib/data-manager.ts b/apps/backend/src/lib/data-manager.ts
index 3ea2edf..5bf04ac 100644
--- a/apps/backend/src/lib/data-manager.ts
+++ b/apps/backend/src/lib/data-manager.ts
@@ -11,6 +11,7 @@ import {
createRolesRepo,
createCustomersRepo,
createBillsRepo,
+ createDashboardRepo,
type StorageSpacesRepo,
type DistributorsRepo,
type ProductsRepo,
@@ -23,6 +24,7 @@ import {
type RolesRepo,
type CustomersRepo,
type BillsRepo,
+ type DashboardRepo,
} from "data-manager-sqlite";
export class DataManager {
@@ -38,6 +40,7 @@ export class DataManager {
readonly roles: RolesRepo;
readonly customers: CustomersRepo;
readonly bills: BillsRepo;
+ readonly dashboard: DashboardRepo;
constructor() {
const { repo: storageSpacesRepo } = createStorageSpacesRepo();
@@ -52,6 +55,7 @@ export class DataManager {
const { repo: rolesRepo } = createRolesRepo();
const { repo: customersRepo } = createCustomersRepo();
const { repo: billsRepo } = createBillsRepo();
+ const { repo: dashboardRepo } = createDashboardRepo();
this.storageSpaces = storageSpacesRepo;
this.distributors = distributorsRepo;
@@ -65,5 +69,6 @@ export class DataManager {
this.roles = rolesRepo;
this.customers = customersRepo;
this.bills = billsRepo;
+ this.dashboard = dashboardRepo;
}
}
diff --git a/apps/backend/src/trpc/pharmanager/v1/dashboard.ts b/apps/backend/src/trpc/pharmanager/v1/dashboard.ts
new file mode 100644
index 0000000..fc06784
--- /dev/null
+++ b/apps/backend/src/trpc/pharmanager/v1/dashboard.ts
@@ -0,0 +1,9 @@
+import { protectedProcedure, router } from "../../init";
+import { dataManager } from "../../../lib/data-manager-instance";
+import { DashboardStatsSchema } from "@repo/shared";
+
+export const dashboardRouter = router({
+ getStats: protectedProcedure
+ .output(DashboardStatsSchema)
+ .query(({ ctx }) => dataManager.dashboard.getStats(ctx.staff.enterpriseId)),
+});
diff --git a/apps/backend/src/trpc/router.ts b/apps/backend/src/trpc/router.ts
index b5abf87..7fbba72 100644
--- a/apps/backend/src/trpc/router.ts
+++ b/apps/backend/src/trpc/router.ts
@@ -10,6 +10,7 @@ import { staffManagementRouter } from "./pharmanager/v1/staffManagement";
import { rolesRouter } from "./pharmanager/v1/roles";
import { billingRouter } from "./pharmanager/v1/billing";
import { customersRouter } from "./pharmanager/v1/customers";
+import { dashboardRouter } from "./pharmanager/v1/dashboard";
export const appRouter = router({
storage: storageRouter,
@@ -23,6 +24,7 @@ export const appRouter = router({
roles: rolesRouter,
billing: billingRouter,
customers: customersRouter,
+ dashboard: dashboardRouter,
});
export type AppRouter = typeof appRouter;
diff --git a/apps/pharmanager/src/routes/index.tsx b/apps/pharmanager/src/routes/index.tsx
index a96e587..d66d09b 100644
--- a/apps/pharmanager/src/routes/index.tsx
+++ b/apps/pharmanager/src/routes/index.tsx
@@ -1,4 +1,16 @@
-import { createFileRoute } from "@tanstack/react-router";
+import { createFileRoute, Link } from "@tanstack/react-router";
+import {
+ Pill,
+ AlertTriangle,
+ DollarSign,
+ Clock,
+ Package,
+ ShoppingCart,
+ Users,
+ Plus,
+ ChevronRight,
+} from "lucide-react";
+import { useDashboard } from "shared-react";
export const Route = createFileRoute("/")({
component: HomePage,
@@ -8,6 +20,248 @@ export const Route = createFileRoute("/")({
},
});
-function HomePage() {
- return
Home
;
+function statStatusClass(qty: number, reorderLevel: number) {
+ if (qty <= 0) return "bg-red-50 text-red-600 border-red-200";
+ if (qty <= reorderLevel * 0.5) return "bg-red-50 text-red-600 border-red-200";
+ if (qty <= reorderLevel) return "bg-amber-50 text-amber-600 border-amber-200";
+ return "bg-emerald-50 text-emerald-600 border-emerald-200";
+}
+
+function statStatusLabel(qty: number, reorderLevel: number) {
+ if (qty <= 0) return "Critical";
+ if (qty <= reorderLevel * 0.5) return "Critical";
+ if (qty <= reorderLevel) return "Low Stock";
+ return "In Stock";
+}
+
+function HomePage() {
+ const { data, isLoading } = useDashboard();
+
+ if (isLoading || !data) {
+ return Loading dashboard...
;
+ }
+
+ return (
+
+ {/* Stats Row */}
+
+
+
+
+
{data.totalProducts}
+
Total Medicines
+
+
+
+
+
+
+
{data.lowStockProducts}
+
Low Stock Items
+
+
+
+
+
+
+
+
+
₹{data.todaySales.toLocaleString("en-IN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+
Today's Sales
+
+
+
+
+
+
+
+
+
{data.expiringBatches}
+
Expiring Soon
+
+
+
+
+ {/* Inventory Table */}
+
+
+ Medicine Inventory
+
+ View All →
+
+
+
+
+
+
+ | Medicine |
+ Category |
+ Quantity |
+ Price |
+ Status |
+
+
+
+ {data.recentProducts.map((p) => (
+
+ |
+
+ {p.name}
+
+ {p.brand}
+ |
+ {p.category} |
+
+ {p.stock}
+ |
+ ₹{p.selling_price.toFixed(2)} |
+
+
+ • {statStatusLabel(p.stock, p.reorder_level)}
+
+ |
+
+ ))}
+
+
+
+
+
+ {/* Widgets Row */}
+
+ {/* Billing Summary */}
+
+
+ Billing Summary
+
+
₹{data.monthlyRevenue.toLocaleString("en-IN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+
Monthly Revenue
+
+
+ Today
+ ₹{data.todaySales.toLocaleString("en-IN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+
+
+ This Week
+ ₹{data.weeklyRevenue.toLocaleString("en-IN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+
+
+ Today Bills
+ {data.todayBillsCount}
+
+
+ Total Invoices
+ {data.totalInvoices}
+
+
+
+ View Billing Details
+
+
+
+
+ {/* Stock Overview */}
+
+
+ Stock Overview
+
+
{data.totalStockUnits.toLocaleString()}
+
Total Units in Stock
+
+
+ Active Medicines
+ {data.totalProducts}
+
+
+ • Low Stock
+ {data.lowStockProducts}
+
+
+ • Critical
+ {data.criticalStock}
+
+
+ Out of Stock
+ {data.outOfStock}
+
+
+
+ View All Batches
+
+
+
+
+ {/* Product Categories */}
+
+
+ Product Categories
+
+
{data.categoryBreakdown.length}
+
Medicine Categories
+
+ {data.categoryBreakdown.map((c) => (
+
+ {c.category}
+ {c.count}
+
+ ))}
+
+
+ Browse Products
+
+
+
+
+ {/* Quick Actions */}
+
+
+ Quick Actions
+
+
+
+
+ Add New Medicine
+
+
+
+ Add Stock Batch
+
+
+
+ New Bill
+
+
+
+ Customer List
+
+
+
+
+
+ );
}
diff --git a/packages/data-manager-sqlite/src/dashboard.ts b/packages/data-manager-sqlite/src/dashboard.ts
new file mode 100644
index 0000000..4015ed0
--- /dev/null
+++ b/packages/data-manager-sqlite/src/dashboard.ts
@@ -0,0 +1,231 @@
+import { count, sum, eq, and, gte, lt, lte } from 'drizzle-orm'
+import { db } from './db-instance'
+import { products } from './schema/products'
+import { bills } from './schema/bills'
+import { stockBatches } from './schema/stockBatches'
+import { units } from './schema/units'
+
+export interface DashboardStats {
+ totalProducts: number
+ lowStockProducts: number
+ todaySales: number
+ monthlyRevenue: number
+ weeklyRevenue: number
+ todayBillsCount: number
+ totalInvoices: number
+ totalStockUnits: number
+ outOfStock: number
+ criticalStock: number
+ expiringBatches: number
+ categoryBreakdown: { category: string; count: number }[]
+ recentProducts: {
+ id: number
+ name: string
+ brand: string
+ category: string
+ selling_price: number
+ stock: number
+ reorder_level: number
+ unit_name: string
+ }[]
+}
+
+export type DashboardRepo = {
+ getStats: (enterpriseId: number) => Promise
+}
+
+function todayStr(): string {
+ return new Date().toISOString().slice(0, 10)
+}
+
+function daysLater(n: number): string {
+ const d = new Date()
+ d.setDate(d.getDate() + n)
+ return d.toISOString().slice(0, 10)
+}
+
+function monthStartStr(): string {
+ const d = new Date()
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-01`
+}
+
+function nextMonthStartStr(): string {
+ const d = new Date()
+ const next = new Date(d.getFullYear(), d.getMonth() + 1, 1)
+ return next.toISOString().slice(0, 10)
+}
+
+function weekStartStr(): string {
+ const d = new Date()
+ const day = d.getDay()
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1)
+ const monday = new Date(d.getFullYear(), d.getMonth(), diff)
+ return monday.toISOString().slice(0, 10)
+}
+
+export function createDashboardRepo(): { repo: DashboardRepo } {
+ const repo: DashboardRepo = {
+ getStats(enterpriseId) {
+ const today = todayStr()
+ const tomorrow = daysLater(1)
+ const monthStart = monthStartStr()
+ const nextMonthStart = nextMonthStartStr()
+ const weekStart = weekStartStr()
+ const thirtyDays = daysLater(30)
+
+ const totalProducts = db
+ .select({ count: count() })
+ .from(products)
+ .where(eq(products.enterpriseId, enterpriseId))
+ .get()!
+
+ const todaySales = db
+ .select({ total: sum(bills.total) })
+ .from(bills)
+ .where(
+ and(
+ eq(bills.enterpriseId, enterpriseId),
+ gte(bills.createdAt, today),
+ lt(bills.createdAt, tomorrow),
+ ),
+ )
+ .get()!
+
+ const monthlyRevenue = db
+ .select({ total: sum(bills.total) })
+ .from(bills)
+ .where(
+ and(
+ eq(bills.enterpriseId, enterpriseId),
+ gte(bills.createdAt, monthStart),
+ lt(bills.createdAt, nextMonthStart),
+ ),
+ )
+ .get()!
+
+ const weeklyRevenue = db
+ .select({ total: sum(bills.total) })
+ .from(bills)
+ .where(
+ and(
+ eq(bills.enterpriseId, enterpriseId),
+ gte(bills.createdAt, weekStart),
+ ),
+ )
+ .get()!
+
+ const todayBillsCount = db
+ .select({ count: count() })
+ .from(bills)
+ .where(
+ and(
+ eq(bills.enterpriseId, enterpriseId),
+ gte(bills.createdAt, today),
+ lt(bills.createdAt, tomorrow),
+ ),
+ )
+ .get()!
+
+ const totalInvoices = db
+ .select({ count: count() })
+ .from(bills)
+ .where(eq(bills.enterpriseId, enterpriseId))
+ .get()!
+
+ const totalStockUnits = db
+ .select({ total: sum(stockBatches.quantity) })
+ .from(stockBatches)
+ .where(eq(stockBatches.enterpriseId, enterpriseId))
+ .get()!
+
+ const expiringBatches = db
+ .select({ count: count() })
+ .from(stockBatches)
+ .where(
+ and(
+ eq(stockBatches.enterpriseId, enterpriseId),
+ gte(stockBatches.expiry, today),
+ lte(stockBatches.expiry, thirtyDays),
+ ),
+ )
+ .get()!
+
+ const categoryBreakdown = db
+ .select({ category: products.category, count: count() })
+ .from(products)
+ .where(eq(products.enterpriseId, enterpriseId))
+ .groupBy(products.category)
+ .all()
+
+ const allProducts = db
+ .select()
+ .from(products)
+ .innerJoin(units, eq(units.id, products.unitId))
+ .where(eq(products.enterpriseId, enterpriseId))
+ .all()
+
+ const stockRows = db
+ .select({ productId: stockBatches.productId, totalStock: sum(stockBatches.quantity) })
+ .from(stockBatches)
+ .where(eq(stockBatches.enterpriseId, enterpriseId))
+ .groupBy(stockBatches.productId)
+ .all()
+
+ const stockMap = new Map()
+ for (const row of stockRows) {
+ stockMap.set(row.productId, Number(row.totalStock) || 0)
+ }
+
+ let lowStockProducts = 0
+ let outOfStock = 0
+ let criticalStock = 0
+
+ const recentProducts: DashboardStats['recentProducts'] = []
+
+ for (const row of allProducts) {
+ const product = row.products
+ const unit = row.units
+ const stock = stockMap.get(product.id) ?? 0
+
+ if (stock <= 0) {
+ outOfStock++
+ } else if (stock <= product.reorderLevel * 0.5) {
+ criticalStock++
+ } else if (stock <= product.reorderLevel) {
+ lowStockProducts++
+ }
+
+ recentProducts.push({
+ id: product.id,
+ name: product.name,
+ brand: product.brand,
+ category: product.category,
+ selling_price: product.sellingPrice,
+ stock,
+ reorder_level: product.reorderLevel,
+ unit_name: unit.name,
+ })
+ }
+
+ recentProducts.sort((a, b) => a.stock - b.stock || a.name.localeCompare(b.name))
+
+ return Promise.resolve({
+ totalProducts: Number(totalProducts.count),
+ lowStockProducts,
+ todaySales: Number(todaySales.total) || 0,
+ monthlyRevenue: Number(monthlyRevenue.total) || 0,
+ weeklyRevenue: Number(weeklyRevenue.total) || 0,
+ todayBillsCount: Number(todayBillsCount.count),
+ totalInvoices: Number(totalInvoices.count),
+ totalStockUnits: Number(totalStockUnits.total) || 0,
+ outOfStock,
+ criticalStock,
+ expiringBatches: Number(expiringBatches.count),
+ categoryBreakdown,
+ recentProducts: recentProducts.slice(0, 10),
+ })
+ },
+ }
+
+ return { repo }
+}
diff --git a/packages/data-manager-sqlite/src/index.ts b/packages/data-manager-sqlite/src/index.ts
index de8c918..03b9efa 100644
--- a/packages/data-manager-sqlite/src/index.ts
+++ b/packages/data-manager-sqlite/src/index.ts
@@ -91,3 +91,8 @@ export {
export { customers } from './schema/customers'
export { bills } from './schema/bills'
export { billItems } from './schema/billItems'
+export {
+ createDashboardRepo,
+ type DashboardStats,
+ type DashboardRepo,
+} from './dashboard'
diff --git a/packages/shared-react/src/hooks/dashboard.ts b/packages/shared-react/src/hooks/dashboard.ts
new file mode 100644
index 0000000..2abb3c3
--- /dev/null
+++ b/packages/shared-react/src/hooks/dashboard.ts
@@ -0,0 +1,5 @@
+import { trpc } from "../trpc";
+
+export function useDashboard() {
+ return trpc.dashboard.getStats.useQuery();
+}
diff --git a/packages/shared-react/src/index.ts b/packages/shared-react/src/index.ts
index 7140826..791a2f3 100644
--- a/packages/shared-react/src/index.ts
+++ b/packages/shared-react/src/index.ts
@@ -11,6 +11,7 @@ export * from './hooks/staffManagement'
export * from './hooks/roles'
export * from './hooks/billing'
export * from './hooks/customers'
+export * from './hooks/dashboard'
export { trpc } from './trpc'
export { useAuthStore, useWhoAmI, useLogin } from './auth'
export type { AuthStaff, AuthEnterprise } from './auth'
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 31e6aae..6f714a3 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -20,6 +20,7 @@ export type {
// Shared schemas
export * from './schemas/product'
export * from './schemas/stock'
+export * from './schemas/dashboard'
// Global constants
export { globalConsts } from './global-consts'
diff --git a/packages/shared/src/schemas/dashboard.ts b/packages/shared/src/schemas/dashboard.ts
new file mode 100644
index 0000000..252da29
--- /dev/null
+++ b/packages/shared/src/schemas/dashboard.ts
@@ -0,0 +1,35 @@
+import { z } from "zod";
+
+export const DashboardStatsSchema = z.object({
+ totalProducts: z.number().int(),
+ lowStockProducts: z.number().int(),
+ todaySales: z.number(),
+ monthlyRevenue: z.number(),
+ weeklyRevenue: z.number(),
+ todayBillsCount: z.number().int(),
+ totalInvoices: z.number().int(),
+ totalStockUnits: z.number().int(),
+ outOfStock: z.number().int(),
+ criticalStock: z.number().int(),
+ expiringBatches: z.number().int(),
+ categoryBreakdown: z.array(
+ z.object({
+ category: z.string(),
+ count: z.number().int(),
+ }),
+ ),
+ recentProducts: z.array(
+ z.object({
+ id: z.number().int(),
+ name: z.string(),
+ brand: z.string(),
+ category: z.string(),
+ selling_price: z.number(),
+ stock: z.number().int(),
+ reorder_level: z.number().int(),
+ unit_name: z.string(),
+ }),
+ ),
+});
+
+export type DashboardStats = z.infer;