Dashboard functionalities extended

This commit is contained in:
Sidharth Prabhu
2026-04-16 11:45:08 +05:30
parent c1a662e185
commit 0e12702fa4
7 changed files with 129 additions and 40 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
ordering-site/* ordering-site/*
ordering_site/ ordering_site/
counter-frontend/

View File

@@ -1,4 +1,5 @@
{ {
"java.compile.nullAnalysis.mode": "automatic", "java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive" "java.configuration.updateBuildConfiguration": "interactive",
"git.ignoreLimitWarning": true
} }

View File

@@ -6,6 +6,7 @@ import com.rit.canteen.sales.service.DashboardService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@@ -18,8 +19,10 @@ public class DashboardController {
private DashboardService dashboardService; private DashboardService dashboardService;
@GetMapping("/stats") @GetMapping("/stats")
public GeneralDashboardData getStats() { public GeneralDashboardData getStats(
return dashboardService.getGeneralDashboardData(); @RequestParam(required = false) @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME) java.time.LocalDateTime from,
@RequestParam(required = false) @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME) java.time.LocalDateTime to) {
return dashboardService.getGeneralDashboardData(from, to);
} }
@GetMapping("/recent-orders") @GetMapping("/recent-orders")

View File

@@ -2,14 +2,16 @@ package com.rit.canteen.sales.model;
public class DashboardStats { public class DashboardStats {
private long totalSales; private long totalSales;
private long periodRevenue;
private int activeOrders; private int activeOrders;
private int dailyCustomers; private int dailyCustomers;
private double growth; private double growth;
public DashboardStats() {} public DashboardStats() {}
public DashboardStats(long totalSales, int activeOrders, int dailyCustomers, double growth) { public DashboardStats(long totalSales, long periodRevenue, int activeOrders, int dailyCustomers, double growth) {
this.totalSales = totalSales; this.totalSales = totalSales;
this.periodRevenue = periodRevenue;
this.activeOrders = activeOrders; this.activeOrders = activeOrders;
this.dailyCustomers = dailyCustomers; this.dailyCustomers = dailyCustomers;
this.growth = growth; this.growth = growth;
@@ -18,6 +20,9 @@ public class DashboardStats {
public long getTotalSales() { return totalSales; } public long getTotalSales() { return totalSales; }
public void setTotalSales(long totalSales) { this.totalSales = totalSales; } public void setTotalSales(long totalSales) { this.totalSales = totalSales; }
public long getPeriodRevenue() { return periodRevenue; }
public void setPeriodRevenue(long periodRevenue) { this.periodRevenue = periodRevenue; }
public int getActiveOrders() { return activeOrders; } public int getActiveOrders() { return activeOrders; }
public void setActiveOrders(int activeOrders) { this.activeOrders = activeOrders; } public void setActiveOrders(int activeOrders) { this.activeOrders = activeOrders; }

View File

@@ -14,6 +14,7 @@ import java.util.Optional;
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> { public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
List<Order> findByUserIdOrderByCreatedAtDesc(Long userId); List<Order> findByUserIdOrderByCreatedAtDesc(Long userId);
long countByCreatedAtGreaterThanEqual(LocalDateTime startOfDay); long countByCreatedAtGreaterThanEqual(LocalDateTime startOfDay);
long countByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
List<Order> findByIsArchivedFalseAndCreatedAtBefore(LocalDateTime timestamp); List<Order> findByIsArchivedFalseAndCreatedAtBefore(LocalDateTime timestamp);
Optional<Order> findByOrderNumber(String orderNumber); Optional<Order> findByOrderNumber(String orderNumber);
@@ -23,6 +24,9 @@ public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecific
@Query("SELECT COUNT(DISTINCT o.userId) FROM Order o WHERE o.createdAt >= :startOfDay") @Query("SELECT COUNT(DISTINCT o.userId) FROM Order o WHERE o.createdAt >= :startOfDay")
long countUniqueUsersToday(@Param("startOfDay") LocalDateTime startOfDay); long countUniqueUsersToday(@Param("startOfDay") LocalDateTime startOfDay);
@Query("SELECT COUNT(DISTINCT o.userId) FROM Order o WHERE o.createdAt >= :start AND o.createdAt <= :end")
long countUniqueUsersInRange(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.createdAt >= :start AND o.createdAt <= :end") @Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.createdAt >= :start AND o.createdAt <= :end")
BigDecimal getRevenuePerPeriod(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end); BigDecimal getRevenuePerPeriod(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end);

View File

@@ -20,14 +20,14 @@ public class DashboardService {
@Autowired @Autowired
private OrderRepository orderRepository; private OrderRepository orderRepository;
public GeneralDashboardData getGeneralDashboardData() { public GeneralDashboardData getGeneralDashboardData(LocalDateTime from, LocalDateTime to) {
LocalDateTime startOfToday = LocalDate.now().atStartOfDay(); if (from == null) from = LocalDate.now().atStartOfDay();
LocalDateTime endOfToday = LocalDate.now().atTime(LocalTime.MAX); if (to == null) to = LocalDate.now().atTime(LocalTime.MAX);
DashboardStats stats = getDashboardStats(); DashboardStats stats = getDashboardStats(from, to);
// 1. Store Overview // 1. Store Overview
List<Object[]> storeData = orderRepository.getStoreOverview(startOfToday, endOfToday); List<Object[]> storeData = orderRepository.getStoreOverview(from, to);
List<Map<String, Object>> storeOverview = new ArrayList<>(); List<Map<String, Object>> storeOverview = new ArrayList<>();
for (Object[] row : storeData) { for (Object[] row : storeData) {
Map<String, Object> store = new HashMap<>(); Map<String, Object> store = new HashMap<>();
@@ -40,7 +40,7 @@ public class DashboardService {
} }
// 2. Hourly Sales // 2. Hourly Sales
List<Object[]> hourlyData = orderRepository.getHourlySales(startOfToday, endOfToday); List<Object[]> hourlyData = orderRepository.getHourlySales(from, to);
List<Map<String, Object>> hourlySales = new ArrayList<>(); List<Map<String, Object>> hourlySales = new ArrayList<>();
// Initialize 24 hours // Initialize 24 hours
for (int i = 0; i < 24; i += 2) { for (int i = 0; i < 24; i += 2) {
@@ -72,7 +72,7 @@ public class DashboardService {
return new GeneralDashboardData(stats, storeOverview, hourlySales, insights); return new GeneralDashboardData(stats, storeOverview, hourlySales, insights);
} }
public DashboardStats getDashboardStats() { public DashboardStats getDashboardStats(LocalDateTime from, LocalDateTime to) {
LocalDateTime startOfToday = LocalDate.now().atStartOfDay(); LocalDateTime startOfToday = LocalDate.now().atStartOfDay();
LocalDateTime endOfToday = LocalDate.now().atTime(LocalTime.MAX); LocalDateTime endOfToday = LocalDate.now().atTime(LocalTime.MAX);
@@ -82,8 +82,11 @@ public class DashboardService {
BigDecimal totalRevenueRaw = orderRepository.getTotalRevenue(); BigDecimal totalRevenueRaw = orderRepository.getTotalRevenue();
long totalSales = totalRevenueRaw != null ? totalRevenueRaw.longValue() : 0; long totalSales = totalRevenueRaw != null ? totalRevenueRaw.longValue() : 0;
int activeOrders = (int) orderRepository.countByCreatedAtGreaterThanEqual(startOfToday); BigDecimal periodRevenueRaw = orderRepository.getRevenuePerPeriod(from, to);
int dailyCustomers = (int) orderRepository.countUniqueUsersToday(startOfToday); long periodRevenue = periodRevenueRaw != null ? periodRevenueRaw.longValue() : 0;
int activeOrders = (int) orderRepository.countByCreatedAtBetween(from, to);
int dailyCustomers = (int) orderRepository.countUniqueUsersInRange(from, to);
BigDecimal todayRevenue = orderRepository.getRevenuePerPeriod(startOfToday, endOfToday); BigDecimal todayRevenue = orderRepository.getRevenuePerPeriod(startOfToday, endOfToday);
BigDecimal yesterdayRevenue = orderRepository.getRevenuePerPeriod(startOfYesterday, endOfYesterday); BigDecimal yesterdayRevenue = orderRepository.getRevenuePerPeriod(startOfYesterday, endOfYesterday);
@@ -99,7 +102,7 @@ public class DashboardService {
growth = 100.0; growth = 100.0;
} }
return new DashboardStats(totalSales, activeOrders, dailyCustomers, growth); return new DashboardStats(totalSales, periodRevenue, activeOrders, dailyCustomers, growth);
} }
public List<Order> getRecentOrders() { public List<Order> getRecentOrders() {

View File

@@ -23,14 +23,66 @@ import { motion } from 'framer-motion';
const Dashboard = () => { const Dashboard = () => {
const [activeTab, setActiveTab] = useState('Sales'); const [activeTab, setActiveTab] = useState('Sales');
const [timeRange, setTimeRange] = useState('Today'); const [timeRange, setTimeRange] = useState('Today');
const [customDates, setCustomDates] = useState({ from: '', to: '' });
const [data, setData] = useState<any>(null); const [data, setData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const getRangeDates = (range: string) => {
const now = new Date();
const start = new Date();
const end = new Date();
// Set end to end of today
end.setHours(23, 59, 59, 999);
switch (range) {
case 'Today':
start.setHours(0, 0, 0, 0);
break;
case 'Yesterday':
start.setDate(now.getDate() - 1);
start.setHours(0, 0, 0, 0);
end.setDate(now.getDate() - 1);
end.setHours(23, 59, 59, 999);
break;
case 'Week':
start.setDate(now.getDate() - 7);
start.setHours(0, 0, 0, 0);
break;
case '30 Days':
start.setDate(now.getDate() - 30);
start.setHours(0, 0, 0, 0);
break;
case 'Custom':
if (customDates.from && customDates.to) {
const from = new Date(customDates.from);
from.setHours(0, 0, 0, 0);
const to = new Date(customDates.to);
to.setHours(23, 59, 59, 999);
return { from: from.toISOString(), to: to.toISOString() };
}
return null;
default:
start.setHours(0, 0, 0, 0);
}
return { from: start.toISOString(), to: end.toISOString() };
};
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const response = await fetch('/api/dashboard/stats'); const range = getRangeDates(timeRange);
let url = '/api/dashboard/stats';
if (range) {
const params = new URLSearchParams();
params.append('from', range.from);
params.append('to', range.to);
url += `?${params.toString()}`;
}
const response = await fetch(url);
if (response.ok) { if (response.ok) {
const result = await response.json(); const result = await response.json();
setData(result); setData(result);
@@ -42,7 +94,7 @@ const Dashboard = () => {
} }
}; };
fetchData(); fetchData();
}, []); }, [timeRange, customDates]);
if (isLoading || !data) { if (isLoading || !data) {
return ( return (
@@ -61,8 +113,8 @@ const Dashboard = () => {
const { stats, storeOverview, hourlySales, insights } = data; const { stats, storeOverview, hourlySales, insights } = data;
const pieData = [ const pieData = [
{ name: 'Full Payment', value: stats.totalSales, color: '#8b5cf6' }, { name: 'Full Payment', value: stats.periodRevenue, color: '#8b5cf6' },
{ name: 'Credit', value: 0, color: '#fbbf24' } // Credits logic can be added later { name: 'Credit', value: 0, color: '#fbbf24' }
]; ];
return ( return (
@@ -118,7 +170,7 @@ const Dashboard = () => {
<div className="flex items-center justify-end mb-4 gap-4"> <div className="flex items-center justify-end mb-4 gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full border-2 border-[#0f4475]/30 bg-[#0f4475]/10" /> <div className="w-2.5 h-2.5 rounded-full border-2 border-[#0f4475]/30 bg-[#0f4475]/10" />
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Today's Revenue</span> <span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{timeRange}'s Revenue</span>
</div> </div>
</div> </div>
@@ -171,8 +223,9 @@ const Dashboard = () => {
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center"> <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">₹{stats.totalSales.toLocaleString()}</h2> <h2 className="text-3xl font-black text-slate-800 tracking-tighter">₹{stats.periodRevenue.toLocaleString()}</h2>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">Life Time</p> <p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">{timeRange === 'Today' ? 'Today' : timeRange}</p>
<p className="text-[8px] font-bold text-slate-300 uppercase tracking-widest mt-0.5">Total: ₹{stats.totalSales.toLocaleString()}</p>
</div> </div>
<div className="flex gap-6 mt-2"> <div className="flex gap-6 mt-2">
{pieData.map(item => ( {pieData.map(item => (
@@ -188,8 +241,9 @@ const Dashboard = () => {
{/* Right Stats Column */} {/* Right Stats Column */}
<div className="col-span-12 lg:col-span-3 space-y-6"> <div className="col-span-12 lg:col-span-3 space-y-6">
{/* Filters */} {/* Filters */}
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between overflow-x-auto gap-2 pb-2 scrollbar-none"> <div className="flex items-center justify-between overflow-x-auto gap-2 pb-2 scrollbar-none">
{['Yesterday', 'Today', 'Week', '30 Days'].map(range => ( {['Yesterday', 'Today', 'Week', '30 Days', 'Custom'].map(range => (
<button <button
key={range} key={range}
title={`Analyze data from ${range.toLowerCase()}`} title={`Analyze data from ${range.toLowerCase()}`}
@@ -200,6 +254,24 @@ const Dashboard = () => {
</button> </button>
))} ))}
</div> </div>
{timeRange === 'Custom' && (
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-top-1 duration-300">
<input
type="date"
value={customDates.from}
onChange={(e) => setCustomDates({ ...customDates, from: e.target.value })}
className="flex-1 bg-white border border-slate-200 rounded-lg px-2 py-1.5 text-[10px] font-bold text-slate-600 outline-none focus:border-[#0f4475]"
/>
<span className="text-slate-300 text-[10px] font-bold">to</span>
<input
type="date"
value={customDates.to}
onChange={(e) => setCustomDates({ ...customDates, to: e.target.value })}
className="flex-1 bg-white border border-slate-200 rounded-lg px-2 py-1.5 text-[10px] font-bold text-slate-600 outline-none focus:border-[#0f4475]"
/>
</div>
)}
</div>
{/* Total Orders Card */} {/* Total Orders Card */}
<div title="View detailed order volume and throughput" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm relative h-[180px] flex flex-col justify-between group hover:border-[#0f4475]/30 transition-all cursor-pointer"> <div title="View detailed order volume and throughput" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm relative h-[180px] flex flex-col justify-between group hover:border-[#0f4475]/30 transition-all cursor-pointer">