Migrace klienta na OpenAPI
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful

This commit is contained in:
Martin Berka 2025-03-19 23:08:46 +01:00
parent f09bc44d63
commit d366882f6b
45 changed files with 1068 additions and 890 deletions

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,7 +65,8 @@ function App() {
if (!auth?.login) {
return
}
getData().then(data => {
getData().then(response => {
const data = response.data
if (data) {
setData(data);
setDayIndex(data.dayIndex);
@ -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)}>{LunchChoice[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>
@ -439,10 +440,10 @@ function App() {
<option></option>
{Object.entries(LunchChoice)
.filter(entry => {
const locationKey = entry[0] 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>

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,84 +0,0 @@
import { toast } from "react-toastify";
import { getToken } from "../Utils";
import { ClientData } from "../../../types";
/**
* 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): Promise<ClientData> => {
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,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,10 @@ 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, updateVote } from "../../../types";
export default function Header() {
const auth = useAuth();
@ -18,12 +16,12 @@ 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 [featureVotes, setFeatureVotes] = useState<FeatureRequest[] | undefined>([]);
useEffect(() => {
if (auth?.login) {
getFeatureVotes().then(votes => {
setFeatureVotes(votes);
getVotes().then(response => {
setFeatureVotes(response.data);
})
}
}, [auth?.login]);
@ -99,8 +97,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 {

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

@ -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

@ -84,7 +84,7 @@ async function getMenu(date: Date): Promise<WeekMenu | undefined> {
* @param date datum, ke kterému získat menu
* @param mock příznak, zda chceme pouze mock data
*/
export async function getRestaurantMenu(restaurant: keyof typeof Restaurant, date?: Date): Promise<RestaurantDayMenu> {
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date): Promise<RestaurantDayMenu> {
const usedDate = date ?? getToday();
const dayOfWeekIndex = getDayOfWeekIndex(usedDate);
const now = new Date().getTime();
@ -210,7 +210,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,7 +237,7 @@ 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);
@ -260,11 +260,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 +312,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 +353,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 +365,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

@ -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

@ -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,7 +1,7 @@
import { FeatureRequest, LunchChoice, PizzaVariant } from "../types";
export type ILocationKey = {
locationKey: keyof typeof LunchChoice,
locationKey: LunchChoice,
}
export type IDayIndex = {

View File

@ -5,642 +5,76 @@ 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"
# 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
- Mám vlastní/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,11 +1,11 @@
// import { defaultPlugins } from '@hey-api/openapi-ts';
import { defaultPlugins } from '@hey-api/openapi-ts';
export default {
input: 'api.yml',
output: 'gen',
plugins: [
// ...defaultPlugins,
// '@hey-api/client-fetch',
...defaultPlugins,
'@hey-api/client-fetch',
{
enums: 'javascript',
name: '@hey-api/typescript',

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,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,19 @@
post:
operationId: updateVote
summary: Aktualizuje hlasování uživatele o dané funkcionalitě.
requestBody:
required: true
content:
application/json:
schema:
type: object
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