13 Commits

Author SHA1 Message Date
67758d91cf Refresh menu, první část 2025-04-15 19:51:41 +02:00
49b8ab5c13 Update server/src/index.ts
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
delete req.headers["cookie"]
2025-04-11 12:06:52 +02:00
9a05ef1fe6 Update server/src/index.ts
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
vic logov
2025-04-11 12:01:58 +02:00
0bfea3765f properta pro logovani headeru
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-04-11 10:42:27 +02:00
962fbe2947 fix hardcoded header name xd
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-04-11 10:06:35 +02:00
d6d6ebb682 Aktualizace posledních změn
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-03-21 00:24:26 +01:00
5bb7de58e7 Odebrání zimní atmosféry 2025-03-21 00:24:17 +01:00
739c7707e1 Migrace serveru na OpenAPI
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-03-20 23:50:47 +01:00
d366882f6b Migrace klienta na OpenAPI
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-03-19 23:08:46 +01:00
f09bc44d63 Oprava nefunkčního odebrání prvního vybraného jídla
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-03-11 20:57:37 +01:00
f0d56f11aa Oprava popisu varianty "neobědvám"
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-03-11 19:41:37 +01:00
f74ec379c8 Oprava výběru možnosti stravování
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-03-06 08:03:49 +01:00
c9fa710070 Oprava buildu
Some checks failed
ci/woodpecker/push/workflow Pipeline failed
2025-03-06 07:59:33 +01:00
60 changed files with 1237 additions and 1003 deletions

View File

@@ -9,6 +9,8 @@ WORKDIR /build
COPY types/package.json ./types/
COPY types/yarn.lock ./types/
COPY types/api.yml ./types/
COPY types/schemas ./types/schemas/
COPY types/paths ./types/paths/
COPY types/openapi-ts.config.ts ./types/
# Zkopírování závislostí - server
@@ -46,7 +48,6 @@ COPY client/src ./client/src
COPY client/public ./client/public
# Zkopírování společných typů
COPY types/RequestTypes.ts ./types/
COPY types/index.ts ./types/
# Vygenerování společných typů z OpenAPI

View File

@@ -1,7 +1,6 @@
import React, { useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import { EVENT_DISCONNECT, EVENT_MESSAGE, SocketContext } from './context/socket';
import { addPizza, createPizzaDay, deletePizzaDay, finishDelivery, finishOrder, lockPizzaDay, removePizza, unlockPizzaDay, updatePizzaDayNote } from './api/PizzaDayApi';
import { useAuth } from './context/auth';
import Login from './Login';
import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap';
@@ -16,15 +15,13 @@ import { useSettings } from './context/settings';
import Footer from './components/Footer';
import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons';
import Loader from './components/Loader';
import { getData, errorHandler, getQrUrl } from './api/Api';
import { addChoice, removeChoices, removeChoice, changeDepartureTime, jdemeObed, updateNote } from './api/FoodApi';
import { getHumanDateTime, isInTheFuture } from './Utils';
import NoteModal from './components/modals/NoteModal';
import { useEasterEgg } from './context/eggs';
import { getImage } from './api/EasterEggApi';
import { Link } from 'react-router';
import { STATS_URL } from './AppRoutes';
import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant } from '../../types';
import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime } from '../../types';
import { getLunchChoiceName } from './enums';
const EVENT_CONNECT = "connect"
@@ -68,11 +65,12 @@ function App() {
if (!auth?.login) {
return
}
getData().then(({ data }) => {
getData().then(response => {
const data = response.data
if (data) {
setData(data);
setDayIndex(data.weekIndex);
dayIndexRef.current = data.weekIndex;
setDayIndex(data.dayIndex);
dayIndexRef.current = data.dayIndex;
setFood(data.menus);
}
}).catch(e => {
@@ -85,9 +83,12 @@ function App() {
if (!auth?.login) {
return
}
getData(dayIndex).then((data: ClientData) => {
getData({ query: { dayIndex: dayIndex } }).then(response => {
const data = response.data;
setData(data);
setFood(data.menus);
if (data) {
setFood(data.menus);
}
}).catch(e => {
setFailure(true);
})
@@ -142,10 +143,10 @@ function App() {
useEffect(() => {
if (choiceRef?.current?.value && choiceRef.current.value !== "") {
const locationKey = choiceRef.current.value as keyof typeof LunchChoice;
const locationKey = choiceRef.current.value as LunchChoice;
const restaurantKey = Object.keys(Restaurant).indexOf(locationKey);
if (restaurantKey > -1 && food) {
const restaurant = Object.keys(Restaurant)[restaurantKey] as keyof typeof Restaurant;
const restaurant = Object.keys(Restaurant)[restaurantKey] as Restaurant;
setFoodChoiceList(food[restaurant]?.food);
setClosed(food[restaurant]?.closed ?? false);
} else {
@@ -177,9 +178,9 @@ function App() {
// Stažení a nastavení easter egg obrázku
useEffect(() => {
if (auth?.login && easterEgg?.url && !eggImage) {
getImage(easterEgg.url).then(data => {
if (data) {
setEggImage(data);
getEasterEggImage({ path: { url: easterEgg.url } }).then(response => {
if (response.data) {
setEggImage(response.data);
// Smazání obrázku z DOMu po animaci
setTimeout(() => {
if (eggRef?.current) {
@@ -191,18 +192,18 @@ function App() {
}
}, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]);
const doAddClickFoodChoice = async (location: keyof typeof LunchChoice, foodIndex?: number) => {
const doAddClickFoodChoice = async (location: LunchChoice, foodIndex?: number) => {
if (document.getSelection()?.type !== 'Range') { // pouze pokud se nejedná o výběr textu
if (auth?.login) {
await errorHandler(() => addChoice(location, foodIndex, dayIndex));
await addChoice({ body: { locationKey: location, foodIndex, dayIndex } });
}
}
}
const doAddChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const locationKey = event.target.value as keyof typeof LunchChoice;
const locationKey = event.target.value as LunchChoice;
if (auth?.login) {
await errorHandler(() => addChoice(locationKey, undefined, dayIndex));
await addChoice({ body: { locationKey, dayIndex } });
if (foodChoiceRef.current?.value) {
foodChoiceRef.current.value = "";
}
@@ -217,16 +218,16 @@ function App() {
const doAddFoodChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => {
if (event.target.value && foodChoiceList?.length && choiceRef.current?.value) {
const locationKey = choiceRef.current.value as keyof typeof LunchChoice;
const locationKey = choiceRef.current.value as LunchChoice;
if (auth?.login) {
await errorHandler(() => addChoice(locationKey, Number(event.target.value), dayIndex));
await addChoice({ body: { locationKey, foodIndex: Number(event.target.value), dayIndex } });
}
}
}
const doRemoveChoices = async (locationKey: keyof typeof LunchChoice) => {
const doRemoveChoices = async (locationKey: LunchChoice) => {
if (auth?.login) {
await errorHandler(() => removeChoices(locationKey, dayIndex));
await removeChoices({ body: { locationKey, dayIndex } });
// Vyresetujeme výběr, aby bylo jasné pro který případně vybíráme jídlo
if (choiceRef?.current?.value) {
choiceRef.current.value = "";
@@ -237,9 +238,9 @@ function App() {
}
}
const doRemoveFoodChoice = async (locationKey: keyof typeof LunchChoice, foodIndex: number) => {
const doRemoveFoodChoice = async (locationKey: LunchChoice, foodIndex: number) => {
if (auth?.login) {
await errorHandler(() => removeChoice(locationKey, foodIndex, dayIndex));
await removeChoice({ body: { locationKey, foodIndex, dayIndex } });
if (choiceRef?.current?.value) {
choiceRef.current.value = "";
}
@@ -251,7 +252,7 @@ function App() {
const saveNote = async (note?: string) => {
if (auth?.login) {
await errorHandler(() => updateNote(note, dayIndex));
await updateNote({ body: { note, dayIndex } });
setNoteModalOpen(false);
}
}
@@ -281,12 +282,12 @@ function App() {
const s = value.split('|');
const pizzaIndex = Number.parseInt(s[0]);
const pizzaSizeIndex = Number.parseInt(s[1]);
await addPizza(pizzaIndex, pizzaSizeIndex);
await addPizza({ body: { pizzaIndex, pizzaSizeIndex } });
}
}
const handlePizzaDelete = async (pizzaOrder: PizzaVariant) => {
await removePizza(pizzaOrder);
await removePizza({ body: { pizzaOrder } });
}
const handlePizzaPoznamkaChange = async () => {
@@ -294,7 +295,7 @@ function App() {
alert("Poznámka může mít maximálně 70 znaků");
return;
}
updatePizzaDayNote(pizzaPoznamkaRef.current?.value);
updatePizzaDayNote({ body: { note: pizzaPoznamkaRef.current?.value } });
}
// const addToCart = async () => {
@@ -323,7 +324,7 @@ function App() {
const handleChangeDepartureTime = async (event: React.ChangeEvent<HTMLSelectElement>) => {
if (foodChoiceList?.length && choiceRef.current?.value) {
await changeDepartureTime(event.target.value, dayIndex);
await changeDepartureTime({ body: { time: event.target.value as DepartureTime, dayIndex } });
}
}
@@ -341,7 +342,7 @@ function App() {
}
}
const renderFoodTable = (location: keyof typeof Restaurant, menu: RestaurantDayMenu) => {
const renderFoodTable = (location: Restaurant, menu: RestaurantDayMenu) => {
let content;
if (menu?.closed) {
content = <h3>Zavřeno</h3>
@@ -363,7 +364,7 @@ function App() {
content = <h3>Chyba načtení dat</h3>
}
return <Col md={12} lg={3} className='mt-3'>
<h3 style={{ cursor: 'pointer' }} onClick={() => doAddClickFoodChoice(location)}>{location}</h3>
<h3 style={{ cursor: 'pointer' }} onClick={() => doAddClickFoodChoice(location)}>{getLunchChoiceName(location)}</h3>
{menu?.lastUpdate && <small>Poslední aktualizace: {getHumanDateTime(new Date(menu.lastUpdate))}</small>}
{content}
</Col>
@@ -409,12 +410,12 @@ function App() {
<div className='wrapper'>
{data.isWeekend ? <h4>Užívejte víkend :)</h4> : <>
<Alert variant={'primary'}>
<img alt="" src='hat.png' style={{ position: "absolute", width: "70px", rotate: "-45deg", left: -40, top: -58 }} />
<img alt="" src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} />
{/* <img alt="" src='hat.png' style={{ position: "absolute", width: "70px", rotate: "-45deg", left: -40, top: -58 }} />
<img alt="" src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} /> */}
Poslední změny:
<ul>
<li>Možnost výběru restaurace a jídel kliknutím v tabulce</li>
<li><Link to={STATS_URL}>Statistiky</Link></li>
<li>Migrace na generované <Link target='_blank' to="https://www.openapis.org">OpenAPI</Link></li>
<li>Odebrání zimní atmosféry</li>
</ul>
</Alert>
{dayIndex != null &&
@@ -437,12 +438,12 @@ function App() {
<p>{`Jak to ${dayIndex == null || dayIndex === data.todayDayIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}</p>
<Form.Select ref={choiceRef} onChange={doAddChoice}>
<option></option>
{Object.keys(Restaurant)
{Object.entries(LunchChoice)
.filter(entry => {
const locationKey = entry as keyof typeof Restaurant;
const locationKey = entry[0] as Restaurant;
return !food[locationKey]?.closed;
})
.map(entry => <option key={entry[0]} value={entry[0]}>{entry[1]}</option>)}
.map(entry => <option key={entry[0]} value={entry[0]}>{getLunchChoiceName(entry[1])}</option>)}
</Form.Select>
<small>Je možné vybrat jen jednu možnost. Výběr jiné odstraní předchozí.</small>
{foodChoiceList && !closed && <>
@@ -466,8 +467,8 @@ function App() {
<Table bordered className='mt-5'>
<tbody>
{Object.keys(data.choices).map(key => {
const locationKey = key as keyof typeof LunchChoice;
const locationName = LunchChoice[locationKey];
const locationKey = key as LunchChoice;
const locationName = getLunchChoiceName(locationKey);
const loginObject = data.choices[locationKey];
if (!loginObject) {
return;
@@ -500,13 +501,13 @@ function App() {
setNoteModalOpen(true);
}} title='Upravit poznámku' className='action-icon' icon={faNoteSticky} />}
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => {
doRemoveChoices(key as keyof typeof LunchChoice);
doRemoveChoices(key as LunchChoice);
}} title={`Odstranit volbu ${locationName}, včetně případných zvolených jídel`} className='action-icon' icon={faTrashCan} />}
</td>
{userChoices?.length && food ? <td>
<ul>
{userChoices?.map(foodIndex => {
const restaurantKey = key as keyof typeof Restaurant;
const restaurantKey = key as Restaurant;
const foodName = food[restaurantKey]?.food?.[foodIndex].name;
return <li key={foodIndex}>
{foodName}
@@ -601,7 +602,7 @@ function App() {
await lockPizzaDay();
}}>Vrátit do "uzamčeno"</Button>
<Button className='danger mb-3' style={{ marginLeft: '20px' }} title="Nastaví stav na 'Doručeno' - koncový stav." onClick={async () => {
await finishDelivery(settings?.bankAccount, settings?.holderName);
await finishDelivery({ body: { bankAccount: settings?.bankAccount, bankAccountHolder: settings?.holderName } });
}}>Doručeno</Button>
</div>
}
@@ -643,7 +644,7 @@ function App() {
data.pizzaDay.state === PizzaDayState.DELIVERED && myOrder?.hasQr ?
<div className='qr-code'>
<h3>QR platba</h3>
<img src={getQrUrl(auth.login)} alt='QR kód' />
<img src={`/api/qr?login=${auth.login}`} alt='QR kód' />
</div> : null
}
</div>
@@ -651,7 +652,7 @@ function App() {
</div>
}
</div>
</>}
</> || "Jejda, něco se nám nepovedlo :("}
</div>
<Footer />
<NoteModal isOpen={noteModalOpen} onClose={() => setNoteModalOpen(false)} onSave={saveNote} />

View File

@@ -1,6 +1,6 @@
import { Routes, Route } from "react-router-dom";
import { ProvideSettings } from "./context/settings";
import Snowfall from "react-snowfall";
// import Snowfall from "react-snowfall";
import { ToastContainer } from "react-toastify";
import { SocketContext, socket } from "./context/socket";
import StatsPage from "./pages/StatsPage";
@@ -16,12 +16,12 @@ export default function AppRoutes() {
<ProvideSettings>
<SocketContext.Provider value={socket}>
<>
<Snowfall style={{
{/* <Snowfall style={{
zIndex: 2,
position: 'fixed',
width: '100vw',
height: '100vh'
}} />
}} /> */}
<App />
</>
<ToastContainer />

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { Button } from 'react-bootstrap';
import { useAuth } from './context/auth';
import { login } from './api/Api';
import { login } from '../../types';
import './Login.css';
/**
@@ -14,9 +14,10 @@ export default function Login() {
useEffect(() => {
if (auth && !auth.login) {
// Vyzkoušíme přihlášení "naprázdno", pokud projde, přihlásili nás trusted headers
login().then(token => {
login().then(response => {
const token = response.data;
if (token) {
auth?.setToken(token);
auth?.setToken(token as unknown as string); // TODO vyřešit, API definice je špatně, je to skutečně string
}
}).catch(error => {
// nezajímá nás
@@ -27,9 +28,9 @@ export default function Login() {
const doLogin = useCallback(async () => {
const length = loginRef?.current?.value.length && loginRef.current.value.replace(/\s/g, '').length
if (length) {
const token = await login(loginRef.current?.value);
if (token) {
auth?.setToken(token);
const response = await login({ body: { login: loginRef.current?.value } });
if (response.data) {
auth?.setToken(response.data as unknown as string); // TODO vyřešit
}
}
}, [auth]);

View File

@@ -1,83 +0,0 @@
import { toast } from "react-toastify";
import { getToken } from "../Utils";
/**
* Wrapper pro volání API, u kterých chceme automaticky zobrazit toaster s chybou ze serveru.
*
* @param apiFunction volaná API funkce
*/
export function errorHandler<T>(apiFunction: () => Promise<T>): Promise<T> {
return new Promise<T>((resolve, reject) => {
apiFunction().then((result) => {
resolve(result);
}).catch(e => {
toast.error(e.message, { theme: "colored" });
});
});
}
async function request<TResponse>(
url: string,
config: RequestInit = {}
): Promise<TResponse> {
config.headers = config?.headers ? new Headers(config.headers) : new Headers();
config.headers.set("Authorization", `Bearer ${getToken()}`);
try {
const response = await fetch(url, config);
if (!response.ok) {
// TODO tohle je blbě, jelikož automaticky očekáváme, že v případě chyby přijde vždy JSON, což není pravda
const json = await response.json();
// Vyhodíme samotnou hlášku z odpovědi, odchytí si jí errorHandler
throw new Error(json.error);
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
return response.json() as TResponse;
} else {
return response.text() as TResponse;
}
} catch (e) {
return Promise.reject(e);
}
}
async function blobRequest(
url: string,
config: RequestInit = {}
): Promise<Blob> {
config.headers = config?.headers ? new Headers(config.headers) : new Headers();
config.headers.set("Authorization", `Bearer ${getToken()}`);
try {
const response = await fetch(url, config);
if (!response.ok) {
const json = await response.json();
// Vyhodíme samotnou hlášku z odpovědi, odchytí si jí errorHandler
throw new Error(json.error);
}
return response.blob()
} catch (e) {
return Promise.reject(e);
}
}
export const api = {
get: <TResponse>(url: string) => request<TResponse>(url),
blobGet: (url: string) => blobRequest(url),
post: <TBody, TResponse>(url: string, body?: TBody) => request<TResponse>(url, { method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' } }),
}
export const getQrUrl = (login: string) => {
return `/api/qr?login=${login}`;
}
export const getData = async (dayIndex?: number) => {
let url = '/api/data';
if (dayIndex != null) {
url += '?dayIndex=' + dayIndex;
}
return await api.get<any>(url);
}
export const login = async (login?: string) => {
return await api.post<any, any>('/api/login', { login });
}

View File

@@ -1,8 +0,0 @@
import { client } from '../../../types/gen/client.gen';
import { getToken } from '../Utils';
client.setConfig({
auth: () => getToken(),
});
export default client

View File

@@ -1,12 +0,0 @@
import { EasterEgg } from "../../../types";
import { api } from "./Api";
const EASTER_EGGS_API_PREFIX = '/api/easterEggs';
export const getEasterEgg = async (): Promise<EasterEgg | undefined> => {
return await api.get<EasterEgg>(`${EASTER_EGGS_API_PREFIX}`);
}
export const getImage = async (url: string) => {
return await api.blobGet(`${EASTER_EGGS_API_PREFIX}/${url}`);
}

View File

@@ -1,28 +0,0 @@
import { AddChoiceRequest, ChangeDepartureTimeRequest, LunchChoice, RemoveChoiceRequest, RemoveChoicesRequest, UpdateNoteRequest } from "../../../types";
import { api } from "./Api";
const FOOD_API_PREFIX = '/api/food';
export const addChoice = async (locationKey: keyof typeof LunchChoice, foodIndex?: number, dayIndex?: number) => {
return await api.post<AddChoiceRequest, void>(`${FOOD_API_PREFIX}/addChoice`, { locationKey, foodIndex, dayIndex });
}
export const removeChoices = async (locationKey: keyof typeof LunchChoice, dayIndex?: number) => {
return await api.post<RemoveChoicesRequest, void>(`${FOOD_API_PREFIX}/removeChoices`, { locationKey, dayIndex });
}
export const removeChoice = async (locationKey: keyof typeof LunchChoice, foodIndex: number, dayIndex?: number) => {
return await api.post<RemoveChoiceRequest, void>(`${FOOD_API_PREFIX}/removeChoice`, { locationKey, foodIndex, dayIndex });
}
export const updateNote = async (note?: string, dayIndex?: number) => {
return await api.post<UpdateNoteRequest, void>(`${FOOD_API_PREFIX}/updateNote`, { note, dayIndex });
}
export const changeDepartureTime = async (time: string, dayIndex?: number) => {
return await api.post<ChangeDepartureTimeRequest, void>(`${FOOD_API_PREFIX}/changeDepartureTime`, { time, dayIndex });
}
export const jdemeObed = async () => {
return await api.post<undefined, void>(`${FOOD_API_PREFIX}/jdemeObed`);
}

View File

@@ -1,44 +0,0 @@
import { AddPizzaRequest, FinishDeliveryRequest, PizzaVariant, RemovePizzaRequest, UpdatePizzaDayNoteRequest, UpdatePizzaFeeRequest } from "../../../types";
import { api } from "./Api";
const PIZZADAY_API_PREFIX = '/api/pizzaDay';
export const createPizzaDay = async () => {
return await api.post<undefined, void>(`${PIZZADAY_API_PREFIX}/create`);
}
export const deletePizzaDay = async () => {
return await api.post<undefined, void>(`${PIZZADAY_API_PREFIX}/delete`);
}
export const lockPizzaDay = async () => {
return await api.post<undefined, void>(`${PIZZADAY_API_PREFIX}/lock`);
}
export const unlockPizzaDay = async () => {
return await api.post<undefined, void>(`${PIZZADAY_API_PREFIX}/unlock`);
}
export const finishOrder = async () => {
return await api.post<undefined, void>(`${PIZZADAY_API_PREFIX}/finishOrder`);
}
export const finishDelivery = async (bankAccount?: string, bankAccountHolder?: string) => {
return await api.post<FinishDeliveryRequest, void>(`${PIZZADAY_API_PREFIX}/finishDelivery`, { bankAccount, bankAccountHolder });
}
export const addPizza = async (pizzaIndex: number, pizzaSizeIndex: number) => {
return await api.post<AddPizzaRequest, void>(`${PIZZADAY_API_PREFIX}/add`, { pizzaIndex, pizzaSizeIndex });
}
export const removePizza = async (pizzaOrder: PizzaVariant) => {
return await api.post<RemovePizzaRequest, void>(`${PIZZADAY_API_PREFIX}/remove`, { pizzaOrder });
}
export const updatePizzaDayNote = async (note?: string) => {
return await api.post<UpdatePizzaDayNoteRequest, void>(`${PIZZADAY_API_PREFIX}/updatePizzaDayNote`, { note });
}
export const updatePizzaFee = async (login: string, text?: string, price?: number) => {
return await api.post<UpdatePizzaFeeRequest, void>(`${PIZZADAY_API_PREFIX}/updatePizzaFee`, { login, text, price });
}

View File

@@ -1,8 +0,0 @@
import { WeeklyStats } from "../../../types";
import { api } from "./Api";
const STATS_API_PREFIX = '/api/stats';
export const getStats = async (startDate: string, endDate: string) => {
return await api.get<WeeklyStats>(`${STATS_API_PREFIX}?startDate=${startDate}&endDate=${endDate}`);
}

View File

@@ -1,12 +0,0 @@
import { FeatureRequest, UpdateFeatureVoteRequest } from "../../../types";
import { api } from "./Api";
const VOTING_API_PREFIX = '/api/voting';
export const getFeatureVotes = async () => {
return await api.get<FeatureRequest[]>(`${VOTING_API_PREFIX}/getVotes`);
}
export const updateFeatureVote = async (option: FeatureRequest, active: boolean) => {
return await api.post<UpdateFeatureVoteRequest, void>(`${VOTING_API_PREFIX}/updateVote`, { option, active });
}

View File

@@ -4,12 +4,11 @@ import { useAuth } from "../context/auth";
import SettingsModal from "./modals/SettingsModal";
import { useSettings } from "../context/settings";
import FeaturesVotingModal from "./modals/FeaturesVotingModal";
import { errorHandler } from "../api/Api";
import { getFeatureVotes, updateFeatureVote } from "../api/VotingApi";
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
import { useNavigate } from "react-router";
import { STATS_URL } from "../AppRoutes";
import { FeatureRequest } from "../../../types";
import { FeatureRequest, getVotes, refreshMenu, Restaurant, updateVote } from "../../../types";
import RefreshMenuModal from "./modals/RefreshMenuModal";
export default function Header() {
const auth = useAuth();
@@ -18,12 +17,13 @@ export default function Header() {
const [settingsModalOpen, setSettingsModalOpen] = useState<boolean>(false);
const [votingModalOpen, setVotingModalOpen] = useState<boolean>(false);
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[]>([]);
const [refreshModalOpen, setRefreshModalOpen] = useState<boolean>(false);
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[] | undefined>([]);
useEffect(() => {
if (auth?.login) {
getFeatureVotes().then(votes => {
setFeatureVotes(votes);
getVotes().then(response => {
setFeatureVotes(response.data);
})
}
}, [auth?.login]);
@@ -40,6 +40,10 @@ export default function Header() {
setPizzaModalOpen(false);
}
const closeRefreshModal = () => {
setRefreshModalOpen(false);
}
const isValidInteger = (str: string) => {
str = str.trim();
if (!str) {
@@ -99,8 +103,8 @@ export default function Header() {
}
const saveFeatureVote = async (option: FeatureRequest, active: boolean) => {
await errorHandler(() => updateFeatureVote(option, active));
const votes = [...featureVotes];
await updateVote({ body: { option, active } });
const votes = [...featureVotes || []];
if (active) {
votes.push(option);
} else {
@@ -109,6 +113,12 @@ export default function Header() {
setFeatureVotes(votes);
}
const handleRefreshMenu = async (restaurants: Restaurant[]) => {
if (restaurants.length > 0) {
await refreshMenu({ body: restaurants });
}
}
return <Navbar variant='dark' expand="lg">
<Navbar.Brand href="/">Luncher</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
@@ -119,6 +129,7 @@ export default function Header() {
<NavDropdown.Item onClick={() => setVotingModalOpen(true)}>Hlasovat o nových funkcích</NavDropdown.Item>
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
<NavDropdown.Item onClick={() => setRefreshModalOpen(true)}>Přenačíst menu</NavDropdown.Item>
<NavDropdown.Divider />
<NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item>
</NavDropdown>
@@ -127,5 +138,6 @@ export default function Header() {
<SettingsModal isOpen={settingsModalOpen} onClose={closeSettingsModal} onSave={saveSettings} />
<FeaturesVotingModal isOpen={votingModalOpen} onClose={closeVotingModal} onChange={saveFeatureVote} initialValues={featureVotes} />
<PizzaCalculatorModal isOpen={pizzaModalOpen} onClose={closePizzaModal} />
<RefreshMenuModal isOpen={refreshModalOpen} onClose={closeRefreshModal} onSubmit={handleRefreshMenu} />
</Navbar>
}

View File

@@ -1,7 +1,6 @@
import { Table } from "react-bootstrap";
import PizzaOrderRow from "./PizzaOrderRow";
import { updatePizzaFee } from "../api/PizzaDayApi";
import { PizzaDayState, PizzaOrder, PizzaVariant } from "../../../types";
import { PizzaDayState, PizzaOrder, PizzaVariant, updatePizzaFee } from "../../../types";
type Props = {
state: PizzaDayState,
@@ -12,7 +11,7 @@ type Props = {
export default function PizzaOrderList({ state, orders, onDelete, creator }: Readonly<Props>) {
const saveFees = async (customer: string, text?: string, price?: number) => {
await updatePizzaFee(customer, text, price);
await updatePizzaFee({ body: { login: customer, text, price } });
}
if (!orders?.length) {

View File

@@ -0,0 +1,53 @@
import { Modal, Button, Form } from "react-bootstrap"
import { Restaurant } from "../../../../types";
import { getRestaurantName } from "../../enums";
import { useState } from "react";
type Props = {
isOpen: boolean,
onClose: () => void,
onSubmit: (restaurants: Restaurant[]) => void,
}
/** Modální dialog pro přenačtení menu jednotlivých podniků. */
export default function RefreshMenuModal({ isOpen, onClose, onSubmit }: Readonly<Props>) {
const [restaurants, setRestaurants] = useState<Restaurant[]>([]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.currentTarget.checked) {
setRestaurants([...restaurants, e.currentTarget.value as Restaurant]);
} else {
setRestaurants(restaurants.filter(restaurant => restaurant !== e.currentTarget.value as Restaurant));
}
}
return <Modal show={isOpen} onHide={onClose} size="lg">
<Modal.Header closeButton>
<Modal.Title>
Vyberte podniky k přenačtení menu
<p style={{ fontSize: '12px' }}>Menu lze přenačíst nejdříve 15 minut od poslední aktualizace</p>
</Modal.Title>
</Modal.Header>
<Modal.Body>
{(Object.keys(Restaurant) as Array<keyof typeof Restaurant>).map(key => {
return <Form.Check
key={key}
type='checkbox'
id={key}
label={getRestaurantName(key as Restaurant)}
onChange={handleChange}
value={key}
/>
})}
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={() => onSubmit(restaurants)} disabled={restaurants.length === 0}>
Přenačíst
</Button>
<Button variant="secondary" onClick={onClose}>
Zrušit
</Button>
</Modal.Footer>
</Modal>
}

View File

@@ -1,7 +1,6 @@
import { useEffect, useState } from "react";
import { getEasterEgg } from "../api/EasterEggApi";
import { AuthContextProps } from "./auth";
import { EasterEgg } from "../../../types";
import { EasterEgg, getEasterEgg } from "../../../types";
export const useEasterEgg = (auth?: AuthContextProps | null): [EasterEgg | undefined, boolean] => {
const [result, setResult] = useState<EasterEgg | undefined>();
@@ -11,7 +10,7 @@ export const useEasterEgg = (auth?: AuthContextProps | null): [EasterEgg | undef
async function fetchEasterEgg() {
if (auth?.login) {
setLoading(true);
const egg = await getEasterEgg();
const egg = (await getEasterEgg())?.data;
setResult(egg);
setLoading(false);
}

41
client/src/enums.ts Normal file
View File

@@ -0,0 +1,41 @@
import { LunchChoice, Restaurant } from "../../types";
export function getRestaurantName(restaurant: Restaurant) {
switch (restaurant) {
case Restaurant.SLADOVNICKA:
return "Sladovnická";
case Restaurant.TECHTOWER:
return "TechTower";
case Restaurant.ZASTAVKAUMICHALA:
return "Zastávka u Michala";
case Restaurant.SENKSERIKOVA:
return "Šenk Šeříková";
default:
return restaurant;
}
}
export function getLunchChoiceName(location: LunchChoice) {
switch (location) {
case Restaurant.SLADOVNICKA:
return "Sladovnická";
case Restaurant.TECHTOWER:
return "TechTower";
case Restaurant.ZASTAVKAUMICHALA:
return "Zastávka u Michala";
case Restaurant.SENKSERIKOVA:
return "Šenk Šeříková";
case LunchChoice.SPSE:
return "SPŠE";
case LunchChoice.PIZZA:
return "Pizza day";
case LunchChoice.OBJEDNAVAM:
return "Budu objednávat";
case LunchChoice.NEOBEDVAM:
return "Mám vlastní/neobědvám";
case LunchChoice.ROZHODUJI:
return "Rozhoduji se";
default:
return location;
}
}

View File

@@ -5,6 +5,24 @@ import 'react-toastify/dist/ReactToastify.css';
import './index.css';
import AppRoutes from './AppRoutes';
import { BrowserRouter } from 'react-router';
import { client } from '../../types/gen/client.gen';
import { getToken } from './Utils';
import { toast } from 'react-toastify';
client.setConfig({
auth: () => getToken(),
baseUrl: '/api', // openapi-ts si to z nějakého důvodu neumí převzít z api.yml
});
// Interceptor na vyhození toasteru při chybě
client.interceptors.response.use(async response => {
// TODO opravit - login je zatím výjimka, voláme ho "naprázdno" abychom zjistili, zda nás nepřihlásily trusted headers
if (!response.ok && response.url.indexOf("/login") == -1) {
const json = await response.json();
toast.error(json.error, { theme: "colored" });
}
return response;
});
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement

View File

@@ -4,12 +4,12 @@ import Header from "../components/Header";
import { useAuth } from "../context/auth";
import Login from "../Login";
import { formatDate, getFirstWorkDayOfWeek, getHumanDate, getLastWorkDayOfWeek } from "../Utils";
import { getStats } from "../api/StatsApi";
import { WeeklyStats, LunchChoice } from "../../../types";
import { WeeklyStats, LunchChoice, getStats } from "../../../types";
import Loader from "../components/Loader";
import { faChevronLeft, faChevronRight, faGear } from "@fortawesome/free-solid-svg-icons";
import { Legend, Line, LineChart, Tooltip, XAxis, YAxis } from "recharts";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { getLunchChoiceName } from "../enums";
import './StatsPage.scss';
const CHART_WIDTH = 1400;
@@ -43,14 +43,15 @@ export default function StatsPage() {
// Přenačtení pro zvolený týden
useEffect(() => {
if (dateRange) {
getStats(formatDate(dateRange[0]), formatDate(dateRange[1])).then(setData);
getStats({ query: { startDate: formatDate(dateRange[0]), endDate: formatDate(dateRange[1]) } }).then(response => {
setData(response.data);
});
}
}, [dateRange]);
const renderLine = (location: LunchChoice) => {
const index = Object.values(LunchChoice).indexOf(location);
const key = Object.keys(LunchChoice)[index];
return <Line key={location} name={location} type="monotone" dataKey={data => data.locations[key] ?? 0} stroke={COLORS[index]} strokeWidth={STROKE_WIDTH} />
return <Line key={location} name={getLunchChoiceName(location)} type="monotone" dataKey={data => data.locations[location] ?? 0} stroke={COLORS[index]} strokeWidth={STROKE_WIDTH} />
}
const handlePreviousWeek = () => {

View File

@@ -54,6 +54,10 @@ app.get("/api/whoami", (req, res) => {
if (!HTTP_REMOTE_USER_ENABLED) {
res.status(403).json({ error: 'Není zapnuté přihlášení z hlaviček' });
}
if(process.env.ENABLE_HEADERS_LOGGING === 'yes'){
delete req.headers["cookie"]
console.log(req.headers)
}
res.send(req.header(HTTP_REMOTE_USER_HEADER_NAME));
})
@@ -61,11 +65,11 @@ app.post("/api/login", (req, res) => {
if (HTTP_REMOTE_USER_ENABLED) { // je rovno app.enabled('trust proxy')
// Autentizace pomocí trusted headers
const remoteUser = req.header(HTTP_REMOTE_USER_HEADER_NAME);
const remoteName = req.header('remote-name');
if (remoteUser && remoteUser.length > 0 && remoteName && remoteName.length > 0) {
res.status(200).json(generateToken(Buffer.from(remoteName, 'latin1').toString(), true));
//const remoteName = req.header('remote-name');
if (remoteUser && remoteUser.length > 0 ) {
res.status(200).json(generateToken(Buffer.from(remoteUser, 'latin1').toString(), true));
} else {
throw Error("Tohle nema nastat nekdo neco dela spatne.");
throw Error("Je zapnuto přihlášení přes hlavičky, ale nepřišla hlavička nebo ??");
}
} else {
// Klasická autentizace loginem
@@ -95,13 +99,16 @@ app.get("/api/qr", (req, res) => {
/** Middleware ověřující JWT token */
app.use("/api/", (req, res, next) => {
if (HTTP_REMOTE_USER_ENABLED) {
const userHeader = req.header(HTTP_REMOTE_USER_HEADER_NAME);
const nameHeader = req.header('remote-name');
const emailHeader = req.header('remote-email');
if (userHeader !== undefined && nameHeader !== undefined) {
const remoteName = Buffer.from(nameHeader, 'latin1').toString();
// Autentizace pomocí trusted headers
const remoteUser = req.header(HTTP_REMOTE_USER_HEADER_NAME);
if(process.env.ENABLE_HEADERS_LOGGING === 'yes'){
delete req.headers["cookie"]
console.log(req.headers)
}
if (remoteUser && remoteUser.length > 0) {
const remoteName = Buffer.from(remoteUser, 'latin1').toString();
if (ENVIRONMENT !== "production") {
console.log("Tvuj username, name a email: %s, %s, %s.", userHeader, remoteName, emailHeader);
console.log("Tvuj username: %s.", remoteName);
}
}
}

View File

@@ -1,4 +1,4 @@
import { WeeklyStats, LunchChoice } from "../../types";
import { WeeklyStats, LunchChoice } from "../../types/gen/types.gen";
// Mockovací data pro podporované podniky, na jeden týden
const MOCK_DATA = {

View File

@@ -4,7 +4,7 @@ import { generateQr } from "./qr";
import getStorage from "./storage";
import { downloadPizzy } from "./chefie";
import { getClientData, getToday, initIfNeeded } from "./service";
import { Pizza, ClientData, PizzaDayState, PizzaSize, PizzaOrder, PizzaVariant, UdalostEnum } from "../../types";
import { Pizza, ClientData, PizzaDayState, PizzaSize, PizzaOrder, PizzaVariant, UdalostEnum } from "../../types/gen/types.gen";
const storage = getStorage();

View File

@@ -2,7 +2,7 @@ import axios from "axios";
import { load } from 'cheerio';
import { getMenuSladovnickaMock, getMenuTechTowerMock, getMenuUMotlikuMock, getMenuZastavkaUmichalaMock, getMenuSenkSerikovaMock } from "./mock";
import { formatDate } from "./utils";
import { Food } from "../../types";
import { Food } from "../../types/gen/types.gen";
// Fráze v názvech jídel, které naznačují že se jedná o polévku
const SOUP_NAMES = [

View File

@@ -3,7 +3,7 @@ import { getLogin } from "../auth";
import { parseToken } from "../utils";
import path from "path";
import fs from "fs";
import { EasterEgg } from "../../../types";
import { EasterEgg } from "../../../types/gen/types.gen";
const EASTER_EGGS_JSON_PATH = path.join(__dirname, "../../.easter-eggs.json");
const IMAGES_PATH = '../../resources/easterEggs';

View File

@@ -1,10 +1,13 @@
import express, { Request } from "express";
import { getLogin, getTrusted } from "../auth";
import { addChoice, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service";
import { addChoice, getDateForWeekIndex, getRestaurantMenu, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service";
import { getDayOfWeekIndex, parseToken } from "../utils";
import { getWebsocket } from "../websocket";
import { callNotifikace } from "../notifikace";
import { AddChoiceRequest, ChangeDepartureTimeRequest, IDayIndex, RemoveChoiceRequest, RemoveChoicesRequest, UdalostEnum, UpdateNoteRequest } from "../../../types";
import { AddChoiceData, ChangeDepartureTimeData, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen";
/** Po jak dlouhé době (v minutách) lze provést nové načtení menu. */
const MENU_REFRESH_INTERVAL = 15;
/**
* Ověří a vrátí index dne v týdnu z požadavku, za předpokladu, že byl předán, a je zároveň
@@ -13,7 +16,7 @@ import { AddChoiceRequest, ChangeDepartureTimeRequest, IDayIndex, RemoveChoiceRe
* @param req request
* @returns index dne v týdnu
*/
const parseValidateFutureDayIndex = (req: Request<{}, any, IDayIndex>) => {
const parseValidateFutureDayIndex = (req: Request<{}, any, AddChoiceData["body"] | UpdateNoteData["body"]>) => {
if (req.body.dayIndex == null) {
throw Error(`Nebyl předán index dne v týdnu.`);
}
@@ -30,7 +33,7 @@ const parseValidateFutureDayIndex = (req: Request<{}, any, IDayIndex>) => {
const router = express.Router();
router.post("/addChoice", async (req: Request<{}, any, AddChoiceRequest>, res, next) => {
router.post("/addChoice", async (req: Request<{}, any, AddChoiceData["body"]>, res, next) => {
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
let date = undefined;
@@ -50,7 +53,7 @@ router.post("/addChoice", async (req: Request<{}, any, AddChoiceRequest>, res, n
} catch (e: any) { next(e) }
});
router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesRequest>, res, next) => {
router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesData["body"]>, res, next) => {
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
let date = undefined;
@@ -70,7 +73,7 @@ router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesRequest>
} catch (e: any) { next(e) }
});
router.post("/removeChoice", async (req: Request<{}, any, RemoveChoiceRequest>, res, next) => {
router.post("/removeChoice", async (req: Request<{}, any, RemoveChoiceData["body"]>, res, next) => {
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
let date = undefined;
@@ -90,7 +93,7 @@ router.post("/removeChoice", async (req: Request<{}, any, RemoveChoiceRequest>,
} catch (e: any) { next(e) }
});
router.post("/updateNote", async (req: Request<{}, any, UpdateNoteRequest>, res, next) => {
router.post("/updateNote", async (req: Request<{}, any, UpdateNoteData["body"]>, res, next) => {
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
const note = req.body.note;
@@ -114,7 +117,7 @@ router.post("/updateNote", async (req: Request<{}, any, UpdateNoteRequest>, res,
} catch (e: any) { next(e) }
});
router.post("/changeDepartureTime", async (req: Request<{}, any, ChangeDepartureTimeRequest>, res, next) => {
router.post("/changeDepartureTime", async (req: Request<{}, any, ChangeDepartureTimeData["body"]>, res, next) => {
const login = getLogin(parseToken(req));
let date = undefined;
if (req.body.dayIndex != null) {
@@ -141,4 +144,25 @@ router.post("/jdemeObed", async (req, res, next) => {
} catch (e: any) { next(e) }
});
router.post("/refreshMenu", async (req, res, next) => {
if (!req.body || !Array.isArray(req.body)) {
return res.status(400).json({ error: "Neplatný požadavek" });
}
try {
const now = new Date();
for (const restaurant of req.body) {
// TODO tohle je technicky špatně, protože pokud aktuálně jídla načtená nejsou, tak je toto volání načte a následně je to načte znovu kvůli force!
const menu = await getRestaurantMenu(restaurant);
if (menu.lastUpdate != null) {
const minutes = (now.getTime() - menu.lastUpdate) / 1000 / 60;
if (minutes < MENU_REFRESH_INTERVAL) {
throw Error(`Podnik ${restaurant} byl přenačtený před ${Math.round(minutes)} minutami. Nové přenačtení lze provést nejdříve za ${Math.round(MENU_REFRESH_INTERVAL - minutes)} minut.`);
}
}
await getRestaurantMenu(restaurant, undefined, true);
}
res.status(200).json({});
} catch (e: any) { next(e) }
});
export default router;

View File

@@ -3,7 +3,7 @@ import { getLogin } from "../auth";
import { createPizzaDay, deletePizzaDay, getPizzaList, addPizzaOrder, removePizzaOrder, lockPizzaDay, unlockPizzaDay, finishPizzaOrder, finishPizzaDelivery, updatePizzaDayNote, updatePizzaFee } from "../pizza";
import { parseToken } from "../utils";
import { getWebsocket } from "../websocket";
import { AddPizzaRequest, FinishDeliveryRequest, RemovePizzaRequest, UpdatePizzaDayNoteRequest, UpdatePizzaFeeRequest } from "../../../types";
import { AddPizzaData, FinishDeliveryData, RemovePizzaData, UpdatePizzaDayNoteData, UpdatePizzaFeeData } from "../../../types";
const router = express.Router();
@@ -22,7 +22,7 @@ router.post("/delete", async (req: Request<{}, any, undefined>, res) => {
getWebsocket().emit("message", data);
});
router.post("/add", async (req: Request<{}, any, AddPizzaRequest>, res) => {
router.post("/add", async (req: Request<{}, any, AddPizzaData["body"]>, res) => {
const login = getLogin(parseToken(req));
if (isNaN(req.body?.pizzaIndex)) {
throw Error("Nebyl předán index pizzy");
@@ -47,7 +47,7 @@ router.post("/add", async (req: Request<{}, any, AddPizzaRequest>, res) => {
res.status(200).json({});
});
router.post("/remove", async (req: Request<{}, any, RemovePizzaRequest>, res) => {
router.post("/remove", async (req: Request<{}, any, RemovePizzaData["body"]>, res) => {
const login = getLogin(parseToken(req));
if (!req.body?.pizzaOrder) {
throw Error("Nebyla předána objednávka");
@@ -78,14 +78,14 @@ router.post("/finishOrder", async (req: Request<{}, any, undefined>, res) => {
res.status(200).json({});
});
router.post("/finishDelivery", async (req: Request<{}, any, FinishDeliveryRequest>, res) => {
router.post("/finishDelivery", async (req: Request<{}, any, FinishDeliveryData["body"]>, res) => {
const login = getLogin(parseToken(req));
const data = await finishPizzaDelivery(login, req.body.bankAccount, req.body.bankAccountHolder);
getWebsocket().emit("message", data);
res.status(200).json({});
});
router.post("/updatePizzaDayNote", async (req: Request<{}, any, UpdatePizzaDayNoteRequest>, res, next) => {
router.post("/updatePizzaDayNote", async (req: Request<{}, any, UpdatePizzaDayNoteData["body"]>, res, next) => {
const login = getLogin(parseToken(req));
try {
if (req.body.note && req.body.note.length > 70) {
@@ -97,7 +97,7 @@ router.post("/updatePizzaDayNote", async (req: Request<{}, any, UpdatePizzaDayNo
} catch (e: any) { next(e) }
});
router.post("/updatePizzaFee", async (req: Request<{}, any, UpdatePizzaFeeRequest>, res, next) => {
router.post("/updatePizzaFee", async (req: Request<{}, any, UpdatePizzaFeeData["body"]>, res, next) => {
const login = getLogin(parseToken(req));
if (!req.body.login) {
return res.status(400).json({ error: "Nebyl předán login cílového uživatele" });

View File

@@ -2,7 +2,7 @@ import express, { Request, Response } from "express";
import { getLogin } from "../auth";
import { parseToken } from "../utils";
import { getStats } from "../stats";
import { WeeklyStats } from "../../../types";
import { WeeklyStats } from "../../../types/gen/types.gen";
const router = express.Router();

View File

@@ -1,18 +1,18 @@
import express, { Request, Response } from "express";
import express, { Request } from "express";
import { getLogin } from "../auth";
import { parseToken } from "../utils";
import { getUserVotes, updateFeatureVote } from "../voting";
import { FeatureRequest, UpdateFeatureVoteRequest } from "../../../types";
import { GetVotesData, UpdateVoteData } from "../../../types";
const router = express.Router();
router.get("/getVotes", async (req: Request<{}, any, undefined>, res: Response<FeatureRequest[]>) => {
router.get("/getVotes", async (req: Request<{}, any, GetVotesData["body"]>, res) => {
const login = getLogin(parseToken(req));
const data = await getUserVotes(login);
res.status(200).json(data);
});
router.post("/updateVote", async (req: Request<{}, any, UpdateFeatureVoteRequest>, res, next) => {
router.post("/updateVote", async (req: Request<{}, any, UpdateVoteData["body"]>, res, next) => {
const login = getLogin(parseToken(req));
if (req.body?.option == null || req.body?.active == null) {
res.status(400).json({ error: "Chybné parametry volání" });

View File

@@ -2,7 +2,7 @@ import { InsufficientPermissions, formatDate, getDayOfWeekIndex, getFirstWorkDay
import getStorage from "./storage";
import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova } from "./restaurants";
import { getTodayMock } from "./mock";
import { ClientData, DepartureTime, LunchChoice, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types";
import { ClientData, DepartureTime, LunchChoice, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types/gen/types.gen";
const storage = getStorage();
const MENU_PREFIX = 'menu';
@@ -78,13 +78,13 @@ async function getMenu(date: Date): Promise<WeekMenu | undefined> {
// TODO přesun do restaurants.ts
/**
* Vrátí menu dané restaurace pro předaný den.
* Pokud neexistuje, provede stažení menu pro příslušný týden a uložení do DB.
* Pokud neexistuje nebo je nastaven příznak force, provede stažení menu pro příslušný týden a uložení do DB.
*
* @param restaurant restaurace
* @param date datum, ke kterému získat menu
* @param mock příznak, zda chceme pouze mock data
* @param force Příznak, zda znovu získat aktuální menu i v případě, že je již načteno. Pokud není předán, provede se načtení pouze v případě, že menu aktuálně nemáme. Pokud je true, provede nové načtení. Pokud je false, neprovede se nové načtení ani v případě, že menu aktuálně nemáme.
*/
export async function getRestaurantMenu(restaurant: keyof typeof Restaurant, date?: Date): Promise<RestaurantDayMenu> {
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, force?: boolean): Promise<RestaurantDayMenu> {
const usedDate = date ?? getToday();
const dayOfWeekIndex = getDayOfWeekIndex(usedDate);
const now = new Date().getTime();
@@ -112,7 +112,8 @@ export async function getRestaurantMenu(restaurant: keyof typeof Restaurant, dat
};
}
}
if (!weekMenu[dayOfWeekIndex][restaurant]?.food?.length) {
if ((!weekMenu[dayOfWeekIndex][restaurant]?.food?.length && force === undefined) || force) {
const firstDay = getFirstWorkDayOfWeek(usedDate);
const mock = process.env.MOCK_DATA === 'true';
switch (restaurant) {
@@ -210,7 +211,7 @@ export async function initIfNeeded(date?: Date) {
* @param date datum, ke kterému se volba vztahuje
* @returns
*/
export async function removeChoices(login: string, trusted: boolean, locationKey: keyof typeof LunchChoice, date?: Date) {
export async function removeChoices(login: string, trusted: boolean, locationKey: LunchChoice, date?: Date) {
const selectedDay = formatDate(date ?? getToday());
let data = await getClientData(date);
validateTrusted(data, login, trusted);
@@ -237,14 +238,14 @@ export async function removeChoices(login: string, trusted: boolean, locationKey
* @param date datum, ke kterému se volba vztahuje
* @returns
*/
export async function removeChoice(login: string, trusted: boolean, locationKey: keyof typeof LunchChoice, foodIndex: number, date?: Date) {
export async function removeChoice(login: string, trusted: boolean, locationKey: LunchChoice, foodIndex: number, date?: Date) {
const selectedDay = formatDate(date ?? getToday());
let data = await getClientData(date);
validateTrusted(data, login, trusted);
if (locationKey in data.choices) {
if (data.choices[locationKey] && login in data.choices[locationKey]) {
const index = data.choices[locationKey][login].selectedFoods?.indexOf(foodIndex);
if (index && index > -1) {
if (index != null && index > -1) {
data.choices[locationKey][login].selectedFoods?.splice(index, 1);
await storage.setData(selectedDay, data);
}
@@ -260,11 +261,11 @@ export async function removeChoice(login: string, trusted: boolean, locationKey:
* @param date datum, ke kterému se volby vztahují
* @param ignoredLocationKey volba, která nebude odstraněna, pokud existuje
*/
async function removeChoiceIfPresent(login: string, date?: Date, ignoredLocationKey?: keyof typeof LunchChoice) {
async function removeChoiceIfPresent(login: string, date?: Date, ignoredLocationKey?: LunchChoice) {
const usedDate = date ?? getToday();
let data = await getClientData(usedDate);
for (const key of Object.keys(data.choices)) {
const locationKey = key as keyof typeof LunchChoice;
const locationKey = key as LunchChoice;
if (ignoredLocationKey != null && ignoredLocationKey == locationKey) {
continue;
}
@@ -312,7 +313,7 @@ function validateTrusted(data: ClientData, login: string, trusted: boolean) {
* @param date datum, ke kterému se volba vztahuje
* @returns aktuální data
*/
export async function addChoice(login: string, trusted: boolean, locationKey: keyof typeof LunchChoice, foodIndex?: number, date?: Date) {
export async function addChoice(login: string, trusted: boolean, locationKey: LunchChoice, foodIndex?: number, date?: Date) {
const usedDate = date ?? getToday();
await initIfNeeded(usedDate);
let data = await getClientData(usedDate);
@@ -353,7 +354,7 @@ export async function addChoice(login: string, trusted: boolean, locationKey: ke
* @param foodIndex index jídla pro danou lokalitu
* @param date datum, pro které je validace prováděna
*/
async function validateFoodIndex(locationKey: keyof typeof LunchChoice, foodIndex?: number, date?: Date) {
async function validateFoodIndex(locationKey: LunchChoice, foodIndex?: number, date?: Date) {
if (foodIndex != null) {
if (typeof foodIndex !== 'number') {
throw Error(`Neplatný index ${foodIndex} typu ${typeof foodIndex}`);
@@ -365,7 +366,7 @@ async function validateFoodIndex(locationKey: keyof typeof LunchChoice, foodInde
throw Error(`Neplatný index ${foodIndex} pro lokalitu ${locationKey} nepodporující indexy`);
}
const usedDate = date ?? getToday();
const menu = await getRestaurantMenu(locationKey as keyof typeof Restaurant, usedDate);
const menu = await getRestaurantMenu(locationKey as Restaurant, usedDate);
if (menu.food?.length && foodIndex > (menu.food.length - 1)) {
throw new Error(`Neplatný index ${foodIndex} pro lokalitu ${locationKey}`);
}

View File

@@ -1,4 +1,4 @@
import { DailyStats, LunchChoice, WeeklyStats } from "../../types";
import { DailyStats, LunchChoice, WeeklyStats } from "../../types/gen/types.gen";
import { getStatsMock } from "./mock";
import { getClientData } from "./service";
import getStorage from "./storage";
@@ -39,7 +39,7 @@ export async function getStats(startDate: string, endDate: string): Promise<Week
locationsStats.locations = {}
}
// TODO dořešit, tohle je zmatek a té hlášce Sonaru nerozumím
locationsStats.locations[locationKey as keyof typeof LunchChoice] = Object.keys(data.choices[locationKey as keyof typeof LunchChoice]!).length;
locationsStats.locations[locationKey as LunchChoice] = Object.keys(data.choices[locationKey as LunchChoice]!).length;
})
}
result.push(locationsStats);

View File

@@ -1,4 +1,4 @@
import { LunchChoice, LunchChoices } from "../../types";
import { LunchChoice, LunchChoices } from "../../types/gen/types.gen";
const DAY_OF_WEEK_FORMAT = new Intl.DateTimeFormat(undefined, { weekday: 'long' });
@@ -118,7 +118,7 @@ export const getUsersByLocation = (choices: LunchChoices, login?: string): strin
const result: string[] = [];
for (const location of Object.entries(choices)) {
const locationKey = location[0] as keyof typeof LunchChoice;
const locationKey = location[0] as LunchChoice;
const locationValue = location[1];
if (login && locationValue[login]) {
for (const username in choices[locationKey]) {

View File

@@ -1,4 +1,4 @@
import { FeatureRequest } from "../../types";
import { FeatureRequest } from "../../types/gen/types.gen";
import getStorage from "./storage";
interface VotingData {

View File

@@ -1,56 +0,0 @@
import { FeatureRequest, LunchChoice, PizzaVariant } from "../types";
export type ILocationKey = {
locationKey: keyof typeof LunchChoice,
}
export type IDayIndex = {
dayIndex?: number,
}
export type AddChoiceRequest = IDayIndex & ILocationKey & {
foodIndex?: number,
}
export type RemoveChoicesRequest = IDayIndex & ILocationKey;
export type RemoveChoiceRequest = IDayIndex & ILocationKey & {
foodIndex: number,
}
export type UpdateNoteRequest = IDayIndex & {
note?: string,
}
export type ChangeDepartureTimeRequest = IDayIndex & {
time: string,
}
export type FinishDeliveryRequest = {
bankAccount?: string,
bankAccountHolder?: string,
}
export type AddPizzaRequest = {
pizzaIndex: number,
pizzaSizeIndex: number,
}
export type RemovePizzaRequest = {
pizzaOrder: PizzaVariant,
}
export type UpdatePizzaDayNoteRequest = {
note?: string,
}
export type UpdatePizzaFeeRequest = {
login: string,
text?: string,
price?: number,
}
export type UpdateFeatureVoteRequest = {
option: FeatureRequest,
active: boolean,
}

View File

@@ -5,642 +5,78 @@ info:
servers:
- url: /api
paths:
# Obecné (/api)
/login:
post:
summary: Přihlášení uživatele
security: [] # Nevyžaduje autentizaci
requestBody:
content:
application/json:
schema:
type: object
properties:
login:
type: string
description: Přihlašovací jméno uživatele. Vyžadováno pouze pokud není předáno pomocí hlaviček.
responses:
"200":
description: Přihlášení bylo úspěšné
content:
application/json:
schema:
$ref: "#/components/schemas/JWTToken"
$ref: "./paths/login.yml"
/qr:
get:
summary: Získání QR kódu pro platbu za Pizza day
security: [] # Nevyžaduje autentizaci
parameters:
- in: query
name: login
schema:
type: string
required: true
description: Přihlašovací jméno uživatele, pro kterého bude vrácen QR kód
responses:
"200":
description: Vygenerovaný QR kód pro platbu
content:
image/png:
schema:
type: string
format: binary
$ref: "./paths/getPizzaQr.yml"
/data:
get:
summary: Načtení klientských dat pro aktuální nebo předaný den
parameters:
- in: query
name: dayIndex
description: Index dne v týdnu. Pokud není předán, je použit aktuální den.
schema:
type: integer
minimum: 0
maximum: 4
responses:
"200":
$ref: "#/components/responses/ClientDataResponse"
/addChoice:
post:
summary: Přidání či nahrazení volby uživatele pro zvolený den/podnik
requestBody:
required: true
content:
application/json:
schema:
required:
- locationKey
allOf:
- locationKey:
$ref: "#/components/schemas/LunchChoice"
- dayIndex:
$ref: "#/components/schemas/DayIndex"
- foodIndex:
$ref: "#/components/schemas/FoodIndex"
responses:
"200":
$ref: "#/components/responses/ClientDataResponse"
/removeChoices:
post:
summary: Odstranění volby uživatele pro zvolený den/podnik, včetně případných jídel
requestBody:
required: true
content:
application/json:
schema:
required:
- locationKey
allOf:
- locationKey:
$ref: "#/components/schemas/LunchChoice"
- dayIndex:
$ref: "#/components/schemas/DayIndex"
responses:
"200":
$ref: "#/components/responses/ClientDataResponse"
$ref: "./paths/getData.yml"
# Restaurace a jídla (/api/food)
/food/addChoice:
$ref: "./paths/food/addChoice.yml"
/food/removeChoice:
$ref: "./paths/food/removeChoice.yml"
/food/updateNote:
$ref: "./paths/food/updateNote.yml"
/food/removeChoices:
$ref: "./paths/food/removeChoices.yml"
/food/changeDepartureTime:
$ref: "./paths/food/changeDepartureTime.yml"
/food/jdemeObed:
$ref: "./paths/food/jdemeObed.yml"
/food/refreshMenu:
$ref: "./paths/food/refreshMenu.yml"
# Pizza day (/api/pizzaDay)
/pizzaDay/create:
$ref: "./paths/pizzaDay/create.yml"
/pizzaDay/delete:
$ref: "./paths/pizzaDay/delete.yml"
/pizzaDay/lock:
$ref: "./paths/pizzaDay/lock.yml"
/pizzaDay/unlock:
$ref: "./paths/pizzaDay/unlock.yml"
/pizzaDay/finishOrder:
$ref: "./paths/pizzaDay/finishOrder.yml"
/pizzaDay/finishDelivery:
$ref: "./paths/pizzaDay/finishDelivery.yml"
/pizzaDay/add:
$ref: "./paths/pizzaDay/addPizza.yml"
/pizzaDay/remove:
$ref: "./paths/pizzaDay/removePizza.yml"
/pizzaDay/updatePizzaDayNote:
$ref: "./paths/pizzaDay/updatePizzaDayNote.yml"
/pizzaDay/updatePizzaFee:
$ref: "./paths/pizzaDay/updatePizzaFee.yml"
# Easter eggy (/api/easterEggs)
/easterEggs:
$ref: "./paths/easterEggs/easterEggs.yml"
/easterEggs/{url}:
$ref: "./paths/easterEggs/easterEgg.yml"
# Statistiky (/api/stats)
/stats:
$ref: "./paths/stats/stats.yml"
# Hlasování (/api/voting)
/voting/getVotes:
$ref: "./paths/voting/getVotes.yml"
/voting/updateVote:
$ref: "./paths/voting/updateVote.yml"
components:
schemas:
# --- OBECNÉ ---
JWTToken:
type: object
description: Klientský JWT token pro autentizaci a autorizaci
required:
- login
- trusted
- iat
properties:
login:
type: string
description: Přihlašovací jméno uživatele
trusted:
type: boolean
description: Příznak, zda se jedná o uživatele ověřeného doménovým přihlášením
iat:
type: number
description: Časové razítko vydání tokenu
ClientData:
description: Klientská data pro jeden konkrétní den. Obsahuje menu všech načtených podniků a volby jednotlivých uživatelů.
type: object
additionalProperties: false
required:
- todayDayIndex
- date
- isWeekend
- choices
properties:
todayDayIndex:
description: Index dnešního dne v týdnu
$ref: "#/components/schemas/DayIndex"
date:
description: Human-readable datum dne
type: string
isWeekend:
description: Příznak, zda je tento den víkend
type: boolean
dayIndex:
description: Index dne v týdnu, ke kterému se vztahují tato data
$ref: "#/components/schemas/DayIndex"
choices:
$ref: "#/components/schemas/LunchChoices"
menus:
$ref: "#/components/schemas/RestaurantDayMenuMap"
pizzaDay:
$ref: "#/components/schemas/PizzaDay"
pizzaList:
description: Seznam dostupných pizz pro předaný den
type: array
items:
$ref: "#/components/schemas/Pizza"
pizzaListLastUpdate:
description: Datum a čas poslední aktualizace pizz
type: string
format: date-time
# --- OBĚDY ---
UserLunchChoice:
description: Konkrétní volba stravování jednoho uživatele v konkrétní den. Může se jednat jak o stravovací podnik, tak možnosti "budu objednávat", "neobědvám" apod.
additionalProperties: false
properties:
# TODO toto je tu z dost špatného důvodu, viz použití - mělo by se místo toho z loginu zjišťovat zda je uživatel trusted
trusted:
description: Příznak, zda byla tato volba provedena uživatelem ověřeným doménovým přihlášením
type: boolean
selectedFoods:
description: Pole indexů vybraných jídel v rámci dané restaurace. Index představuje pořadí jídla v menu dané restaurace.
type: array
items:
type: integer
departureTime:
description: Čas preferovaného odchodu do dané restaurace v human-readable formátu (např. 12:00)
type: string
note:
description: Volitelná, veřejně viditelná uživatelská poznámka k vybrané volbě
type: string
LocationLunchChoicesMap:
description: Objekt, kde klíčem je možnost stravování ((#/components/schemas/LunchChoice)) a hodnotou množina uživatelů s touto volbou ((#/components/schemas/LunchChoices)).
type: object
additionalProperties:
$ref: "#/components/schemas/UserLunchChoice"
LunchChoices:
description: Objekt, představující volby všech uživatelů pro konkrétní den. Klíčem je (#/components/schemas/LunchChoice).
type: object
properties:
SLADOVNICKA:
$ref: "#/components/schemas/LocationLunchChoicesMap"
TECHTOWER:
$ref: "#/components/schemas/LocationLunchChoicesMap"
ZASTAVKAUMICHALA:
$ref: "#/components/schemas/LocationLunchChoicesMap"
SENKSERIKOVA:
$ref: "#/components/schemas/LocationLunchChoicesMap"
SPSE:
$ref: "#/components/schemas/LocationLunchChoicesMap"
PIZZA:
$ref: "#/components/schemas/LocationLunchChoicesMap"
OBJEDNAVAM:
$ref: "#/components/schemas/LocationLunchChoicesMap"
NEOBEDVAM:
$ref: "#/components/schemas/LocationLunchChoicesMap"
ROZHODUJI:
$ref: "#/components/schemas/LocationLunchChoicesMap"
Restaurant:
description: Stravovací zařízení (restaurace, jídelna, hospoda, ...)
type: string
enum:
- Sladovnická
- TechTower
- Zastávka u Michala
- Šenk Šeříková
x-enum-varnames:
- SLADOVNICKA
- TECHTOWER
- ZASTAVKAUMICHALA
- SENKSERIKOVA
LunchChoice:
description: Konkrétní možnost stravování (konkrétní restaurace, pizza day, objednání, neobědvání, rozhodování se, ...)
type: string
enum:
- Sladovnická
- TechTower
- Zastávka u Michala
- Šenk Šeříková
- SPŠE
- Pizza day
- Budu objednávat
- Neobědvám
- Rozhoduji se
x-enum-varnames:
- SLADOVNICKA
- TECHTOWER
- ZASTAVKAUMICHALA
- SENKSERIKOVA
- SPSE
- PIZZA
- OBJEDNAVAM
- NEOBEDVAM
- ROZHODUJI
DayIndex:
description: Index dne v týdnu (0 = pondělí, 4 = pátek)
type: integer
minimum: 0
maximum: 4
FoodIndex:
description: Pořadový index jídla v menu konkrétní restaurace
type: integer
minimum: 0
Food:
description: Konkrétní jídlo z menu restaurace
type: object
additionalProperties: false
required:
- name
- isSoup
properties:
amount:
description: Množství standardní porce, např. 0,33l nebo 150g
type: string
name:
description: Název/popis jídla
type: string
price:
description: Cena ve formátu '135 Kč'
type: string
isSoup:
description: Příznak, zda se jedná o polévku
type: boolean
RestaurantDayMenu:
description: Menu restaurace na konkrétní den
type: object
additionalProperties: false
properties:
lastUpdate:
description: UNIX timestamp poslední aktualizace menu
type: integer
closed:
description: Příznak, zda je daný podnik v daný den zavřený
type: boolean
food:
description: Seznam jídel pro daný den
type: array
items:
$ref: "#/components/schemas/Food"
RestaurantDayMenuMap:
description: Objekt, kde klíčem je podnik ((#/components/schemas/Restaurant)) a hodnotou denní menu daného podniku ((#/components/schemas/RestaurantDayMenu))
type: object
additionalProperties: false
properties:
SLADOVNICKA:
$ref: "#/components/schemas/RestaurantDayMenu"
TECHTOWER:
$ref: "#/components/schemas/RestaurantDayMenu"
ZASTAVKAUMICHALA:
$ref: "#/components/schemas/RestaurantDayMenu"
SENKSERIKOVA:
$ref: "#/components/schemas/RestaurantDayMenu"
WeekMenu:
description: Pole týdenních menu jednotlivých podniků. Indexem je den v týdnu (0 = pondělí, 4 = pátek), hodnotou denní menu daného podniku.
type: array
minItems: 5
maxItems: 5
items:
$ref: "#/components/schemas/RestaurantDayMenuMap"
DepartureTime:
description: Preferovaný čas odchodu na oběd
type: string
enum:
- "10:00"
- "10:15"
- "10:30"
- "10:45"
- "11:00"
- "11:15"
- "11:30"
- "11:45"
- "12:00"
- "12:15"
- "12:30"
- "12:45"
- "13:00"
x-enum-varnames:
- T10_00
- T10_15
- T10_30
- T10_45
- T11_00
- T11_15
- T11_30
- T11_45
- T12_00
- T12_15
- T12_30
- T12_45
- T13_00
# --- HLASOVÁNÍ ---
FeatureRequest:
type: string
enum:
- Ruční generování QR kódů mimo Pizza day (např. při objednávání)
- Možnost označovat si jídla jako oblíbená (taková jídla by se uživateli následně zvýrazňovala)
- Možnost úhrady v podniku za všechny jednou osobou a následné generování QR ostatním
- Zrušení \"užívejte víkend\", místo toho umožnit zpětně náhled na uplynulý týden
- Umožnění zobrazení vygenerovaného QR kódu i po následující dny (dokud ho uživatel ručně \"nezavře\", např. tlačítkem \"Zaplatil jsem\")
- Zobrazování náhledů (fotografií) pizz v rámci Pizza day
- Statistiky (nejoblíbenější podnik, nejpopulárnější jídla, nejobjednávanější pizzy, nejčastější uživatelé, ...)
- Vylepšení responzivního designu
- Zvýšení zabezpečení aplikace
- Zvýšená ochrana proti chybám uživatele (potvrzovací dialogy, překliky, ...)
- Celkové vylepšení UI/UX
- Zlepšení dokumentace/postupů pro ostatní vývojáře
x-enum-varnames:
- CUSTOM_QR
- FAVORITES
- SINGLE_PAYMENT
- NO_WEEKENDS
- QR_FOREVER
- PIZZA_PICTURES
- STATISTICS
- RESPONSIVITY
- SECURITY
- SAFETY
- UI
- DEVELOPMENT
# --- EASTER EGGS ---
EasterEgg:
description: Data pro zobrazení easter eggů
type: object
additionalProperties: false
required:
- path
- url
- startOffset
- endOffset
- duration
properties:
path:
type: string
url:
type: string
startOffset:
type: number
endOffset:
type: number
duration:
type: number
width:
type: string
zIndex:
type: integer
position:
type: string
enum:
- absolute
animationName:
type: string
animationDuration:
type: string
animationTimingFunction:
type: string
# --- STATISTIKY ---
LocationStats:
description: Objekt, kde klíčem je zvolená možnost a hodnotou počet uživatelů, kteří tuto možnosti zvolili
type: object
additionalProperties: false
properties:
# Bohužel OpenAPI neumí nadefinovat objekt, kde klíčem může být pouze hodnota existujícího enumu :(
SLADOVNICKA:
type: number
TECHTOWER:
type: number
ZASTAVKAUMICHALA:
type: number
SENKSERIKOVA:
type: number
SPSE:
type: number
PIZZA:
type: number
OBJEDNAVAM:
type: number
NEOBEDVAM:
type: number
ROZHODUJI:
type: number
DailyStats:
description: Statistika vybraných možností pro jeden konkrétní den
type: object
additionalProperties: false
required:
- date
- locations
properties:
date:
description: Datum v human-readable formátu
type: string
locations:
$ref: "#/components/schemas/LocationStats"
WeeklyStats:
description: Pole statistik vybraných možností pro jeden konkrétní týden. Index představuje den v týdnu (0 = pondělí, 4 = pátek)
type: array
minItems: 5
maxItems: 5
items:
$ref: "#/components/schemas/DailyStats"
# --- PIZZA DAY ---
PizzaDayState:
description: Stav pizza day
type: string
enum:
- Pizza day nebyl založen
- Pizza day je založen
- Objednávky uzamčeny
- Pizzy objednány
- Pizzy doručeny
x-enum-varnames:
- NOT_CREATED
- CREATED
- LOCKED
- ORDERED
- DELIVERED
# TODO toto je jen rozšířená varianta PizzaVariant - sloučit do jednoho objektu
PizzaSize:
description: Údaje o konkrétní variantě pizzy
type: object
additionalProperties: false
required:
- varId
- size
- pizzaPrice
- boxPrice
- price
properties:
varId:
description: Unikátní identifikátor varianty pizzy
type: integer
size:
description: Velikost pizzy, např. "30cm"
type: string
pizzaPrice:
description: Cena samotné pizzy v Kč
type: number
boxPrice:
description: Cena krabice pizzy v Kč
type: number
price:
description: Celková cena (pizza + krabice)
type: number
Pizza:
description: Údaje o konkrétní pizze.
type: object
additionalProperties: false
required:
- name
- ingredients
- sizes
properties:
name:
description: Název pizzy
type: string
ingredients:
description: Seznam obsažených ingrediencí
type: array
items:
type: string
sizes:
description: Dostupné velikosti pizzy
type: array
items:
$ref: "#/components/schemas/PizzaSize"
PizzaVariant:
description: Konkrétní varianta (velikost) jedné pizzy.
type: object
additionalProperties: false
required:
- varId
- name
- size
- price
properties:
varId:
description: Unikátní identifikátor varianty pizzy
type: integer
name:
description: Název pizzy
type: string
size:
description: Velikost pizzy (např. "30cm")
type: string
price:
description: Cena pizzy v Kč, včetně krabice
type: number
PizzaOrder:
description: Údaje o objednávce pizzy jednoho uživatele.
type: object
additionalProperties: false
required:
- customer
- totalPrice
- hasQr
properties:
customer:
description: Jméno objednávajícího uživatele
type: string
pizzaList:
description: Seznam variant pizz k objednání (typicky bývá jen jedna)
type: array
items:
$ref: "#/components/schemas/PizzaVariant"
fee:
description: Příplatek (např. za extra ingredience)
type: object
properties:
text:
description: Popis příplatku (např. "kuřecí maso navíc")
type: string
price:
description: Cena příplatku v Kč
type: number
totalPrice:
description: Celková cena všech objednaných pizz daného uživatele, včetně krabic a příplatků
type: number
hasQr:
description: |
Příznak, pokud je k této objednávce vygenerován QR kód pro platbu. To je typicky pravda, pokud:
- objednávající má v nastavení vyplněno číslo účtu
- pizza day je ve stavu DELIVERED (Pizzy byly doručeny)
note:
description: Volitelná uživatelská poznámka pro objednávajícího (např. "bez oliv")
type: string
PizzaDay:
description: Data o Pizza day pro konkrétní den
type: object
additionalProperties: false
properties:
state:
$ref: "#/components/schemas/PizzaDayState"
creator:
description: "Jméno zakladatele pizza day"
type: string
orders:
description: Pole objednávek jednotlivých uživatelů
type: array
items:
$ref: "#/components/schemas/PizzaOrder"
# --- NOTIFIKACE ---
UdalostEnum:
type: string
enum:
- Zahájen pizza day
- Objednána pizza
- Jdeme na oběd
x-enum-varnames:
- ZAHAJENA_PIZZA
- OBJEDNANA_PIZZA
- JDEME_NA_OBED
NotifikaceInput:
type: object
required:
- udalost
- user
properties:
udalost:
$ref: "#/components/schemas/UdalostEnum"
user:
type: string
NotifikaceData:
type: object
required:
- input
properties:
input:
$ref: "#/components/schemas/NotifikaceInput"
gotify:
type: boolean
teams:
type: boolean
ntfy:
type: boolean
GotifyServer:
type: object
required:
- server
- api_keys
properties:
server:
type: string
api_keys:
type: array
items:
type: string
$ref: "./schemas/_index.yml"
responses:
ClientDataResponse:
description: Aktuální data pro klienta
content:
application/json:
schema:
$ref: "#/components/schemas/ClientData"
$ref: "./schemas/_index.yml#/ClientData"
securitySchemes:
bearerAuth:
type: http

View File

@@ -1,2 +1 @@
export * from './RequestTypes';
export * from './gen';

View File

@@ -0,0 +1,18 @@
get:
operationId: getEasterEggImage
summary: Vrátí obrázek konkrétního easter eggu
parameters:
- in: path
name: url
required: true
schema:
type: string
description: URL easter eggu
responses:
"200":
content:
image/png:
description: Obrázek easter eggu
schema:
type: string
format: binary

View File

@@ -0,0 +1,9 @@
get:
operationId: getEasterEgg
summary: Vrátí náhodně metadata jednoho z definovaných easter egg obrázků pro přihlášeného uživatele, nebo nic, pokud žádné definované nemá.
responses:
"200":
content:
application/json:
schema:
$ref: "../../schemas/_index.yml#/EasterEgg"

View File

@@ -0,0 +1,20 @@
post:
operationId: addChoice
summary: Přidání či nahrazení volby uživatele pro zvolený den/podnik
requestBody:
required: true
content:
application/json:
schema:
required:
- locationKey
properties:
locationKey:
$ref: "../../schemas/_index.yml#/LunchChoice"
dayIndex:
$ref: "../../schemas/_index.yml#/DayIndex"
foodIndex:
$ref: "../../schemas/_index.yml#/FoodIndex"
responses:
"200":
$ref: "../../api.yml#/components/responses/ClientDataResponse"

View File

@@ -0,0 +1,16 @@
post:
operationId: changeDepartureTime
summary: Úprava preferovaného času odchodu do aktuálně zvoleného podniku.
requestBody:
required: true
content:
application/json:
schema:
properties:
dayIndex:
$ref: "../../schemas/_index.yml#/DayIndex"
time:
$ref: "../../schemas/_index.yml#/DepartureTime"
responses:
"200":
$ref: "../../api.yml#/components/responses/ClientDataResponse"

View File

@@ -0,0 +1,6 @@
post:
operationId: jdemeObed
summary: Odeslání notifikací "jdeme na oběd" dle konfigurace.
responses:
"200":
description: Notifikace byly odeslány.

View File

@@ -0,0 +1,15 @@
post:
operationId: refreshMenu
summary: Přenačtení menu vybraných podniků
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: "../../schemas/_index.yml#/Restaurant"
responses:
"200":
description: Menu bylo přenačteno

View File

@@ -0,0 +1,21 @@
post:
operationId: removeChoice
summary: Odstranění jednoho zvoleného jídla uživatele pro zvolený den/podnik
requestBody:
required: true
content:
application/json:
schema:
required:
- foodIndex
- locationKey
properties:
foodIndex:
$ref: "../../schemas/_index.yml#/FoodIndex"
locationKey:
$ref: "../../schemas/_index.yml#/LunchChoice"
dayIndex:
$ref: "../../schemas/_index.yml#/DayIndex"
responses:
"200":
$ref: "../../api.yml#/components/responses/ClientDataResponse"

View File

@@ -0,0 +1,18 @@
post:
operationId: removeChoices
summary: Odstranění volby uživatele pro zvolený den/podnik, včetně případných jídel
requestBody:
required: true
content:
application/json:
schema:
required:
- locationKey
properties:
locationKey:
$ref: "../../schemas/_index.yml#/LunchChoice"
dayIndex:
$ref: "../../schemas/_index.yml#/DayIndex"
responses:
"200":
$ref: "../../api.yml#/components/responses/ClientDataResponse"

View File

@@ -0,0 +1,16 @@
post:
operationId: updateNote
summary: Nastavení poznámky k volbě uživatele
requestBody:
required: true
content:
application/json:
schema:
properties:
dayIndex:
$ref: "../../schemas/_index.yml#/DayIndex"
note:
type: string
responses:
"200":
$ref: "../../api.yml#/components/responses/ClientDataResponse"

14
types/paths/getData.yml Normal file
View File

@@ -0,0 +1,14 @@
get:
operationId: getData
summary: Načtení klientských dat pro aktuální nebo předaný den
parameters:
- in: query
name: dayIndex
description: Index dne v týdnu. Pokud není předán, je použit aktuální den.
schema:
type: integer
minimum: 0
maximum: 4
responses:
"200":
$ref: "../api.yml#/components/responses/ClientDataResponse"

View File

@@ -0,0 +1,19 @@
get:
operationId: getPizzaQr
summary: Získání QR kódu pro platbu za Pizza day
security: [] # Nevyžaduje autentizaci
parameters:
- in: query
name: login
schema:
type: string
required: true
description: Přihlašovací jméno uživatele, pro kterého bude vrácen QR kód
responses:
"200":
description: Vygenerovaný QR kód pro platbu
content:
image/png:
schema:
type: string
format: binary

20
types/paths/login.yml Normal file
View File

@@ -0,0 +1,20 @@
post:
operationId: login
summary: Přihlášení uživatele
security: [] # Nevyžaduje autentizaci
requestBody:
content:
application/json:
schema:
type: object
properties:
login:
type: string
description: Přihlašovací jméno uživatele. Vyžadováno pouze pokud není předáno pomocí hlaviček.
responses:
"200":
description: Přihlášení bylo úspěšné
content:
application/json:
schema:
$ref: "../schemas/_index.yml#/JWTToken"

View File

@@ -0,0 +1,21 @@
post:
operationId: addPizza
summary: Přidání pizzy do objednávky.
requestBody:
required: true
content:
application/json:
schema:
required:
- pizzaIndex
- pizzaSizeIndex
properties:
pizzaIndex:
description: Index pizzy v nabídce
type: integer
pizzaSizeIndex:
description: Index velikosti pizzy v nabídce variant
type: integer
responses:
"200":
description: Přidání pizzy do objednávky proběhlo úspěšně.

View File

@@ -0,0 +1,6 @@
post:
operationId: createPizzaDay
summary: Založení pizza day.
responses:
"200":
description: Pizza day byl založen.

View File

@@ -0,0 +1,6 @@
post:
operationId: deletePizzaDay
summary: Smazání pizza day.
responses:
"200":
description: Pizza day byl smazán.

View File

@@ -0,0 +1,18 @@
post:
operationId: finishDelivery
summary: Převod pizza day do stavu "Pizzy byly doručeny". Pokud má objednávající nastaveno číslo účtu, je ostatním uživatelům vygenerován a následně zobrazen QR kód pro úhradu jejich objednávky.
requestBody:
required: true
content:
application/json:
schema:
properties:
bankAccount:
description: Číslo bankovního účtu objednávajícího
type: string
bankAccountHolder:
description: Jméno majitele bankovního účtu
type: string
responses:
"200":
description: Pizza day byl přepnut do stavu "Pizzy doručeny".

View File

@@ -0,0 +1,6 @@
post:
operationId: finishOrder
summary: Přepnutí pizza day do stavu "Pizzy objednány". Není možné měnit objednávky, příslušným uživatelům je odeslána notifikace o provedené objednávce.
responses:
"200":
description: Pizza day byl přepnut do stavu "Pizzy objednány".

View File

@@ -0,0 +1,6 @@
post:
operationId: lockPizzaDay
summary: Uzamkne pizza day. Nebude možné přidávat či odebírat pizzy.
responses:
"200":
description: Pizza day byl uzamčen.

View File

@@ -0,0 +1,16 @@
post:
operationId: removePizza
summary: Odstranění pizzy z objednávky.
requestBody:
required: true
content:
application/json:
schema:
required:
- pizzaOrder
properties:
pizzaOrder:
$ref: "../../schemas/_index.yml#/PizzaVariant"
responses:
"200":
description: Odstranění pizzy z objednávky proběhlo úspěšně.

View File

@@ -0,0 +1,6 @@
post:
operationId: unlockPizzaDay
summary: Odemkne pizza day. Bude opět možné přidávat či odebírat pizzy.
responses:
"200":
description: Pizza day byl odemčen.

View File

@@ -0,0 +1,15 @@
post:
operationId: updatePizzaDayNote
summary: Nastavení poznámky k objednávkám pizz přihlášeného uživatele.
requestBody:
required: true
content:
application/json:
schema:
properties:
note:
type: string
description: Poznámka k objednávkám pizz, např "bez oliv".
responses:
"200":
description: Nastavení poznámky k objednávkám pizz proběhlo úspěšně.

View File

@@ -0,0 +1,23 @@
post:
operationId: updatePizzaFee
summary: Nastavení přirážky/slevy k objednávce pizz uživatele.
requestBody:
required: true
content:
application/json:
schema:
required:
- login
properties:
login:
type: string
description: Login cíleného uživatele
text:
type: string
description: Textový popis přirážky/slevy
price:
type: number
description: Částka přirážky/slevy v Kč
responses:
"200":
description: Nastavení přirážky/slevy proběhlo úspěšně.

View File

@@ -0,0 +1,23 @@
get:
operationId: getStats
summary: Vrátí statistiky způsobu stravování pro předaný rozsah dat.
parameters:
- in: query
name: startDate
required: true
schema:
type: string
description: Počáteční datum pro načtení statistik
- in: query
name: endDate
required: true
schema:
type: string
description: Koncové datum pro načtení statistik
responses:
"200":
description: Statistiky způsobu stravování. Každý prvek v poli představuje statistiky pro jeden den z předaného rozsahu dat.
content:
application/json:
schema:
$ref: "../../schemas/_index.yml#/WeeklyStats"

View File

@@ -0,0 +1,11 @@
get:
operationId: getVotes
summary: Vrátí statistiky hlasování o nových funkcích.
responses:
"200":
content:
application/json:
schema:
type: array
items:
$ref: "../../schemas/_index.yml#/FeatureRequest"

View File

@@ -0,0 +1,22 @@
post:
operationId: updateVote
summary: Aktualizuje hlasování uživatele o dané funkcionalitě.
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- option
- active
properties:
option:
description: Hlasovací možnost, kterou uživatel zvolil.
$ref: "../../schemas/_index.yml#/FeatureRequest"
active:
type: boolean
description: True, pokud uživatel hlasoval pro, jinak false.
responses:
"200":
description: Hlasování bylo úspěšně aktualizováno.

521
types/schemas/_index.yml Normal file
View File

@@ -0,0 +1,521 @@
# --- OBECNÉ ---
JWTToken:
type: object
description: Klientský JWT token pro autentizaci a autorizaci
required:
- login
- trusted
- iat
properties:
login:
type: string
description: Přihlašovací jméno uživatele
trusted:
type: boolean
description: Příznak, zda se jedná o uživatele ověřeného doménovým přihlášením
iat:
type: number
description: Časové razítko vydání tokenu
ClientData:
description: Klientská data pro jeden konkrétní den. Obsahuje menu všech načtených podniků a volby jednotlivých uživatelů.
type: object
additionalProperties: false
required:
- todayDayIndex
- date
- isWeekend
- choices
properties:
todayDayIndex:
description: Index dnešního dne v týdnu
$ref: "#/DayIndex"
date:
description: Human-readable datum dne
type: string
isWeekend:
description: Příznak, zda je tento den víkend
type: boolean
dayIndex:
description: Index dne v týdnu, ke kterému se vztahují tato data
$ref: "#/DayIndex"
choices:
$ref: "#/LunchChoices"
menus:
$ref: "#/RestaurantDayMenuMap"
pizzaDay:
$ref: "#/PizzaDay"
pizzaList:
description: Seznam dostupných pizz pro předaný den
type: array
items:
$ref: "#/Pizza"
pizzaListLastUpdate:
description: Datum a čas poslední aktualizace pizz
type: string
format: date-time
# --- OBĚDY ---
UserLunchChoice:
description: Konkrétní volba stravování jednoho uživatele v konkrétní den. Může se jednat jak o stravovací podnik, tak možnosti "budu objednávat", "neobědvám" apod.
additionalProperties: false
properties:
# TODO toto je tu z dost špatného důvodu, viz použití - mělo by se místo toho z loginu zjišťovat zda je uživatel trusted
trusted:
description: Příznak, zda byla tato volba provedena uživatelem ověřeným doménovým přihlášením
type: boolean
selectedFoods:
description: Pole indexů vybraných jídel v rámci dané restaurace. Index představuje pořadí jídla v menu dané restaurace.
type: array
items:
type: integer
departureTime:
description: Čas preferovaného odchodu do dané restaurace v human-readable formátu (např. 12:00)
type: string
note:
description: Volitelná, veřejně viditelná uživatelská poznámka k vybrané volbě
type: string
LocationLunchChoicesMap:
description: Objekt, kde klíčem je možnost stravování ((#LunchChoice)) a hodnotou množina uživatelů s touto volbou ((#LunchChoices)).
type: object
additionalProperties:
$ref: "#/UserLunchChoice"
LunchChoices:
description: Objekt, představující volby všech uživatelů pro konkrétní den. Klíčem je (#LunchChoice).
type: object
properties:
SLADOVNICKA:
$ref: "#/LocationLunchChoicesMap"
TECHTOWER:
$ref: "#/LocationLunchChoicesMap"
ZASTAVKAUMICHALA:
$ref: "#/LocationLunchChoicesMap"
SENKSERIKOVA:
$ref: "#/LocationLunchChoicesMap"
SPSE:
$ref: "#/LocationLunchChoicesMap"
PIZZA:
$ref: "#/LocationLunchChoicesMap"
OBJEDNAVAM:
$ref: "#/LocationLunchChoicesMap"
NEOBEDVAM:
$ref: "#/LocationLunchChoicesMap"
ROZHODUJI:
$ref: "#/LocationLunchChoicesMap"
Restaurant:
description: Stravovací zařízení (restaurace, jídelna, hospoda, ...)
type: string
enum:
- SLADOVNICKA
- TECHTOWER
- ZASTAVKAUMICHALA
- SENKSERIKOVA
LunchChoice:
description: Konkrétní možnost stravování (konkrétní restaurace, pizza day, objednání, neobědvání, rozhodování se, ...)
type: string
enum:
- SLADOVNICKA
- TECHTOWER
- ZASTAVKAUMICHALA
- SENKSERIKOVA
- SPSE
- PIZZA
- OBJEDNAVAM
- NEOBEDVAM
- ROZHODUJI
DayIndex:
description: Index dne v týdnu (0 = pondělí, 4 = pátek)
type: integer
minimum: 0
maximum: 4
FoodIndex:
description: Pořadový index jídla v menu konkrétní restaurace
type: integer
minimum: 0
Food:
description: Konkrétní jídlo z menu restaurace
type: object
additionalProperties: false
required:
- name
- isSoup
properties:
amount:
description: Množství standardní porce, např. 0,33l nebo 150g
type: string
name:
description: Název/popis jídla
type: string
price:
description: Cena ve formátu '135 Kč'
type: string
isSoup:
description: Příznak, zda se jedná o polévku
type: boolean
RestaurantDayMenu:
description: Menu restaurace na konkrétní den
type: object
additionalProperties: false
properties:
lastUpdate:
description: UNIX timestamp poslední aktualizace menu
type: integer
closed:
description: Příznak, zda je daný podnik v daný den zavřený
type: boolean
food:
description: Seznam jídel pro daný den
type: array
items:
$ref: "#/Food"
RestaurantDayMenuMap:
description: Objekt, kde klíčem je podnik ((#Restaurant)) a hodnotou denní menu daného podniku ((#RestaurantDayMenu))
type: object
additionalProperties: false
properties:
SLADOVNICKA:
$ref: "#/RestaurantDayMenu"
TECHTOWER:
$ref: "#/RestaurantDayMenu"
ZASTAVKAUMICHALA:
$ref: "#/RestaurantDayMenu"
SENKSERIKOVA:
$ref: "#/RestaurantDayMenu"
WeekMenu:
description: Pole týdenních menu jednotlivých podniků. Indexem je den v týdnu (0 = pondělí, 4 = pátek), hodnotou denní menu daného podniku.
type: array
minItems: 5
maxItems: 5
items:
$ref: "#/RestaurantDayMenuMap"
DepartureTime:
description: Preferovaný čas odchodu na oběd
type: string
enum:
- "10:00"
- "10:15"
- "10:30"
- "10:45"
- "11:00"
- "11:15"
- "11:30"
- "11:45"
- "12:00"
- "12:15"
- "12:30"
- "12:45"
- "13:00"
x-enum-varnames:
- T10_00
- T10_15
- T10_30
- T10_45
- T11_00
- T11_15
- T11_30
- T11_45
- T12_00
- T12_15
- T12_30
- T12_45
- T13_00
# --- HLASOVÁNÍ ---
FeatureRequest:
type: string
enum:
- Ruční generování QR kódů mimo Pizza day (např. při objednávání)
- Možnost označovat si jídla jako oblíbená (taková jídla by se uživateli následně zvýrazňovala)
- Možnost úhrady v podniku za všechny jednou osobou a následné generování QR ostatním
- Zrušení \"užívejte víkend\", místo toho umožnit zpětně náhled na uplynulý týden
- Umožnění zobrazení vygenerovaného QR kódu i po následující dny (dokud ho uživatel ručně \"nezavře\", např. tlačítkem \"Zaplatil jsem\")
- Zobrazování náhledů (fotografií) pizz v rámci Pizza day
- Statistiky (nejoblíbenější podnik, nejpopulárnější jídla, nejobjednávanější pizzy, nejčastější uživatelé, ...)
- Vylepšení responzivního designu
- Zvýšení zabezpečení aplikace
- Zvýšená ochrana proti chybám uživatele (potvrzovací dialogy, překliky, ...)
- Celkové vylepšení UI/UX
- Zlepšení dokumentace/postupů pro ostatní vývojáře
x-enum-varnames:
- CUSTOM_QR
- FAVORITES
- SINGLE_PAYMENT
- NO_WEEKENDS
- QR_FOREVER
- PIZZA_PICTURES
- STATISTICS
- RESPONSIVITY
- SECURITY
- SAFETY
- UI
- DEVELOPMENT
# --- EASTER EGGS ---
EasterEgg:
description: Data pro zobrazení easter eggů ssss
type: object
additionalProperties: false
required:
- path
- url
- startOffset
- endOffset
- duration
properties:
path:
type: string
url:
type: string
startOffset:
type: number
endOffset:
type: number
duration:
type: number
width:
type: string
zIndex:
type: integer
position:
type: string
enum:
- absolute
animationName:
type: string
animationDuration:
type: string
animationTimingFunction:
type: string
# --- STATISTIKY ---
LocationStats:
description: Objekt, kde klíčem je zvolená možnost a hodnotou počet uživatelů, kteří tuto možnosti zvolili
type: object
additionalProperties: false
properties:
# Bohužel OpenAPI neumí nadefinovat objekt, kde klíčem může být pouze hodnota existujícího enumu :(
SLADOVNICKA:
type: number
TECHTOWER:
type: number
ZASTAVKAUMICHALA:
type: number
SENKSERIKOVA:
type: number
SPSE:
type: number
PIZZA:
type: number
OBJEDNAVAM:
type: number
NEOBEDVAM:
type: number
ROZHODUJI:
type: number
DailyStats:
description: Statistika vybraných možností pro jeden konkrétní den
type: object
additionalProperties: false
required:
- date
- locations
properties:
date:
description: Datum v human-readable formátu
type: string
locations:
$ref: "#/LocationStats"
WeeklyStats:
description: Pole statistik vybraných možností pro jeden konkrétní týden. Index představuje den v týdnu (0 = pondělí, 4 = pátek)
type: array
minItems: 5
maxItems: 5
items:
$ref: "#/DailyStats"
# --- PIZZA DAY ---
PizzaDayState:
description: Stav pizza day
type: string
enum:
- Pizza day nebyl založen
- Pizza day je založen
- Objednávky uzamčeny
- Pizzy objednány
- Pizzy doručeny
x-enum-varnames:
- NOT_CREATED
- CREATED
- LOCKED
- ORDERED
- DELIVERED
# TODO toto je jen rozšířená varianta PizzaVariant - sloučit do jednoho objektu
PizzaSize:
description: Údaje o konkrétní variantě pizzy
type: object
additionalProperties: false
required:
- varId
- size
- pizzaPrice
- boxPrice
- price
properties:
varId:
description: Unikátní identifikátor varianty pizzy
type: integer
size:
description: Velikost pizzy, např. "30cm"
type: string
pizzaPrice:
description: Cena samotné pizzy v Kč
type: number
boxPrice:
description: Cena krabice pizzy v Kč
type: number
price:
description: Celková cena (pizza + krabice)
type: number
Pizza:
description: Údaje o konkrétní pizze.
type: object
additionalProperties: false
required:
- name
- ingredients
- sizes
properties:
name:
description: Název pizzy
type: string
ingredients:
description: Seznam obsažených ingrediencí
type: array
items:
type: string
sizes:
description: Dostupné velikosti pizzy
type: array
items:
$ref: "#/PizzaSize"
PizzaVariant:
description: Konkrétní varianta (velikost) jedné pizzy.
type: object
additionalProperties: false
required:
- varId
- name
- size
- price
properties:
varId:
description: Unikátní identifikátor varianty pizzy
type: integer
name:
description: Název pizzy
type: string
size:
description: Velikost pizzy (např. "30cm")
type: string
price:
description: Cena pizzy v Kč, včetně krabice
type: number
PizzaOrder:
description: Údaje o objednávce pizzy jednoho uživatele.
type: object
additionalProperties: false
required:
- customer
- totalPrice
- hasQr
properties:
customer:
description: Jméno objednávajícího uživatele
type: string
pizzaList:
description: Seznam variant pizz k objednání (typicky bývá jen jedna)
type: array
items:
$ref: "#/PizzaVariant"
fee:
description: Příplatek (např. za extra ingredience)
type: object
properties:
text:
description: Popis příplatku (např. "kuřecí maso navíc")
type: string
price:
description: Cena příplatku v Kč
type: number
totalPrice:
description: Celková cena všech objednaných pizz daného uživatele, včetně krabic a příplatků
type: number
hasQr:
description: |
Příznak, pokud je k této objednávce vygenerován QR kód pro platbu. To je typicky pravda, pokud:
- objednávající má v nastavení vyplněno číslo účtu
- pizza day je ve stavu DELIVERED (Pizzy byly doručeny)
note:
description: Volitelná uživatelská poznámka pro objednávajícího (např. "bez oliv")
type: string
PizzaDay:
description: Data o Pizza day pro konkrétní den
type: object
additionalProperties: false
properties:
state:
$ref: "#/PizzaDayState"
creator:
description: "Jméno zakladatele pizza day"
type: string
orders:
description: Pole objednávek jednotlivých uživatelů
type: array
items:
$ref: "#/PizzaOrder"
# --- NOTIFIKACE ---
UdalostEnum:
type: string
enum:
- Zahájen pizza day
- Objednána pizza
- Jdeme na oběd
x-enum-varnames:
- ZAHAJENA_PIZZA
- OBJEDNANA_PIZZA
- JDEME_NA_OBED
NotifikaceInput:
type: object
required:
- udalost
- user
properties:
udalost:
$ref: "#/UdalostEnum"
user:
type: string
NotifikaceData:
type: object
required:
- input
properties:
input:
$ref: "#/NotifikaceInput"
gotify:
type: boolean
teams:
type: boolean
ntfy:
type: boolean
GotifyServer:
type: object
required:
- server
- api_keys
properties:
server:
type: string
api_keys:
type: array
items:
type: string