fix sync usr to admin & fix notification plate & Rut Validation
This commit is contained in:
@@ -83,6 +83,10 @@ app.put('/api/plates/:id/approve', authenticateToken, isAdmin, async (req, res)
|
|||||||
where: { id: parseInt(id) },
|
where: { id: parseInt(id) },
|
||||||
data: { status }
|
data: { status }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Notify Users via WebSocket
|
||||||
|
io.emit('plate_status_updated', plate);
|
||||||
|
|
||||||
res.json(plate);
|
res.json(plate);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
@@ -219,6 +223,10 @@ app.post('/api/people/bulk-approve', authenticateToken, isAdmin, async (req, res
|
|||||||
where: { addedById: parseInt(userId), status: 'PENDING' },
|
where: { addedById: parseInt(userId), status: 'PENDING' },
|
||||||
data: { status: 'APPROVED' }
|
data: { status: 'APPROVED' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Notify Users via WebSocket
|
||||||
|
io.emit('people_updated', { userId });
|
||||||
|
|
||||||
res.json({ message: 'Bulk approval successful' });
|
res.json({ message: 'Bulk approval successful' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import io from 'socket.io-client';
|
import io from 'socket.io-client';
|
||||||
import { Users, CheckCircle, XCircle, Shield, Trash2, Camera, Clock, Calendar } from 'lucide-react';
|
import { Users, CheckCircle, XCircle, Shield, Trash2, Camera, Clock, Calendar, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
||||||
const socket = io(API_URL);
|
const socket = io(API_URL);
|
||||||
@@ -20,6 +20,8 @@ function AdminDashboard({ token }) {
|
|||||||
|
|
||||||
// Visitor State
|
// Visitor State
|
||||||
const [activePeople, setActivePeople] = useState([]);
|
const [activePeople, setActivePeople] = useState([]);
|
||||||
|
const [searchRut, setSearchRut] = useState('');
|
||||||
|
const [searchResult, setSearchResult] = useState(null); // null | { found: true, data: ... } | { found: false }
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -77,6 +79,18 @@ function AdminDashboard({ token }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSearchRut = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const normalizedRut = searchRut.replace(/\./g, '').toUpperCase();
|
||||||
|
const found = activePeople.find(p => p.rut.toUpperCase() === normalizedRut);
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
setSearchResult({ found: true, data: found });
|
||||||
|
} else {
|
||||||
|
setSearchResult({ found: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreateUser = async (e) => {
|
const handleCreateUser = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
@@ -181,6 +195,63 @@ function AdminDashboard({ token }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Visitor Lookup Banner */}
|
||||||
|
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2 text-slate-300">
|
||||||
|
<Shield size={20} /> Visitor Check
|
||||||
|
</h3>
|
||||||
|
<form onSubmit={handleSearchRut} className="flex gap-4">
|
||||||
|
<input
|
||||||
|
className="flex-1 bg-slate-900 border border-slate-600 rounded-lg px-4 py-3 font-mono text-lg tracking-wider uppercase focus:ring-2 focus:ring-blue-500 outline-none transition-all"
|
||||||
|
placeholder="Enter RUT (e.g. 12345678-9)"
|
||||||
|
value={searchRut}
|
||||||
|
onChange={e => setSearchRut(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="bg-blue-600 hover:bg-blue-500 text-white px-8 rounded-lg font-bold transition-all shadow-lg shadow-blue-900/50">
|
||||||
|
Check Status
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{searchResult && (
|
||||||
|
<div className={`mt-6 rounded-xl p-6 border relative animate-in fade-in slide-in-from-top-4 duration-300 ${searchResult.found ? 'bg-gradient-to-r from-blue-900/50 to-cyan-900/50 border-blue-500/50' : 'bg-gradient-to-r from-red-900/50 to-orange-900/50 border-red-500/50'}`}>
|
||||||
|
<button
|
||||||
|
onClick={() => { setSearchResult(null); setSearchRut(''); }}
|
||||||
|
className="absolute top-4 right-4 text-slate-400 hover:text-white transition-colors bg-slate-800/50 rounded-full p-1"
|
||||||
|
>
|
||||||
|
<XCircle size={24} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{searchResult.found ? (
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="bg-green-500/20 p-3 rounded-full text-green-400">
|
||||||
|
<CheckCircle size={32} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xl font-bold text-white mb-1">Access Authorized</h4>
|
||||||
|
<div className="space-y-1 text-slate-300">
|
||||||
|
<p className="flex items-center gap-2"><Users size={16} className="text-blue-400" /> Visitor: <span className="font-semibold text-white">{searchResult.data.name}</span></p>
|
||||||
|
<p className="flex items-center gap-2"><Shield size={16} className="text-purple-400" /> Status: <span className="font-bold text-green-400">{searchResult.data.status}</span></p>
|
||||||
|
<p className="text-sm text-slate-500 mt-2 border-t border-slate-700/50 pt-2">
|
||||||
|
Authorized by: <span className="text-slate-300 font-mono">@{searchResult.data.addedBy?.username || 'Unknown'}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="bg-red-500/20 p-3 rounded-full text-red-400">
|
||||||
|
<AlertCircle size={32} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xl font-bold text-white mb-1">No Record Found</h4>
|
||||||
|
<p className="text-slate-300">This RUT is not listed in the visitor registry.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{viewMode === 'live' ? (
|
{viewMode === 'live' ? (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<div className="bg-black rounded-xl overflow-hidden aspect-video border border-slate-700 relative">
|
<div className="bg-black rounded-xl overflow-hidden aspect-video border border-slate-700 relative">
|
||||||
|
|||||||
@@ -23,7 +23,15 @@ function UserDashboard({ token, username }) {
|
|||||||
setDetections(prev => [data, ...prev].slice(0, 5));
|
setDetections(prev => [data, ...prev].slice(0, 5));
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => socket.off('new_detection');
|
// Real-time status updates
|
||||||
|
socket.on('plate_status_updated', () => fetchPlates());
|
||||||
|
socket.on('people_updated', () => fetchPeople());
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off('new_detection');
|
||||||
|
socket.off('plate_status_updated');
|
||||||
|
socket.off('people_updated');
|
||||||
|
};
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
const fetchPlates = async () => {
|
const fetchPlates = async () => {
|
||||||
@@ -62,7 +70,7 @@ function UserDashboard({ token, username }) {
|
|||||||
});
|
});
|
||||||
setNewPlate({ number: '', owner: '' });
|
setNewPlate({ number: '', owner: '' });
|
||||||
fetchPlates();
|
fetchPlates();
|
||||||
alert('Plate registered! Waiting for admin approval.');
|
// Alert removed for better UX
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Error: ' + err.message);
|
alert('Error: ' + err.message);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user