Purchase Dashboard functionality added.

This commit is contained in:
Sidharth Prabhu
2026-04-20 10:14:48 +05:30
parent 7691ab31c7
commit 4ef59700f3
6 changed files with 219 additions and 40 deletions

View File

@@ -1,6 +1,7 @@
package com.rit.canteen.sales.controller; package com.rit.canteen.sales.controller;
import com.rit.canteen.sales.model.GeneralDashboardData; import com.rit.canteen.sales.model.GeneralDashboardData;
import com.rit.canteen.sales.model.ProcurementDashboardData;
import com.rit.canteen.sales.model.Order; import com.rit.canteen.sales.model.Order;
import com.rit.canteen.sales.service.DashboardService; import com.rit.canteen.sales.service.DashboardService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -25,6 +26,11 @@ public class DashboardController {
return dashboardService.getGeneralDashboardData(from, to); return dashboardService.getGeneralDashboardData(from, to);
} }
@GetMapping("/procurement")
public ProcurementDashboardData getProcurementStats() {
return dashboardService.getProcurementDashboardData();
}
@GetMapping("/recent-orders") @GetMapping("/recent-orders")
public List<Order> getRecentOrders() { public List<Order> getRecentOrders() {
return dashboardService.getRecentOrders(); return dashboardService.getRecentOrders();

View File

@@ -6,15 +6,19 @@ public class DashboardStats {
private int activeOrders; private int activeOrders;
private int dailyCustomers; private int dailyCustomers;
private double growth; private double growth;
private long totalExpenses;
private long periodExpenses;
public DashboardStats() {} public DashboardStats() {}
public DashboardStats(long totalSales, long periodRevenue, int activeOrders, int dailyCustomers, double growth) { public DashboardStats(long totalSales, long periodRevenue, int activeOrders, int dailyCustomers, double growth, long totalExpenses, long periodExpenses) {
this.totalSales = totalSales; this.totalSales = totalSales;
this.periodRevenue = periodRevenue; this.periodRevenue = periodRevenue;
this.activeOrders = activeOrders; this.activeOrders = activeOrders;
this.dailyCustomers = dailyCustomers; this.dailyCustomers = dailyCustomers;
this.growth = growth; this.growth = growth;
this.totalExpenses = totalExpenses;
this.periodExpenses = periodExpenses;
} }
public long getTotalSales() { return totalSales; } public long getTotalSales() { return totalSales; }
@@ -31,4 +35,10 @@ public class DashboardStats {
public double getGrowth() { return growth; } public double getGrowth() { return growth; }
public void setGrowth(double growth) { this.growth = growth; } public void setGrowth(double growth) { this.growth = growth; }
public long getTotalExpenses() { return totalExpenses; }
public void setTotalExpenses(long totalExpenses) { this.totalExpenses = totalExpenses; }
public long getPeriodExpenses() { return periodExpenses; }
public void setPeriodExpenses(long periodExpenses) { this.periodExpenses = periodExpenses; }
} }

View File

@@ -0,0 +1,27 @@
package com.rit.canteen.sales.model;
import java.util.List;
import java.util.Map;
public class ProcurementDashboardData {
private Map<String, Object> stats;
private List<Map<String, Object>> trends;
private List<Map<String, Object>> topVendors;
public ProcurementDashboardData() {}
public ProcurementDashboardData(Map<String, Object> stats, List<Map<String, Object>> trends, List<Map<String, Object>> topVendors) {
this.stats = stats;
this.trends = trends;
this.topVendors = topVendors;
}
public Map<String, Object> getStats() { return stats; }
public void setStats(Map<String, Object> stats) { this.stats = stats; }
public List<Map<String, Object>> getTrends() { return trends; }
public void setTrends(List<Map<String, Object>> trends) { this.trends = trends; }
public List<Map<String, Object>> getTopVendors() { return topVendors; }
public void setTopVendors(List<Map<String, Object>> topVendors) { this.topVendors = topVendors; }
}

View File

@@ -5,6 +5,9 @@ import com.rit.canteen.sales.model.GeneralDashboardData;
import com.rit.canteen.sales.model.Order; import com.rit.canteen.sales.model.Order;
import com.rit.canteen.sales.model.TrendingItem; import com.rit.canteen.sales.model.TrendingItem;
import com.rit.canteen.sales.repository.OrderRepository; import com.rit.canteen.sales.repository.OrderRepository;
import com.rit.canteen.sales.repository.PurchaseOrderRepository;
import com.rit.canteen.sales.repository.VendorRepository;
import com.rit.canteen.sales.model.ProcurementDashboardData;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -21,6 +24,12 @@ public class DashboardService {
@Autowired @Autowired
private OrderRepository orderRepository; private OrderRepository orderRepository;
@Autowired
private PurchaseOrderRepository purchaseOrderRepository;
@Autowired
private VendorRepository vendorRepository;
public GeneralDashboardData getGeneralDashboardData(LocalDateTime from, LocalDateTime to) { public GeneralDashboardData getGeneralDashboardData(LocalDateTime from, LocalDateTime to) {
if (from == null) from = LocalDate.now().atStartOfDay(); if (from == null) from = LocalDate.now().atStartOfDay();
if (to == null) to = LocalDate.now().atTime(LocalTime.MAX); if (to == null) to = LocalDate.now().atTime(LocalTime.MAX);
@@ -130,6 +139,12 @@ public class DashboardService {
BigDecimal todayRevenue = orderRepository.getRevenuePerPeriod(startOfToday, endOfToday); BigDecimal todayRevenue = orderRepository.getRevenuePerPeriod(startOfToday, endOfToday);
BigDecimal yesterdayRevenue = orderRepository.getRevenuePerPeriod(startOfYesterday, endOfYesterday); BigDecimal yesterdayRevenue = orderRepository.getRevenuePerPeriod(startOfYesterday, endOfYesterday);
BigDecimal totalExpensesRaw = purchaseOrderRepository.getTotalPurchaseAmount();
long totalExpenses = totalExpensesRaw != null ? totalExpensesRaw.longValue() : 0;
BigDecimal periodExpensesRaw = purchaseOrderRepository.getTotalPurchaseAmountInRange(from, to);
long periodExpenses = periodExpensesRaw != null ? periodExpensesRaw.longValue() : 0;
double growth = 0; double growth = 0;
if (yesterdayRevenue != null && yesterdayRevenue.compareTo(BigDecimal.ZERO) > 0) { if (yesterdayRevenue != null && yesterdayRevenue.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal today = todayRevenue != null ? todayRevenue : BigDecimal.ZERO; BigDecimal today = todayRevenue != null ? todayRevenue : BigDecimal.ZERO;
@@ -141,7 +156,49 @@ public class DashboardService {
growth = 100.0; growth = 100.0;
} }
return new DashboardStats(totalSales, periodRevenue, activeOrders, dailyCustomers, growth); return new DashboardStats(totalSales, periodRevenue, activeOrders, dailyCustomers, growth, totalExpenses, periodExpenses);
}
public ProcurementDashboardData getProcurementDashboardData() {
// 1. Stats
Map<String, Object> stats = new HashMap<>();
BigDecimal total = purchaseOrderRepository.getTotalPurchaseAmount();
stats.put("totalProcurement", total != null ? total : BigDecimal.ZERO);
stats.put("activeVendors", vendorRepository.count());
stats.put("pendingPOs", purchaseOrderRepository.countByStatus("OPEN"));
stats.put("fillRate", 94.2); // Derived or static for now
// 2. Trends (Last 6 entries)
List<Object[]> trendRaw = purchaseOrderRepository.getPurchaseTrend();
List<Map<String, Object>> trends = new ArrayList<>();
String[] months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
for (int i = Math.max(0, trendRaw.size() - 6); i < trendRaw.size(); i++) {
Object[] row = trendRaw.get(i);
LocalDateTime date = (LocalDateTime) row[0];
Map<String, Object> point = new HashMap<>();
point.put("month", months[date.getMonthValue() - 1]);
point.put("purchases", row[1]);
point.put("orders", 1); // Sample for volume
trends.add(point);
}
// 3. Top Vendors
List<Object[]> vendorRaw = purchaseOrderRepository.getVendorSummary(
LocalDate.now().minusMonths(1).atStartOfDay(),
LocalDateTime.now()
);
List<Map<String, Object>> topVendors = new ArrayList<>();
for (Object[] row : vendorRaw) {
Map<String, Object> v = new HashMap<>();
v.put("name", row[0]);
v.put("volume", row[1]);
v.put("orders", row[2]);
v.put("color", "bg-indigo-100 text-indigo-600");
topVendors.add(v);
}
return new ProcurementDashboardData(stats, trends, topVendors);
} }
public List<Order> getRecentOrders() { public List<Order> getRecentOrders() {

View File

@@ -7,7 +7,9 @@ import {
ArrowRight, ArrowRight,
Wallet, Wallet,
BarChart3, BarChart3,
MessageSquare MessageSquare,
Flame,
TrendingUp
} from 'lucide-react'; } from 'lucide-react';
import { import {
AreaChart, AreaChart,
@@ -114,7 +116,7 @@ const Dashboard = () => {
); );
} }
const { stats, storeOverview, hourlySales, insights } = data; const { stats, storeOverview, hourlySales, insights, trendingItems } = data;
const pieData = [ const pieData = [
{ name: 'Full Payment', value: stats.periodRevenue, color: '#8b5cf6' }, { name: 'Full Payment', value: stats.periodRevenue, color: '#8b5cf6' },
@@ -316,12 +318,58 @@ const Dashboard = () => {
<h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest leading-none">Total Expenses</h4> <h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest leading-none">Total Expenses</h4>
</div> </div>
<div className="text-center pb-2"> <div className="text-center pb-2">
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">₹0</h2> <h2 className="text-3xl font-black text-slate-800 tracking-tighter">₹{stats.periodExpenses.toLocaleString()}</h2>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Trending Items Section */}
<section>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-rose-50 text-rose-500 rounded-xl">
<Flame size={20} />
</div>
<h3 className="text-sm font-black text-slate-800 tracking-tight uppercase tracking-[0.1em]">Best Selling Food</h3>
</div>
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
Performance Index <TrendingUp size={12} className="text-emerald-500" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{trendingItems.map((item: any, idx: number) => (
<motion.div
key={idx}
whileHover={{ y: -5 }}
className="bg-white p-5 rounded-[28px] border border-slate-100 shadow-sm hover:shadow-xl hover:border-indigo-500/20 transition-all group overflow-hidden relative cursor-default"
>
<div className="absolute top-4 right-4 bg-white/80 backdrop-blur-md px-2.5 py-1 rounded-lg text-[10px] font-black text-slate-900 border border-slate-100 z-10 shadow-sm">
TOP #{idx+1}
</div>
<div className="w-full h-32 rounded-2xl mb-5 overflow-hidden bg-slate-50">
<img
src={item.imageUrl}
alt={item.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700 opacity-90 group-hover:opacity-100"
/>
</div>
<div className="flex justify-between items-start mb-2">
<div>
<h4 className="text-sm font-black text-slate-800 leading-tight mb-1 group-hover:text-indigo-600 transition-colors uppercase tracking-tight">{item.name}</h4>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{item.category}</p>
</div>
</div>
<div className="flex items-center justify-between mt-4 pt-4 border-t border-slate-50">
<div className="text-[10px] text-slate-400 font-black uppercase tracking-tight">Orders Count</div>
<div className="text-lg font-black text-indigo-900 tracking-tighter">{item.orderCount} <span className="text-[10px] font-bold text-slate-300">Unit(s)</span></div>
</div>
</motion.div>
))}
</div>
</section>
{/* Bottom Section */} {/* Bottom Section */}
<div className="grid grid-cols-12 gap-8 mt-4 pb-12"> <div className="grid grid-cols-12 gap-8 mt-4 pb-12">
{/* Store Insights */} {/* Store Insights */}

View File

@@ -1,3 +1,4 @@
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { import {
ShoppingBag, ShoppingBag,
@@ -21,14 +22,7 @@ import {
Bar Bar
} from 'recharts'; } from 'recharts';
const data = [ // Removed static data
{ month: 'Jan', purchases: 4500, orders: 12 },
{ month: 'Feb', purchases: 5200, orders: 15 },
{ month: 'Mar', purchases: 4800, orders: 14 },
{ month: 'Apr', purchases: 6100, orders: 18 },
{ month: 'May', purchases: 5500, orders: 16 },
{ month: 'Jun', purchases: 6700, orders: 20 },
];
const StatCard = ({ title, value, change, icon: Icon, color, delay }: any) => ( const StatCard = ({ title, value, change, icon: Icon, color, delay }: any) => (
<motion.div <motion.div
@@ -52,6 +46,43 @@ const StatCard = ({ title, value, change, icon: Icon, color, delay }: any) => (
); );
const VendorDashboard = () => { const VendorDashboard = () => {
const [data, setData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
setIsLoading(true);
const response = await fetch('/api/dashboard/procurement');
if (response.ok) {
const result = await response.json();
setData(result);
}
} catch (error) {
console.error('Error fetching procurement data:', error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
if (isLoading || !data) {
return (
<div className="h-screen flex items-center justify-center bg-slate-50/50">
<motion.div
animate={{ scale: [1, 1.1, 1], opacity: [0.5, 1, 0.5] }}
transition={{ repeat: Infinity, duration: 1.5 }}
className="text-[#231651] font-black uppercase tracking-widest text-sm"
>
Analyzing Supply Chain...
</motion.div>
</div>
);
}
const { stats, trends, topVendors } = data;
return ( return (
<div className="p-8 space-y-8 animate-in fade-in duration-500"> <div className="p-8 space-y-8 animate-in fade-in duration-500">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
@@ -67,10 +98,10 @@ const VendorDashboard = () => {
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatCard title="Total Procurement" value="$28,450.00" change={12} icon={DollarSign} color="bg-blue-500" delay={0.1} /> <StatCard title="Total Procurement" value={`${stats.totalProcurement.toLocaleString()}`} change={12} icon={DollarSign} color="bg-blue-500" delay={0.1} />
<StatCard title="Active Vendors" value="24" change={5} icon={Users} color="bg-emerald-500" delay={0.2} /> <StatCard title="Active Vendors" value={stats.activeVendors} change={5} icon={Users} color="bg-emerald-500" delay={0.2} />
<StatCard title="Pending POs" value="08" change={2} icon={Package} color="bg-amber-500" delay={0.3} /> <StatCard title="Pending POs" value={stats.pendingPOs.toString().padStart(2, '0')} change={2} icon={Package} color="bg-amber-500" delay={0.3} />
<StatCard title="Supply Fill Rate" value="94.2%" change={1.5} icon={TrendingUp} color="bg-violet-500" delay={0.4} /> <StatCard title="Supply Fill Rate" value={`${stats.fillRate}%`} change={1.5} icon={TrendingUp} color="bg-violet-500" delay={0.4} />
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
@@ -87,7 +118,7 @@ const VendorDashboard = () => {
</div> </div>
<div className="h-[300px] w-full"> <div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart data={data}> <AreaChart data={trends}>
<defs> <defs>
<linearGradient id="colorPurchases" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="colorPurchases" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#231651" stopOpacity={0.1}/> <stop offset="5%" stopColor="#231651" stopOpacity={0.1}/>
@@ -117,7 +148,7 @@ const VendorDashboard = () => {
</div> </div>
<div className="h-[300px] w-full"> <div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart data={data}> <BarChart data={trends}>
<XAxis dataKey="month" axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 11, fontWeight: 700}} dy={10} /> <XAxis dataKey="month" axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 11, fontWeight: 700}} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 11, fontWeight: 700}} /> <YAxis axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 11, fontWeight: 700}} />
<Tooltip /> <Tooltip />
@@ -131,14 +162,11 @@ const VendorDashboard = () => {
<div className="bg-white rounded-3xl border border-[#e2e8f0] shadow-sm p-6 overflow-hidden"> <div className="bg-white rounded-3xl border border-[#e2e8f0] shadow-sm p-6 overflow-hidden">
<h3 className="text-lg font-bold text-[#1e293b] mb-6">Top Performing Vendors</h3> <h3 className="text-lg font-bold text-[#1e293b] mb-6">Top Performing Vendors</h3>
<div className="space-y-4"> <div className="space-y-4">
{[ {topVendors.length > 0 ? (
{ name: 'Fresh Foods Co.', orders: 45, volume: '$12,400', color: 'bg-blue-100 text-blue-600' }, topVendors.map((v: any, i: number) => (
{ name: 'Dairy Plus', orders: 28, volume: '$8,200', color: 'bg-emerald-100 text-emerald-600' },
{ name: 'Bakery World', orders: 15, volume: '$3,150', color: 'bg-amber-100 text-amber-600' },
].map((v, i) => (
<div key={i} className="flex items-center justify-between p-4 bg-gray-50/50 rounded-2xl hover:bg-gray-50 transition-all cursor-pointer group"> <div key={i} className="flex items-center justify-between p-4 bg-gray-50/50 rounded-2xl hover:bg-gray-50 transition-all cursor-pointer group">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className={`w-10 h-10 rounded-xl ${v.color} flex items-center justify-center font-bold text-sm`}> <div className={`w-10 h-10 rounded-xl ${v.color || 'bg-indigo-100 text-indigo-600'} flex items-center justify-center font-bold text-sm`}>
{v.name.charAt(0)} {v.name.charAt(0)}
</div> </div>
<div> <div>
@@ -147,11 +175,14 @@ const VendorDashboard = () => {
</div> </div>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-sm font-black text-[#1e293b]">{v.volume}</p> <p className="text-sm font-black text-[#1e293b]">{v.volume.toLocaleString()}</p>
<p className="text-[10px] font-bold text-emerald-600 uppercase">Excellent Status</p> <p className="text-[10px] font-bold text-emerald-600 uppercase">Excellent Status</p>
</div> </div>
</div> </div>
))} ))
) : (
<div className="text-center py-8 text-slate-400 font-medium italic">No vendor performance data available for this period.</div>
)}
</div> </div>
</div> </div>
</div> </div>