@@ -0,0 +1,90 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { transitClient } from '../api'
|
||||
|
||||
export default function BusLinesManager({ token, user }) {
|
||||
const client = transitClient(token)
|
||||
const [busLines, setBusLines] = useState([])
|
||||
const [lineNumber, setLineNumber] = useState('')
|
||||
const [startStop, setStartStop] = useState('')
|
||||
const [endStop, setEndStop] = useState('')
|
||||
const [estimatedDurationMinutes, setEstimatedDurationMinutes] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [message, setMessage] = useState(null)
|
||||
const [isAdmin] = useState(Boolean(user?.userCode))
|
||||
|
||||
const loadBusLines = async () => {
|
||||
try {
|
||||
const res = await client.get('/admin/bus-lines')
|
||||
setBusLines(res.data.busLines || [])
|
||||
} catch (err) {
|
||||
setMessage('Error: ' + (err.response?.data?.error || err.message))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadBusLines()
|
||||
}, [])
|
||||
|
||||
const createBusLine = async (e) => {
|
||||
e.preventDefault()
|
||||
setMessage(null)
|
||||
try {
|
||||
const res = await client.post('/admin/bus-lines', {
|
||||
lineNumber: Number(lineNumber),
|
||||
startStop,
|
||||
endStop,
|
||||
estimatedDurationMinutes: Number(estimatedDurationMinutes),
|
||||
description,
|
||||
})
|
||||
setLineNumber('')
|
||||
setStartStop('')
|
||||
setEndStop('')
|
||||
setEstimatedDurationMinutes('')
|
||||
setDescription('')
|
||||
setMessage('Bus line created: #' + (res.data.busLine?.id || 'ok'))
|
||||
await loadBusLines()
|
||||
} catch (err) {
|
||||
setMessage('Error: ' + (err.response?.data?.error || err.message))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Buslijnen</h2>
|
||||
<p>Maak en bewerk buslijnen.</p>
|
||||
</div>
|
||||
<button className="secondary" onClick={loadBusLines}>Refresh</button>
|
||||
</div>
|
||||
|
||||
<div className="list-grid">
|
||||
{busLines.map(line => (
|
||||
<article className="mini-card" key={line.id}>
|
||||
<strong>#{line.id} Lijn {line.lineNumber}</strong>
|
||||
<div>{line.startStop} → {line.endStop}</div>
|
||||
<div>{line.estimatedDurationMinutes} min</div>
|
||||
{line.description ? <div className="muted">{line.description}</div> : null}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isAdmin ? (
|
||||
<form onSubmit={createBusLine} className="stack-form">
|
||||
<div className="form-row">
|
||||
<input placeholder="Line number" value={lineNumber} onChange={e => setLineNumber(e.target.value)} />
|
||||
<input placeholder="Start stop" value={startStop} onChange={e => setStartStop(e.target.value)} />
|
||||
<input placeholder="End stop" value={endStop} onChange={e => setEndStop(e.target.value)} />
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<input placeholder="Duration in minutes" value={estimatedDurationMinutes} onChange={e => setEstimatedDurationMinutes(e.target.value)} />
|
||||
<input placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} />
|
||||
<button type="submit">Create bus line</button>
|
||||
</div>
|
||||
</form>
|
||||
) : <div>You must be admin to create bus lines.</div>}
|
||||
|
||||
{message && <div className="message">{message}</div>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { transitClient } from '../api'
|
||||
|
||||
export default function FleetManager({ token, user }) {
|
||||
const client = transitClient(token)
|
||||
const [buses, setBuses] = useState([])
|
||||
const [name, setName] = useState('')
|
||||
const [seatCapacity, setSeatCapacity] = useState('')
|
||||
const [licensePlate, setLicensePlate] = useState('')
|
||||
const [message, setMessage] = useState(null)
|
||||
const [canCreateBus, setCanCreateBus] = useState(true)
|
||||
const [isAdmin] = useState(Boolean(user?.userCode))
|
||||
|
||||
const loadBuses = async () => {
|
||||
try {
|
||||
const res = await client.get('/admin/fleet/buses')
|
||||
setBuses(res.data.buses || [])
|
||||
} catch (err) {
|
||||
setMessage('Error: ' + (err.response?.data?.error || err.message))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadBuses()
|
||||
}, [])
|
||||
|
||||
const createBus = async (e) => {
|
||||
e.preventDefault()
|
||||
setMessage(null)
|
||||
try {
|
||||
const res = await client.post('/admin/fleet/buses', {
|
||||
name,
|
||||
seatCapacity: Number(seatCapacity),
|
||||
licensePlate,
|
||||
})
|
||||
setName('')
|
||||
setSeatCapacity('')
|
||||
setLicensePlate('')
|
||||
setMessage('Bus created: #' + (res.data.bus?.id || 'ok'))
|
||||
await loadBuses()
|
||||
} catch (err) {
|
||||
const details = err.response?.data?.details || err.response?.data?.error || err.message
|
||||
if (String(details).includes('permission denied') || err.response?.status === 403) {
|
||||
setCanCreateBus(false)
|
||||
setMessage('Fleet create is read-only on this database user.')
|
||||
return
|
||||
}
|
||||
setMessage('Error: ' + details)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Fleet</h2>
|
||||
<p>Beheer voertuigen voor de vloot.</p>
|
||||
</div>
|
||||
<button className="secondary" onClick={loadBuses}>Refresh</button>
|
||||
</div>
|
||||
|
||||
<div className="list-grid">
|
||||
{buses.map(bus => (
|
||||
<article className="mini-card" key={bus.id}>
|
||||
<strong>#{bus.id} {bus.name}</strong>
|
||||
<div>Seats: {bus.seatCapacity}</div>
|
||||
<div>Plate: {bus.licensePlate}</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isAdmin && canCreateBus ? (
|
||||
<form onSubmit={createBus} className="stack-form">
|
||||
<div className="form-row">
|
||||
<input placeholder="Bus name" value={name} onChange={e => setName(e.target.value)} />
|
||||
<input placeholder="Seat capacity" value={seatCapacity} onChange={e => setSeatCapacity(e.target.value)} />
|
||||
<input placeholder="License plate" value={licensePlate} onChange={e => setLicensePlate(e.target.value)} />
|
||||
<button type="submit">Create bus</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="muted">Fleet create is read-only on this database user.</div>
|
||||
)}
|
||||
|
||||
{message && <div className="message">{message}</div>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { transitClient } from '../api'
|
||||
|
||||
export default function LinkStop({ token, user }) {
|
||||
const client = transitClient(token)
|
||||
const [busLines, setBusLines] = useState([])
|
||||
const [busLineId, setBusLineId] = useState('')
|
||||
const [stopId, setStopId] = useState('')
|
||||
const [order, setOrder] = useState('')
|
||||
const [eta, setEta] = useState('')
|
||||
const [message, setMessage] = useState(null)
|
||||
const [stops, setStops] = useState([])
|
||||
|
||||
const isAdmin = Boolean(user?.userCode)
|
||||
|
||||
const loadBusLines = async () => {
|
||||
try {
|
||||
const res = await client.get('/admin/bus-lines')
|
||||
setBusLines(res.data.busLines || [])
|
||||
} catch (err) {
|
||||
setMessage('Error: ' + (err.response?.data?.error || err.message))
|
||||
}
|
||||
}
|
||||
|
||||
const loadStopsForLine = async (lineId = busLineId) => {
|
||||
if (!lineId) return
|
||||
setMessage(null)
|
||||
try {
|
||||
const res = await client.get(`/bus-lines/${lineId}/stops`)
|
||||
setStops(res.data.stops || [])
|
||||
} catch (err) {
|
||||
setMessage('Error: ' + (err.response?.data?.error || err.message))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadBusLines()
|
||||
}, [])
|
||||
|
||||
const link = async (e) => {
|
||||
e.preventDefault()
|
||||
setMessage(null)
|
||||
try {
|
||||
const res = await client.post(`/admin/bus-lines/${busLineId}/stops`, {
|
||||
stopId: parseInt(stopId,10),
|
||||
stopOrder: parseInt(order,10),
|
||||
estimatedArrivalMinutes: eta ? parseInt(eta,10) : null
|
||||
})
|
||||
setMessage('Linked: ' + res.data.routeStop.id)
|
||||
await loadStopsForLine()
|
||||
} catch (err) {
|
||||
if (err.response?.status === 403) setMessage('Forbidden: not admin')
|
||||
else setMessage('Error: ' + (err.response?.data?.error || err.message))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Stops per lijn</h2>
|
||||
<p>Koppel stops aan lijnen en bekijk de volgorde.</p>
|
||||
</div>
|
||||
<button className="secondary" onClick={() => loadStopsForLine()}>Refresh</button>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<input
|
||||
placeholder="Bus line ID"
|
||||
value={busLineId}
|
||||
onChange={e => {
|
||||
setBusLineId(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<button type="button" onClick={() => loadStopsForLine()}>Load Stops</button>
|
||||
</div>
|
||||
|
||||
{busLines.length > 0 && (
|
||||
<div className="muted" style={{ marginBottom: 10 }}>
|
||||
Known lines: {busLines.map(line => `#${line.id} (lijn ${line.lineNumber})`).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ul>
|
||||
{stops.map(s => (
|
||||
<li key={s.id}>{s.order}. {s.name} ({s.latitude}, {s.longitude})</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h3>Link a Stop to Line</h3>
|
||||
{isAdmin ? (
|
||||
<form onSubmit={link} className="form-row">
|
||||
<input placeholder="Stop ID" value={stopId} onChange={e=>setStopId(e.target.value)} />
|
||||
<input placeholder="Stop Order" value={order} onChange={e=>setOrder(e.target.value)} />
|
||||
<input placeholder="ETA minutes (optional)" value={eta} onChange={e=>setEta(e.target.value)} />
|
||||
<button type="submit">Link Stop</button>
|
||||
</form>
|
||||
) : (
|
||||
<div>You must be admin to link stops.</div>
|
||||
)}
|
||||
{message && <div className="message">{message}</div>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import React, { useState } from 'react'
|
||||
import { authClient } from '../api'
|
||||
|
||||
export default function Login({ onLogin }) {
|
||||
const [userCode, setUserCode] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const submit = async (e) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await authClient.post('/login', { userCode, password })
|
||||
const token = res.data.token
|
||||
// fetch /me to get payload
|
||||
const me = await authClient.get('/me', { headers: { Authorization: `Bearer ${token}` } })
|
||||
onLogin(token, me.data.user)
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'login_failed')
|
||||
} finally { setLoading(false) }
|
||||
}
|
||||
|
||||
const devToken = async () => {
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await authClient.post('/dev/token', { userId: 1, userCode: 42 })
|
||||
const token = res.data.token
|
||||
const me = await authClient.get('/me', { headers: { Authorization: `Bearer ${token}` } })
|
||||
onLogin(token, me.data.user)
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'dev_token_failed')
|
||||
} finally { setLoading(false) }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login">
|
||||
<h2>Admin Login</h2>
|
||||
<form onSubmit={submit}>
|
||||
<label>
|
||||
User Code
|
||||
<input value={userCode} onChange={e => setUserCode(e.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
|
||||
</label>
|
||||
<button type="submit" disabled={loading}>{loading ? 'Please wait...' : 'Login'}</button>
|
||||
{error && <div className="error">{error}</div>}
|
||||
</form>
|
||||
<p>Be sure `ADMIN_USER_CODES` contains your `userCode` for admin actions.</p>
|
||||
<hr />
|
||||
<div>
|
||||
<button onClick={devToken} disabled={loading}>Get Dev Token (if available)</button>
|
||||
<div style={{fontSize:12, marginTop:6}}>Uses `/dev/token` on the auth API for quick local testing.</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { transitClient } from '../api'
|
||||
|
||||
export default function StopsManager({ token, user }) {
|
||||
const client = transitClient(token)
|
||||
const [stops, setStops] = useState([])
|
||||
const [name, setName] = useState('')
|
||||
const [lat, setLat] = useState('')
|
||||
const [lon, setLon] = useState('')
|
||||
const [message, setMessage] = useState(null)
|
||||
const [isAdmin] = useState(Boolean(user?.userCode))
|
||||
|
||||
const loadStops = async () => {
|
||||
try {
|
||||
const res = await client.get('/admin/stops')
|
||||
setStops(res.data.stops || [])
|
||||
} catch (err) {
|
||||
setMessage('Error: ' + (err.response?.data?.error || err.message))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadStops()
|
||||
}, [])
|
||||
|
||||
const createStop = async (e) => {
|
||||
e.preventDefault()
|
||||
setMessage(null)
|
||||
try {
|
||||
const res = await client.post('/admin/stops', {
|
||||
name,
|
||||
latitude: parseFloat(lat),
|
||||
longitude: parseFloat(lon)
|
||||
})
|
||||
setMessage('Stop created: #' + res.data.stop.id)
|
||||
setName(''); setLat(''); setLon('')
|
||||
await loadStops()
|
||||
} catch (err) {
|
||||
const errMsg = err.response?.data?.error || err.message
|
||||
if (err.response?.status === 401) {
|
||||
setMessage('Unauthorized: invalid token')
|
||||
} else if (err.response?.status === 403) {
|
||||
setMessage('Forbidden: you are not an admin')
|
||||
} else {
|
||||
setMessage('Error: ' + errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Stops</h2>
|
||||
<p>Maak herbruikbare haltes aan.</p>
|
||||
</div>
|
||||
<button className="secondary" onClick={loadStops}>Refresh</button>
|
||||
</div>
|
||||
<div className="list-grid">
|
||||
{stops.map(stop => (
|
||||
<article className="mini-card" key={stop.id}>
|
||||
<strong>#{stop.id} {stop.name}</strong>
|
||||
<div>{stop.latitude}, {stop.longitude}</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
{isAdmin ? (
|
||||
<form onSubmit={createStop} className="stack-form">
|
||||
<div className="form-row">
|
||||
<input placeholder="Name" value={name} onChange={e=>setName(e.target.value)} />
|
||||
<input placeholder="Latitude" value={lat} onChange={e=>setLat(e.target.value)} />
|
||||
<input placeholder="Longitude" value={lon} onChange={e=>setLon(e.target.value)} />
|
||||
<button type="submit">Create Stop</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div>You must be admin to create stops.</div>
|
||||
)}
|
||||
|
||||
{message && <div className="message">{message}</div>}
|
||||
<p>Note: To view stops per line, use the Bus Line Stops tool below.</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user