Refund added

This commit is contained in:
Sidharth Prabhu
2026-06-24 14:06:31 +05:30
parent d325a704cf
commit f915a11eb8
16 changed files with 344 additions and 36 deletions

View File

@@ -81,6 +81,7 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.POST, "/api/auth/change-pin").authenticated() .requestMatchers(HttpMethod.POST, "/api/auth/change-pin").authenticated()
.requestMatchers(HttpMethod.PUT, "/api/auth/users/*").authenticated() .requestMatchers(HttpMethod.PUT, "/api/auth/users/*").authenticated()
.requestMatchers(HttpMethod.PUT, "/api/orders/*").authenticated() .requestMatchers(HttpMethod.PUT, "/api/orders/*").authenticated()
.requestMatchers(HttpMethod.POST, "/api/orders/*/cancel").authenticated()
.requestMatchers("/api/counter/**").hasAnyRole("MASTER", "MANAGER", "STAFF") .requestMatchers("/api/counter/**").hasAnyRole("MASTER", "MANAGER", "STAFF")
// ── STAFF/MANAGER/MASTER: All other management APIs ── // ── STAFF/MANAGER/MASTER: All other management APIs ──

View File

@@ -224,6 +224,35 @@ public class OrderController {
return ResponseEntity.ok(orderRepository.findByUserIdOrderByCreatedAtDesc(userId)); return ResponseEntity.ok(orderRepository.findByUserIdOrderByCreatedAtDesc(userId));
} }
@PostMapping("/{id}/cancel")
public ResponseEntity<?> cancelOrder(@PathVariable Long id) {
try {
return orderRepository.findById(id).map(order -> {
// ── SECURITY: Verify ownership of order ──
Long tokenUserId = getTokenUserId();
if (tokenUserId != null && !tokenUserId.equals(order.getUserId()) && !isStaff()) {
return ResponseEntity.status(403).body(Map.of("error", "Access denied"));
}
String oldStatus = order.getStatus();
if ("COMPLETED".equalsIgnoreCase(oldStatus) || "DELIVERED".equalsIgnoreCase(oldStatus) || "CANCELLED".equalsIgnoreCase(oldStatus)) {
return ResponseEntity.status(400).body(Map.of("error", "Cannot cancel an order that is already " + oldStatus));
}
if ("RITZ_TOKEN".equals(order.getPaymentMethod())) {
tokenService.refund(order.getUserId(), "ORD-" + order.getDisplayOrderId(),
order.getTotalAmount(), "Order Cancelled");
}
order.setStatus("CANCELLED");
orderRepository.save(order);
return ResponseEntity.ok(Map.of("success", true, "message", "Order cancelled successfully"));
}).orElse(ResponseEntity.notFound().build());
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of("error", e.getMessage()));
}
}
// ── STAFF/MASTER: order management ──────────────────────────────────── // ── STAFF/MASTER: order management ────────────────────────────────────
@PatchMapping("/{id}/status") @PatchMapping("/{id}/status")

View File

@@ -6,3 +6,18 @@
--color-background: #f4fbf7; --color-background: #f4fbf7;
--font-inter: "Inter", sans-serif; --font-inter: "Inter", sans-serif;
} }
/* Tillo Spinner Replacement for Counter Dashboard */
.animate-spin {
animation: none !important;
border-color: transparent !important;
border-top-color: transparent !important;
background-image: url('./assets/tillo_spinner.gif') !important;
background-size: contain !important;
background-position: center !important;
background-repeat: no-repeat !important;
}
.animate-spin * {
display: none !important;
}

View File

@@ -206,6 +206,23 @@ const Orders: React.FC = () => {
} }
}; };
const handleCancelOrder = async (orderId: number) => {
if (!window.confirm('Are you sure you want to cancel this order? Tokens will be refunded.')) return;
try {
const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/${orderId}/cancel`, {
method: 'POST'
});
if (response.ok) {
setShowActionMenu(false);
fetchOrders();
} else {
alert('Failed to cancel order');
}
} catch (error) {
console.error('Error cancelling order:', error);
}
};
const handleEditOrder = () => { const handleEditOrder = () => {
if (!selectedOrder) return; if (!selectedOrder) return;
setEditingItems([...selectedOrder.items]); setEditingItems([...selectedOrder.items]);
@@ -620,6 +637,14 @@ const Orders: React.FC = () => {
> >
<RotateCcw size={14} className="text-amber-500" /> Mark Undelivered <RotateCcw size={14} className="text-amber-500" /> Mark Undelivered
</button> </button>
{selectedOrder.status.toUpperCase() !== 'CANCELLED' && selectedOrder.status.toUpperCase() !== 'COMPLETED' && (
<button
onClick={() => handleCancelOrder(selectedOrder.id)}
className="w-full text-left px-4 py-3 text-xs font-bold text-rose-600 hover:bg-rose-50 flex items-center gap-3 transition-colors cursor-pointer"
>
<XIcon size={14} className="text-rose-600" /> Cancel Order
</button>
)}
</div> </div>
)} )}
</div> </div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

View File

@@ -173,3 +173,18 @@
font-size: 10pt !important; font-size: 10pt !important;
} }
} }
/* Tillo Spinner Replacement for Admin Dashboard */
.animate-spin {
animation: none !important;
border-color: transparent !important;
border-top-color: transparent !important;
background-image: url('./assets/tillo_spinner.gif') !important;
background-size: contain !important;
background-position: center !important;
background-repeat: no-repeat !important;
}
.animate-spin * {
display: none !important;
}

View File

@@ -221,7 +221,6 @@ const Login = () => {
> >
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Merchant Partner</span> <span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Merchant Partner</span>
<span className="text-xs font-bold text-[#001828] mt-0.5">Rajalakshmi Institute of Technology</span>
</div> </div>
<img src={ritLogo} alt="RIT Logo" className="h-12 w-auto object-contain filter drop-shadow-sm" /> <img src={ritLogo} alt="RIT Logo" className="h-12 w-auto object-contain filter drop-shadow-sm" />
</motion.div> </motion.div>

View File

@@ -190,6 +190,23 @@ const Orders: React.FC = () => {
} }
}; };
const handleCancelOrder = async (orderId: number) => {
if (!window.confirm('Are you sure you want to cancel this order? Tokens will be refunded.')) return;
try {
const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/${orderId}/cancel`, {
method: 'POST'
});
if (response.ok) {
setShowActionMenu(false);
fetchOrders();
} else {
alert('Failed to cancel order');
}
} catch (error) {
console.error('Error cancelling order:', error);
}
};
const handleEditOrder = () => { const handleEditOrder = () => {
if (!selectedOrder) return; if (!selectedOrder) return;
setEditingItems([...selectedOrder.items]); setEditingItems([...selectedOrder.items]);
@@ -604,6 +621,14 @@ const Orders: React.FC = () => {
> >
<RotateCcw size={14} className="text-amber-500" /> Mark Undelivered <RotateCcw size={14} className="text-amber-500" /> Mark Undelivered
</button> </button>
{selectedOrder.status.toUpperCase() !== 'CANCELLED' && selectedOrder.status.toUpperCase() !== 'COMPLETED' && (
<button
onClick={() => handleCancelOrder(selectedOrder.id)}
className="w-full text-left px-4 py-3 text-xs font-bold text-rose-600 hover:bg-rose-50 flex items-center gap-3 transition-colors cursor-pointer"
>
<XIcon size={14} className="text-rose-600" /> Cancel Order
</button>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -82,7 +82,7 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ order, userName, userId,
> >
<div className="feedback-header"> <div className="feedback-header">
<h2>Rate Your Meal</h2> <h2>Rate Your Meal</h2>
<p>How was your experience with Order #{order.orderNumber.split('-')[1]}?</p> <p>How was your experience?</p>
</div> </div>
<form onSubmit={handleSubmit} className="feedback-body"> <form onSubmit={handleSubmit} className="feedback-body">

View File

@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Bell, User, ShoppingBag, LogOut } from 'lucide-react'; import { ArrowLeft, Bell, User, ShoppingBag, LogOut } from 'lucide-react';
import { useCart } from '../contexts/CartContext'; import { useCart } from '../contexts/CartContext';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import ritLogo from '../assets/RIT_Logo.png'; import tilloLogo from '../assets/Tillo.png';
import './Header.css'; import './Header.css';
interface HeaderProps { interface HeaderProps {
@@ -33,8 +33,8 @@ const Header: React.FC<HeaderProps> = ({ title, onBack, showCart = true }) => {
) : ( ) : (
<div className="header-logo-container"> <div className="header-logo-container">
<img <img
src={ritLogo} src={tilloLogo}
alt="RIT Logo" alt="Tillo Logo"
className="header-logo" className="header-logo"
onClick={() => navigate('/')} onClick={() => navigate('/')}
/> />

View File

@@ -93,9 +93,43 @@ a {
} }
.loading-spinner { .loading-spinner {
padding: 20px; display: flex !important;
color: var(--primary); flex-direction: column !important;
font-weight: 600; align-items: center !important;
justify-content: center !important;
gap: 12px !important;
border: none !important;
border-radius: 0 !important;
animation: none !important;
width: auto !important;
height: auto !important;
padding: 20px !important;
color: var(--primary) !important;
font-weight: 600 !important;
}
.loading-spinner::before {
content: '' !important;
display: block !important;
width: 48px !important;
height: 48px !important;
background-image: url('./assets/tillo_spinner.gif') !important;
background-size: contain !important;
background-position: center !important;
background-repeat: no-repeat !important;
}
.loading-spinner-small {
width: 18px !important;
height: 18px !important;
border: none !important;
border-radius: 0 !important;
animation: none !important;
background-image: url('./assets/tillo_spinner.gif') !important;
background-size: contain !important;
background-position: center !important;
background-repeat: no-repeat !important;
display: inline-block !important;
} }
/* Global QR Overlay Styles */ /* Global QR Overlay Styles */
@@ -110,7 +144,8 @@ a {
} }
.qr-completed canvas, .qr-completed canvas,
.qr-expired canvas { .qr-expired canvas,
.qr-cancelled canvas {
filter: blur(8px) !important; filter: blur(8px) !important;
opacity: 0.6 !important; opacity: 0.6 !important;
} }
@@ -119,6 +154,10 @@ a {
filter: blur(12px) grayscale(100%) !important; filter: blur(12px) grayscale(100%) !important;
} }
.qr-cancelled canvas {
filter: blur(10px) !important;
}
.qr-overlay { .qr-overlay {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -150,6 +189,10 @@ a {
background: #374151 !important; background: #374151 !important;
} }
.overlay-text.cancelled {
background: #dc2626 !important;
}
.overlay-text.mini { .overlay-text.mini {
padding: 4px 8px; padding: 4px 8px;
font-size: 0.75rem; font-size: 0.75rem;
@@ -157,7 +200,8 @@ a {
} }
.latest-qr-wrapper.qr-completed canvas, .latest-qr-wrapper.qr-completed canvas,
.latest-qr-wrapper.qr-expired canvas { .latest-qr-wrapper.qr-expired canvas,
.latest-qr-wrapper.qr-cancelled canvas {
filter: blur(4px) !important; filter: blur(4px) !important;
} }

View File

@@ -51,6 +51,8 @@
.login-header-logo { .login-header-logo {
display: flex; display: flex;
flex-direction: column;
align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 24px; margin-bottom: 24px;
} }
@@ -269,6 +271,7 @@
opacity: 0; opacity: 0;
transform: scale(0.95); transform: scale(0.95);
} }
to { to {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
@@ -319,12 +322,27 @@
margin: 0 2px; margin: 0 2px;
} }
.loading-dots span:nth-child(2) { animation-delay: 0.2s; } .loading-dots span:nth-child(2) {
.loading-dots span:nth-child(3) { animation-delay: 0.4s; } animation-delay: 0.2s;
}
.loading-dots span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes loadingDots { @keyframes loadingDots {
0%, 80%, 100% { transform: translateY(0); opacity: 0.2; }
40% { transform: translateY(-4px); opacity: 1; } 0%,
80%,
100% {
transform: translateY(0);
opacity: 0.2;
}
40% {
transform: translateY(-4px);
opacity: 1;
}
} }
.error-message { .error-message {
@@ -373,3 +391,54 @@
border-radius: 24px; border-radius: 24px;
} }
} }
.tillo-logo {
height: 80px;
width: auto;
object-fit: contain;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.1));
}
.login-merchant-card {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 20px;
padding: 16px 20px;
border: 1px solid rgba(255, 255, 255, 0.5);
box-shadow:
0 10px 30px rgba(35, 22, 81, 0.05),
inset 0 0 0 1px rgba(255, 255, 255, 0.5);
gap: 16px;
}
.merchant-info {
display: flex;
flex-direction: column;
text-align: left;
}
.merchant-label {
font-size: 10px;
font-weight: 800;
color: var(--text-light);
text-transform: uppercase;
letter-spacing: 0.8px;
}
.merchant-name {
font-size: 13px;
font-weight: 700;
color: var(--text-dark);
margin-top: 2px;
}
.merchant-rit-logo {
height: 48px;
width: auto;
object-fit: contain;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.05));
}

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Phone, User as UserIcon, Lock, ChevronLeft } from 'lucide-react'; import { Phone, User as UserIcon, Lock, ChevronLeft } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import tilloLogo from '../assets/Tillo.png';
import ritLogo from '../assets/RIT_Logo.png'; import ritLogo from '../assets/RIT_Logo.png';
import './LoginScreen.css'; import './LoginScreen.css';
@@ -91,7 +92,7 @@ const LoginScreen: React.FC = () => {
<div className="login-content"> <div className="login-content">
<div className="login-card animate-slideUp"> <div className="login-card animate-slideUp">
<div className="login-header-logo flex flex-col items-center justify-center"> <div className="login-header-logo flex flex-col items-center justify-center">
<img src={ritLogo} alt="RIT Logo" className="rit-logo" /> <img src={tilloLogo} alt="Tillo Logo" className="tillo-logo" />
<span className="login-tagline">Till yo tummy is full...</span> <span className="login-tagline">Till yo tummy is full...</span>
</div> </div>
@@ -214,6 +215,13 @@ const LoginScreen: React.FC = () => {
)} )}
</div> </div>
<div className="login-merchant-card animate-slideUp">
<div className="merchant-info">
<span className="merchant-label">Merchant Partner</span>
</div>
<img src={ritLogo} alt="RIT Logo" className="merchant-rit-logo" />
</div>
<div className="login-footer-info"> <div className="login-footer-info">
<p>© 2026 Rajalakshmi Institute of Technology</p> <p>© 2026 Rajalakshmi Institute of Technology</p>
<p>Tillo Canteen Management System</p> <p>Tillo Canteen Management System</p>

View File

@@ -42,6 +42,8 @@ const MyOrdersScreen: React.FC = () => {
const [unavailableItems, setUnavailableItems] = useState<string[]>([]); const [unavailableItems, setUnavailableItems] = useState<string[]>([]);
const [pendingItems, setPendingItems] = useState<FoodItem[]>([]); const [pendingItems, setPendingItems] = useState<FoodItem[]>([]);
const [isCancelling, setIsCancelling] = useState(false);
const isOrderExpired = (order: Order) => { const isOrderExpired = (order: Order) => {
if (order.isArchived) return true; if (order.isArchived) return true;
const orderDate = new Date(order.createdAt).toDateString(); const orderDate = new Date(order.createdAt).toDateString();
@@ -126,6 +128,35 @@ const MyOrdersScreen: React.FC = () => {
} }
}; };
const handleCancelOrder = async (orderId: number) => {
if (!window.confirm('Are you sure you want to cancel this order?')) return;
setIsCancelling(true);
try {
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/${orderId}/cancel`, {
method: 'POST',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
const data = await response.json();
if (data.success) {
fetchOrders();
if (selectedOrder?.id === orderId) {
setSelectedOrder({ ...selectedOrder, status: 'CANCELLED' });
}
} else {
alert(data.message || 'Failed to cancel order');
}
} catch (error) {
console.error('Error cancelling order:', error);
alert('Error cancelling order');
} finally {
setIsCancelling(false);
}
};
const performRepeat = (items: FoodItem[]) => { const performRepeat = (items: FoodItem[]) => {
clearCart(); clearCart();
items.forEach(item => addToCart(item)); items.forEach(item => addToCart(item));
@@ -170,7 +201,7 @@ const MyOrdersScreen: React.FC = () => {
<div className="order-status-badge">{latestOrder.status}</div> <div className="order-status-badge">{latestOrder.status}</div>
</div> </div>
<div className={`latest-qr-wrapper ${latestOrder.status.toUpperCase() === 'COMPLETED' ? 'qr-completed' : ''} ${isOrderExpired(latestOrder) ? 'qr-expired' : ''}`}> <div className={`latest-qr-wrapper ${latestOrder.status.toUpperCase() === 'COMPLETED' ? 'qr-completed' : ''} ${latestOrder.status.toUpperCase() === 'CANCELLED' ? 'qr-cancelled' : ''} ${isOrderExpired(latestOrder) ? 'qr-expired' : ''}`}>
<div className="qr-container"> <div className="qr-container">
<QRCodeCanvas value={latestOrder.orderNumber} size={120} className="mini-qr" /> <QRCodeCanvas value={latestOrder.orderNumber} size={120} className="mini-qr" />
{latestOrder.status.toUpperCase() === 'COMPLETED' && ( {latestOrder.status.toUpperCase() === 'COMPLETED' && (
@@ -178,6 +209,11 @@ const MyOrdersScreen: React.FC = () => {
<span className="overlay-text mini">COMPLETED</span> <span className="overlay-text mini">COMPLETED</span>
</div> </div>
)} )}
{latestOrder.status.toUpperCase() === 'CANCELLED' && (
<div className="qr-overlay mini">
<span className="overlay-text mini cancelled">CANCELLED</span>
</div>
)}
{isOrderExpired(latestOrder) && ( {isOrderExpired(latestOrder) && (
<div className="qr-overlay mini"> <div className="qr-overlay mini">
<span className="overlay-text mini expired">EXPIRED</span> <span className="overlay-text mini expired">EXPIRED</span>
@@ -185,7 +221,7 @@ const MyOrdersScreen: React.FC = () => {
)} )}
</div> </div>
<div className="qr-hint-text"> <div className="qr-hint-text">
{isOrderExpired(latestOrder) ? 'QR Expired' : (latestOrder.status.toUpperCase() === 'COMPLETED' ? 'Order Fulfilled' : 'Tap to enlarge QR')} {isOrderExpired(latestOrder) ? 'QR Expired' : (latestOrder.status.toUpperCase() === 'COMPLETED' ? 'Order Fulfilled' : latestOrder.status.toUpperCase() === 'CANCELLED' ? 'Order Cancelled' : 'Tap to enlarge QR')}
</div> </div>
</div> </div>
@@ -200,15 +236,31 @@ const MyOrdersScreen: React.FC = () => {
<span className="amount-text">R{latestOrder.totalAmount.toFixed(2)}</span> <span className="amount-text">R{latestOrder.totalAmount.toFixed(2)}</span>
</div> </div>
<div style={{ display: 'flex', gap: '8px' }}>
<button <button
className="repeat-order-btn mini" className="repeat-order-btn mini"
style={{ flex: 1 }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleRepeatOrder(latestOrder); handleRepeatOrder(latestOrder);
}} }}
> >
<RefreshCcw size={16} /> Repeat Order <RefreshCcw size={16} /> Repeat
</button> </button>
{latestOrder.status.toUpperCase() !== 'COMPLETED' && latestOrder.status.toUpperCase() !== 'CANCELLED' && !isOrderExpired(latestOrder) && (
<button
className="repeat-order-btn mini"
style={{ flex: 1, backgroundColor: '#fee2e2', color: '#dc2626' }}
disabled={isCancelling}
onClick={(e) => {
e.stopPropagation();
handleCancelOrder(latestOrder.id);
}}
>
<X size={16} /> Cancel
</button>
)}
</div>
</div> </div>
</section> </section>
)} )}
@@ -270,7 +322,7 @@ const MyOrdersScreen: React.FC = () => {
<p className="modal-order-id">Order #{selectedOrder.displayOrderId}</p> <p className="modal-order-id">Order #{selectedOrder.displayOrderId}</p>
</div> </div>
<div className={`modal-qr-section ${selectedOrder.status.toUpperCase() === 'COMPLETED' ? 'qr-completed' : ''} ${isOrderExpired(selectedOrder) ? 'qr-expired' : ''}`}> <div className={`modal-qr-section ${selectedOrder.status.toUpperCase() === 'COMPLETED' ? 'qr-completed' : ''} ${selectedOrder.status.toUpperCase() === 'CANCELLED' ? 'qr-cancelled' : ''} ${isOrderExpired(selectedOrder) ? 'qr-expired' : ''}`}>
<div className="qr-container"> <div className="qr-container">
<QRCodeCanvas value={selectedOrder.orderNumber} size={200} includeMargin={true} /> <QRCodeCanvas value={selectedOrder.orderNumber} size={200} includeMargin={true} />
{selectedOrder.status.toUpperCase() === 'COMPLETED' && ( {selectedOrder.status.toUpperCase() === 'COMPLETED' && (
@@ -278,6 +330,11 @@ const MyOrdersScreen: React.FC = () => {
<span className="overlay-text">COMPLETED</span> <span className="overlay-text">COMPLETED</span>
</div> </div>
)} )}
{selectedOrder.status.toUpperCase() === 'CANCELLED' && (
<div className="qr-overlay">
<span className="overlay-text cancelled">CANCELLED</span>
</div>
)}
{isOrderExpired(selectedOrder) && ( {isOrderExpired(selectedOrder) && (
<div className="qr-overlay"> <div className="qr-overlay">
<span className="overlay-text expired">EXPIRED</span> <span className="overlay-text expired">EXPIRED</span>
@@ -289,6 +346,8 @@ const MyOrdersScreen: React.FC = () => {
? 'QR Expired' ? 'QR Expired'
: selectedOrder.status.toUpperCase() === 'COMPLETED' : selectedOrder.status.toUpperCase() === 'COMPLETED'
? 'This order has been fulfilled' ? 'This order has been fulfilled'
: selectedOrder.status.toUpperCase() === 'CANCELLED'
? 'This order has been cancelled'
: 'Show this QR code at the counter'} : 'Show this QR code at the counter'}
</p> </p>
</div> </div>
@@ -323,9 +382,21 @@ const MyOrdersScreen: React.FC = () => {
</div> </div>
</div> </div>
<button className="repeat-order-btn" onClick={() => handleRepeatOrder(selectedOrder)}> <div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
<RefreshCcw size={18} /> Repeat this order <button className="repeat-order-btn" style={{ flex: 1, marginTop: 0 }} onClick={() => handleRepeatOrder(selectedOrder)}>
<RefreshCcw size={18} /> Repeat
</button> </button>
{selectedOrder.status.toUpperCase() !== 'COMPLETED' && selectedOrder.status.toUpperCase() !== 'CANCELLED' && !isOrderExpired(selectedOrder) && (
<button
className="repeat-order-btn"
style={{ flex: 1, marginTop: 0, backgroundColor: '#fee2e2', color: '#dc2626' }}
disabled={isCancelling}
onClick={() => handleCancelOrder(selectedOrder.id)}
>
<X size={18} /> Cancel
</button>
)}
</div>
</motion.div> </motion.div>
</motion.div> </motion.div>
)} )}

View File

@@ -23,7 +23,7 @@ const SuccessScreen: React.FC = () => {
<h1 className="success-title">Order Placed!</h1> <h1 className="success-title">Order Placed!</h1>
<p className="success-order-id">Order ID #{displayOrderId}</p> <p className="success-order-id">Order ID #{displayOrderId}</p>
<div className={`qr-container ${status === 'COMPLETED' ? 'qr-completed' : ''} ${archived ? 'qr-expired' : ''}`}> <div className={`qr-container ${status === 'COMPLETED' ? 'qr-completed' : ''} ${status === 'CANCELLED' ? 'qr-cancelled' : ''} ${archived ? 'qr-expired' : ''}`}>
<div className="qr-wrapper-inner"> <div className="qr-wrapper-inner">
<QRCodeCanvas <QRCodeCanvas
value={orderNumber} value={orderNumber}
@@ -37,6 +37,11 @@ const SuccessScreen: React.FC = () => {
<span className="overlay-text">COMPLETED</span> <span className="overlay-text">COMPLETED</span>
</div> </div>
)} )}
{status === 'CANCELLED' && (
<div className="qr-overlay">
<span className="overlay-text cancelled">CANCELLED</span>
</div>
)}
{archived && ( {archived && (
<div className="qr-overlay"> <div className="qr-overlay">
<span className="overlay-text expired">EXPIRED</span> <span className="overlay-text expired">EXPIRED</span>
@@ -48,6 +53,8 @@ const SuccessScreen: React.FC = () => {
? 'This order has expired' ? 'This order has expired'
: status === 'COMPLETED' : status === 'COMPLETED'
? 'Order has been fulfilled' ? 'Order has been fulfilled'
: status === 'CANCELLED'
? 'Order has been cancelled'
: 'Scan this QR at the counter to collect your food'} : 'Scan this QR at the counter to collect your food'}
</p> </p>
</div> </div>

BIN
tillo_spinner.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB