Dashboard functionalities extended
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
ordering-site/*
|
ordering-site/*
|
||||||
ordering_site/
|
ordering_site/
|
||||||
|
counter-frontend/
|
||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 (
|
||||||
@@ -115,12 +167,12 @@ const Dashboard = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<div className="h-[280px] w-full mt-4">
|
<div className="h-[280px] w-full mt-4">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
@@ -170,10 +222,11 @@ const Dashboard = () => {
|
|||||||
</Pie>
|
</Pie>
|
||||||
</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>
|
||||||
</div>
|
<p className="text-[8px] font-bold text-slate-300 uppercase tracking-widest mt-0.5">Total: ₹{stats.totalSales.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
<div className="flex gap-6 mt-2">
|
<div className="flex gap-6 mt-2">
|
||||||
{pieData.map(item => (
|
{pieData.map(item => (
|
||||||
<div key={item.name} className="flex items-center gap-2">
|
<div key={item.name} className="flex items-center gap-2">
|
||||||
@@ -188,18 +241,37 @@ 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 items-center justify-between overflow-x-auto gap-2 pb-2 scrollbar-none">
|
<div className="flex flex-col gap-3">
|
||||||
{['Yesterday', 'Today', 'Week', '30 Days'].map(range => (
|
<div className="flex items-center justify-between overflow-x-auto gap-2 pb-2 scrollbar-none">
|
||||||
<button
|
{['Yesterday', 'Today', 'Week', '30 Days', 'Custom'].map(range => (
|
||||||
key={range}
|
<button
|
||||||
title={`Analyze data from ${range.toLowerCase()}`}
|
key={range}
|
||||||
onClick={() => setTimeRange(range)}
|
title={`Analyze data from ${range.toLowerCase()}`}
|
||||||
className={`whitespace-nowrap px-3 py-2 text-[10px] font-black uppercase tracking-widest transition-all rounded-xl ${timeRange === range ? 'text-white bg-[#0f4475] shadow-md shadow-[#0f4475]/20' : 'text-slate-400 hover:text-slate-600'}`}
|
onClick={() => setTimeRange(range)}
|
||||||
>
|
className={`whitespace-nowrap px-3 py-2 text-[10px] font-black uppercase tracking-widest transition-all rounded-xl ${timeRange === range ? 'text-white bg-[#0f4475] shadow-md shadow-[#0f4475]/20' : 'text-slate-400 hover:text-slate-600'}`}
|
||||||
{range}
|
>
|
||||||
</button>
|
{range}
|
||||||
))}
|
</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">
|
||||||
|
|||||||
Reference in New Issue
Block a user