Repeat orders added
This commit is contained in:
47
ordering_site/package-lock.json
generated
47
ordering_site/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "ordering_site",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.38.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.4",
|
||||
@@ -1834,6 +1835,33 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.38.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
|
||||
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.38.0",
|
||||
"motion-utils": "^12.36.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -2398,6 +2426,21 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.38.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
|
||||
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.36.0"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.36.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
|
||||
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -2824,9 +2867,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.38.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.4",
|
||||
|
||||
@@ -9,6 +9,7 @@ interface AuthContextType {
|
||||
register: (mobileNumber: string, name: string, pin: string) => Promise<{ success: boolean; message: string }>;
|
||||
logout: () => void;
|
||||
changePin: (currentPin: string, newPin: string) => Promise<{ success: boolean; message: string }>;
|
||||
updateProfile: (name: string, mobileNumber: string) => Promise<{ success: boolean; message: string }>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
@@ -29,7 +30,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
const checkUserExists = async (mobileNumber: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/check`, {
|
||||
const response = await fetch(`${API_BASE_URL}/check`, { cache: 'no-store',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mobileNumber }),
|
||||
@@ -42,7 +43,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
const login = async (mobileNumber: string, pin: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/login`, {
|
||||
const response = await fetch(`${API_BASE_URL}/login`, { cache: 'no-store',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mobileNumber, pin }),
|
||||
@@ -61,7 +62,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
const register = async (mobileNumber: string, name: string, pin: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/register`, {
|
||||
const response = await fetch(`${API_BASE_URL}/register`, { cache: 'no-store',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mobileNumber, name, pin }),
|
||||
@@ -80,7 +81,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
const logout = () => {
|
||||
if (user) {
|
||||
fetch(`${API_BASE_URL}/logout`, {
|
||||
fetch(`${API_BASE_URL}/logout`, { cache: 'no-store',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mobileNumber: user.mobileNumber }),
|
||||
@@ -93,7 +94,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const changePin = async (currentPin: string, newPin: string) => {
|
||||
if (!user) return { success: false, message: 'Not logged in' };
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/change-pin`, {
|
||||
const response = await fetch(`${API_BASE_URL}/change-pin`, { cache: 'no-store',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mobileNumber: user.mobileNumber, currentPin, newPin }),
|
||||
@@ -105,8 +106,38 @@ 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 response = await fetch(`${API_BASE_URL}/users/${user.id}`, { cache: 'no-store',
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, mobileNumber }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const updatedUserDto = await response.json();
|
||||
// The DTO from backend might have different structure, let's map it back to User type
|
||||
const updatedUser: User = {
|
||||
id: updatedUserDto.id,
|
||||
name: updatedUserDto.name,
|
||||
mobileNumber: updatedUserDto.mobileNumber,
|
||||
isLoggedIn: updatedUserDto.loggedIn
|
||||
};
|
||||
setUser(updatedUser);
|
||||
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||
return { success: true, message: 'Profile updated successfully' };
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
return { success: false, message: errorData.message || 'Failed to update profile' };
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, message: 'Network error. Please try again.' };
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, isLoading, checkUserExists, login, register, logout, changePin }}>
|
||||
<AuthContext.Provider value={{ user, isLoading, checkUserExists, login, register, logout, changePin, updateProfile }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
@@ -29,9 +29,9 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setError(null);
|
||||
try {
|
||||
const [baseItemsRes, productsRes, stallsRes] = await Promise.all([
|
||||
fetch(`${API_BASE_URL}/base-items?size=100`),
|
||||
fetch(`${API_BASE_URL}/products?size=100`),
|
||||
fetch(`${API_BASE_URL}/stalls/active`)
|
||||
fetch(`${API_BASE_URL}/base-items?size=100`, { cache: 'no-store' }),
|
||||
fetch(`${API_BASE_URL}/products?size=100`, { cache: 'no-store' }),
|
||||
fetch(`${API_BASE_URL}/stalls/active`, { cache: 'no-store' })
|
||||
]);
|
||||
|
||||
if (!baseItemsRes.ok || !productsRes.ok || !stallsRes.ok) {
|
||||
|
||||
@@ -319,3 +319,148 @@
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Repeat Order Button */
|
||||
.repeat-order-btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 24px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 15px rgba(35, 22, 81, 0.2);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.repeat-order-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.repeat-order-btn.mini {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
background-color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Stock Alert Modal */
|
||||
.stock-alert-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 3000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.stock-alert-modal {
|
||||
background: white;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-radius: 28px;
|
||||
padding: 32px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2);
|
||||
animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@keyframes modalPop {
|
||||
from { transform: scale(0.8); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.alert-icon-wrapper {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: #fff1f2;
|
||||
color: #e11d48;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stock-alert-modal h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--text-dark);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stock-alert-modal p {
|
||||
font-size: 14px;
|
||||
color: var(--text-mid);
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.unavailable-items-list {
|
||||
width: 100%;
|
||||
background: var(--bg);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.unavailable-item-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #e11d48;
|
||||
background: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #fecdd3;
|
||||
}
|
||||
|
||||
.alert-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-proceed {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-discard {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: var(--bg);
|
||||
color: var(--text-mid);
|
||||
border-radius: 16px;
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Clock, ShoppingBag as ShoppingBagIcon, ChevronRight as ChevronRightIcon, X } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Clock, ShoppingBag as ShoppingBagIcon, ChevronRight as ChevronRightIcon, X, RefreshCcw, AlertCircle, ShoppingCart } from 'lucide-react';
|
||||
import { QRCodeCanvas } from 'qrcode.react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import Header from '../components/Header';
|
||||
import BottomNav from '../components/BottomNav';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useFood } from '../contexts/FoodContext';
|
||||
import { useCart } from '../contexts/CartContext';
|
||||
import type { FoodItem } from '../types';
|
||||
import './MyOrdersScreen.css';
|
||||
|
||||
interface Order {
|
||||
@@ -23,11 +28,20 @@ interface Order {
|
||||
}
|
||||
|
||||
const MyOrdersScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const { foodItems } = useFood();
|
||||
const { addToCart, clearCart } = useCart();
|
||||
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
|
||||
|
||||
// State for stock alert
|
||||
const [showStockAlert, setShowStockAlert] = useState(false);
|
||||
const [unavailableItems, setUnavailableItems] = useState<string[]>([]);
|
||||
const [pendingItems, setPendingItems] = useState<FoodItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.id) {
|
||||
fetchOrders();
|
||||
@@ -46,6 +60,50 @@ const MyOrdersScreen: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRepeatOrder = (order: Order) => {
|
||||
const missing: string[] = [];
|
||||
const available: FoodItem[] = [];
|
||||
|
||||
order.items.forEach(orderItem => {
|
||||
// Find the current food item by name (since orders store names)
|
||||
// Robustness: matching by name is fallback if IDs aren't directly available in OrderItem DTO
|
||||
const currentItem = foodItems.find(fi => fi.name === orderItem.productName);
|
||||
|
||||
if (currentItem) {
|
||||
if (currentItem.stock && currentItem.stock > 0) {
|
||||
// Add multiple times based on original quantity
|
||||
for (let i = 0; i < orderItem.quantity; i++) {
|
||||
available.push(currentItem);
|
||||
}
|
||||
} else {
|
||||
missing.push(orderItem.productName);
|
||||
}
|
||||
} else {
|
||||
missing.push(orderItem.productName); // Item no longer exists in catalog
|
||||
}
|
||||
});
|
||||
|
||||
if (missing.length > 0) {
|
||||
setUnavailableItems(missing);
|
||||
setPendingItems(available);
|
||||
setShowStockAlert(true);
|
||||
} else {
|
||||
// All items available
|
||||
performRepeat(available);
|
||||
}
|
||||
};
|
||||
|
||||
const performRepeat = (items: FoodItem[]) => {
|
||||
clearCart();
|
||||
items.forEach(item => addToCart(item));
|
||||
navigate('/cart');
|
||||
};
|
||||
|
||||
const handleProceedPartial = () => {
|
||||
performRepeat(pendingItems);
|
||||
setShowStockAlert(false);
|
||||
};
|
||||
|
||||
const latestOrder = orders.length > 0 ? orders[0] : null;
|
||||
const previousOrders = orders.length > 1 ? orders.slice(1) : [];
|
||||
|
||||
@@ -108,6 +166,16 @@ const MyOrdersScreen: React.FC = () => {
|
||||
<span>Total Amount</span>
|
||||
<span className="amount-text">₹{latestOrder.totalAmount.toFixed(2)}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="repeat-order-btn mini"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRepeatOrder(latestOrder);
|
||||
}}
|
||||
>
|
||||
<RefreshCcw size={16} /> Repeat Order
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
@@ -143,74 +211,141 @@ const MyOrdersScreen: React.FC = () => {
|
||||
</main>
|
||||
|
||||
{/* Order Detail Modal / Overlay */}
|
||||
{selectedOrder && (
|
||||
<div className="order-detail-overlay" onClick={() => setSelectedOrder(null)}>
|
||||
<div className="order-detail-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="close-details" onClick={() => setSelectedOrder(null)}>
|
||||
<X size={24} />
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{selectedOrder && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="order-detail-overlay"
|
||||
onClick={() => setSelectedOrder(null)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ y: '100%' }}
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: '100%' }}
|
||||
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
||||
className="order-detail-modal"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button className="close-details" onClick={() => setSelectedOrder(null)}>
|
||||
<X size={24} />
|
||||
</button>
|
||||
|
||||
<div className="modal-header">
|
||||
<h3>Order Details</h3>
|
||||
<p className="modal-order-id">Order #{selectedOrder.displayOrderId}</p>
|
||||
</div>
|
||||
<div className="modal-header">
|
||||
<h3>Order Details</h3>
|
||||
<p className="modal-order-id">Order #{selectedOrder.displayOrderId}</p>
|
||||
</div>
|
||||
|
||||
<div className={`modal-qr-section ${selectedOrder.status.toUpperCase() === 'COMPLETED' ? 'qr-completed' : ''} ${selectedOrder.archived ? 'qr-expired' : ''}`}>
|
||||
<div className="qr-container">
|
||||
<QRCodeCanvas value={selectedOrder.orderNumber} size={200} includeMargin={true} />
|
||||
{selectedOrder.status.toUpperCase() === 'COMPLETED' && (
|
||||
<div className="qr-overlay">
|
||||
<span className="overlay-text">COMPLETED</span>
|
||||
</div>
|
||||
)}
|
||||
{selectedOrder.archived && (
|
||||
<div className="qr-overlay">
|
||||
<span className="overlay-text expired">EXPIRED</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="modal-qr-hint">
|
||||
{selectedOrder.archived
|
||||
? 'This order has expired'
|
||||
: selectedOrder.status.toUpperCase() === 'COMPLETED'
|
||||
? 'This order has been fulfilled'
|
||||
: 'Show this QR code at the counter'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="modal-info-list">
|
||||
<div className="info-item">
|
||||
<span className="label">Date</span>
|
||||
<span className="value">{new Date(selectedOrder.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="label">Payment</span>
|
||||
<span className="value">{selectedOrder.paymentMethod}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="label">Status</span>
|
||||
<span className={`value status-badge ${selectedOrder.status.toLowerCase()}`}>
|
||||
{selectedOrder.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-items-section">
|
||||
<h4>Items Ordered</h4>
|
||||
{selectedOrder.items.map((item, idx) => (
|
||||
<div key={idx} className="modal-item-row">
|
||||
<span>{item.quantity} x {item.productName}</span>
|
||||
<span>₹{(item.price * item.quantity).toFixed(0)}</span>
|
||||
<div className={`modal-qr-section ${selectedOrder.status.toUpperCase() === 'COMPLETED' ? 'qr-completed' : ''} ${selectedOrder.archived ? 'qr-expired' : ''}`}>
|
||||
<div className="qr-container">
|
||||
<QRCodeCanvas value={selectedOrder.orderNumber} size={200} includeMargin={true} />
|
||||
{selectedOrder.status.toUpperCase() === 'COMPLETED' && (
|
||||
<div className="qr-overlay">
|
||||
<span className="overlay-text">COMPLETED</span>
|
||||
</div>
|
||||
)}
|
||||
{selectedOrder.archived && (
|
||||
<div className="qr-overlay">
|
||||
<span className="overlay-text expired">EXPIRED</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="modal-total-row">
|
||||
<span>Grand Total</span>
|
||||
<span>₹{selectedOrder.totalAmount.toFixed(2)}</span>
|
||||
<p className="modal-qr-hint">
|
||||
{selectedOrder.archived
|
||||
? 'This order has expired'
|
||||
: selectedOrder.status.toUpperCase() === 'COMPLETED'
|
||||
? 'This order has been fulfilled'
|
||||
: 'Show this QR code at the counter'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="modal-info-list">
|
||||
<div className="info-item">
|
||||
<span className="label">Date</span>
|
||||
<span className="value">{new Date(selectedOrder.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="label">Payment</span>
|
||||
<span className="value">{selectedOrder.paymentMethod}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="label">Status</span>
|
||||
<span className={`value status-badge ${selectedOrder.status.toLowerCase()}`}>
|
||||
{selectedOrder.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-items-section">
|
||||
<h4>Items Ordered</h4>
|
||||
{selectedOrder.items.map((item, idx) => (
|
||||
<div key={idx} className="modal-item-row">
|
||||
<span>{item.quantity} x {item.productName}</span>
|
||||
<span>₹{(item.price * item.quantity).toFixed(0)}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="modal-total-row">
|
||||
<span>Grand Total</span>
|
||||
<span>₹{selectedOrder.totalAmount.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="repeat-order-btn" onClick={() => handleRepeatOrder(selectedOrder)}>
|
||||
<RefreshCcw size={18} /> Repeat this order
|
||||
</button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Stock Alert Dialog */}
|
||||
<AnimatePresence>
|
||||
{showStockAlert && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="stock-alert-overlay"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
className="stock-alert-modal"
|
||||
>
|
||||
<div className="alert-icon-wrapper">
|
||||
<AlertCircle size={32} />
|
||||
</div>
|
||||
<h3>Items Unavailable</h3>
|
||||
<p>Some items from your previous order are currently out of stock or unavailable.</p>
|
||||
|
||||
<div className="unavailable-items-list">
|
||||
{unavailableItems.map((item, idx) => (
|
||||
<div key={idx} className="unavailable-item-chip">
|
||||
<X size={14} /> {item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="alert-actions">
|
||||
{pendingItems.length > 0 ? (
|
||||
<button className="btn-proceed" onClick={handleProceedPartial}>
|
||||
Proceed without these items
|
||||
</button>
|
||||
) : (
|
||||
<div style={{ marginBottom: '12px', fontSize: '13px', color: '#e11d48', fontWeight: '600' }}>
|
||||
All items in this order are currently unavailable.
|
||||
</div>
|
||||
)}
|
||||
<button className="btn-discard" onClick={() => setShowStockAlert(false)}>
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
|
||||
@@ -205,3 +205,140 @@
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
background: var(--surface);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--border);
|
||||
animation: modalIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalIn {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 24px 24px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: var(--bg);
|
||||
border: none;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-mid);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 12px 24px 24px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-mid);
|
||||
margin-bottom: 8px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 16px 16px 16px 48px;
|
||||
background: var(--bg);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 16px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
border-color: var(--primary);
|
||||
background: var(--surface);
|
||||
box-shadow: 0 0 0 4px var(--primary-light);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 0 24px 24px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
background: var(--bg);
|
||||
color: var(--text-mid);
|
||||
border-radius: 16px;
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
flex: 2;
|
||||
padding: 16px;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 4px 15px rgba(35, 22, 81, 0.2);
|
||||
}
|
||||
|
||||
.btn-save:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ShoppingBag,
|
||||
@@ -7,16 +7,33 @@ import {
|
||||
LogOut,
|
||||
ChevronRight,
|
||||
Phone,
|
||||
ShieldCheck
|
||||
ShieldCheck,
|
||||
User,
|
||||
X,
|
||||
User as UserIcon
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import Header from '../components/Header';
|
||||
import BottomNav from '../components/BottomNav';
|
||||
import './ProfileScreen.css';
|
||||
|
||||
const ProfileScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, logout } = useAuth();
|
||||
const { user, logout, updateProfile } = useAuth();
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [editName, setEditName] = useState(user?.name || '');
|
||||
const [editMobile, setEditMobile] = useState(user?.mobileNumber || '');
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setEditName(user.name);
|
||||
setEditMobile(user.mobileNumber);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleLogout = () => {
|
||||
if (window.confirm('Are you sure you want to logout?')) {
|
||||
@@ -25,6 +42,39 @@ const ProfileScreen: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditClick = () => {
|
||||
setEditName(user?.name || '');
|
||||
setEditMobile(user?.mobileNumber || '');
|
||||
setError(null);
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const handleUpdateProfile = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!editName.trim() || !editMobile.trim()) {
|
||||
setError('Name and mobile number are required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[0-9]{10}$/.test(editMobile)) {
|
||||
setError('Mobile number must be 10 digits');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdating(true);
|
||||
setError(null);
|
||||
|
||||
const result = await updateProfile(editName, editMobile);
|
||||
setIsUpdating(false);
|
||||
|
||||
if (result.success) {
|
||||
setIsEditModalOpen(false);
|
||||
// Optional: Show success message
|
||||
} else {
|
||||
setError(result.message);
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' },
|
||||
{ icon: ShieldCheck, label: 'Account Security', sub: 'Change Security PIN', path: '/change-pin' },
|
||||
@@ -49,10 +99,9 @@ const ProfileScreen: React.FC = () => {
|
||||
<h2 className="user-name">{user?.name || 'User Name'}</h2>
|
||||
<div className="user-contact">
|
||||
<span className="contact-item"><Phone size={14} /> {user?.mobileNumber || '+91 XXXXX XXXXX'}</span>
|
||||
{/* <span className="contact-item"><Mail size={14} /> user@example.com</span> */}
|
||||
</div>
|
||||
</div>
|
||||
<button className="edit-btn">Edit</button>
|
||||
<button className="edit-btn" onClick={handleEditClick}>Edit</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -98,6 +147,70 @@ const ProfileScreen: React.FC = () => {
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Edit Profile Modal */}
|
||||
<AnimatePresence>
|
||||
{isEditModalOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="modal-overlay"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
exit={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
className="modal-container"
|
||||
>
|
||||
<div className="modal-header">
|
||||
<h2>Edit Profile</h2>
|
||||
<button type="button" className="close-btn" onClick={() => setIsEditModalOpen(false)}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleUpdateProfile}>
|
||||
<div className="modal-body">
|
||||
{error && <div className="error-message" style={{ color: '#ff4d4f', fontSize: '0.85rem', marginBottom: '16px', fontWeight: '600' }}>{error}</div>}
|
||||
|
||||
<div className="form-group">
|
||||
<label>Full Name</label>
|
||||
<div className="input-wrapper">
|
||||
<UserIcon className="input-icon" size={20} />
|
||||
<input
|
||||
type="text"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
placeholder="Enter your name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Mobile Number</label>
|
||||
<div className="input-wrapper">
|
||||
<Phone className="input-icon" size={20} />
|
||||
<input
|
||||
type="tel"
|
||||
value={editMobile}
|
||||
onChange={(e) => setEditMobile(e.target.value)}
|
||||
placeholder="10-digit mobile number"
|
||||
maxLength={10}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn-cancel" onClick={() => setIsEditModalOpen(false)}>Cancel</button>
|
||||
<button type="submit" className="btn-save" disabled={isUpdating}>
|
||||
{isUpdating ? 'Updating...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user