diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 2ee573a..8390e65 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -14,6 +14,19 @@ model User { password String role String @default("USER") // ADMIN, USER plates Plate[] + people Person[] +} + +model Person { + id Int @id @default(autoincrement()) + rut String @unique + name String + status String @default("PENDING") // PENDING, APPROVED, DENIED + startDate DateTime + endDate DateTime + createdAt DateTime @default(now()) + addedBy User? @relation(fields: [addedById], references: [id]) + addedById Int? } model Plate { diff --git a/backend/src/index.js b/backend/src/index.js index e32061f..fa7816b 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -147,6 +147,76 @@ app.get('/api/recent', async (req, res) => { } }); +// Helper: RUT Validation +function validateRut(rut) { + if (!rut || !/^[0-9]+-[0-9kK]{1}$/.test(rut)) return false; + let [num, dv] = rut.split('-'); + let total = 0; + let multiple = 2; + for (let i = num.length - 1; i >= 0; i--) { + total += parseInt(num.charAt(i)) * multiple; + multiple = (multiple + 1) % 8 || 2; + } + let res = 11 - (total % 11); + let finalDv = res === 11 ? '0' : res === 10 ? 'K' : res.toString(); + return finalDv.toUpperCase() === dv.toUpperCase(); +} + +// People CRUD +app.get('/api/people', authenticateToken, async (req, res) => { + try { + const where = req.user.role === 'ADMIN' ? {} : { addedById: req.user.id }; + const people = await prisma.person.findMany({ + where, + include: { addedBy: { select: { username: true } } } + }); + res.json(people); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.post('/api/people', authenticateToken, async (req, res) => { + const { rut, name, durationDays } = req.body; + if (!validateRut(rut)) { + return res.status(400).json({ error: 'Invalid RUT format (12345678-K)' }); + } + + const startDate = new Date(); + const endDate = new Date(); + endDate.setDate(endDate.getDate() + parseInt(durationDays || 1)); + + try { + const person = await prisma.person.create({ + data: { + rut, + name, + startDate, + endDate, + status: 'PENDING', + addedById: req.user.id + } + }); + res.json(person); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// Admin: Bulk Approve People by User +app.post('/api/people/bulk-approve', authenticateToken, isAdmin, async (req, res) => { + const { userId } = req.body; + try { + await prisma.person.updateMany({ + where: { addedById: parseInt(userId), status: 'PENDING' }, + data: { status: 'APPROVED' } + }); + res.json({ message: 'Bulk approval successful' }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + // Detection Endpoint (from Python) app.post('/api/detect', async (req, res) => { const { plate_number } = req.body; diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx index 4a3fd32..4531b80 100644 --- a/frontend/src/pages/AdminDashboard.jsx +++ b/frontend/src/pages/AdminDashboard.jsx @@ -10,7 +10,7 @@ function AdminDashboard({ token }) { const [users, setUsers] = useState([]); const [plates, setPlates] = useState([]); const [newUser, setNewUser] = useState({ username: '', password: '', role: 'USER' }); - const [activeTab, setActiveTab] = useState('monitor'); // 'monitor' | 'plates' | 'users' + const [activeTab, setActiveTab] = useState('monitor'); // 'monitor' | 'plates' | 'users' | 'visitors' // Monitor State const [detections, setDetections] = useState([]); @@ -18,6 +18,9 @@ function AdminDashboard({ token }) { const [viewMode, setViewMode] = useState('live'); // 'live' | 'history' const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); + // Visitor State + const [activePeople, setActivePeople] = useState([]); + useEffect(() => { fetchData(); @@ -38,10 +41,11 @@ function AdminDashboard({ token }) { const fetchData = async () => { try { const authHeader = { headers: { Authorization: `Bearer ${token}` } }; - const [usersRes, platesRes, recentRes] = await Promise.all([ + const [usersRes, platesRes, recentRes, peopleRes] = await Promise.all([ axios.get(`${API_URL}/api/auth`, authHeader).catch(err => ({ data: [] })), axios.get(`${API_URL}/api/plates`, authHeader), - axios.get(`${API_URL}/api/recent`, authHeader) + axios.get(`${API_URL}/api/recent`, authHeader), + axios.get(`${API_URL}/api/people`, authHeader) ]); setUsers(usersRes.data); setPlates(platesRes.data); @@ -50,6 +54,7 @@ function AdminDashboard({ token }) { status: log.accessStatus, timestamp: log.timestamp }))); + setActivePeople(peopleRes.data); } catch (err) { console.error(err); } @@ -101,10 +106,23 @@ function AdminDashboard({ token }) { } }; + const handleBulkApprove = async (userId) => { + if (!confirm('Approve ALL pending visitors for this user?')) return; + try { + await axios.post(`${API_URL}/api/people/bulk-approve`, { userId }, { + headers: { Authorization: `Bearer ${token}` } + }); + fetchData(); + alert('All visitors approved'); + } catch (err) { + alert(err.message); + } + }; + const StatusBadge = ({ status }) => ( {status} @@ -137,6 +155,9 @@ function AdminDashboard({ token }) { > User Management + diff --git a/frontend/src/pages/UserDashboard.jsx b/frontend/src/pages/UserDashboard.jsx index 200916c..752b8a3 100644 --- a/frontend/src/pages/UserDashboard.jsx +++ b/frontend/src/pages/UserDashboard.jsx @@ -10,9 +10,12 @@ function UserDashboard({ token, username }) { const [plates, setPlates] = useState([]); const [newPlate, setNewPlate] = useState({ number: '', owner: '' }); const [detections, setDetections] = useState([]); + const [people, setPeople] = useState([]); + const [newPerson, setNewPerson] = useState({ name: '', rut: '', durationDays: 1 }); useEffect(() => { fetchPlates(); + fetchPeople(); // Listen for live detections (optional, maybe user wants to see their plates detected?) // For now, let's show all global detections but emphasize this is a "Portal" @@ -37,6 +40,17 @@ function UserDashboard({ token, username }) { } }; + const fetchPeople = async () => { + try { + const res = await axios.get(`${API_URL}/api/people`, { + headers: { Authorization: `Bearer ${token}` } + }); + setPeople(res.data); + } catch (err) { + console.error(err); + } + }; + const handleRegister = async (e) => { e.preventDefault(); try { @@ -54,76 +68,106 @@ function UserDashboard({ token, username }) { } }; + const handleRegisterPerson = async (e) => { + e.preventDefault(); + try { + await axios.post(`${API_URL}/api/people`, newPerson, { + headers: { Authorization: `Bearer ${token}` } + }); + setNewPerson({ name: '', rut: '', durationDays: 1 }); + fetchPeople(); + alert('Person registered! Waiting for admin approval.'); + } catch (err) { + alert('Error: ' + (err.response?.data?.error || err.message)); + } + }; + return (
Manage your vehicles and access.
-Manage your vehicles and visitors.
+No plates registered.
} - {plates.map(plate => ( -No plates registered.
} + {plates.map(plate => ( +No visitors registered.
} + {people.map(p => ( +