From ee98c1c45e066493e11a1ab3bbc1f66b35b4ce6e Mon Sep 17 00:00:00 2001 From: Sidharth Prabhu Date: Mon, 20 Apr 2026 21:08:17 +0530 Subject: [PATCH] Dashboard adoptation --- .../sales/controller/OrderController.java | 1 + .../sales/controller/WalletController.java | 16 + .../TokenTransactionRepository.java | 11 + .../sales/repository/UserRepository.java | 2 +- .../sales/service/DashboardService.java | 22 +- frontend/src/App.tsx | 2 + frontend/src/components/Sidebar.tsx | 3 +- frontend/src/pages/Dashboard.tsx | 21 +- frontend/src/pages/ManageWallets.tsx | 294 ++++++++++++++++++ frontend/src/pages/StoreDashboard.tsx | 61 +++- 10 files changed, 418 insertions(+), 15 deletions(-) create mode 100644 frontend/src/pages/ManageWallets.tsx diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java index 850aa5d3..8f717950 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java @@ -21,6 +21,7 @@ import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java b/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java index ff258644..65a929ab 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java @@ -9,8 +9,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @RestController @RequestMapping("/api/wallet") @@ -34,6 +36,20 @@ public class WalletController { } } + @GetMapping("/users") + public ResponseEntity>> getUsers() { + List users = userRepository.findAll(); + List> userList = users.stream().map(user -> { + Map map = new HashMap<>(); + map.put("id", user.getId()); + map.put("name", user.getName()); + map.put("mobileNumber", user.getMobileNumber()); + map.put("ritzTokenBalance", user.getRitzTokenBalance()); + return map; + }).collect(Collectors.toList()); + return ResponseEntity.ok(userList); + } + @GetMapping("/transactions/{userId}") public ResponseEntity> getTransactions(@PathVariable Long userId) { return ResponseEntity.ok(tokenService.getTransactions(userId)); diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/TokenTransactionRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/TokenTransactionRepository.java index 43758234..c9a1f0a2 100644 --- a/backend/src/main/java/com/rit/canteen/sales/repository/TokenTransactionRepository.java +++ b/backend/src/main/java/com/rit/canteen/sales/repository/TokenTransactionRepository.java @@ -2,11 +2,22 @@ package com.rit.canteen.sales.repository; import com.rit.canteen.sales.model.TokenTransaction; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; + +import java.math.BigDecimal; +import java.time.LocalDateTime; import java.util.List; @Repository public interface TokenTransactionRepository extends JpaRepository { List findByUserIdOrderByTimestampDesc(Long userId); List findAllByOrderByTimestampDesc(); + + @Query("SELECT SUM(t.amount) FROM TokenTransaction t WHERE t.type = :type") + BigDecimal sumByType(@Param("type") TokenTransaction.TransactionType type); + + @Query("SELECT SUM(t.amount) FROM TokenTransaction t WHERE t.type = :type AND t.timestamp >= :start AND t.timestamp <= :end") + BigDecimal sumByTypeInRange(@Param("type") TokenTransaction.TransactionType type, @Param("start") LocalDateTime start, @Param("end") LocalDateTime end); } diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java index 05a565e0..b017d41b 100644 --- a/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java +++ b/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java @@ -16,7 +16,7 @@ public interface UserRepository extends JpaRepository { "OR u.mobileNumber LIKE CONCAT('%', :search, '%')") org.springframework.data.domain.Page findByNameOrMobileContainingIgnoreCase(String search, org.springframework.data.domain.Pageable pageable); - @jakarta.persistence.Lock(jakarta.persistence.LockModeType.PESSIMISTIC_WRITE) + @org.springframework.data.jpa.repository.Lock(jakarta.persistence.LockModeType.PESSIMISTIC_WRITE) @Query("SELECT u FROM User u WHERE u.id = :id") Optional findByIdWithLock(@org.springframework.data.repository.query.Param("id") Long id); } diff --git a/backend/src/main/java/com/rit/canteen/sales/service/DashboardService.java b/backend/src/main/java/com/rit/canteen/sales/service/DashboardService.java index dc188e18..249836d9 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/DashboardService.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/DashboardService.java @@ -7,6 +7,7 @@ import com.rit.canteen.sales.model.TrendingItem; 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.repository.TokenTransactionRepository; import com.rit.canteen.sales.model.ProcurementDashboardData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -29,11 +30,15 @@ public class DashboardService { @Autowired private VendorRepository vendorRepository; + + @Autowired + private TokenTransactionRepository tokenTransactionRepository; public GeneralDashboardData getGeneralDashboardData(LocalDateTime from, LocalDateTime to) { if (from == null) from = LocalDate.now().atStartOfDay(); if (to == null) to = LocalDate.now().atTime(LocalTime.MAX); + System.out.println("[DIAGNOSTIC] Final timestamp range for service logic: " + from + " to " + to); DashboardStats stats = getDashboardStats(from, to); System.out.println("Fetching dashboard data for range: " + from + " to " + to); @@ -94,7 +99,9 @@ public class DashboardService { orderInsight.put("color", "bg-rose-50 text-rose-600 border-rose-100"); insights.add(orderInsight); - BigDecimal avg = stats.getTotalSales() > 0 ? BigDecimal.valueOf(stats.getTotalSales()).divide(BigDecimal.valueOf(stats.getActiveOrders()), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO; + BigDecimal avg = (stats.getTotalSales() > 0 && stats.getActiveOrders() > 0) + ? BigDecimal.valueOf(stats.getTotalSales()).divide(BigDecimal.valueOf(stats.getActiveOrders()), 2, RoundingMode.HALF_UP) + : BigDecimal.ZERO; Map avgInsight = new HashMap<>(); avgInsight.put("text", "₹" + avg + " average order value! Either everyone's hungry or just living large 🔥😋"); avgInsight.put("color", "bg-emerald-50 text-emerald-600 border-emerald-100"); @@ -127,17 +134,22 @@ public class DashboardService { LocalDateTime startOfYesterday = LocalDate.now().minusDays(1).atStartOfDay(); LocalDateTime endOfYesterday = LocalDate.now().minusDays(1).atTime(LocalTime.MAX); - BigDecimal totalRevenueRaw = orderRepository.getTotalRevenue(); + System.out.println("[DIAGNOSTIC] Fetching Total Revenue..."); + BigDecimal totalRevenueRaw = tokenTransactionRepository.sumByType(com.rit.canteen.sales.model.TokenTransaction.TransactionType.TOPUP); + System.out.println("[DIAGNOSTIC] Raw Total Revenue: " + totalRevenueRaw); long totalSales = totalRevenueRaw != null ? totalRevenueRaw.longValue() : 0; - BigDecimal periodRevenueRaw = orderRepository.getRevenuePerPeriod(from, to); + System.out.println("[DIAGNOSTIC] Fetching Period Revenue for range: " + from + " to " + to); + BigDecimal periodRevenueRaw = tokenTransactionRepository.sumByTypeInRange(com.rit.canteen.sales.model.TokenTransaction.TransactionType.TOPUP, from, to); + System.out.println("[DIAGNOSTIC] Raw Period Revenue: " + periodRevenueRaw); 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 yesterdayRevenue = orderRepository.getRevenuePerPeriod(startOfYesterday, endOfYesterday); + BigDecimal todayRevenue = tokenTransactionRepository.sumByTypeInRange(com.rit.canteen.sales.model.TokenTransaction.TransactionType.TOPUP, startOfToday, endOfToday); + BigDecimal yesterdayRevenue = tokenTransactionRepository.sumByTypeInRange(com.rit.canteen.sales.model.TokenTransaction.TransactionType.TOPUP, startOfYesterday, endOfYesterday); + System.out.println("[DIAGNOSTIC] Today vs Yesterday: " + todayRevenue + " / " + yesterdayRevenue); BigDecimal totalExpensesRaw = purchaseOrderRepository.getTotalPurchaseAmount(); long totalExpenses = totalExpensesRaw != null ? totalExpensesRaw.longValue() : 0; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 614834a8..9a0f3eb2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,6 +25,7 @@ import Reports from './pages/Reports.tsx'; import Feedback from './pages/Feedback.tsx'; import Ritz from './pages/Ritz.tsx'; import RitzCirculation from './pages/RitzCirculation.tsx'; +import ManageWallets from './pages/ManageWallets.tsx'; const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { const isLoggedIn = sessionStorage.getItem('isLoggedIn') === 'true'; @@ -94,6 +95,7 @@ function App() { } /> } /> } /> + } /> {/* Stores */} } /> diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 009f86ce..36a71ff2 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -122,7 +122,8 @@ const menuItems: MenuItem[] = [ icon: CircleDollarSign, subMenu: [ { title: 'Overview', path: '/ritz/overview' }, - { title: 'Ritz in Circulation', path: '/ritz/circulation' } + { title: 'Ritz in Circulation', path: '/ritz/circulation' }, + { title: 'Manage Wallets', path: '/ritz/wallets' } ] }, ]; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 4c3853fb..ad1394fd 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -33,6 +33,22 @@ const Dashboard = () => { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); + const toLocalISOString = (date: Date) => { + const tzo = -date.getTimezoneOffset(), + dif = tzo >= 0 ? '+' : '-', + pad = (num: number) => { + const norm = Math.floor(Math.abs(num)); + return (norm < 10 ? '0' : '') + norm; + }; + return date.getFullYear() + + '-' + pad(date.getMonth() + 1) + + '-' + pad(date.getDate()) + + 'T' + pad(date.getHours()) + + ':' + pad(date.getMinutes()) + + ':' + pad(date.getSeconds()) + + '.' + pad(date.getMilliseconds()); + }; + const getRangeDates = (range: string) => { const now = new Date(); const start = new Date(); @@ -65,13 +81,13 @@ const Dashboard = () => { 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 { from: toLocalISOString(from), to: toLocalISOString(to) }; } return null; default: start.setHours(0, 0, 0, 0); } - return { from: start.toISOString(), to: end.toISOString() }; + return { from: toLocalISOString(start), to: toLocalISOString(end) }; }; useEffect(() => { @@ -88,6 +104,7 @@ const Dashboard = () => { url += `?${params.toString()}`; } + console.log('[DASHBOARD-TRACE] Fetching stats from:', url); const response = await fetch(url); if (response.ok) { const result = await response.json(); diff --git a/frontend/src/pages/ManageWallets.tsx b/frontend/src/pages/ManageWallets.tsx new file mode 100644 index 00000000..831d078c --- /dev/null +++ b/frontend/src/pages/ManageWallets.tsx @@ -0,0 +1,294 @@ +import { useState, useEffect } from 'react'; +import { + Users, + Search, + Wallet, + Plus, + X, + CheckCircle2, + AlertCircle, + ArrowRight, + User as UserIcon, + Phone +} from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface User { + id: number; + name: string; + mobileNumber: string; + ritzTokenBalance: number; +} + +const ManageWallets = () => { + const [users, setUsers] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedUser, setSelectedUser] = useState(null); + const [amount, setAmount] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + const [status, setStatus] = useState<{ type: 'success' | 'error'; message: string } | null>(null); + + const fetchUsers = async () => { + try { + setIsLoading(true); + const response = await fetch('/api/wallet/users'); + if (response.ok) { + const data = await response.json(); + setUsers(data); + } + } catch (error) { + console.error('Error fetching users:', error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchUsers(); + }, []); + + const handleTopUp = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedUser || !amount || parseFloat(amount) <= 0) return; + + setIsProcessing(true); + setStatus(null); + + try { + const response = await fetch('/api/wallet/topup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: selectedUser.id, + amount: parseFloat(amount) + }), + }); + + const data = await response.json(); + if (data.success) { + setStatus({ type: 'success', message: `Successfully added ${amount} Ritz to ${selectedUser.name}'s wallet` }); + fetchUsers(); // Refresh list + setAmount(''); + setTimeout(() => { + setIsModalOpen(false); + setStatus(null); + }, 2000); + } else { + setStatus({ type: 'error', message: data.error || 'Failed to credit tokens' }); + } + } catch (error) { + setStatus({ type: 'error', message: 'Connection error. Please try again.' }); + } finally { + setIsProcessing(false); + } + }; + + const filteredUsers = users.filter(user => + (user.name?.toLowerCase() || '').includes(searchQuery.toLowerCase()) || + user.mobileNumber.includes(searchQuery) + ); + + return ( +
+ {/* Header */} +
+
+
+

Ritz Section

+
+

Manage Wallets

+

+ i + Admin panel for manual Ritz Token crediting and balance monitoring. +

+
+ +
+
+ + setSearchQuery(e.target.value)} + className="bg-white border border-slate-200 rounded-xl pl-10 pr-4 py-2 text-sm outline-none focus:border-[#0f4475] transition-all w-64 shadow-sm" + /> +
+
+
+ + {/* Main Content */} +
+
+ + + + + + + + + + + + {isLoading ? ( + + + + ) : filteredUsers.length === 0 ? ( + + + + ) : ( + filteredUsers.map((user) => ( + + + + + + + + )) + )} + +
User IDCustomer DetailsMobile NumberWallet BalanceActions
+ + Loading Wallet Data... + +
+ No users found matching your search. +
#{user.id} +
+
+ +
+
+

{user.name || 'Anonymous User'}

+

Registered Member

+
+
+
+
+ + {user.mobileNumber} +
+
+
+ + R{user.ritzTokenBalance.toLocaleString()} +
+
+ +
+
+
+ + {/* Credit Modal */} + + {isModalOpen && selectedUser && ( +
+ !isProcessing && setIsModalOpen(false)} + className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" + /> + +
+
+
+
+ +
+

Credit Ritz Tokens

+
+ +
+ +
+
+ + Target User +
+

{selectedUser.name}

+

{selectedUser.mobileNumber}

+
+ +
+
+ +
+
R
+ setAmount(e.target.value)} + placeholder="0.00" + className="w-full bg-slate-50 border border-slate-100 rounded-2xl pl-10 pr-4 py-4 text-2xl font-black text-slate-800 outline-none focus:bg-white focus:border-[#0f4475]/30 transition-all" + required + autoFocus + /> +
+
+ + {status && ( + + {status.type === 'success' ? : } + {status.message} + + )} + + +
+
+
+
+ )} +
+
+ ); +}; + +export default ManageWallets; diff --git a/frontend/src/pages/StoreDashboard.tsx b/frontend/src/pages/StoreDashboard.tsx index 02a8efff..e80a6e74 100644 --- a/frontend/src/pages/StoreDashboard.tsx +++ b/frontend/src/pages/StoreDashboard.tsx @@ -41,16 +41,67 @@ const StoreDashboard = () => { return isNaN(num) ? '0' : num.toLocaleString(); }; + const toLocalISOString = (date: Date) => { + const tzo = -date.getTimezoneOffset(), + pad = (num: number) => { + const norm = Math.floor(Math.abs(num)); + return (norm < 10 ? '0' : '') + norm; + }; + return date.getFullYear() + + '-' + pad(date.getMonth() + 1) + + '-' + pad(date.getDate()) + + 'T' + pad(date.getHours()) + + ':' + pad(date.getMinutes()) + + ':' + pad(date.getSeconds()) + + '.' + pad(date.getMilliseconds()); + }; + + 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; + default: + start.setHours(0, 0, 0, 0); + } + return { from: toLocalISOString(start), to: toLocalISOString(end) }; + }; + useEffect(() => { const fetchStats = async () => { try { setIsLoading(true); - const response = await fetch('/api/dashboard/stats'); + const range = getRangeDates(timeRange); + const params = new URLSearchParams(); + params.append('from', range.from); + params.append('to', range.to); + + const response = await fetch(`/api/dashboard/stats?${params.toString()}`); if (response.ok) { const data = await response.json(); console.log('Dashboard data received successfully:', data); - // Set basic stats from general stats if available if (data.stats) { setStats({ totalSales: data.stats.totalSales, @@ -60,7 +111,6 @@ const StoreDashboard = () => { }); } - // Override with RIT Canteen specific data if found in overview if (data.storeOverview && data.storeOverview.length > 0) { const ritStore = data.storeOverview.find((s: any) => s.name === 'RIT Canteen'); if (ritStore) { @@ -68,12 +118,11 @@ const StoreDashboard = () => { ...prev, totalSales: ritStore.sale, activeOrders: ritStore.orders, - dailyCustomers: ritStore.orders * 0.9, // Approximation + dailyCustomers: ritStore.orders * 0.9, })); } } - // Set other dynamic data if (data.trendingItems) setTrendingItems(data.trendingItems); if (data.hourlySales) setSalesData(data.hourlySales); if (data.insights) setInsights(data.insights); @@ -85,7 +134,7 @@ const StoreDashboard = () => { } }; fetchStats(); - }, []); + }, [timeRange]); const pieData = [ { name: 'Full Payment', value: Number(stats.totalSales) || 0, color: '#8b5cf6' },