Dashboard bug fixed
This commit is contained in:
@@ -42,6 +42,13 @@ public class ProductController {
|
|||||||
return productRepository.findByIsDraftTrue();
|
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}")
|
@GetMapping("/category/{categoryName}")
|
||||||
public List<Product> getProductsByCategory(@PathVariable String categoryName) {
|
public List<Product> getProductsByCategory(@PathVariable String categoryName) {
|
||||||
return productRepository.findByCategory(categoryName);
|
return productRepository.findByCategory(categoryName);
|
||||||
|
|||||||
@@ -31,6 +31,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)")
|
@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);
|
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);
|
boolean existsByNameAndCategory(String name, String category);
|
||||||
@org.springframework.data.jpa.repository.Modifying
|
@org.springframework.data.jpa.repository.Modifying
|
||||||
@org.springframework.data.jpa.repository.Query("UPDATE Product p SET p.stock = p.stock - :quantity WHERE p.id = :id AND p.stock >= :quantity")
|
@org.springframework.data.jpa.repository.Query("UPDATE Product p SET p.stock = p.stock - :quantity WHERE p.id = :id AND p.stock >= :quantity")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.rit.canteen.sales.repository.OrderRepository;
|
|||||||
import com.rit.canteen.sales.repository.PurchaseOrderRepository;
|
import com.rit.canteen.sales.repository.PurchaseOrderRepository;
|
||||||
import com.rit.canteen.sales.repository.VendorRepository;
|
import com.rit.canteen.sales.repository.VendorRepository;
|
||||||
import com.rit.canteen.sales.repository.TokenTransactionRepository;
|
import com.rit.canteen.sales.repository.TokenTransactionRepository;
|
||||||
|
import com.rit.canteen.sales.repository.UserRepository;
|
||||||
import com.rit.canteen.sales.model.ProcurementDashboardData;
|
import com.rit.canteen.sales.model.ProcurementDashboardData;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -34,6 +35,9 @@ public class DashboardService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private TokenTransactionRepository tokenTransactionRepository;
|
private TokenTransactionRepository tokenTransactionRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
public GeneralDashboardData getGeneralDashboardData(LocalDateTime from, LocalDateTime to) {
|
public GeneralDashboardData getGeneralDashboardData(LocalDateTime from, LocalDateTime to) {
|
||||||
java.time.ZoneId zone = java.time.ZoneId.of("Asia/Kolkata");
|
java.time.ZoneId zone = java.time.ZoneId.of("Asia/Kolkata");
|
||||||
if (from == null) from = java.time.LocalDate.now(zone).atStartOfDay();
|
if (from == null) from = java.time.LocalDate.now(zone).atStartOfDay();
|
||||||
@@ -168,6 +172,8 @@ public class DashboardService {
|
|||||||
BigDecimal periodExpensesRaw = purchaseOrderRepository.getTotalPurchaseAmountInRange(from, to);
|
BigDecimal periodExpensesRaw = purchaseOrderRepository.getTotalPurchaseAmountInRange(from, to);
|
||||||
long periodExpenses = periodExpensesRaw != null ? periodExpensesRaw.longValue() : 0;
|
long periodExpenses = periodExpensesRaw != null ? periodExpensesRaw.longValue() : 0;
|
||||||
|
|
||||||
|
long suspendedUserCount = userRepository.countByIsSuspended(true);
|
||||||
|
|
||||||
double growth = 0;
|
double growth = 0;
|
||||||
if (yesterdayRevenue != null && yesterdayRevenue.compareTo(BigDecimal.ZERO) > 0) {
|
if (yesterdayRevenue != null && yesterdayRevenue.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
BigDecimal today = todayRevenue != null ? todayRevenue : BigDecimal.ZERO;
|
BigDecimal today = todayRevenue != null ? todayRevenue : BigDecimal.ZERO;
|
||||||
@@ -179,7 +185,7 @@ public class DashboardService {
|
|||||||
growth = 100.0;
|
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() {
|
public ProcurementDashboardData getProcurementDashboardData() {
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ const Customers: React.FC = () => {
|
|||||||
<CircleDollarSign size={16} />
|
<CircleDollarSign size={16} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-black text-[#231651]">
|
<span className="text-sm font-black text-[#231651]">
|
||||||
₹{user.ritzTokenBalance?.toLocaleString() || '0'}
|
R{user.ritzTokenBalance?.toLocaleString() || '0'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -400,7 +400,7 @@ const Customers: React.FC = () => {
|
|||||||
</span>
|
</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">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ const RitzPage: React.FC = () => {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<span className={`text-base font-black ${t.type === 'TOPUP' ? 'text-emerald-600' : 'text-slate-800'}`}>
|
<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>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
|||||||
import { CartProvider } from './contexts/CartContext';
|
import { CartProvider } from './contexts/CartContext';
|
||||||
import { AuthProvider } from './contexts/AuthContext';
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
import { FoodProvider } from './contexts/FoodContext';
|
import { FoodProvider } from './contexts/FoodContext';
|
||||||
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
import ProtectedRoute from './components/ProtectedRoute';
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
import HomeScreen from './pages/HomeScreen';
|
import HomeScreen from './pages/HomeScreen';
|
||||||
import CategoryScreen from './pages/CategoryScreen';
|
import CategoryScreen from './pages/CategoryScreen';
|
||||||
@@ -16,11 +17,13 @@ import ChangePinScreen from './pages/ChangePinScreen';
|
|||||||
import StallDetailScreen from './pages/StallDetailScreen';
|
import StallDetailScreen from './pages/StallDetailScreen';
|
||||||
import WalletScreen from './pages/WalletScreen';
|
import WalletScreen from './pages/WalletScreen';
|
||||||
import TopUpScreen from './pages/TopUpScreen';
|
import TopUpScreen from './pages/TopUpScreen';
|
||||||
|
import PreferencesScreen from './pages/PreferencesScreen';
|
||||||
import StockAlert from './components/StockAlert';
|
import StockAlert from './components/StockAlert';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<FoodProvider>
|
<FoodProvider>
|
||||||
<CartProvider>
|
<CartProvider>
|
||||||
@@ -90,11 +93,17 @@ function App() {
|
|||||||
<TopUpScreen />
|
<TopUpScreen />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
<Route path="/preferences" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<PreferencesScreen />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
</CartProvider>
|
</CartProvider>
|
||||||
</FoodProvider>
|
</FoodProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
height: 65px;
|
height: 65px;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: var(--surface);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(16px);
|
||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(16px);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -30,12 +30,12 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: var(--surface);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
box-shadow: 0 10px 30px rgba(35, 22, 81, 0.12);
|
box-shadow: var(--shadow);
|
||||||
color: var(--text-dark);
|
color: var(--text-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-count {
|
.cart-tab-count {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-mid);
|
color: var(--text-mid);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const CartTab: React.FC = () => {
|
|||||||
<div className="cart-left-info" onClick={() => navigate('/cart')}>
|
<div className="cart-left-info" onClick={() => navigate('/cart')}>
|
||||||
<div className="cart-stats">
|
<div className="cart-stats">
|
||||||
<ShoppingBag size={16} className="cart-icon" />
|
<ShoppingBag size={16} className="cart-icon" />
|
||||||
<span className="cart-count">
|
<span className="cart-tab-count">
|
||||||
{totalItems} {totalItems === 1 ? 'item' : 'items'}
|
{totalItems} {totalItems === 1 ? 'item' : 'items'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
background-color: rgba(35, 22, 81, 0.05); /* Light Purple Wash */
|
background-color: var(--primary-light);
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
|
|
||||||
.cart-count {
|
.cart-count {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: #000000; /* Black text as requested earlier */
|
color: #000000;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
@@ -173,3 +173,34 @@
|
|||||||
text-overflow: ellipsis;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%);
|
background: var(--primary-light);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,9 +165,9 @@
|
|||||||
bottom: -12px;
|
bottom: -12px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
background-color: white;
|
background-color: var(--surface);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.12);
|
box-shadow: var(--shadow);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
:root {
|
:root {
|
||||||
|
/* LIGHT THEME (Default) */
|
||||||
--primary: #231651;
|
--primary: #231651;
|
||||||
|
--primary-rgb: 35, 22, 81;
|
||||||
--primary-light: #f0f1ff;
|
--primary-light: #f0f1ff;
|
||||||
--bg: #ffffff;
|
--bg: #f8f9fe;
|
||||||
--surface: #ffffff;
|
--surface: #ffffff;
|
||||||
--border: rgba(35, 22, 81, 0.1);
|
--border: rgba(35, 22, 81, 0.08);
|
||||||
--green: #00B894;
|
--green: #00B894;
|
||||||
--green-light: #E6FFFA;
|
--green-light: #E6FFFA;
|
||||||
--red: #ff4d4f;
|
--red: #ff4d4f;
|
||||||
@@ -12,6 +14,28 @@
|
|||||||
--text-light: #7b71af;
|
--text-light: #7b71af;
|
||||||
--star: #f4c640;
|
--star: #f4c640;
|
||||||
--shadow: 0 4px 20px rgba(35, 22, 81, 0.05);
|
--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);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|||||||
@@ -39,8 +39,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ritz-payment-card.insufficient-state {
|
.ritz-payment-card.insufficient-state {
|
||||||
border: 1px solid rgba(244, 63, 94, 0.3);
|
border: 1px solid rgba(244, 63, 94, 0.4);
|
||||||
background: rgba(244, 63, 94, 0.03);
|
background: var(--surface);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -106,16 +106,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.insufficient-val {
|
.insufficient-val {
|
||||||
color: #f43f5e !important;
|
color: var(--red) !important;
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
opacity: 0.7;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-tokens-checkout-btn {
|
.add-tokens-checkout-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
background: white;
|
background: var(--surface);
|
||||||
border: 1.5px dashed rgba(99, 102, 241, 0.4);
|
border: 1.5px dashed var(--primary);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -158,13 +158,13 @@
|
|||||||
.insufficient-warning-premium {
|
.insufficient-warning-premium {
|
||||||
margin: 1.5rem;
|
margin: 1.5rem;
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
background: white;
|
background: var(--surface);
|
||||||
border: 1px solid rgba(244, 63, 94, 0.1);
|
border: 1px solid var(--border);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
box-shadow: 0 12px 30px -10px rgba(244, 63, 94, 0.15);
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.token-warning-visual {
|
.token-warning-visual {
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
.shortfall-amount {
|
.shortfall-amount {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #334155;
|
color: var(--text-dark);
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.address-card.selected .address-icon, .payment-card.selected .payment-icon {
|
.address-card.selected .address-icon, .payment-card.selected .payment-icon {
|
||||||
background-color: white;
|
background-color: var(--primary-light);
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@
|
|||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
background-color: var(--surface);
|
background-color: var(--surface);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
box-shadow: 0 -4px 20px rgba(0,0,0,0.05);
|
box-shadow: var(--shadow);
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
z-index: 101;
|
z-index: 101;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
background-color: #f8f9ff;
|
background-color: var(--input-bg);
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -147,9 +147,9 @@
|
|||||||
height: 200px;
|
height: 200px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #ffffff;
|
background-color: var(--surface);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
border: 1px solid var(--border);
|
||||||
box-shadow: 0 10px 25px rgba(35, 22, 81, 0.08);
|
box-shadow: var(--shadow);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
|
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -280,7 +280,7 @@
|
|||||||
bottom: 80px; /* Above bottom nav */
|
bottom: 80px; /* Above bottom nav */
|
||||||
left: 16px;
|
left: 16px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
background: white;
|
background: var(--surface);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -58,10 +58,74 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-emoji {
|
.placeholder-emoji {
|
||||||
font-size: 4rem;
|
font-size: 3rem;
|
||||||
filter: drop-shadow(0 4px 10px rgba(0,0,0,0.1));
|
opacity: 0.6;
|
||||||
animation: float 3s ease-in-out infinite;
|
}
|
||||||
z-index: 1;
|
|
||||||
|
/* 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 {
|
@keyframes float {
|
||||||
|
|||||||
@@ -10,18 +10,79 @@ import './ItemDetailScreen.css';
|
|||||||
const ItemDetailScreen: React.FC = () => {
|
const ItemDetailScreen: React.FC = () => {
|
||||||
const { itemId } = useParams<{ itemId: string }>();
|
const { itemId } = useParams<{ itemId: string }>();
|
||||||
const { addToCart, updateQuantity, getItemQuantity } = useCart();
|
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 quantity = item ? getItemQuantity(item.id) : 0;
|
||||||
const isLimitReached = item && item.stock !== undefined && quantity >= item.stock && item.stock > 0;
|
const isLimitReached = item && item.stock !== undefined && quantity >= item.stock && item.stock > 0;
|
||||||
|
|
||||||
if (isLoading && foodItems.length === 0) {
|
if ((isGlobalLoading && foodItems.length === 0) || isFetching) {
|
||||||
return <div className="container" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Loading...</div>;
|
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 && !isGlobalLoading && !isFetching) {
|
||||||
if (!item) return null;
|
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 (
|
return (
|
||||||
<div className={`container item-detail-page ${item.stock === 0 ? 'out-of-stock' : ''}`}>
|
<div className={`container item-detail-page ${item.stock === 0 ? 'out-of-stock' : ''}`}>
|
||||||
|
|||||||
@@ -26,10 +26,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.latest-order-card {
|
.latest-order-card {
|
||||||
background: white;
|
background: var(--surface);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
|
box-shadow: var(--shadow);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.order-item-list {
|
.order-item-list {
|
||||||
background: white;
|
background: var(--surface);
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
|
|
||||||
.order-detail-modal {
|
.order-detail-modal {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: white;
|
background: var(--surface);
|
||||||
border-radius: 24px 24px 0 0;
|
border-radius: 24px 24px 0 0;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
background: white;
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -401,7 +401,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stock-alert-modal {
|
.stock-alert-modal {
|
||||||
background: white;
|
background: var(--surface);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
border-radius: 28px;
|
border-radius: 28px;
|
||||||
@@ -463,10 +463,10 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #e11d48;
|
color: #e11d48;
|
||||||
background: white;
|
background: var(--bg);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid #fecdd3;
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-actions {
|
.alert-actions {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ const ProfileScreen: React.FC = () => {
|
|||||||
{ icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' },
|
{ icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' },
|
||||||
{ icon: Wallet, label: 'My Wallet', sub: 'Ritz Tokens & History', path: '/wallet' },
|
{ icon: Wallet, label: 'My Wallet', sub: 'Ritz Tokens & History', path: '/wallet' },
|
||||||
{ icon: ShieldCheck, label: 'Account Security', sub: 'Change Security PIN', path: '/change-pin' },
|
{ 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: '#' }
|
{ icon: HelpCircle, label: 'Help & Support', sub: 'FAQs, Contact Us', path: '#' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.stall-detail-page {
|
.stall-detail-page {
|
||||||
background-color: #f7f8fc;
|
background-color: var(--bg);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,9 +12,25 @@
|
|||||||
.detail-header.sticky {
|
.detail-header.sticky {
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: white;
|
background: var(--surface);
|
||||||
z-index: 100;
|
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 {
|
.stall-detail-page .back-button {
|
||||||
@@ -22,7 +38,7 @@
|
|||||||
top: 16px;
|
top: 16px;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background: white;
|
background: var(--surface);
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -30,7 +46,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-image-container {
|
.header-image-container {
|
||||||
@@ -62,11 +78,11 @@
|
|||||||
|
|
||||||
.stall-main-card {
|
.stall-main-card {
|
||||||
margin: -40px 16px 20px;
|
margin: -40px 16px 20px;
|
||||||
background: white;
|
background: var(--surface);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 8px 30px rgba(35, 22, 81, 0.08);
|
box-shadow: var(--shadow);
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +175,7 @@
|
|||||||
padding: 40px 24px;
|
padding: 40px 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
background: white;
|
background: var(--surface);
|
||||||
margin: 0 16px;
|
margin: 0 16px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--border);
|
||||||
|
|||||||
Reference in New Issue
Block a user