Bill Bot 1

This commit is contained in:
Sidharth Prabhu
2026-06-17 21:02:49 +05:30
parent df9f451fc8
commit a96e36e409
30 changed files with 1665 additions and 516 deletions

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Star, Send, X } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Star } from 'lucide-react';
import { motion } from 'framer-motion';
import './FeedbackModal.css';
interface OrderItem {

View File

@@ -5,7 +5,7 @@ interface AuthContextType {
user: User | null;
isLoading: boolean;
checkUserExists: (mobileNumber: string) => Promise<{ success: boolean; userExists: boolean; message: string }>;
login: (mobileNumber: string, pin: string) => Promise<{ success: boolean; message: string }>;
login: (mobileNumber: string, pin: string) => Promise<{ success: boolean; message: string; isSuspended?: boolean }>;
register: (mobileNumber: string, name: string, pin: string) => Promise<{ success: boolean; message: string }>;
logout: () => void;
changePin: (currentPin: string, newPin: string) => Promise<{ success: boolean; message: string }>;
@@ -53,9 +53,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (data.success && data.user) {
setUser(data.user);
localStorage.setItem('user', JSON.stringify(data.user));
if (data.token) {
localStorage.setItem('token', data.token);
}
return { success: true, message: data.message };
}
return { success: false, message: data.message || 'Login failed' };
return { success: false, message: data.message || 'Login failed', isSuspended: data.suspended || data.isSuspended };
} catch (error) {
return { success: false, message: 'Network error. Please try again.' };
}
@@ -72,6 +75,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (data.success && data.user) {
setUser(data.user);
localStorage.setItem('user', JSON.stringify(data.user));
if (data.token) {
localStorage.setItem('token', data.token);
}
return { success: true, message: data.message };
}
return { success: false, message: data.message || 'Registration failed' };
@@ -82,22 +88,31 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const logout = () => {
if (user) {
const token = localStorage.getItem('token');
fetch(`${API_BASE_URL}/logout`, { cache: 'no-store',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify({ mobileNumber: user.mobileNumber }),
});
}
setUser(null);
localStorage.removeItem('user');
localStorage.removeItem('token');
};
const changePin = async (currentPin: string, newPin: string) => {
if (!user) return { success: false, message: 'Not logged in' };
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/change-pin`, { cache: 'no-store',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify({ mobileNumber: user.mobileNumber, currentPin, newPin }),
});
const data = await response.json();
@@ -110,9 +125,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const updateProfile = async (name: string, mobileNumber: string) => {
if (!user) return { success: false, message: 'Not logged in' };
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/users/${user.id}`, { cache: 'no-store',
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify({ name, mobileNumber }),
});
@@ -141,7 +160,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const refreshUser = async () => {
if (!user) return;
try {
const response = await fetch(`${API_BASE_URL}/user/${user.mobileNumber}`, { cache: 'no-store' });
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/user/${user.mobileNumber}`, {
cache: 'no-store',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
if (response.ok) {
const data = await response.json();
const updatedUser: User = {

View File

@@ -7,7 +7,7 @@ interface FoodContextType {
stalls: Stall[];
isLoading: boolean;
error: string | null;
refreshData: () => Promise<void>;
refreshData: (silent?: boolean) => Promise<void>;
}
const FoodContext = createContext<FoodContextType | undefined>(undefined);

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
CircleDollarSign,
Wallet,
ChevronRight,
AlertCircle,
@@ -24,7 +23,6 @@ const CheckoutScreen: React.FC = () => {
const { refreshData } = useFood();
const [isProcessing, setIsProcessing] = useState(false);
const [currentBalance, setCurrentBalance] = useState<number>(user?.ritzTokenBalance || 0);
const [isLoadingBalance, setIsLoadingBalance] = useState(true);
// Conflict state
const [stockConflicts, setStockConflicts] = useState<any[]>([]);
@@ -37,14 +35,16 @@ const CheckoutScreen: React.FC = () => {
const fetchBalance = async () => {
if (!user) return;
try {
setIsLoadingBalance(true);
const response = await fetch(`http://${window.location.hostname}:8080/api/wallet/balance/${user.id}`);
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/wallet/balance/${user.id}`, {
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
const data = await response.json();
setCurrentBalance(data.balance || 0);
} catch (error) {
console.error('Error fetching balance:', error);
} finally {
setIsLoadingBalance(false);
}
};
@@ -76,9 +76,13 @@ const CheckoutScreen: React.FC = () => {
};
try {
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify(orderData),
});

View File

@@ -35,7 +35,12 @@ const HomeScreen: React.FC = () => {
if (sessionStorage.getItem('feedback_dismissed')) return;
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/feedback/latest-unrated/${user?.id}`);
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/feedback/latest-unrated/${user?.id}`, {
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
if (response.status === 200) {
const order = await response.json();
console.log('Unrated order found:', order);
@@ -53,8 +58,12 @@ const HomeScreen: React.FC = () => {
if (!unratedOrder) return;
try {
const token = localStorage.getItem('token');
await fetch(`http://${window.location.hostname}:8080/api/feedback/skip/${unratedOrder.id}`, {
method: 'POST'
method: 'POST',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
setShowFeedbackModal(false);
setShowFeedbackSnackbar(false);
@@ -70,9 +79,13 @@ const HomeScreen: React.FC = () => {
const handleFeedbackSubmit = async (feedbackData: any) => {
try {
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/feedback/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify(feedbackData)
});
@@ -112,7 +125,7 @@ const HomeScreen: React.FC = () => {
<div className="container" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 24, textAlign: 'center' }}>
<h2 style={{ marginBottom: 12 }}>Oops! Something went wrong</h2>
<p style={{ color: 'var(--text-mid)', marginBottom: 24 }}>{error}</p>
<button className="primary-button" onClick={refreshData} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<button className="primary-button" onClick={() => refreshData()} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<RefreshCcw size={18} /> Try Again
</button>
</div>

View File

@@ -1,4 +1,5 @@
import React from 'react';
import type { FoodItem } from '../types';
import { useParams } from 'react-router-dom';
import { AlertCircle } from 'lucide-react';
import Header from '../components/Header';
@@ -56,21 +57,18 @@ const ItemDetailScreen: React.FC = () => {
fetchItem();
}
}, [itemId, contextItem]);
const quantity = item ? getItemQuantity(item.id) : 0;
const isLimitReached = item && item.stock !== undefined && quantity >= item.stock && item.stock > 0;
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>
if (isGlobalLoading || isFetching || !item) {
if (isGlobalLoading || isFetching) {
return (
<div className="container item-loading-wrapper">
<div className="loading-spinner-wrapper">
<div className="loading-spinner"></div>
<p>Loading Delights...</p>
</div>
</div>
</div>
);
}
);
}
if (!item && !isGlobalLoading && !isFetching) {
return (
<div className="container item-not-found-wrapper">
<Header />
@@ -84,6 +82,9 @@ const ItemDetailScreen: React.FC = () => {
);
}
const quantity = getItemQuantity(item.id);
const isLimitReached = item.stock !== undefined && quantity >= item.stock && item.stock > 0;
return (
<div className={`container item-detail-page ${item.stock === 0 ? 'out-of-stock' : ''}`}>
<Header title="" />

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Clock, ShoppingBag as ShoppingBagIcon, ChevronRight as ChevronRightIcon, X, RefreshCcw, AlertCircle, ShoppingCart } from 'lucide-react';
import { Clock, ShoppingBag as ShoppingBagIcon, ChevronRight as ChevronRightIcon, X, RefreshCcw, AlertCircle } from 'lucide-react';
import { QRCodeCanvas } from 'qrcode.react';
import { motion, AnimatePresence } from 'framer-motion';
import Header from '../components/Header';
@@ -58,7 +58,7 @@ const MyOrdersScreen: React.FC = () => {
// Polling for live updates (Sync when Dashboard regenerates QR or changes status)
useEffect(() => {
let interval: NodeJS.Timeout;
let interval: ReturnType<typeof setInterval>;
// Only poll if there are active (non-finalized) orders
const hasActiveOrders = orders.some(o =>
@@ -79,7 +79,12 @@ const MyOrdersScreen: React.FC = () => {
const fetchOrders = async () => {
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/user/${user?.id}`);
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/user/${user?.id}`, {
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
const data = await response.json();
setOrders(data);
} catch (error) {
@@ -132,10 +137,14 @@ const MyOrdersScreen: React.FC = () => {
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' },
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify({
...selectedOrder,
orderNumber: newOrderNumber

View File

@@ -71,9 +71,9 @@ const StallDetailScreen: React.FC = () => {
if (!prevStall) return prevStall;
return {
...prevStall,
products: prevStall.products.map(p =>
products: prevStall.products ? prevStall.products.map(p =>
p.id === update.productId.toString() ? { ...p, stock: update.stock } : p
)
) : []
};
});
} catch (err) {