diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java b/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java index 7abf22ca..81cb3200 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java @@ -78,6 +78,16 @@ public class TerminalController { return ResponseEntity.noContent().build(); } + @PutMapping("/{id}") + public ResponseEntity updateTerminal(@PathVariable Long id, @jakarta.validation.Valid @RequestBody Terminal terminalDetails) { + Terminal updated = terminalService.updateTerminal(id, terminalDetails); + if (updated != null) { + return ResponseEntity.ok(updated); + } else { + return ResponseEntity.notFound().build(); + } + } + // ────────────────────────────────────────────────────────────── // Order Lookup (ESP32 uses X-API-KEY) // ────────────────────────────────────────────────────────────── diff --git a/backend/src/main/java/com/rit/canteen/sales/service/TerminalService.java b/backend/src/main/java/com/rit/canteen/sales/service/TerminalService.java index 6316de65..445d2b37 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/TerminalService.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/TerminalService.java @@ -43,4 +43,16 @@ public class TerminalService { terminalRepository.deleteById(id); } } + + public Terminal updateTerminal(Long id, Terminal details) { + if (id == null) return null; + return terminalRepository.findById(id).map(existing -> { + existing.setName(details.getName()); + existing.setLocation(details.getLocation()); + if (details.getPin() != null && !details.getPin().trim().isEmpty()) { + existing.setPin(details.getPin().trim()); + } + return terminalRepository.save(existing); + }).orElse(null); + } } diff --git a/frontend/src/components/EditTerminalModal.tsx b/frontend/src/components/EditTerminalModal.tsx new file mode 100644 index 00000000..baaec2a3 --- /dev/null +++ b/frontend/src/components/EditTerminalModal.tsx @@ -0,0 +1,187 @@ +import { apiFetch } from '../api'; +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Monitor, MapPin, Lock, Pencil } from 'lucide-react'; +import Numpad from './Numpad.tsx'; + +interface EditTerminalModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; + terminalId: number | null; + initialName: string; + initialLocation: string; +} + +const EditTerminalModal: React.FC = ({ + isOpen, onClose, onSuccess, terminalId, initialName, initialLocation +}) => { + const [name, setName] = useState(initialName); + const [location, setLocation] = useState(initialLocation); + const [pin, setPin] = useState(''); + const [changePin, setChangePin] = useState(false); + const [step, setStep] = useState(1); // 1: Details, 2: PIN (optional) + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (isOpen) { + setName(initialName); + setLocation(initialLocation); + setPin(''); + setChangePin(false); + setStep(1); + } + }, [isOpen, initialName, initialLocation]); + + const handleSubmit = async () => { + if (!terminalId) return; + setLoading(true); + try { + const body: Record = { name, location }; + if (changePin && pin.length === 4) { + body.pin = pin; + } + const response = await apiFetch(`/api/terminals/${terminalId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (response.ok) { + onSuccess(); + onClose(); + } + } catch (error) { + console.error('Failed to update terminal:', error); + } finally { + setLoading(false); + } + }; + + if (!isOpen) return null; + + return ( +
+ +
+

Edit Terminal

+ +
+ +
+ + {step === 1 ? ( + +
+ + setName(e.target.value)} + placeholder="e.g. Counter 1" + className="w-full h-12 px-4 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#001828]/20 focus:border-[#001828] outline-none transition-all" + /> +
+ +
+ + setLocation(e.target.value)} + placeholder="e.g. Main Entrance" + className="w-full h-12 px-4 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#001828]/20 focus:border-[#001828] outline-none transition-all" + /> +
+ + {/* Change PIN toggle */} + + + {changePin ? ( + + ) : ( + + )} +
+ ) : ( + +
+
+ +
+

New Security PIN

+

Enter a new 4-digit security PIN

+
+ + + +
+ + +
+
+ )} +
+
+
+
+ ); +}; + +export default EditTerminalModal; diff --git a/frontend/src/pages/Terminals.tsx b/frontend/src/pages/Terminals.tsx index ac1a6fb7..d03a9c56 100644 --- a/frontend/src/pages/Terminals.tsx +++ b/frontend/src/pages/Terminals.tsx @@ -7,6 +7,7 @@ import { MapPin, ShieldCheck, Trash2, + Pencil, Search, ExternalLink, Info, @@ -20,6 +21,7 @@ import { import AddTerminalModal from '../components/AddTerminalModal.tsx'; import PinVerificationModal from '../components/PinVerificationModal.tsx'; import LinkDeviceModal from '../components/LinkDeviceModal.tsx'; +import EditTerminalModal from '../components/EditTerminalModal.tsx'; interface Terminal { id: number; @@ -38,6 +40,7 @@ const Terminals = () => { const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isPinModalOpen, setIsPinModalOpen] = useState(false); const [isLinkModalOpen, setIsLinkModalOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [selectedTerminal, setSelectedTerminal] = useState(null); const fetchTerminals = async () => { @@ -59,26 +62,36 @@ const Terminals = () => { fetchTerminals(); }, []); - const handleDelete = async (id: number, e: React.MouseEvent) => { - e.stopPropagation(); + const handleDelete = async (id: number) => { if (window.confirm('Are you sure you want to remove this terminal?')) { try { const response = await apiFetch(`/api/terminals/${id}`, { method: 'DELETE' }); - if (response.ok) fetchTerminals(); + if (response.ok) { + fetchTerminals(); + } else { + const data = await response.json().catch(() => ({})); + alert(data.message || `Failed to delete terminal (HTTP ${response.status})`); + } } catch (error) { console.error('Failed to delete terminal:', error); + alert('Network error: Could not delete terminal'); } } }; - const handleUnpair = async (id: number, e: React.MouseEvent) => { - e.stopPropagation(); + const handleUnpair = async (id: number) => { if (window.confirm('Unpair this device? It will need to be re-paired on next boot.')) { try { const response = await apiFetch(`/api/terminals/${id}/unpair`, { method: 'POST' }); - if (response.ok) fetchTerminals(); + if (response.ok) { + fetchTerminals(); + } else { + const data = await response.json().catch(() => ({})); + alert(data.message || `Failed to unpair device (HTTP ${response.status})`); + } } catch (error) { console.error('Failed to unpair device:', error); + alert('Network error: Could not unpair device'); } } }; @@ -173,14 +186,10 @@ const Terminals = () => { layout key={terminal.id} whileHover={{ y: -5 }} - onClick={() => { - setSelectedTerminal(terminal); - setIsPinModalOpen(true); - }} className="bg-white p-6 rounded-[2.5rem] border border-gray-100 shadow-sm hover:shadow-xl hover:shadow-[#001828]/5 transition-all cursor-pointer group relative overflow-hidden" > {/* Card Background Pattern */} -
+
@@ -193,10 +202,12 @@ const Terminals = () => { }`}>
-
+ {/* Action buttons — completely separate from card click */} +
{terminal.paired && ( )} +
-
-

{terminal.name}

-
- - {terminal.location} + {/* Clickable area for viewing API Key (opens PIN modal) */} +
{ + setSelectedTerminal(terminal); + setIsPinModalOpen(true); + }} + onKeyDown={(e) => { if (e.key === 'Enter') { setSelectedTerminal(terminal); setIsPinModalOpen(true); }}} + > +
+

{terminal.name}

+
+ + {terminal.location} +
-
- {/* Device ID for paired terminals */} - {terminal.paired && terminal.deviceId && ( -
- - {terminal.deviceId} -
- )} + {/* Device ID for paired terminals */} + {terminal.paired && terminal.deviceId && ( +
+ + {terminal.deviceId} +
+ )} -
-
+
+
+ {terminal.paired ? ( + <> +
+ Paired + + ) : ( + <> +
+ Unpaired + + )} +
+ {terminal.paired ? ( - <> -
- Paired - +
+ + View API Key + +
) : ( - <> -
- Unpaired - + )}
- - {terminal.paired ? ( -
- - View API Key - -
- ) : ( - - )}
@@ -311,6 +344,18 @@ const Terminals = () => { terminalName={selectedTerminal?.name || ''} /> + { + setIsEditModalOpen(false); + setSelectedTerminal(null); + }} + onSuccess={fetchTerminals} + terminalId={selectedTerminal?.id || null} + initialName={selectedTerminal?.name || ''} + initialLocation={selectedTerminal?.location || ''} + /> +