diff --git a/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java b/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java index 37da4405..637d631d 100644 --- a/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java +++ b/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java @@ -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 ── diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java index d4fde9a6..8745f63b 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java @@ -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") diff --git a/counter-frontend/src/index.css b/counter-frontend/src/index.css index 64e7557c..0f7ad15b 100644 --- a/counter-frontend/src/index.css +++ b/counter-frontend/src/index.css @@ -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; +} diff --git a/counter-frontend/src/pages/Orders.tsx b/counter-frontend/src/pages/Orders.tsx index 04b42667..ef0bf048 100644 --- a/counter-frontend/src/pages/Orders.tsx +++ b/counter-frontend/src/pages/Orders.tsx @@ -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 = () => { > Mark Undelivered + {selectedOrder.status.toUpperCase() !== 'CANCELLED' && selectedOrder.status.toUpperCase() !== 'COMPLETED' && ( + + )} )} diff --git a/frontend/src/assets/tillo_spinner.gif b/frontend/src/assets/tillo_spinner.gif new file mode 100644 index 00000000..df79042d Binary files /dev/null and b/frontend/src/assets/tillo_spinner.gif differ diff --git a/frontend/src/index.css b/frontend/src/index.css index 53fce46f..4316f8f8 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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; +} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index fb8fcee8..87567840 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -221,7 +221,6 @@ const Login = () => { >
Merchant Partner - Rajalakshmi Institute of Technology
RIT Logo diff --git a/frontend/src/pages/Orders.tsx b/frontend/src/pages/Orders.tsx index a07be2eb..7a19bd90 100644 --- a/frontend/src/pages/Orders.tsx +++ b/frontend/src/pages/Orders.tsx @@ -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 = () => { > Mark Undelivered + {selectedOrder.status.toUpperCase() !== 'CANCELLED' && selectedOrder.status.toUpperCase() !== 'COMPLETED' && ( + + )} )} diff --git a/ordering_site/src/components/FeedbackModal.tsx b/ordering_site/src/components/FeedbackModal.tsx index 2d7884eb..631f2ef6 100644 --- a/ordering_site/src/components/FeedbackModal.tsx +++ b/ordering_site/src/components/FeedbackModal.tsx @@ -82,7 +82,7 @@ const FeedbackModal: React.FC = ({ order, userName, userId, >

Rate Your Meal

-

How was your experience with Order #{order.orderNumber.split('-')[1]}?

+

How was your experience?

diff --git a/ordering_site/src/components/Header.tsx b/ordering_site/src/components/Header.tsx index 34d53f2f..cd357efa 100644 --- a/ordering_site/src/components/Header.tsx +++ b/ordering_site/src/components/Header.tsx @@ -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 = ({ title, onBack, showCart = true }) => { ) : (
RIT Logo navigate('/')} /> diff --git a/ordering_site/src/index.css b/ordering_site/src/index.css index 3cf17f28..d5b85d70 100644 --- a/ordering_site/src/index.css +++ b/ordering_site/src/index.css @@ -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; } diff --git a/ordering_site/src/pages/LoginScreen.css b/ordering_site/src/pages/LoginScreen.css index 9ab0a2e1..a3e0f026 100644 --- a/ordering_site/src/pages/LoginScreen.css +++ b/ordering_site/src/pages/LoginScreen.css @@ -18,7 +18,7 @@ height: 100%; z-index: 0; background-color: #ffffff; - background-image: + background-image: radial-gradient(at 0% 0%, rgba(35, 22, 81, 0.15) 0px, transparent 50%), radial-gradient(at 100% 0%, rgba(0, 208, 148, 0.1) 0px, transparent 50%), radial-gradient(at 100% 100%, rgba(35, 22, 81, 0.1) 0px, transparent 50%), @@ -44,13 +44,15 @@ border-radius: 32px; padding: 40px 32px; border: 1px solid rgba(255, 255, 255, 0.5); - box-shadow: + box-shadow: 0 20px 50px rgba(35, 22, 81, 0.1), inset 0 0 0 1px rgba(255, 255, 255, 0.5); } .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)); +} \ No newline at end of file diff --git a/ordering_site/src/pages/LoginScreen.tsx b/ordering_site/src/pages/LoginScreen.tsx index a9f190b1..db7be5e4 100644 --- a/ordering_site/src/pages/LoginScreen.tsx +++ b/ordering_site/src/pages/LoginScreen.tsx @@ -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 = () => {
- RIT Logo + Tillo Logo Till yo tummy is full...
@@ -214,6 +215,13 @@ const LoginScreen: React.FC = () => { )}
+
+
+ Merchant Partner +
+ RIT Logo +
+

© 2026 Rajalakshmi Institute of Technology

Tillo Canteen Management System

diff --git a/ordering_site/src/pages/MyOrdersScreen.tsx b/ordering_site/src/pages/MyOrdersScreen.tsx index c8082db9..2a5689a8 100644 --- a/ordering_site/src/pages/MyOrdersScreen.tsx +++ b/ordering_site/src/pages/MyOrdersScreen.tsx @@ -41,6 +41,8 @@ const MyOrdersScreen: React.FC = () => { const [showStockAlert, setShowStockAlert] = useState(false); const [unavailableItems, setUnavailableItems] = useState([]); const [pendingItems, setPendingItems] = useState([]); + + const [isCancelling, setIsCancelling] = useState(false); const isOrderExpired = (order: Order) => { if (order.isArchived) return true; @@ -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 = () => {
{latestOrder.status}
-
+
{latestOrder.status.toUpperCase() === 'COMPLETED' && ( @@ -178,6 +209,11 @@ const MyOrdersScreen: React.FC = () => { COMPLETED
)} + {latestOrder.status.toUpperCase() === 'CANCELLED' && ( +
+ CANCELLED +
+ )} {isOrderExpired(latestOrder) && (
EXPIRED @@ -185,7 +221,7 @@ const MyOrdersScreen: React.FC = () => { )}
- {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')}
@@ -200,15 +236,31 @@ const MyOrdersScreen: React.FC = () => { R{latestOrder.totalAmount.toFixed(2)}
- +
+ + {latestOrder.status.toUpperCase() !== 'COMPLETED' && latestOrder.status.toUpperCase() !== 'CANCELLED' && !isOrderExpired(latestOrder) && ( + + )} +
)} @@ -270,7 +322,7 @@ const MyOrdersScreen: React.FC = () => {

Order #{selectedOrder.displayOrderId}

-
+
{selectedOrder.status.toUpperCase() === 'COMPLETED' && ( @@ -278,6 +330,11 @@ const MyOrdersScreen: React.FC = () => { COMPLETED
)} + {selectedOrder.status.toUpperCase() === 'CANCELLED' && ( +
+ CANCELLED +
+ )} {isOrderExpired(selectedOrder) && (
EXPIRED @@ -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'}

@@ -323,9 +382,21 @@ const MyOrdersScreen: React.FC = () => {
- +
+ + {selectedOrder.status.toUpperCase() !== 'COMPLETED' && selectedOrder.status.toUpperCase() !== 'CANCELLED' && !isOrderExpired(selectedOrder) && ( + + )} +
)} diff --git a/ordering_site/src/pages/SuccessScreen.tsx b/ordering_site/src/pages/SuccessScreen.tsx index 56497fdc..35815834 100644 --- a/ordering_site/src/pages/SuccessScreen.tsx +++ b/ordering_site/src/pages/SuccessScreen.tsx @@ -23,7 +23,7 @@ const SuccessScreen: React.FC = () => {

Order Placed!

Order ID #{displayOrderId}

-
+
{ COMPLETED
)} + {status === 'CANCELLED' && ( +
+ CANCELLED +
+ )} {archived && (
EXPIRED @@ -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'}

diff --git a/tillo_spinner.gif b/tillo_spinner.gif new file mode 100644 index 00000000..df79042d Binary files /dev/null and b/tillo_spinner.gif differ