DONE: first init
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import StationIntroPage from './pages/StationIntroPage/StationIntroPage.jsx';
|
||||
import ShowQueuesForTellerPage from './pages/ShowQueuesForTellerPage/ShowQueuesForTellerPage';
|
||||
import Header from './components/Header/Header.jsx';
|
||||
import CurrentTicketPage from './pages/CurrentTicketPage/CurrentTicketPage.jsx';
|
||||
|
||||
export default function App() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route exact path='/' element={<StationIntroPage />} />
|
||||
<Route exact path="/teller-queue/:stationId" element={<ShowQueuesForTellerPage />} />
|
||||
<Route path="/display/:stationId" element={<CurrentTicketPage/>} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
export default function ExampleButton({ text, onClick }) {
|
||||
return (
|
||||
<button onClick={() => onClick()}>
|
||||
{ text }
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
import './ExampleComponent.css';
|
||||
|
||||
export default function ExampleComponent() {
|
||||
return (
|
||||
<div className="centered">This is a component</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
header.main-header {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
margin: 0 0 3% -2%;
|
||||
background-color: #d3e2f8;
|
||||
width: 101vw;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
margin-top: 7px;
|
||||
margin-right: auto;
|
||||
margin-left: 2%;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import './Header.css';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
|
||||
export default function Header() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<header className="main-header">
|
||||
<h2 className="header-logo" onClick={() => navigate(`/`)}>BBQMS</h2>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const SERVER_URL = 'http://localhost:8080';
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Koristiti ovu funkciju za fetchanje u buducnosti kad god je to moguce.
|
||||
*/
|
||||
export async function fetchData(url, method, body) {
|
||||
const headers = new Headers();
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
headers.append('Authorization', `Bearer ${ token }`);
|
||||
}
|
||||
|
||||
headers.append('Content-Type', 'application/json');
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: method || 'GET',
|
||||
headers: headers,
|
||||
body: body ? JSON.stringify(body) : null
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const data = res.ok && res.body ? await res.json() : null;
|
||||
|
||||
if (res.ok) {
|
||||
//na svaki ispravan rezultat treba da dobijemo novi token da refreshamo stari
|
||||
const newToken = res.headers.get('Auth-Token');
|
||||
if (newToken) {
|
||||
localStorage.setItem('token', newToken);
|
||||
}
|
||||
}
|
||||
|
||||
return { data: data, success: res.ok };
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* A hook for using the native JS interval API. The interval is released after the user component dismounts.
|
||||
* @param callback function to be executed
|
||||
* @param period interval between executions of the callback function. The first execution happens instantly.
|
||||
*/
|
||||
export function useInterval(callback, period) {
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => callback(), period)
|
||||
return () => clearInterval(interval);
|
||||
}, [ period ]);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'General Sans', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0 20px;
|
||||
background-color: ghostwhite;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* ovdje definisite konstante boje i sl. koje cete koristiti na vise mjesta */
|
||||
--blue: #334257;
|
||||
--light-blue: #548CA8;
|
||||
--dark-blue: #476072;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import { BrowserRouter } from "react-router-dom"
|
||||
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
.message{
|
||||
font-size: 1000%;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useInterval } from '../../hooks/hooks.jsx';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import '../CurrentTicketPage/CurrentTicketPage.css';
|
||||
|
||||
export default function CurrentTicketPage() {
|
||||
const [currentTicket, setCurrentTicket] = useState('');
|
||||
const { stationId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrentTicket();
|
||||
}, [stationId]);
|
||||
|
||||
useInterval(() => fetchCurrentTicket(), 3000);
|
||||
|
||||
const fetchCurrentTicket = async () => {
|
||||
try {
|
||||
const response = await fetchData(`${SERVER_URL}/api/v1/teller/current-ticket/${stationId}`, 'GET');
|
||||
if (response.success) {
|
||||
if (typeof response.data === 'object' && response.data.message) {
|
||||
setCurrentTicket(response.data.message);
|
||||
} else {
|
||||
setCurrentTicket(response.data.number);
|
||||
}
|
||||
} else {
|
||||
setCurrentTicket('There is no current ticket for this station');
|
||||
console.error('Error fetching current ticket:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
setCurrentTicket('There is no current ticket for this station');
|
||||
console.log("Error fetching current ticket:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center mt-5">
|
||||
<h1 className="message">{currentTicket}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.center {
|
||||
text-align: center;
|
||||
margin-top: 2%;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useState } from "react";
|
||||
import ExampleComponent from "../../components/ExampleComponent/ExampleComponent";
|
||||
import ExampleButton from "../../components/ExampleButton/ExampleButton";
|
||||
|
||||
import './Home.css'
|
||||
import { SERVER_URL } from "../../constants";
|
||||
|
||||
export default function Home() {
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
async function getMessageFromBackend() {
|
||||
const response = await fetch(SERVER_URL + '/api/v1/teller');
|
||||
const body = await response.json();
|
||||
|
||||
setMessage('Got from backend: ' + body.text);
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<ExampleComponent />
|
||||
<div className="center">
|
||||
<ExampleButton text={ "Click to get message from backend" } onClick={ getMessageFromBackend }/>
|
||||
<p>{ message }</p>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Table } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { useInterval } from '../../hooks/hooks.jsx';
|
||||
import { fetchData } from '../../fetching/Fetch.js'
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
const styles = {
|
||||
stationRow: {
|
||||
backgroundColor: "#ADD8E6",
|
||||
}
|
||||
};
|
||||
|
||||
export default function ShowQueuesForTellerPage() {
|
||||
const url = `${ SERVER_URL }/api/v1/`;
|
||||
const [ tickets, setTickets ] = useState([]);
|
||||
const [ usedUndo, setUsedUndo ] = useState(false)
|
||||
const [ timedOut, setTimedOut ] = useState(false)
|
||||
|
||||
const { stationId} = useParams()
|
||||
|
||||
useEffect(() => {
|
||||
fetchTicketsForTellerStation();
|
||||
}, [ stationId ]);
|
||||
|
||||
useInterval(fetchTicketsForTellerStation, 10000)
|
||||
|
||||
function fetchTicketsForTellerStation() {
|
||||
fetchData(`${ url }stations/${ stationId }/tickets`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setTickets)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function advanceQueue() {
|
||||
fetchData(`${ url }teller/advance-queue/${ stationId }`, 'POST')
|
||||
.then(fetchTicketsForTellerStation)
|
||||
.then(() => setUsedUndo(false))
|
||||
.then(timeOutTeller)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function undoQueue() {
|
||||
fetchData(`${ url }teller/undo-queue/${ stationId }`, 'POST')
|
||||
.then(fetchTicketsForTellerStation)
|
||||
.then(() => setUsedUndo(true))
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function timeOutTeller() {
|
||||
setTimedOut(true)
|
||||
setTimeout(() => setTimedOut(false), 5000)
|
||||
// Can't advance more often than every 5 seconds.
|
||||
}
|
||||
|
||||
const hasAssignedTicket = tickets.some(ticket => ticket.station?.id == stationId)
|
||||
|
||||
return (
|
||||
<div className="text-center mt-5">
|
||||
<h1>Queues</h1>
|
||||
<div className="d-flex gap-2 justify-content-center">
|
||||
<Button
|
||||
variant="primary"
|
||||
style={ { backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8' } }
|
||||
onClick={ advanceQueue }
|
||||
disabled={ timedOut }
|
||||
>
|
||||
Advance Queue
|
||||
</Button>
|
||||
<Button variant="primary"
|
||||
style={ { backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8' } }
|
||||
onClick={ undoQueue }
|
||||
disabled={ usedUndo || !hasAssignedTicket }>
|
||||
Undo
|
||||
</Button>
|
||||
<Button variant="primary"
|
||||
style={ { backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8' } }
|
||||
onClick={ fetchTicketsForTellerStation }>
|
||||
Refresh
|
||||
</Button>
|
||||
<Link to={`/display/${stationId}`} target="_blank" rel="noopener noreferrer">
|
||||
<Button
|
||||
variant="secondary"
|
||||
>
|
||||
Current Ticket
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div style={ { marginTop: '20px' } }>
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ticket number</th>
|
||||
<th>Service</th>
|
||||
<th>Creation at</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tickets.map((ticket, index) => {
|
||||
const createdAt = new Date(ticket.createdAt);
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td style={ticket.station?.id == stationId ? styles.stationRow : {}}>{ticket.number}</td>
|
||||
<td>{ticket.service.name}</td>
|
||||
<td>{createdAt.toLocaleString()}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Dropdown, Button } from 'react-bootstrap';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const StationIntroPage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [branches, setBranches] = useState([]);
|
||||
const [selectedBranch, setSelectedBranch] = useState(null);
|
||||
const [stations, setStations] = useState([]);
|
||||
const [selectedStation, setSelectedStation] = useState(null);
|
||||
|
||||
const url = `${ SERVER_URL }/api/v1/`;
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(`${ url }branches/DFLT`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setBranches)
|
||||
.catch(console.error)
|
||||
}, []);
|
||||
|
||||
function handleBranchSelect(branch) {
|
||||
setSelectedBranch(branch)
|
||||
fetchData(`${ url }stations/DFLT/${ branch.id }`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setStations)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function handleStationSelect(station) {
|
||||
navigate(`/teller-queue/${ station.id }`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-center mt-5">
|
||||
<div className="border p-3" style={{ maxWidth: '500px', margin: '0 auto' }}>
|
||||
<h1 className="mb-4">Select Branch and Station</h1>
|
||||
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="primary" id="dropdown-branch" style={{ width: '100%' }}>
|
||||
{selectedBranch ? selectedBranch.name : 'Select Branch'}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu style={{ width: '100%' }}>
|
||||
{branches.map(branch => (
|
||||
<Dropdown.Item key={branch.id} onClick={() => handleBranchSelect(branch)}>
|
||||
{branch.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
|
||||
{selectedBranch && (
|
||||
<div className="mt-3">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="primary" id="dropdown-station" style={{ width: '100%' }}>
|
||||
{selectedStation ? selectedStation.name : 'Select Station'}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu style={{ width: '100%' }}>
|
||||
{stations.map(station => (
|
||||
<Dropdown.Item key={station.id} onClick={() => handleStationSelect(station)}>
|
||||
{station.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StationIntroPage;
|
||||
Reference in New Issue
Block a user