DONE: first init
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM node:21-alpine AS build
|
||||
WORKDIR /teller-app
|
||||
COPY package*.json .
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "preview"]
|
||||
@@ -0,0 +1,7 @@
|
||||
## Build instructions
|
||||
|
||||
- cd **teller-app**
|
||||
- npm **install**
|
||||
- npm **run dev**
|
||||
|
||||
#### **NOTE:** requires Node 21
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BBQMS Teller App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+4493
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "teller-app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3000",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.3",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.22.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.64",
|
||||
"@types/react-dom": "^18.2.21",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"vite": "^5.1.6"
|
||||
}
|
||||
}
|
||||
Generated
+3373
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in New Issue
Block a user