Refresh menu, první část
This commit is contained in:
parent
49b8ab5c13
commit
67758d91cf
@ -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,11 +1,14 @@
|
|||||||
import express, { Request } 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 } from "../service";
|
import { addChoice, getDateForWeekIndex, getRestaurantMenu, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service";
|
||||||
import { getDayOfWeekIndex, parseToken } from "../utils";
|
import { getDayOfWeekIndex, parseToken } from "../utils";
|
||||||
import { getWebsocket } from "../websocket";
|
import { getWebsocket } from "../websocket";
|
||||||
import { callNotifikace } from "../notifikace";
|
import { 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. */
|
||||||
|
const MENU_REFRESH_INTERVAL = 15;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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ň
|
||||||
* roven nebo vyšší indexu dnešního dne.
|
* roven nebo vyšší indexu dnešního dne.
|
||||||
@ -141,4 +144,25 @@ router.post("/jdemeObed", async (req, res, next) => {
|
|||||||
} catch (e: any) { next(e) }
|
} 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" });
|
||||||
|
}
|
||||||
|
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.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await getRestaurantMenu(restaurant, undefined, true);
|
||||||
|
}
|
||||||
|
res.status(200).json({});
|
||||||
|
} catch (e: any) { next(e) }
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
@ -78,13 +78,13 @@ async function getMenu(date: Date): Promise<WeekMenu | undefined> {
|
|||||||
// TODO přesun do restaurants.ts
|
// TODO přesun do restaurants.ts
|
||||||
/**
|
/**
|
||||||
* Vrátí menu dané restaurace pro předaný den.
|
* Vrátí menu dané restaurace pro předaný den.
|
||||||
* Pokud neexistuje, provede 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 mock příznak, zda chceme pouze mock data
|
* @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): 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();
|
||||||
@ -112,7 +112,8 @@ export async function getRestaurantMenu(restaurant: Restaurant, date?: Date): Pr
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!weekMenu[dayOfWeekIndex][restaurant]?.food?.length) {
|
|
||||||
|
if ((!weekMenu[dayOfWeekIndex][restaurant]?.food?.length && force === undefined) || force) {
|
||||||
const firstDay = getFirstWorkDayOfWeek(usedDate);
|
const firstDay = getFirstWorkDayOfWeek(usedDate);
|
||||||
const mock = process.env.MOCK_DATA === 'true';
|
const mock = process.env.MOCK_DATA === 'true';
|
||||||
switch (restaurant) {
|
switch (restaurant) {
|
||||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user