From c3bb004dc6ea041a9d5799928b0ac1f362f645d9 Mon Sep 17 00:00:00 2001 From: Sidharth Prabhu Date: Wed, 24 Jun 2026 08:08:12 +0530 Subject: [PATCH] Counter dashboard global search added --- .../sales/controller/OrderController.java | 21 +++- .../sales/controller/PurchaseController.java | 14 ++- .../repository/PurchaseOrderRepository.java | 9 ++ .../sales/service/DevicePairingService.java | 29 +++-- .../sales/service/PurchaseService.java | 28 +++++ frontend/src/App.tsx | 4 +- frontend/src/components/Sidebar.tsx | 5 +- frontend/src/pages/PurchaseSummary.tsx | 119 +++++++++++++++--- frontend/src/pages/StoreDashboard.tsx | 79 ++++++++---- ordering_site/src/pages/MyOrdersScreen.tsx | 58 +-------- 10 files changed, 252 insertions(+), 114 deletions(-) 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 0a974fc0..d4fde9a6 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 @@ -284,13 +284,28 @@ public class OrderController { existingOrder.setTotalAmount(newAmount); existingOrder.setPaymentMethod(updatedOrder.getPaymentMethod()); - existingOrder.getItems().clear(); + if (updatedOrder.getStatus() != null) { + existingOrder.setStatus(updatedOrder.getStatus().toUpperCase()); + } + + // Safely copy items into new instances (without IDs) to trigger correct orphan removal + List newItems = new ArrayList<>(); if (updatedOrder.getItems() != null) { - for (OrderItem newItem : updatedOrder.getItems()) { + for (OrderItem item : updatedOrder.getItems()) { + OrderItem newItem = new OrderItem(); + newItem.setProductId(item.getProductId()); + newItem.setProductName(item.getProductName()); + newItem.setPrice(item.getPrice()); + newItem.setQuantity(item.getQuantity()); + newItem.setStallId(item.getStallId()); + newItem.setStallName(item.getStallName()); newItem.setOrder(existingOrder); - existingOrder.getItems().add(newItem); + newItems.add(newItem); } } + existingOrder.getItems().clear(); + existingOrder.getItems().addAll(newItems); + Order saved = orderRepository.save(existingOrder); return ResponseEntity.ok(saved); }).orElse(ResponseEntity.notFound().build()); diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/PurchaseController.java b/backend/src/main/java/com/rit/canteen/sales/controller/PurchaseController.java index 5c5406b9..123a9d50 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/PurchaseController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/PurchaseController.java @@ -28,7 +28,12 @@ public class PurchaseController { private SystemNotificationService notificationService; @GetMapping("/orders") - public List getAllOrders() { + public List getAllOrders( + @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) { + if (from != null && to != null) { + return purchaseService.getAllOrdersInRange(from, to); + } return purchaseService.getAllOrders(); } @@ -83,7 +88,12 @@ public class PurchaseController { } @GetMapping("/summary") - public Map getSummary() { + public Map getSummary( + @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) { + if (from != null && to != null) { + return purchaseService.getPurchaseSummaryInRange(from, to); + } return purchaseService.getPurchaseSummary(); } diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/PurchaseOrderRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/PurchaseOrderRepository.java index f12c6499..6d1fa90b 100644 --- a/backend/src/main/java/com/rit/canteen/sales/repository/PurchaseOrderRepository.java +++ b/backend/src/main/java/com/rit/canteen/sales/repository/PurchaseOrderRepository.java @@ -23,6 +23,12 @@ public interface PurchaseOrderRepository extends JpaRepository= :start AND p.date <= :end") java.math.BigDecimal getTotalPurchaseAmountInRange(@org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start, @org.springframework.data.repository.query.Param("end") java.time.LocalDateTime end); + @org.springframework.data.jpa.repository.Query("SELECT SUM(COALESCE(p.amount, 0) - COALESCE(p.paidTotal, 0)) FROM PurchaseOrder p WHERE p.date >= :start AND p.date <= :end") + java.math.BigDecimal getTotalBalanceAmountInRange(@org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start, @org.springframework.data.repository.query.Param("end") java.time.LocalDateTime end); + + @org.springframework.data.jpa.repository.Query("SELECT COUNT(p) FROM PurchaseOrder p WHERE p.status != 'PAID' AND p.date >= :start AND p.date <= :end") + long countUnpaidBillsInRange(@org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start, @org.springframework.data.repository.query.Param("end") java.time.LocalDateTime end); + @org.springframework.data.jpa.repository.Query("SELECT p.vendor.name, SUM(p.amount), COUNT(p) FROM PurchaseOrder p WHERE p.date >= :start AND p.date <= :end GROUP BY p.vendor.name") List getVendorSummary(@org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start, @org.springframework.data.repository.query.Param("end") java.time.LocalDateTime end); @@ -32,6 +38,9 @@ public interface PurchaseOrderRepository extends JpaRepository getPurchaseTrend(); + @org.springframework.data.jpa.repository.Query("SELECT p FROM PurchaseOrder p WHERE p.date >= :start AND p.date <= :end ORDER BY p.date DESC") + List findByDateInRange(@org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start, @org.springframework.data.repository.query.Param("end") java.time.LocalDateTime end); + long countByStatus(String status); @org.springframework.data.jpa.repository.Query("SELECT SUM(i.quantity) FROM PurchaseOrder p JOIN p.items i WHERE p.status = 'OPEN'") diff --git a/backend/src/main/java/com/rit/canteen/sales/service/DevicePairingService.java b/backend/src/main/java/com/rit/canteen/sales/service/DevicePairingService.java index b0b53b19..608d7afe 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/DevicePairingService.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/DevicePairingService.java @@ -66,18 +66,27 @@ public class DevicePairingService { public Map registerOtp(String otp, String deviceId) { PairingRequest existing = pendingPairings.get(otp); - // If this OTP was already linked by admin, return the apiKey - if (existing != null && existing.completed && existing.deviceId.equals(deviceId)) { - // Clean up after delivering the key - pendingPairings.remove(otp); - return Map.of( - "status", "PAIRED", - "apiKey", existing.apiKey, - "terminalId", existing.terminalId - ); + if (existing != null) { + // If this OTP was already linked by admin, return the apiKey + if (existing.completed) { + // Clean up after delivering the key + pendingPairings.remove(otp); + return Map.of( + "status", "PAIRED", + "apiKey", existing.apiKey, + "terminalId", existing.terminalId + ); + } + + // If the existing request has a placeholder device ID but this call has a real one, update it + if (existing.deviceId.equals("ESP32-Device") && !deviceId.equals("ESP32-Device")) { + pendingPairings.put(otp, new PairingRequest(otp, deviceId)); + } + + return Map.of("status", "WAITING"); } - // Register or refresh the OTP + // Register new OTP pendingPairings.put(otp, new PairingRequest(otp, deviceId)); return Map.of("status", "WAITING"); diff --git a/backend/src/main/java/com/rit/canteen/sales/service/PurchaseService.java b/backend/src/main/java/com/rit/canteen/sales/service/PurchaseService.java index 3f09fed1..e3a171c1 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/PurchaseService.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/PurchaseService.java @@ -40,6 +40,10 @@ public class PurchaseService { return purchaseOrderRepository.findAll(); } + public List getAllOrdersInRange(java.time.LocalDateTime start, java.time.LocalDateTime end) { + return purchaseOrderRepository.findByDateInRange(start, end); + } + public List getAllVendors() { return vendorRepository.findAll(); } @@ -132,6 +136,30 @@ public class PurchaseService { return summary; } + public Map getPurchaseSummaryInRange(java.time.LocalDateTime start, java.time.LocalDateTime end) { + Map summary = new HashMap<>(); + BigDecimal total = purchaseOrderRepository.getTotalPurchaseAmountInRange(start, end); + BigDecimal balance = purchaseOrderRepository.getTotalBalanceAmountInRange(start, end); + long unpaidCount = purchaseOrderRepository.countUnpaidBillsInRange(start, end); + + summary.put("totalAmount", total != null ? total : BigDecimal.ZERO); + summary.put("balanceAmount", balance != null ? balance : BigDecimal.ZERO); + summary.put("paidAmount", (total != null ? total : BigDecimal.ZERO).subtract(balance != null ? balance : BigDecimal.ZERO)); + summary.put("unpaidCount", unpaidCount); + + List trendData = purchaseOrderRepository.getPurchaseTrendInRange(start, end); + List> trend = new ArrayList<>(); + for (Object[] row : trendData) { + Map point = new HashMap<>(); + point.put("date", row[0].toString()); + point.put("amount", row[1]); + trend.add(point); + } + summary.put("trend", trend); + + return summary; + } + public Map getIntentSummary() { Map summary = new HashMap<>(); summary.put("openCount", purchaseOrderRepository.countByStatus("OPEN")); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2b56bab5..9a9d12d5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -80,9 +80,7 @@ function App() { } /> } /> } /> - } /> - } /> - } /> + {/* Inventory */} } /> diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 81d6bb3c..9fe0677f 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -80,10 +80,7 @@ const menuItems: MenuItem[] = [ { title: 'Orders Dashboard', path: '/purchases/intent/orders-dashboard' }, { title: 'Receives Dashboard', path: '/purchases/intent/receives-dashboard' }, { title: 'Orders', path: '/purchases/intent/orders' }, - { title: 'Receives', path: '/purchases/intent/receives' }, - { title: 'Receives Summary', path: '/purchases/intent/receives-summary' }, - { title: 'Request', path: '/purchases/intent/request' }, - { title: 'Stores', path: '/purchases/intent/stores' } + { title: 'Receives', path: '/purchases/intent/receives' } ] } ] diff --git a/frontend/src/pages/PurchaseSummary.tsx b/frontend/src/pages/PurchaseSummary.tsx index c1ba76ac..6cc4ae32 100644 --- a/frontend/src/pages/PurchaseSummary.tsx +++ b/frontend/src/pages/PurchaseSummary.tsx @@ -19,18 +19,86 @@ import { motion } from 'framer-motion'; const PurchaseSummary = () => { const [activeRange, setActiveRange] = useState('Today'); + const [customDates, setCustomDates] = useState({ from: '', to: '' }); const [data, setData] = useState(null); const [bills, setBills] = useState([]); const [isLoading, setIsLoading] = useState(true); + 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; + 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: toLocalISOString(from), to: toLocalISOString(to) }; + } + return null; + default: + start.setHours(0, 0, 0, 0); + } + return { from: toLocalISOString(start), to: toLocalISOString(end) }; + }; + useEffect(() => { const fetchData = async () => { try { setIsLoading(true); - // Fetch summary and bills in parallel + const range = getRangeDates(activeRange); + if (!range) { + setIsLoading(false); + return; + } + + const params = new URLSearchParams(); + params.append('from', range.from); + params.append('to', range.to); + const queryStr = `?${params.toString()}`; + + // Fetch summary and bills in parallel with range params const [summaryRes, billsRes] = await Promise.all([ - apiFetch('/api/purchases/summary'), - apiFetch('/api/purchases/orders') + apiFetch(`/api/purchases/summary${queryStr}`), + apiFetch(`/api/purchases/orders${queryStr}`) ]); if (summaryRes.ok && billsRes.ok) { @@ -46,7 +114,7 @@ const PurchaseSummary = () => { } }; fetchData(); - }, []); + }, [activeRange, customDates]); if (isLoading || !data) { return ( @@ -84,18 +152,37 @@ const PurchaseSummary = () => {

Summary

-
- {['Yesterday', 'Today', 'Week', '30 Days'].map(range => ( - - ))} -
- +
+
+ {['Yesterday', 'Today', 'Week', '30 Days', 'Custom'].map(range => ( + + ))} +
+ {activeRange === 'Custom' && ( +
+ setCustomDates({ ...customDates, from: e.target.value })} + className="bg-white border border-slate-200 rounded-lg px-2 py-1.5 text-[10px] font-bold text-slate-600 outline-none focus:border-[#003317]" + /> + to + setCustomDates({ ...customDates, to: e.target.value })} + className="bg-white border border-slate-200 rounded-lg px-2 py-1.5 text-[10px] font-bold text-slate-600 outline-none focus:border-[#003317]" + /> +
+ )} +
+
diff --git a/frontend/src/pages/StoreDashboard.tsx b/frontend/src/pages/StoreDashboard.tsx index 0aa210f9..07c57e47 100644 --- a/frontend/src/pages/StoreDashboard.tsx +++ b/frontend/src/pages/StoreDashboard.tsx @@ -1,4 +1,4 @@ -import { apiFetch } from '../api'; +import { apiFetch } from '../api'; import { useState, useEffect } from 'react'; import { ChevronRight, @@ -27,6 +27,7 @@ import { motion } from 'framer-motion'; const StoreDashboard = () => { const [activeTab, setActiveTab] = useState('Sales'); const [timeRange, setTimeRange] = useState('Today'); + const [customDates, setCustomDates] = useState({ from: '', to: '' }); const [stats, setStats] = useState({ totalSales: 0, activeOrders: 0, @@ -85,6 +86,15 @@ const StoreDashboard = () => { 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: toLocalISOString(from), to: toLocalISOString(to) }; + } + return null; default: start.setHours(0, 0, 0, 0); } @@ -95,12 +105,16 @@ const StoreDashboard = () => { const fetchStats = async () => { try { setIsLoading(true); - const range = getRangeDates(timeRange); - const params = new URLSearchParams(); - params.append('from', range.from); - params.append('to', range.to); + const range = getRangeDates(timeRange); + if (!range) { + setIsLoading(false); + return; + } + const params = new URLSearchParams(); + params.append('from', range.from); + params.append('to', range.to); - const response = await apiFetch(`/api/dashboard/stats?${params.toString()}`); + const response = await apiFetch(`/api/dashboard/stats?${params.toString()}`); if (response.ok) { const data = await response.json(); console.log('Dashboard data received successfully:', data); @@ -138,7 +152,7 @@ const StoreDashboard = () => { } }; fetchStats(); - }, [timeRange]); + }, [timeRange, customDates]); const pieData = [ { name: 'Full Payment', value: Number(stats.totalSales) || 0, color: '#8b5cf6' }, @@ -263,22 +277,41 @@ const StoreDashboard = () => { {/* Right Stats Column */}
- {/* Filters */} -
- {['Yesterday', 'Today', 'Week', '30 Days', 'Custom'].map(range => ( - - ))} -
+ {/* Filters */} +
+
+ {['Yesterday', 'Today', 'Week', '30 Days', 'Custom'].map(range => ( + + ))} +
+ {timeRange === 'Custom' && ( +
+ 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-[#003317]" + /> + to + 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-[#003317]" + /> +
+ )} +
{/* Total Orders Card */}
diff --git a/ordering_site/src/pages/MyOrdersScreen.tsx b/ordering_site/src/pages/MyOrdersScreen.tsx index 4ad3cdfc..c8082db9 100644 --- a/ordering_site/src/pages/MyOrdersScreen.tsx +++ b/ordering_site/src/pages/MyOrdersScreen.tsx @@ -41,7 +41,6 @@ const MyOrdersScreen: React.FC = () => { const [showStockAlert, setShowStockAlert] = useState(false); const [unavailableItems, setUnavailableItems] = useState([]); const [pendingItems, setPendingItems] = useState([]); - const [isRegenerating, setIsRegenerating] = useState(false); const isOrderExpired = (order: Order) => { if (order.isArchived) return true; @@ -133,44 +132,6 @@ const MyOrdersScreen: React.FC = () => { navigate('/cart'); }; - const handleRegenerateQR = async () => { - if (!selectedOrder) return; - setIsRegenerating(true); - try { - const token = localStorage.getItem('token'); - const newOrderNumber = `ORD-${Math.random().toString(36).substring(2, 10).toUpperCase()}`; - const response = await fetch(`http://${window.location.hostname}:8080/api/orders/${selectedOrder.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - ...(token ? { 'Authorization': `Bearer ${token}` } : {}) - }, - body: JSON.stringify({ - ...selectedOrder, - orderNumber: newOrderNumber - }) - }); - - if (response.ok) { - await fetchOrders(); - // Since selectedOrder is used in the modal, we need to update it as well - const updated = orders.find(o => o.id === selectedOrder.id); - if (updated) setSelectedOrder({ ...updated, orderNumber: newOrderNumber }); - else fetchOrders().then(() => { - // Fallback refresh to catch the update - setOrders(prev => prev.map(o => o.id === selectedOrder.id ? { ...o, orderNumber: newOrderNumber } : o)); - setSelectedOrder(prev => prev ? { ...prev, orderNumber: newOrderNumber } : null); - }); - } else { - alert('Failed to regenerate sync code'); - } - } catch (error) { - console.error('Error regenerating QR:', error); - } finally { - setIsRegenerating(false); - } - }; - const handleProceedPartial = () => { performRepeat(pendingItems); setShowStockAlert(false); @@ -324,23 +285,14 @@ const MyOrdersScreen: React.FC = () => { )}

- {isOrderExpired(selectedOrder) + {isOrderExpired(selectedOrder) ? 'QR Expired' - : selectedOrder.status.toUpperCase() === 'COMPLETED' - ? 'This order has been fulfilled' + : selectedOrder.status.toUpperCase() === 'COMPLETED' + ? 'This order has been fulfilled' : 'Show this QR code at the counter'}

- -
- -
+
+
Date {new Date(selectedOrder.createdAt).toLocaleString()}