DONE: first init

This commit is contained in:
ISMAIL MASSERAN
2026-07-20 16:10:22 +08:00
commit 3189e3a1e3
272 changed files with 48853 additions and 0 deletions
+20
View File
@@ -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>
);
}
+1
View File
@@ -0,0 +1 @@
export const SERVER_URL = 'http://localhost:8080';
+35
View File
@@ -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 };
}
+13
View File
@@ -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 ]);
}
+22
View File
@@ -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;
}
+14
View File
@@ -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%;
}
+28
View File
@@ -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;