Compare commits
1 Commits
d144c55bf7
...
feat/enumF
| Author | SHA1 | Date | |
|---|---|---|---|
| bc039a361d |
22
.gitignore
vendored
22
.gitignore
vendored
@@ -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*
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
variables:
|
|
||||||
- &node_image 'node:22-alpine'
|
|
||||||
- &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}}"
|
|
||||||
20
Dockerfile
20
Dockerfile
@@ -1,7 +1,5 @@
|
|||||||
ARG NODE_VERSION="node:22-alpine"
|
|
||||||
|
|
||||||
# Builder
|
# Builder
|
||||||
FROM ${NODE_VERSION} AS builder
|
FROM node:18-alpine3.18 AS builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
@@ -47,18 +45,16 @@ WORKDIR /build/client
|
|||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM ${NODE_VERSION}
|
FROM node:18-alpine3.18
|
||||||
|
ENV LANG cs_CZ.UTF-8
|
||||||
RUN apk add --no-cache tzdata
|
ENV NODE_ENV production
|
||||||
ENV TZ=Europe/Prague \
|
|
||||||
LC_ALL=cs_CZ.UTF-8 \
|
|
||||||
NODE_ENV=production
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Vykopírování sestaveného serveru
|
# Vykopírování sestaveného serveru
|
||||||
COPY --from=builder /build/server/node_modules ./server/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 server/resources ./server/resources
|
||||||
|
|
||||||
# Vykopírování sestaveného klienta
|
# Vykopírování sestaveného klienta
|
||||||
COPY --from=builder /build/client/dist ./public
|
COPY --from=builder /build/client/dist ./public
|
||||||
@@ -67,10 +63,8 @@ COPY --from=builder /build/client/dist ./public
|
|||||||
COPY /server/.env.production ./server/src
|
COPY /server/.env.production ./server/src
|
||||||
|
|
||||||
# Zkopírování konfigurace easter eggů
|
# Zkopírování konfigurace easter eggů
|
||||||
RUN if [ -f /server/.easter-eggs.json ]; then cp /server/.easter-eggs.json ./server/; fi
|
# TODO tohle spadne když nebude existovat!
|
||||||
|
COPY /server/.easter-eggs.json ./server/
|
||||||
# Export /data/db.json do složky /data
|
|
||||||
VOLUME ["/data"]
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
ARG NODE_VERSION="node:22-alpine"
|
|
||||||
|
|
||||||
FROM ${NODE_VERSION}
|
|
||||||
|
|
||||||
RUN apk add --no-cache tzdata
|
|
||||||
ENV TZ=Europe/Prague \
|
|
||||||
LC_ALL=cs_CZ.UTF-8 \
|
|
||||||
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" ]
|
|
||||||
@@ -10,7 +10,7 @@ Aplikace sestává ze dvou modulů + společných TypeScript definic (adresář
|
|||||||
## Spuštění pro vývoj
|
## Spuštění pro vývoj
|
||||||
### Závislosti
|
### Závislosti
|
||||||
#### Klient/server
|
#### Klient/server
|
||||||
- [Node.js 22.x](https://nodejs.dev)
|
- [Node.js 18.x](https://nodejs.dev)
|
||||||
- [Yarn 1.22.x (Classic)](https://classic.yarnpkg.com)
|
- [Yarn 1.22.x (Classic)](https://classic.yarnpkg.com)
|
||||||
|
|
||||||
### Spuštění na *nix platformách
|
### Spuštění na *nix platformách
|
||||||
|
|||||||
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
|
src/types
|
||||||
@@ -21,12 +21,9 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-jwt": "^1.2.0",
|
"react-jwt": "^1.2.0",
|
||||||
"react-modal": "^3.16.1",
|
"react-modal": "^3.16.1",
|
||||||
"react-router": "^7.2.0",
|
|
||||||
"react-router-dom": "^7.2.0",
|
|
||||||
"react-select-search": "^4.1.6",
|
"react-select-search": "^4.1.6",
|
||||||
"react-snowfall": "^2.2.0",
|
"react-snowfall": "^2.2.0",
|
||||||
"react-toastify": "^10.0.4",
|
"react-toastify": "^10.0.4",
|
||||||
"recharts": "^2.15.1",
|
|
||||||
"sass": "^1.80.6",
|
"sass": "^1.80.6",
|
||||||
"socket.io-client": "^4.6.1",
|
"socket.io-client": "^4.6.1",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
@@ -34,8 +31,9 @@
|
|||||||
"vite-tsconfig-paths": "^5.1.4"
|
"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 && vite",
|
||||||
|
"build": "yarn copy-types && vite build"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
|||||||
@@ -8,23 +8,22 @@ 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.scss';
|
||||||
|
import { SelectSearchOption } from 'react-select-search';
|
||||||
import { faCircleCheck, faNoteSticky, faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
import { faCircleCheck, faNoteSticky, faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { useSettings } from './context/settings';
|
import { useSettings } from './context/settings';
|
||||||
import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, DayMenu, DepartureTime, LocationKey } from '../../types';
|
import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices, DayMenu, DepartureTime } 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, jdemeObed, updateNote } from './api/FoodApi';
|
||||||
import { getHumanDateTime, isInTheFuture } from './Utils';
|
import { getHumanDateTime } from './Utils';
|
||||||
import NoteModal from './components/modals/NoteModal';
|
import NoteModal from './components/modals/NoteModal';
|
||||||
import { useEasterEgg } from './context/eggs';
|
import { useEasterEgg } from './context/eggs';
|
||||||
import { getImage } from './api/EasterEggApi';
|
import { getImage } from './api/EasterEggApi';
|
||||||
import { Link } from 'react-router';
|
|
||||||
import { STATS_URL } from './AppRoutes';
|
|
||||||
|
|
||||||
const EVENT_CONNECT = "connect"
|
const EVENT_CONNECT = "connect"
|
||||||
|
|
||||||
@@ -142,8 +141,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);
|
||||||
@@ -191,17 +193,8 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]);
|
}, [auth?.login, easterEgg?.duration, easterEgg?.url, eggImage]);
|
||||||
|
|
||||||
const doAddClickFoodChoice = async (location: Locations, foodIndex?: number) => {
|
|
||||||
if (document.getSelection()?.type !== 'Range') { // pouze pokud se nejedná o výběr textu
|
|
||||||
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 locationKey = event.target.value as Locations;
|
||||||
if (auth?.login) {
|
if (auth?.login) {
|
||||||
await errorHandler(() => addChoice(locationKey, undefined, dayIndex));
|
await errorHandler(() => addChoice(locationKey, undefined, dayIndex));
|
||||||
if (foodChoiceRef.current?.value) {
|
if (foodChoiceRef.current?.value) {
|
||||||
@@ -218,14 +211,14 @@ function App() {
|
|||||||
|
|
||||||
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 locationKey = choiceRef.current.value as Locations;
|
||||||
if (auth?.login) {
|
if (auth?.login) {
|
||||||
await errorHandler(() => addChoice(locationKey, Number(event.target.value), dayIndex));
|
await errorHandler(() => addChoice(locationKey, Number(event.target.value), dayIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const doRemoveChoices = async (locationKey: LocationKey) => {
|
const doRemoveChoices = async (locationKey: Locations) => {
|
||||||
if (auth?.login) {
|
if (auth?.login) {
|
||||||
await errorHandler(() => removeChoices(locationKey, dayIndex));
|
await errorHandler(() => removeChoices(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
|
||||||
@@ -238,7 +231,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const doRemoveFoodChoice = async (locationKey: LocationKey, foodIndex: number) => {
|
const doRemoveFoodChoice = async (locationKey: Locations, foodIndex: number) => {
|
||||||
if (auth?.login) {
|
if (auth?.login) {
|
||||||
await errorHandler(() => removeChoice(locationKey, foodIndex, dayIndex));
|
await errorHandler(() => removeChoice(locationKey, foodIndex, dayIndex));
|
||||||
if (choiceRef?.current?.value) {
|
if (choiceRef?.current?.value) {
|
||||||
@@ -342,17 +335,15 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderFoodTable = (location: Locations, menu: DayMenu) => {
|
const renderFoodTable = (name: string, menu: DayMenu) => {
|
||||||
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.map((f: Food, index: number) =>
|
{menu.food.filter(f => (settings?.hideSoups ? !f.isSoup : true)).map((f: any, index: number) =>
|
||||||
(!hideSoups || !f.isSoup) &&
|
<tr key={index}>
|
||||||
<tr key={f.name} onClick={() => doAddClickFoodChoice(location, 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>
|
||||||
@@ -363,8 +354,8 @@ 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={6} className='mt-3'>
|
||||||
<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: {getHumanDateTime(new Date(menu.lastUpdate))}</small>}
|
||||||
{content}
|
{content}
|
||||||
</Col>
|
</Col>
|
||||||
@@ -414,8 +405,8 @@ function App() {
|
|||||||
<img src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} />
|
<img src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} />
|
||||||
Poslední změny:
|
Poslední změny:
|
||||||
<ul>
|
<ul>
|
||||||
<li>Možnost výběru restaurace a jídel kliknutím v tabulce</li>
|
<li>Zimní atmosféra</li>
|
||||||
<li><Link to={STATS_URL}>Statistiky</Link></li>
|
<li>Odstranění podniku U Motlíků</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Alert>
|
</Alert>
|
||||||
{dayIndex != null &&
|
{dayIndex != null &&
|
||||||
@@ -426,11 +417,9 @@ function App() {
|
|||||||
</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'>
|
||||||
@@ -440,8 +429,11 @@ function App() {
|
|||||||
<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;
|
||||||
})
|
})
|
||||||
@@ -459,9 +451,7 @@ 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)
|
{Object.values(DepartureTime).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>
|
||||||
</>}
|
</>}
|
||||||
</>}
|
</>}
|
||||||
@@ -469,20 +459,21 @@ function App() {
|
|||||||
<Table bordered className='mt-5'>
|
<Table bordered className='mt-5'>
|
||||||
<tbody>
|
<tbody>
|
||||||
{Object.keys(data.choices).map(key => {
|
{Object.keys(data.choices).map(key => {
|
||||||
const locationKey = key as LocationKey;
|
const locationKey = key as keyof typeof Locations;
|
||||||
|
console.log("Mapuji location key", locationKey);
|
||||||
const locationName = Locations[locationKey];
|
const locationName = Locations[locationKey];
|
||||||
|
console.log("Location name", locationName);
|
||||||
|
console.log("Obsah data.choices", data.choices);
|
||||||
const loginObject = data.choices[locationKey];
|
const loginObject = data.choices[locationKey];
|
||||||
|
// TODO toto je hovnokód, mělo by to být napsané tak, aby si na to TypeScript nemohl stěžovat
|
||||||
if (!loginObject) {
|
if (!loginObject) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const locationLoginList = Object.entries(loginObject);
|
const locationLoginList = Object.entries(loginObject);
|
||||||
const locationPickCount = locationLoginList.length
|
console.log("Entries", locationLoginList);
|
||||||
return (
|
return (
|
||||||
<tr key={key}>
|
<tr key={key}>
|
||||||
{(locationPickCount?? 0) > 1 ? (
|
<td>{locationName}</td>
|
||||||
<td>{locationName} ({locationPickCount})</td>
|
|
||||||
) : (
|
|
||||||
<td>{locationName}</td>)}
|
|
||||||
<td className='p-0'>
|
<td className='p-0'>
|
||||||
<Table>
|
<Table>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -503,20 +494,20 @@ function App() {
|
|||||||
setNoteModalOpen(true);
|
setNoteModalOpen(true);
|
||||||
}} title='Upravit poznámku' className='action-icon' icon={faNoteSticky} />}
|
}} title='Upravit poznámku' className='action-icon' icon={faNoteSticky} />}
|
||||||
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => {
|
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => {
|
||||||
doRemoveChoices(key as LocationKey);
|
doRemoveChoices(key as Locations); // TODO dořešit
|
||||||
}} 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>
|
||||||
<ul>
|
<ul>
|
||||||
{userChoices?.map(foodIndex => {
|
{userChoices?.map(foodIndex => {
|
||||||
// TODO narovnat, tohle je zbytečně složité
|
const locationsKey = Object.keys(Locations)[Number(key)]
|
||||||
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 && canChangeChoice && <FontAwesomeIcon onClick={() => {
|
||||||
doRemoveFoodChoice(key as LocationKey, foodIndex);
|
doRemoveFoodChoice(key as Locations, foodIndex); // TODO dořešit
|
||||||
}} title={`Odstranit ${foodName}`} className='action-icon' icon={faTrashCan} />}
|
}} title={`Odstranit ${foodName}`} className='action-icon' icon={faTrashCan} />}
|
||||||
</li>
|
</li>
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Routes, Route } from "react-router-dom";
|
|
||||||
import { ProvideSettings } from "./context/settings";
|
|
||||||
import Snowfall from "react-snowfall";
|
|
||||||
import { ToastContainer } from "react-toastify";
|
|
||||||
import { SocketContext, socket } from "./context/socket";
|
|
||||||
import StatsPage from "./pages/StatsPage";
|
|
||||||
import App from "./App";
|
|
||||||
|
|
||||||
export const STATS_URL = '/stats';
|
|
||||||
|
|
||||||
export default function AppRoutes() {
|
|
||||||
return (
|
|
||||||
<Routes>
|
|
||||||
<Route path={STATS_URL} element={<StatsPage />} />
|
|
||||||
<Route path="/" element={
|
|
||||||
<ProvideSettings>
|
|
||||||
<SocketContext.Provider value={socket}>
|
|
||||||
<>
|
|
||||||
<Snowfall style={{
|
|
||||||
zIndex: 2,
|
|
||||||
position: 'fixed',
|
|
||||||
width: '100vw',
|
|
||||||
height: '100vh'
|
|
||||||
}} />
|
|
||||||
<App />
|
|
||||||
</>
|
|
||||||
<ToastContainer />
|
|
||||||
</SocketContext.Provider>
|
|
||||||
</ProvideSettings>
|
|
||||||
} />
|
|
||||||
</Routes>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import {DepartureTime} from "../../types";
|
|
||||||
|
|
||||||
const TOKEN_KEY = "token";
|
const TOKEN_KEY = "token";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,62 +43,3 @@ export function getHumanDateTime(datetime: Date) {
|
|||||||
return `${day}.${month}.${year} ${hours}:${minutes}`;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vrátí index dne v týdnu, kde pondělí=0, neděle=6
|
|
||||||
*
|
|
||||||
* @param date datum
|
|
||||||
* @returns index dne v týdnu
|
|
||||||
*/
|
|
||||||
export const getDayOfWeekIndex = (date: Date) => {
|
|
||||||
// https://stackoverflow.com/a/4467559
|
|
||||||
return (((date.getDay() - 1) % 7) + 7) % 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Vrátí první pracovní den v týdnu předaného data. */
|
|
||||||
export function getFirstWorkDayOfWeek(date: Date) {
|
|
||||||
const firstDay = new Date(date.getTime());
|
|
||||||
firstDay.setDate(date.getDate() - getDayOfWeekIndex(date));
|
|
||||||
return firstDay;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Vrátí poslední pracovní den v týdnu předaného data. */
|
|
||||||
export function getLastWorkDayOfWeek(date: Date) {
|
|
||||||
const lastDay = new Date(date.getTime());
|
|
||||||
lastDay.setDate(date.getDate() + (4 - getDayOfWeekIndex(date)));
|
|
||||||
return lastDay;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Vrátí datum v ISO formátu. */
|
|
||||||
export function formatDate(date: Date, format?: string) {
|
|
||||||
let day = String(date.getDate()).padStart(2, '0');
|
|
||||||
let month = String(date.getMonth() + 1).padStart(2, "0");
|
|
||||||
let year = String(date.getFullYear());
|
|
||||||
|
|
||||||
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í. */
|
|
||||||
export function getHumanDate(date: Date) {
|
|
||||||
let currentDay = String(date.getDate()).padStart(2, '0');
|
|
||||||
let currentMonth = String(date.getMonth() + 1).padStart(2, "0");
|
|
||||||
let currentYear = date.getFullYear();
|
|
||||||
return `${currentDay}.${currentMonth}.${currentYear}`;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EasterEgg } from "../../../types";
|
import { EasterEgg } from "../types";
|
||||||
import { api } from "./Api";
|
import { api } from "./Api";
|
||||||
|
|
||||||
const EASTER_EGGS_API_PREFIX = '/api/easterEggs';
|
const EASTER_EGGS_API_PREFIX = '/api/easterEggs';
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { AddChoiceRequest, ChangeDepartureTimeRequest, LocationKey, RemoveChoiceRequest, RemoveChoicesRequest, UpdateNoteRequest } from "../../../types";
|
import { AddChoiceRequest, ChangeDepartureTimeRequest, Locations, 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 (locationKey: keyof typeof Locations, foodIndex?: number, dayIndex?: number) => {
|
||||||
return await api.post<AddChoiceRequest, void>(`${FOOD_API_PREFIX}/addChoice`, { locationKey, foodIndex, dayIndex });
|
return await api.post<AddChoiceRequest, void>(`${FOOD_API_PREFIX}/addChoice`, { locationKey, foodIndex, dayIndex });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const removeChoices = async (locationKey: LocationKey, dayIndex?: number) => {
|
export const removeChoices = async (locationKey: keyof typeof Locations, dayIndex?: number) => {
|
||||||
return await api.post<RemoveChoicesRequest, void>(`${FOOD_API_PREFIX}/removeChoices`, { locationKey, dayIndex });
|
return await api.post<RemoveChoicesRequest, void>(`${FOOD_API_PREFIX}/removeChoices`, { locationKey, dayIndex });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const removeChoice = async (locationKey: LocationKey, foodIndex: number, dayIndex?: number) => {
|
export const removeChoice = async (locationKey: keyof typeof Locations, foodIndex: number, dayIndex?: number) => {
|
||||||
return await api.post<RemoveChoiceRequest, void>(`${FOOD_API_PREFIX}/removeChoice`, { locationKey, foodIndex, dayIndex });
|
return await api.post<RemoveChoiceRequest, void>(`${FOOD_API_PREFIX}/removeChoice`, { locationKey, foodIndex, dayIndex });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AddPizzaRequest, FinishDeliveryRequest, PizzaOrder, RemovePizzaRequest, UpdatePizzaDayNoteRequest, UpdatePizzaFeeRequest } from "../../../types";
|
import { AddPizzaRequest, FinishDeliveryRequest, PizzaOrder, RemovePizzaRequest, UpdatePizzaDayNoteRequest, UpdatePizzaFeeRequest } from "../types";
|
||||||
import { api } from "./Api";
|
import { api } from "./Api";
|
||||||
|
|
||||||
const PIZZADAY_API_PREFIX = '/api/pizzaDay';
|
const PIZZADAY_API_PREFIX = '/api/pizzaDay';
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { WeeklyStats } from "../../../types";
|
|
||||||
import { api } from "./Api";
|
|
||||||
|
|
||||||
const STATS_API_PREFIX = '/api/stats';
|
|
||||||
|
|
||||||
export const getStats = async (startDate: string, endDate: string) => {
|
|
||||||
return await api.get<WeeklyStats>(`${STATS_API_PREFIX}?startDate=${startDate}&endDate=${endDate}`);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FeatureRequest, UpdateFeatureVoteRequest } from "../../../types";
|
import { FeatureRequest, UpdateFeatureVoteRequest } from "../types";
|
||||||
import { api } from "./Api";
|
import { api } from "./Api";
|
||||||
|
|
||||||
const VOTING_API_PREFIX = '/api/voting';
|
const VOTING_API_PREFIX = '/api/voting';
|
||||||
|
|||||||
@@ -4,18 +4,15 @@ import { useAuth } from "../context/auth";
|
|||||||
import SettingsModal from "./modals/SettingsModal";
|
import SettingsModal from "./modals/SettingsModal";
|
||||||
import { useSettings } from "../context/settings";
|
import { useSettings } from "../context/settings";
|
||||||
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";
|
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
import { STATS_URL } from "../AppRoutes";
|
|
||||||
|
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const navigate = useNavigate();
|
|
||||||
const [settingsModalOpen, setSettingsModalOpen] = useState<boolean>(false);
|
const [settingsModalOpen, setSettingsModalOpen] = useState<boolean>(false);
|
||||||
const [votingModalOpen, setVotingModalOpen] = useState<boolean>(false);
|
const [votingModalOpen, setVotingModalOpen] = useState<boolean>(false);
|
||||||
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
|
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
|
||||||
@@ -111,7 +108,7 @@ export default function Header() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <Navbar variant='dark' expand="lg">
|
return <Navbar variant='dark' expand="lg">
|
||||||
<Navbar.Brand href="/">Luncher</Navbar.Brand>
|
<Navbar.Brand>Luncher</Navbar.Brand>
|
||||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||||
<Navbar.Collapse id="basic-navbar-nav">
|
<Navbar.Collapse id="basic-navbar-nav">
|
||||||
<Nav className="nav">
|
<Nav className="nav">
|
||||||
@@ -119,7 +116,6 @@ export default function Header() {
|
|||||||
<NavDropdown.Item onClick={() => setSettingsModalOpen(true)}>Nastavení</NavDropdown.Item>
|
<NavDropdown.Item onClick={() => setSettingsModalOpen(true)}>Nastavení</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.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
|
||||||
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
|
||||||
<NavDropdown.Divider />
|
<NavDropdown.Divider />
|
||||||
<NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item>
|
<NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item>
|
||||||
</NavDropdown>
|
</NavDropdown>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { getEasterEgg } from "../api/EasterEggApi";
|
import { getEasterEgg } from "../api/EasterEggApi";
|
||||||
import { AuthContextProps } from "./auth";
|
import { AuthContextProps } from "./auth";
|
||||||
import { EasterEgg } from "../../../types";
|
import { EasterEgg } from "../types";
|
||||||
|
|
||||||
export const useEasterEgg = (auth?: AuthContextProps | null): [EasterEgg | undefined, boolean] => {
|
export const useEasterEgg = (auth?: AuthContextProps | null): [EasterEgg | undefined, boolean] => {
|
||||||
const [result, setResult] = useState<EasterEgg | undefined>();
|
const [result, setResult] = useState<EasterEgg | undefined>();
|
||||||
|
|||||||
@@ -1,20 +1,33 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import { SocketContext, socket } from './context/socket';
|
||||||
import { ProvideAuth } from './context/auth';
|
import { ProvideAuth } from './context/auth';
|
||||||
|
import { ToastContainer } from 'react-toastify';
|
||||||
|
import { ProvideSettings } from './context/settings';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import AppRoutes from './AppRoutes';
|
import Snowfall from 'react-snowfall';
|
||||||
import { BrowserRouter } from 'react-router';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
);
|
);
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
|
||||||
<ProvideAuth>
|
<ProvideAuth>
|
||||||
<AppRoutes />
|
<ProvideSettings>
|
||||||
|
<SocketContext.Provider value={socket}>
|
||||||
|
<>
|
||||||
|
<Snowfall style={{
|
||||||
|
zIndex: 2,
|
||||||
|
position: 'fixed',
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh'}} />
|
||||||
|
<App />
|
||||||
|
</>
|
||||||
|
<ToastContainer />
|
||||||
|
</SocketContext.Provider>
|
||||||
|
</ProvideSettings>
|
||||||
</ProvideAuth>
|
</ProvideAuth>
|
||||||
</BrowserRouter>
|
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
.stats-page {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
.week-navigator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: xx-large;
|
|
||||||
|
|
||||||
.date-range {
|
|
||||||
margin: 5px 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import Footer from "../components/Footer";
|
|
||||||
import Header from "../components/Header";
|
|
||||||
import { useAuth } from "../context/auth";
|
|
||||||
import Login from "../Login";
|
|
||||||
import { formatDate, getFirstWorkDayOfWeek, getHumanDate, getLastWorkDayOfWeek } from "../Utils";
|
|
||||||
import { getStats } from "../api/StatsApi";
|
|
||||||
import { WeeklyStats, LocationKey, Locations } from "../../../types";
|
|
||||||
import Loader from "../components/Loader";
|
|
||||||
import { faChevronLeft, faChevronRight, faGear } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { Legend, Line, LineChart, Tooltip, XAxis, YAxis } from "recharts";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import './StatsPage.scss';
|
|
||||||
|
|
||||||
const CHART_WIDTH = 1400;
|
|
||||||
const CHART_HEIGHT = 700;
|
|
||||||
const STROKE_WIDTH = 2.5;
|
|
||||||
|
|
||||||
const COLORS = [
|
|
||||||
// Komentáře jsou kvůli vizualizaci barev ve VS Code
|
|
||||||
'#ff1493', // #ff1493
|
|
||||||
'#1e90ff', // #1e90ff
|
|
||||||
'#c5a700', // #c5a700
|
|
||||||
'#006400', // #006400
|
|
||||||
'#b300ff', // #b300ff
|
|
||||||
'#ff4500', // #ff4500
|
|
||||||
'#bc8f8f', // #bc8f8f
|
|
||||||
'#00ff00', // #00ff00
|
|
||||||
'#7c7c7c', // #7c7c7c
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function StatsPage() {
|
|
||||||
const auth = useAuth();
|
|
||||||
const [dateRange, setDateRange] = useState<Date[]>();
|
|
||||||
const [data, setData] = useState<WeeklyStats>();
|
|
||||||
|
|
||||||
// Prvotní nastavení aktuálního týdne
|
|
||||||
useEffect(() => {
|
|
||||||
const today = new Date();
|
|
||||||
setDateRange([getFirstWorkDayOfWeek(today), getLastWorkDayOfWeek(today)]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Přenačtení pro zvolený týden
|
|
||||||
useEffect(() => {
|
|
||||||
if (dateRange) {
|
|
||||||
getStats(formatDate(dateRange[0]), formatDate(dateRange[1])).then(setData);
|
|
||||||
}
|
|
||||||
}, [dateRange]);
|
|
||||||
|
|
||||||
const renderLine = (location: Locations) => {
|
|
||||||
const index = Object.values(Locations).indexOf(location);
|
|
||||||
const key = Object.keys(Locations)[index];
|
|
||||||
return <Line key={location} name={location} type="monotone" dataKey={data => data.locations[key] ?? 0} stroke={COLORS[index]} strokeWidth={STROKE_WIDTH} />
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePreviousWeek = () => {
|
|
||||||
if (dateRange) {
|
|
||||||
const previousStartDate = new Date(dateRange[0]);
|
|
||||||
previousStartDate.setDate(previousStartDate.getDate() - 7);
|
|
||||||
const previousEndDate = new Date(previousStartDate);
|
|
||||||
previousEndDate.setDate(previousEndDate.getDate() + 4);
|
|
||||||
setDateRange([previousStartDate, previousEndDate]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNextWeek = () => {
|
|
||||||
if (dateRange) {
|
|
||||||
const nextStartDate = new Date(dateRange[0]);
|
|
||||||
nextStartDate.setDate(nextStartDate.getDate() + 7);
|
|
||||||
const nextEndDate = new Date(nextStartDate);
|
|
||||||
nextEndDate.setDate(nextEndDate.getDate() + 4);
|
|
||||||
setDateRange([nextStartDate, nextEndDate]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: any) => {
|
|
||||||
if (e.keyCode === 37) {
|
|
||||||
handlePreviousWeek();
|
|
||||||
} else if (e.keyCode === 39) {
|
|
||||||
handleNextWeek()
|
|
||||||
}
|
|
||||||
}, [dateRange]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}
|
|
||||||
}, [handleKeyDown]);
|
|
||||||
|
|
||||||
if (!auth?.login) {
|
|
||||||
return <Login />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dateRange) {
|
|
||||||
return <Loader
|
|
||||||
icon={faGear}
|
|
||||||
description={'Načítám data...'}
|
|
||||||
animation={'fa-bounce'}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Header />
|
|
||||||
<div className="stats-page">
|
|
||||||
<h1>Statistiky</h1>
|
|
||||||
<div className="week-navigator">
|
|
||||||
<FontAwesomeIcon title="Předchozí týden" icon={faChevronLeft} style={{ cursor: "pointer" }} onClick={handlePreviousWeek} />
|
|
||||||
<h2 className="date-range">{getHumanDate(dateRange[0])} - {getHumanDate(dateRange[1])}</h2>
|
|
||||||
<FontAwesomeIcon title="Následující týden" icon={faChevronRight} style={{ cursor: "pointer" }} onClick={handleNextWeek} />
|
|
||||||
</div>
|
|
||||||
<LineChart width={CHART_WIDTH} height={CHART_HEIGHT} data={data}>
|
|
||||||
{Object.values(Locations).map(location => renderLine(location))}
|
|
||||||
<XAxis dataKey="date" />
|
|
||||||
<YAxis />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
</LineChart>
|
|
||||||
</div>
|
|
||||||
<Footer />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
243
client/yarn.lock
243
client/yarn.lock
@@ -725,62 +725,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.20.7"
|
"@babel/types" "^7.20.7"
|
||||||
|
|
||||||
"@types/cookie@^0.6.0":
|
|
||||||
version "0.6.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5"
|
|
||||||
integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
|
|
||||||
|
|
||||||
"@types/d3-array@^3.0.3":
|
|
||||||
version "3.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5"
|
|
||||||
integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==
|
|
||||||
|
|
||||||
"@types/d3-color@*":
|
|
||||||
version "3.1.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
|
|
||||||
integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
|
|
||||||
|
|
||||||
"@types/d3-ease@^3.0.0":
|
|
||||||
version "3.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
|
|
||||||
integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
|
|
||||||
|
|
||||||
"@types/d3-interpolate@^3.0.1":
|
|
||||||
version "3.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
|
|
||||||
integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
|
|
||||||
dependencies:
|
|
||||||
"@types/d3-color" "*"
|
|
||||||
|
|
||||||
"@types/d3-path@*":
|
|
||||||
version "3.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a"
|
|
||||||
integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==
|
|
||||||
|
|
||||||
"@types/d3-scale@^4.0.2":
|
|
||||||
version "4.0.9"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb"
|
|
||||||
integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==
|
|
||||||
dependencies:
|
|
||||||
"@types/d3-time" "*"
|
|
||||||
|
|
||||||
"@types/d3-shape@^3.1.0":
|
|
||||||
version "3.1.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555"
|
|
||||||
integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==
|
|
||||||
dependencies:
|
|
||||||
"@types/d3-path" "*"
|
|
||||||
|
|
||||||
"@types/d3-time@*", "@types/d3-time@^3.0.0":
|
|
||||||
version "3.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f"
|
|
||||||
integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==
|
|
||||||
|
|
||||||
"@types/d3-timer@^3.0.0":
|
|
||||||
version "3.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
|
|
||||||
integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
|
|
||||||
|
|
||||||
"@types/estree@1.0.6":
|
"@types/estree@1.0.6":
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
||||||
@@ -941,7 +885,7 @@ classnames@^2.3.2:
|
|||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
|
||||||
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
||||||
|
|
||||||
clsx@^2.0.0, clsx@^2.1.0:
|
clsx@^2.1.0:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||||
@@ -963,87 +907,11 @@ convert-source-map@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
||||||
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
|
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
|
||||||
|
|
||||||
cookie@^1.0.1:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610"
|
|
||||||
integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==
|
|
||||||
|
|
||||||
csstype@^3.0.2:
|
csstype@^3.0.2:
|
||||||
version "3.1.3"
|
version "3.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||||
|
|
||||||
"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6:
|
|
||||||
version "3.2.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
|
|
||||||
integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
|
|
||||||
dependencies:
|
|
||||||
internmap "1 - 2"
|
|
||||||
|
|
||||||
"d3-color@1 - 3":
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
|
|
||||||
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
|
|
||||||
|
|
||||||
d3-ease@^3.0.1:
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
|
|
||||||
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
|
|
||||||
|
|
||||||
"d3-format@1 - 3":
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
|
|
||||||
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
|
|
||||||
|
|
||||||
"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
|
|
||||||
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
|
|
||||||
dependencies:
|
|
||||||
d3-color "1 - 3"
|
|
||||||
|
|
||||||
d3-path@^3.1.0:
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
|
|
||||||
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
|
|
||||||
|
|
||||||
d3-scale@^4.0.2:
|
|
||||||
version "4.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
|
|
||||||
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
|
|
||||||
dependencies:
|
|
||||||
d3-array "2.10.0 - 3"
|
|
||||||
d3-format "1 - 3"
|
|
||||||
d3-interpolate "1.2.0 - 3"
|
|
||||||
d3-time "2.1.1 - 3"
|
|
||||||
d3-time-format "2 - 4"
|
|
||||||
|
|
||||||
d3-shape@^3.1.0:
|
|
||||||
version "3.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
|
|
||||||
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
|
|
||||||
dependencies:
|
|
||||||
d3-path "^3.1.0"
|
|
||||||
|
|
||||||
"d3-time-format@2 - 4":
|
|
||||||
version "4.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
|
|
||||||
integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
|
|
||||||
dependencies:
|
|
||||||
d3-time "1 - 3"
|
|
||||||
|
|
||||||
"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0:
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
|
|
||||||
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
|
|
||||||
dependencies:
|
|
||||||
d3-array "2 - 3"
|
|
||||||
|
|
||||||
d3-timer@^3.0.1:
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
|
|
||||||
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
|
|
||||||
|
|
||||||
debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
|
debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
||||||
@@ -1058,11 +926,6 @@ debug@~4.3.1, debug@~4.3.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.3"
|
ms "^2.1.3"
|
||||||
|
|
||||||
decimal.js-light@^2.4.1:
|
|
||||||
version "2.5.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
|
|
||||||
integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
|
|
||||||
|
|
||||||
dequal@^2.0.3:
|
dequal@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||||
@@ -1147,11 +1010,6 @@ escape-string-regexp@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
|
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
|
||||||
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
|
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
|
||||||
|
|
||||||
eventemitter3@^4.0.1:
|
|
||||||
version "4.0.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
|
||||||
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
|
||||||
|
|
||||||
exenv@^1.2.0:
|
exenv@^1.2.0:
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
|
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
|
||||||
@@ -1168,11 +1026,6 @@ expect@^29.0.0:
|
|||||||
jest-message-util "^29.7.0"
|
jest-message-util "^29.7.0"
|
||||||
jest-util "^29.7.0"
|
jest-util "^29.7.0"
|
||||||
|
|
||||||
fast-equals@^5.0.1:
|
|
||||||
version "5.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.2.2.tgz#885d7bfb079fac0ce0e8450374bce29e9b742484"
|
|
||||||
integrity sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==
|
|
||||||
|
|
||||||
fill-range@^7.1.1:
|
fill-range@^7.1.1:
|
||||||
version "7.1.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||||
@@ -1215,11 +1068,6 @@ immutable@^5.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1"
|
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1"
|
||||||
integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==
|
integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==
|
||||||
|
|
||||||
"internmap@1 - 2":
|
|
||||||
version "2.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
|
|
||||||
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
|
|
||||||
|
|
||||||
invariant@^2.2.4:
|
invariant@^2.2.4:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||||
@@ -1311,11 +1159,6 @@ json5@^2.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
||||||
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
||||||
|
|
||||||
lodash@^4.17.21:
|
|
||||||
version "4.17.21"
|
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
|
||||||
|
|
||||||
loose-envify@^1.0.0, loose-envify@^1.4.0:
|
loose-envify@^1.0.0, loose-envify@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
@@ -1448,7 +1291,7 @@ react-is@^16.13.1, react-is@^16.3.2:
|
|||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
react-is@^18.0.0, react-is@^18.3.1:
|
react-is@^18.0.0:
|
||||||
version "18.3.1"
|
version "18.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
||||||
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
||||||
@@ -1480,37 +1323,11 @@ react-refresh@^0.14.2:
|
|||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9"
|
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9"
|
||||||
integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
|
integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
|
||||||
|
|
||||||
react-router-dom@^7.2.0:
|
|
||||||
version "7.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.2.0.tgz#b8a7eae7827cd5207cf91e89807d01217737797d"
|
|
||||||
integrity sha512-cU7lTxETGtQRQbafJubvZKHEn5izNABxZhBY0Jlzdv0gqQhCPQt2J8aN5ZPjS6mQOXn5NnirWNh+FpE8TTYN0Q==
|
|
||||||
dependencies:
|
|
||||||
react-router "7.2.0"
|
|
||||||
|
|
||||||
react-router@7.2.0, react-router@^7.2.0:
|
|
||||||
version "7.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.2.0.tgz#a8f2729d39f634a7a870d14dd906a1b406f39d6f"
|
|
||||||
integrity sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/cookie" "^0.6.0"
|
|
||||||
cookie "^1.0.1"
|
|
||||||
set-cookie-parser "^2.6.0"
|
|
||||||
turbo-stream "2.4.0"
|
|
||||||
|
|
||||||
react-select-search@^4.1.6:
|
react-select-search@^4.1.6:
|
||||||
version "4.1.8"
|
version "4.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/react-select-search/-/react-select-search-4.1.8.tgz#435bdd7893685d45332813ad65b000e0dafcfbac"
|
resolved "https://registry.yarnpkg.com/react-select-search/-/react-select-search-4.1.8.tgz#435bdd7893685d45332813ad65b000e0dafcfbac"
|
||||||
integrity sha512-mzHhYzpaAJ3iYDjayydfb+grvvP59VWlLUWLqP26Trvm4xj2rzLnksopWZdkwWORB3dhAneqmXos3RWOMjFOxQ==
|
integrity sha512-mzHhYzpaAJ3iYDjayydfb+grvvP59VWlLUWLqP26Trvm4xj2rzLnksopWZdkwWORB3dhAneqmXos3RWOMjFOxQ==
|
||||||
|
|
||||||
react-smooth@^4.0.4:
|
|
||||||
version "4.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.4.tgz#a5875f8bb61963ca61b819cedc569dc2453894b4"
|
|
||||||
integrity sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==
|
|
||||||
dependencies:
|
|
||||||
fast-equals "^5.0.1"
|
|
||||||
prop-types "^15.8.1"
|
|
||||||
react-transition-group "^4.4.5"
|
|
||||||
|
|
||||||
react-snowfall@^2.2.0:
|
react-snowfall@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-snowfall/-/react-snowfall-2.2.0.tgz#0856a72c4d29d27a6a14301c0f5deb51296fb4df"
|
resolved "https://registry.yarnpkg.com/react-snowfall/-/react-snowfall-2.2.0.tgz#0856a72c4d29d27a6a14301c0f5deb51296fb4df"
|
||||||
@@ -1545,27 +1362,6 @@ readdirp@^4.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a"
|
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a"
|
||||||
integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==
|
integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==
|
||||||
|
|
||||||
recharts-scale@^0.4.4:
|
|
||||||
version "0.4.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9"
|
|
||||||
integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==
|
|
||||||
dependencies:
|
|
||||||
decimal.js-light "^2.4.1"
|
|
||||||
|
|
||||||
recharts@^2.15.1:
|
|
||||||
version "2.15.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.1.tgz#0941adf0402528d54f6d81997eb15840c893aa3c"
|
|
||||||
integrity sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==
|
|
||||||
dependencies:
|
|
||||||
clsx "^2.0.0"
|
|
||||||
eventemitter3 "^4.0.1"
|
|
||||||
lodash "^4.17.21"
|
|
||||||
react-is "^18.3.1"
|
|
||||||
react-smooth "^4.0.4"
|
|
||||||
recharts-scale "^0.4.4"
|
|
||||||
tiny-invariant "^1.3.1"
|
|
||||||
victory-vendor "^36.6.8"
|
|
||||||
|
|
||||||
regenerator-runtime@^0.14.0:
|
regenerator-runtime@^0.14.0:
|
||||||
version "0.14.1"
|
version "0.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
|
||||||
@@ -1620,11 +1416,6 @@ semver@^6.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||||
|
|
||||||
set-cookie-parser@^2.6.0:
|
|
||||||
version "2.7.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943"
|
|
||||||
integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==
|
|
||||||
|
|
||||||
slash@^3.0.0:
|
slash@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||||
@@ -1667,11 +1458,6 @@ supports-color@^7.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-flag "^4.0.0"
|
has-flag "^4.0.0"
|
||||||
|
|
||||||
tiny-invariant@^1.3.1:
|
|
||||||
version "1.3.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
|
|
||||||
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
|
|
||||||
|
|
||||||
to-regex-range@^5.0.1:
|
to-regex-range@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
||||||
@@ -1689,11 +1475,6 @@ tslib@^2.8.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||||
|
|
||||||
turbo-stream@2.4.0:
|
|
||||||
version "2.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/turbo-stream/-/turbo-stream-2.4.0.tgz#1e4fca6725e90fa14ac4adb782f2d3759a5695f0"
|
|
||||||
integrity sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==
|
|
||||||
|
|
||||||
typescript@^5.3.3:
|
typescript@^5.3.3:
|
||||||
version "5.7.2"
|
version "5.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
|
||||||
@@ -1732,26 +1513,6 @@ update-browserslist-db@^1.1.1:
|
|||||||
escalade "^3.2.0"
|
escalade "^3.2.0"
|
||||||
picocolors "^1.1.0"
|
picocolors "^1.1.0"
|
||||||
|
|
||||||
victory-vendor@^36.6.8:
|
|
||||||
version "36.9.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801"
|
|
||||||
integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/d3-array" "^3.0.3"
|
|
||||||
"@types/d3-ease" "^3.0.0"
|
|
||||||
"@types/d3-interpolate" "^3.0.1"
|
|
||||||
"@types/d3-scale" "^4.0.2"
|
|
||||||
"@types/d3-shape" "^3.1.0"
|
|
||||||
"@types/d3-time" "^3.0.0"
|
|
||||||
"@types/d3-timer" "^3.0.0"
|
|
||||||
d3-array "^3.1.6"
|
|
||||||
d3-ease "^3.0.1"
|
|
||||||
d3-interpolate "^3.0.1"
|
|
||||||
d3-scale "^4.0.2"
|
|
||||||
d3-shape "^3.1.0"
|
|
||||||
d3-time "^3.0.0"
|
|
||||||
d3-timer "^3.0.1"
|
|
||||||
|
|
||||||
vite-tsconfig-paths@^5.1.4:
|
vite-tsconfig-paths@^5.1.4:
|
||||||
version "5.1.4"
|
version "5.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz#d9a71106a7ff2c1c840c6f1708042f76a9212ed4"
|
resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz#d9a71106a7ff2c1c840c6f1708042f76a9212ed4"
|
||||||
|
|||||||
4
server/.gitignore
vendored
4
server/.gitignore
vendored
@@ -1,5 +1,7 @@
|
|||||||
|
/node_modules
|
||||||
/dist
|
/dist
|
||||||
/resources/easterEggs
|
data.json
|
||||||
.env.production
|
.env.production
|
||||||
.env.development
|
.env.development
|
||||||
.easter-eggs.json
|
.easter-eggs.json
|
||||||
|
/resources/easterEggs
|
||||||
@@ -12,7 +12,6 @@ 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";
|
import easterEggRoutes from "./routes/easterEggRoutes";
|
||||||
import statsRoutes from "./routes/statsRoutes";
|
|
||||||
|
|
||||||
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}`) });
|
||||||
@@ -101,11 +100,9 @@ app.use("/api/", (req, res, next) => {
|
|||||||
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' });
|
||||||
}
|
}
|
||||||
@@ -133,10 +130,7 @@ 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("/api/easterEggs", easterEggRoutes);
|
||||||
app.use("/api/stats", statsRoutes);
|
app.use(express.static('public'))
|
||||||
|
|
||||||
app.use('/stats', express.static('public'));
|
|
||||||
app.use(express.static('public'));
|
|
||||||
|
|
||||||
// Middleware pro zpracování chyb
|
// Middleware pro zpracování chyb
|
||||||
app.use((err: any, req: any, res: any, next: any) => {
|
app.use((err: any, req: any, res: any, next: any) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { WeeklyStats, Locations } from "../../types";
|
import { getDayOfWeekIndex } from "./utils";
|
||||||
|
|
||||||
// Mockovací data pro podporované podniky, na jeden týden
|
// Mockovací data pro podporované podniky, na jeden týden
|
||||||
const MOCK_DATA = {
|
const MOCK_DATA = {
|
||||||
@@ -427,181 +427,7 @@ const MOCK_DATA = {
|
|||||||
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 +1180,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,90 +1196,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStatsMock = (): WeeklyStats => {
|
|
||||||
// TODO stačilo by iterovat ten enum, jako to už děláme jinde
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
date: '24.02.',
|
|
||||||
locations: {
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '25.02.',
|
|
||||||
locations: {
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '26.02.',
|
|
||||||
locations: {
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '27.02.',
|
|
||||||
locations: {
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '28.02.',
|
|
||||||
locations: {
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10),
|
|
||||||
[Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,58 +1,58 @@
|
|||||||
/** Notifikace */
|
/** Notifikace pro gotify*/
|
||||||
import { ClientData, NotififaceInput, NotifikaceData } from '../../types';
|
import { ClientData, 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 { getToday } from "./service";
|
||||||
import { formatDate, getUsersByLocation, getHumanTime } from "./utils";
|
import { formatDate, getUsersByLocation } from "./utils";
|
||||||
import getStorage from "./storage";
|
import getStorage from "./storage";
|
||||||
|
|
||||||
const storage = getStorage();
|
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[]> => {
|
export const gotifyCall = async (data: NotififaceInput, gotifyServers?: GotifyServer[]): Promise<any[]> => {
|
||||||
// if (!Array.isArray(gotifyServers)) {
|
if (!Array.isArray(gotifyServers)) {
|
||||||
// return []
|
return []
|
||||||
// }
|
}
|
||||||
// const urls = gotifyServers.flatMap(gotifyServer =>
|
const urls = gotifyServers.flatMap(gotifyServer =>
|
||||||
// gotifyServer.api_keys.map(apiKey => `${gotifyServer.server}/message?token=${apiKey}`));
|
gotifyServer.api_keys.map(apiKey => `${gotifyServer.server}/message?token=${apiKey}`));
|
||||||
//
|
|
||||||
// const dataPayload = {
|
const dataPayload = {
|
||||||
// title: "Luncher",
|
title: "Luncher",
|
||||||
// message: `${data.udalost} - spustil:${data.user}`,
|
message: `${data.udalost} - spustil:${data.user}`,
|
||||||
// priority: 7,
|
priority: 7,
|
||||||
// };
|
};
|
||||||
//
|
|
||||||
// const headers = { "Content-Type": "application/json" };
|
const headers = { "Content-Type": "application/json" };
|
||||||
//
|
|
||||||
// const promises = urls.map(url =>
|
const promises = urls.map(url =>
|
||||||
// axios.post(url, dataPayload, { headers }).then(response => {
|
axios.post(url, dataPayload, { headers }).then(response => {
|
||||||
// response.data = {
|
response.data = {
|
||||||
// success: true,
|
success: true,
|
||||||
// message: "Notifikace doručena",
|
message: "Notifikace doručena",
|
||||||
// };
|
};
|
||||||
// return response;
|
return response;
|
||||||
// }).catch(error => {
|
}).catch(error => {
|
||||||
// if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
// const axiosError = error as AxiosError;
|
const axiosError = error as AxiosError;
|
||||||
// if (axiosError.response) {
|
if (axiosError.response) {
|
||||||
// axiosError.response.data = {
|
axiosError.response.data = {
|
||||||
// success: false,
|
success: false,
|
||||||
// message: "fail",
|
message: "fail",
|
||||||
// };
|
};
|
||||||
// console.log(error)
|
console.log(error)
|
||||||
// return axiosError.response;
|
return axiosError.response;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// // Handle unknown error without a response
|
// Handle unknown error without a response
|
||||||
// console.log(error, "unknown error");
|
console.log(error, "unknown error");
|
||||||
// })
|
})
|
||||||
// );
|
);
|
||||||
// return promises;
|
return promises;
|
||||||
// };
|
};
|
||||||
|
|
||||||
export const ntfyCall = async (data: NotififaceInput) => {
|
export const ntfyCall = async (data: NotififaceInput) => {
|
||||||
const url = process.env.NTFY_HOST
|
const url = process.env.NTFY_HOST
|
||||||
@@ -97,64 +97,27 @@ export const ntfyCall = async (data: NotififaceInput) => {
|
|||||||
return promises;
|
return promises;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const teamsCall = async (data: NotififaceInput) => {
|
|
||||||
const url = process.env.TEAMS_WEBHOOK_URL;
|
|
||||||
const title = data.udalost;
|
|
||||||
let time = new Date();
|
|
||||||
time.setTime(time.getTime() + 1000 * 60);
|
|
||||||
const message = 'Odcházíme v ' + getHumanTime(time) + ', ' + data.user;
|
|
||||||
const card = {
|
|
||||||
'@type': 'MessageCard',
|
|
||||||
'@context': 'http://schema.org/extensions',
|
|
||||||
'themeColor': "0072C6", // light blue
|
|
||||||
summary: 'Summary description',
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
activityTitle: title,
|
|
||||||
text: message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
console.log("TEAMS_WEBHOOK_URL není definován v env")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post(url, card, {
|
|
||||||
headers: {
|
|
||||||
'content-type': 'application/vnd.microsoft.teams.card.o365connector'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return `${response.status} - ${response.statusText}`;
|
|
||||||
} catch (err) {
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 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, teams = true, gotify = false, ntfy = true }: NotifikaceData) => {
|
export const callNotifikace = async ({ input, teams = true, gotify = false, ntfy = true }: NotifikaceData) => {
|
||||||
const notifications = [];
|
const notifications = [];
|
||||||
|
|
||||||
if (ntfy) {
|
if (ntfy) {
|
||||||
const ntfyPromises = await ntfyCall(input);
|
const ntfyPromises = await ntfyCall(input);
|
||||||
if (ntfyPromises) {
|
if (ntfyPromises) {
|
||||||
notifications.push(...ntfyPromises);
|
notifications.push(...ntfyPromises);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Zatím není
|
||||||
if (teams) {
|
if (teams) {
|
||||||
const teamsPromises = await teamsCall(input);
|
notifications.push(teamsCall(input));
|
||||||
if (teamsPromises) {
|
}*/
|
||||||
notifications.push(teamsPromises);
|
|
||||||
|
// Add more notifications as necessary
|
||||||
|
|
||||||
|
//gotify bych řekl, že už je deprecated
|
||||||
|
if (gotify) {
|
||||||
|
const gotifyPromises = await gotifyCall(input, gotifyData);
|
||||||
|
notifications.push(...gotifyPromises);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// gotify bych řekl, že už je deprecated
|
|
||||||
// if (gotify) {
|
|
||||||
// const gotifyPromises = await gotifyCall(input, gotifyData);
|
|
||||||
// notifications.push(...gotifyPromises);
|
|
||||||
// }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await Promise.all(notifications);
|
const results = await Promise.all(notifications);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
// 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 = [
|
||||||
@@ -14,9 +13,9 @@ const SOUP_NAMES = [
|
|||||||
'fazolová',
|
'fazolová',
|
||||||
'cuketový krém',
|
'cuketový krém',
|
||||||
'boršč',
|
'boršč',
|
||||||
'slepičí s ',
|
'slepičí s',
|
||||||
'zeleninová s ',
|
'zeleninová s',
|
||||||
'hovězí s ',
|
'hovězí s',
|
||||||
'kachní kaldoun',
|
'kachní kaldoun',
|
||||||
'dršťková'
|
'dršťková'
|
||||||
];
|
];
|
||||||
@@ -26,8 +25,6 @@ const DAYS_IN_WEEK = ['pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', '
|
|||||||
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.
|
||||||
@@ -330,105 +327,3 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
|
|||||||
}
|
}
|
||||||
return result;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ router.post("/changeDepartureTime", async (req: Request<{}, any, ChangeDeparture
|
|||||||
router.post("/jdemeObed", async (req, res, next) => {
|
router.post("/jdemeObed", async (req, res, next) => {
|
||||||
const login = getLogin(parseToken(req));
|
const login = getLogin(parseToken(req));
|
||||||
try {
|
try {
|
||||||
await callNotifikace({ input: { user: login, udalost: UdalostEnum.JDEME_NA_OBED }, gotify: false })
|
await callNotifikace({ input: { user: login, udalost: UdalostEnum.JDEME_OBED }, gotify: false })
|
||||||
res.status(200).json({});
|
res.status(200).json({});
|
||||||
} catch (e: any) { next(e) }
|
} catch (e: any) { next(e) }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import express, { Request, Response } from "express";
|
|
||||||
import { getLogin } from "../auth";
|
|
||||||
import { parseToken } from "../utils";
|
|
||||||
import { WeeklyStats } from "../../../types";
|
|
||||||
import { getStats } from "../stats";
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get("/", async (req: Request<{}, any, undefined>, res: Response<WeeklyStats>) => {
|
|
||||||
getLogin(parseToken(req));
|
|
||||||
if (typeof req.query.startDate === 'string' && typeof req.query.endDate === 'string') {
|
|
||||||
try {
|
|
||||||
const data = await getStats(req.query.startDate, req.query.endDate);
|
|
||||||
return res.status(200).json(data);
|
|
||||||
} catch (e) {
|
|
||||||
// necháme to zatím spadnout na 400
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.sendStatus(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
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, DayMenu, 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";
|
||||||
|
|
||||||
const storage = getStorage();
|
const storage = getStorage();
|
||||||
@@ -10,7 +10,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();
|
||||||
}
|
}
|
||||||
@@ -61,8 +61,6 @@ export async function getData(date?: Date): Promise<ClientData> {
|
|||||||
[Restaurants.SLADOVNICKA]: await getRestaurantMenu(Restaurants.SLADOVNICKA, date),
|
[Restaurants.SLADOVNICKA]: await getRestaurantMenu(Restaurants.SLADOVNICKA, date),
|
||||||
// [Restaurants.UMOTLIKU]: await getRestaurantMenu(Restaurants.UMOTLIKU, date),
|
// [Restaurants.UMOTLIKU]: await getRestaurantMenu(Restaurants.UMOTLIKU, date),
|
||||||
[Restaurants.TECHTOWER]: await getRestaurantMenu(Restaurants.TECHTOWER, date),
|
[Restaurants.TECHTOWER]: await getRestaurantMenu(Restaurants.TECHTOWER, date),
|
||||||
[Restaurants.ZASTAVKAUMICHALA]: await getRestaurantMenu(Restaurants.ZASTAVKAUMICHALA, date),
|
|
||||||
[Restaurants.SENKSERIKOVA]: await getRestaurantMenu(Restaurants.SENKSERIKOVA, date),
|
|
||||||
}
|
}
|
||||||
clientData = await addVolatileData(clientData);
|
clientData = await addVolatileData(clientData);
|
||||||
return clientData;
|
return clientData;
|
||||||
@@ -170,32 +168,6 @@ export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): P
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Selhalo načtení jídel pro podnik TechTower", e);
|
console.error("Selhalo načtení jídel pro podnik TechTower", e);
|
||||||
}
|
}
|
||||||
case Restaurants.ZASTAVKAUMICHALA:
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
await storage.setData(getMenuKey(usedDate), menus);
|
||||||
}
|
}
|
||||||
@@ -224,7 +196,7 @@ export async function initIfNeeded(date?: Date) {
|
|||||||
* @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, locationKey: keyof typeof 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);
|
||||||
@@ -251,7 +223,7 @@ export async function removeChoices(login: string, trusted: boolean, locationKey
|
|||||||
* @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, locationKey: keyof typeof 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);
|
||||||
@@ -268,19 +240,14 @@ 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;
|
const locationKey = key as keyof typeof Locations;
|
||||||
if (ignoredLocationKey != null && ignoredLocationKey == locationKey) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (data.choices[locationKey] && login in data.choices[locationKey]) {
|
if (data.choices[locationKey] && login in data.choices[locationKey]) {
|
||||||
delete data.choices[locationKey][login];
|
delete data.choices[locationKey][login];
|
||||||
if (Object.keys(data.choices[locationKey]).length === 0) {
|
if (Object.keys(data.choices[locationKey]).length === 0) {
|
||||||
@@ -325,67 +292,43 @@ function validateTrusted(data: ClientData, login: string, trusted: boolean) {
|
|||||||
* @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, locationKey: keyof typeof Locations, foodIndex?: number, date?: Date) {
|
||||||
const usedDate = date ?? getToday();
|
const usedDate = date ?? getToday();
|
||||||
await initIfNeeded(usedDate);
|
await initIfNeeded(usedDate);
|
||||||
const selectedDate = formatDate(usedDate);
|
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);
|
||||||
await validateFoodIndex(locationKey, foodIndex, date);
|
|
||||||
// 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 (!data.choices) {
|
||||||
if (!(data.choices[locationKey])) {
|
console.log("Klíč", Locations[locationKey]); // TODO smazat
|
||||||
data.choices[locationKey] = {}
|
data.choices = {
|
||||||
|
[Locations[locationKey]]: {}
|
||||||
}
|
}
|
||||||
if (!(login in data.choices[locationKey])) {
|
|
||||||
if (!data.choices[locationKey]) {
|
|
||||||
data.choices[locationKey] = {}
|
|
||||||
}
|
|
||||||
data.choices[locationKey][login] = {
|
|
||||||
trusted,
|
|
||||||
options: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (foodIndex != null && !data.choices[locationKey][login].options.includes(foodIndex)) {
|
|
||||||
data.choices[locationKey][login].options.push(foodIndex);
|
|
||||||
}
|
}
|
||||||
|
console.log("Máme choices", data.choices);
|
||||||
|
console.log("Hodnota locationKey", locationKey);
|
||||||
|
// if (!(data.choices[locationKey])) {
|
||||||
|
// data?.choices[locationKey] = {}
|
||||||
|
// }
|
||||||
|
// if (!(login in data.choices[locationKey])) {
|
||||||
|
// if (!data.choices[locationKey]) {
|
||||||
|
// data.choices[locationKey] = {}
|
||||||
|
// }
|
||||||
|
// data.choices[locationKey][login] = {
|
||||||
|
// trusted,
|
||||||
|
// options: []
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// if (foodIndex != null && !data.choices[locationKey][login].options.includes(foodIndex)) {
|
||||||
|
// data.choices[locationKey][login].options.push(foodIndex);
|
||||||
|
// }
|
||||||
await storage.setData(selectedDate, data);
|
await storage.setData(selectedDate, data);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Zvaliduje platnost indexu jídla pro vybranou lokalitu a datum.
|
|
||||||
*
|
|
||||||
* @param locationKey vybraná lokalita
|
|
||||||
* @param foodIndex index jídla pro danou lokalitu
|
|
||||||
* @param date datum, pro které je validace prováděna
|
|
||||||
*/
|
|
||||||
async function validateFoodIndex(locationKey: LocationKey, foodIndex?: number, date?: Date) {
|
|
||||||
if (foodIndex != null) {
|
|
||||||
if (typeof foodIndex !== 'number') {
|
|
||||||
throw Error(`Neplatný index ${foodIndex} typu ${typeof foodIndex}`);
|
|
||||||
}
|
|
||||||
if (foodIndex < 0) {
|
|
||||||
throw Error(`Neplatný index ${foodIndex}`);
|
|
||||||
}
|
|
||||||
if (!(locationKey in Restaurants)) {
|
|
||||||
throw Error(`Neplatný index ${foodIndex} pro lokalitu ${locationKey} nepodporující indexy`);
|
|
||||||
}
|
|
||||||
const usedDate = date ?? getToday();
|
|
||||||
const restaurantKey = Restaurants[locationKey as keyof typeof Restaurants]
|
|
||||||
const menu = await getRestaurantMenu(restaurantKey, usedDate);
|
|
||||||
if (foodIndex > (menu.food.length - 1)) {
|
|
||||||
throw new Error(`Neplatný index ${foodIndex} pro lokalitu ${locationKey}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aktualizuje poznámku k aktuálně vybrané možnosti.
|
* Aktualizuje poznámku k aktuálně vybrané možnosti.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import { WeeklyStats, DayData, Locations, DailyStats, LocationKey } from "../../types";
|
|
||||||
import { getStatsMock } from "./mock";
|
|
||||||
import getStorage from "./storage";
|
|
||||||
import { formatDate } from "./utils";
|
|
||||||
|
|
||||||
const storage = getStorage();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vypočte a vrátí statistiky jednotlivých možností pro předaný rozsah dat.
|
|
||||||
*
|
|
||||||
* @param startDate počáteční datum
|
|
||||||
* @param endDate koncové datum
|
|
||||||
* @returns statistiky pro zadaný rozsah dat
|
|
||||||
*/
|
|
||||||
export async function getStats(startDate: string, endDate: string): Promise<WeeklyStats> {
|
|
||||||
if (process.env.MOCK_DATA === 'true') {
|
|
||||||
return getStatsMock();
|
|
||||||
}
|
|
||||||
const start = new Date(startDate);
|
|
||||||
const end = new Date(endDate);
|
|
||||||
|
|
||||||
// Dočasná validace, aby to někdo ručně neshodil
|
|
||||||
const daysDiff = ((end as any) - (start as any)) / (1000 * 60 * 60 * 24);
|
|
||||||
if (daysDiff > 4) {
|
|
||||||
throw Error('Neplatný rozsah');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = [];
|
|
||||||
for (const date = start; date <= end; date.setDate(date.getDate() + 1)) {
|
|
||||||
const locationsStats: DailyStats = {
|
|
||||||
// TODO vytáhnout do utils funkce
|
|
||||||
date: `${String(date.getDate()).padStart(2, "0")}.${String(date.getMonth() + 1).padStart(2, "0")}.`,
|
|
||||||
locations: {}
|
|
||||||
}
|
|
||||||
const data: DayData = await storage.getData(formatDate(date));
|
|
||||||
if (data?.choices) {
|
|
||||||
Object.keys(data.choices).forEach(locationKey => {
|
|
||||||
locationsStats.locations[locationKey as LocationKey] = Object.keys(data.choices[locationKey as LocationKey]!).length;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
result.push(locationsStats);
|
|
||||||
}
|
|
||||||
return result as WeeklyStats;
|
|
||||||
}
|
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import { Choices, LocationKey } from "../../types";
|
import { Choices, Locations } from "../../types";
|
||||||
|
|
||||||
const DAY_OF_WEEK_FORMAT = new Intl.DateTimeFormat(undefined, {weekday: 'long'});
|
|
||||||
|
|
||||||
/** 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í. */
|
||||||
@@ -17,7 +13,7 @@ export function getHumanDate(date: Date) {
|
|||||||
let currentDay = String(date.getDate()).padStart(2, '0');
|
let currentDay = String(date.getDate()).padStart(2, '0');
|
||||||
let currentMonth = String(date.getMonth() + 1).padStart(2, "0");
|
let currentMonth = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
let currentYear = date.getFullYear();
|
let currentYear = date.getFullYear();
|
||||||
let currentDayOfWeek = DAY_OF_WEEK_FORMAT.format(date);
|
let currentDayOfWeek = date.toLocaleDateString("CZ-cs", { weekday: 'long' });
|
||||||
return `${currentDay}.${currentMonth}.${currentYear} (${currentDayOfWeek})`;
|
return `${currentDay}.${currentMonth}.${currentYear} (${currentDayOfWeek})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,16 +114,18 @@ export const getUsersByLocation = (choices: Choices, login: string): string[] =>
|
|||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
|
|
||||||
for (const location of Object.entries(choices)) {
|
for (const location of Object.entries(choices)) {
|
||||||
const locationKey = location[0] as LocationKey;
|
const locationKey = location[0];
|
||||||
const locationValue = location[1];
|
const locationValue = location[1];
|
||||||
if (locationValue[login]) {
|
console.log("locationKey", locationKey);
|
||||||
for (const username in choices[locationKey]) {
|
console.log("locationValue", locationValue);
|
||||||
if (choices[locationKey].hasOwnProperty(username)) {
|
// if (locationValues[login]) {
|
||||||
result.push(username);
|
// for (const username in choices[locationKey]) {
|
||||||
}
|
// if (choices[locationKey].hasOwnProperty(username)) {
|
||||||
}
|
// result.push(username);
|
||||||
break;
|
// }
|
||||||
}
|
// }
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FeatureRequest, LocationKey, PizzaOrder } from "./Types";
|
import { FeatureRequest, Locations, PizzaOrder } from "./Types";
|
||||||
|
|
||||||
export type ILocationKey = {
|
export type ILocationKey = {
|
||||||
locationKey: LocationKey,
|
locationKey: keyof typeof Locations,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IDayIndex = {
|
export type IDayIndex = {
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ export enum Restaurants {
|
|||||||
SLADOVNICKA = 'sladovnicka',
|
SLADOVNICKA = 'sladovnicka',
|
||||||
// UMOTLIKU = 'uMotliku',
|
// UMOTLIKU = 'uMotliku',
|
||||||
TECHTOWER = 'techTower',
|
TECHTOWER = 'techTower',
|
||||||
ZASTAVKAUMICHALA = 'zastavkaUmichala',
|
|
||||||
SENKSERIKOVA = 'senkSerikova',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FoodChoices = {
|
export type FoodChoices = {
|
||||||
@@ -14,9 +12,8 @@ export type FoodChoices = {
|
|||||||
note?: string,
|
note?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO okomentovat / rozdělit
|
|
||||||
export type Choices = {
|
export type Choices = {
|
||||||
[location in LocationKey]?: {
|
[location in keyof typeof Locations]?: {
|
||||||
[login: string]: FoodChoices
|
[login: string]: FoodChoices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,13 +108,10 @@ 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',
|
||||||
@@ -125,13 +119,10 @@ export enum Locations {
|
|||||||
ROZHODUJI = 'Rozhoduji se',
|
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_NA_OBED = "Jdeme na oběd",
|
JDEME_OBED = "Jdeme oběd",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NotififaceInput = {
|
export type NotififaceInput = {
|
||||||
@@ -221,12 +212,3 @@ export type AnimationPosition = {
|
|||||||
"--end-bottom"?: string,
|
"--end-bottom"?: string,
|
||||||
rotate?: string,
|
rotate?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Statistiky pro jeden konkrétní den. */
|
|
||||||
export type DailyStats = {
|
|
||||||
date: string, // zkrácené datum v human-readable formátu (např. 24.02.)
|
|
||||||
locations: { [location in LocationKey]?: number }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Statistiky pro jeden konkrétní týden (pondělí-pátek) */
|
|
||||||
export type WeeklyStats = [DailyStats, DailyStats, DailyStats, DailyStats, DailyStats];
|
|
||||||
Reference in New Issue
Block a user