Compare commits
1 Commits
a9709a944f
...
feat/refre
| Author | SHA1 | Date | |
|---|---|---|---|
| 67758d91cf |
@@ -414,8 +414,8 @@ function App() {
|
|||||||
<img alt="" src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} /> */}
|
<img alt="" src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} /> */}
|
||||||
Poslední změny:
|
Poslední změny:
|
||||||
<ul>
|
<ul>
|
||||||
<li>Podpora ručního refresh týdne</li>
|
<li>Migrace na generované <Link target='_blank' to="https://www.openapis.org">OpenAPI</Link></li>
|
||||||
<li>Úprava pro přepracovanou podobu stránek Sladovnická</li>
|
<li>Odebrání zimní atmosféry</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Alert>
|
</Alert>
|
||||||
{dayIndex != null &&
|
{dayIndex != null &&
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import FeaturesVotingModal from "./modals/FeaturesVotingModal";
|
|||||||
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
|
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { STATS_URL } from "../AppRoutes";
|
import { STATS_URL } from "../AppRoutes";
|
||||||
import { FeatureRequest, getVotes, updateVote } from "../../../types";
|
import { FeatureRequest, getVotes, refreshMenu, Restaurant, updateVote } from "../../../types";
|
||||||
|
import RefreshMenuModal from "./modals/RefreshMenuModal";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
@@ -16,6 +17,7 @@ export default function Header() {
|
|||||||
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);
|
||||||
|
const [refreshModalOpen, setRefreshModalOpen] = useState<boolean>(false);
|
||||||
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[] | undefined>([]);
|
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[] | undefined>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -38,6 +40,10 @@ export default function Header() {
|
|||||||
setPizzaModalOpen(false);
|
setPizzaModalOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const closeRefreshModal = () => {
|
||||||
|
setRefreshModalOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
const isValidInteger = (str: string) => {
|
const isValidInteger = (str: string) => {
|
||||||
str = str.trim();
|
str = str.trim();
|
||||||
if (!str) {
|
if (!str) {
|
||||||
@@ -107,6 +113,12 @@ export default function Header() {
|
|||||||
setFeatureVotes(votes);
|
setFeatureVotes(votes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRefreshMenu = async (restaurants: Restaurant[]) => {
|
||||||
|
if (restaurants.length > 0) {
|
||||||
|
await refreshMenu({ body: restaurants });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return <Navbar variant='dark' expand="lg">
|
return <Navbar variant='dark' expand="lg">
|
||||||
<Navbar.Brand href="/">Luncher</Navbar.Brand>
|
<Navbar.Brand href="/">Luncher</Navbar.Brand>
|
||||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||||
@@ -117,6 +129,7 @@ export default function Header() {
|
|||||||
<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.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
||||||
|
<NavDropdown.Item onClick={() => setRefreshModalOpen(true)}>Přenačíst menu</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>
|
||||||
@@ -125,5 +138,6 @@ export default function Header() {
|
|||||||
<SettingsModal isOpen={settingsModalOpen} onClose={closeSettingsModal} onSave={saveSettings} />
|
<SettingsModal isOpen={settingsModalOpen} onClose={closeSettingsModal} onSave={saveSettings} />
|
||||||
<FeaturesVotingModal isOpen={votingModalOpen} onClose={closeVotingModal} onChange={saveFeatureVote} initialValues={featureVotes} />
|
<FeaturesVotingModal isOpen={votingModalOpen} onClose={closeVotingModal} onChange={saveFeatureVote} initialValues={featureVotes} />
|
||||||
<PizzaCalculatorModal isOpen={pizzaModalOpen} onClose={closePizzaModal} />
|
<PizzaCalculatorModal isOpen={pizzaModalOpen} onClose={closePizzaModal} />
|
||||||
|
<RefreshMenuModal isOpen={refreshModalOpen} onClose={closeRefreshModal} onSubmit={handleRefreshMenu} />
|
||||||
</Navbar>
|
</Navbar>
|
||||||
}
|
}
|
||||||
53
client/src/components/modals/RefreshMenuModal.tsx
Normal file
53
client/src/components/modals/RefreshMenuModal.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Modal, Button, Form } from "react-bootstrap"
|
||||||
|
import { Restaurant } from "../../../../types";
|
||||||
|
import { getRestaurantName } from "../../enums";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean,
|
||||||
|
onClose: () => void,
|
||||||
|
onSubmit: (restaurants: Restaurant[]) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modální dialog pro přenačtení menu jednotlivých podniků. */
|
||||||
|
export default function RefreshMenuModal({ isOpen, onClose, onSubmit }: Readonly<Props>) {
|
||||||
|
|
||||||
|
const [restaurants, setRestaurants] = useState<Restaurant[]>([]);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.currentTarget.checked) {
|
||||||
|
setRestaurants([...restaurants, e.currentTarget.value as Restaurant]);
|
||||||
|
} else {
|
||||||
|
setRestaurants(restaurants.filter(restaurant => restaurant !== e.currentTarget.value as Restaurant));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Modal show={isOpen} onHide={onClose} size="lg">
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>
|
||||||
|
Vyberte podniky k přenačtení menu
|
||||||
|
<p style={{ fontSize: '12px' }}>Menu lze přenačíst nejdříve 15 minut od poslední aktualizace</p>
|
||||||
|
</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
{(Object.keys(Restaurant) as Array<keyof typeof Restaurant>).map(key => {
|
||||||
|
return <Form.Check
|
||||||
|
key={key}
|
||||||
|
type='checkbox'
|
||||||
|
id={key}
|
||||||
|
label={getRestaurantName(key as Restaurant)}
|
||||||
|
onChange={handleChange}
|
||||||
|
value={key}
|
||||||
|
/>
|
||||||
|
})}
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant="primary" onClick={() => onSubmit(restaurants)} disabled={restaurants.length === 0}>
|
||||||
|
Přenačíst
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" onClick={onClose}>
|
||||||
|
Zrušit
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useRef } from "react";
|
||||||
import { Modal, Button, Alert } from "react-bootstrap"
|
import { Modal, Button } from "react-bootstrap"
|
||||||
import { useSettings } from "../../context/settings";
|
import { useSettings } from "../../context/settings";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -15,41 +15,6 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly<Prop
|
|||||||
const nameRef = useRef<HTMLInputElement>(null);
|
const nameRef = useRef<HTMLInputElement>(null);
|
||||||
const hideSoupsRef = useRef<HTMLInputElement>(null);
|
const hideSoupsRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Pro refresh jidel
|
|
||||||
const refreshPassRef = useRef<HTMLInputElement>(null);
|
|
||||||
const refreshTypeRef = useRef<HTMLSelectElement>(null);
|
|
||||||
const [refreshLoading, setRefreshLoading] = useState(false);
|
|
||||||
const [refreshMessage, setRefreshMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
const password = refreshPassRef.current?.value;
|
|
||||||
const type = refreshTypeRef.current?.value;
|
|
||||||
if (!password || !type) {
|
|
||||||
setRefreshMessage({ type: 'error', text: 'Zadejte heslo a typ refresh.' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRefreshLoading(true);
|
|
||||||
setRefreshMessage(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/food/refresh?type=${type}&heslo=${encodeURIComponent(password)}`);
|
|
||||||
const data = await res.json();
|
|
||||||
if (res.ok) {
|
|
||||||
setRefreshMessage({ type: 'success', text: 'Uspesny fetch' });
|
|
||||||
if (refreshPassRef.current) {
|
|
||||||
// Clean hesla xd
|
|
||||||
refreshPassRef.current.value = '';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setRefreshMessage({ type: 'error', text: data.error || 'Chyba při obnovování jídelníčku.' });
|
|
||||||
}
|
|
||||||
} catch (error) { }
|
|
||||||
finally {
|
|
||||||
setRefreshLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Modal show={isOpen} onHide={onClose} size="lg">
|
return <Modal show={isOpen} onHide={onClose} size="lg">
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title><h2>Nastavení</h2></Modal.Title>
|
<Modal.Title><h2>Nastavení</h2></Modal.Title>
|
||||||
@@ -59,48 +24,6 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly<Prop
|
|||||||
<span title="V nabídkách nebudou zobrazovány polévky. Tato funkce je experimentální, a zejména u TechTower bývá často problém polévky spolehlivě rozeznat. V případě využití této funkce průběžně nahlašujte stále se zobrazující polévky." style={{ "cursor": "help" }}>
|
<span title="V nabídkách nebudou zobrazovány polévky. Tato funkce je experimentální, a zejména u TechTower bývá často problém polévky spolehlivě rozeznat. V případě využití této funkce průběžně nahlašujte stále se zobrazující polévky." style={{ "cursor": "help" }}>
|
||||||
<input ref={hideSoupsRef} type="checkbox" defaultChecked={settings?.hideSoups} /> Skrýt polévky
|
<input ref={hideSoupsRef} type="checkbox" defaultChecked={settings?.hideSoups} /> Skrýt polévky
|
||||||
</span>
|
</span>
|
||||||
<hr />
|
|
||||||
<h4>Obnovit jídelníček</h4>
|
|
||||||
<p>Ruční refresh dat z restaurací.</p>
|
|
||||||
|
|
||||||
{refreshMessage && (
|
|
||||||
<Alert variant={refreshMessage.type === 'success' ? 'success' : 'danger'}>
|
|
||||||
{refreshMessage.text}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
Heslo: <input
|
|
||||||
ref={refreshPassRef}
|
|
||||||
type="password"
|
|
||||||
placeholder="Zadejte heslo"
|
|
||||||
className="form-control d-inline-block"
|
|
||||||
style={{ width: 'auto', marginLeft: '10px' }}
|
|
||||||
onKeyDown={e => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
Typ refreshe: <select
|
|
||||||
ref={refreshTypeRef}
|
|
||||||
className="form-select d-inline-block"
|
|
||||||
style={{ width: 'auto', marginLeft: '10px' }}
|
|
||||||
defaultValue="week"
|
|
||||||
>
|
|
||||||
<option value="week">Týden</option>
|
|
||||||
<option value="day">Den</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="info"
|
|
||||||
onClick={handleRefresh}
|
|
||||||
disabled={refreshLoading}
|
|
||||||
className="mb-3"
|
|
||||||
>
|
|
||||||
{refreshLoading ? 'Refreshing...' : 'Refresh'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
<h4>Bankovní účet</h4>
|
<h4>Bankovní účet</h4>
|
||||||
<p>Nastavením čísla účtu umožníte automatické generování QR kódů pro úhradu za vámi provedené objednávky v rámci Pizza day.<br />Pokud vaše číslo účtu neobsahuje předčíslí, je možné ho zcela vynechat.<br /><br />Číslo účtu není ukládáno na serveru, posílá se na něj pouze za účelem vygenerování QR kódů.</p>
|
<p>Nastavením čísla účtu umožníte automatické generování QR kódů pro úhradu za vámi provedené objednávky v rámci Pizza day.<br />Pokud vaše číslo účtu neobsahuje předčíslí, je možné ho zcela vynechat.<br /><br />Číslo účtu není ukládáno na serveru, posílá se na něj pouze za účelem vygenerování QR kódů.</p>
|
||||||
|
|||||||
23
run_dev.sh
23
run_dev.sh
@@ -1,18 +1,5 @@
|
|||||||
#!/bin/bash
|
export NODE_ENV=development
|
||||||
# Spustí server a klienta v samostatných panelech uvnitř stejného tmux okna.
|
cd types && yarn install && yarn openapi-ts
|
||||||
# Pokud už daná tmux session existuje, pouze se k ní připojí.
|
cd server && yarn install && yarn start &
|
||||||
|
cd client && yarn install && yarn start &
|
||||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
wait
|
||||||
|
|
||||||
SESSION="luncher"
|
|
||||||
|
|
||||||
if ! tmux has-session -t $SESSION 2>/dev/null; then
|
|
||||||
cd types && yarn openapi-ts && cd ..
|
|
||||||
tmux new-session -d -s $SESSION
|
|
||||||
tmux send-keys -t $SESSION:0 "cd $SCRIPT_DIR" Enter
|
|
||||||
tmux split-window -v
|
|
||||||
tmux send-keys -t $SESSION:0.0 "cd server && export NODE_ENV=development && yarn startReload" Enter
|
|
||||||
tmux send-keys -t $SESSION:0.1 "cd client && yarn start" Enter
|
|
||||||
fi
|
|
||||||
|
|
||||||
tmux attach-session -t $SESSION
|
|
||||||
1
server/.gitignore
vendored
1
server/.gitignore
vendored
@@ -1,4 +1,3 @@
|
|||||||
/data
|
|
||||||
/dist
|
/dist
|
||||||
/resources/easterEggs
|
/resources/easterEggs
|
||||||
/src/gen
|
/src/gen
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { generateToken, verify } from "./auth";
|
|||||||
import { InsufficientPermissions } from "./utils";
|
import { InsufficientPermissions } from "./utils";
|
||||||
import { initWebsocket } from "./websocket";
|
import { initWebsocket } from "./websocket";
|
||||||
import pizzaDayRoutes from "./routes/pizzaDayRoutes";
|
import pizzaDayRoutes from "./routes/pizzaDayRoutes";
|
||||||
import foodRoutes, { refreshMetoda } 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";
|
import statsRoutes from "./routes/statsRoutes";
|
||||||
@@ -96,9 +96,6 @@ app.get("/api/qr", (req, res) => {
|
|||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
|
|
||||||
// Přeskočení auth pro refresh dat xd
|
|
||||||
app.use("/api/food/refresh", refreshMetoda);
|
|
||||||
|
|
||||||
/** Middleware ověřující JWT token */
|
/** Middleware ověřující JWT token */
|
||||||
app.use("/api/", (req, res, next) => {
|
app.use("/api/", (req, res, next) => {
|
||||||
if (HTTP_REMOTE_USER_ENABLED) {
|
if (HTTP_REMOTE_USER_ENABLED) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const SOUP_NAMES = [
|
|||||||
const DAYS_IN_WEEK = ['pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota', 'neděle'];
|
const DAYS_IN_WEEK = ['pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota', 'neděle'];
|
||||||
|
|
||||||
// URL na týdenní menu jednotlivých restaurací
|
// URL na týdenní menu jednotlivých restaurací
|
||||||
const SLADOVNICKA_URL = 'https://sladovnicka.unasplzenchutna.cz/cz/#denni-nabidka';
|
const SLADOVNICKA_URL = 'https://sladovnicka.unasplzenchutna.cz/cz/denni-nabidka';
|
||||||
const U_MOTLIKU_URL = 'https://www.umotliku.cz/menu';
|
const U_MOTLIKU_URL = 'https://www.umotliku.cz/menu';
|
||||||
const TECHTOWER_URL = 'https://www.equifarm.cz/restaurace-techtower';
|
const TECHTOWER_URL = 'https://www.equifarm.cz/restaurace-techtower';
|
||||||
const ZASTAVKAUMICHALA_URL = 'https://www.zastavkaumichala.cz';
|
const ZASTAVKAUMICHALA_URL = 'https://www.zastavkaumichala.cz';
|
||||||
@@ -78,65 +78,81 @@ export const getMenuSladovnicka = async (firstDayOfWeek: Date, mock: boolean = f
|
|||||||
const html = await getHtml(SLADOVNICKA_URL);
|
const html = await getHtml(SLADOVNICKA_URL);
|
||||||
const $ = load(html);
|
const $ = load(html);
|
||||||
|
|
||||||
const menuContentElements = $('#daily-menu-content-list').children('[id^="daily-menu-content-"]');
|
const list = $('ul.tab-links').children();
|
||||||
// Prozatím předpokládáme, že budou mít vždy elementy pro všech 5 dní v týdnu, i pokud bude zavřeno
|
|
||||||
if (menuContentElements.length < 5) {
|
|
||||||
throw Error("Neočekávaný počet dní v menu Sladovnické: " + menuContentElements.length + ", očekáváno 5 (možná je některý den zavřeno?)");
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: Food[][] = [];
|
const result: Food[][] = [];
|
||||||
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
|
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
|
||||||
const dayChildren = $(menuContentElements[dayIndex]).children();
|
const currentDate = new Date(firstDayOfWeek);
|
||||||
// Prozatím předpokládáme, že budou mít vždy polévku a hlavní jídla
|
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex);
|
||||||
if (dayChildren.length < 2) {
|
const searchedDayText = `${currentDate.getDate()}.${currentDate.getMonth() + 1}.${capitalize(DAYS_IN_WEEK[dayIndex])}`;
|
||||||
throw Error("Neočekávaný počet children v menu Sladovnické pro den " + dayIndex + ": " + dayChildren.length + ", očekávány alespoň 2 (polévka a hlavní jídlo)");
|
// Najdeme index pro vstupní datum (např. při svátcích bude posunutý)
|
||||||
|
// TODO validovat, že vstupní datum je v aktuálním týdnu
|
||||||
|
// TODO tenhle způsob je zbytečně komplikovaný - stačilo by hledat rovnou v div.tab-content, protože každý den tam má datum taky (akorát je print-only)
|
||||||
|
let index = undefined;
|
||||||
|
list.each((i, dayRow) => {
|
||||||
|
const rowText = $(dayRow).first().text().trim();
|
||||||
|
if (rowText === searchedDayText) {
|
||||||
|
index = i;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (index === undefined) {
|
||||||
|
// Pravděpodobně svátek, nebo je zavřeno
|
||||||
|
result[dayIndex] = [{
|
||||||
|
amount: undefined,
|
||||||
|
name: "Pro daný den nebyla nalezena denní nabídka",
|
||||||
|
price: "",
|
||||||
|
isSoup: false,
|
||||||
|
}];
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parsování polévky
|
// Dle dohledaného indexu najdeme správný tabpanel
|
||||||
const soupElement = dayChildren.get(0);
|
const rows = $('div.tab-content').children();
|
||||||
const soupTable = $(soupElement).find('table tbody tr');
|
if (index >= rows.length) {
|
||||||
const soupCells = soupTable.children('td');
|
throw Error("V HTML nebyl nalezen řádek menu pro index " + index);
|
||||||
|
}
|
||||||
|
const tabPanel = $(rows.get(index));
|
||||||
|
|
||||||
|
// Opětovná validace, že daný tabpanel je pro vstupní datum
|
||||||
|
const headers = tabPanel.find('h2');
|
||||||
|
if (headers.length !== 3) {
|
||||||
|
throw Error("Neočekávaný počet elementů h2 v menu pro datum " + searchedDayText + ", očekávány 3, ale nalezeno bylo " + headers.length);
|
||||||
|
}
|
||||||
|
const dayText = $(headers.get(0)).text().trim();
|
||||||
|
if (dayText !== searchedDayText) {
|
||||||
|
throw Error("Neočekávaný datum na řádce nalezeného dne: '" + dayText + "', ale očekáváno bylo '" + searchedDayText + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// V tabpanelu očekáváme dvě tabulky - pro polévku a pro hlavní jídlo
|
||||||
|
const tables = tabPanel.find('table');
|
||||||
|
if (tables.length !== 2) {
|
||||||
|
throw Error("Neočekávaný počet tabulek na řádce nalezeného dne: " + tables.length + ", ale očekávány byly 2");
|
||||||
|
}
|
||||||
|
const currentDayFood: Food[] = [];
|
||||||
|
// Polévka - div -> table -> tbody -> tr -> 3x td
|
||||||
|
const soupCells = $(tables.get(0)).children().first().children().first().children();
|
||||||
if (soupCells.length !== 3) {
|
if (soupCells.length !== 3) {
|
||||||
throw Error("Neočekávaný počet buněk v tabulce polévky: " + soupCells.length + ", ale očekávány byly 3");
|
throw Error("Neočekávaný počet buněk v tabulce polévky: " + soupCells.length + ", ale očekávány byly 3");
|
||||||
}
|
}
|
||||||
|
|
||||||
const soupAmount = sanitizeText($(soupCells.get(0)).text());
|
|
||||||
const soupName = sanitizeText($(soupCells.get(1)).text());
|
|
||||||
const soupPrice = sanitizeText($(soupCells.get(2)).text().replace(' ', '\xA0'));
|
|
||||||
|
|
||||||
// Parsování hlavních jídel
|
|
||||||
const mainCourseElement = dayChildren.get(1);
|
|
||||||
const mainCourseTable = $(mainCourseElement).find('table tbody');
|
|
||||||
const mainCourseRows = mainCourseTable.children('tr');
|
|
||||||
|
|
||||||
const currentDayFood: Food[] = [];
|
|
||||||
|
|
||||||
// Přidáme polévku do seznamu jídel
|
|
||||||
currentDayFood.push({
|
currentDayFood.push({
|
||||||
amount: soupAmount,
|
amount: sanitizeText($(soupCells.get(0)).text()),
|
||||||
name: soupName,
|
name: sanitizeText($(soupCells.get(1)).text()),
|
||||||
price: soupPrice,
|
price: sanitizeText($(soupCells.get(2)).text().replace(' ', '\xA0')),
|
||||||
isSoup: true,
|
isSoup: true,
|
||||||
});
|
});
|
||||||
|
// Hlavní jídla - div -> table -> tbody -> 3x tr
|
||||||
// Projdeme všechny řádky hlavních jídel
|
const mainCourseRows = $(tables.get(1)).children().first().children();
|
||||||
mainCourseRows.each((i, row) => {
|
mainCourseRows.each((i, foodRow) => {
|
||||||
const cells = $(row).children('td');
|
const foodCells = $(foodRow).children();
|
||||||
const amount = sanitizeText($(cells.get(0)).text());
|
if (foodCells.length !== 3) {
|
||||||
const name = sanitizeText($(cells.get(1)).text());
|
throw Error("Neočekávaný počet buněk v řádku jídla: " + foodCells.length + ", ale očekávány byly 3");
|
||||||
const price = sanitizeText($(cells.get(2)).text().replace(' ', '\xA0'));
|
|
||||||
|
|
||||||
// Přeskočíme prázdné řádky (první řádek může být prázdný)
|
|
||||||
if (name.trim().length > 0) {
|
|
||||||
currentDayFood.push({
|
|
||||||
amount,
|
|
||||||
name,
|
|
||||||
price,
|
|
||||||
isSoup: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
currentDayFood.push({
|
||||||
|
amount: sanitizeText($(foodCells.get(0)).text()),
|
||||||
|
name: sanitizeText($(foodCells.get(1)).text()),
|
||||||
|
price: sanitizeText($(foodCells.get(2)).text().replace(' ', '\xA0')),
|
||||||
|
isSoup: false,
|
||||||
|
});
|
||||||
|
})
|
||||||
result[dayIndex] = currentDayFood;
|
result[dayIndex] = currentDayFood;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -275,7 +291,7 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
|
|||||||
|
|
||||||
const result: Food[][] = [];
|
const result: Food[][] = [];
|
||||||
// TODO validovat, že v textu nalezeného <font> je rozsah, do kterého spadá vstupní datum
|
// TODO validovat, že v textu nalezeného <font> je rozsah, do kterého spadá vstupní datum
|
||||||
const siblings = secondTry ? $($(font).parent().parent().parent().next('h3').children('font')[0]).children('p') : $(font).parent().parent().siblings();
|
const siblings = secondTry ? $(font).parent().siblings() : $(font).parent().parent().siblings();
|
||||||
let parsing = false;
|
let parsing = false;
|
||||||
let currentDayIndex = 0;
|
let currentDayIndex = 0;
|
||||||
for (let i = 0; i < siblings.length; i++) {
|
for (let i = 0; i < siblings.length; i++) {
|
||||||
@@ -326,8 +342,7 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
|
|||||||
return getMenuZastavkaUmichalaMock();
|
return getMenuZastavkaUmichalaMock();
|
||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date();
|
const nowDate = new Date().getDate();
|
||||||
today.setHours(0,0,0,0);
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"Cookie": "_nss=1; PHPSESSID=9e37de17e0326b0942613d6e67a30e69",
|
"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",
|
"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",
|
||||||
@@ -336,8 +351,8 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
|
|||||||
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
|
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
|
||||||
const currentDate = new Date(firstDayOfWeek);
|
const currentDate = new Date(firstDayOfWeek);
|
||||||
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex);
|
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex);
|
||||||
currentDate.setHours(0,0,0,0);
|
|
||||||
if (currentDate < today || (currentDate.getTime() === today.getTime() && new Date().getHours() >= 14)) {
|
if (currentDate.getDate() < nowDate || (currentDate.getDate() === nowDate && new Date().getHours() >= 14)) {
|
||||||
result[dayIndex] = [{
|
result[dayIndex] = [{
|
||||||
amount: undefined,
|
amount: undefined,
|
||||||
name: "Pro tento den není uveřejněna nabídka jídel",
|
name: "Pro tento den není uveřejněna nabídka jídel",
|
||||||
@@ -345,9 +360,8 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
|
|||||||
isSoup: false,
|
isSoup: false,
|
||||||
}];
|
}];
|
||||||
} else {
|
} else {
|
||||||
const url = (currentDate.getTime() === today.getTime())
|
const url = (currentDate.getDate() === nowDate) ?
|
||||||
? ZASTAVKAUMICHALA_URL
|
ZASTAVKAUMICHALA_URL : ZASTAVKAUMICHALA_URL + '/?do=dailyMenu-changeDate&dailyMenu-dateString=' + formatDate(currentDate, 'DD.MM.YYYY');
|
||||||
: ZASTAVKAUMICHALA_URL + '/?do=dailyMenu-changeDate&dailyMenu-dateString=' + formatDate(currentDate, 'DD.MM.YYYY');
|
|
||||||
const html = await axios.get(url, {
|
const html = await axios.get(url, {
|
||||||
headers,
|
headers,
|
||||||
}).then(res => res.data).then(content => content);
|
}).then(res => res.data).then(content => content);
|
||||||
@@ -387,13 +401,11 @@ export const getMenuSenkSerikova = async (firstDayOfWeek: Date, mock: boolean =
|
|||||||
}).then(res => decoder.decode(new Uint8Array(res.data))).then(content => content);
|
}).then(res => decoder.decode(new Uint8Array(res.data))).then(content => content);
|
||||||
const $ = load(html);
|
const $ = load(html);
|
||||||
|
|
||||||
const today = new Date();
|
const nowDate = new Date().getDate();
|
||||||
today.setHours(0,0,0,0);
|
|
||||||
const currentDate = new Date(firstDayOfWeek);
|
const currentDate = new Date(firstDayOfWeek);
|
||||||
const result: Food[][] = [];
|
const result: Food[][] = [];
|
||||||
let dayIndex = 0;
|
let dayIndex = 0;
|
||||||
currentDate.setHours(0,0,0,0);
|
while (currentDate.getDate() < nowDate) {
|
||||||
while (currentDate < today) {
|
|
||||||
result[dayIndex] = [{
|
result[dayIndex] = [{
|
||||||
amount: undefined,
|
amount: undefined,
|
||||||
name: "Pro tento den není uveřejněna nabídka jídel",
|
name: "Pro tento den není uveřejněna nabídka jídel",
|
||||||
|
|||||||
@@ -1,51 +1,13 @@
|
|||||||
import express, { Request, Response } from "express";
|
import express, { Request } from "express";
|
||||||
import { getLogin, getTrusted } from "../auth";
|
import { getLogin, getTrusted } from "../auth";
|
||||||
import { addChoice, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote, getRestaurantMenu, fetchRestaurantWeekMenuData, saveRestaurantWeekMenu } from "../service";
|
import { addChoice, getDateForWeekIndex, getRestaurantMenu, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service";
|
||||||
import { getDayOfWeekIndex, parseToken, getFirstWorkDayOfWeek } from "../utils";
|
import { getDayOfWeekIndex, parseToken } from "../utils";
|
||||||
import { getWebsocket } from "../websocket";
|
import { getWebsocket } from "../websocket";
|
||||||
import { callNotifikace } from "../notifikace";
|
import { callNotifikace } from "../notifikace";
|
||||||
import { AddChoiceData, ChangeDepartureTimeData, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen";
|
import { AddChoiceData, ChangeDepartureTimeData, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen";
|
||||||
|
|
||||||
|
/** Po jak dlouhé době (v minutách) lze provést nové načtení menu. */
|
||||||
// RateLimit na refresh endpoint
|
const MENU_REFRESH_INTERVAL = 15;
|
||||||
interface RateLimitEntry {
|
|
||||||
count: number;
|
|
||||||
resetTime: number;
|
|
||||||
}
|
|
||||||
const rateLimits: Record<string, RateLimitEntry> = {};
|
|
||||||
const RATE_LIMIT = 1; // maximální počet požadavků za minutu
|
|
||||||
const RATE_LIMIT_WINDOW = 30 * 60 * 1000; // je to v ms (x * 1min)
|
|
||||||
|
|
||||||
// Kontrola ratelimitu
|
|
||||||
function checkRateLimit(key: string, limit: number = RATE_LIMIT): boolean {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Vyčištění starých záznamů
|
|
||||||
Object.keys(rateLimits).forEach(k => {
|
|
||||||
if (rateLimits[k].resetTime < now) {
|
|
||||||
delete rateLimits[k];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Kontrola, že záznam existuje a platí
|
|
||||||
if (rateLimits[key] && rateLimits[key].resetTime > now) {
|
|
||||||
// Záznam platí a kontroluje se limit
|
|
||||||
if (rateLimits[key].count >= limit) {
|
|
||||||
return false; // Překročen limit
|
|
||||||
}
|
|
||||||
|
|
||||||
// ++ xd
|
|
||||||
rateLimits[key].count++;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// + klic
|
|
||||||
rateLimits[key] = {
|
|
||||||
count: 1,
|
|
||||||
resetTime: now + RATE_LIMIT_WINDOW
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ověří a vrátí index dne v týdnu z požadavku, za předpokladu, že byl předán, a je zároveň
|
* Ověří a vrátí index dne v týdnu z požadavku, za předpokladu, že byl předán, a je zároveň
|
||||||
@@ -182,85 +144,25 @@ router.post("/jdemeObed", async (req, res, next) => {
|
|||||||
} catch (e: any) { next(e) }
|
} catch (e: any) { next(e) }
|
||||||
});
|
});
|
||||||
|
|
||||||
// /api/food/refresh?type=week&heslo=docasnyheslo
|
router.post("/refreshMenu", async (req, res, next) => {
|
||||||
export const refreshMetoda = async (req: Request, res: Response) => {
|
if (!req.body || !Array.isArray(req.body)) {
|
||||||
const { type, heslo } = req.query as { type?: string; heslo?: string };
|
return res.status(400).json({ error: "Neplatný požadavek" });
|
||||||
if (heslo !== "docasnyheslo" && heslo !== "tohleheslopavelnesmizjistit123") {
|
|
||||||
return res.status(403).json({ error: "Neplatné heslo" });
|
|
||||||
}
|
|
||||||
if (!checkRateLimit("refresh") && heslo !== "tohleheslopavelnesmizjistit123") {
|
|
||||||
return res.status(429).json({ error: "Refresh už se zavolal, chvíli počkej :))" });
|
|
||||||
}
|
|
||||||
if (type !== "week" && type !== "day") {
|
|
||||||
return res.status(400).json({ error: "Neznámý typ refresh" });
|
|
||||||
}
|
|
||||||
if (type === "day") {
|
|
||||||
return res.status(400).json({ error: "ještě neumim TODO..." });
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Pro všechny restaurace refreshni menu na aktuální týden
|
const now = new Date();
|
||||||
const restaurants = ["SLADOVNICKA", "TECHTOWER", "ZASTAVKAUMICHALA", "SENKSERIKOVA"] as const;
|
for (const restaurant of req.body) {
|
||||||
const firstDay = getFirstWorkDayOfWeek(getToday());
|
// TODO tohle je technicky špatně, protože pokud aktuálně jídla načtená nejsou, tak je toto volání načte a následně je to načte znovu kvůli force!
|
||||||
const results: Record<string, any> = {};
|
const menu = await getRestaurantMenu(restaurant);
|
||||||
const successfulRestaurants: string[] = [];
|
if (menu.lastUpdate != null) {
|
||||||
const failedRestaurants: string[] = [];
|
const minutes = (now.getTime() - menu.lastUpdate) / 1000 / 60;
|
||||||
|
if (minutes < MENU_REFRESH_INTERVAL) {
|
||||||
// Nejdříve načíst všechna data bez ukládání
|
throw Error(`Podnik ${restaurant} byl přenačtený před ${Math.round(minutes)} minutami. Nové přenačtení lze provést nejdříve za ${Math.round(MENU_REFRESH_INTERVAL - minutes)} minut.`);
|
||||||
for (const rest of restaurants) {
|
|
||||||
try {
|
|
||||||
const weekData = await fetchRestaurantWeekMenuData(rest, firstDay);
|
|
||||||
results[rest] = weekData;
|
|
||||||
|
|
||||||
// Kontrola validity dat
|
|
||||||
if (weekData && weekData.length > 0 &&
|
|
||||||
weekData.some(dayMenu => dayMenu && dayMenu.length > 0)) {
|
|
||||||
successfulRestaurants.push(rest);
|
|
||||||
} else {
|
|
||||||
failedRestaurants.push(rest);
|
|
||||||
results[rest] = { error: "Žádná validní data" };
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
failedRestaurants.push(rest);
|
|
||||||
results[rest] = { error: `Chyba při načítání: ${error}` };
|
|
||||||
}
|
}
|
||||||
|
await getRestaurantMenu(restaurant, undefined, true);
|
||||||
}
|
}
|
||||||
|
res.status(200).json({});
|
||||||
// Pokud se nepodařilo načíst žádnou restauraci
|
} catch (e: any) { next(e) }
|
||||||
if (successfulRestaurants.length === 0) {
|
});
|
||||||
return res.status(400).json({
|
|
||||||
error: "Nepodařilo se získat validní data z žádné restaurace",
|
|
||||||
failed: failedRestaurants,
|
|
||||||
results: results
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uložit pouze validní data
|
|
||||||
for (const rest of successfulRestaurants) {
|
|
||||||
try {
|
|
||||||
await saveRestaurantWeekMenu(rest as any, firstDay, results[rest]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Chyba při ukládání dat pro ${rest}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Připravit odpověď
|
|
||||||
const response: any = {
|
|
||||||
ok: true,
|
|
||||||
refreshed: results,
|
|
||||||
successful: successfulRestaurants
|
|
||||||
};
|
|
||||||
|
|
||||||
if (failedRestaurants.length > 0) {
|
|
||||||
response.warning = `Nepodařilo se načíst: ${failedRestaurants.join(', ')}`;
|
|
||||||
response.failed = failedRestaurants;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json(response);
|
|
||||||
} catch (e: any) {
|
|
||||||
res.status(500).json({ error: e?.message || "Chyba při refreshi" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
router.get("/refresh", refreshMetoda);
|
|
||||||
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -76,107 +76,15 @@ async function getMenu(date: Date): Promise<WeekMenu | undefined> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO přesun do restaurants.ts
|
// TODO přesun do restaurants.ts
|
||||||
/**
|
|
||||||
* Načte menu dané restaurace pro celý týden bez ukládání do storage.
|
|
||||||
* Používá se pro validaci dat před uložením.
|
|
||||||
*
|
|
||||||
* @param restaurant restaurace
|
|
||||||
* @param firstDay první pracovní den týdne
|
|
||||||
* @returns pole menu pro jednotlivé dny týdne
|
|
||||||
*/
|
|
||||||
export async function fetchRestaurantWeekMenuData(restaurant: Restaurant, firstDay: Date): Promise<any[]> {
|
|
||||||
return await fetchRestaurantWeekMenu(restaurant, firstDay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uloží týdenní menu restaurace do storage.
|
|
||||||
*
|
|
||||||
* @param restaurant restaurace
|
|
||||||
* @param date datum z týdne, pro který ukládat menu
|
|
||||||
* @param weekData data týdenního menu
|
|
||||||
*/
|
|
||||||
export async function saveRestaurantWeekMenu(restaurant: Restaurant, date: Date, weekData: any[]): Promise<void> {
|
|
||||||
const now = new Date().getTime();
|
|
||||||
let weekMenu = await getMenu(date);
|
|
||||||
weekMenu ??= [{}, {}, {}, {}, {}];
|
|
||||||
|
|
||||||
// Inicializace struktury pro restauraci
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
weekMenu[i] ??= {};
|
|
||||||
weekMenu[i][restaurant] ??= {
|
|
||||||
lastUpdate: now,
|
|
||||||
closed: false,
|
|
||||||
food: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uložení dat pro všechny dny
|
|
||||||
for (let i = 0; i < weekData.length && i < weekMenu.length; i++) {
|
|
||||||
weekMenu[i][restaurant]!.food = weekData[i];
|
|
||||||
weekMenu[i][restaurant]!.lastUpdate = now;
|
|
||||||
|
|
||||||
// Detekce uzavření pro každou restauraci
|
|
||||||
switch (restaurant) {
|
|
||||||
case 'SLADOVNICKA':
|
|
||||||
if (weekData[i].length === 1 && weekData[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
|
|
||||||
weekMenu[i][restaurant]!.closed = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'TECHTOWER':
|
|
||||||
if (weekData[i]?.length === 1 && weekData[i][0].name.toLowerCase() === 'svátek') {
|
|
||||||
weekMenu[i][restaurant]!.closed = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'ZASTAVKAUMICHALA':
|
|
||||||
if (weekData[i]?.length === 1 && weekData[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
|
|
||||||
weekMenu[i][restaurant]!.closed = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'SENKSERIKOVA':
|
|
||||||
if (weekData[i]?.length === 1 && weekData[i][0].name === 'Pro tento den nebylo zadáno menu.') {
|
|
||||||
weekMenu[i][restaurant]!.closed = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uložení do storage
|
|
||||||
await storage.setData(getMenuKey(date), weekMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Načte menu dané restaurace pro celý týden bez ukládání do storage.
|
|
||||||
*
|
|
||||||
* @param restaurant restaurace
|
|
||||||
* @param firstDay první pracovní den týdne
|
|
||||||
* @returns pole menu pro jednotlivé dny týdne
|
|
||||||
*/
|
|
||||||
async function fetchRestaurantWeekMenu(restaurant: Restaurant, firstDay: Date): Promise<any[]> {
|
|
||||||
const mock = process.env.MOCK_DATA === 'true';
|
|
||||||
|
|
||||||
switch (restaurant) {
|
|
||||||
case 'SLADOVNICKA':
|
|
||||||
return await getMenuSladovnicka(firstDay, mock);
|
|
||||||
case 'TECHTOWER':
|
|
||||||
return await getMenuTechTower(firstDay, mock);
|
|
||||||
case 'ZASTAVKAUMICHALA':
|
|
||||||
return await getMenuZastavkaUmichala(firstDay, mock);
|
|
||||||
case 'SENKSERIKOVA':
|
|
||||||
return await getMenuSenkSerikova(firstDay, mock);
|
|
||||||
default:
|
|
||||||
throw new Error(`Nepodporovaná restaurace: ${restaurant}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vrátí menu dané restaurace pro předaný den.
|
* Vrátí menu dané restaurace pro předaný den.
|
||||||
* Pokud neexistuje, provede stažení menu pro příslušný týden a uložení do DB.
|
* Pokud neexistuje nebo je nastaven příznak force, provede stažení menu pro příslušný týden a uložení do DB.
|
||||||
*
|
*
|
||||||
* @param restaurant restaurace
|
* @param restaurant restaurace
|
||||||
* @param date datum, ke kterému získat menu
|
* @param date datum, ke kterému získat menu
|
||||||
* @param forceRefresh příznak vynuceného obnovení
|
* @param force Příznak, zda znovu získat aktuální menu i v případě, že je již načteno. Pokud není předán, provede se načtení pouze v případě, že menu aktuálně nemáme. Pokud je true, provede nové načtení. Pokud je false, neprovede se nové načtení ani v případě, že menu aktuálně nemáme.
|
||||||
*/
|
*/
|
||||||
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, forceRefresh = false): Promise<RestaurantDayMenu> {
|
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, force?: boolean): Promise<RestaurantDayMenu> {
|
||||||
const usedDate = date ?? getToday();
|
const usedDate = date ?? getToday();
|
||||||
const dayOfWeekIndex = getDayOfWeekIndex(usedDate);
|
const dayOfWeekIndex = getDayOfWeekIndex(usedDate);
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
@@ -189,56 +97,94 @@ export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, for
|
|||||||
}
|
}
|
||||||
|
|
||||||
let weekMenu = await getMenu(usedDate);
|
let weekMenu = await getMenu(usedDate);
|
||||||
weekMenu ??= [{}, {}, {}, {}, {}];
|
if (weekMenu == null) {
|
||||||
for (let i = 0; i < 5; i++) {
|
weekMenu = [{}, {}, {}, {}, {}];
|
||||||
weekMenu[i] ??= {};
|
|
||||||
weekMenu[i][restaurant] ??= {
|
|
||||||
lastUpdate: now,
|
|
||||||
closed: false,
|
|
||||||
food: [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (forceRefresh || !weekMenu[dayOfWeekIndex][restaurant]?.food?.length) {
|
for (let i = 0; i < 5; i++) {
|
||||||
const firstDay = getFirstWorkDayOfWeek(usedDate);
|
if (weekMenu[i] == null) {
|
||||||
|
weekMenu[i] = {};
|
||||||
try {
|
|
||||||
const restaurantWeekFood = await fetchRestaurantWeekMenu(restaurant, firstDay);
|
|
||||||
|
|
||||||
// Aktualizace menu pro všechny dny
|
|
||||||
for (let i = 0; i < restaurantWeekFood.length && i < weekMenu.length; i++) {
|
|
||||||
weekMenu[i][restaurant]!.food = restaurantWeekFood[i];
|
|
||||||
weekMenu[i][restaurant]!.lastUpdate = now;
|
|
||||||
|
|
||||||
// Detekce uzavření pro každou restauraci
|
|
||||||
switch (restaurant) {
|
|
||||||
case 'SLADOVNICKA':
|
|
||||||
if (restaurantWeekFood[i].length === 1 && restaurantWeekFood[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
|
|
||||||
weekMenu[i][restaurant]!.closed = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'TECHTOWER':
|
|
||||||
if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name.toLowerCase() === 'svátek') {
|
|
||||||
weekMenu[i][restaurant]!.closed = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'ZASTAVKAUMICHALA':
|
|
||||||
if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
|
|
||||||
weekMenu[i][restaurant]!.closed = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'SENKSERIKOVA':
|
|
||||||
if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name === 'Pro tento den nebylo zadáno menu.') {
|
|
||||||
weekMenu[i][restaurant]!.closed = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uložení do storage
|
|
||||||
await storage.setData(getMenuKey(usedDate), weekMenu);
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`Selhalo načtení jídel pro podnik ${restaurant}`, e);
|
|
||||||
}
|
}
|
||||||
|
if (weekMenu[i][restaurant] == null) {
|
||||||
|
weekMenu[i][restaurant] = {
|
||||||
|
lastUpdate: now,
|
||||||
|
closed: false,
|
||||||
|
food: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!weekMenu[dayOfWeekIndex][restaurant]?.food?.length && force === undefined) || force) {
|
||||||
|
const firstDay = getFirstWorkDayOfWeek(usedDate);
|
||||||
|
const mock = process.env.MOCK_DATA === 'true';
|
||||||
|
switch (restaurant) {
|
||||||
|
case 'SLADOVNICKA':
|
||||||
|
try {
|
||||||
|
const sladovnickaFood = await getMenuSladovnicka(firstDay, mock);
|
||||||
|
for (let i = 0; i < sladovnickaFood.length; i++) {
|
||||||
|
weekMenu[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') {
|
||||||
|
weekMenu[i][restaurant]!.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Selhalo načtení jídel pro podnik Sladovnická", e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// case 'UMOTLIKU':
|
||||||
|
// try {
|
||||||
|
// const uMotlikuFood = await getMenuUMotliku(firstDay, mock);
|
||||||
|
// for (let i = 0; i < uMotlikuFood.length; i++) {
|
||||||
|
// menus[i][restaurant]!.food = uMotlikuFood[i];
|
||||||
|
// if (uMotlikuFood[i].length === 1 && uMotlikuFood[i][0].name.toLowerCase() === 'zavřeno') {
|
||||||
|
// menus[i][restaurant]!.closed = true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (e: any) {
|
||||||
|
// console.error("Selhalo načtení jídel pro podnik U Motlíků", e);
|
||||||
|
// }
|
||||||
|
// break;
|
||||||
|
case 'TECHTOWER':
|
||||||
|
try {
|
||||||
|
const techTowerFood = await getMenuTechTower(firstDay, mock);
|
||||||
|
for (let i = 0; i < techTowerFood.length; i++) {
|
||||||
|
weekMenu[i][restaurant]!.food = techTowerFood[i];
|
||||||
|
if (techTowerFood[i]?.length === 1 && techTowerFood[i][0].name.toLowerCase() === 'svátek') {
|
||||||
|
weekMenu[i][restaurant]!.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Selhalo načtení jídel pro podnik TechTower", e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ZASTAVKAUMICHALA':
|
||||||
|
try {
|
||||||
|
const zastavkaUmichalaFood = await getMenuZastavkaUmichala(firstDay, mock);
|
||||||
|
for (let i = 0; i < zastavkaUmichalaFood.length; i++) {
|
||||||
|
weekMenu[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.') {
|
||||||
|
weekMenu[i][restaurant]!.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Selhalo načtení jídel pro podnik Zastávka u Michala", e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'SENKSERIKOVA':
|
||||||
|
try {
|
||||||
|
const senkSerikovaFood = await getMenuSenkSerikova(firstDay, mock);
|
||||||
|
for (let i = 0; i < senkSerikovaFood.length; i++) {
|
||||||
|
weekMenu[i][restaurant]!.food = senkSerikovaFood[i];
|
||||||
|
if (senkSerikovaFood[i]?.length === 1 && senkSerikovaFood[i][0].name === 'Pro tento den nebylo zadáno menu.') {
|
||||||
|
weekMenu[i][restaurant]!.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Selhalo načtení jídel pro podnik Pivovarský šenk Šeříková", e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await storage.setData(getMenuKey(usedDate), weekMenu);
|
||||||
}
|
}
|
||||||
return weekMenu[dayOfWeekIndex][restaurant]!;
|
return weekMenu[dayOfWeekIndex][restaurant]!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ paths:
|
|||||||
$ref: "./paths/food/changeDepartureTime.yml"
|
$ref: "./paths/food/changeDepartureTime.yml"
|
||||||
/food/jdemeObed:
|
/food/jdemeObed:
|
||||||
$ref: "./paths/food/jdemeObed.yml"
|
$ref: "./paths/food/jdemeObed.yml"
|
||||||
|
/food/refreshMenu:
|
||||||
|
$ref: "./paths/food/refreshMenu.yml"
|
||||||
|
|
||||||
# Pizza day (/api/pizzaDay)
|
# Pizza day (/api/pizzaDay)
|
||||||
/pizzaDay/create:
|
/pizzaDay/create:
|
||||||
|
|||||||
15
types/paths/food/refreshMenu.yml
Normal file
15
types/paths/food/refreshMenu.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
post:
|
||||||
|
operationId: refreshMenu
|
||||||
|
summary: Přenačtení menu vybraných podniků
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "../../schemas/_index.yml#/Restaurant"
|
||||||
|
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Menu bylo přenačteno
|
||||||
Reference in New Issue
Block a user