Compare commits
20 Commits
feat/refre
...
480fe725f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
480fe725f1
|
|||
| d2845f7d0f | |||
| 269f1994bc | |||
| 3dcda2028e | |||
| cfffd2b31d | |||
| 58bb5f4e7d | |||
| a3dfdb17e8 | |||
| 124fdce69d | |||
| ff20394b97 | |||
| a77a04bcdf | |||
|
42852805e0
|
|||
| d767730b19 | |||
| fa4f9903cb | |||
| cf8be8c64f | |||
| 4c2b08adf8 | |||
|
62cc82da9a
|
|||
|
40c113a4c8
|
|||
|
7681584d11
|
|||
| c670b4212a | |||
| 5fd90de3f8 |
@@ -414,8 +414,7 @@ function App() {
|
||||
<img alt="" src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} /> */}
|
||||
Poslední změny:
|
||||
<ul>
|
||||
<li>Migrace na generované <Link target='_blank' to="https://www.openapis.org">OpenAPI</Link></li>
|
||||
<li>Odebrání zimní atmosféry</li>
|
||||
<li>Podpora ručního refresh týdne</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
{dayIndex != null &&
|
||||
|
||||
@@ -7,8 +7,7 @@ import FeaturesVotingModal from "./modals/FeaturesVotingModal";
|
||||
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
|
||||
import { useNavigate } from "react-router";
|
||||
import { STATS_URL } from "../AppRoutes";
|
||||
import { FeatureRequest, getVotes, refreshMenu, Restaurant, updateVote } from "../../../types";
|
||||
import RefreshMenuModal from "./modals/RefreshMenuModal";
|
||||
import { FeatureRequest, getVotes, updateVote } from "../../../types";
|
||||
|
||||
export default function Header() {
|
||||
const auth = useAuth();
|
||||
@@ -17,7 +16,6 @@ export default function Header() {
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState<boolean>(false);
|
||||
const [votingModalOpen, setVotingModalOpen] = useState<boolean>(false);
|
||||
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
|
||||
const [refreshModalOpen, setRefreshModalOpen] = useState<boolean>(false);
|
||||
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[] | undefined>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -40,10 +38,6 @@ export default function Header() {
|
||||
setPizzaModalOpen(false);
|
||||
}
|
||||
|
||||
const closeRefreshModal = () => {
|
||||
setRefreshModalOpen(false);
|
||||
}
|
||||
|
||||
const isValidInteger = (str: string) => {
|
||||
str = str.trim();
|
||||
if (!str) {
|
||||
@@ -113,12 +107,6 @@ export default function Header() {
|
||||
setFeatureVotes(votes);
|
||||
}
|
||||
|
||||
const handleRefreshMenu = async (restaurants: Restaurant[]) => {
|
||||
if (restaurants.length > 0) {
|
||||
await refreshMenu({ body: restaurants });
|
||||
}
|
||||
}
|
||||
|
||||
return <Navbar variant='dark' expand="lg">
|
||||
<Navbar.Brand href="/">Luncher</Navbar.Brand>
|
||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||
@@ -129,7 +117,6 @@ export default function Header() {
|
||||
<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.Item onClick={() => setRefreshModalOpen(true)}>Přenačíst menu</NavDropdown.Item>
|
||||
<NavDropdown.Divider />
|
||||
<NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item>
|
||||
</NavDropdown>
|
||||
@@ -138,6 +125,5 @@ export default function Header() {
|
||||
<SettingsModal isOpen={settingsModalOpen} onClose={closeSettingsModal} onSave={saveSettings} />
|
||||
<FeaturesVotingModal isOpen={votingModalOpen} onClose={closeVotingModal} onChange={saveFeatureVote} initialValues={featureVotes} />
|
||||
<PizzaCalculatorModal isOpen={pizzaModalOpen} onClose={closePizzaModal} />
|
||||
<RefreshMenuModal isOpen={refreshModalOpen} onClose={closeRefreshModal} onSubmit={handleRefreshMenu} />
|
||||
</Navbar>
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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 } from "react";
|
||||
import { Modal, Button } from "react-bootstrap"
|
||||
import { useRef, useState } from "react";
|
||||
import { Modal, Button, Alert } from "react-bootstrap"
|
||||
import { useSettings } from "../../context/settings";
|
||||
|
||||
type Props = {
|
||||
@@ -15,6 +15,41 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly<Prop
|
||||
const nameRef = 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">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title><h2>Nastavení</h2></Modal.Title>
|
||||
@@ -24,6 +59,48 @@ 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" }}>
|
||||
<input ref={hideSoupsRef} type="checkbox" defaultChecked={settings?.hideSoups} /> Skrýt polévky
|
||||
</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 />
|
||||
<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>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { generateToken, verify } from "./auth";
|
||||
import { InsufficientPermissions } from "./utils";
|
||||
import { initWebsocket } from "./websocket";
|
||||
import pizzaDayRoutes from "./routes/pizzaDayRoutes";
|
||||
import foodRoutes from "./routes/foodRoutes";
|
||||
import foodRoutes, { refreshMetoda } from "./routes/foodRoutes";
|
||||
import votingRoutes from "./routes/votingRoutes";
|
||||
import easterEggRoutes from "./routes/easterEggRoutes";
|
||||
import statsRoutes from "./routes/statsRoutes";
|
||||
@@ -96,6 +96,9 @@ 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 */
|
||||
app.use("/api/", (req, res, next) => {
|
||||
if (HTTP_REMOTE_USER_ENABLED) {
|
||||
|
||||
@@ -291,7 +291,7 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
|
||||
|
||||
const result: Food[][] = [];
|
||||
// TODO validovat, že v textu nalezeného <font> je rozsah, do kterého spadá vstupní datum
|
||||
const siblings = secondTry ? $(font).parent().siblings() : $(font).parent().parent().siblings();
|
||||
const siblings = secondTry ? $($(font).parent().parent().parent().next('h3').children('font')[0]).children('p') : $(font).parent().parent().siblings();
|
||||
let parsing = false;
|
||||
let currentDayIndex = 0;
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
@@ -342,7 +342,8 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
|
||||
return getMenuZastavkaUmichalaMock();
|
||||
}
|
||||
|
||||
const nowDate = new Date().getDate();
|
||||
const today = new Date();
|
||||
today.setHours(0,0,0,0);
|
||||
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",
|
||||
@@ -351,8 +352,8 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
|
||||
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
|
||||
const currentDate = new Date(firstDayOfWeek);
|
||||
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex);
|
||||
|
||||
if (currentDate.getDate() < nowDate || (currentDate.getDate() === nowDate && new Date().getHours() >= 14)) {
|
||||
currentDate.setHours(0,0,0,0);
|
||||
if (currentDate < today || (currentDate.getTime() === today.getTime() && new Date().getHours() >= 14)) {
|
||||
result[dayIndex] = [{
|
||||
amount: undefined,
|
||||
name: "Pro tento den není uveřejněna nabídka jídel",
|
||||
@@ -360,8 +361,9 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
|
||||
isSoup: false,
|
||||
}];
|
||||
} else {
|
||||
const url = (currentDate.getDate() === nowDate) ?
|
||||
ZASTAVKAUMICHALA_URL : ZASTAVKAUMICHALA_URL + '/?do=dailyMenu-changeDate&dailyMenu-dateString=' + formatDate(currentDate, 'DD.MM.YYYY');
|
||||
const url = (currentDate.getTime() === today.getTime())
|
||||
? 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);
|
||||
@@ -401,11 +403,13 @@ export const getMenuSenkSerikova = async (firstDayOfWeek: Date, mock: boolean =
|
||||
}).then(res => decoder.decode(new Uint8Array(res.data))).then(content => content);
|
||||
const $ = load(html);
|
||||
|
||||
const nowDate = new Date().getDate();
|
||||
const today = new Date();
|
||||
today.setHours(0,0,0,0);
|
||||
const currentDate = new Date(firstDayOfWeek);
|
||||
const result: Food[][] = [];
|
||||
let dayIndex = 0;
|
||||
while (currentDate.getDate() < nowDate) {
|
||||
currentDate.setHours(0,0,0,0);
|
||||
while (currentDate < today) {
|
||||
result[dayIndex] = [{
|
||||
amount: undefined,
|
||||
name: "Pro tento den není uveřejněna nabídka jídel",
|
||||
|
||||
@@ -1,13 +1,51 @@
|
||||
import express, { Request } from "express";
|
||||
import express, { Request, Response } from "express";
|
||||
import { getLogin, getTrusted } from "../auth";
|
||||
import { addChoice, getDateForWeekIndex, getRestaurantMenu, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service";
|
||||
import { getDayOfWeekIndex, parseToken } from "../utils";
|
||||
import { addChoice, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote, getRestaurantMenu, fetchRestaurantWeekMenuData, saveRestaurantWeekMenu } from "../service";
|
||||
import { getDayOfWeekIndex, parseToken, getFirstWorkDayOfWeek } from "../utils";
|
||||
import { getWebsocket } from "../websocket";
|
||||
import { callNotifikace } from "../notifikace";
|
||||
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. */
|
||||
const MENU_REFRESH_INTERVAL = 15;
|
||||
|
||||
// RateLimit na refresh endpoint
|
||||
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ň
|
||||
@@ -144,25 +182,85 @@ router.post("/jdemeObed", async (req, res, next) => {
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
router.post("/refreshMenu", async (req, res, next) => {
|
||||
if (!req.body || !Array.isArray(req.body)) {
|
||||
return res.status(400).json({ error: "Neplatný požadavek" });
|
||||
// /api/food/refresh?type=week&heslo=docasnyheslo
|
||||
export const refreshMetoda = async (req: Request, res: Response) => {
|
||||
const { type, heslo } = req.query as { type?: string; heslo?: string };
|
||||
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 {
|
||||
const now = new Date();
|
||||
for (const restaurant of req.body) {
|
||||
// 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 menu = await getRestaurantMenu(restaurant);
|
||||
if (menu.lastUpdate != null) {
|
||||
const minutes = (now.getTime() - menu.lastUpdate) / 1000 / 60;
|
||||
if (minutes < MENU_REFRESH_INTERVAL) {
|
||||
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.`);
|
||||
// Pro všechny restaurace refreshni menu na aktuální týden
|
||||
const restaurants = ["SLADOVNICKA", "TECHTOWER", "ZASTAVKAUMICHALA", "SENKSERIKOVA"] as const;
|
||||
const firstDay = getFirstWorkDayOfWeek(getToday());
|
||||
const results: Record<string, any> = {};
|
||||
const successfulRestaurants: string[] = [];
|
||||
const failedRestaurants: string[] = [];
|
||||
|
||||
// Nejdříve načíst všechna data bez ukládání
|
||||
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);
|
||||
|
||||
// Pokud se nepodařilo načíst žádnou restauraci
|
||||
if (successfulRestaurants.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: "Nepodařilo se získat validní data z žádné restaurace",
|
||||
failed: failedRestaurants,
|
||||
results: results
|
||||
});
|
||||
}
|
||||
res.status(200).json({});
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
// 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;
|
||||
@@ -76,15 +76,107 @@ async function getMenu(date: Date): Promise<WeekMenu | undefined> {
|
||||
}
|
||||
|
||||
// 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.
|
||||
* Pokud neexistuje nebo je nastaven příznak force, provede stažení menu pro příslušný týden a uložení do DB.
|
||||
* Pokud neexistuje, provede stažení menu pro příslušný týden a uložení do DB.
|
||||
*
|
||||
* @param restaurant restaurace
|
||||
* @param date datum, ke kterému získat menu
|
||||
* @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.
|
||||
* @param forceRefresh příznak vynuceného obnovení
|
||||
*/
|
||||
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, force?: boolean): Promise<RestaurantDayMenu> {
|
||||
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, forceRefresh = false): Promise<RestaurantDayMenu> {
|
||||
const usedDate = date ?? getToday();
|
||||
const dayOfWeekIndex = getDayOfWeekIndex(usedDate);
|
||||
const now = new Date().getTime();
|
||||
@@ -97,94 +189,56 @@ export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, for
|
||||
}
|
||||
|
||||
let weekMenu = await getMenu(usedDate);
|
||||
if (weekMenu == null) {
|
||||
weekMenu = [{}, {}, {}, {}, {}];
|
||||
}
|
||||
weekMenu ??= [{}, {}, {}, {}, {}];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (weekMenu[i] == null) {
|
||||
weekMenu[i] = {};
|
||||
}
|
||||
if (weekMenu[i][restaurant] == null) {
|
||||
weekMenu[i][restaurant] = {
|
||||
weekMenu[i] ??= {};
|
||||
weekMenu[i][restaurant] ??= {
|
||||
lastUpdate: now,
|
||||
closed: false,
|
||||
food: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if ((!weekMenu[dayOfWeekIndex][restaurant]?.food?.length && force === undefined) || force) {
|
||||
if (forceRefresh || !weekMenu[dayOfWeekIndex][restaurant]?.food?.length) {
|
||||
const firstDay = getFirstWorkDayOfWeek(usedDate);
|
||||
const mock = process.env.MOCK_DATA === 'true';
|
||||
|
||||
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':
|
||||
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') {
|
||||
if (restaurantWeekFood[i].length === 1 && restaurantWeekFood[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') {
|
||||
if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[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.') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
} 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.') {
|
||||
if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[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;
|
||||
}
|
||||
}
|
||||
|
||||
// Uložení do storage
|
||||
await storage.setData(getMenuKey(usedDate), weekMenu);
|
||||
} catch (e: any) {
|
||||
console.error(`Selhalo načtení jídel pro podnik ${restaurant}`, e);
|
||||
}
|
||||
}
|
||||
return weekMenu[dayOfWeekIndex][restaurant]!;
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ paths:
|
||||
$ref: "./paths/food/changeDepartureTime.yml"
|
||||
/food/jdemeObed:
|
||||
$ref: "./paths/food/jdemeObed.yml"
|
||||
/food/refreshMenu:
|
||||
$ref: "./paths/food/refreshMenu.yml"
|
||||
|
||||
# Pizza day (/api/pizzaDay)
|
||||
/pizzaDay/create:
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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