Compare commits
26 Commits
feat/DayOf
...
d144c55bf7
| Author | SHA1 | Date | |
|---|---|---|---|
| d144c55bf7 | |||
| 999a517404 | |||
| 68bafa808c | |||
| a34614c8db | |||
| f4e31cea36 | |||
| 8dda6b1014 | |||
| f9c7d647f7 | |||
| ca400638d1 | |||
| 0af78e72d9 | |||
|
|
8137ca6fc0 | ||
|
|
3817126ac0 | ||
|
|
c1856b2eee | ||
|
|
eaf0bc353d | ||
| ff650ec3b8 | |||
| f8aa293413 | |||
| cafcd0a467 | |||
| 9e247eb2a1 | |||
| 469a6b9031 | |||
|
|
89dec1c194 | ||
|
|
f3af64923c | ||
|
|
44b09a9d1a | ||
|
|
c311cc2fd7 | ||
|
|
a9fe369abc | ||
|
|
ea9fe980f0 | ||
|
|
d367826ce0 | ||
|
|
fdf1ae938f |
22
.gitignore
vendored
22
.gitignore
vendored
@@ -1,23 +1 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
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*
|
||||
|
||||
56
.woodpecker/workflow.yaml
Normal file
56
.woodpecker/workflow.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
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,5 +1,7 @@
|
||||
ARG NODE_VERSION="node:22-alpine"
|
||||
|
||||
# Builder
|
||||
FROM node:18-alpine3.18 AS builder
|
||||
FROM ${NODE_VERSION} AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@@ -45,16 +47,18 @@ WORKDIR /build/client
|
||||
RUN yarn build
|
||||
|
||||
# Runner
|
||||
FROM node:18-alpine3.18
|
||||
ENV LANG cs_CZ.UTF-8
|
||||
ENV NODE_ENV production
|
||||
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 --from=builder /build/server/node_modules ./server/node_modules
|
||||
COPY --from=builder /build/server/dist ./
|
||||
COPY server/resources ./server/resources
|
||||
|
||||
# Vykopírování sestaveného klienta
|
||||
COPY --from=builder /build/client/dist ./public
|
||||
@@ -63,8 +67,10 @@ COPY --from=builder /build/client/dist ./public
|
||||
COPY /server/.env.production ./server/src
|
||||
|
||||
# Zkopírování konfigurace easter eggů
|
||||
# TODO tohle spadne když nebude existovat!
|
||||
COPY /server/.easter-eggs.json ./server/
|
||||
RUN if [ -f /server/.easter-eggs.json ]; then cp /server/.easter-eggs.json ./server/; fi
|
||||
|
||||
# Export /data/db.json do složky /data
|
||||
VOLUME ["/data"]
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
||||
26
Dockerfile-Woodpecker
Normal file
26
Dockerfile-Woodpecker
Normal file
@@ -0,0 +1,26 @@
|
||||
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
|
||||
### Závislosti
|
||||
#### Klient/server
|
||||
- [Node.js 18.x](https://nodejs.dev)
|
||||
- [Node.js 22.x](https://nodejs.dev)
|
||||
- [Yarn 1.22.x (Classic)](https://classic.yarnpkg.com)
|
||||
|
||||
### Spuštění na *nix platformách
|
||||
|
||||
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@@ -1,3 +1,2 @@
|
||||
build
|
||||
dist
|
||||
src/types
|
||||
@@ -21,9 +21,12 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-jwt": "^1.2.0",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-router": "^7.2.0",
|
||||
"react-router-dom": "^7.2.0",
|
||||
"react-select-search": "^4.1.6",
|
||||
"react-snowfall": "^2.2.0",
|
||||
"react-toastify": "^10.0.4",
|
||||
"recharts": "^2.15.1",
|
||||
"sass": "^1.80.6",
|
||||
"socket.io-client": "^4.6.1",
|
||||
"typescript": "^5.3.3",
|
||||
@@ -31,9 +34,8 @@
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"copy-types": "cp -r ../types ./src",
|
||||
"start": "yarn copy-types && vite",
|
||||
"build": "yarn copy-types && vite build"
|
||||
"start": "yarn vite",
|
||||
"build": "yarn vite build"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
||||
@@ -8,13 +8,12 @@ import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap';
|
||||
import Header from './components/Header';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import PizzaOrderList from './components/PizzaOrderList';
|
||||
import SelectSearch, { SelectedOptionValue } from 'react-select-search';
|
||||
import SelectSearch, {SelectedOptionValue, SelectSearchOption} from 'react-select-search';
|
||||
import 'react-select-search/style.css';
|
||||
import './App.scss';
|
||||
import { SelectSearchOption } from 'react-select-search';
|
||||
import { faCircleCheck, faNoteSticky, faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
||||
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, LocationKey } from '../../types';
|
||||
import Footer from './components/Footer';
|
||||
import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
import Loader from './components/Loader';
|
||||
@@ -24,6 +23,8 @@ import { getHumanDateTime, isInTheFuture } from './Utils';
|
||||
import NoteModal from './components/modals/NoteModal';
|
||||
import { useEasterEgg } from './context/eggs';
|
||||
import { getImage } from './api/EasterEggApi';
|
||||
import { Link } from 'react-router';
|
||||
import { STATS_URL } from './AppRoutes';
|
||||
|
||||
const EVENT_CONNECT = "connect"
|
||||
|
||||
@@ -190,6 +191,15 @@ function App() {
|
||||
}
|
||||
}, [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 locationKey = event.target.value as LocationKey;
|
||||
if (auth?.login) {
|
||||
@@ -332,15 +342,17 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const renderFoodTable = (name: string, menu: DayMenu) => {
|
||||
const renderFoodTable = (location: Locations, menu: DayMenu) => {
|
||||
let content;
|
||||
if (menu?.closed) {
|
||||
content = <h3>Zavřeno</h3>
|
||||
} else if (menu?.food?.length > 0) {
|
||||
const hideSoups = settings?.hideSoups;
|
||||
content = <Table striped bordered hover>
|
||||
<tbody>
|
||||
{menu.food.filter(f => (settings?.hideSoups ? !f.isSoup : true)).map((f: any, index: number) =>
|
||||
<tr key={index}>
|
||||
<tbody style={{ cursor: 'pointer' }}>
|
||||
{menu.food.map((f: Food, index: number) =>
|
||||
(!hideSoups || !f.isSoup) &&
|
||||
<tr key={f.name} onClick={() => doAddClickFoodChoice(location, index)}>
|
||||
<td>{f.amount}</td>
|
||||
<td>{f.name}</td>
|
||||
<td>{f.price}</td>
|
||||
@@ -351,8 +363,8 @@ function App() {
|
||||
} else {
|
||||
content = <h3>Chyba načtení dat</h3>
|
||||
}
|
||||
return <Col md={12} lg={4} className='mt-3'>
|
||||
<h3>{name}</h3>
|
||||
return <Col md={12} lg={3} className='mt-3'>
|
||||
<h3 style={{ cursor: 'pointer' }} onClick={() => doAddClickFoodChoice(location, undefined)}>{location}</h3>
|
||||
{menu?.lastUpdate && <small>Poslední aktualizace: {getHumanDateTime(new Date(menu.lastUpdate))}</small>}
|
||||
{content}
|
||||
</Col>
|
||||
@@ -402,8 +414,8 @@ function App() {
|
||||
<img src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} />
|
||||
Poslední změny:
|
||||
<ul>
|
||||
<li>Zimní atmosféra</li>
|
||||
<li>Odstranění podniku U Motlíků</li>
|
||||
<li>Možnost výběru restaurace a jídel kliknutím v tabulce</li>
|
||||
<li><Link to={STATS_URL}>Statistiky</Link></li>
|
||||
</ul>
|
||||
</Alert>
|
||||
{dayIndex != null &&
|
||||
@@ -414,10 +426,11 @@ function App() {
|
||||
</div>
|
||||
}
|
||||
<Row className='food-tables'>
|
||||
{food[Restaurants.SLADOVNICKA] && renderFoodTable('Sladovnická', food[Restaurants.SLADOVNICKA])}
|
||||
{/* {food[Restaurants.UMOTLIKU] && renderFoodTable('U Motlíků', food[Restaurants.UMOTLIKU])} */}
|
||||
{food[Restaurants.TECHTOWER] && renderFoodTable('TechTower', food[Restaurants.TECHTOWER])}
|
||||
{food[Restaurants.ZASTAVKAUMICHALA] && renderFoodTable('Zastávka u Michala', food[Restaurants.ZASTAVKAUMICHALA])}
|
||||
{food[Restaurants.SLADOVNICKA] && renderFoodTable(Locations.SLADOVNICKA, food[Restaurants.SLADOVNICKA])}
|
||||
{/* {food[Restaurants.UMOTLIKU] && renderFoodTable(food[Restaurants.UMOTLIKU])} */}
|
||||
{food[Restaurants.TECHTOWER] && renderFoodTable(Locations.TECHTOWER, food[Restaurants.TECHTOWER])}
|
||||
{food[Restaurants.ZASTAVKAUMICHALA] && renderFoodTable(Locations.ZASTAVKAUMICHALA, food[Restaurants.ZASTAVKAUMICHALA])}
|
||||
{food[Restaurants.SENKSERIKOVA] && renderFoodTable(Locations.SENKSERIKOVA, food[Restaurants.SENKSERIKOVA])}
|
||||
</Row>
|
||||
<div className='content-wrapper'>
|
||||
<div className='content'>
|
||||
@@ -463,9 +476,13 @@ function App() {
|
||||
return;
|
||||
}
|
||||
const locationLoginList = Object.entries(loginObject);
|
||||
const locationPickCount = locationLoginList.length
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td>{locationName}</td>
|
||||
{(locationPickCount?? 0) > 1 ? (
|
||||
<td>{locationName} ({locationPickCount})</td>
|
||||
) : (
|
||||
<td>{locationName}</td>)}
|
||||
<td className='p-0'>
|
||||
<Table>
|
||||
<tbody>
|
||||
|
||||
33
client/src/AppRoutes.tsx
Normal file
33
client/src/AppRoutes.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -53,7 +53,54 @@ 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";
|
||||
|
||||
const EASTER_EGGS_API_PREFIX = '/api/easterEggs';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AddChoiceRequest, ChangeDepartureTimeRequest, LocationKey, RemoveChoiceRequest, RemoveChoicesRequest, UpdateNoteRequest } from "../types";
|
||||
import { AddChoiceRequest, ChangeDepartureTimeRequest, LocationKey, RemoveChoiceRequest, RemoveChoicesRequest, UpdateNoteRequest } from "../../../types";
|
||||
import { api } from "./Api";
|
||||
|
||||
const FOOD_API_PREFIX = '/api/food';
|
||||
|
||||
@@ -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";
|
||||
|
||||
const PIZZADAY_API_PREFIX = '/api/pizzaDay';
|
||||
|
||||
8
client/src/api/StatsApi.ts
Normal file
8
client/src/api/StatsApi.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
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";
|
||||
|
||||
const VOTING_API_PREFIX = '/api/voting';
|
||||
|
||||
@@ -4,15 +4,18 @@ import { useAuth } from "../context/auth";
|
||||
import SettingsModal from "./modals/SettingsModal";
|
||||
import { useSettings } from "../context/settings";
|
||||
import FeaturesVotingModal from "./modals/FeaturesVotingModal";
|
||||
import { FeatureRequest } from "../types";
|
||||
import { FeatureRequest } from "../../../types";
|
||||
import { errorHandler } from "../api/Api";
|
||||
import { getFeatureVotes, updateFeatureVote } from "../api/VotingApi";
|
||||
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
|
||||
import { useNavigate } from "react-router";
|
||||
import { STATS_URL } from "../AppRoutes";
|
||||
|
||||
|
||||
export default function Header() {
|
||||
const auth = useAuth();
|
||||
const settings = useSettings();
|
||||
const navigate = useNavigate();
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState<boolean>(false);
|
||||
const [votingModalOpen, setVotingModalOpen] = useState<boolean>(false);
|
||||
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
|
||||
@@ -108,7 +111,7 @@ export default function Header() {
|
||||
}
|
||||
|
||||
return <Navbar variant='dark' expand="lg">
|
||||
<Navbar.Brand>Luncher</Navbar.Brand>
|
||||
<Navbar.Brand href="/">Luncher</Navbar.Brand>
|
||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||
<Navbar.Collapse id="basic-navbar-nav">
|
||||
<Nav className="nav">
|
||||
@@ -116,6 +119,7 @@ export default function Header() {
|
||||
<NavDropdown.Item onClick={() => setSettingsModalOpen(true)}>Nastavení</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={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
||||
<NavDropdown.Divider />
|
||||
<NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item>
|
||||
</NavDropdown>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Table } from "react-bootstrap";
|
||||
import { Order, PizzaDayState, PizzaOrder } from "../types";
|
||||
import { Order, PizzaDayState, PizzaOrder } from "../../../types";
|
||||
import PizzaOrderRow from "./PizzaOrderRow";
|
||||
import { updatePizzaFee } from "../api/PizzaDayApi";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faMoneyBill1, faTrashCan } from "@fortawesome/free-regular-svg-icons";
|
||||
import { useAuth } from "../context/auth";
|
||||
import { Order, PizzaDayState, PizzaOrder } from "../types";
|
||||
import { Order, PizzaDayState, PizzaOrder } from "../../../types";
|
||||
import PizzaAdditionalFeeModal from "./modals/PizzaAdditionalFeeModal";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Modal, Button, Form } from "react-bootstrap"
|
||||
import { FeatureRequest } from "../../types";
|
||||
import { FeatureRequest } from "../../../../types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { getEasterEgg } from "../api/EasterEggApi";
|
||||
import { AuthContextProps } from "./auth";
|
||||
import { EasterEgg } from "../types";
|
||||
import { EasterEgg } from "../../../types";
|
||||
|
||||
export const useEasterEgg = (auth?: AuthContextProps | null): [EasterEgg | undefined, boolean] => {
|
||||
const [result, setResult] = useState<EasterEgg | undefined>();
|
||||
|
||||
@@ -1,33 +1,20 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import { SocketContext, socket } from './context/socket';
|
||||
import { ProvideAuth } from './context/auth';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { ProvideSettings } from './context/settings';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import './index.css';
|
||||
import Snowfall from 'react-snowfall';
|
||||
import AppRoutes from './AppRoutes';
|
||||
import { BrowserRouter } from 'react-router';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<ProvideAuth>
|
||||
<ProvideSettings>
|
||||
<SocketContext.Provider value={socket}>
|
||||
<>
|
||||
<Snowfall style={{
|
||||
zIndex: 2,
|
||||
position: 'fixed',
|
||||
width: '100vw',
|
||||
height: '100vh'}} />
|
||||
<App />
|
||||
</>
|
||||
<ToastContainer />
|
||||
</SocketContext.Provider>
|
||||
</ProvideSettings>
|
||||
<AppRoutes />
|
||||
</ProvideAuth>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
16
client/src/pages/StatsPage.scss
Normal file
16
client/src/pages/StatsPage.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
124
client/src/pages/StatsPage.tsx
Normal file
124
client/src/pages/StatsPage.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
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,6 +725,62 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
||||
@@ -885,7 +941,7 @@ classnames@^2.3.2:
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
|
||||
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
||||
|
||||
clsx@^2.1.0:
|
||||
clsx@^2.0.0, clsx@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
@@ -907,11 +963,87 @@ convert-source-map@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
||||
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:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||
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:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
||||
@@ -926,6 +1058,11 @@ debug@~4.3.1, debug@~4.3.2:
|
||||
dependencies:
|
||||
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:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||
@@ -1010,6 +1147,11 @@ escape-string-regexp@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
|
||||
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:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
|
||||
@@ -1026,6 +1168,11 @@ expect@^29.0.0:
|
||||
jest-message-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:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||
@@ -1068,6 +1215,11 @@ immutable@^5.0.2:
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1"
|
||||
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:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
@@ -1159,6 +1311,11 @@ json5@^2.2.3:
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
||||
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:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
@@ -1291,7 +1448,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"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-is@^18.0.0:
|
||||
react-is@^18.0.0, react-is@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
||||
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
||||
@@ -1323,11 +1480,37 @@ react-refresh@^0.14.2:
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9"
|
||||
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:
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/react-select-search/-/react-select-search-4.1.8.tgz#435bdd7893685d45332813ad65b000e0dafcfbac"
|
||||
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:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-snowfall/-/react-snowfall-2.2.0.tgz#0856a72c4d29d27a6a14301c0f5deb51296fb4df"
|
||||
@@ -1362,6 +1545,27 @@ readdirp@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a"
|
||||
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:
|
||||
version "0.14.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
|
||||
@@ -1416,6 +1620,11 @@ semver@^6.3.1:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
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:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||
@@ -1458,6 +1667,11 @@ supports-color@^7.1.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
||||
@@ -1475,6 +1689,11 @@ tslib@^2.8.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
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:
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
|
||||
@@ -1513,6 +1732,26 @@ update-browserslist-db@^1.1.1:
|
||||
escalade "^3.2.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:
|
||||
version "5.1.4"
|
||||
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,7 +1,5 @@
|
||||
/node_modules
|
||||
/dist
|
||||
data.json
|
||||
/resources/easterEggs
|
||||
.env.production
|
||||
.env.development
|
||||
.easter-eggs.json
|
||||
/resources/easterEggs
|
||||
@@ -12,6 +12,7 @@ import pizzaDayRoutes from "./routes/pizzaDayRoutes";
|
||||
import foodRoutes from "./routes/foodRoutes";
|
||||
import votingRoutes from "./routes/votingRoutes";
|
||||
import easterEggRoutes from "./routes/easterEggRoutes";
|
||||
import statsRoutes from "./routes/statsRoutes";
|
||||
|
||||
const ENVIRONMENT = process.env.NODE_ENV || 'production';
|
||||
dotenv.config({ path: path.resolve(__dirname, `./.env.${ENVIRONMENT}`) });
|
||||
@@ -100,9 +101,11 @@ app.use("/api/", (req, res, next) => {
|
||||
const emailHeader = req.header('remote-email');
|
||||
if (userHeader !== undefined && nameHeader !== undefined) {
|
||||
const remoteName = Buffer.from(nameHeader, 'latin1').toString();
|
||||
if (ENVIRONMENT !== "production"){
|
||||
console.log("Tvuj username, name a email: %s, %s, %s.", userHeader, remoteName, emailHeader);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!req.headers.authorization) {
|
||||
return res.status(401).json({ error: 'Nebyl předán autentizační token' });
|
||||
}
|
||||
@@ -130,7 +133,10 @@ app.use("/api/pizzaDay", pizzaDayRoutes);
|
||||
app.use("/api/food", foodRoutes);
|
||||
app.use("/api/voting", votingRoutes);
|
||||
app.use("/api/easterEggs", easterEggRoutes);
|
||||
app.use(express.static('public'))
|
||||
app.use("/api/stats", statsRoutes);
|
||||
|
||||
app.use('/stats', express.static('public'));
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Middleware pro zpracování chyb
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { WeeklyStats, Locations } from "../../types";
|
||||
|
||||
// Mockovací data pro podporované podniky, na jeden týden
|
||||
const MOCK_DATA = {
|
||||
'sladovnicka': {
|
||||
'MONDAY': [
|
||||
'sladovnicka': [
|
||||
[
|
||||
{
|
||||
amount: "0,25l",
|
||||
name: "Kulajda",
|
||||
@@ -27,7 +29,7 @@ const MOCK_DATA = {
|
||||
isSoup: false,
|
||||
}
|
||||
],
|
||||
'TUESDAY': [
|
||||
[
|
||||
{
|
||||
amount: "0,25l",
|
||||
name: "Hovězí vývar s kapáním",
|
||||
@@ -53,7 +55,7 @@ const MOCK_DATA = {
|
||||
isSoup: false,
|
||||
}
|
||||
],
|
||||
'WEDNESDAY': [
|
||||
[
|
||||
{
|
||||
amount: "0,25l",
|
||||
name: "Zelná polévka s klobásou",
|
||||
@@ -79,7 +81,7 @@ const MOCK_DATA = {
|
||||
isSoup: false,
|
||||
}
|
||||
],
|
||||
'THURSDAY': [
|
||||
[
|
||||
{
|
||||
amount: "0,25l",
|
||||
name: "Kuřecí vývar s nudlemi",
|
||||
@@ -105,7 +107,7 @@ const MOCK_DATA = {
|
||||
isSoup: false,
|
||||
}
|
||||
],
|
||||
'FRIDAY': [
|
||||
[
|
||||
{
|
||||
amount: "0,25l",
|
||||
name: "Dršťková polévka",
|
||||
@@ -131,7 +133,7 @@ const MOCK_DATA = {
|
||||
isSoup: false,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
'uMotliku': [
|
||||
[
|
||||
{
|
||||
@@ -515,7 +517,91 @@ const MOCK_DATA = {
|
||||
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
|
||||
@@ -1272,7 +1358,7 @@ const MOCK_PIZZA_LIST = [
|
||||
* Funkce vrací mock datu ve formátu YYYY-MM-DD
|
||||
*/
|
||||
export const getTodayMock = (): Date => {
|
||||
return new Date('2025-01-08'); // pátek
|
||||
return new Date('2025-01-10'); // pátek
|
||||
}
|
||||
|
||||
export const getMenuSladovnickaMock = () => {
|
||||
@@ -1291,6 +1377,86 @@ export const getMenuZastavkaUmichalaMock = () => {
|
||||
return MOCK_DATA['zastavkaUmichala'];
|
||||
}
|
||||
|
||||
export const getMenuSenkSerikovaMock = () => {
|
||||
return MOCK_DATA['senkSerikova'];
|
||||
}
|
||||
|
||||
export const getPizzaListMock = () => {
|
||||
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 pro gotify*/
|
||||
import { ClientData, GotifyServer, NotififaceInput, NotifikaceData } from '../../types';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
/** Notifikace */
|
||||
import { ClientData, NotififaceInput, NotifikaceData } from '../../types';
|
||||
import axios from 'axios';
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { getToday } from "./service";
|
||||
import { formatDate, getUsersByLocation } from "./utils";
|
||||
import { formatDate, getUsersByLocation, getHumanTime } from "./utils";
|
||||
import getStorage from "./storage";
|
||||
|
||||
const storage = getStorage();
|
||||
const ENVIRONMENT = process.env.NODE_ENV || 'production'
|
||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
||||
|
||||
const gotifyDataRaw = process.env.GOTIFY_SERVERS_AND_KEYS || "{}";
|
||||
const gotifyData: GotifyServer[] = JSON.parse(gotifyDataRaw);
|
||||
export const gotifyCall = async (data: NotififaceInput, gotifyServers?: GotifyServer[]): Promise<any[]> => {
|
||||
if (!Array.isArray(gotifyServers)) {
|
||||
return []
|
||||
}
|
||||
const urls = gotifyServers.flatMap(gotifyServer =>
|
||||
gotifyServer.api_keys.map(apiKey => `${gotifyServer.server}/message?token=${apiKey}`));
|
||||
|
||||
const dataPayload = {
|
||||
title: "Luncher",
|
||||
message: `${data.udalost} - spustil:${data.user}`,
|
||||
priority: 7,
|
||||
};
|
||||
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
|
||||
const promises = urls.map(url =>
|
||||
axios.post(url, dataPayload, { headers }).then(response => {
|
||||
response.data = {
|
||||
success: true,
|
||||
message: "Notifikace doručena",
|
||||
};
|
||||
return response;
|
||||
}).catch(error => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const axiosError = error as AxiosError;
|
||||
if (axiosError.response) {
|
||||
axiosError.response.data = {
|
||||
success: false,
|
||||
message: "fail",
|
||||
};
|
||||
console.log(error)
|
||||
return axiosError.response;
|
||||
}
|
||||
}
|
||||
// Handle unknown error without a response
|
||||
console.log(error, "unknown error");
|
||||
})
|
||||
);
|
||||
return promises;
|
||||
};
|
||||
// const gotifyDataRaw = process.env.GOTIFY_SERVERS_AND_KEYS || "{}";
|
||||
// const gotifyData: GotifyServer[] = JSON.parse(gotifyDataRaw);
|
||||
// export const gotifyCall = async (data: NotififaceInput, gotifyServers?: GotifyServer[]): Promise<any[]> => {
|
||||
// if (!Array.isArray(gotifyServers)) {
|
||||
// return []
|
||||
// }
|
||||
// const urls = gotifyServers.flatMap(gotifyServer =>
|
||||
// gotifyServer.api_keys.map(apiKey => `${gotifyServer.server}/message?token=${apiKey}`));
|
||||
//
|
||||
// const dataPayload = {
|
||||
// title: "Luncher",
|
||||
// message: `${data.udalost} - spustil:${data.user}`,
|
||||
// priority: 7,
|
||||
// };
|
||||
//
|
||||
// const headers = { "Content-Type": "application/json" };
|
||||
//
|
||||
// const promises = urls.map(url =>
|
||||
// axios.post(url, dataPayload, { headers }).then(response => {
|
||||
// response.data = {
|
||||
// success: true,
|
||||
// message: "Notifikace doručena",
|
||||
// };
|
||||
// return response;
|
||||
// }).catch(error => {
|
||||
// if (axios.isAxiosError(error)) {
|
||||
// const axiosError = error as AxiosError;
|
||||
// if (axiosError.response) {
|
||||
// axiosError.response.data = {
|
||||
// success: false,
|
||||
// message: "fail",
|
||||
// };
|
||||
// console.log(error)
|
||||
// return axiosError.response;
|
||||
// }
|
||||
// }
|
||||
// // Handle unknown error without a response
|
||||
// console.log(error, "unknown error");
|
||||
// })
|
||||
// );
|
||||
// return promises;
|
||||
// };
|
||||
|
||||
export const ntfyCall = async (data: NotififaceInput) => {
|
||||
const url = process.env.NTFY_HOST
|
||||
@@ -97,27 +97,64 @@ export const ntfyCall = async (data: NotififaceInput) => {
|
||||
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*/
|
||||
export const callNotifikace = async ({ input, teams = true, gotify = false, ntfy = true }: NotifikaceData) => {
|
||||
const notifications = [];
|
||||
|
||||
if (ntfy) {
|
||||
const ntfyPromises = await ntfyCall(input);
|
||||
if (ntfyPromises) {
|
||||
notifications.push(...ntfyPromises);
|
||||
}
|
||||
}
|
||||
/* Zatím není
|
||||
if (teams) {
|
||||
notifications.push(teamsCall(input));
|
||||
}*/
|
||||
|
||||
// Add more notifications as necessary
|
||||
|
||||
//gotify bych řekl, že už je deprecated
|
||||
if (gotify) {
|
||||
const gotifyPromises = await gotifyCall(input, gotifyData);
|
||||
notifications.push(...gotifyPromises);
|
||||
const teamsPromises = await teamsCall(input);
|
||||
if (teamsPromises) {
|
||||
notifications.push(teamsPromises);
|
||||
}
|
||||
}
|
||||
// gotify bych řekl, že už je deprecated
|
||||
// if (gotify) {
|
||||
// const gotifyPromises = await gotifyCall(input, gotifyData);
|
||||
// notifications.push(...gotifyPromises);
|
||||
// }
|
||||
|
||||
try {
|
||||
const results = await Promise.all(notifications);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import axios from "axios";
|
||||
import { load } from 'cheerio';
|
||||
import { DayOfWeek, DayOfWeekEnum, DayOfWeekIndex, Food, RestaurantWeeklyMenu } from "../../types";
|
||||
import { getMenuSladovnickaMock, getMenuTechTowerMock, getMenuUMotlikuMock, getMenuZastavkaUmichalaMock } from "./mock";
|
||||
import { Food } from "../../types";
|
||||
import {getMenuSladovnickaMock, getMenuTechTowerMock, getMenuUMotlikuMock, getMenuZastavkaUmichalaMock, getMenuSenkSerikovaMock} from "./mock";
|
||||
import {formatDate} from "./utils";
|
||||
|
||||
// Fráze v názvech jídel, které naznačují že se jedná o polévku
|
||||
const SOUP_NAMES = [
|
||||
@@ -69,7 +70,7 @@ const getHtml = async (url: string): Promise<any> => {
|
||||
* @param mock zda vrátit mock data
|
||||
* @returns seznam jídel pro daný týden
|
||||
*/
|
||||
export const getMenuSladovnicka = async (firstDayOfWeek: Date, mock: boolean = false): Promise<RestaurantWeeklyMenu> => {
|
||||
export const getMenuSladovnicka = async (firstDayOfWeek: Date, mock: boolean = false): Promise<Food[][]> => {
|
||||
if (mock) {
|
||||
return getMenuSladovnickaMock();
|
||||
}
|
||||
@@ -78,8 +79,7 @@ export const getMenuSladovnicka = async (firstDayOfWeek: Date, mock: boolean = f
|
||||
const $ = load(html);
|
||||
|
||||
const list = $('ul.tab-links').children();
|
||||
const result: RestaurantWeeklyMenu = {};
|
||||
// TODO upravit až bude enum
|
||||
const result: Food[][] = [];
|
||||
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
|
||||
const currentDate = new Date(firstDayOfWeek);
|
||||
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex);
|
||||
@@ -97,8 +97,7 @@ export const getMenuSladovnicka = async (firstDayOfWeek: Date, mock: boolean = f
|
||||
})
|
||||
if (index === undefined) {
|
||||
// Pravděpodobně svátek, nebo je zavřeno
|
||||
const index: number = Object.keys(DayOfWeekEnum).indexOf('Casual'); // 1
|
||||
result[dayIndex as DayOfWeekEnum] = [{
|
||||
result[dayIndex] = [{
|
||||
amount: undefined,
|
||||
name: "Pro daný den nebyla nalezena denní nabídka",
|
||||
price: "",
|
||||
@@ -345,13 +344,16 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
|
||||
}
|
||||
|
||||
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 < now) {
|
||||
if (currentDate.getDate() !== nowDate) {
|
||||
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",
|
||||
@@ -360,13 +362,13 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
|
||||
}];
|
||||
continue;
|
||||
} else {
|
||||
// let dateString = formatDate(currentDate, 'DD.MM.YYYY');
|
||||
// const html = await getHtml(ZASTAVKAUMICHALA_URL + '/?do=dailyMenu-changeDate&dailyMenu-dateString=' + dateString);
|
||||
const html = await getHtml(ZASTAVKAUMICHALA_URL);
|
||||
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 row = $($('.foodsList li')[0]).text();
|
||||
|
||||
const currentDayFood: Food[] = [];
|
||||
$('.foodsList li').each((index, element) => {
|
||||
currentDayFood.push({
|
||||
@@ -381,3 +383,52 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
|
||||
}
|
||||
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) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
try {
|
||||
await callNotifikace({ input: { user: login, udalost: UdalostEnum.JDEME_OBED }, gotify: false })
|
||||
await callNotifikace({ input: { user: login, udalost: UdalostEnum.JDEME_NA_OBED }, gotify: false })
|
||||
res.status(200).json({});
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
22
server/src/routes/statsRoutes.ts
Normal file
22
server/src/routes/statsRoutes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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 { ClientData, Restaurants, RestaurantDailyMenu, DepartureTime, DayData, WeekMenu, LocationKey, DayOfWeekIndex, daysOfWeeksIndices, DayOfWeekEnum, DayOfWeek } from "../../types";
|
||||
import { ClientData, Restaurants, DayMenu, DepartureTime, DayData, WeekMenu, LocationKey } from "../../types";
|
||||
import getStorage from "./storage";
|
||||
import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku, getMenuZastavkaUmichala } from "./restaurants";
|
||||
import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku, getMenuZastavkaUmichala, getMenuSenkSerikova } from "./restaurants";
|
||||
import { getTodayMock } from "./mock";
|
||||
|
||||
const storage = getStorage();
|
||||
@@ -62,6 +62,7 @@ export async function getData(date?: Date): Promise<ClientData> {
|
||||
// [Restaurants.UMOTLIKU]: await getRestaurantMenu(Restaurants.UMOTLIKU, 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);
|
||||
return clientData;
|
||||
@@ -97,7 +98,7 @@ async function getMenu(date: Date): Promise<WeekMenu | undefined> {
|
||||
* @param date datum, ke kterému získat menu
|
||||
* @param mock příznak, zda chceme pouze mock data
|
||||
*/
|
||||
export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): Promise<RestaurantDailyMenu> {
|
||||
export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): Promise<DayMenu> {
|
||||
const usedDate = date ?? getToday();
|
||||
const dayOfWeekIndex = getDayOfWeekIndex(usedDate);
|
||||
const now = new Date().getTime();
|
||||
@@ -111,9 +112,9 @@ export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): P
|
||||
|
||||
let menus = await getMenu(usedDate);
|
||||
if (menus == null) {
|
||||
menus = {};
|
||||
menus = [];
|
||||
}
|
||||
daysOfWeeksIndices.forEach(i => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (menus[i] == null) {
|
||||
menus[i] = {};
|
||||
}
|
||||
@@ -124,9 +125,6 @@ export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): P
|
||||
food: [],
|
||||
};
|
||||
}
|
||||
})
|
||||
if (!menus[dayOfWeekIndex]) {
|
||||
menus[dayOfWeekIndex] = {};
|
||||
}
|
||||
if (!menus[dayOfWeekIndex][restaurant]?.food?.length) {
|
||||
const firstDay = getFirstWorkDayOfWeek(usedDate);
|
||||
@@ -134,15 +132,7 @@ export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): P
|
||||
switch (restaurant) {
|
||||
case Restaurants.SLADOVNICKA:
|
||||
try {
|
||||
// TODO tady jsme v háji, protože z následujících metod vracíme arbitrárně dlouhé pole (musíme vracet omezené na maximálně 0-7 prvků)
|
||||
const sladovnickaFood = await getMenuSladovnicka(firstDay, mock);
|
||||
for (const i in DayOfWeekEnum) {
|
||||
menus[i][restaurant]!.food = sladovnickaFood[i];
|
||||
// Velice chatrný a nespolehlivý způsob detekce uzavření...
|
||||
if (sladovnickaFood[i].length === 1 && sladovnickaFood[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
|
||||
menus[i][restaurant]!.closed = true;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < sladovnickaFood.length; i++) {
|
||||
menus[i][restaurant]!.food = sladovnickaFood[i];
|
||||
// Velice chatrný a nespolehlivý způsob detekce uzavření...
|
||||
@@ -193,6 +183,19 @@ export async function getRestaurantMenu(restaurant: Restaurants, date?: Date): P
|
||||
} 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);
|
||||
}
|
||||
@@ -265,14 +268,19 @@ export async function removeChoice(login: string, trusted: boolean, locationKey:
|
||||
}
|
||||
|
||||
/**
|
||||
* Odstraní kompletně volbu uživatele.
|
||||
* Odstraní kompletně volbu uživatele, vyjma ignoredLocationKey (pokud byla předána a existuje).
|
||||
*
|
||||
* @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) {
|
||||
async function removeChoiceIfPresent(login: string, date: string, ignoredLocationKey?: LocationKey) {
|
||||
let data: DayData = await storage.getData(date);
|
||||
for (const key of Object.keys(data.choices)) {
|
||||
const locationKey = key as LocationKey;
|
||||
if (ignoredLocationKey != null && ignoredLocationKey == locationKey) {
|
||||
continue;
|
||||
}
|
||||
if (data.choices[locationKey] && login in data.choices[locationKey]) {
|
||||
delete data.choices[locationKey][login];
|
||||
if (Object.keys(data.choices[locationKey]).length === 0) {
|
||||
@@ -323,9 +331,13 @@ export async function addChoice(login: string, trusted: boolean, locationKey: Lo
|
||||
const selectedDate = formatDate(usedDate);
|
||||
let data: DayData = await storage.getData(selectedDate);
|
||||
validateTrusted(data, login, trusted);
|
||||
await validateFoodIndex(locationKey, foodIndex, date);
|
||||
// Pokud měníme pouze lokaci, mažeme případné předchozí
|
||||
if (foodIndex == null) {
|
||||
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[locationKey])) {
|
||||
@@ -347,6 +359,33 @@ export async function addChoice(login: string, trusted: boolean, locationKey: Lo
|
||||
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.
|
||||
*
|
||||
|
||||
44
server/src/stats.ts
Normal file
44
server/src/stats.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
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,8 +1,17 @@
|
||||
import JSONdb from 'simple-json-db';
|
||||
import { StorageInterface } from "./StorageInterface";
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const db = new JSONdb('./data.json');
|
||||
const dbPath = path.resolve(__dirname, '../../data/db.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.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Choices, DayOfWeekIndex, LocationKey } from "../../types";
|
||||
import { Choices, LocationKey } from "../../types";
|
||||
|
||||
const DAY_OF_WEEK_FORMAT = new Intl.DateTimeFormat(undefined, {weekday: 'long'});
|
||||
|
||||
/** Vrátí datum v ISO formátu. */
|
||||
export function formatDate(date: Date, format?: string) {
|
||||
@@ -15,7 +17,7 @@ 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();
|
||||
let currentDayOfWeek = date.toLocaleDateString("CZ-cs", { weekday: 'long' });
|
||||
let currentDayOfWeek = DAY_OF_WEEK_FORMAT.format(date);
|
||||
return `${currentDay}.${currentMonth}.${currentYear} (${currentDayOfWeek})`;
|
||||
}
|
||||
|
||||
@@ -32,9 +34,9 @@ export function getHumanTime(time: Date) {
|
||||
* @param date datum
|
||||
* @returns index dne v týdnu
|
||||
*/
|
||||
export const getDayOfWeekIndex = (date: Date): DayOfWeekIndex => {
|
||||
export const getDayOfWeekIndex = (date: Date) => {
|
||||
// https://stackoverflow.com/a/4467559
|
||||
return ((((date.getDay() - 1) % 7) + 7) % 7) as DayOfWeekIndex;
|
||||
return (((date.getDay() - 1) % 7) + 7) % 7;
|
||||
}
|
||||
|
||||
/** Vrátí true, pokud je předané datum o víkendu. */
|
||||
|
||||
@@ -4,6 +4,7 @@ export enum Restaurants {
|
||||
// UMOTLIKU = 'uMotliku',
|
||||
TECHTOWER = 'techTower',
|
||||
ZASTAVKAUMICHALA = 'zastavkaUmichala',
|
||||
SENKSERIKOVA = 'senkSerikova',
|
||||
}
|
||||
|
||||
export type FoodChoices = {
|
||||
@@ -70,36 +71,20 @@ interface PizzaDay {
|
||||
orders: Order[], // seznam objednávek jednotlivých lidí
|
||||
}
|
||||
|
||||
/** Index dne v týdnu (0 = pondělí, 6 = neděle) */
|
||||
// TODO tohle by měl být (seřazený) enum MONDAY-SUNDAY, ne číslo
|
||||
export const daysOfWeeksIndices = [0, 1, 2, 3, 4, 5, 6] as const;
|
||||
export type DayOfWeekIndex = typeof daysOfWeeksIndices[number]
|
||||
|
||||
const daysOfWeek = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'] as const;
|
||||
export type DayOfWeek = typeof daysOfWeek[number];
|
||||
|
||||
/** Denní menu všech dostupných podniků. */
|
||||
export type DailyMenu = {
|
||||
[restaurant in Restaurants]?: RestaurantDailyMenu
|
||||
}
|
||||
|
||||
/** Týdenní menu jednotlivých restaurací. */
|
||||
export type WeekMenu = {
|
||||
[dayIndex in DayOfWeek]?: DailyMenu
|
||||
[dayIndex: number]: {
|
||||
[restaurant in Restaurants]?: DayMenu
|
||||
}
|
||||
|
||||
/** Týdenní menu jedné restaurace. */
|
||||
export type RestaurantWeeklyMenu = {
|
||||
[key in DayOfWeek]?: Food[]
|
||||
}
|
||||
|
||||
/** Data vztahující se k jednomu konkrétnímu dni. */
|
||||
export type DayData = {
|
||||
date: string, // datum dne
|
||||
isWeekend: boolean, // příznak, zda je datum víkend
|
||||
weekIndex: DayOfWeekIndex, // index dne v týdnu (0-6)
|
||||
weekIndex: number, // index dne v týdnu (0-6)
|
||||
choices: Choices, // seznam voleb uživatelů
|
||||
menus?: { [restaurant in Restaurants]?: RestaurantDailyMenu }, // menu jednotlivých restaurací
|
||||
menus?: { [restaurant in Restaurants]?: DayMenu }, // menu jednotlivých restaurací
|
||||
pizzaDay?: PizzaDay, // pizza day pro dnešní den, pokud existuje
|
||||
pizzaList?: Pizza[], // seznam dostupných pizz pro dnešní den
|
||||
pizzaListLastUpdate?: Date, // datum a čas poslední aktualizace pizz
|
||||
@@ -107,11 +92,11 @@ export type DayData = {
|
||||
|
||||
/** Veškerá data pro zobrazení na klientovi. */
|
||||
export type ClientData = DayData & {
|
||||
todayWeekIndex?: DayOfWeekIndex, // index dnešního dne v týdnu (0-6)
|
||||
todayWeekIndex?: number, // index dnešního dne v týdnu (0-6)
|
||||
}
|
||||
|
||||
/** Nabídka jídel jednoho podniku pro jeden konkrétní den. */
|
||||
export type RestaurantDailyMenu = {
|
||||
export type DayMenu = {
|
||||
lastUpdate: number, // UNIX timestamp poslední aktualizace menu
|
||||
closed: boolean, // příznak, zda je daný podnik v tento den zavřený
|
||||
food: Food[], // seznam jídel v menu
|
||||
@@ -126,11 +111,13 @@ export type Food = {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
SLADOVNICKA = 'Sladovnická',
|
||||
// UMOTLIKU = 'U Motlíků',
|
||||
TECHTOWER = 'TechTower',
|
||||
ZASTAVKAUMICHALA = 'Zastávka u Michala',
|
||||
SENKSERIKOVA = 'Pivovarský šenk Šeříková',
|
||||
SPSE = 'SPŠE',
|
||||
PIZZA = 'Pizza day',
|
||||
OBJEDNAVAM = 'Budu objednávat',
|
||||
@@ -144,7 +131,7 @@ export type LocationKey = keyof typeof Locations;
|
||||
export enum UdalostEnum {
|
||||
ZAHAJENA_PIZZA = "Zahájen pizza day",
|
||||
OBJEDNANA_PIZZA = "Objednána pizza",
|
||||
JDEME_OBED = "Jdeme oběd",
|
||||
JDEME_NA_OBED = "Jdeme na oběd",
|
||||
}
|
||||
|
||||
export type NotififaceInput = {
|
||||
@@ -234,3 +221,12 @@ export type AnimationPosition = {
|
||||
"--end-bottom"?: 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