Dashboard bug fixed

This commit is contained in:
Sidharth Prabhu
2026-04-21 16:12:26 +05:30
parent b4bf6ebeb3
commit 239c055fc4
20 changed files with 286 additions and 65 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -42,6 +42,13 @@ public class ProductController {
return productRepository.findByIsDraftTrue();
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
return productRepository.findByIdWithStalls(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/category/{categoryName}")
public List<Product> getProductsByCategory(@PathVariable String categoryName) {
return productRepository.findByCategory(categoryName);

View File

@@ -30,6 +30,9 @@ public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT p FROM Product p WHERE p.productId = :productId AND (p.isDraft = false OR p.isDraft IS NULL)")
java.util.Optional<Product> findByProductIdRobust(@org.springframework.data.repository.query.Param("productId") String productId);
@Query("SELECT p FROM Product p LEFT JOIN FETCH p.stalls WHERE p.id = :id")
java.util.Optional<Product> findByIdWithStalls(@org.springframework.data.repository.query.Param("id") Long id);
boolean existsByNameAndCategory(String name, String category);
@org.springframework.data.jpa.repository.Modifying

View File

@@ -8,6 +8,7 @@ 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.repository.UserRepository;
import com.rit.canteen.sales.model.ProcurementDashboardData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -34,6 +35,9 @@ public class DashboardService {
@Autowired
private TokenTransactionRepository tokenTransactionRepository;
@Autowired
private UserRepository userRepository;
public GeneralDashboardData getGeneralDashboardData(LocalDateTime from, LocalDateTime to) {
java.time.ZoneId zone = java.time.ZoneId.of("Asia/Kolkata");
if (from == null) from = java.time.LocalDate.now(zone).atStartOfDay();
@@ -168,6 +172,8 @@ public class DashboardService {
BigDecimal periodExpensesRaw = purchaseOrderRepository.getTotalPurchaseAmountInRange(from, to);
long periodExpenses = periodExpensesRaw != null ? periodExpensesRaw.longValue() : 0;
long suspendedUserCount = userRepository.countByIsSuspended(true);
double growth = 0;
if (yesterdayRevenue != null && yesterdayRevenue.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal today = todayRevenue != null ? todayRevenue : BigDecimal.ZERO;
@@ -179,7 +185,7 @@ public class DashboardService {
growth = 100.0;
}
return new DashboardStats(totalSales, periodRevenue, activeOrders, dailyCustomers, growth, totalExpenses, periodExpenses);
return new DashboardStats(totalSales, periodRevenue, activeOrders, dailyCustomers, growth, totalExpenses, periodExpenses, suspendedUserCount);
}
public ProcurementDashboardData getProcurementDashboardData() {

View File

@@ -305,7 +305,7 @@ const Customers: React.FC = () => {
<CircleDollarSign size={16} />
</div>
<span className="text-sm font-black text-[#231651]">
{user.ritzTokenBalance?.toLocaleString() || '0'}
R{user.ritzTokenBalance?.toLocaleString() || '0'}
</span>
</div>
</td>
@@ -400,7 +400,7 @@ const Customers: React.FC = () => {
</span>
)}
<span className="px-2 py-0.5 bg-indigo-50 text-indigo-600 text-[10px] font-bold rounded-lg border border-indigo-100 uppercase tracking-wider">
{user.ritzTokenBalance?.toLocaleString() || '0'} TOKENS
R{user.ritzTokenBalance?.toLocaleString() || '0'} TOKENS
</span>
</div>

View File

@@ -195,7 +195,7 @@ const RitzPage: React.FC = () => {
</td>
<td className="px-6 py-4">
<span className={`text-base font-black ${t.type === 'TOPUP' ? 'text-emerald-600' : 'text-slate-800'}`}>
{t.type === 'TOPUP' ? '+' : '-'} {t.amount.toLocaleString()}
{t.type === 'TOPUP' ? '+' : '-'} R{t.amount.toLocaleString()}
</span>
</td>
<td className="px-6 py-4">

View File

@@ -2,6 +2,7 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { CartProvider } from './contexts/CartContext';
import { AuthProvider } from './contexts/AuthContext';
import { FoodProvider } from './contexts/FoodContext';
import { ThemeProvider } from './contexts/ThemeContext';
import ProtectedRoute from './components/ProtectedRoute';
import HomeScreen from './pages/HomeScreen';
import CategoryScreen from './pages/CategoryScreen';
@@ -16,14 +17,16 @@ import ChangePinScreen from './pages/ChangePinScreen';
import StallDetailScreen from './pages/StallDetailScreen';
import WalletScreen from './pages/WalletScreen';
import TopUpScreen from './pages/TopUpScreen';
import PreferencesScreen from './pages/PreferencesScreen';
import StockAlert from './components/StockAlert';
import './App.css';
function App() {
return (
<AuthProvider>
<FoodProvider>
<CartProvider>
<ThemeProvider>
<AuthProvider>
<FoodProvider>
<CartProvider>
<StockAlert />
<Router>
<Routes>
@@ -90,11 +93,17 @@ function App() {
<TopUpScreen />
</ProtectedRoute>
} />
<Route path="/preferences" element={
<ProtectedRoute>
<PreferencesScreen />
</ProtectedRoute>
} />
</Routes>
</Router>
</CartProvider>
</FoodProvider>
</AuthProvider>
</ThemeProvider>
);
}

View File

@@ -6,9 +6,9 @@
width: 100%;
max-width: 600px;
height: 65px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
background: var(--surface);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
display: flex;
justify-content: space-around;
align-items: center;

View File

@@ -30,12 +30,12 @@
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.95);
background: var(--surface);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 16px;
border: 1px solid var(--border);
box-shadow: 0 10px 30px rgba(35, 22, 81, 0.12);
box-shadow: var(--shadow);
color: var(--text-dark);
}
@@ -62,7 +62,7 @@
flex-shrink: 0;
}
.cart-count {
.cart-tab-count {
font-size: 0.75rem;
font-weight: 700;
color: var(--text-mid);

View File

@@ -19,7 +19,7 @@ const CartTab: React.FC = () => {
<div className="cart-left-info" onClick={() => navigate('/cart')}>
<div className="cart-stats">
<ShoppingBag size={16} className="cart-icon" />
<span className="cart-count">
<span className="cart-tab-count">
{totalItems} {totalItems === 1 ? 'item' : 'items'}
</span>
</div>

View File

@@ -123,7 +123,7 @@
display: flex;
align-items: center;
gap: 10px;
background-color: rgba(35, 22, 81, 0.05); /* Light Purple Wash */
background-color: var(--primary-light);
padding: 10px 14px;
border-radius: 12px;
border: 1px solid var(--border);
@@ -144,7 +144,7 @@
.cart-count {
background-color: white;
color: #000000; /* Black text as requested earlier */
color: #000000;
border-radius: 50%;
width: 18px;
height: 18px;
@@ -173,3 +173,34 @@
text-overflow: ellipsis;
}
}
/* Dark Mode Overrides for Light Header */
[data-theme='dark'] .app-header {
background-color: #ffffff;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
[data-theme='dark'] .app-header .header-title,
[data-theme='dark'] .app-header .location-label {
color: #231651;
}
[data-theme='dark'] .app-header .location-sub {
color: #4a3e7a;
}
[data-theme='dark'] .app-header .back-button,
[data-theme='dark'] .app-header .icon-button:not(.profile) {
background-color: #f0f1ff;
color: #231651;
}
[data-theme='dark'] .app-header .search-bar {
background-color: #f0f1ff;
border-color: rgba(35, 22, 81, 0.1);
}
[data-theme='dark'] .app-header .cart-count {
background-color: #231651;
color: white;
}

View File

@@ -151,7 +151,7 @@
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%);
background: var(--primary-light);
border: 1px solid var(--border);
}
@@ -165,9 +165,9 @@
bottom: -12px;
left: 50%;
transform: translateX(-50%);
background-color: white;
background-color: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.12);
box-shadow: var(--shadow);
border: 1px solid var(--border);
min-width: 80px;
display: flex;

View File

@@ -1,9 +1,11 @@
:root {
/* LIGHT THEME (Default) */
--primary: #231651;
--primary-rgb: 35, 22, 81;
--primary-light: #f0f1ff;
--bg: #ffffff;
--bg: #f8f9fe;
--surface: #ffffff;
--border: rgba(35, 22, 81, 0.1);
--border: rgba(35, 22, 81, 0.08);
--green: #00B894;
--green-light: #E6FFFA;
--red: #ff4d4f;
@@ -12,6 +14,28 @@
--text-light: #7b71af;
--star: #f4c640;
--shadow: 0 4px 20px rgba(35, 22, 81, 0.05);
--input-bg: #f8f9ff;
--modal-overlay: rgba(35, 22, 81, 0.3);
}
[data-theme='dark'] {
/* DARK THEME - Visual Poetry Deep Slate & Indigo */
--primary: #9d87ff; /* Softer, glowing purple for dark mode */
--primary-rgb: 157, 135, 255;
--primary-light: #251b4d;
--bg: #0b0e14;
--surface: #151921;
--border: rgba(157, 135, 255, 0.12);
--green: #00d2aa;
--green-light: #062d26;
--red: #ff6b6b;
--text-dark: #f0f2f5;
--text-mid: #a0aec0;
--text-light: #718096;
--star: #ffd700;
--shadow: 0 4px 30px rgba(0, 0, 0, 0.4);
--input-bg: #1c212b;
--modal-overlay: rgba(0, 0, 0, 0.7);
}
* {

View File

@@ -39,8 +39,8 @@
}
.ritz-payment-card.insufficient-state {
border: 1px solid rgba(244, 63, 94, 0.3);
background: rgba(244, 63, 94, 0.03);
border: 1px solid rgba(244, 63, 94, 0.4);
background: var(--surface);
position: relative;
overflow: hidden;
}
@@ -106,16 +106,16 @@
}
.insufficient-val {
color: #f43f5e !important;
color: var(--red) !important;
text-decoration: line-through;
opacity: 0.7;
opacity: 0.8;
}
.add-tokens-checkout-btn {
width: 100%;
margin-top: 12px;
background: white;
border: 1.5px dashed rgba(99, 102, 241, 0.4);
background: var(--surface);
border: 1.5px dashed var(--primary);
padding: 1rem;
border-radius: 18px;
display: flex;
@@ -158,13 +158,13 @@
.insufficient-warning-premium {
margin: 1.5rem;
padding: 1.25rem;
background: white;
border: 1px solid rgba(244, 63, 94, 0.1);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 24px;
display: flex;
align-items: center;
gap: 1.25rem;
box-shadow: 0 12px 30px -10px rgba(244, 63, 94, 0.15);
box-shadow: var(--shadow);
}
.token-warning-visual {
@@ -201,7 +201,7 @@
.shortfall-amount {
font-size: 1rem;
font-weight: 800;
color: #334155;
color: var(--text-dark);
margin-bottom: 0.25rem;
}
@@ -248,7 +248,7 @@
}
.address-card.selected .address-icon, .payment-card.selected .payment-icon {
background-color: white;
background-color: var(--primary-light);
color: var(--primary);
}
@@ -366,7 +366,7 @@
max-width: 600px;
background-color: var(--surface);
padding: 16px;
box-shadow: 0 -4px 20px rgba(0,0,0,0.05);
box-shadow: var(--shadow);
border-top: 1px solid var(--border);
z-index: 101;
}

View File

@@ -25,7 +25,7 @@
display: flex;
align-items: center;
gap: 10px;
background-color: #f8f9ff;
background-color: var(--input-bg);
padding: 10px 14px;
border-radius: 12px;
border: 1px solid var(--border);
@@ -147,9 +147,9 @@
height: 200px;
border-radius: 24px;
overflow: hidden;
background-color: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.04);
box-shadow: 0 10px 25px rgba(35, 22, 81, 0.08);
background-color: var(--surface);
border: 1px solid var(--border);
box-shadow: var(--shadow);
cursor: pointer;
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
position: relative;
@@ -280,7 +280,7 @@
bottom: 80px; /* Above bottom nav */
left: 16px;
right: 16px;
background: white;
background: var(--surface);
border-radius: 20px;
padding: 12px 16px;
display: flex;

View File

@@ -58,10 +58,74 @@
}
.placeholder-emoji {
font-size: 4rem;
filter: drop-shadow(0 4px 10px rgba(0,0,0,0.1));
animation: float 3s ease-in-out infinite;
z-index: 1;
font-size: 3rem;
opacity: 0.6;
}
/* Loading & Not Found States */
.item-loading-wrapper, .item-not-found-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
background: var(--bg);
}
.loading-spinner-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
color: var(--text-mid);
font-weight: 600;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.not-found-content {
padding: 40px 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.not-found-icon {
color: var(--text-light);
margin-bottom: 8px;
}
.not-found-content h2 {
font-size: 1.5rem;
font-weight: 800;
color: var(--text-dark);
}
.not-found-content p {
color: var(--text-mid);
max-width: 250px;
}
.back-home-btn {
margin-top: 12px;
padding: 12px 24px;
background: var(--primary);
color: white;
border-radius: 12px;
font-weight: 700;
border: none;
}
@keyframes float {

View File

@@ -10,18 +10,79 @@ import './ItemDetailScreen.css';
const ItemDetailScreen: React.FC = () => {
const { itemId } = useParams<{ itemId: string }>();
const { addToCart, updateQuantity, getItemQuantity } = useCart();
const { foodItems, isLoading } = useFood();
const { foodItems, isLoading: isGlobalLoading } = useFood();
const [fetchedItem, setFetchedItem] = React.useState<FoodItem | null>(null);
const [isFetching, setIsFetching] = React.useState(false);
const item = foodItems.find((i) => i.id === itemId);
const contextItem = foodItems.find((i) => i.id === itemId);
const item = contextItem || fetchedItem;
React.useEffect(() => {
if (!contextItem && itemId) {
const fetchItem = async () => {
setIsFetching(true);
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/products/${itemId}`);
if (response.ok) {
const data = await response.json();
// Map backend product to FoodItem type
const rawImg = data.imageData?.trim();
const finalImage = rawImg ? (rawImg.startsWith('data:') ? rawImg : `data:image/png;base64,${rawImg}`) : '';
const stallInfo = data.stalls && data.stalls.length > 0 ? data.stalls[0] : null;
const mappedItem: FoodItem = {
id: data.id.toString(),
name: data.name,
description: data.description || 'Quality food prepared with care',
price: data.price || data.basePrice || 0,
category: data.category,
image: finalImage,
isVeg: data.veg,
isPopular: data.active,
stock: data.stock,
stallId: stallInfo?.id?.toString(),
stallName: stallInfo?.name
};
setFetchedItem(mappedItem);
}
} catch (err) {
console.error('Error fetching individual product:', err);
} finally {
setIsFetching(false);
}
};
fetchItem();
}
}, [itemId, contextItem]);
const quantity = item ? getItemQuantity(item.id) : 0;
const isLimitReached = item && item.stock !== undefined && quantity >= item.stock && item.stock > 0;
if (isLoading && foodItems.length === 0) {
return <div className="container" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Loading...</div>;
if ((isGlobalLoading && foodItems.length === 0) || isFetching) {
return (
<div className="container item-loading-wrapper">
<div className="loading-spinner-wrapper">
<div className="loading-spinner"></div>
<p>Loading Delights...</p>
</div>
</div>
);
}
if (!item && !isLoading) return <div className="container" style={{ padding: 24 }}>Item not found</div>;
if (!item) return null;
if (!item && !isGlobalLoading && !isFetching) {
return (
<div className="container item-not-found-wrapper">
<Header />
<div className="not-found-content">
<AlertCircle size={64} className="not-found-icon" />
<h2>Item Not Found</h2>
<p>Oops! The item you're looking for seems to have vanished.</p>
<button className="back-home-btn" onClick={() => window.history.back()}>Go Back</button>
</div>
</div>
);
}
return (
<div className={`container item-detail-page ${item.stock === 0 ? 'out-of-stock' : ''}`}>

View File

@@ -26,10 +26,10 @@
}
.latest-order-card {
background: white;
background: var(--surface);
border-radius: 24px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
box-shadow: var(--shadow);
border: 1px solid var(--border);
display: flex;
flex-direction: column;
@@ -129,7 +129,7 @@
}
.order-item-list {
background: white;
background: var(--surface);
padding: 14px;
border-radius: 16px;
display: flex;
@@ -203,7 +203,7 @@
.order-detail-modal {
width: 100%;
background: white;
background: var(--surface);
border-radius: 24px 24px 0 0;
padding: 24px;
display: flex;
@@ -263,7 +263,7 @@
display: flex;
align-items: center;
gap: 8px;
background: white;
background: var(--surface);
border: 1px solid var(--border);
padding: 8px 16px;
border-radius: 20px;
@@ -401,7 +401,7 @@
}
.stock-alert-modal {
background: white;
background: var(--surface);
width: 100%;
max-width: 400px;
border-radius: 28px;
@@ -463,10 +463,10 @@
font-size: 13px;
font-weight: 700;
color: #e11d48;
background: white;
background: var(--bg);
padding: 8px 12px;
border-radius: 10px;
border: 1px solid #fecdd3;
border: 1px solid var(--border);
}
.alert-actions {

View File

@@ -79,7 +79,7 @@ const ProfileScreen: React.FC = () => {
{ icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' },
{ icon: Wallet, label: 'My Wallet', sub: 'Ritz Tokens & History', path: '/wallet' },
{ icon: ShieldCheck, label: 'Account Security', sub: 'Change Security PIN', path: '/change-pin' },
{ icon: Settings, label: 'Preferences', sub: 'Notifications, Language', path: '#' },
{ icon: Settings, label: 'Preferences', sub: 'Dark Mode, Language', path: '/preferences' },
{ icon: HelpCircle, label: 'Help & Support', sub: 'FAQs, Contact Us', path: '#' }
];

View File

@@ -1,5 +1,5 @@
.stall-detail-page {
background-color: #f7f8fc;
background-color: var(--bg);
min-height: 100vh;
}
@@ -12,9 +12,25 @@
.detail-header.sticky {
height: auto;
padding: 12px 16px;
background: white;
background: var(--surface);
z-index: 100;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
box-shadow: var(--shadow);
}
.stall-detail-page .add-to-cart-container {
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
background-color: var(--surface);
border-radius: 8px;
box-shadow: var(--shadow);
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
color: var(--primary);
box-shadow: var(--shadow);
}
.stall-detail-page .back-button {
@@ -22,7 +38,7 @@
top: 16px;
left: 16px;
z-index: 10;
background: white;
background: var(--surface);
width: 40px;
height: 40px;
border-radius: 50%;
@@ -30,7 +46,7 @@
align-items: center;
justify-content: center;
color: var(--primary);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
box-shadow: var(--shadow);
}
.header-image-container {
@@ -62,11 +78,11 @@
.stall-main-card {
margin: -40px 16px 20px;
background: white;
background: var(--surface);
border-radius: 24px;
padding: 24px;
position: relative;
box-shadow: 0 8px 30px rgba(35, 22, 81, 0.08);
box-shadow: var(--shadow);
z-index: 5;
}
@@ -159,7 +175,7 @@
padding: 40px 24px;
text-align: center;
color: var(--text-light);
background: white;
background: var(--surface);
margin: 0 16px;
border-radius: 16px;
border: 1px dashed var(--border);