Refund added
This commit is contained in:
@@ -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 ──
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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;
|
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">
|
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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('/')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
BIN
tillo_spinner.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 MiB |
Reference in New Issue
Block a user