Bill Bot 1
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { apiFetch } from '../api';
|
||||
import { apiFetch } from '../api';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
@@ -10,16 +10,25 @@ import {
|
||||
Search,
|
||||
ExternalLink,
|
||||
Info,
|
||||
Key
|
||||
Key,
|
||||
Wifi,
|
||||
WifiOff,
|
||||
LinkIcon,
|
||||
Unlink,
|
||||
Smartphone
|
||||
} from 'lucide-react';
|
||||
import AddTerminalModal from '../components/AddTerminalModal.tsx';
|
||||
import PinVerificationModal from '../components/PinVerificationModal.tsx';
|
||||
import LinkDeviceModal from '../components/LinkDeviceModal.tsx';
|
||||
|
||||
interface Terminal {
|
||||
id: number;
|
||||
name: string;
|
||||
location: string;
|
||||
apiKey: string;
|
||||
paired: boolean;
|
||||
deviceId: string | null;
|
||||
pairedAt: string | null;
|
||||
}
|
||||
|
||||
const Terminals = () => {
|
||||
@@ -28,6 +37,7 @@ const Terminals = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
|
||||
const [selectedTerminal, setSelectedTerminal] = useState<Terminal | null>(null);
|
||||
|
||||
const fetchTerminals = async () => {
|
||||
@@ -61,11 +71,26 @@ const Terminals = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnpair = async (id: number, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
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();
|
||||
} catch (error) {
|
||||
console.error('Failed to unpair device:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const pairedCount = terminals.filter(t => t.paired).length;
|
||||
|
||||
const filteredTerminals = terminals.filter(t => {
|
||||
const query = (searchQuery || '').toLowerCase();
|
||||
return (t.name || '').toLowerCase().includes(query) ||
|
||||
(t.location || '').toLowerCase().includes(query);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto space-y-8 font-inter">
|
||||
{/* Header Section */}
|
||||
@@ -94,26 +119,28 @@ const Terminals = () => {
|
||||
<Monitor size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-sm font-medium">Active Devices</p>
|
||||
<p className="text-white/60 text-sm font-medium">Registered Devices</p>
|
||||
<p className="text-2xl font-bold">{terminals.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-3xl border border-gray-100 shadow-sm flex items-center gap-4">
|
||||
<div className="p-3 bg-green-50 text-green-600 rounded-2xl">
|
||||
<ShieldCheck size={24} />
|
||||
<div className={`p-3 rounded-2xl ${pairedCount > 0 ? 'bg-green-50 text-green-600' : 'bg-gray-50 text-gray-400'}`}>
|
||||
<Wifi size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm font-medium">System Status</p>
|
||||
<p className="text-xl font-bold text-gray-900 border-b-2 border-green-500 inline-block leading-tight">Secure & Online</p>
|
||||
<p className="text-gray-500 text-sm font-medium">Paired Devices</p>
|
||||
<p className="text-xl font-bold text-gray-900">
|
||||
{pairedCount} <span className="text-sm font-medium text-gray-400">/ {terminals.length}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 p-6 rounded-3xl flex items-center gap-4 border border-blue-100">
|
||||
<Info size={24} className="text-blue-600 shrink-0" />
|
||||
<p className="text-sm text-blue-700 font-medium leading-relaxed">
|
||||
Terminals are used at physical counters to scan order QRs and print bills.
|
||||
Power on a device and enter its 6-digit OTP here to pair it with a terminal.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,15 +186,30 @@ const Terminals = () => {
|
||||
|
||||
<div className="relative z-10 space-y-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="p-4 bg-gray-50 text-[#231651] rounded-2xl border border-gray-200 group-hover:bg-[#231651] group-hover:text-white transition-colors duration-300">
|
||||
<div className={`p-4 rounded-2xl border transition-colors duration-300 ${
|
||||
terminal.paired
|
||||
? 'bg-green-50 text-green-600 border-green-200 group-hover:bg-green-600 group-hover:text-white'
|
||||
: 'bg-gray-50 text-[#231651] border-gray-200 group-hover:bg-[#231651] group-hover:text-white'
|
||||
}`}>
|
||||
<Monitor size={28} />
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => handleDelete(terminal.id, e)}
|
||||
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-all"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
<div className="flex items-center gap-1">
|
||||
{terminal.paired && (
|
||||
<button
|
||||
onClick={(e) => handleUnpair(terminal.id, e)}
|
||||
title="Unpair device"
|
||||
className="p-2 text-gray-400 hover:text-orange-500 hover:bg-orange-50 rounded-xl transition-all"
|
||||
>
|
||||
<Unlink size={16} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => handleDelete(terminal.id, e)}
|
||||
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-all"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -178,16 +220,48 @@ const Terminals = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Device ID for paired terminals */}
|
||||
{terminal.paired && terminal.deviceId && (
|
||||
<div className="flex items-center gap-2 text-xs text-gray-400 font-mono bg-gray-50 px-3 py-1.5 rounded-lg w-fit">
|
||||
<Smartphone size={12} />
|
||||
<span>{terminal.deviceId}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4 flex items-center justify-between border-t border-gray-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||
<span className="text-[12px] font-bold text-gray-500 uppercase tracking-wider">Device Ready</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[#231651] font-bold text-sm">
|
||||
<Key size={16} />
|
||||
<span>View API Key</span>
|
||||
<ExternalLink size={14} />
|
||||
{terminal.paired ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||
<span className="text-[12px] font-bold text-green-600 uppercase tracking-wider">Paired</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-gray-300" />
|
||||
<span className="text-[12px] font-bold text-gray-400 uppercase tracking-wider">Unpaired</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{terminal.paired ? (
|
||||
<div className="flex items-center gap-2 text-[#231651] font-bold text-sm">
|
||||
<Key size={16} />
|
||||
<span>View API Key</span>
|
||||
<ExternalLink size={14} />
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedTerminal(terminal);
|
||||
setIsLinkModalOpen(true);
|
||||
}}
|
||||
className="flex items-center gap-2 text-indigo-600 font-bold text-sm hover:text-indigo-700 transition-colors bg-indigo-50 px-3 py-1.5 rounded-xl hover:bg-indigo-100"
|
||||
>
|
||||
<LinkIcon size={14} />
|
||||
<span>Link Device</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -226,6 +300,17 @@ const Terminals = () => {
|
||||
terminalName={selectedTerminal?.name || ''}
|
||||
/>
|
||||
|
||||
<LinkDeviceModal
|
||||
isOpen={isLinkModalOpen}
|
||||
onClose={() => {
|
||||
setIsLinkModalOpen(false);
|
||||
setSelectedTerminal(null);
|
||||
}}
|
||||
onSuccess={fetchTerminals}
|
||||
terminalId={selectedTerminal?.id || null}
|
||||
terminalName={selectedTerminal?.name || ''}
|
||||
/>
|
||||
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
||||
.font-inter { font-family: 'Inter', sans-serif; }
|
||||
|
||||
Reference in New Issue
Block a user