1 Commits

Author SHA1 Message Date
3db03acde5 Stage 1: Příprava pro týdenní parser 2023-10-11 20:47:49 +02:00
62 changed files with 11817 additions and 7800 deletions

22
.gitignore vendored
View File

@@ -1 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -1,56 +0,0 @@
variables:
- &node_image 'node:18-alpine3.18'
- &branch 'master'
when:
- event: push
branch: *branch
steps:
- name: Install server dependencies
image: *node_image
commands:
- cd server
- yarn install --frozen-lockfile
- name: Install client dependencies
image: *node_image
commands:
- cd client
- yarn install --frozen-lockfile
- name: Build server
depends_on: [Install server dependencies]
image: *node_image
commands:
- cd server
- yarn build
- name: Build client
depends_on: [Install client dependencies]
image: *node_image
commands:
- cd client
- yarn build
- name: Build Docker image
depends_on: [Build server, Build client]
image: woodpeckerci/plugin-docker-buildx
settings:
dockerfile: Dockerfile-Woodpecker
platforms: linux/amd64
registry:
from_secret: REPO_URL
username:
from_secret: REPO_USERNAME
password:
from_secret: REPO_PASSWORD
repo:
from_secret: REPO_NAME
- name: Discord notification - build
image: appleboy/drone-discord
depends_on: [Build Docker image]
when:
- status: [success, failure]
settings:
webhook_id:
from_secret: DISCORD_WEBHOOK_ID
webhook_token:
from_secret: DISCORD_WEBHOOK_TOKEN
message: "{{#success build.status}}✅ Sestavení {{build.number}} proběhlo úspěšně.{{else}}❌ Sestavení {{build.number}} selhalo.{{/success}}\n\nPipeline: {{build.link}}\nPoslední commit: {{commit.message}}Autor: {{commit.author}}"

View File

@@ -1,46 +1,28 @@
# Builder # Builder
FROM node:18-alpine3.18 AS builder FROM node:18-alpine3.18 as builder
WORKDIR /build WORKDIR /build
# Zkopírování závislostí - server COPY package.json .
COPY yarn.lock .
COPY server/package.json ./server/ COPY server/package.json ./server/
COPY server/yarn.lock ./server/
# Zkopírování závislostí - klient
COPY client/package.json ./client/ COPY client/package.json ./client/
COPY client/yarn.lock ./client/
# Instalace závislostí - server
WORKDIR /build/server
RUN yarn install --frozen-lockfile RUN yarn install --frozen-lockfile
# Instalace závislostí - klient
WORKDIR /build/client
RUN yarn install --frozen-lockfile
WORKDIR /build
# Zkopírování build závislostí - server
COPY server/tsconfig.json ./server/ COPY server/tsconfig.json ./server/
COPY server/src ./server/src/ COPY server/src ./server/src/
# Zkopírování build závislostí - klient
COPY client/tsconfig.json ./client/ COPY client/tsconfig.json ./client/
COPY client/vite.config.ts ./client/
COPY client/vite-env.d.ts ./client/
COPY client/index.html ./client/
COPY client/src ./client/src COPY client/src ./client/src
COPY client/public ./client/public COPY client/public ./client/public
# Zkopírování společných typů
COPY types ./types/ COPY types ./types/
# Sestavení serveru
WORKDIR /build/server WORKDIR /build/server
RUN yarn build RUN yarn build
# Sestavení klienta
WORKDIR /build/client WORKDIR /build/client
RUN yarn build RUN yarn build
@@ -51,22 +33,11 @@ ENV NODE_ENV production
WORKDIR /app WORKDIR /app
# Vykopírování sestaveného serveru COPY --from=builder /build/node_modules ./node_modules
COPY --from=builder /build/server/node_modules ./server/node_modules
COPY --from=builder /build/server/dist ./ COPY --from=builder /build/server/dist ./
COPY --from=builder /build/client/build ./public
# Vykopírování sestaveného klienta
COPY --from=builder /build/client/dist ./public
# Zkopírování produkčních .env serveru
COPY /server/.env.production ./server/src COPY /server/.env.production ./server/src
# Zkopírování konfigurace easter eggů
RUN if [ -f /server/.easter-eggs.json ]; then cp /server/.easter-eggs.json ./server/; fi
# Export /data/db.json do složky /data
VOLUME ["/data"]
EXPOSE 3000 EXPOSE 3000
CMD [ "node", "./server/src/index.js" ] CMD [ "node", "./server/src/index.js" ]

View File

@@ -1,21 +0,0 @@
FROM node:18-alpine3.18
ENV LANG=cs_CZ.UTF-8
ENV NODE_ENV=production
WORKDIR /app
# Vykopírování sestaveného serveru
COPY ./server/node_modules ./server/node_modules
COPY ./server/dist ./
# TODO tohle není dobře, má to být součástí serveru
# COPY ./server/resources ./resources
# Vykopírování sestaveného klienta
COPY ./client/dist ./public
# Zkopírování konfigurace easter eggů
RUN if [ -f ./server/.easter-eggs.json ]; then cp ./server/.easter-eggs.json ./server/; fi
EXPOSE 3000
CMD [ "node", "./server/src/index.js" ]

View File

@@ -1,7 +1,4 @@
# TODO # TODO
- [ ] HTTP_REMOTE_TRUSTED_IPS se nikde nevalidují, hlavičky jsou přijímány odkudkoli
- [ ] V případě zapnutí přihlašování přes trusted headers nefunguje standardní přihlášení (nevrátí žádnou odpověď)
- [ ] Nemělo by se jít dostat na přihlašovací formulář (měla by tam být nanejvýš hláška nebo přesměrování)
- [ ] Možnost úhrady celé útraty jednou osobou - [ ] Možnost úhrady celé útraty jednou osobou
- Základní myšlenka: jedna osoba uhradí celou útratu (v zájmu rychlosti odbavení), ostatním se automaticky vygeneruje QR kód, kterým následně uhradí svoji část útraty - Základní myšlenka: jedna osoba uhradí celou útratu (v zájmu rychlosti odbavení), ostatním se automaticky vygeneruje QR kód, kterým následně uhradí svoji část útraty
- Obecně to bude problém např. pokud si někdo objedná něco navíc (pití apod.) - Obecně to bude problém např. pokud si někdo objedná něco navíc (pití apod.)
@@ -37,7 +34,7 @@
- [ ] Pizzy se samy budou při naklikání přidávat do košíku - [ ] Pizzy se samy budou při naklikání přidávat do košíku
- [ ] Nutno nejprve vyřešit předávání PHPSESSIONID cookie na pizzachefie.cz pomocí fetch() - [ ] Nutno nejprve vyřešit předávání PHPSESSIONID cookie na pizzachefie.cz pomocí fetch()
- [ ] Ceny krabic za pizzu jsou napevno v kódu - problém, pokud se někdy změní - [ ] Ceny krabic za pizzu jsou napevno v kódu - problém, pokud se někdy změní
- [X] Umožnit u Pizza day ručně připočíst cenu za přísady - [ ] Umožnit u Pizza day ručně připočíst cenu za přísady
- [X] Prvotní načtení pizz při založení Pizza Day trvá a nic se během toho nezobrazuje (např. loader) - [X] Prvotní načtení pizz při založení Pizza Day trvá a nic se během toho nezobrazuje (např. loader)
- [X] Po doručení zobrazit komu zaplatit (kdo objednával) - [X] Po doručení zobrazit komu zaplatit (kdo objednával)
- [x] Zbytečně nescrapovat každý den pizzy z Pizza Chefie, dokud není založen Pizza Day - [x] Zbytečně nescrapovat každý den pizzy z Pizza Chefie, dokud není založen Pizza Day

2
client/.gitignore vendored
View File

@@ -1,2 +1,2 @@
build build
dist src/types

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Moderní webová aplikace pro lepší správu obědových preferencí" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>Luncher</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -3,40 +3,39 @@
"version": "0.1.0", "version": "0.1.0",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"type": "module",
"homepage": ".", "homepage": ".",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0", "@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@types/jest": "^29.5.12", "@types/jest": "^27.5.2",
"@types/node": "^20.11.20", "@types/node": "^16.18.23",
"@types/react": "^19.0.0", "@types/react": "^18.0.33",
"@types/react-dom": "^19.0.0", "@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^4.3.4",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"react": "^19.0.0", "react": "^18.2.0",
"react-bootstrap": "^2.7.2", "react-bootstrap": "^2.7.2",
"react-dom": "^19.0.0", "react-dom": "^18.2.0",
"react-jwt": "^1.2.0", "react-jwt": "^1.2.0",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-scripts": "5.0.1",
"react-select-search": "^4.1.6", "react-select-search": "^4.1.6",
"react-snowfall": "^2.2.0", "react-toastify": "^9.1.3",
"react-toastify": "^10.0.4",
"sass": "^1.80.6",
"socket.io-client": "^4.6.1", "socket.io-client": "^4.6.1",
"typescript": "^5.3.3", "typescript": "^4.9.5"
"vite": "^6.0.3",
"vite-tsconfig-paths": "^5.1.4"
}, },
"scripts": { "scripts": {
"start": "yarn vite", "copy-types": "cp -r ../types ./src",
"build": "yarn vite build" "start": "yarn copy-types && react-scripts start",
"build": "yarn copy-types && react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app" "react-app",
"react-app/jest"
] ]
}, },
"browserslist": { "browserslist": {
@@ -53,6 +52,6 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"prettier": "^3.2.5" "prettier": "^2.8.8"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

43
client/public/index.html Normal file
View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Moderní webová aplikace pro lepší správu obědových preferencí" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Luncher</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -56,7 +56,7 @@
} }
.title { .title {
margin: 50px 20px; margin: 50px 30px;
} }
.food-tables { .food-tables {
@@ -124,45 +124,3 @@
align-items: center; align-items: center;
font-size: xx-large; font-size: xx-large;
} }
@keyframes bounce-in {
0% {
left: var(--start-left);
right: var(--start-right);
top: var(--start-top);
bottom: var(--start-bottom);
}
25% {
left: var(--end-left);
right: var(--end-right);
top: var(--end-top);
bottom: var(--end-bottom);
}
50% {
left: var(--start-left);
right: var(--start-right);
top: var(--start-top);
bottom: var(--start-bottom);
}
75% {
left: var(--end-left);
right: var(--end-right);
top: var(--end-top);
bottom: var(--end-bottom);
}
100% {
left: var(--start-left);
right: var(--start-right);
top: var(--start-top);
bottom: var(--start-bottom);
}
}
// TODO zjistit, zda to nedokážeme lépe - tohle je kvůli overflow easter egg obrázků, ale skrývá to úplně scrollbar
html {
overflow-x: hidden;
}

View File

@@ -8,41 +8,27 @@ import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap';
import Header from './components/Header'; import Header from './components/Header';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import PizzaOrderList from './components/PizzaOrderList'; import PizzaOrderList from './components/PizzaOrderList';
import SelectSearch, {SelectedOptionValue, SelectSearchOption} from 'react-select-search'; import SelectSearch, { SelectedOptionValue } from 'react-select-search';
import 'react-select-search/style.css'; import 'react-select-search/style.css';
import './App.scss'; import './App.css';
import { faCircleCheck, faNoteSticky, faTrashCan } from '@fortawesome/free-regular-svg-icons'; import { SelectSearchOption } from 'react-select-search';
import { useSettings } from './context/settings'; import { faCircleCheck, faTrashCan } from '@fortawesome/free-regular-svg-icons';
import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, DayMenu, DepartureTime, LocationKey } from '../../types'; import { useBank } from './context/bank';
import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, Menu } from './types';
import Footer from './components/Footer'; import Footer from './components/Footer';
import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons'; import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons';
import Loader from './components/Loader'; import Loader from './components/Loader';
import { getData, errorHandler, getQrUrl } from './api/Api'; import { getData, errorHandler, getQrUrl } from './api/Api';
import { addChoice, removeChoices, removeChoice, changeDepartureTime, jdemeObed, updateNote } from './api/FoodApi'; import { addChoice, removeChoices, removeChoice, changeDepartureTime } from './api/FoodApi';
import { getHumanDateTime, isInTheFuture } from './Utils';
import NoteModal from './components/modals/NoteModal';
import { useEasterEgg } from './context/eggs';
import { getImage } from './api/EasterEggApi';
const EVENT_CONNECT = "connect" const EVENT_CONNECT = "connect"
// Fixní styl pro všechny easter egg obrázky
const EASTER_EGG_STYLE = {
zIndex: 1,
animationName: "bounce-in",
animationTimingFunction: "ease"
}
// Výchozí doba trvání animace v sekundách, pokud není přetíženo v konfiguračním JSONu
const EASTER_EGG_DEFAULT_DURATION = 0.75;
function App() { function App() {
const auth = useAuth(); const auth = useAuth();
const settings = useSettings(); const bank = useBank();
const [easterEgg, easterEggLoading] = useEasterEgg(auth);
const [isConnected, setIsConnected] = useState<boolean>(false); const [isConnected, setIsConnected] = useState<boolean>(false);
const [data, setData] = useState<ClientData>(); const [data, setData] = useState<ClientData>();
const [food, setFood] = useState<{ [key in Restaurants]?: DayMenu }>(); const [food, setFood] = useState<{ [key in Restaurants]?: Menu }>();
const [myOrder, setMyOrder] = useState<Order>(); const [myOrder, setMyOrder] = useState<Order>();
const [foodChoiceList, setFoodChoiceList] = useState<Food[]>(); const [foodChoiceList, setFoodChoiceList] = useState<Food[]>();
const [closed, setClosed] = useState<boolean>(false); const [closed, setClosed] = useState<boolean>(false);
@@ -50,13 +36,10 @@ function App() {
const choiceRef = useRef<HTMLSelectElement>(null); const choiceRef = useRef<HTMLSelectElement>(null);
const foodChoiceRef = useRef<HTMLSelectElement>(null); const foodChoiceRef = useRef<HTMLSelectElement>(null);
const departureChoiceRef = useRef<HTMLSelectElement>(null); const departureChoiceRef = useRef<HTMLSelectElement>(null);
const pizzaPoznamkaRef = useRef<HTMLInputElement>(null); const poznamkaRef = useRef<HTMLInputElement>(null);
const [failure, setFailure] = useState<boolean>(false); const [failure, setFailure] = useState<boolean>(false);
const [dayIndex, setDayIndex] = useState<number>(); const [dayIndex, setDayIndex] = useState<number>();
const [loadingPizzaDay, setLoadingPizzaDay] = useState<boolean>(false); const [loadingPizzaDay, setLoadingPizzaDay] = useState<boolean>(false);
const [noteModalOpen, setNoteModalOpen] = useState<boolean>(false);
const [eggImage, setEggImage] = useState<Blob>();
const eggRef = useRef<HTMLImageElement>(null);
// Prazvláštní workaround, aby socket.io listener viděl aktuální hodnotu // Prazvláštní workaround, aby socket.io listener viděl aktuální hodnotu
// https://medium.com/@kishorkrishna/cant-access-latest-state-inside-socket-io-listener-heres-how-to-fix-it-1522a5abebdb // https://medium.com/@kishorkrishna/cant-access-latest-state-inside-socket-io-listener-heres-how-to-fix-it-1522a5abebdb
const dayIndexRef = useRef<number | undefined>(dayIndex); const dayIndexRef = useRef<number | undefined>(dayIndex);
@@ -140,8 +123,11 @@ function App() {
useEffect(() => { useEffect(() => {
if (choiceRef?.current?.value && choiceRef.current.value !== "") { if (choiceRef?.current?.value && choiceRef.current.value !== "") {
const locationKey = choiceRef.current.value as LocationKey; // TODO: wtf, cos pil, když jsi tohle psal?
const restaurantKey = Object.keys(Restaurants).indexOf(locationKey); const key = choiceRef?.current?.value;
const locationIndex = Object.keys(Locations).indexOf(key as unknown as Locations);
const locationsKey = Object.keys(Locations)[locationIndex];
const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey);
if (restaurantKey > -1 && food) { if (restaurantKey > -1 && food) {
const restaurant = Object.values(Restaurants)[restaurantKey]; const restaurant = Object.values(Restaurants)[restaurantKey];
setFoodChoiceList(food[restaurant]?.food); setFoodChoiceList(food[restaurant]?.food);
@@ -172,58 +158,29 @@ function App() {
} }
}, [handleKeyDown]); }, [handleKeyDown]);
// Stažení a nastavení easter egg obrázku
useEffect(() => {
if (auth?.login && easterEgg?.url && !eggImage) {
getImage(easterEgg.url).then(data => {
if (data) {
setEggImage(data);
// Smazání obrázku z DOMu po animaci
setTimeout(() => {
if (eggRef?.current) {
eggRef.current.remove();
}
}, (easterEgg.duration || EASTER_EGG_DEFAULT_DURATION) * 1000);
}
});
}
}, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]);
const doAddClickFoodChoice = async (location: Locations, foodIndex?: number) => {
const locationKey = Object.keys(Locations).find(key => Locations[key as keyof typeof Locations] === location) as LocationKey;
if (auth?.login) {
await errorHandler(() => addChoice(locationKey, foodIndex, dayIndex));
}
}
const doAddChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => { const doAddChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const locationKey = event.target.value as LocationKey; const index = Object.keys(Locations).indexOf(event.target.value as unknown as Locations);
if (auth?.login) { if (auth?.login) {
await errorHandler(() => addChoice(locationKey, undefined, dayIndex)); await errorHandler(() => addChoice(index, undefined, dayIndex));
if (foodChoiceRef.current?.value) { if (foodChoiceRef.current?.value) {
foodChoiceRef.current.value = ""; foodChoiceRef.current.value = "";
} }
} }
} }
const doJdemeObed = async (locationKey: LocationKey) => {
if (auth?.login) {
await jdemeObed(locationKey);
}
}
const doAddFoodChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => { const doAddFoodChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => {
if (event.target.value && foodChoiceList?.length && choiceRef.current?.value) { if (event.target.value && foodChoiceList?.length && choiceRef.current?.value) {
const locationKey = choiceRef.current.value as LocationKey; const restaurantKey = choiceRef.current.value;
if (auth?.login) { if (auth?.login) {
await errorHandler(() => addChoice(locationKey, Number(event.target.value), dayIndex)); const locationIndex = Object.keys(Locations).indexOf(restaurantKey as unknown as Locations);
await errorHandler(() => addChoice(locationIndex, Number(event.target.value), dayIndex));
} }
} }
} }
const doRemoveChoices = async (locationKey: LocationKey) => { const doRemoveChoices = async (locationKey: string) => {
if (auth?.login) { if (auth?.login) {
await errorHandler(() => removeChoices(locationKey, dayIndex)); await errorHandler(() => removeChoices(Number(locationKey), dayIndex));
// Vyresetujeme výběr, aby bylo jasné pro který případně vybíráme jídlo // Vyresetujeme výběr, aby bylo jasné pro který případně vybíráme jídlo
if (choiceRef?.current?.value) { if (choiceRef?.current?.value) {
choiceRef.current.value = ""; choiceRef.current.value = "";
@@ -234,9 +191,9 @@ function App() {
} }
} }
const doRemoveFoodChoice = async (locationKey: LocationKey, foodIndex: number) => { const doRemoveFoodChoice = async (locationKey: string, foodIndex: number) => {
if (auth?.login) { if (auth?.login) {
await errorHandler(() => removeChoice(locationKey, foodIndex, dayIndex)); await errorHandler(() => removeChoice(Number(locationKey), foodIndex, dayIndex));
if (choiceRef?.current?.value) { if (choiceRef?.current?.value) {
choiceRef.current.value = ""; choiceRef.current.value = "";
} }
@@ -246,13 +203,6 @@ function App() {
} }
} }
const saveNote = async (note?: string) => {
if (auth?.login) {
await errorHandler(() => updateNote(note, dayIndex));
setNoteModalOpen(false);
}
}
const pizzaSuggestions = useMemo(() => { const pizzaSuggestions = useMemo(() => {
if (!data?.pizzaList) { if (!data?.pizzaList) {
return []; return [];
@@ -286,12 +236,12 @@ function App() {
await removePizza(pizzaOrder); await removePizza(pizzaOrder);
} }
const handlePizzaPoznamkaChange = async () => { const handlePoznamkaChange = async () => {
if (pizzaPoznamkaRef.current?.value && pizzaPoznamkaRef.current.value.length > 70) { if (poznamkaRef.current?.value && poznamkaRef.current.value.length > 100) {
alert("Poznámka může mít maximálně 70 znaků"); alert("Poznámka může mít maximálně 100 znaků");
return; return;
} }
updatePizzaDayNote(pizzaPoznamkaRef.current?.value); updatePizzaDayNote(poznamkaRef.current?.value);
} }
// const addToCart = async () => { // const addToCart = async () => {
@@ -338,16 +288,15 @@ function App() {
} }
} }
const renderFoodTable = (location: Locations, menu: DayMenu) => { const renderFoodTable = (name: string, menu: Menu) => {
let content; let content;
if (menu?.closed) { if (menu?.closed) {
content = <h3>Zavřeno</h3> content = <h3>Zavřeno</h3>
} else if (menu?.food?.length > 0) { } else if (menu?.food?.length > 0) {
const hideSoups = settings?.hideSoups;
content = <Table striped bordered hover> content = <Table striped bordered hover>
<tbody style={{ cursor: 'pointer' }}> <tbody>
{menu.food.filter(f => (hideSoups ? !f.isSoup : true)).map((f: any, index: number) => {menu.food.map((f: any, index: number) =>
<tr key={index} onClick={() => doAddClickFoodChoice(location, hideSoups ? index + 1 : index)}> <tr key={index}>
<td>{f.amount}</td> <td>{f.amount}</td>
<td>{f.name}</td> <td>{f.name}</td>
<td>{f.price}</td> <td>{f.price}</td>
@@ -358,9 +307,9 @@ function App() {
} else { } else {
content = <h3>Chyba načtení dat</h3> content = <h3>Chyba načtení dat</h3>
} }
return <Col md={12} lg={3} className='mt-3'> return <Col md={12} lg={4}>
<h3 style={{ cursor: 'pointer' }} onClick={() => doAddClickFoodChoice(location, undefined)}>{location}</h3> <h3>{name}</h3>
{menu?.lastUpdate && <small>Poslední aktualizace: {getHumanDateTime(new Date(menu.lastUpdate))}</small>} {menu?.lastUpdate && <small>Poslední aktualizace: {menu.lastUpdate}</small>}
{content} {content}
</Col> </Col>
} }
@@ -394,49 +343,49 @@ function App() {
} }
const noOrders = data?.pizzaDay?.orders?.length === 0; const noOrders = data?.pizzaDay?.orders?.length === 0;
const canChangeChoice = dayIndex == null || data.todayWeekIndex == null || dayIndex >= data.todayWeekIndex;
const { path, url, startOffset, endOffset, duration, ...style } = easterEgg || {};
return ( return (
<> <>
{easterEgg && eggImage && <img ref={eggRef} alt='' src={URL.createObjectURL(eggImage)} style={{ position: 'absolute', ...EASTER_EGG_STYLE, ...style, animationDuration: `${duration ?? EASTER_EGG_DEFAULT_DURATION}s` }} />}
<Header /> <Header />
<div className='wrapper'> <div className='wrapper'>
{data.isWeekend ? <h4>Užívejte víkend :)</h4> : <> {data.isWeekend ? <h4>Užívejte víkend :)</h4> : <>
<Alert variant={'primary'}> <Alert variant={'primary'}>
<img src='hat.png' style={{ position: "absolute", width: "70px", rotate: "-45deg", left: -40, top: -58 }} />
<img src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} />
Poslední změny: Poslední změny:
<ul> <ul>
<li>Přidání restaurací Zastávka u Michala a Pivovarský šenk Šeříková</li> <li>Oprava generování QR kódů pro Pizza day</li>
<li>Možnost výběru restaurace a jídel kliknutím v tabulce</li> <li>Serverová validace času odchodu</li>
<li>Loader při zakládání Pizza day</li>
<li>Možnost ručního zadání příplatku k Pizza day objednávkám</li>
<li>Vylepšená detekce uzavření pro podniky Sladovnická a TechTower</li>
<li>Úprava zvýraznění aktuálního dne</li>
<li>Možnost hlasování o nových funkcích</li>
</ul> </ul>
</Alert> </Alert>
{dayIndex != null && {dayIndex != null &&
<div className='day-navigator'> <div className='day-navigator'>
<FontAwesomeIcon title="Předchozí den" icon={faChevronLeft} style={{ cursor: "pointer", visibility: dayIndex > 0 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex - 1)} /> {dayIndex > 0 && <FontAwesomeIcon title="Předchozí den" icon={faChevronLeft} style={{ cursor: "pointer" }} onClick={() => handleDayChange(dayIndex - 1)} />}
<h1 className='title' style={{ color: dayIndex === data.todayWeekIndex ? 'black' : 'gray' }}>{data.date}</h1> <h1 className='title' style={{ color: dayIndex === data.todayWeekIndex ? 'black' : 'gray' }}>{data.date}</h1>
<FontAwesomeIcon title="Následující den" icon={faChevronRight} style={{ cursor: "pointer", visibility: dayIndex < 4 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex + 1)} /> {dayIndex < 4 && <FontAwesomeIcon title="Následující den" icon={faChevronRight} style={{ cursor: "pointer" }} onClick={() => handleDayChange(dayIndex + 1)} />}
</div> </div>
} }
<Row className='food-tables'> <Row className='food-tables'>
{food[Restaurants.SLADOVNICKA] && renderFoodTable(Locations.SLADOVNICKA, food[Restaurants.SLADOVNICKA])} {food[Restaurants.SLADOVNICKA] && renderFoodTable('Sladovnická', food[Restaurants.SLADOVNICKA])}
{/* {food[Restaurants.UMOTLIKU] && renderFoodTable(food[Restaurants.UMOTLIKU])} */} {food[Restaurants.UMOTLIKU] && renderFoodTable('U Motlíků', food[Restaurants.UMOTLIKU])}
{food[Restaurants.TECHTOWER] && renderFoodTable(Locations.TECHTOWER, food[Restaurants.TECHTOWER])} {food[Restaurants.TECHTOWER] && renderFoodTable('TechTower', food[Restaurants.TECHTOWER])}
{food[Restaurants.ZASTAVKAUMICHALA] && renderFoodTable(Locations.ZASTAVKAUMICHALA, food[Restaurants.ZASTAVKAUMICHALA])}
{food[Restaurants.SENKSERIKOVA] && renderFoodTable(Locations.SENKSERIKOVA, food[Restaurants.SENKSERIKOVA])}
</Row> </Row>
<div className='content-wrapper'> <div className='content-wrapper'>
<div className='content'> <div className='content'>
{canChangeChoice && <> {(dayIndex == null || data.todayWeekIndex == null || dayIndex >= data.todayWeekIndex) && <>
<p>{`Jak to ${dayIndex == null || dayIndex === data.todayWeekIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}</p> <p>{`Jak to ${dayIndex == null || dayIndex === data.todayWeekIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}</p>
<Form.Select ref={choiceRef} onChange={doAddChoice}> <Form.Select ref={choiceRef} onChange={doAddChoice}>
<option></option> <option></option>
{Object.entries(Locations) {Object.entries(Locations)
.filter(entry => { .filter(entry => {
const locationKey = entry[0] as LocationKey; // TODO: wtf, cos pil, když jsi tohle psal? v2
const restaurantKey = Object.keys(Restaurants).indexOf(locationKey); const key = entry[0];
const locationIndex = Object.keys(Locations).indexOf(key as unknown as Locations);
const locationsKey = Object.keys(Locations)[locationIndex];
const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey);
const v = Object.values(Restaurants)[restaurantKey]; const v = Object.values(Restaurants)[restaurantKey];
return v == null || !food[v]?.closed; return v == null || !food[v]?.closed;
}) })
@@ -454,26 +403,18 @@ function App() {
<p style={{ marginTop: "10px" }}>V kolik hodin preferuješ odchod?</p> <p style={{ marginTop: "10px" }}>V kolik hodin preferuješ odchod?</p>
<Form.Select ref={departureChoiceRef} onChange={handleChangeDepartureTime}> <Form.Select ref={departureChoiceRef} onChange={handleChangeDepartureTime}>
<option></option> <option></option>
{Object.values(DepartureTime) {data.departureTimes.map(time => <option key={time} value={time}>{time}</option>)}
.filter(time => isInTheFuture(time))
.map(time => <option key={time} value={time}>{time}</option>)}
</Form.Select> </Form.Select>
</>} </>}
</>} </>}
{Object.keys(data.choices).length > 0 ? {Object.keys(data.choices).length > 0 ?
<Table bordered className='mt-5'> <Table bordered className='mt-5'>
<tbody> <tbody>
{Object.keys(data.choices).map(key => { {Object.keys(data.choices).map((locationKey: string) => {
const locationKey = key as LocationKey; const locationName = Object.values(Locations)[Number(locationKey)];
const locationName = Locations[locationKey]; const locationLoginList = Object.entries(data.choices[Number(locationKey)]);
const loginObject = data.choices[locationKey];
if (!loginObject) {
return;
}
const locationLoginList = Object.entries(loginObject);
const disabled = false;
return ( return (
<tr key={key}> <tr key={locationKey}>
<td>{locationName}</td> <td>{locationName}</td>
<td className='p-0'> <td className='p-0'>
<Table> <Table>
@@ -484,31 +425,27 @@ function App() {
const userChoices = userPayload?.options; const userChoices = userPayload?.options;
const trusted = userPayload?.trusted || false; const trusted = userPayload?.trusted || false;
return <tr key={index}> return <tr key={index}>
<td> <td className='text-nowrap'>
{trusted && <span className='trusted-icon'> {trusted && <span className='trusted-icon'>
<FontAwesomeIcon title='Uživatel ověřený doménovým přihlášením' icon={faCircleCheck} style={{ cursor: "help" }} /> <FontAwesomeIcon title='Uživatel ověřený doménovým přihlášením' icon={faCircleCheck} style={{ cursor: "help" }} />
</span>} </span>}
{login} {login}
{userPayload.departureTime && <small> ({userPayload.departureTime})</small>} {userPayload.departureTime && <small> ({userPayload.departureTime})</small>}
{userPayload.note && <small style={{ overflowWrap: 'anywhere' }}> ({userPayload.note})</small>} {login === auth.login && <FontAwesomeIcon onClick={() => {
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => { doRemoveChoices(locationKey);
setNoteModalOpen(true);
}} title='Upravit poznámku' className='action-icon' icon={faNoteSticky} />}
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => {
doRemoveChoices(key as LocationKey);
}} title={`Odstranit volbu ${locationName}, včetně případných zvolených jídel`} className='action-icon' icon={faTrashCan} />} }} title={`Odstranit volbu ${locationName}, včetně případných zvolených jídel`} className='action-icon' icon={faTrashCan} />}
</td> </td>
{userChoices?.length && food ? <td> {userChoices?.length && food ? <td className='w-100'>
<ul> <ul>
{userChoices?.map(foodIndex => { {userChoices?.map(foodIndex => {
// TODO narovnat, tohle je zbytečně složité const locationsKey = Object.keys(Locations)[Number(locationKey)]
const restaurantKey = Object.keys(Restaurants).indexOf(key); const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey);
const restaurant = Object.values(Restaurants)[restaurantKey]; const restaurant = Object.values(Restaurants)[restaurantKey];
const foodName = food[restaurant]?.food[foodIndex].name; const foodName = food[restaurant]?.food[foodIndex].name;
return <li key={foodIndex}> return <li key={foodIndex}>
{foodName} {foodName}
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => { {login === auth.login && <FontAwesomeIcon onClick={() => {
doRemoveFoodChoice(key as LocationKey, foodIndex); doRemoveFoodChoice(locationKey, foodIndex);
}} title={`Odstranit ${foodName}`} className='action-icon' icon={faTrashCan} />} }} title={`Odstranit ${foodName}`} className='action-icon' icon={faTrashCan} />}
</li> </li>
})} })}
@@ -520,9 +457,6 @@ function App() {
</tbody> </tbody>
</Table> </Table>
</td> </td>
<td>
<Button onClick={() => doJdemeObed(locationKey)} disabled={false}>Jdeme na oběd</Button>
</td>
</tr>) </tr>)
} }
)} )}
@@ -541,12 +475,10 @@ function App() {
<FontAwesomeIcon icon={faGear} className='fa-spin' /> Zjišťujeme dostupné pizzy <FontAwesomeIcon icon={faGear} className='fa-spin' /> Zjišťujeme dostupné pizzy
</span> </span>
: :
<>
<Button onClick={async () => { <Button onClick={async () => {
setLoadingPizzaDay(true); setLoadingPizzaDay(true);
await createPizzaDay().then(() => setLoadingPizzaDay(false)); await createPizzaDay().then(() => setLoadingPizzaDay(false));
}}>Založit Pizza day</Button> }}>Založit Pizza day</Button>
</>
} }
</div> </div>
} }
@@ -603,7 +535,7 @@ function App() {
await lockPizzaDay(); await lockPizzaDay();
}}>Vrátit do "uzamčeno"</Button> }}>Vrátit do "uzamčeno"</Button>
<Button className='danger mb-3' style={{ marginLeft: '20px' }} title="Nastaví stav na 'Doručeno' - koncový stav." onClick={async () => { <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(bank?.bankAccount, bank?.holderName);
}}>Doručeno</Button> }}>Doručeno</Button>
</div> </div>
} }
@@ -623,19 +555,16 @@ function App() {
options={pizzaSuggestions} options={pizzaSuggestions}
placeholder='Vyhledat pizzu...' placeholder='Vyhledat pizzu...'
onChange={handlePizzaChange} onChange={handlePizzaChange}
onBlur={_ => { }}
onFocus={_ => { }}
/> />
Poznámka: <input ref={pizzaPoznamkaRef} className='mt-3' type="text" onKeyDown={event => { Poznámka: <input ref={poznamkaRef} className='mt-3' type="text" onKeyDown={event => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
handlePizzaPoznamkaChange(); handlePoznamkaChange();
} }
event.stopPropagation();
}} /> }} />
<Button <Button
style={{ marginLeft: '20px' }} style={{ marginLeft: '20px' }}
disabled={!myOrder?.pizzaList?.length} disabled={!myOrder?.pizzaList?.length}
onClick={handlePizzaPoznamkaChange}> onClick={handlePoznamkaChange}>
Uložit Uložit
</Button> </Button>
</div> </div>
@@ -656,7 +585,6 @@ function App() {
</>} </>}
</div> </div>
<Footer /> <Footer />
<NoteModal isOpen={noteModalOpen} onClose={() => setNoteModalOpen(false)} onSave={saveNote} />
</> </>
); );
} }

View File

@@ -1,4 +1,14 @@
import {DepartureTime} from "../../types"; /**
* Vrátí kořenovou URL serveru na základě aktuálního prostředí (vývojovou či produkční).
*
* @returns kořenová URL serveru
*/
export const getBaseUrl = (): string => {
if (process.env.PUBLIC_URL) {
return process.env.PUBLIC_URL;
}
return 'http://127.0.0.1:3001';
}
const TOKEN_KEY = "token"; const TOKEN_KEY = "token";
@@ -26,39 +36,3 @@ export const getToken = (): string | null => {
export const deleteToken = () => { export const deleteToken = () => {
localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(TOKEN_KEY);
} }
/**
* Vrátí human-readable reprezentaci předaného data a času pro zobrazení.
* Příklady:
* - dnes 10:52
* - 10.05.2023 10:52
*/
export function getHumanDateTime(datetime: Date) {
let hours = String(datetime.getHours()).padStart(2, '0');
let minutes = String(datetime.getMinutes()).padStart(2, "0");
if (new Date().toDateString() === datetime.toDateString()) {
return `dnes ${hours}:${minutes}`;
} else {
let day = String(datetime.getDate()).padStart(2, '0');
let month = String(datetime.getMonth() + 1).padStart(2, "0");
let year = datetime.getFullYear();
return `${day}.${month}.${year} ${hours}:${minutes}`;
}
}
/**
* Vrátí true, pokud je předaný čas větší než aktuální čas.
*/
export function isInTheFuture(time: DepartureTime) {
const now = new Date();
const currentHours = now.getHours();
const currentMinutes = now.getMinutes();
const currentDate = now.toDateString();
const [hours, minutes] = time.split(':').map(Number);
if (currentDate === now.toDateString()) {
return hours > currentHours || (hours === currentHours && minutes > currentMinutes);
}
return true;
}

View File

@@ -1,5 +1,5 @@
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { getToken } from "../Utils"; import { getBaseUrl, getToken } from "../Utils";
/** /**
* Wrapper pro volání API, u kterých chceme automaticky zobrazit toaster s chybou ze serveru. * Wrapper pro volání API, u kterých chceme automaticky zobrazit toaster s chybou ze serveru.
@@ -23,38 +23,13 @@ async function request<TResponse>(
config.headers = config?.headers ? new Headers(config.headers) : new Headers(); config.headers = config?.headers ? new Headers(config.headers) : new Headers();
config.headers.set("Authorization", `Bearer ${getToken()}`); config.headers.set("Authorization", `Bearer ${getToken()}`);
try { try {
const response = await fetch(url, config); const response = await fetch(getBaseUrl() + url, config);
if (!response.ok) { 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(); const json = await response.json();
// Vyhodíme samotnou hlášku z odpovědi, odchytí si jí errorHandler // Vyhodíme samotnou hlášku z odpovědi, odchytí si jí errorHandler
throw new Error(json.error); throw new Error(json.error);
} }
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
return response.json() as TResponse; 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) { } catch (e) {
return Promise.reject(e); return Promise.reject(e);
} }
@@ -62,12 +37,11 @@ async function blobRequest(
export const api = { export const api = {
get: <TResponse>(url: string) => request<TResponse>(url), get: <TResponse>(url: string) => request<TResponse>(url),
blobGet: (url: string) => blobRequest(url), post: <TBody extends BodyInit, TResponse>(url: string, body: TBody) => request<TResponse>(url, { method: 'POST', body, headers: { 'Content-Type': 'application/json' } }),
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) => { export const getQrUrl = (login: string) => {
return `/api/qr?login=${login}`; return `${getBaseUrl()}/api/qr?login=${login}`;
} }
export const getData = async (dayIndex?: number) => { export const getData = async (dayIndex?: number) => {
@@ -79,5 +53,5 @@ export const getData = async (dayIndex?: number) => {
} }
export const login = async (login?: string) => { export const login = async (login?: string) => {
return await api.post<any, any>('/api/login', { login }); return await api.post<any, any>('/api/login', JSON.stringify({ 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,36 +1,19 @@
import {
AddChoiceRequest,
ChangeDepartureTimeRequest,
JdemeObedRequest,
LocationKey,
RemoveChoiceRequest,
RemoveChoicesRequest,
UpdateNoteRequest
} from "../../../types";
import { api } from "./Api"; import { api } from "./Api";
const FOOD_API_PREFIX = '/api/food'; const FOOD_API_PREFIX = '/api/food';
export const addChoice = async (locationKey: LocationKey, foodIndex?: number, dayIndex?: number) => { export const addChoice = async (locationIndex: number, foodIndex?: number, dayIndex?: number) => {
return await api.post<AddChoiceRequest, void>(`${FOOD_API_PREFIX}/addChoice`, { locationKey, foodIndex, dayIndex }); return await api.post<any, any>(`${FOOD_API_PREFIX}/addChoice`, JSON.stringify({ locationIndex, foodIndex, dayIndex }));
} }
export const removeChoices = async (locationKey: LocationKey, dayIndex?: number) => { export const removeChoices = async (locationIndex: number, dayIndex?: number) => {
return await api.post<RemoveChoicesRequest, void>(`${FOOD_API_PREFIX}/removeChoices`, { locationKey, dayIndex }); return await api.post<any, any>(`${FOOD_API_PREFIX}/removeChoices`, JSON.stringify({ locationIndex, dayIndex }));
} }
export const removeChoice = async (locationKey: LocationKey, foodIndex: number, dayIndex?: number) => { export const removeChoice = async (locationIndex: number, foodIndex: number, dayIndex?: number) => {
return await api.post<RemoveChoiceRequest, void>(`${FOOD_API_PREFIX}/removeChoice`, { locationKey, foodIndex, dayIndex }); return await api.post<any, any>(`${FOOD_API_PREFIX}/removeChoice`, JSON.stringify({ locationIndex, 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) => { export const changeDepartureTime = async (time: string, dayIndex?: number) => {
return await api.post<ChangeDepartureTimeRequest, void>(`${FOOD_API_PREFIX}/changeDepartureTime`, { time, dayIndex }); return await api.post<any, any>(`${FOOD_API_PREFIX}/changeDepartureTime`, JSON.stringify({ time, dayIndex }));
}
export const jdemeObed = async (locationKey: LocationKey) => {
return await api.post<JdemeObedRequest, void>(`${FOOD_API_PREFIX}/jdemeObed`, { locationKey });
} }

View File

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

View File

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

View File

@@ -2,6 +2,6 @@ import { Navbar } from "react-bootstrap";
export default function Footer() { export default function Footer() {
return <Navbar className="text-light" variant='dark' expand="lg" style={{ display: "flex", justifyContent: "center" }}> return <Navbar className="text-light" variant='dark' expand="lg" style={{ display: "flex", justifyContent: "center" }}>
<span>🄯 Žádná práva nevyhrazena. TODO a zdrojové kódy dostupné <a href="https://gitea.melancholik.eu/mates/Luncher">zde</a>.</span> <span>🄯 Kancelář 51, Marbes s.r.o. Žádná práva nevyhrazena. TODO a zdrojové kódy dostupné <a href="https://gitea.melancholik.eu/mates/Luncher">zde</a>.</span>
</Navbar > </Navbar >
} }

View File

@@ -1,21 +1,19 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Navbar, Nav, NavDropdown } from "react-bootstrap"; import { Navbar, Nav, NavDropdown } from "react-bootstrap";
import { useAuth } from "../context/auth"; import { useAuth } from "../context/auth";
import SettingsModal from "./modals/SettingsModal"; import BankAccountModal from "./modals/BankAccountModal";
import { useSettings } from "../context/settings"; import { useBank } from "../context/bank";
import FeaturesVotingModal from "./modals/FeaturesVotingModal"; import FeaturesVotingModal from "./modals/FeaturesVotingModal";
import { FeatureRequest } from "../../../types"; import { FeatureRequest } from "../types";
import { errorHandler } from "../api/Api"; import { errorHandler } from "../api/Api";
import { getFeatureVotes, updateFeatureVote } from "../api/VotingApi"; import { getFeatureVotes, updateFeatureVote } from "../api/VotingApi";
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
export default function Header() { export default function Header() {
const auth = useAuth(); const auth = useAuth();
const settings = useSettings(); const bank = useBank();
const [settingsModalOpen, setSettingsModalOpen] = useState<boolean>(false); const [bankModalOpen, setBankModalOpen] = useState<boolean>(false);
const [votingModalOpen, setVotingModalOpen] = 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[]>([]);
useEffect(() => { useEffect(() => {
@@ -26,18 +24,14 @@ export default function Header() {
} }
}, [auth?.login]); }, [auth?.login]);
const closeSettingsModal = () => { const closeBankModal = () => {
setSettingsModalOpen(false); setBankModalOpen(false);
} }
const closeVotingModal = () => { const closeVotingModal = () => {
setVotingModalOpen(false); setVotingModalOpen(false);
} }
const closePizzaModal = () => {
setPizzaModalOpen(false);
}
const isValidInteger = (str: string) => { const isValidInteger = (str: string) => {
str = str.trim(); str = str.trim();
if (!str) { if (!str) {
@@ -48,7 +42,7 @@ export default function Header() {
return n !== Infinity && String(n) === str && n >= 0; return n !== Infinity && String(n) === str && n >= 0;
} }
const saveSettings = (bankAccountNumber?: string, bankAccountHolderName?: string, hideSoupsOption?: boolean) => { const saveBankAccount = (bankAccountNumber?: string, bankAccountHolderName?: string) => {
if (bankAccountNumber) { if (bankAccountNumber) {
try { try {
// Validace kódu banky // Validace kódu banky
@@ -90,10 +84,9 @@ export default function Header() {
return return
} }
} }
settings?.setBankAccountNumber(bankAccountNumber); bank?.setBankAccountNumber(bankAccountNumber);
settings?.setBankAccountHolderName(bankAccountHolderName); bank?.setBankAccountHolderName(bankAccountHolderName);
settings?.setHideSoupsOption(hideSoupsOption); closeBankModal();
closeSettingsModal();
} }
const saveFeatureVote = async (option: FeatureRequest, active: boolean) => { const saveFeatureVote = async (option: FeatureRequest, active: boolean) => {
@@ -113,16 +106,13 @@ export default function Header() {
<Navbar.Collapse id="basic-navbar-nav"> <Navbar.Collapse id="basic-navbar-nav">
<Nav className="nav"> <Nav className="nav">
<NavDropdown align="end" title={auth?.login} id="basic-nav-dropdown"> <NavDropdown align="end" title={auth?.login} id="basic-nav-dropdown">
<NavDropdown.Item onClick={() => setSettingsModalOpen(true)}>Nastavení</NavDropdown.Item> <NavDropdown.Item onClick={() => setBankModalOpen(true)}>Nastavit číslo účtu</NavDropdown.Item>
<NavDropdown.Item onClick={() => setVotingModalOpen(true)}>Hlasovat o nových funkcích</NavDropdown.Item> <NavDropdown.Item onClick={() => setVotingModalOpen(true)}>Hlasovat o nových funkcích</NavDropdown.Item>
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
<NavDropdown.Divider />
<NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item> <NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item>
</NavDropdown> </NavDropdown>
</Nav> </Nav>
</Navbar.Collapse> </Navbar.Collapse>
<SettingsModal isOpen={settingsModalOpen} onClose={closeSettingsModal} onSave={saveSettings} /> <BankAccountModal isOpen={bankModalOpen} onClose={closeBankModal} onSave={saveBankAccount} />
<FeaturesVotingModal isOpen={votingModalOpen} onClose={closeVotingModal} onChange={saveFeatureVote} initialValues={featureVotes} /> <FeaturesVotingModal isOpen={votingModalOpen} onClose={closeVotingModal} onChange={saveFeatureVote} initialValues={featureVotes} />
<PizzaCalculatorModal isOpen={pizzaModalOpen} onClose={closePizzaModal} />
</Navbar> </Navbar>
} }

View File

@@ -1,5 +1,5 @@
import { Table } from "react-bootstrap"; import { Table } from "react-bootstrap";
import { Order, PizzaDayState, PizzaOrder } from "../../../types"; import { Order, PizzaDayState, PizzaOrder } from "../types";
import PizzaOrderRow from "./PizzaOrderRow"; import PizzaOrderRow from "./PizzaOrderRow";
import { updatePizzaFee } from "../api/PizzaDayApi"; import { updatePizzaFee } from "../api/PizzaDayApi";

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMoneyBill1, faTrashCan } from "@fortawesome/free-regular-svg-icons"; import { faMoneyBill1, faTrashCan } from "@fortawesome/free-regular-svg-icons";
import { useAuth } from "../context/auth"; import { useAuth } from "../context/auth";
import { Order, PizzaDayState, PizzaOrder } from "../../../types"; import { Order, PizzaDayState, PizzaOrder } from "../types";
import PizzaAdditionalFeeModal from "./modals/PizzaAdditionalFeeModal"; import PizzaAdditionalFeeModal from "./modals/PizzaAdditionalFeeModal";
type Props = { type Props = {

View File

@@ -1,40 +1,33 @@
import { useRef } from "react"; import { useRef } from "react";
import { Modal, Button } from "react-bootstrap" import { Modal, Button } from "react-bootstrap"
import { useSettings } from "../../context/settings"; import { useBank } from "../../context/bank";
type Props = { type Props = {
isOpen: boolean, isOpen: boolean,
onClose: () => void, onClose: () => void,
onSave: (bankAccountNumber?: string, bankAccountHolderName?: string, hideSoupsOption?: boolean) => void, onSave: (bankAccountNumber?: string, bankAccountHolderName?: string) => void,
} }
/** Modální dialog pro uživatelská nastavení. */ /** Modální dialog pro nastavení čísla účtu a jména majitele. */
export default function SettingsModal({ isOpen, onClose, onSave }: Props) { export default function BankAccountModal({ isOpen, onClose, onSave }: Props) {
const settings = useSettings(); const bank = useBank();
const bankAccountRef = useRef<HTMLInputElement>(null); const bankAccountRef = useRef<HTMLInputElement>(null);
const nameRef = useRef<HTMLInputElement>(null); const nameRef = useRef<HTMLInputElement>(null);
const hideSoupsRef = useRef<HTMLInputElement>(null);
return <Modal show={isOpen} onHide={onClose} size="lg"> return <Modal show={isOpen} onHide={onClose} size="lg">
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title><h2>Nastavení</h2></Modal.Title> <Modal.Title>Bankovní účet</Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<h4>Obecné</h4>
<span title="V nabídkách nebudou zobrazovány polévky. Tato funkce je experimentální, a zejména u TechTower bývá často problém polévky spolehlivě rozeznat. V případě využití této funkce průběžně nahlašujte stále se zobrazující polévky." style={{ "cursor": "help" }}>
<input ref={hideSoupsRef} type="checkbox" defaultChecked={settings?.hideSoups} /> Skrýt polévky
</span>
<hr />
<h4>Bankovní účet</h4>
<p>Nastavením čísla účtu umožníte automatické generování QR kódů pro úhradu za vámi provedené objednávky v rámci Pizza day.<br />Pokud vaše číslo účtu neobsahuje předčíslí, je možné ho zcela vynechat.<br /><br />Číslo účtu není ukládáno na serveru, posílá se na něj pouze za účelem vygenerování QR kódů.</p> <p>Nastavením čísla účtu umožníte automatické generování QR kódů pro úhradu za vámi provedené objednávky v rámci Pizza day.<br />Pokud vaše číslo účtu neobsahuje předčíslí, je možné ho zcela vynechat.<br /><br />Číslo účtu není ukládáno na serveru, posílá se na něj pouze za účelem vygenerování QR kódů.</p>
Číslo účtu: <input className="mb-3" ref={bankAccountRef} type="text" placeholder="123456-1234567890/1234" defaultValue={settings?.bankAccount} onKeyDown={e => e.stopPropagation()} /> <br /> Číslo účtu: <input className="mb-3" ref={bankAccountRef} type="text" placeholder="123456-1234567890/1234" defaultValue={bank?.bankAccount} /> <br />
Název příjemce (jméno majitele účtu): <input ref={nameRef} type="text" placeholder="Jan Novák" defaultValue={settings?.holderName} onKeyDown={e => e.stopPropagation()} /> Název příjemce (jméno majitele účtu): <input ref={nameRef} type="text" placeholder="Jan Novák" defaultValue={bank?.holderName} />
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button variant="secondary" onClick={onClose}> <Button variant="secondary" onClick={onClose}>
Storno Storno
</Button> </Button>
<Button variant="primary" onClick={() => onSave(bankAccountRef.current?.value, nameRef.current?.value, hideSoupsRef.current?.checked)}> <Button variant="primary" onClick={() => onSave(bankAccountRef.current?.value, nameRef.current?.value)}>
Uložit Uložit
</Button> </Button>
</Modal.Footer> </Modal.Footer>

View File

@@ -1,5 +1,5 @@
import { Modal, Button, Form } from "react-bootstrap" import { Modal, Button, Form } from "react-bootstrap"
import { FeatureRequest } from "../../../../types"; import { FeatureRequest } from "../../types";
type Props = { type Props = {
isOpen: boolean, isOpen: boolean,
@@ -19,7 +19,7 @@ export default function FeaturesVotingModal({ isOpen, onClose, onChange, initial
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title> <Modal.Title>
Hlasujte pro nové funkce Hlasujte pro nové funkce
<p style={{ fontSize: '12px' }}>Je možno vybrat maximálně 4 možnosti</p> <p style={{ fontSize: '12px' }}>Je možno vybrat maximálně 3 možnosti</p>
</Modal.Title> </Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>

View File

@@ -1,36 +0,0 @@
import { useRef } from "react";
import { Modal, Button, Form } from "react-bootstrap"
type Props = {
isOpen: boolean,
onClose: () => void,
onSave: (note?: string) => void,
}
/** Modální dialog pro úpravu obecné poznámky. */
export default function NoteModal({ isOpen, onClose, onSave }: Props) {
const note = useRef<HTMLInputElement>(null);
const save = () => {
onSave(note?.current?.value);
}
return <Modal show={isOpen} onHide={onClose}>
<Modal.Header closeButton>
<Modal.Title>Úprava poznámky</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form.Control ref={note} autoFocus={true} type="text" id="note" onKeyDown={event => {
if (event.key === 'Enter') {
save();
}
event.stopPropagation();
}} />
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={save}>
Uložit
</Button>
</Modal.Footer>
</Modal>
}

View File

@@ -1,141 +0,0 @@
import { useRef, useState } from "react";
import { Modal, Button, Row, Col } from "react-bootstrap"
type Props = {
isOpen: boolean,
onClose: () => void,
}
type Result = {
pizza1?: {
diameter?: number,
area?: number,
pricePerM?: number,
},
pizza2?: {
diameter?: number,
area?: number,
pricePerM?: number,
}
choice?: number,
ratio?: number,
diameterDiff?: number,
}
/** Modální dialog pro výpočet výhodnosti pizzy. */
export default function PizzaCalculatorModal({ isOpen, onClose }: Props) {
const diameter1Ref = useRef<HTMLInputElement>(null);
const price1Ref = useRef<HTMLInputElement>(null);
const diameter2Ref = useRef<HTMLInputElement>(null);
const price2Ref = useRef<HTMLInputElement>(null);
const [result, setResult] = useState<Result | null>(null);
const recalculate = () => {
const r: Result = { ...result }
// 1. pizza
if (diameter1Ref.current?.value) {
const diameter1 = parseInt(diameter1Ref.current?.value);
if (!r.pizza1) {
r.pizza1 = {};
}
if (diameter1 && diameter1 > 0) {
r.pizza1.diameter = diameter1;
r.pizza1.area = Math.PI * Math.pow(diameter1 / 2, 2);
if (price1Ref.current?.value) {
const price1 = parseInt(price1Ref.current?.value);
if (price1) {
r.pizza1.pricePerM = price1 / r.pizza1.area;
} else {
r.pizza1.pricePerM = undefined;
}
}
} else {
r.pizza1.area = undefined;
}
}
// 2. pizza
if (diameter2Ref.current?.value) {
const diameter2 = parseInt(diameter2Ref.current?.value);
if (!r.pizza2) {
r.pizza2 = {};
}
if (diameter2 && diameter2 > 0) {
r.pizza2.diameter = diameter2;
r.pizza2.area = Math.PI * Math.pow(diameter2 / 2, 2);
if (price2Ref.current?.value) {
const price2 = parseInt(price2Ref.current?.value);
if (price2) {
r.pizza2.pricePerM = price2 / r.pizza2.area;
} else {
r.pizza2.pricePerM = undefined;
}
}
} else {
r.pizza2.area = undefined;
}
}
// Srovnání
if (r.pizza1?.pricePerM && r.pizza2?.pricePerM && r.pizza1.diameter && r.pizza2.diameter) {
r.choice = r.pizza1.pricePerM < r.pizza2.pricePerM ? 1 : 2;
const bigger = r.pizza1.pricePerM > r.pizza2.pricePerM ? r.pizza1.pricePerM : r.pizza2.pricePerM;
const smaller = r.pizza1.pricePerM < r.pizza2.pricePerM ? r.pizza1.pricePerM : r.pizza2.pricePerM;
r.ratio = (bigger / smaller) - 1;
r.diameterDiff = Math.abs(r.pizza1.diameter - r.pizza2.diameter);
} else {
r.choice = undefined;
r.ratio = undefined;
r.diameterDiff = undefined;
}
setResult(r);
}
const close = () => {
setResult(null);
onClose();
}
return <Modal show={isOpen} onHide={onClose}>
<Modal.Header closeButton>
<Modal.Title>Pizza kalkulačka</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>Zadejte parametry pizzy pro jejich srovnání.</p>
<Row>
<Col size="6">
<input className="mb-3" ref={diameter1Ref} type="number" step="1" min="1" placeholder="Průměr 1. pizzy (cm)" onChange={recalculate} onKeyDown={e => e.stopPropagation()} />
</Col>
<Col size="6">
<input className="mb-3" ref={diameter2Ref} type="number" step="1" min="1" placeholder="Průměr 2. pizzy (cm)" onChange={recalculate} onKeyDown={e => e.stopPropagation()} />
</Col>
</Row>
<Row>
<Col size="6">
<input className="mb-3" ref={price1Ref} type="number" min="1" placeholder="Cena 1. pizzy (Kč)" onChange={recalculate} onKeyDown={e => e.stopPropagation()} />
</Col>
<Col size="6">
<input className="mb-3" ref={price2Ref} type="number" min="1" placeholder="Cena 2. pizzy (Kč)" onChange={recalculate} onKeyDown={e => e.stopPropagation()} />
</Col>
</Row>
<Row>
<Col size="6">
{result?.pizza1?.area && <p>Plocha: <b>{Math.round(result.pizza1.area * 10) / 10}</b> cm²</p>}
{result?.pizza1?.pricePerM && <p>Cena za m²: <b>{Math.round(result.pizza1.pricePerM * 1000000) / 100}</b> </p>}
</Col>
<Col size="6">
{result?.pizza2?.area && <p>Plocha: <b>{Math.round(result.pizza2.area * 10) / 10}</b> cm²</p>}
{result?.pizza2?.pricePerM && <p>Cena za m²: <b>{Math.round(result.pizza2.pricePerM * 1000000) / 100}</b> </p>}
</Col>
</Row>
{(result?.choice && result?.ratio && result?.ratio > 0 && result?.diameterDiff != null && <p><b>{result.choice}. pizza</b> je zhruba o <b>{Math.round(result.ratio * 1000) / 10}%</b> výhodnější než {result.choice === 1 ? "2" : "1"}. pizza.</p>) || ''}
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={close}>
Zavřít
</Button>
</Modal.Footer>
</Modal>
}

View File

@@ -5,7 +5,6 @@ import { deleteToken, getToken, storeToken } from "../Utils";
export type AuthContextProps = { export type AuthContextProps = {
login?: string, login?: string,
trusted?: boolean,
setToken: (name: string) => void, setToken: (name: string) => void,
logout: () => void, logout: () => void,
} }
@@ -27,7 +26,6 @@ export const useAuth = () => {
function useProvideAuth(): AuthContextProps { function useProvideAuth(): AuthContextProps {
const [loginName, setLoginName] = useState<string | undefined>(); const [loginName, setLoginName] = useState<string | undefined>();
const [trusted, setTrusted] = useState<boolean | undefined>();
const [token, setToken] = useState<string | null>(getToken()); const [token, setToken] = useState<string | null>(getToken());
const { decodedToken } = useJwt(token || ''); const { decodedToken } = useJwt(token || '');
@@ -42,10 +40,8 @@ function useProvideAuth(): AuthContextProps {
useEffect(() => { useEffect(() => {
if (decodedToken) { if (decodedToken) {
setLoginName((decodedToken as any).login); setLoginName((decodedToken as any).login);
setTrusted((decodedToken as any).trusted);
} else { } else {
setLoginName(undefined); setLoginName(undefined);
setTrusted(undefined);
} }
}, [decodedToken]); }, [decodedToken]);
@@ -54,7 +50,6 @@ function useProvideAuth(): AuthContextProps {
const logoutUrl = (decodedToken as any).logoutUrl; const logoutUrl = (decodedToken as any).logoutUrl;
setToken(null); setToken(null);
setLoginName(undefined); setLoginName(undefined);
setTrusted(undefined);
if (trusted && logoutUrl?.length) { if (trusted && logoutUrl?.length) {
window.location.replace(logoutUrl); window.location.replace(logoutUrl);
} }
@@ -62,7 +57,6 @@ function useProvideAuth(): AuthContextProps {
return { return {
login: loginName, login: loginName,
trusted,
setToken, setToken,
logout, logout,
} }

View File

@@ -3,36 +3,32 @@ import { useEffect } from "react"
const BANK_ACCOUNT_NUMBER_KEY = 'bank_account_number'; const BANK_ACCOUNT_NUMBER_KEY = 'bank_account_number';
const BANK_ACCOUNT_HOLDER_KEY = 'bank_account_holder_name'; const BANK_ACCOUNT_HOLDER_KEY = 'bank_account_holder_name';
const HIDE_SOUPS_KEY = 'hide_soups';
export type SettingsContextProps = { export type BankContextProps = {
bankAccount?: string, bankAccount?: string,
holderName?: string, holderName?: string,
hideSoups?: boolean,
setBankAccountNumber: (accountNumber?: string) => void, setBankAccountNumber: (accountNumber?: string) => void,
setBankAccountHolderName: (holderName?: string) => void, setBankAccountHolderName: (holderName?: string) => void,
setHideSoupsOption: (hideSoups?: boolean) => void,
} }
type ContextProps = { type ContextProps = {
children: ReactNode children: ReactNode
} }
const settingsContext = React.createContext<SettingsContextProps | null>(null); const bankContext = React.createContext<BankContextProps | null>(null);
export function ProvideSettings(props: ContextProps) { export function ProvideBank(props: ContextProps) {
const settings = useProvideSettings(); const bank = useProvideBank();
return <settingsContext.Provider value={settings}>{props.children}</settingsContext.Provider> return <bankContext.Provider value={bank}>{props.children}</bankContext.Provider>
} }
export const useSettings = () => { export const useBank = () => {
return useContext(settingsContext); return useContext(bankContext);
} }
function useProvideSettings(): SettingsContextProps { function useProvideBank(): BankContextProps {
const [bankAccount, setBankAccount] = useState<string | undefined>(); const [bankAccount, setBankAccount] = useState<string | undefined>();
const [holderName, setHolderName] = useState<string | undefined>(); const [holderName, setHolderName] = useState<string | undefined>();
const [hideSoups, setHideSoups] = useState<boolean | undefined>();
useEffect(() => { useEffect(() => {
const accountNumber = localStorage.getItem(BANK_ACCOUNT_NUMBER_KEY); const accountNumber = localStorage.getItem(BANK_ACCOUNT_NUMBER_KEY);
@@ -43,10 +39,6 @@ function useProvideSettings(): SettingsContextProps {
if (holderName) { if (holderName) {
setHolderName(holderName); setHolderName(holderName);
} }
const hideSoups = localStorage.getItem(HIDE_SOUPS_KEY);
if (hideSoups !== null) {
setHideSoups(hideSoups === 'true' ? true : false);
}
}, []) }, [])
useEffect(() => { useEffect(() => {
@@ -65,14 +57,6 @@ function useProvideSettings(): SettingsContextProps {
} }
}, [holderName]); }, [holderName]);
useEffect(() => {
if (hideSoups) {
localStorage.setItem(HIDE_SOUPS_KEY, hideSoups ? 'true' : 'false');
} else {
localStorage.removeItem(HIDE_SOUPS_KEY);
}
}, [hideSoups]);
function setBankAccountNumber(bankAccount?: string) { function setBankAccountNumber(bankAccount?: string) {
setBankAccount(bankAccount); setBankAccount(bankAccount);
} }
@@ -81,16 +65,10 @@ function useProvideSettings(): SettingsContextProps {
setHolderName(holderName); setHolderName(holderName);
} }
function setHideSoupsOption(hideSoups?: boolean) {
setHideSoups(hideSoups);
}
return { return {
bankAccount, bankAccount,
holderName, holderName,
hideSoups,
setBankAccountNumber, setBankAccountNumber,
setBankAccountHolderName, setBankAccountHolderName,
setHideSoupsOption,
} }
} }

View File

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

View File

@@ -4,10 +4,9 @@ import App from './App';
import { SocketContext, socket } from './context/socket'; import { SocketContext, socket } from './context/socket';
import { ProvideAuth } from './context/auth'; import { ProvideAuth } from './context/auth';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import { ProvideSettings } from './context/settings'; import { ProvideBank } from './context/bank';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import './index.css'; import './index.css';
import Snowfall from 'react-snowfall';
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
@@ -15,19 +14,12 @@ const root = ReactDOM.createRoot(
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<ProvideAuth> <ProvideAuth>
<ProvideSettings> <ProvideBank>
<SocketContext.Provider value={socket}> <SocketContext.Provider value={socket}>
<>
<Snowfall style={{
zIndex: 2,
position: 'fixed',
width: '100vw',
height: '100vh'}} />
<App /> <App />
</>
<ToastContainer /> <ToastContainer />
</SocketContext.Provider> </SocketContext.Provider>
</ProvideSettings> </ProvideBank>
</ProvideAuth> </ProvideAuth>
</React.StrictMode> </React.StrictMode>
); );

1
client/src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -1,14 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "es5",
"lib": [ "lib": [
"dom", "dom",
"dom.iterable", "dom.iterable",
"esnext" "esnext"
], ],
"types": [
"vite/client"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,16 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import viteTsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
// depending on your application, base can also be "/"
base: '',
plugins: [react(), viteTsconfigPaths()],
server: {
open: true,
port: 3000,
proxy: {
'/api': 'http://localhost:3001',
}
},
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
version: '3.8'
services: services:
redis: redis:
image: redis/redis-stack-server:7.2.0-RC3 image: redis/redis-stack-server:7.2.0-RC3

7
package.json Normal file
View File

@@ -0,0 +1,7 @@
{
"private": true,
"workspaces": [
"client",
"server"
]
}

View File

@@ -24,17 +24,3 @@
# To je užitečné pro odesílání upozornění na různé servery Gotify s různými klíči API. # To je užitečné pro odesílání upozornění na různé servery Gotify s různými klíči API.
# Struktura dat je ve formátu JSON a je uložena jako řetězec. # Struktura dat je ve formátu JSON a je uložena jako řetězec.
# GOTIFY_SERVERS_AND_KEYS='[{"server":"https://notification.server.eu", "api_keys":["key1", "key2"]},{"server":"https://notification.server2.eu", "api_keys":["key3", "key4"]}]' # GOTIFY_SERVERS_AND_KEYS='[{"server":"https://notification.server.eu", "api_keys":["key1", "key2"]},{"server":"https://notification.server2.eu", "api_keys":["key3", "key4"]}]'
#NTFY_HOST = "http://192.168.0.113:80"
#NTFY_USERNAME="username"
#NTFY_PASSWD="password"
# Zapne přihlašování pomocí důvěryhodných hlaviček (trusted headers). Výchozí hodnota je false.
# V případě zapnutí je nutno vyplnit také HTTP_REMOTE_TRUSTED_IPS.
# HTTP_REMOTE_USER_ENABLED=true
# Seznam IP adres nebo rozsahů oddělených čárkou, ze kterých budou akceptovány důvěryhodné hlavičky.
# HTTP_REMOTE_TRUSTED_IPS=127.0.0.1,192.168.1.0/24
# Název důvěryhodné hlavičky obsahující login uživatele. Výchozí hodnota je 'remote-user'.
# HTTP_REMOTE_USER_HEADER_NAME=remote-user

4
server/.gitignore vendored
View File

@@ -1,5 +1,5 @@
/node_modules
/dist /dist
/resources/easterEggs data.json
.env.production .env.production
.env.development .env.development
.easter-eggs.json

View File

@@ -6,7 +6,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "ts-node src/index.ts", "start": "ts-node src/index.ts",
"startReload": "nodemon --watch src src/index.ts", "startReload": "nodemon src/index.ts",
"build": "tsc -p .", "build": "tsc -p .",
"test": "jest" "test": "jest"
}, },
@@ -15,13 +15,12 @@
"@babel/preset-env": "^7.22.20", "@babel/preset-env": "^7.22.20",
"@babel/preset-typescript": "^7.23.0", "@babel/preset-typescript": "^7.23.0",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.14", "@types/jsonwebtoken": "^9.0.2",
"@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.2.5",
"@types/node": "^20.11.20",
"@types/request-promise": "^4.1.48", "@types/request-promise": "^4.1.48",
"babel-jest": "^29.7.0", "babel-jest": "^29.7.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"nodemon": "^3.1.0", "nodemon": "^2.0.22",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.0.2" "typescript": "^5.0.2"
}, },
@@ -29,7 +28,7 @@
"axios": "^1.4.0", "axios": "^1.4.0",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.1.3",
"express": "^4.18.2", "express": "^4.18.2",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"redis": "^4.6.7", "redis": "^4.6.7",

View File

@@ -11,7 +11,6 @@ import { initWebsocket } from "./websocket";
import pizzaDayRoutes from "./routes/pizzaDayRoutes"; import pizzaDayRoutes from "./routes/pizzaDayRoutes";
import foodRoutes from "./routes/foodRoutes"; import foodRoutes from "./routes/foodRoutes";
import votingRoutes from "./routes/votingRoutes"; import votingRoutes from "./routes/votingRoutes";
import easterEggRoutes from "./routes/easterEggRoutes";
const ENVIRONMENT = process.env.NODE_ENV || 'production'; const ENVIRONMENT = process.env.NODE_ENV || 'production';
dotenv.config({ path: path.resolve(__dirname, `./.env.${ENVIRONMENT}`) }); dotenv.config({ path: path.resolve(__dirname, `./.env.${ENVIRONMENT}`) });
@@ -32,48 +31,26 @@ app.use(cors({
origin: '*' origin: '*'
})); }));
// Zapínatelný login přes hlavičky - pokud je zapnutý nepovolí "basicauth"
const HTTP_REMOTE_USER_ENABLED = process.env.HTTP_REMOTE_USER_ENABLED === 'true' || false;
const HTTP_REMOTE_USER_HEADER_NAME = process.env.HTTP_REMOTE_USER_HEADER_NAME || 'remote-user';
if (HTTP_REMOTE_USER_ENABLED) {
if (!process.env.HTTP_REMOTE_TRUSTED_IPS) {
throw new Error('Je zapnutý login z hlaviček, ale není nastaven rozsah adres ze kterých hlavička může přijít.');
}
const HTTP_REMOTE_TRUSTED_IPS = process.env.HTTP_REMOTE_TRUSTED_IPS.split(',').map(ip => ip.trim());
//TODO: nevim jak udelat console.log pouze pro "debug"
//console.log("Budu věřit hlavičkám z: " + HTTP_REMOTE_TRUSTED_IPS);
app.set('trust proxy', HTTP_REMOTE_TRUSTED_IPS);
console.log('Zapnutý login přes hlavičky z proxy.');
}
// ----------- Metody nevyžadující token -------------- // ----------- Metody nevyžadující token --------------
app.get("/api/whoami", (req, res) => { app.get("/api/whoami", (req, res) => {
if (!HTTP_REMOTE_USER_ENABLED) { res.send(req.header('remote-user'));
res.status(403).json({ error: 'Není zapnuté přihlášení z hlaviček' });
}
res.send(req.header(HTTP_REMOTE_USER_HEADER_NAME));
}) })
app.post("/api/login", (req, res) => { app.post("/api/login", (req, res) => {
if (HTTP_REMOTE_USER_ENABLED) { // je rovno app.enabled('trust proxy')
// Autentizace pomocí trusted headers // Autentizace pomocí trusted headers
const remoteUser = req.header(HTTP_REMOTE_USER_HEADER_NAME); const remoteUser = req.header('remote-user');
const remoteName = req.header('remote-name'); const remoteName = req.header('remote-name');
if (remoteUser && remoteUser.length > 0 && remoteName && remoteName.length > 0) { if (remoteUser && remoteUser.length > 0 && remoteName && remoteName.length > 0) {
res.status(200).json(generateToken(Buffer.from(remoteName, 'latin1').toString(), true)); res.status(200).json(generateToken(Buffer.from(remoteName, 'latin1').toString(), true));
} else { return;
throw Error("Tohle nema nastat nekdo neco dela spatne.");
} }
} else {
// Klasická autentizace loginem // Klasická autentizace loginem
if (!req.body?.login || req.body.login.trim().length === 0) { if (!req.body?.login || req.body.login.trim().length === 0) {
throw Error("Nebyl předán login"); throw Error("Nebyl předán login");
} }
// TODO zavést podmínky pro délku loginu (min i max) // TODO zavést podmínky pro délku loginu (min i max)
res.status(200).json(generateToken(req.body.login, false)); res.status(200).json(generateToken(req.body.login, false));
}
}); });
// TODO dočasné řešení - QR se zobrazuje přes <img>, nemáme sem jak dostat token // TODO dočasné řešení - QR se zobrazuje přes <img>, nemáme sem jak dostat token
@@ -94,17 +71,13 @@ app.get("/api/qr", (req, res) => {
/** Middleware ověřující JWT token */ /** Middleware ověřující JWT token */
app.use("/api/", (req, res, next) => { app.use("/api/", (req, res, next) => {
if (HTTP_REMOTE_USER_ENABLED) { const userHeader = req.header('remote-user');
const userHeader = req.header(HTTP_REMOTE_USER_HEADER_NAME);
const nameHeader = req.header('remote-name'); const nameHeader = req.header('remote-name');
const emailHeader = req.header('remote-email'); const emailHeader = req.header('remote-email');
if (userHeader !== undefined && nameHeader !== undefined) { if (userHeader !== undefined && nameHeader !== undefined) {
const remoteName = Buffer.from(nameHeader, 'latin1').toString(); const remoteName = Buffer.from(nameHeader, 'latin1').toString();
if (ENVIRONMENT !== "production"){
console.log("Tvuj username, name a email: %s, %s, %s.", userHeader, remoteName, emailHeader); console.log("Tvuj username, name a email: %s, %s, %s.", userHeader, remoteName, emailHeader);
} }
}
}
if (!req.headers.authorization) { if (!req.headers.authorization) {
return res.status(401).json({ error: 'Nebyl předán autentizační token' }); return res.status(401).json({ error: 'Nebyl předán autentizační token' });
} }
@@ -131,7 +104,6 @@ app.get("/api/data", async (req, res) => {
app.use("/api/pizzaDay", pizzaDayRoutes); app.use("/api/pizzaDay", pizzaDayRoutes);
app.use("/api/food", foodRoutes); app.use("/api/food", foodRoutes);
app.use("/api/voting", votingRoutes); app.use("/api/voting", votingRoutes);
app.use("/api/easterEggs", easterEggRoutes);
app.use(express.static('public')) app.use(express.static('public'))
// Middleware pro zpracování chyb // Middleware pro zpracování chyb

View File

@@ -277,25 +277,13 @@ const MOCK_DATA = {
{ {
amount: "-", amount: "-",
name: "Čočka na kyselo, opečená klobása, okurka, chléb", name: "Čočka na kyselo, opečená klobása, okurka, chléb",
price: "130\xA0Kč", price: "120\xA0Kč",
isSoup: false, isSoup: false,
}, },
{ {
amount: "-", amount: "-",
name: "Smažená brokolice, brambory, tatarská omáčka", name: "Kuřecí medailonky se sýrovou omáčkou, hranolky",
price: "na\xA0váhu", price: "170\xA0",
isSoup: false,
},
{
amount: "-",
name: "Uzený vepřový bůček, bramborové pyré",
price: "na\xA0váhu",
isSoup: false,
},
{
amount: "-",
name: "Kuřecí medailonky v sýrové omáčce, hranolky",
price: "na\xA0váhu",
isSoup: false, isSoup: false,
} }
], ],
@@ -309,25 +297,13 @@ const MOCK_DATA = {
{ {
amount: "-", amount: "-",
name: "Zvěřinový guláš, knedlík", name: "Zvěřinový guláš, knedlík",
price: "130\xA0Kč", price: "120\xA0Kč",
isSoup: false, isSoup: false,
}, },
{ {
amount: "-", amount: "-",
name: "Čínské nudle se zeleninou a vejcem", name: "Smažený hermelín, brambory, tatarská omáčka",
price: "na\xA0váhu", price: "170\xA0",
isSoup: false,
},
{
amount: "-",
name: "Jitrnice/jelito, brambory, zelný salát s křenem, hořčice",
price: "na\xA0váhu",
isSoup: false,
},
{
amount: "-",
name: "Vídeňská roštěná se smaženou cibulkou, jasmínová rýže",
price: "na\xA0váhu",
isSoup: false, isSoup: false,
} }
], ],
@@ -341,25 +317,13 @@ const MOCK_DATA = {
{ {
amount: "-", amount: "-",
name: "Kuřecí směs se zeleninou, rýže", name: "Kuřecí směs se zeleninou, rýže",
price: "130\xA0Kč", price: "120\xA0Kč",
isSoup: false, isSoup: false,
}, },
{ {
amount: "-", amount: "-",
name: "Tvarohové knedlíky s meruňkami, strouhaný tvaroh, máslo, cukr", name: "Hambuger Black Angus s čedarem a slaninou, cibulové kroužky",
price: "na\xA0váhu", price: "220\xA0",
isSoup: false,
},
{
amount: "-",
name: "Ovar, křen, hořčice, pečivo",
price: "na\xA0váhu",
isSoup: false,
},
{
amount: "-",
name: "Telecí holandský řízek s uzeným sýrem, bramborové pyré",
price: "na\xA0váhu",
isSoup: false, isSoup: false,
} }
], ],
@@ -373,25 +337,13 @@ const MOCK_DATA = {
{ {
amount: "-", amount: "-",
name: "Rizoto s vepřovým masem, okurka", name: "Rizoto s vepřovým masem, okurka",
price: "130\xA0Kč", price: "120\xA0Kč",
isSoup: false, isSoup: false,
}, },
{ {
amount: "-", amount: "-",
name: "Tortellini s parmezánovou omáčkou", name: "Steak z lososa, grilovaná zelenina",
price: "na\xA0váhu", price: "220\xA0",
isSoup: false,
},
{
amount: "-",
name: "Pečený prejt, brambory, zelný salát",
price: "na\xA0váhu",
isSoup: false,
},
{
amount: "-",
name: "Chobotnice na grilu, grilovaná zelenina, bylinková bageta",
price: "na\xA0váhu",
isSoup: false, isSoup: false,
} }
], ],
@@ -405,203 +357,17 @@ const MOCK_DATA = {
{ {
amount: "-", amount: "-",
name: "Krůtí perkelt, těstoviny", name: "Krůtí perkelt, těstoviny",
price: "130\xA0Kč", price: "120\xA0Kč",
isSoup: false, isSoup: false,
}, },
{ {
amount: "-", amount: "-",
name: "Grilovaný hermelín, bulgurový salát se zeleninou", name: "Grilovaná vepřová panenka, parmazánové pyré",
price: "na\xA0váhu", price: "170\xA0",
isSoup: false,
},
{
amount: "-",
name: "Zabijačkový guláš, karlovarský knedlík",
price: "na\xA0váhu",
isSoup: false,
},
{
amount: "-",
name: "Vepřový plátek na žampionech, jasmínová rýže",
price: "na\xA0váhu",
isSoup: false, isSoup: false,
} }
] ]
], ]
'zastavkaUmichala': [
[
{
amount: "-",
name: "Fazolačka s klobásou & zakysačkou",
price: "39\xA0Kč",
isSoup: true,
},
{
amount: "-",
name: "Zeleninová musaka lilek, cuketa, tomatové sugo & sýrový bešamel",
price: "135\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Slovácké strapačky s uzenou slaninou, zelím, mletým pepřem & sekanou petrželkou",
price: "140\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Hovězí guláš s vejcem, zeleninovou garniturkou & žemlovými knedlíky",
price: "145\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Kuřecí roláda s kaštanovou nádivkou, demi-glace & smetanovou bramborovou kaší",
price: "150\xA0Kč",
isSoup: false,
}
],
[
{
amount: "-",
name: "Hovězí vývar se zeleninou a játrovou rýží",
price: "39\xA0Kč",
isSoup: true,
},
{
amount: "-",
name: "Pečené vepřové koleno, křen, hořčice, chléb",
price: "320\xA0Kč",
isSoup: false,
}
],
[
{
amount: "-",
name: "Zeleninová polévka s kuskusem",
price: "39\xA0Kč",
isSoup: true,
},
{
amount: "-",
name: "Poutine (trhané vepřové, hranolky, sýr, čalamáda, pikantní omáčka)",
price: "190\xA0Kč",
isSoup: false,
}
],
[
{
amount: "-",
name: "Hrachová polévka s uzeninou",
price: "39\xA0Kč",
isSoup: true,
},
{
amount: "-",
name: "Vepřový řízek z kotlety, domácí bramborový salát",
price: "170\xA0Kč",
isSoup: false,
}
],
[
{
amount: "-",
name: "Cibulačka se sýrem",
price: "39\xA0Kč",
isSoup: true,
},
{
amount: "-",
name: "Burger z Chuck rollu, hranolky, tatarská omáčka",
price: "200\xA0Kč",
isSoup: false,
}
],
],
'senkSerikova': [
[
{
amount: "-",
name: "Drůbeží vývar s masem a nudlemi",
price: "45\xA0Kč",
isSoup: true,
},
{
amount: "-",
name: "Vepřová pečeně se zelím a houskovým knedlíkem",
price: "155\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Špagety s kuřecím masem, špenátem a smetanou",
price: "145\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Medailonky z vepřové panenky s fazolkami se slaninou, šťouchané brambory",
price: "185\xA0Kč",
isSoup: false,
}
],
[
{
amount: "-",
name: "Mrkvová polévka se zázvorem a kokosovým mlékem",
price: "45\xA0Kč",
isSoup: true,
},
{
amount: "-",
name: "Hovězí po Burgundsku, bramborová kaše",
price: "155\xA0Kč",
isSoup: false,
}
],
[
{
amount: "-",
name: "Hovězí vývar s játrovými knedlíčky",
price: "45\xA0Kč",
isSoup: true,
},
{
amount: "-",
name: "Kuřecí plátky na sušených rajčatech, bylinkách a česneku, bramborová kaše",
price: "155\xA0Kč",
isSoup: false,
}
],
[
{
amount: "-",
name: "Kuřecí vývar s rýží",
price: "45\xA0Kč",
isSoup: true,
},
{
amount: "-",
name: "Rajská s plněnou paprikou, knedlík",
price: "170\xA0Kč",
isSoup: false,
}
],
[
{
amount: "-",
name: "Mexická fazolová polévka",
price: "45\xA0Kč",
isSoup: true,
},
{
amount: "-",
name: "Ragú z trhané kachny, onsen vejce, soté ze špenátu a ředkvičky, bramborové pyré, lanýžová sůl, zelený olej",
price: "189\xA0Kč",
isSoup: false,
}
],
],
} }
// Mockovací data pro Pizza day // Mockovací data pro Pizza day
@@ -1354,11 +1120,8 @@ const MOCK_PIZZA_LIST = [
} }
] ]
/** export const getTodayMock = () => {
* Funkce vrací mock datu ve formátu YYYY-MM-DD return '2023-05-31'; // středa
*/
export const getTodayMock = (): Date => {
return new Date('2025-01-10'); // pátek
} }
export const getMenuSladovnickaMock = () => { export const getMenuSladovnickaMock = () => {
@@ -1373,14 +1136,6 @@ export const getMenuTechTowerMock = () => {
return MOCK_DATA['techTower']; return MOCK_DATA['techTower'];
} }
export const getMenuZastavkaUmichalaMock = () => {
return MOCK_DATA['zastavkaUmichala'];
}
export const getMenuSenkSerikovaMock = () => {
return MOCK_DATA['senkSerikova'];
}
export const getPizzaListMock = () => { export const getPizzaListMock = () => {
return MOCK_PIZZA_LIST; return MOCK_PIZZA_LIST;
} }

View File

@@ -1,160 +1,71 @@
/** Notifikace */ /** Notifikace pro gotify*/
import {ClientData, Locations, NotififaceInput} from '../../types'; import { GotifyServer, NotififaceInput, NotifikaceData } from '../../types';
import axios from 'axios'; import axios, { AxiosError } from 'axios';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { getToday } from "./service";
import {formatDate, getUsersByLocation, getHumanTime} from "./utils";
import getStorage from "./storage";
const storage = getStorage();
const ENVIRONMENT = process.env.NODE_ENV || 'production' const ENVIRONMENT = process.env.NODE_ENV || 'production'
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) }); dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
// const gotifyDataRaw = process.env.GOTIFY_SERVERS_AND_KEYS || "{}"; const gotifyDataRaw = process.env.GOTIFY_SERVERS_AND_KEYS || "{}";
// const gotifyData: GotifyServer[] = JSON.parse(gotifyDataRaw); const gotifyData: GotifyServer[] = JSON.parse(gotifyDataRaw);
// export const gotifyCall = async (data: NotififaceInput, gotifyServers?: GotifyServer[]): Promise<any[]> => {
// if (!Array.isArray(gotifyServers)) {
// return []
// }
// const urls = gotifyServers.flatMap(gotifyServer =>
// gotifyServer.api_keys.map(apiKey => `${gotifyServer.server}/message?token=${apiKey}`));
//
// const dataPayload = {
// title: "Luncher",
// message: `${data.udalost} - spustil:${data.user}`,
// priority: 7,
// };
//
// const headers = { "Content-Type": "application/json" };
//
// const promises = urls.map(url =>
// axios.post(url, dataPayload, { headers }).then(response => {
// response.data = {
// success: true,
// message: "Notifikace doručena",
// };
// return response;
// }).catch(error => {
// if (axios.isAxiosError(error)) {
// const axiosError = error as AxiosError;
// if (axiosError.response) {
// axiosError.response.data = {
// success: false,
// message: "fail",
// };
// console.log(error)
// return axiosError.response;
// }
// }
// // Handle unknown error without a response
// console.log(error, "unknown error");
// })
// );
// return promises;
// };
export const ntfyCall = async (data: NotififaceInput) => { export const gotifyCall = async (data: NotififaceInput, gotifyServers?: GotifyServer[]): Promise<any[]> => {
const url = process.env.NTFY_HOST if (!Array.isArray(gotifyServers)) {
const username = process.env.NTFY_USERNAME; return []
const password = process.env.NTFY_PASSWD;
if (!url) {
console.log("NTFY_HOST není definován v env")
return
} }
if (!username) { const urls = gotifyServers.flatMap(gotifyServer =>
console.log("NTFY_USERNAME není definován v env") gotifyServer.api_keys.map(apiKey => `${gotifyServer.server}/message?token=${apiKey}`));
return
}
if (!password) {
console.log("NTFY_PASSWD není definován v env")
return
}
const today = formatDate(getToday());
let clientData: ClientData = await storage.getData(today);
const usersByLocation = getUsersByLocation(clientData.choices, data.user)
const token = Buffer.from(`${username}:${password}`, 'utf8').toString('base64'); const dataPayload = {
const promises = usersByLocation.map(async user => { title: "Luncher",
try { message: `${data.udalost} - spustil:${data.user}`,
// Odstraníme mezery a diakritiku a převedeme na lowercase priority: 7,
const topic = user.normalize('NFD').replace(' ', '').replace(/[\u0300-\u036f]/g, '').toLowerCase();
const response = await axios({
url: `${url}/${topic}`,
method: 'POST',
data: `${data.udalost} - spustil:${data.user}`,
headers: {
'Authorization': `Basic ${token}`,
'Tag': 'meat_on_bone'
}
});
console.log(response.data);
} catch (error) {
console.error(error);
}
})
return promises;
}
export const teamsCall = async (data: NotififaceInput) => {
const url = process.env.TEAMS_WEBHOOK_URL;
if (!url) {
console.log("TEAMS_WEBHOOK_URL není definován v env")
return
}
const title = data.udalost;
// const today = formatDate(getToday());
// let clientData: ClientData = await storage.getData(today);
// const usersByLocation = getUsersByLocation(clientData.choices, data.user)
let location = data.locationKey ? ` odcházíme do ${Locations[data.locationKey] ?? ''}` : ' jdeme na oběd';
const message = 'V ' + getHumanTime(getToday()) + location;
const card = {
'@type': 'MessageCard',
'@context': 'http://schema.org/extensions',
'themeColor': "0072C6", // light blue
summary: 'Summary description',
sections: [
{
activityTitle: title,
text: message,
},
],
}; };
if (!url) { const headers = { "Content-Type": "application/json" };
console.log("TEAMS_WEBHOOK_URL není definován v env")
return
}
try { const promises = urls.map(url =>
const response = await axios.post(url, card, { axios.post(url, dataPayload, { headers }).then(response => {
headers: { response.data = {
'content-type': 'application/vnd.microsoft.teams.card.o365connector' success: true,
}, message: "Notifikace doručena",
}); };
return `${response.status} - ${response.statusText}`; return response;
} catch (err) { }).catch(error => {
return err; if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
if (axiosError.response) {
axiosError.response.data = {
success: false,
message: "fail",
};
console.log(error)
return axiosError.response;
} }
} }
// Handle unknown error without a response
console.log(error, "unknown error");
})
);
return promises;
};
/** Zavolá notifikace na všechny konfigurované způsoby notifikace, přetížení proměných na false pro jednotlivé způsoby je vypne*/ /** Zavolá notifikace na všechny konfigurované způsoby notifikace, přetížení proměných na false pro jednotlivé způsoby je vypne*/
export const callNotifikace = async (input: NotififaceInput) => { export const callNotifikace = async ({ input, teams = true, gotify = true }: NotifikaceData) => {
const notifications = []; const notifications = [];
const ntfyPromises = await ntfyCall(input); if (gotify) {
if (ntfyPromises) { const gotifyPromises = await gotifyCall(input, gotifyData);
notifications.push(...ntfyPromises); notifications.push(...gotifyPromises);
} }
const teamsPromises = await teamsCall(input);
if (teamsPromises) { /* Zatím není
notifications.push(teamsPromises); if (teams) {
} notifications.push(teamsCall(input));
// gotify bych řekl, že už je deprecated }*/
// const gotifyPromises = await gotifyCall(input, gotifyData);
// notifications.push(...gotifyPromises); // Add more notifications as necessary
try { try {
const results = await Promise.all(notifications); const results = await Promise.all(notifications);

View File

@@ -52,7 +52,7 @@ export async function createPizzaDay(creator: string): Promise<ClientData> {
const pizzaList = await getPizzaList(); const pizzaList = await getPizzaList();
const data: ClientData = { pizzaDay: { state: PizzaDayState.CREATED, creator, orders: [] }, pizzaList, ...clientData }; const data: ClientData = { pizzaDay: { state: PizzaDayState.CREATED, creator, orders: [] }, pizzaList, ...clientData };
await storage.setData(today, data); await storage.setData(today, data);
callNotifikace({ udalost: UdalostEnum.ZAHAJENA_PIZZA, user: creator }) callNotifikace({ input: { udalost: UdalostEnum.ZAHAJENA_PIZZA, user: creator } })
return data; return data;
} }
@@ -207,7 +207,7 @@ export async function finishPizzaOrder(login: string) {
} }
clientData.pizzaDay.state = PizzaDayState.ORDERED; clientData.pizzaDay.state = PizzaDayState.ORDERED;
await storage.setData(today, clientData); await storage.setData(today, clientData);
callNotifikace({ udalost: UdalostEnum.OBJEDNANA_PIZZA, user: clientData?.pizzaDay?.creator }) callNotifikace({ input: { udalost: UdalostEnum.OBJEDNANA_PIZZA, user: clientData?.pizzaDay?.creator } })
return clientData; return clientData;
} }

View File

@@ -1,33 +1,17 @@
import axios from "axios"; import axios from "axios";
import { load } from 'cheerio'; import { load } from 'cheerio';
import { Food } from "../../types"; import { Food } from "../../types";
import {getMenuSladovnickaMock, getMenuTechTowerMock, getMenuUMotlikuMock, getMenuZastavkaUmichalaMock, getMenuSenkSerikovaMock} from "./mock"; import { getMenuSladovnickaMock, getMenuTechTowerMock, getMenuUMotlikuMock } from "./mock";
import {formatDate} from "./utils"; import { getDayOfWeekIndex } from "./utils";
// Fráze v názvech jídel, které naznačují že se jedná o polévku // Fráze v názvech jídel, které naznačují že se jedná o polévku
const SOUP_NAMES = [ const SOUP_NAMES = ['polévka', 'česnečka', 'česnekový krém', 'cibulačka', 'vývar']
'polévka',
'česnečka',
'česnekový krém',
'cibulačka',
'vývar',
'fazolová',
'cuketový krém',
'boršč',
'slepičí s ',
'zeleninová s ',
'hovězí s ',
'kachní kaldoun',
'dršťková'
];
const DAYS_IN_WEEK = ['pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota', 'neděle']; const DAYS_IN_WEEK = ['pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota', 'neděle'];
// URL na týdenní menu jednotlivých restaurací // URL na týdenní menu jednotlivých restaurací
const SLADOVNICKA_URL = 'https://sladovnicka.unasplzenchutna.cz/cz/denni-nabidka'; const SLADOVNICKA_URL = 'https://sladovnicka.unasplzenchutna.cz/cz/denni-nabidka';
const U_MOTLIKU_URL = 'https://www.umotliku.cz/menu'; const U_MOTLIKU_URL = 'https://www.umotliku.cz/menu';
const TECHTOWER_URL = 'https://www.equifarm.cz/restaurace-techtower'; const TECHTOWER_URL = 'https://www.equifarm.cz/restaurace-techtower';
const ZASTAVKAUMICHALA_URL = 'https://www.zastavkaumichala.cz';
const SENKSERIKOVA_URL = 'https://www.menicka.cz/6561-pivovarsky-senk-serikova.html';
/** /**
* Vrátí true, pokud předaný text obsahuje některé ze slov, které naznačuje, že se jedná o polévku. * Vrátí true, pokud předaný text obsahuje některé ze slov, které naznačuje, že se jedná o polévku.
@@ -154,7 +138,7 @@ export const getMenuSladovnicka = async (firstDayOfWeek: Date, mock: boolean = f
isSoup: false, isSoup: false,
}); });
}) })
result[dayIndex] = currentDayFood; result[index] = currentDayFood;
} }
return result; return result;
} }
@@ -174,33 +158,8 @@ export const getMenuUMotliku = async (firstDayOfWeek: Date, mock: boolean = fals
const html = await getHtml(U_MOTLIKU_URL); const html = await getHtml(U_MOTLIKU_URL);
const $ = load(html); const $ = load(html);
// Najdeme první tabulku, nad kterou je v H3 datum začínající co nejdřívějším dnem v aktuálním týdnu const table = $('table.table.table-hover.Xtable-striped').first();
const tables = $('table.table.table-hover.Xtable-striped'); const body = table.children().first();
let usedTable;
let usedDate = new Date(firstDayOfWeek.getTime());
for (let i = 0; i < 4; i++) {
const dayOfWeekString = `${usedDate.getDate()}.${usedDate.getMonth() + 1}.`;
for (const tableNode of tables) {
const table = $(tableNode);
const h3 = table.parent().prev();
const s1 = h3.text().split("-")[0].split(".");
const foundFirstDayString = `${s1[0]}.${s1[1]}.`;
if (foundFirstDayString === dayOfWeekString) {
usedTable = table;
}
}
if (usedTable != null) {
break;
}
usedDate.setDate(usedDate.getDate() + 1);
}
if (usedTable == null) {
const firstDayOfWeekString = `${firstDayOfWeek.getDate()}.${firstDayOfWeek.getMonth() + 1}.`;
throw Error(`Nepodařilo se najít tabulku pro týden začínající ${firstDayOfWeekString}`);
}
const body = usedTable.children().first();
const rows = body.children(); const rows = body.children();
const result: Food[][] = []; const result: Food[][] = [];
@@ -267,60 +226,50 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
const html = await getHtml(TECHTOWER_URL); const html = await getHtml(TECHTOWER_URL);
const $ = load(html); const $ = load(html);
let secondTry = false; const fonts = $('font.wsw-41');
// První pokus - varianta "Obědy"
let fonts = $('font.wsw-41');
let font = undefined; let font = undefined;
fonts.each((i, f) => { fonts.each((i, f) => {
if ($(f).text().trim().startsWith('Obědy')) { if ($(f).text().trim().startsWith('Obědy')) {
font = f; font = f;
} }
}) })
// Druhý pokus - varianta "Jídelní lístek"
if (!font) {
fonts = $('font.wnd-font-size-90');
fonts.each((i, f) => {
if ($(f).text().trim().startsWith('Jídelní lístek')) {
font = f;
secondTry = true;
}
})
}
if (!font) { if (!font) {
throw Error('Chyba: nenalezen <font> pro obědy v HTML Techtower.'); throw Error('Chyba: nenalezen <font> pro obědy v HTML Techtower.');
} }
const result: Food[][] = [];
// TODO validovat, že v textu nalezeného <font> je rozsah, do kterého spadá vstupní datum // TODO validovat, že v textu nalezeného <font> je rozsah, do kterého spadá vstupní datum
const siblings = secondTry ? $(font).parent().siblings() : $(font).parent().parent().siblings(); const siblings = $(font).parent().parent().siblings();
let parsing = false; let parsing = false;
let currentDayIndex = 0; const result: Food[][] = [];
// TODO toto je kvůli poslednímu "línému" refaktoru neoptimální, stačilo by to projít jedním cyklem
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
if (!(dayIndex in result)) {
result[dayIndex] = [];
}
for (let i = 0; i < siblings.length; i++) { for (let i = 0; i < siblings.length; i++) {
const text = $(siblings.get(i)).text().trim().replace('\t', '').replace('\n', ' '); const text = $(siblings.get(i)).text().trim().replace('\t', '').replace('\n', ' ');
if (DAYS_IN_WEEK.includes(text.toLocaleLowerCase())) { if (DAYS_IN_WEEK.includes(text)) {
// Zjistíme aktuální index if (text === DAYS_IN_WEEK[dayIndex]) {
currentDayIndex = DAYS_IN_WEEK.indexOf(text.toLocaleLowerCase()); // Našli jsme dnešní den, odtud začínáme parsovat jídla
if (!parsing) {
// Našli jsme libovolný den v týdnu a ještě neparsujeme, tak začneme
parsing = true; parsing = true;
continue
} else if (parsing) {
// Už parsujeme jídla, ale narazili jsme na následující den - končíme
break;
} }
} else if (parsing) { } else if (parsing) {
if (text.length == 0) { if (text.length == 0) {
// Prázdná řádka - bývá na zcela náhodných místech ¯\_(ツ)_/¯ // Prázdná řádka - končíme (je za pátečním menu TechTower)
continue; break;
} }
let price = 'na\xA0váhu'; let price = '? Kč';
let name = text.replace('•', ''); let name = text;
if (text.toLowerCase().endsWith('kč')) { if (text.toLowerCase().endsWith('kč')) {
const tmp = text.replace('\xA0', ' ').split(' '); const tmp = text.replace('\xA0', ' ').split(' ');
const split = [tmp.slice(0, -2).join(' ')].concat(tmp.slice(-2)); const split = [tmp.slice(0, -2).join(' ')].concat(tmp.slice(-2));
price = `${split.slice(1)[0]}\xA0Kč` price = `${split.slice(1)[0]}\xA0Kč`
name = split[0].replace('•', ''); name = split[0]
} }
if (result[currentDayIndex] == null) { result[dayIndex].push({
result[currentDayIndex] = [];
}
result[currentDayIndex].push({
amount: '-', amount: '-',
name, name,
price, price,
@@ -328,107 +277,6 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
}) })
} }
} }
return result;
}
/**
* Získá obědovou nabídku ZastavkaUmichala pro jeden týden.
*
* @param firstDayOfWeek první den v týdnu, pro který získat menu
* @param mock zda vrátit mock data
* @returns seznam jídel pro dané datum
*/
export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolean = false): Promise<Food[][]> => {
if (mock) {
return getMenuZastavkaUmichalaMock();
}
const nowDate = new Date().getDate();
const headers = {
"Cookie": "_nss=1; PHPSESSID=9e37de17e0326b0942613d6e67a30e69",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
};
const result: Food[][] = [];
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
const currentDate = new Date(firstDayOfWeek);
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex);
if (currentDate.getDate() < nowDate || (currentDate.getDate() === nowDate && new Date().getHours() >= 14)) {
result[dayIndex] = [{
amount: undefined,
name: "Pro tento den není uveřejněna nabídka jídel",
price: "",
isSoup: false,
}];
continue;
} else {
const url = (currentDate.getDate() === nowDate) ?
ZASTAVKAUMICHALA_URL : ZASTAVKAUMICHALA_URL + '/?do=dailyMenu-changeDate&dailyMenu-dateString=' + formatDate(currentDate, 'DD.MM.YYYY');
const html = await axios.get(url, {
headers,
}).then(res => res.data).then(content => content);
const $ = load(html);
const currentDayFood: Food[] = [];
$('.foodsList li').each((index, element) => {
currentDayFood.push({
amount: '-',
name: sanitizeText($(element).contents().not('span').text()),
price: sanitizeText($(element).find('span').text()),
isSoup: (index === 0),
});
});
result[dayIndex] = currentDayFood;
}
} }
return result; return result;
} }
/**
* Získá obědovou nabídku SenkSerikova pro jeden týden.
*
* @param firstDayOfWeek první den v týdnu, pro který získat menu
* @param mock zda vrátit mock data
* @returns seznam jídel pro dané datum
*/
export const getMenuSenkSerikova = async (firstDayOfWeek: Date, mock: boolean = false): Promise<Food[][]> => {
if (mock) {
return getMenuSenkSerikovaMock();
}
const decoder = new TextDecoder('windows-1250');
const html = await axios.get(SENKSERIKOVA_URL, {
responseType: 'arraybuffer',
responseEncoding: 'binary'
}).then(res => decoder.decode(new Uint8Array(res.data))).then(content => content);
const $ = load(html);
const nowDate = new Date().getDate();
const currentDate = new Date(firstDayOfWeek);
const result: Food[][] = [];
let dayIndex = 0;
while(currentDate.getDate() < nowDate) {
result[dayIndex] = [{
amount: undefined,
name: "Pro tento den není uveřejněna nabídka jídel",
price: "",
isSoup: false,
}];
dayIndex = dayIndex + 1;
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex);
}
$('.menicka').each((i, element) => {
const currentDayFood: Food[] = [];
$(element).find('.popup-gallery li').each((j, element) => {
currentDayFood.push({
amount: '-',
name: $(element).children('div.polozka').text(),
price: $(element).children('div.cena').text().replace(/ /g, '\xA0'),
isSoup: $(element).hasClass('polevka'),
});
});
result[dayIndex++] = currentDayFood;
});
return result;
}

View File

@@ -1,156 +0,0 @@
import express, { NextFunction } from "express";
import { getLogin, getTrusted } from "../auth";
import { parseToken } from "../utils";
import path from "path";
import fs from "fs";
import { EasterEgg } from "../../../types";
const EASTER_EGGS_JSON_PATH = path.join(__dirname, "../../.easter-eggs.json");
const IMAGES_PATH = '../../resources/easterEggs';
type EasterEggsJson = {
[key: string]: EasterEgg[]
}
function generateUrl() {
let result = '';
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
let counter = 0;
while (counter < 32) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result;
}
/**
* Vrátí náhodně jeden z definovaných easter egg obrázků pro přihlášeného uživatele.
*
* @param req request
* @param res response
* @param next next
* @returns náhodný easter egg obrázek, nebo 404 pokud žádný není definován
*/
function getEasterEggImage(req: any, res: any, next: NextFunction) {
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
try {
// TODO vrátit!
// if (trusted) {
if (true) {
if (login in easterEggs) {
const imagePath = easterEggs[login][Math.floor(Math.random() * easterEggs[login].length)].path;
res.sendFile(path.join(__dirname, IMAGES_PATH, imagePath));
return;
}
}
res.sendStatus(404);
} catch (e: any) { next(e) }
}
function getRandomPosition(startOffset: number, endOffset: number) {
const choice = Math.floor(Math.random() * 4);
if (choice === 0) {
// Vlevo nahoře
return {
left: `${startOffset}px`,
startLeft: `${startOffset}px`,
"--start-left": `${startOffset}px`,
top: `${startOffset}px`,
startTop: `${startOffset}px`,
"--start-top": `${startOffset}px`,
endLeft: `${endOffset}px`,
"--end-left": `${endOffset}px`,
endTop: `${endOffset}px`,
"--end-top": `${endOffset}px`,
rotate: '135deg',
}
} else if (choice === 1) {
// Vpravo nahoře
return {
right: `${startOffset}px`,
startRight: `${startOffset}px`,
"--start-right": `${startOffset}px`,
top: `${startOffset}px`,
startTop: `${startOffset}px`,
"--start-top": `${startOffset}px`,
endRight: `${endOffset}px`,
"--end-right": `${endOffset}px`,
endTop: `${endOffset}px`,
"--end-top": `${endOffset}px`,
rotate: '-135deg',
}
} else if (choice === 2) {
// Vpravo dole
return {
right: `${startOffset}px`,
startRight: `${startOffset}px`,
"--start-right": `${startOffset}px`,
bottom: `${startOffset}px`,
startBottom: `${startOffset}px`,
"--start-bottom": `${startOffset}px`,
endRight: `${endOffset}px`,
"--end-right": `${endOffset}px`,
endBottom: `${endOffset}px`,
"--end-bottom": `${endOffset}px`,
rotate: '-45deg',
}
} else if (choice === 3) {
// Vlevo dole
return {
left: `${startOffset}px`,
startLeft: `${startOffset}px`,
"--start-left": `${startOffset}px`,
bottom: `${startOffset}px`,
startBottom: `${startOffset}px`,
"--start-bottom": `${startOffset}px`,
endLeft: `${endOffset}px`,
"--end-left": `${endOffset}px`,
endBottom: `${endOffset}px`,
"--end-bottom": `${endOffset}px`,
rotate: '45deg',
}
}
}
const router = express.Router();
let easterEggs: EasterEggsJson;
// Registrace náhodných URL pro všechny existující easter eggy
if (fs.existsSync(EASTER_EGGS_JSON_PATH)) {
const content = fs.readFileSync(EASTER_EGGS_JSON_PATH, 'utf-8');
easterEggs = JSON.parse(content);
for (const [key, eggs] of Object.entries(easterEggs)) {
for (const easterEgg of eggs) {
const url = generateUrl();
easterEgg.url = url;
router.get(`/${url}`, async (req, res, next) => {
return getEasterEggImage(req, res, next);
});
}
}
}
// Získání náhodného easter eggu pro přihlášeného uživatele
router.get("/", async (req, res, next) => {
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
try {
// TODO vrátit!
// if (trusted) {
if (true) {
if (easterEggs && login in easterEggs) {
const randomEasterEgg = easterEggs[login][Math.floor(Math.random() * easterEggs[login].length)];
const { path, startOffset, endOffset, ...strippedEasterEgg } = randomEasterEgg; // Path klient k ničemu nepotřebuje a nemá ho znát
return res.status(200).json({ ...strippedEasterEgg, ...getRandomPosition(startOffset, endOffset) });
}
}
return res.status(200).send();
} catch (e: any) { next(e) }
});
export default router;

View File

@@ -1,19 +1,8 @@
import express, { Request } from "express"; import express from "express";
import { getLogin, getTrusted } from "../auth"; import { getLogin, getTrusted } from "../auth";
import { addChoice, addVolatileData, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service"; import { getDateForWeekIndex, addChoice, removeChoices, removeChoice, updateDepartureTime, getToday } from "../service";
import { getDayOfWeekIndex, parseToken } from "../utils"; import { getDayOfWeekIndex, parseToken } from "../utils";
import { getWebsocket } from "../websocket"; import { getWebsocket } from "../websocket";
import { callNotifikace } from "../notifikace";
import {
AddChoiceRequest,
ChangeDepartureTimeRequest,
IDayIndex,
JdemeObedRequest,
RemoveChoiceRequest,
RemoveChoicesRequest,
UdalostEnum,
UpdateNoteRequest
} from "../../../types";
/** /**
* Ověří a vrátí index dne v týdnu z požadavku, za předpokladu, že byl předán, a je zároveň * Ověří a vrátí index dne v týdnu z požadavku, za předpokladu, že byl předán, a je zároveň
@@ -22,12 +11,12 @@ import {
* @param req request * @param req request
* @returns index dne v týdnu * @returns index dne v týdnu
*/ */
const parseValidateFutureDayIndex = (req: Request<{}, any, IDayIndex>) => { const parseValidateFutureDayIndex = (req: any) => {
if (req.body.dayIndex == null) { if (req.body.dayIndex == null) {
throw Error(`Nebyl předán index dne v týdnu.`); throw Error(`Nebyl předán index dne v týdnu.`);
} }
const todayDayIndex = getDayOfWeekIndex(getToday()); const todayDayIndex = getDayOfWeekIndex(getToday());
const dayIndex = req.body.dayIndex; const dayIndex = parseInt(req.body.dayIndex);
if (isNaN(dayIndex)) { if (isNaN(dayIndex)) {
throw Error(`Neplatný index dne v týdnu: ${req.body.dayIndex}`); throw Error(`Neplatný index dne v týdnu: ${req.body.dayIndex}`);
} }
@@ -39,9 +28,10 @@ const parseValidateFutureDayIndex = (req: Request<{}, any, IDayIndex>) => {
const router = express.Router(); const router = express.Router();
router.post("/addChoice", async (req: Request<{}, any, AddChoiceRequest>, res, next) => { router.post("/addChoice", async (req, res, next) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req)); const trusted = getTrusted(parseToken(req));
if (req.body.locationIndex > -1) {
let date = undefined; let date = undefined;
if (req.body.dayIndex != null) { if (req.body.dayIndex != null) {
let dayIndex; let dayIndex;
@@ -53,13 +43,15 @@ router.post("/addChoice", async (req: Request<{}, any, AddChoiceRequest>, res, n
date = getDateForWeekIndex(dayIndex); date = getDateForWeekIndex(dayIndex);
} }
try { try {
const data = await addChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date); const data = await addChoice(login, trusted, req.body.locationIndex, req.body.foodIndex, date);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
return res.status(200).json(data); return res.status(200).json(data);
} catch (e: any) { next(e) } } catch (e: any) { next(e) }
}
return res.status(400); // TODO přidat popis chyby
}); });
router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesRequest>, res, next) => { router.post("/removeChoices", async (req, res, next) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req)); const trusted = getTrusted(parseToken(req));
let date = undefined; let date = undefined;
@@ -73,13 +65,13 @@ router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesRequest>
date = getDateForWeekIndex(dayIndex); date = getDateForWeekIndex(dayIndex);
} }
try { try {
const data = await removeChoices(login, trusted, req.body.locationKey, date); const data = await removeChoices(login, trusted, req.body.locationIndex, date);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
res.status(200).json(data); res.status(200).json(data);
} catch (e: any) { next(e) } } catch (e: any) { next(e) }
}); });
router.post("/removeChoice", async (req: Request<{}, any, RemoveChoiceRequest>, res, next) => { router.post("/removeChoice", async (req, res, next) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req)); const trusted = getTrusted(parseToken(req));
let date = undefined; let date = undefined;
@@ -93,37 +85,13 @@ router.post("/removeChoice", async (req: Request<{}, any, RemoveChoiceRequest>,
date = getDateForWeekIndex(dayIndex); date = getDateForWeekIndex(dayIndex);
} }
try { try {
const data = await removeChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date); const data = await removeChoice(login, trusted, req.body.locationIndex, req.body.foodIndex, date);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
res.status(200).json(data); res.status(200).json(data);
} catch (e: any) { next(e) } } catch (e: any) { next(e) }
}); });
router.post("/updateNote", async (req: Request<{}, any, UpdateNoteRequest>, res, next) => { router.post("/changeDepartureTime", async (req, res, next) => {
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
const note = req.body.note;
try {
if (note && note.length > 70) {
throw Error("Poznámka může mít maximálně 70 znaků");
}
let date = undefined;
if (req.body.dayIndex != null) {
let dayIndex;
try {
dayIndex = parseValidateFutureDayIndex(req);
} catch (e: any) {
return res.status(400).json({ error: e.message });
}
date = getDateForWeekIndex(dayIndex);
}
const data = await updateNote(login, trusted, note, date);
getWebsocket().emit("message", await addVolatileData(data));
res.status(200).json(data);
} catch (e: any) { next(e) }
});
router.post("/changeDepartureTime", async (req: Request<{}, any, ChangeDepartureTimeRequest>, res, next) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
let date = undefined; let date = undefined;
if (req.body.dayIndex != null) { if (req.body.dayIndex != null) {
@@ -137,17 +105,9 @@ router.post("/changeDepartureTime", async (req: Request<{}, any, ChangeDeparture
} }
try { try {
const data = await updateDepartureTime(login, req.body?.time, date); const data = await updateDepartureTime(login, req.body?.time, date);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
res.status(200).json(data); res.status(200).json(data);
} catch (e: any) { next(e) } } catch (e: any) { next(e) }
}); });
router.post("/jdemeObed", async (req: Request<{}, any, JdemeObedRequest>, res, next) => {
const login = getLogin(parseToken(req));
try {
await callNotifikace({ user: login, udalost: UdalostEnum.JDEME_OBED, locationKey: req.body.locationKey });
res.status(200).json({});
} catch (e: any) { next(e) }
});
export default router; export default router;

View File

@@ -1,29 +1,27 @@
import express, { Request } from "express"; import express from "express";
import { getLogin } from "../auth"; import { getLogin } from "../auth";
import { createPizzaDay, deletePizzaDay, getPizzaList, addPizzaOrder, removePizzaOrder, lockPizzaDay, unlockPizzaDay, finishPizzaOrder, finishPizzaDelivery, updatePizzaDayNote, updatePizzaFee } from "../pizza"; import { createPizzaDay, deletePizzaDay, getPizzaList, addPizzaOrder, removePizzaOrder, lockPizzaDay, unlockPizzaDay, finishPizzaOrder, finishPizzaDelivery, updatePizzaDayNote, updatePizzaFee } from "../pizza";
import { parseToken } from "../utils"; import { parseToken } from "../utils";
import { getWebsocket } from "../websocket"; import { getWebsocket } from "../websocket";
import { addVolatileData } from "../service";
import { AddPizzaRequest, FinishDeliveryRequest, RemovePizzaRequest, UpdatePizzaDayNoteRequest, UpdatePizzaFeeRequest } from "../../../types";
const router = express.Router(); const router = express.Router();
/** Založí pizza day pro aktuální den, za předpokladu že dosud neexistuje. */ /** Založí pizza day pro aktuální den, za předpokladu že dosud neexistuje. */
router.post("/create", async (req: Request<{}, any, undefined>, res) => { router.post("/create", async (req, res) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
const data = await createPizzaDay(login); const data = await createPizzaDay(login);
res.status(200).json(data); res.status(200).json(data);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
}); });
/** Smaže pizza day pro aktuální den, za předpokladu že existuje. */ /** Smaže pizza day pro aktuální den, za předpokladu že existuje. */
router.post("/delete", async (req: Request<{}, any, undefined>, res) => { router.post("/delete", async (req, res) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
const data = await deletePizzaDay(login); const data = await deletePizzaDay(login);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
}); });
router.post("/add", async (req: Request<{}, any, AddPizzaRequest>, res) => { router.post("/add", async (req, res) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
if (isNaN(req.body?.pizzaIndex)) { if (isNaN(req.body?.pizzaIndex)) {
throw Error("Nebyl předán index pizzy"); throw Error("Nebyl předán index pizzy");
@@ -44,68 +42,66 @@ router.post("/add", async (req: Request<{}, any, AddPizzaRequest>, res) => {
throw Error("Neplatný index velikosti pizzy: " + pizzaSizeIndex); throw Error("Neplatný index velikosti pizzy: " + pizzaSizeIndex);
} }
const data = await addPizzaOrder(login, pizzy[pizzaIndex], pizzy[pizzaIndex].sizes[pizzaSizeIndex]); const data = await addPizzaOrder(login, pizzy[pizzaIndex], pizzy[pizzaIndex].sizes[pizzaSizeIndex]);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
res.status(200).json({}); res.status(200).json({});
}); });
router.post("/remove", async (req: Request<{}, any, RemovePizzaRequest>, res) => { router.post("/remove", async (req, res) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
if (!req.body?.pizzaOrder) { if (!req.body?.pizzaOrder) {
throw Error("Nebyla předána objednávka"); throw Error("Nebyla předána objednávka");
} }
const data = await removePizzaOrder(login, req.body?.pizzaOrder); const data = await removePizzaOrder(login, req.body?.pizzaOrder);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
res.status(200).json({}); res.status(200).json({});
}); });
router.post("/lock", async (req: Request<{}, any, undefined>, res) => { router.post("/lock", async (req, res) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
const data = await lockPizzaDay(login); const data = await lockPizzaDay(login);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
res.status(200).json({}); res.status(200).json({});
}); });
router.post("/unlock", async (req: Request<{}, any, undefined>, res) => { router.post("/unlock", async (req, res) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
const data = await unlockPizzaDay(login); const data = await unlockPizzaDay(login);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
res.status(200).json({}); res.status(200).json({});
}); });
router.post("/finishOrder", async (req: Request<{}, any, undefined>, res) => { router.post("/finishOrder", async (req, res) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
const data = await finishPizzaOrder(login); const data = await finishPizzaOrder(login);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
res.status(200).json({}); res.status(200).json({});
}); });
router.post("/finishDelivery", async (req: Request<{}, any, FinishDeliveryRequest>, res) => { router.post("/finishDelivery", async (req, res) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
const data = await finishPizzaDelivery(login, req.body.bankAccount, req.body.bankAccountHolder); const data = await finishPizzaDelivery(login, req.body.bankAccount, req.body.bankAccountHolder);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
res.status(200).json({}); res.status(200).json({});
}); });
router.post("/updatePizzaDayNote", async (req: Request<{}, any, UpdatePizzaDayNoteRequest>, res, next) => { router.post("/updatePizzaDayNote", async (req, res) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
try { if (req.body.note && req.body.note.length > 100) {
if (req.body.note && req.body.note.length > 70) { throw Error("Poznámka může mít maximálně 100 znaků");
throw Error("Poznámka může mít maximálně 70 znaků");
} }
const data = await updatePizzaDayNote(login, req.body.note); const data = await updatePizzaDayNote(login, req.body.note);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
res.status(200).json(data); res.status(200).json(data);
} catch (e: any) { next(e) }
}); });
router.post("/updatePizzaFee", async (req: Request<{}, any, UpdatePizzaFeeRequest>, res, next) => { router.post("/updatePizzaFee", async (req, res, next) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
if (!req.body.login) { if (!req.body.login) {
return res.status(400).json({ error: "Nebyl předán login cílového uživatele" }); return res.status(400).json({ error: "Nebyl předán login cílového uživatele" });
} }
try { try {
const data = await updatePizzaFee(login, req.body.login, req.body.text, req.body.price); const data = await updatePizzaFee(login, req.body.login, req.body.text, req.body.price);
getWebsocket().emit("message", await addVolatileData(data)); getWebsocket().emit("message", data);
res.status(200).json(data); res.status(200).json(data);
} catch (e: any) { next(e) } } catch (e: any) { next(e) }
}); });

View File

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

View File

@@ -1,8 +1,9 @@
import { InsufficientPermissions, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getIsWeekend, getWeekNumber } from "./utils"; import { InsufficientPermissions, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getHumanTime, getIsWeekend, getLastWorkDayOfWeek, getWeekNumber } from "./utils";
import { ClientData, Restaurants, DayMenu, DepartureTime, DayData, WeekMenu, LocationKey } from "../../types"; import { ClientData, Locations, Restaurants, Menu, DepartureTime, DayData, WeekMenu } from "../../types";
import getStorage from "./storage"; import getStorage from "./storage";
import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku, getMenuZastavkaUmichala, getMenuSenkSerikova } from "./restaurants"; import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants";
import { getTodayMock } from "./mock"; import { getTodayMock } from "./mock";
import { first } from "cheerio/lib/api/traversing";
const storage = getStorage(); const storage = getStorage();
const MENU_PREFIX = 'menu'; const MENU_PREFIX = 'menu';
@@ -10,7 +11,7 @@ const MENU_PREFIX = 'menu';
/** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */ /** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */
export function getToday(): Date { export function getToday(): Date {
if (process.env.MOCK_DATA === 'true') { if (process.env.MOCK_DATA === 'true') {
return getTodayMock(); return new Date(getTodayMock());
} }
return new Date(); return new Date();
} }
@@ -35,20 +36,10 @@ function getEmptyData(date?: Date): ClientData {
isWeekend: getIsWeekend(usedDate), isWeekend: getIsWeekend(usedDate),
weekIndex: getDayOfWeekIndex(usedDate), weekIndex: getDayOfWeekIndex(usedDate),
choices: {}, choices: {},
departureTimes: Object.values(DepartureTime), // TODO tohle zmizí, bude se přidávat do dat dynamicky
}; };
} }
/**
* Přidá k datům "dopočítaná" data, která nejsou přímo uložena v databázi.
*
* @param data data z databáze
* @returns obohacená data
*/
export async function addVolatileData(data: ClientData): Promise<ClientData> {
data.todayWeekIndex = getDayOfWeekIndex(getToday());
return data;
}
/** /**
* Vrátí veškerá klientská data pro předaný den, nebo aktuální den, pokud není předán. * Vrátí veškerá klientská data pro předaný den, nebo aktuální den, pokud není předán.
*/ */
@@ -56,157 +47,94 @@ export async function getData(date?: Date): Promise<ClientData> {
const targetDate = date ?? getToday(); const targetDate = date ?? getToday();
const dateString = formatDate(targetDate); const dateString = formatDate(targetDate);
const data: DayData = await storage.getData(dateString) || getEmptyData(date); const data: DayData = await storage.getData(dateString) || getEmptyData(date);
let clientData: ClientData = { ...data }; const clientData: ClientData = { ...data };
clientData.menus = { clientData.todayWeekIndex = getDayOfWeekIndex(getToday());
[Restaurants.SLADOVNICKA]: await getRestaurantMenu(Restaurants.SLADOVNICKA, date), // Dotažení jídel, pokud je ještě nemáme
// [Restaurants.UMOTLIKU]: await getRestaurantMenu(Restaurants.UMOTLIKU, date), if (!data.menus) {
[Restaurants.TECHTOWER]: await getRestaurantMenu(Restaurants.TECHTOWER, date), data.menus = {
[Restaurants.ZASTAVKAUMICHALA]: await getRestaurantMenu(Restaurants.ZASTAVKAUMICHALA, date), [Restaurants.SLADOVNICKA]: await getRestaurantMenu(Restaurants.SLADOVNICKA, targetDate),
[Restaurants.SENKSERIKOVA]: await getRestaurantMenu(Restaurants.SENKSERIKOVA, date), [Restaurants.UMOTLIKU]: await getRestaurantMenu(Restaurants.UMOTLIKU, targetDate),
[Restaurants.TECHTOWER]: await getRestaurantMenu(Restaurants.TECHTOWER, targetDate),
} }
clientData = await addVolatileData(clientData); await storage.setData(dateString, data);
return clientData; clientData.menus = data.menus;
} }
return data;
/**
* Vrátí klíč, pod kterým je uloženo menu pro předané datum.
*
* @param date datum
* @returns databázový klíč
*/
function getMenuKey(date: Date) {
const weekNumber = getWeekNumber(date);
return `${MENU_PREFIX}_${date.getFullYear()}_${weekNumber}`;
} }
/** /**
* Vrátí menu restaurací pro předané datum, pokud již existují. * Vrátí menu restaurací pro předané datum, pokud již existují.
* Jinak založí a vrátí prázdný objekt.
* *
* @param date datum * @param date datum
* @returns menu restaurací pro předané datum * @returns menu restaurací pro předané datum
*/ */
async function getMenu(date: Date): Promise<WeekMenu | undefined> { async function getMenu(date: Date): Promise<WeekMenu> {
return await storage.getData(getMenuKey(date)); const weekNumber = getWeekNumber(date);
const menuKey = `${MENU_PREFIX}_${date.getFullYear()}_${weekNumber}`;
const menus: WeekMenu = await storage.getData(menuKey);
if (!menus) {
storage.setData(menuKey, {});
return {};
}
return menus;
} }
// TODO přesun do restaurants.ts // TODO přesun do restaurants.ts
/** /**
* Vrátí menu dané restaurace pro předaný den. * Vrátí menu dané restaurace pro předaný den. Pokud neexistuje, provede jeho stažení a uložení do DB.
* Pokud neexistuje, provede stažení menu pro příslušný týden a uložení do DB.
* *
* @param restaurant restaurace * @param restaurant restaurace
* @param date datum, ke kterému získat menu * @param date datum
* @param mock příznak, zda chceme pouze mock data * @param mock příznak, zda chceme pouze mock data
*/ */
export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): Promise<DayMenu> { export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): Promise<Menu> {
const usedDate = date ?? getToday(); const usedDate = date ?? getToday();
const dayOfWeekIndex = getDayOfWeekIndex(usedDate); await initIfNeeded(usedDate);
const now = new Date().getTime(); const selectedDay = formatDate(usedDate);
if (getIsWeekend(usedDate)) { const clientData: DayData = await storage.getData(selectedDay);
return { const weekNumber = getWeekNumber(usedDate);
lastUpdate: now, const menus = await getMenu(usedDate);
closed: true, if (!menus[weekNumber]) {
food: [], menus[weekNumber] = {};
};
} }
if (!menus[weekNumber][restaurant]) {
let menus = await getMenu(usedDate); menus[weekNumber][restaurant] = {
if (menus == null) { lastUpdate: getHumanTime(new Date()),
menus = [];
}
for (let i = 0; i < 5; i++) {
if (menus[i] == null) {
menus[i] = {};
}
if (menus[i][restaurant] == null) {
menus[i][restaurant] = {
lastUpdate: now,
closed: false, closed: false,
food: [], food: [],
}; };
}
}
if (!menus[dayOfWeekIndex][restaurant]?.food?.length) {
const firstDay = getFirstWorkDayOfWeek(usedDate); const firstDay = getFirstWorkDayOfWeek(usedDate);
const mock = process.env.MOCK_DATA === 'true'; const mock = process.env.MOCK_DATA === 'true';
switch (restaurant) { switch (restaurant) {
case Restaurants.SLADOVNICKA: case Restaurants.SLADOVNICKA:
try {
const sladovnickaFood = await getMenuSladovnicka(firstDay, mock); const sladovnickaFood = await getMenuSladovnicka(firstDay, mock);
for (let i = 0; i < sladovnickaFood.length; i++) { menus[weekNumber][restaurant]!.food = sladovnickaFood;
menus[i][restaurant]!.food = sladovnickaFood[i];
// Velice chatrný a nespolehlivý způsob detekce uzavření... // Velice chatrný a nespolehlivý způsob detekce uzavření...
if (sladovnickaFood[i].length === 1 && sladovnickaFood[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') { if (sladovnickaFood.length === 1 && sladovnickaFood[0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
menus[i][restaurant]!.closed = true; clientData.menus[restaurant]!.closed = true;
} }
} break;
} catch (e: any) { case Restaurants.UMOTLIKU:
console.error("Selhalo načtení jídel pro podnik Sladovnická", e); const uMotlikuFood = await getMenuUMotliku(date, mock);
clientData.menus[restaurant]!.food = uMotlikuFood;
if (uMotlikuFood.length === 1 && uMotlikuFood[0].name.toLowerCase() === 'zavřeno') {
clientData.menus[restaurant]!.closed = true;
} }
break; break;
// case Restaurants.UMOTLIKU:
// try {
// const uMotlikuFood = await getMenuUMotliku(firstDay, mock);
// for (let i = 0; i < uMotlikuFood.length; i++) {
// menus[i][restaurant]!.food = uMotlikuFood[i];
// if (uMotlikuFood[i].length === 1 && uMotlikuFood[i][0].name.toLowerCase() === 'zavřeno') {
// menus[i][restaurant]!.closed = true;
// }
// }
// } catch (e: any) {
// console.error("Selhalo načtení jídel pro podnik U Motlíků", e);
// }
// break;
case Restaurants.TECHTOWER: case Restaurants.TECHTOWER:
try { const techTowerFood = await getMenuTechTower(date, mock);
const techTowerFood = await getMenuTechTower(firstDay, mock); clientData.menus[restaurant]!.food = techTowerFood;
for (let i = 0; i < techTowerFood.length; i++) { if (techTowerFood.length === 1 && techTowerFood[0].name.toLowerCase() === 'svátek') {
menus[i][restaurant]!.food = techTowerFood[i]; clientData.menus[restaurant]!.closed = true;
if (techTowerFood[i]?.length === 1 && techTowerFood[i][0].name.toLowerCase() === 'svátek') {
menus[i][restaurant]!.closed = true;
}
} }
break; break;
} catch (e: any) {
console.error("Selhalo načtení jídel pro podnik TechTower", e);
} }
case Restaurants.ZASTAVKAUMICHALA: storage.setData(selectedDay, clientData);
try {
const zastavkaUmichalaFood = await getMenuZastavkaUmichala(firstDay, mock);
for (let i = 0; i < zastavkaUmichalaFood.length; i++) {
menus[i][restaurant]!.food = zastavkaUmichalaFood[i];
if (zastavkaUmichalaFood[i]?.length === 1 && zastavkaUmichalaFood[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
menus[i][restaurant]!.closed = true;
} }
} return menus[restaurant]!;
break;
} catch (e: any) {
console.error("Selhalo načtení jídel pro podnik Zastávka u Michala", e);
}
case Restaurants.SENKSERIKOVA:
try {
const senkSerikovaFood = await getMenuSenkSerikova(firstDay, mock);
for (let i = 0; i < senkSerikovaFood.length; i++) {
menus[i][restaurant]!.food = senkSerikovaFood[i];
if (senkSerikovaFood[i]?.length === 1 && senkSerikovaFood[i][0].name === 'Pro tento den nebylo zadáno menu.') {
menus[i][restaurant]!.closed = true;
}
}
break;
} catch (e: any) {
console.error("Selhalo načtení jídel pro podnik Pivovarský šenk Šeříková", e);
}
}
await storage.setData(getMenuKey(usedDate), menus);
}
return menus[dayOfWeekIndex][restaurant]!;
} }
/**
* Inicializuje výchozí data pro předané datum, nebo dnešek, pokud není datum předáno.
*
* @param date datum
*/
export async function initIfNeeded(date?: Date) { export async function initIfNeeded(date?: Date) {
const usedDate = formatDate(date ?? getToday()); const usedDate = formatDate(date ?? getToday());
const hasData = await storage.hasData(usedDate); const hasData = await storage.hasData(usedDate);
@@ -220,19 +148,19 @@ export async function initIfNeeded(date?: Date) {
* *
* @param login login uživatele * @param login login uživatele
* @param trusted příznak, zda se jedná o ověřeného uživatele * @param trusted příznak, zda se jedná o ověřeného uživatele
* @param locationKey vybrané "umístění" * @param location vybrané "umístění"
* @param date datum, ke kterému se volba vztahuje * @param date datum, ke kterému se volba vztahuje
* @returns * @returns
*/ */
export async function removeChoices(login: string, trusted: boolean, locationKey: LocationKey, date?: Date) { export async function removeChoices(login: string, trusted: boolean, location: Locations, date?: Date) {
const selectedDay = formatDate(date ?? getToday()); const selectedDay = formatDate(date ?? getToday());
let data: DayData = await storage.getData(selectedDay); let data: DayData = await storage.getData(selectedDay);
validateTrusted(data, login, trusted); validateTrusted(data, login, trusted);
if (locationKey in data.choices) { if (location in data.choices) {
if (data.choices[locationKey] && login in data.choices[locationKey]) { if (login in data.choices[location]) {
delete data.choices[locationKey][login] delete data.choices[location][login]
if (Object.keys(data.choices[locationKey]).length === 0) { if (Object.keys(data.choices[location]).length === 0) {
delete data.choices[locationKey] delete data.choices[location]
} }
await storage.setData(selectedDay, data); await storage.setData(selectedDay, data);
} }
@@ -246,20 +174,20 @@ export async function removeChoices(login: string, trusted: boolean, locationKey
* *
* @param login login uživatele * @param login login uživatele
* @param trusted příznak, zda se jedná o ověřeného uživatele * @param trusted příznak, zda se jedná o ověřeného uživatele
* @param locationKey vybrané "umístění" * @param location vybrané "umístění"
* @param foodIndex index jídla v jídelním lístku daného umístění, pokud existuje * @param foodIndex index jídla v jídelním lístku daného umístění, pokud existuje
* @param date datum, ke kterému se volba vztahuje * @param date datum, ke kterému se volba vztahuje
* @returns * @returns
*/ */
export async function removeChoice(login: string, trusted: boolean, locationKey: LocationKey, foodIndex: number, date?: Date) { export async function removeChoice(login: string, trusted: boolean, location: Locations, foodIndex: number, date?: Date) {
const selectedDay = formatDate(date ?? getToday()); const selectedDay = formatDate(date ?? getToday());
let data: DayData = await storage.getData(selectedDay); let data: DayData = await storage.getData(selectedDay);
validateTrusted(data, login, trusted); validateTrusted(data, login, trusted);
if (locationKey in data.choices) { if (location in data.choices) {
if (data.choices[locationKey] && login in data.choices[locationKey]) { if (login in data.choices[location]) {
const index = data.choices[locationKey][login].options.indexOf(foodIndex); const index = data.choices[location][login].options.indexOf(foodIndex);
if (index > -1) { if (index > -1) {
data.choices[locationKey][login].options.splice(index, 1) data.choices[location][login].options.splice(index, 1)
await storage.setData(selectedDay, data); await storage.setData(selectedDay, data);
} }
} }
@@ -268,23 +196,17 @@ export async function removeChoice(login: string, trusted: boolean, locationKey:
} }
/** /**
* Odstraní kompletně volbu uživatele, vyjma ignoredLocationKey (pokud byla předána a existuje). * Odstraní kompletně volbu uživatele.
* *
* @param login login uživatele * @param login login uživatele
* @param date datum, ke kterému se volby vztahují
* @param ignoredLocationKey volba, která nebude odstraněna, pokud existuje
*/ */
async function removeChoiceIfPresent(login: string, date: string, ignoredLocationKey?: LocationKey) { async function removeChoiceIfPresent(login: string, date: string) {
let data: DayData = await storage.getData(date); let data: DayData = await storage.getData(date);
for (const key of Object.keys(data.choices)) { for (const key of Object.keys(data.choices)) {
const locationKey = key as LocationKey; if (login in data.choices[key]) {
if (ignoredLocationKey != null && ignoredLocationKey == locationKey) { delete data.choices[key][login];
continue; if (Object.keys(data.choices[key]).length === 0) {
} delete data.choices[key];
if (data.choices[locationKey] && login in data.choices[locationKey]) {
delete data.choices[locationKey][login];
if (Object.keys(data.choices[locationKey]).length === 0) {
delete data.choices[locationKey];
} }
await storage.setData(date, data); await storage.setData(date, data);
} }
@@ -319,71 +241,37 @@ function validateTrusted(data: ClientData, login: string, trusted: boolean) {
* *
* @param login login uživatele * @param login login uživatele
* @param trusted příznak, zda se jedná o ověřeného uživatele * @param trusted příznak, zda se jedná o ověřeného uživatele
* @param locationKey vybrané "umístění" * @param location vybrané "umístění"
* @param foodIndex volitelný index jídla v daném umístění * @param foodIndex volitelný index jídla v daném umístění
* @param trusted příznak, zda se jedná o ověřeného uživatele * @param trusted příznak, zda se jedná o ověřeného uživatele
* @param date datum, ke kterému se volba vztahuje * @param date datum, ke kterému se volba vztahuje
* @returns aktuální data * @returns aktuální data
*/ */
export async function addChoice(login: string, trusted: boolean, locationKey: LocationKey, foodIndex?: number, date?: Date) { export async function addChoice(login: string, trusted: boolean, location: Locations, foodIndex?: number, date?: Date) {
const usedDate = date ?? getToday(); await initIfNeeded();
await initIfNeeded(usedDate); const selectedDate = formatDate(date ?? getToday());
const selectedDate = formatDate(usedDate);
let data: DayData = await storage.getData(selectedDate); let data: DayData = await storage.getData(selectedDate);
validateTrusted(data, login, trusted); validateTrusted(data, login, trusted);
// Pokud měníme pouze lokaci, mažeme případné předchozí // Pokud měníme pouze lokaci, mažeme případné předchozí
if (foodIndex == null) { if (foodIndex == null) {
data = await removeChoiceIfPresent(login, selectedDate); data = await removeChoiceIfPresent(login, selectedDate);
} else {
// Mažeme případné ostatní volby (měla by být maximálně jedna)
removeChoiceIfPresent(login, selectedDate, locationKey);
} }
// TODO vytáhnout inicializaci "prázdné struktury" do vlastní funkce if (!(location in data.choices)) {
if (!(data.choices[locationKey])) { data.choices[location] = {};
data.choices[locationKey] = {}
} }
if (!(login in data.choices[locationKey])) { if (!(login in data.choices[location])) {
if (!data.choices[locationKey]) { data.choices[location][login] = {
data.choices[locationKey] = {}
}
data.choices[locationKey][login] = {
trusted, trusted,
options: [] options: []
}; };
} }
if (foodIndex != null && !data.choices[locationKey][login].options.includes(foodIndex)) { if (foodIndex != null && !data.choices[location][login].options.includes(foodIndex)) {
data.choices[locationKey][login].options.push(foodIndex); data.choices[location][login].options.push(foodIndex);
} }
await storage.setData(selectedDate, data); await storage.setData(selectedDate, data);
return data; return data;
} }
/**
* Aktualizuje poznámku k aktuálně vybrané možnosti.
*
* @param login login uživatele
* @param trusted příznak, zda se jedná o ověřeného uživatele
* @param note poznámka
* @param date datum, ke kterému se volba vztahuje
*/
export async function updateNote(login: string, trusted: boolean, note?: string, date?: Date) {
const usedDate = date ?? getToday();
await initIfNeeded(usedDate);
const selectedDate = formatDate(usedDate);
let data: DayData = await storage.getData(selectedDate);
validateTrusted(data, login, trusted);
const userEntry = data.choices != null && Object.entries(data.choices).find(entry => entry[1][login] != null);
if (userEntry) {
if (!note || !note.length) {
delete userEntry[1][login].note;
} else {
userEntry[1][login].note = note;
}
await storage.setData(selectedDate, data);
}
return data;
}
/** /**
* Aktualizuje preferovaný čas odchodu strávníka. * Aktualizuje preferovaný čas odchodu strávníka.
* *

View File

@@ -1,17 +1,8 @@
import JSONdb from 'simple-json-db'; import JSONdb from 'simple-json-db';
import { StorageInterface } from "./StorageInterface"; import { StorageInterface } from "./StorageInterface";
import * as fs from 'fs';
import * as path from 'path';
const dbPath = path.resolve(__dirname, '../../data/db.json'); const db = new JSONdb('./data.json');
const dbDir = path.dirname(dbPath);
// Zajistěte, že adresář existuje
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
const db = new JSONdb(dbPath);
/** /**
* Implementace úložiště používající JSON soubor. * Implementace úložiště používající JSON soubor.
*/ */

View File

@@ -1,13 +1,9 @@
import {Choices, LocationKey } from "../../types";
/** Vrátí datum v ISO formátu. */ /** Vrátí datum v ISO formátu. */
export function formatDate(date: Date, format?: string) { export function formatDate(date: Date) {
let day = String(date.getDate()).padStart(2, '0'); let currentDay = String(date.getDate()).padStart(2, '0');
let month = String(date.getMonth() + 1).padStart(2, "0"); let currentMonth = String(date.getMonth() + 1).padStart(2, "0");
let year = String(date.getFullYear()); let currentYear = date.getFullYear();
return `${currentYear}-${currentMonth}-${currentDay}`;
const f = (format === undefined) ? 'YYYY-MM-DD' : format;
return f.replace('DD', day).replace('MM', month).replace('YYYY', year);
} }
/** Vrátí human-readable reprezentaci předaného data pro zobrazení. */ /** Vrátí human-readable reprezentaci předaného data pro zobrazení. */
@@ -111,22 +107,3 @@ export const checkBodyParams = (req: any, paramNames: string[]) => {
// TODO umístit do samostatného souboru // TODO umístit do samostatného souboru
export class InsufficientPermissions extends Error { } export class InsufficientPermissions extends Error { }
export const getUsersByLocation = (choices: Choices, login: string): string[] => {
const result: string[] = [];
for (const location of Object.entries(choices)) {
const locationKey = location[0] as LocationKey;
const locationValue = location[1];
if (locationValue[login]) {
for (const username in choices[locationKey]) {
if (choices[locationKey].hasOwnProperty(username)) {
result.push(username);
}
}
break;
}
}
return result;
}

View File

@@ -46,8 +46,8 @@ export async function updateFeatureVote(login: string, option: FeatureRequest, a
} }
} }
} else if (active) { } else if (active) {
if (data[login].length == 4) { if (data[login].length == 3) {
throw Error('Je možné hlasovat pro maximálně 4 možnosti'); throw Error('Je možné hlasovat pro maximálně 3 možnosti');
} }
data[login].push(option); data[login].push(option);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,58 +0,0 @@
import { FeatureRequest, LocationKey, PizzaOrder } from "./Types";
export type ILocationKey = {
locationKey: LocationKey,
}
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 JdemeObedRequest = ILocationKey;
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: PizzaOrder,
}
export type UpdatePizzaDayNoteRequest = {
note?: string,
}
export type UpdatePizzaFeeRequest = {
login: string,
text?: string,
price?: number,
}
export type UpdateFeatureVoteRequest = {
option: FeatureRequest,
active: boolean,
}

View File

@@ -1,28 +1,24 @@
/** Výčtový typ pro restaurace, pro které umíme získat a parsovat obědové menu. */ /** Výčtový typ pro restaurace, pro které umíme získat a parsovat obědové menu. */
export enum Restaurants { export enum Restaurants {
SLADOVNICKA = 'sladovnicka', SLADOVNICKA = 'sladovnicka',
// UMOTLIKU = 'uMotliku', UMOTLIKU = 'uMotliku',
TECHTOWER = 'techTower', TECHTOWER = 'techTower',
ZASTAVKAUMICHALA = 'zastavkaUmichala',
SENKSERIKOVA = 'senkSerikova',
} }
export type FoodChoices = { export interface FoodChoices {
trusted: boolean, trusted: boolean,
options: number[], options: number[],
departureTime?: string, departureTime?: string,
note?: string,
} }
// TODO okomentovat / rozdělit export interface Choices {
export type Choices = { [location: string]: {
[location in LocationKey]?: {
[login: string]: FoodChoices [login: string]: FoodChoices
} },
} }
/** Velikost konkrétní pizzy */ /** Velikost konkrétní pizzy */
export type PizzaSize = { export interface PizzaSize {
varId: number, // unikátní ID varianty pizzy varId: number, // unikátní ID varianty pizzy
size: string, // velikost pizzy, např. "30cm" size: string, // velikost pizzy, např. "30cm"
pizzaPrice: number, // cena samotné pizzy pizzaPrice: number, // cena samotné pizzy
@@ -31,14 +27,14 @@ export type PizzaSize = {
} }
/** Jedna konkrétní pizza */ /** Jedna konkrétní pizza */
export type Pizza = { export interface Pizza {
name: string, // název pizzy name: string, // název pizzy
ingredients: string[], // seznam ingrediencí ingredients: string[], // seznam ingrediencí
sizes: PizzaSize[], // dostupné velikosti pizzy sizes: PizzaSize[], // dostupné velikosti pizzy
} }
/** Objednávka jedné konkrétní pizzy */ /** Objednávka jedné konkrétní pizzy */
export type PizzaOrder = { export interface PizzaOrder {
varId: number, // unikátní ID varianty pizzy varId: number, // unikátní ID varianty pizzy
name: string, // název pizzy name: string, // název pizzy
size: string, // velikost pizzy jako string (30cm) size: string, // velikost pizzy jako string (30cm)
@@ -46,7 +42,7 @@ export type PizzaOrder = {
} }
/** Celková objednávka jednoho člověka */ /** Celková objednávka jednoho člověka */
export type Order = { export interface Order {
customer: string, // jméno objednatele customer: string, // jméno objednatele
pizzaList: PizzaOrder[], // seznam objednaných pizz pizzaList: PizzaOrder[], // seznam objednaných pizz
fee?: { text?: string, price: number }, // příplatek (např. za extra ingredience) fee?: { text?: string, price: number }, // příplatek (např. za extra ingredience)
@@ -72,38 +68,40 @@ interface PizzaDay {
} }
/** Týdenní menu jednotlivých restaurací. */ /** Týdenní menu jednotlivých restaurací. */
export type WeekMenu = { export interface WeekMenu {
[dayIndex: number]: { [dayIndex: number]: {
[restaurant in Restaurants]?: DayMenu [restaurant in Restaurants]?: Menu
} }
} }
/** Data vztahující se k jednomu konkrétnímu dni. */ /** Data vztahující se k jednomu konkrétnímu dni. */
export type DayData = { export interface DayData {
date: string, // datum dne date: string, // datum dne
isWeekend: boolean, // příznak, zda je datum víkend isWeekend: boolean, // příznak, zda je datum víkend
weekIndex: number, // index dne v týdnu (0-6) weekIndex: number, // index dne v týdnu (0-6)
choices: Choices, // seznam voleb uživatelů choices: Choices, // seznam voleb uživatelů
menus?: { [restaurant in Restaurants]?: DayMenu }, // menu jednotlivých restaurací // TODO smazat
departureTimes: DepartureTime[], // seznam možných časů odchodu
menus?: { [restaurant in Restaurants]?: Menu }, // menu jednotlivých restaurací
pizzaDay?: PizzaDay, // pizza day pro dnešní den, pokud existuje pizzaDay?: PizzaDay, // pizza day pro dnešní den, pokud existuje
pizzaList?: Pizza[], // seznam dostupných pizz pro dnešní den pizzaList?: Pizza[], // seznam dostupných pizz pro dnešní den
pizzaListLastUpdate?: Date, // datum a čas poslední aktualizace pizz pizzaListLastUpdate?: Date, // datum a čas poslední aktualizace pizz
} }
/** Veškerá data pro zobrazení na klientovi. */ /** Veškerá data pro zobrazení na klientovi. */
export type ClientData = DayData & { export interface ClientData extends DayData {
todayWeekIndex?: number, // index dnešního dne v týdnu (0-6) todayWeekIndex?: number, // index dnešního dne v týdnu (0-6)
} }
/** Nabídka jídel jednoho podniku pro jeden konkrétní den. */ /** Nabídka jídel jednoho podniku. */
export type DayMenu = { export interface Menu {
lastUpdate: number, // UNIX timestamp poslední aktualizace menu lastUpdate: string, // human-readable čas poslední aktualizace menu
closed: boolean, // příznak, zda je daný podnik v tento den zavřený closed: boolean, // příznak, zda je daný podnik aktuálně zavřený
food: Food[], // seznam jídel v menu food: Food[], // seznam jídel v menu
} }
/** Jídlo z obědového menu restaurace. */ /** Jídlo z obědového menu restaurace. */
export type Food = { export interface Food {
amount?: string, // množství standardní porce, např. 0,33l nebo 150g amount?: string, // množství standardní porce, např. 0,33l nebo 150g
name: string, // název/popis jídla name: string, // název/popis jídla
price: string, // cena ve formátu '135 Kč' price: string, // cena ve formátu '135 Kč'
@@ -111,36 +109,33 @@ export type Food = {
} }
// TODO tohle je dost špatné pojmenování, vzhledem k tomu co to obsahuje // TODO tohle je dost špatné pojmenování, vzhledem k tomu co to obsahuje
// TODO pokud by se použilo ovládáni výběru obědu kliknutím, pak bych restaurace z tohoto výčtu vyhodil
export enum Locations { export enum Locations {
SLADOVNICKA = 'Sladovnická', SLADOVNICKA = 'Sladovnická',
// UMOTLIKU = 'U Motlíků', UMOTLIKU = 'U Motlíků',
TECHTOWER = 'TechTower', TECHTOWER = 'TechTower',
ZASTAVKAUMICHALA = 'Zastávka u Michala',
SENKSERIKOVA = 'Pivovarský šenk Šeříková',
SPSE = 'SPŠE', SPSE = 'SPŠE',
PIZZA = 'Pizza day', PIZZA = 'Pizza day',
OBJEDNAVAM = 'Budu objednávat', OBJEDNAVAM = 'Budu objednávat',
NEOBEDVAM = 'Mám vlastní/neobědvám', NEOBEDVAM = 'Mám vlastní/neobědvám',
ROZHODUJI = 'Rozhoduji se',
} }
// TODO totéž
export type LocationKey = keyof typeof Locations;
export enum UdalostEnum { export enum UdalostEnum {
ZAHAJENA_PIZZA = "Zahájen pizza day", ZAHAJENA_PIZZA = "Zahájen pizza day",
OBJEDNANA_PIZZA = "Objednána pizza", OBJEDNANA_PIZZA = "Objednána pizza"
JDEME_OBED = "Jdeme na oběd",
} }
export type NotififaceInput = { export interface NotififaceInput {
locationKey?: LocationKey,
udalost: UdalostEnum, udalost: UdalostEnum,
user: string, user: string,
} }
export type GotifyServer = { export interface NotifikaceData {
input: NotififaceInput,
gotify?: boolean,
teams?: boolean,
}
export interface GotifyServer {
server: string; server: string;
api_keys: string[]; api_keys: string[];
} }
@@ -163,55 +158,12 @@ export enum DepartureTime {
} }
export enum FeatureRequest { export enum FeatureRequest {
CUSTOM_QR = "Ruční generování QR kódů mimo Pizza day (např. při objednává)", SINGLE_PAYMENT = "Možnost úhrady v podniku jednou osobou a generování QR pro ostatní",
FAVORITES = "Možnost označovat si jídla jako oblíbená (taková jídla by se uživateli následně zvýrazňovala)", NOTIFICATIONS = "Podpora push notifikací na mobil",
SINGLE_PAYMENT = "Možnost úhrady v podniku za všechny jednou osobou a následné generování QR ostatním", STATISTICS = "Statistiky (nejoblíbenější podnik, nejpopulárnější jídla, nejobjednávanější pizzy, ...)",
NO_WEEKENDS = "Zrušení \"užívejte víkend\", místo toho umožnit zpětně náhled na uplynulý týden",
QR_FOREVER = "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\")",
PIZZA_PICTURES = "Zobrazování náhledů (fotografií) pizz v rámci Pizza day",
STATISTICS = "Statistiky (nejoblíbenější podnik, nejpopulárnější jídla, nejobjednávanější pizzy, nejčastější uživatelé, ...)",
RESPONSIVITY = "Vylepšení responzivního designu", RESPONSIVITY = "Vylepšení responzivního designu",
SECURITY = "Zvýšení zabezpečení aplikace", SECURITY = "Zvýšení zabezpečení aplikace",
SAFETY = "Zvýšená ochrana proti chybám uživatele (potvrzovací dialogy, překliky, ...)", SAFETY = "Zvýšená ochrana proti chybám uživatele (potvrzovací dialogy, překliky, ...)",
UI = "Celkové vylepšení UI/UX", UI = "Celkové vylepšení UI/UX",
DEVELOPMENT = "Zlepšení dokumentace/postupů pro ostatní vývojáře" DEVELOPMENT = "Zlepšení dokumentace/postupů pro ostatní vývojáře"
} }
export type EasterEgg = {
path: string;
url: string;
startOffset: number;
endOffset: number;
duration: number;
width?: string;
zIndex?: number;
position?: "absolute";
animationName?: string;
animationDuration?: string;
animationTimingFunction?: string;
}
// TODO aktuálně se k ničemu nepoužívá
export type AnimationPosition = {
left?: string,
startLeft?: string,
"--start-left"?: string,
right?: string,
startRight?: string,
"--start-right"?: string,
top?: string,
startTop?: string,
"--start-top"?: string,
bottom?: string,
startBottom?: string,
"--start-bottom"?: string,
endLeft?: string,
"--end-left"?: string,
endRight?: string,
"--end-right"?: string,
endTop?: string,
"--end-top"?: string,
endBottom?: string,
"--end-bottom"?: string,
rotate?: string,
}

View File

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

11200
yarn.lock Normal file

File diff suppressed because it is too large Load Diff