Refund added
This commit is contained in:
@@ -81,6 +81,7 @@ public class SecurityConfig {
|
||||
.requestMatchers(HttpMethod.POST, "/api/auth/change-pin").authenticated()
|
||||
.requestMatchers(HttpMethod.PUT, "/api/auth/users/*").authenticated()
|
||||
.requestMatchers(HttpMethod.PUT, "/api/orders/*").authenticated()
|
||||
.requestMatchers(HttpMethod.POST, "/api/orders/*/cancel").authenticated()
|
||||
.requestMatchers("/api/counter/**").hasAnyRole("MASTER", "MANAGER", "STAFF")
|
||||
|
||||
// ── STAFF/MANAGER/MASTER: All other management APIs ──
|
||||
|
||||
@@ -224,6 +224,35 @@ public class OrderController {
|
||||
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 ────────────────────────────────────
|
||||
|
||||
@PatchMapping("/{id}/status")
|
||||
|
||||
@@ -6,3 +6,18 @@
|
||||
--color-background: #f4fbf7;
|
||||
--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;
|
||||
}
|
||||
|
||||
@@ -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 = () => {
|
||||
if (!selectedOrder) return;
|
||||
setEditingItems([...selectedOrder.items]);
|
||||
@@ -620,6 +637,14 @@ const Orders: React.FC = () => {
|
||||
>
|
||||
<RotateCcw size={14} className="text-amber-500" /> Mark Undelivered
|
||||
</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>
|
||||
|
||||
BIN
frontend/src/assets/tillo_spinner.gif
Normal file
BIN
frontend/src/assets/tillo_spinner.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 MiB |
@@ -173,3 +173,18 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -221,7 +221,6 @@ const Login = () => {
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
<img src={ritLogo} alt="RIT Logo" className="h-12 w-auto object-contain filter drop-shadow-sm" />
|
||||
</motion.div>
|
||||
|
||||
@@ -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 = () => {
|
||||
if (!selectedOrder) return;
|
||||
setEditingItems([...selectedOrder.items]);
|
||||
@@ -604,6 +621,14 @@ const Orders: React.FC = () => {
|
||||
>
|
||||
<RotateCcw size={14} className="text-amber-500" /> Mark Undelivered
|
||||
</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>
|
||||
|
||||
@@ -82,7 +82,7 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({ order, userName, userId,
|
||||
>
|
||||
<div className="feedback-header">
|
||||
<h2>Rate Your Meal</h2>
|
||||
<p>How was your experience with Order #{order.orderNumber.split('-')[1]}?</p>
|
||||
<p>How was your experience?</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="feedback-body">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Bell, User, ShoppingBag, LogOut } from 'lucide-react';
|
||||
import { useCart } from '../contexts/CartContext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import ritLogo from '../assets/RIT_Logo.png';
|
||||
import tilloLogo from '../assets/Tillo.png';
|
||||
import './Header.css';
|
||||
|
||||
interface HeaderProps {
|
||||
@@ -33,8 +33,8 @@ const Header: React.FC<HeaderProps> = ({ title, onBack, showCart = true }) => {
|
||||
) : (
|
||||
<div className="header-logo-container">
|
||||
<img
|
||||
src={ritLogo}
|
||||
alt="RIT Logo"
|
||||
src={tilloLogo}
|
||||
alt="Tillo Logo"
|
||||
className="header-logo"
|
||||
onClick={() => navigate('/')}
|
||||
/>
|
||||
|
||||
@@ -93,9 +93,43 @@ a {
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
padding: 20px;
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
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 */
|
||||
@@ -110,7 +144,8 @@ a {
|
||||
}
|
||||
|
||||
.qr-completed canvas,
|
||||
.qr-expired canvas {
|
||||
.qr-expired canvas,
|
||||
.qr-cancelled canvas {
|
||||
filter: blur(8px) !important;
|
||||
opacity: 0.6 !important;
|
||||
}
|
||||
@@ -119,6 +154,10 @@ a {
|
||||
filter: blur(12px) grayscale(100%) !important;
|
||||
}
|
||||
|
||||
.qr-cancelled canvas {
|
||||
filter: blur(10px) !important;
|
||||
}
|
||||
|
||||
.qr-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -150,6 +189,10 @@ a {
|
||||
background: #374151 !important;
|
||||
}
|
||||
|
||||
.overlay-text.cancelled {
|
||||
background: #dc2626 !important;
|
||||
}
|
||||
|
||||
.overlay-text.mini {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
@@ -157,7 +200,8 @@ a {
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
|
||||
.login-header-logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@@ -157,7 +159,7 @@
|
||||
box-shadow: 0 0 0 4px rgba(35, 22, 81, 0.05);
|
||||
}
|
||||
|
||||
.login-form input:focus + .input-icon {
|
||||
.login-form input:focus+.input-icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
@@ -269,6 +271,7 @@
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
@@ -319,12 +322,27 @@
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
|
||||
.loading-dots span:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@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 {
|
||||
@@ -373,3 +391,54 @@
|
||||
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));
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Phone, User as UserIcon, Lock, ChevronLeft } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import tilloLogo from '../assets/Tillo.png';
|
||||
import ritLogo from '../assets/RIT_Logo.png';
|
||||
import './LoginScreen.css';
|
||||
|
||||
@@ -91,7 +92,7 @@ const LoginScreen: React.FC = () => {
|
||||
<div className="login-content">
|
||||
<div className="login-card animate-slideUp">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -214,6 +215,13 @@ const LoginScreen: React.FC = () => {
|
||||
)}
|
||||
</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">
|
||||
<p>© 2026 Rajalakshmi Institute of Technology</p>
|
||||
<p>Tillo Canteen Management System</p>
|
||||
|
||||
@@ -42,6 +42,8 @@ const MyOrdersScreen: React.FC = () => {
|
||||
const [unavailableItems, setUnavailableItems] = useState<string[]>([]);
|
||||
const [pendingItems, setPendingItems] = useState<FoodItem[]>([]);
|
||||
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
|
||||
const isOrderExpired = (order: Order) => {
|
||||
if (order.isArchived) return true;
|
||||
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[]) => {
|
||||
clearCart();
|
||||
items.forEach(item => addToCart(item));
|
||||
@@ -170,7 +201,7 @@ const MyOrdersScreen: React.FC = () => {
|
||||
<div className="order-status-badge">{latestOrder.status}</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">
|
||||
<QRCodeCanvas value={latestOrder.orderNumber} size={120} className="mini-qr" />
|
||||
{latestOrder.status.toUpperCase() === 'COMPLETED' && (
|
||||
@@ -178,6 +209,11 @@ const MyOrdersScreen: React.FC = () => {
|
||||
<span className="overlay-text mini">COMPLETED</span>
|
||||
</div>
|
||||
)}
|
||||
{latestOrder.status.toUpperCase() === 'CANCELLED' && (
|
||||
<div className="qr-overlay mini">
|
||||
<span className="overlay-text mini cancelled">CANCELLED</span>
|
||||
</div>
|
||||
)}
|
||||
{isOrderExpired(latestOrder) && (
|
||||
<div className="qr-overlay mini">
|
||||
<span className="overlay-text mini expired">EXPIRED</span>
|
||||
@@ -185,7 +221,7 @@ const MyOrdersScreen: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -200,15 +236,31 @@ const MyOrdersScreen: React.FC = () => {
|
||||
<span className="amount-text">R{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 style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
className="repeat-order-btn mini"
|
||||
style={{ flex: 1 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRepeatOrder(latestOrder);
|
||||
}}
|
||||
>
|
||||
<RefreshCcw size={16} /> Repeat
|
||||
</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>
|
||||
</section>
|
||||
)}
|
||||
@@ -270,7 +322,7 @@ const MyOrdersScreen: React.FC = () => {
|
||||
<p className="modal-order-id">Order #{selectedOrder.displayOrderId}</p>
|
||||
</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">
|
||||
<QRCodeCanvas value={selectedOrder.orderNumber} size={200} includeMargin={true} />
|
||||
{selectedOrder.status.toUpperCase() === 'COMPLETED' && (
|
||||
@@ -278,6 +330,11 @@ const MyOrdersScreen: React.FC = () => {
|
||||
<span className="overlay-text">COMPLETED</span>
|
||||
</div>
|
||||
)}
|
||||
{selectedOrder.status.toUpperCase() === 'CANCELLED' && (
|
||||
<div className="qr-overlay">
|
||||
<span className="overlay-text cancelled">CANCELLED</span>
|
||||
</div>
|
||||
)}
|
||||
{isOrderExpired(selectedOrder) && (
|
||||
<div className="qr-overlay">
|
||||
<span className="overlay-text expired">EXPIRED</span>
|
||||
@@ -289,7 +346,9 @@ const MyOrdersScreen: React.FC = () => {
|
||||
? 'QR Expired'
|
||||
: selectedOrder.status.toUpperCase() === 'COMPLETED'
|
||||
? 'This order has been fulfilled'
|
||||
: 'Show this QR code at the counter'}
|
||||
: selectedOrder.status.toUpperCase() === 'CANCELLED'
|
||||
? 'This order has been cancelled'
|
||||
: 'Show this QR code at the counter'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="modal-info-list">
|
||||
@@ -323,9 +382,21 @@ const MyOrdersScreen: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="repeat-order-btn" onClick={() => handleRepeatOrder(selectedOrder)}>
|
||||
<RefreshCcw size={18} /> Repeat this order
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
|
||||
<button className="repeat-order-btn" style={{ flex: 1, marginTop: 0 }} onClick={() => handleRepeatOrder(selectedOrder)}>
|
||||
<RefreshCcw size={18} /> Repeat
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,7 @@ const SuccessScreen: React.FC = () => {
|
||||
<h1 className="success-title">Order Placed!</h1>
|
||||
<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">
|
||||
<QRCodeCanvas
|
||||
value={orderNumber}
|
||||
@@ -37,6 +37,11 @@ const SuccessScreen: React.FC = () => {
|
||||
<span className="overlay-text">COMPLETED</span>
|
||||
</div>
|
||||
)}
|
||||
{status === 'CANCELLED' && (
|
||||
<div className="qr-overlay">
|
||||
<span className="overlay-text cancelled">CANCELLED</span>
|
||||
</div>
|
||||
)}
|
||||
{archived && (
|
||||
<div className="qr-overlay">
|
||||
<span className="overlay-text expired">EXPIRED</span>
|
||||
@@ -48,7 +53,9 @@ const SuccessScreen: React.FC = () => {
|
||||
? 'This order has expired'
|
||||
: status === 'COMPLETED'
|
||||
? 'Order has been fulfilled'
|
||||
: 'Scan this QR at the counter to collect your food'}
|
||||
: status === 'CANCELLED'
|
||||
? 'Order has been cancelled'
|
||||
: 'Scan this QR at the counter to collect your food'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
BIN
tillo_spinner.gif
Normal file
BIN
tillo_spinner.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 MiB |
Reference in New Issue
Block a user