dodelej me z pc
This commit is contained in:
+9
-10
@@ -7,6 +7,7 @@ self.addEventListener('push', (event) => {
|
|||||||
body: data.body,
|
body: data.body,
|
||||||
icon: '/favicon.ico',
|
icon: '/favicon.ico',
|
||||||
tag: 'lunch-reminder',
|
tag: 'lunch-reminder',
|
||||||
|
data: { login: data.login },
|
||||||
actions: [
|
actions: [
|
||||||
{ action: 'neobedvam', title: 'Mám vlastní/neobědvám' },
|
{ action: 'neobedvam', title: 'Mám vlastní/neobědvám' },
|
||||||
],
|
],
|
||||||
@@ -18,28 +19,26 @@ self.addEventListener('notificationclick', (event) => {
|
|||||||
event.notification.close();
|
event.notification.close();
|
||||||
|
|
||||||
if (event.action === 'neobedvam') {
|
if (event.action === 'neobedvam') {
|
||||||
event.waitUntil(
|
const login = event.notification.data?.login;
|
||||||
self.registration.pushManager.getSubscription().then((subscription) => {
|
if (login) {
|
||||||
if (!subscription) return;
|
event.waitUntil(
|
||||||
return fetch('/api/notifications/push/quickChoice', {
|
fetch('/api/notifications/push/quickChoice', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ endpoint: subscription.endpoint }),
|
body: JSON.stringify({ login }),
|
||||||
});
|
})
|
||||||
})
|
);
|
||||||
);
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
self.clients.matchAll({ type: 'window' }).then((clientList) => {
|
self.clients.matchAll({ type: 'window' }).then((clientList) => {
|
||||||
// Pokud je již otevřené okno, zaostříme na něj
|
|
||||||
for (const client of clientList) {
|
for (const client of clientList) {
|
||||||
if (client.url.includes(self.location.origin) && 'focus' in client) {
|
if (client.url.includes(self.location.origin) && 'focus' in client) {
|
||||||
return client.focus();
|
return client.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Jinak otevřeme nové
|
|
||||||
return self.clients.openWindow('/');
|
return self.clients.openWindow('/');
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-1
@@ -18,7 +18,7 @@ import Loader from './components/Loader';
|
|||||||
import { getHumanDateTime, isInTheFuture, formatDateString } from './Utils';
|
import { getHumanDateTime, isInTheFuture, formatDateString } from './Utils';
|
||||||
import NoteModal from './components/modals/NoteModal';
|
import NoteModal from './components/modals/NoteModal';
|
||||||
import { useEasterEgg } from './context/eggs';
|
import { useEasterEgg } from './context/eggs';
|
||||||
import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr } from '../../types';
|
import { ClientData, Food, MealSlot, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr } from '../../types';
|
||||||
import { getLunchChoiceName } from './enums';
|
import { getLunchChoiceName } from './enums';
|
||||||
// import FallingLeaves, { LEAF_PRESETS, LEAF_COLOR_THEMES } from './FallingLeaves';
|
// import FallingLeaves, { LEAF_PRESETS, LEAF_COLOR_THEMES } from './FallingLeaves';
|
||||||
// import './FallingLeaves.scss';
|
// import './FallingLeaves.scss';
|
||||||
@@ -124,6 +124,7 @@ function App() {
|
|||||||
});
|
});
|
||||||
socket.on(EVENT_MESSAGE, (newData: ClientData) => {
|
socket.on(EVENT_MESSAGE, (newData: ClientData) => {
|
||||||
// console.log("Přijata nová data ze socketu", newData);
|
// console.log("Přijata nová data ze socketu", newData);
|
||||||
|
if (newData.slot === MealSlot.EXTRA) return;
|
||||||
// Aktualizujeme pouze, pokud jsme dostali data pro den, který máme aktuálně zobrazený
|
// Aktualizujeme pouze, pokud jsme dostali data pro den, který máme aktuálně zobrazený
|
||||||
if (dayIndexRef.current == null || newData.dayIndex === dayIndexRef.current) {
|
if (dayIndexRef.current == null || newData.dayIndex === dayIndexRef.current) {
|
||||||
setData(newData);
|
setData(newData);
|
||||||
|
|||||||
@@ -5,14 +5,24 @@ import { SnowOverlay } from 'react-snow-overlay';
|
|||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import { SocketContext, socket } from "./context/socket";
|
import { SocketContext, socket } from "./context/socket";
|
||||||
import StatsPage from "./pages/StatsPage";
|
import StatsPage from "./pages/StatsPage";
|
||||||
|
import ExtraPage from "./pages/ExtraPage";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
export const STATS_URL = '/stats';
|
export const STATS_URL = '/stats';
|
||||||
|
export const VECERE_URL = '/vecere';
|
||||||
|
|
||||||
export default function AppRoutes() {
|
export default function AppRoutes() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={STATS_URL} element={<StatsPage />} />
|
<Route path={STATS_URL} element={<StatsPage />} />
|
||||||
|
<Route path={VECERE_URL} element={
|
||||||
|
<ProvideSettings>
|
||||||
|
<SocketContext.Provider value={socket}>
|
||||||
|
<ExtraPage />
|
||||||
|
<ToastContainer />
|
||||||
|
</SocketContext.Provider>
|
||||||
|
</ProvideSettings>
|
||||||
|
} />
|
||||||
<Route path="/" element={
|
<Route path="/" element={
|
||||||
<ProvideSettings>
|
<ProvideSettings>
|
||||||
<SocketContext.Provider value={socket}>
|
<SocketContext.Provider value={socket}>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import GenerateQrModal from "./modals/GenerateQrModal";
|
|||||||
import GenerateMockDataModal from "./modals/GenerateMockDataModal";
|
import GenerateMockDataModal from "./modals/GenerateMockDataModal";
|
||||||
import ClearMockDataModal from "./modals/ClearMockDataModal";
|
import ClearMockDataModal from "./modals/ClearMockDataModal";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { STATS_URL } from "../AppRoutes";
|
import { STATS_URL, VECERE_URL } from "../AppRoutes";
|
||||||
import { FeatureRequest, getVotes, updateVote, LunchChoices } from "../../../types";
|
import { FeatureRequest, getVotes, updateVote, LunchChoices } from "../../../types";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -197,6 +197,7 @@ export default function Header({ choices, dayIndex }: Props) {
|
|||||||
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
|
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
|
||||||
<NavDropdown.Item onClick={handleQrMenuClick}>Generování QR kódů</NavDropdown.Item>
|
<NavDropdown.Item onClick={handleQrMenuClick}>Generování QR kódů</NavDropdown.Item>
|
||||||
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
||||||
|
<NavDropdown.Item onClick={() => navigate(VECERE_URL)}>Večeře</NavDropdown.Item>
|
||||||
<NavDropdown.Item onClick={() => setChangelogModalOpen(true)}>Novinky</NavDropdown.Item>
|
<NavDropdown.Item onClick={() => setChangelogModalOpen(true)}>Novinky</NavDropdown.Item>
|
||||||
{IS_DEV && (
|
{IS_DEV && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import { useContext, useEffect, useRef, useState } from 'react';
|
||||||
|
import { Button, Table } from 'react-bootstrap';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faCircleCheck, faNoteSticky, faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import { faBasketShopping, faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import {
|
||||||
|
ClientData, LunchChoice, MealSlot, UserLunchChoice,
|
||||||
|
addChoice, removeChoices, updateNote, setBuyer, getData,
|
||||||
|
} from '../../../types';
|
||||||
|
import { EVENT_MESSAGE, SocketContext } from '../context/socket';
|
||||||
|
import { useAuth } from '../context/auth';
|
||||||
|
import Login from '../Login';
|
||||||
|
import Header from '../components/Header';
|
||||||
|
import Footer from '../components/Footer';
|
||||||
|
import Loader from '../components/Loader';
|
||||||
|
import NoteModal from '../components/modals/NoteModal';
|
||||||
|
|
||||||
|
const SLOT = MealSlot.EXTRA;
|
||||||
|
|
||||||
|
export default function ExtraPage() {
|
||||||
|
const auth = useAuth();
|
||||||
|
const socket = useContext(SocketContext);
|
||||||
|
const [data, setData] = useState<ClientData | undefined>();
|
||||||
|
const [failure, setFailure] = useState(false);
|
||||||
|
const [noteModalOpen, setNoteModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const r = await getData({ query: { slot: SLOT } });
|
||||||
|
if (r.data) setData(r.data);
|
||||||
|
} catch {
|
||||||
|
setFailure(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!auth?.login) return;
|
||||||
|
fetchData();
|
||||||
|
}, [auth?.login]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket.on(EVENT_MESSAGE, (newData: ClientData) => {
|
||||||
|
if (newData.slot === SLOT) setData(newData);
|
||||||
|
});
|
||||||
|
return () => { socket.off(EVENT_MESSAGE); };
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
const myChoice = data?.choices?.OBJEDNAVAM?.[auth?.login ?? ''];
|
||||||
|
const isIn = !!myChoice;
|
||||||
|
const isBuyer = myChoice?.isBuyer ?? false;
|
||||||
|
|
||||||
|
const joinOrder = async () => {
|
||||||
|
if (!auth?.login) return;
|
||||||
|
await addChoice({ body: { locationKey: LunchChoice.OBJEDNAVAM, slot: SLOT } });
|
||||||
|
await fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const joinAndBuy = async () => {
|
||||||
|
if (!auth?.login) return;
|
||||||
|
await addChoice({ body: { locationKey: LunchChoice.OBJEDNAVAM, slot: SLOT } });
|
||||||
|
await setBuyer({ body: { slot: SLOT } });
|
||||||
|
await fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const leaveOrder = async () => {
|
||||||
|
if (!auth?.login) return;
|
||||||
|
await removeChoices({ body: { locationKey: LunchChoice.OBJEDNAVAM, slot: SLOT } });
|
||||||
|
await fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleBuyer = async () => {
|
||||||
|
if (!auth?.login) return;
|
||||||
|
await setBuyer({ body: { slot: SLOT } });
|
||||||
|
await fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveNote = async (note?: string) => {
|
||||||
|
if (!auth?.login) return;
|
||||||
|
await updateNote({ body: { note, slot: SLOT } });
|
||||||
|
setNoteModalOpen(false);
|
||||||
|
await fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!auth?.login) return <Login />;
|
||||||
|
|
||||||
|
if (failure) return (
|
||||||
|
<Loader icon={faSearch} description="Nepodařilo se načíst data" animation="fa-beat" />
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data) return (
|
||||||
|
<Loader icon={faSearch} description="Načítám..." animation="fa-bounce" />
|
||||||
|
);
|
||||||
|
|
||||||
|
const orderEntries = Object.entries(data.choices?.OBJEDNAVAM ?? {}) as [string, UserLunchChoice][];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<Header choices={data.choices} />
|
||||||
|
<div className="wrapper">
|
||||||
|
<h1 className="title">Večeře</h1>
|
||||||
|
<p style={{ color: 'var(--luncher-text-muted)' }}>Extra jídlo pro ty, kdo zůstávají déle</p>
|
||||||
|
|
||||||
|
<div className="content-wrapper">
|
||||||
|
<div className="content">
|
||||||
|
<div className="choice-section fade-in">
|
||||||
|
{!isIn ? (
|
||||||
|
<div className="d-flex gap-2 flex-wrap">
|
||||||
|
<Button variant="primary" onClick={joinOrder}>
|
||||||
|
Přidám se
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline-primary" onClick={joinAndBuy}>
|
||||||
|
<FontAwesomeIcon icon={faBasketShopping} className="me-2" />
|
||||||
|
Budu objednávat
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="d-flex gap-2 flex-wrap align-items-center">
|
||||||
|
<span style={{ color: 'var(--luncher-text-secondary)' }}>
|
||||||
|
{isBuyer ? 'Objednáváš.' : 'Jsi přidán/a k objednávce.'}
|
||||||
|
</span>
|
||||||
|
<Button variant="outline-secondary" size="sm" onClick={toggleBuyer}>
|
||||||
|
<FontAwesomeIcon icon={faBasketShopping} className="me-1" />
|
||||||
|
{isBuyer ? 'Odebrat roli objednávajícího' : 'Označit se jako objednávající'}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline-secondary" size="sm" onClick={() => setNoteModalOpen(true)}>
|
||||||
|
<FontAwesomeIcon icon={faNoteSticky} className="me-1" />
|
||||||
|
Poznámka
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline-danger" size="sm" onClick={leaveOrder}>
|
||||||
|
<FontAwesomeIcon icon={faTrashCan} className="me-1" />
|
||||||
|
Odhlásit se
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{orderEntries.length > 0 && (
|
||||||
|
<Table className="choices-table mt-4 fade-in">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Budu objednávat / Přidám se</td>
|
||||||
|
<td className="p-0">
|
||||||
|
<Table className="nested-table">
|
||||||
|
<tbody>
|
||||||
|
{orderEntries.map(([login, payload]) => (
|
||||||
|
<tr key={login}>
|
||||||
|
<td>
|
||||||
|
<div className="user-row">
|
||||||
|
<div className="user-info">
|
||||||
|
{payload.trusted && (
|
||||||
|
<span className="trusted-icon" title="Ověřený uživatel">
|
||||||
|
<FontAwesomeIcon icon={faCircleCheck} style={{ cursor: 'help' }} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<strong>{login}</strong>
|
||||||
|
{payload.note && (
|
||||||
|
<span className="ms-2" style={{ fontSize: 'small', color: 'var(--luncher-text-secondary)' }}>
|
||||||
|
({payload.note})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="user-actions">
|
||||||
|
{payload.isBuyer && (
|
||||||
|
<span title="Objednávající">
|
||||||
|
<FontAwesomeIcon icon={faBasketShopping} className="buyer-icon" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{login === auth.login && (
|
||||||
|
<>
|
||||||
|
<span title="Upravit poznámku">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
onClick={() => setNoteModalOpen(true)}
|
||||||
|
className="action-icon"
|
||||||
|
icon={faNoteSticky}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span title="Odhlásit se z objednávky">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
onClick={leaveOrder}
|
||||||
|
className="action-icon"
|
||||||
|
icon={faTrashCan}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
<NoteModal
|
||||||
|
isOpen={noteModalOpen}
|
||||||
|
onClose={() => setNoteModalOpen(false)}
|
||||||
|
onSave={saveNote}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Generated
+3941
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"gitnexus": "^1.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-1
@@ -142,7 +142,11 @@ app.get("/api/data", async (req, res) => {
|
|||||||
// Na víkendu zobrazíme pátek místo hlášky "Užívejte víkend"
|
// Na víkendu zobrazíme pátek místo hlášky "Užívejte víkend"
|
||||||
date = getDateForWeekIndex(4);
|
date = getDateForWeekIndex(4);
|
||||||
}
|
}
|
||||||
const data = await getData(date);
|
const slotParam = typeof req.query.slot === 'string' ? req.query.slot : undefined;
|
||||||
|
if (slotParam && slotParam !== 'obed' && slotParam !== 'extra') {
|
||||||
|
return res.status(400).json({ error: 'Neplatný slot' });
|
||||||
|
}
|
||||||
|
const data = await getData(date, slotParam);
|
||||||
// Připojíme nevyřízené QR kódy pro přihlášeného uživatele
|
// Připojíme nevyřízené QR kódy pro přihlášeného uživatele
|
||||||
try {
|
try {
|
||||||
const login = getLogin(parseToken(req));
|
const login = getLogin(parseToken(req));
|
||||||
|
|||||||
@@ -14,13 +14,10 @@ interface RegistryEntry {
|
|||||||
|
|
||||||
type Registry = Record<string, RegistryEntry>;
|
type Registry = Record<string, RegistryEntry>;
|
||||||
|
|
||||||
/** Mapa login → datum (YYYY-MM-DD), kdy byl uživatel naposledy upozorněn. */
|
/** Mapa login → timestamp (ms) posledního odeslání připomínky. */
|
||||||
const remindedToday = new Map<string, string>();
|
const lastReminded = new Map<string, number>();
|
||||||
|
|
||||||
function getTodayDateString(): string {
|
const REMINDER_COOLDOWN_MS = 60 * 60 * 1000; // 60 minut mezi připomínkami
|
||||||
const now = new Date();
|
|
||||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentTimeHHMM(): string {
|
function getCurrentTimeHHMM(): string {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -59,7 +56,7 @@ export async function unsubscribePush(login: string): Promise<void> {
|
|||||||
const registry = await getRegistry();
|
const registry = await getRegistry();
|
||||||
delete registry[login];
|
delete registry[login];
|
||||||
await saveRegistry(registry);
|
await saveRegistry(registry);
|
||||||
remindedToday.delete(login);
|
lastReminded.delete(login);
|
||||||
console.log(`Push reminder: uživatel ${login} odhlášen z připomínek`);
|
console.log(`Push reminder: uživatel ${login} odhlášen z připomínek`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +90,6 @@ async function checkAndSendReminders(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentTime = getCurrentTimeHHMM();
|
const currentTime = getCurrentTimeHHMM();
|
||||||
const todayStr = getTodayDateString();
|
|
||||||
|
|
||||||
// Získáme data pro dnešek jednou pro všechny uživatele
|
// Získáme data pro dnešek jednou pro všechny uživatele
|
||||||
let clientData;
|
let clientData;
|
||||||
@@ -110,8 +106,9 @@ async function checkAndSendReminders(): Promise<void> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Už jsme dnes připomenuli
|
// Cooldown — nepřipomínat častěji než jednou za hodinu
|
||||||
if (remindedToday.get(login) === todayStr) {
|
const last = lastReminded.get(login) ?? 0;
|
||||||
|
if (Date.now() - last < REMINDER_COOLDOWN_MS) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +124,10 @@ async function checkAndSendReminders(): Promise<void> {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
title: 'Luncher',
|
title: 'Luncher',
|
||||||
body: 'Ještě nemáte zvolený oběd!',
|
body: 'Ještě nemáte zvolený oběd!',
|
||||||
|
login,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
remindedToday.set(login, todayStr);
|
lastReminded.set(login, Date.now());
|
||||||
console.log(`Push reminder: odeslána připomínka uživateli ${login}`);
|
console.log(`Push reminder: odeslána připomínka uživateli ${login}`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.statusCode === 410 || error.statusCode === 404) {
|
if (error.statusCode === 410 || error.statusCode === 404) {
|
||||||
|
|||||||
@@ -69,11 +69,20 @@ const parseValidateFutureDayIndex = (req: Request<{}, any, AddChoiceData["body"]
|
|||||||
return dayIndex;
|
return dayIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseSlot = (body: Record<string, any>): string | undefined => {
|
||||||
|
const slot = body?.slot;
|
||||||
|
if (slot != null && slot !== 'obed' && slot !== 'extra') {
|
||||||
|
throw Error(`Neplatný slot: ${slot}`);
|
||||||
|
}
|
||||||
|
return slot ?? undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post("/addChoice", async (req: Request<{}, any, AddChoiceData["body"]>, res, next) => {
|
router.post("/addChoice", async (req: Request<{}, any, AddChoiceData["body"]>, res, next) => {
|
||||||
const login = getLogin(parseToken(req));
|
const login = getLogin(parseToken(req));
|
||||||
const trusted = getTrusted(parseToken(req));
|
const trusted = getTrusted(parseToken(req));
|
||||||
|
const slot = parseSlot(req.body);
|
||||||
let date = undefined;
|
let date = undefined;
|
||||||
if (req.body.dayIndex != null) {
|
if (req.body.dayIndex != null) {
|
||||||
let dayIndex;
|
let dayIndex;
|
||||||
@@ -85,7 +94,7 @@ router.post("/addChoice", async (req: Request<{}, any, AddChoiceData["body"]>, r
|
|||||||
date = getDateForWeekIndex(dayIndex);
|
date = getDateForWeekIndex(dayIndex);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const data = await addChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date);
|
const data = await addChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date, slot);
|
||||||
getWebsocket().emit("message", data);
|
getWebsocket().emit("message", data);
|
||||||
return res.status(200).json(data);
|
return res.status(200).json(data);
|
||||||
} catch (e: any) { next(e) }
|
} catch (e: any) { next(e) }
|
||||||
@@ -94,6 +103,7 @@ router.post("/addChoice", async (req: Request<{}, any, AddChoiceData["body"]>, r
|
|||||||
router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesData["body"]>, res, next) => {
|
router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesData["body"]>, res, next) => {
|
||||||
const login = getLogin(parseToken(req));
|
const login = getLogin(parseToken(req));
|
||||||
const trusted = getTrusted(parseToken(req));
|
const trusted = getTrusted(parseToken(req));
|
||||||
|
const slot = parseSlot(req.body);
|
||||||
let date = undefined;
|
let date = undefined;
|
||||||
if (req.body.dayIndex != null) {
|
if (req.body.dayIndex != null) {
|
||||||
let dayIndex;
|
let dayIndex;
|
||||||
@@ -105,7 +115,7 @@ router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesData["bo
|
|||||||
date = getDateForWeekIndex(dayIndex);
|
date = getDateForWeekIndex(dayIndex);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const data = await removeChoices(login, trusted, req.body.locationKey, date);
|
const data = await removeChoices(login, trusted, req.body.locationKey, date, slot);
|
||||||
getWebsocket().emit("message", data);
|
getWebsocket().emit("message", data);
|
||||||
res.status(200).json(data);
|
res.status(200).json(data);
|
||||||
} catch (e: any) { next(e) }
|
} catch (e: any) { next(e) }
|
||||||
@@ -114,6 +124,7 @@ router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesData["bo
|
|||||||
router.post("/removeChoice", async (req: Request<{}, any, RemoveChoiceData["body"]>, res, next) => {
|
router.post("/removeChoice", async (req: Request<{}, any, RemoveChoiceData["body"]>, res, next) => {
|
||||||
const login = getLogin(parseToken(req));
|
const login = getLogin(parseToken(req));
|
||||||
const trusted = getTrusted(parseToken(req));
|
const trusted = getTrusted(parseToken(req));
|
||||||
|
const slot = parseSlot(req.body);
|
||||||
let date = undefined;
|
let date = undefined;
|
||||||
if (req.body.dayIndex != null) {
|
if (req.body.dayIndex != null) {
|
||||||
let dayIndex;
|
let dayIndex;
|
||||||
@@ -125,7 +136,7 @@ router.post("/removeChoice", async (req: Request<{}, any, RemoveChoiceData["body
|
|||||||
date = getDateForWeekIndex(dayIndex);
|
date = getDateForWeekIndex(dayIndex);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const data = await removeChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date);
|
const data = await removeChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date, slot);
|
||||||
getWebsocket().emit("message", data);
|
getWebsocket().emit("message", data);
|
||||||
res.status(200).json(data);
|
res.status(200).json(data);
|
||||||
} catch (e: any) { next(e) }
|
} catch (e: any) { next(e) }
|
||||||
@@ -135,6 +146,7 @@ router.post("/updateNote", async (req: Request<{}, any, UpdateNoteData["body"]>,
|
|||||||
const login = getLogin(parseToken(req));
|
const login = getLogin(parseToken(req));
|
||||||
const trusted = getTrusted(parseToken(req));
|
const trusted = getTrusted(parseToken(req));
|
||||||
const note = req.body.note;
|
const note = req.body.note;
|
||||||
|
const slot = parseSlot(req.body);
|
||||||
try {
|
try {
|
||||||
if (note && note.length > 70) {
|
if (note && note.length > 70) {
|
||||||
throw Error("Poznámka může mít maximálně 70 znaků");
|
throw Error("Poznámka může mít maximálně 70 znaků");
|
||||||
@@ -149,7 +161,7 @@ router.post("/updateNote", async (req: Request<{}, any, UpdateNoteData["body"]>,
|
|||||||
}
|
}
|
||||||
date = getDateForWeekIndex(dayIndex);
|
date = getDateForWeekIndex(dayIndex);
|
||||||
}
|
}
|
||||||
const data = await updateNote(login, trusted, note, date);
|
const data = await updateNote(login, trusted, note, date, slot);
|
||||||
getWebsocket().emit("message", data);
|
getWebsocket().emit("message", data);
|
||||||
res.status(200).json(data);
|
res.status(200).json(data);
|
||||||
} catch (e: any) { next(e) }
|
} catch (e: any) { next(e) }
|
||||||
@@ -184,8 +196,9 @@ router.post("/jdemeObed", async (req, res, next) => {
|
|||||||
|
|
||||||
router.post("/updateBuyer", async (req, res, next) => {
|
router.post("/updateBuyer", async (req, res, next) => {
|
||||||
const login = getLogin(parseToken(req));
|
const login = getLogin(parseToken(req));
|
||||||
|
const slot = parseSlot(req.body ?? {});
|
||||||
try {
|
try {
|
||||||
const data = await updateBuyer(login);
|
const data = await updateBuyer(login, slot);
|
||||||
getWebsocket().emit("message", data);
|
getWebsocket().emit("message", data);
|
||||||
res.status(200).json({});
|
res.status(200).json({});
|
||||||
} catch (e: any) { next(e) }
|
} catch (e: any) { next(e) }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import express, { Request } from "express";
|
|||||||
import { getLogin } from "../auth";
|
import { getLogin } from "../auth";
|
||||||
import { parseToken } from "../utils";
|
import { parseToken } from "../utils";
|
||||||
import { getNotificationSettings, saveNotificationSettings } from "../notifikace";
|
import { getNotificationSettings, saveNotificationSettings } from "../notifikace";
|
||||||
import { subscribePush, unsubscribePush, getVapidPublicKey, findLoginByEndpoint } from "../pushReminder";
|
import { subscribePush, unsubscribePush, getVapidPublicKey } from "../pushReminder";
|
||||||
import { addChoice } from "../service";
|
import { addChoice } from "../service";
|
||||||
import { getWebsocket } from "../websocket";
|
import { getWebsocket } from "../websocket";
|
||||||
import { UpdateNotificationSettingsData } from "../../../types";
|
import { UpdateNotificationSettingsData } from "../../../types";
|
||||||
@@ -66,16 +66,12 @@ router.post("/push/unsubscribe", async (req, res, next) => {
|
|||||||
} catch (e: any) { next(e) }
|
} catch (e: any) { next(e) }
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Rychlá akce z push notifikace — nastaví volbu bez JWT (identita přes subscription endpoint). */
|
/** Rychlá akce z push notifikace — nastaví volbu bez JWT (identita přes login v payloadu). */
|
||||||
router.post("/push/quickChoice", async (req, res, next) => {
|
router.post("/push/quickChoice", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { endpoint } = req.body;
|
const { login } = req.body;
|
||||||
if (!endpoint) {
|
|
||||||
return res.status(400).json({ error: "Nebyl předán endpoint" });
|
|
||||||
}
|
|
||||||
const login = await findLoginByEndpoint(endpoint);
|
|
||||||
if (!login) {
|
if (!login) {
|
||||||
return res.status(404).json({ error: "Subscription nenalezena" });
|
return res.status(400).json({ error: "Nebyl předán login" });
|
||||||
}
|
}
|
||||||
const data = await addChoice(login, false, 'NEOBEDVAM', undefined, undefined);
|
const data = await addChoice(login, false, 'NEOBEDVAM', undefined, undefined);
|
||||||
getWebsocket().emit("message", data);
|
getWebsocket().emit("message", data);
|
||||||
|
|||||||
+66
-58
@@ -3,11 +3,16 @@ import getStorage from "./storage";
|
|||||||
import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova } from "./restaurants";
|
import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova } from "./restaurants";
|
||||||
import { getTodayMock } from "./mock";
|
import { getTodayMock } from "./mock";
|
||||||
import { removeAllUserPizzas } from "./pizza";
|
import { removeAllUserPizzas } from "./pizza";
|
||||||
import { ClientData, DepartureTime, LunchChoice, PizzaDayState, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types/gen/types.gen";
|
import { ClientData, DepartureTime, LunchChoice, MealSlot, PizzaDayState, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types/gen/types.gen";
|
||||||
|
|
||||||
const storage = getStorage();
|
const storage = getStorage();
|
||||||
const MENU_PREFIX = 'menu';
|
const MENU_PREFIX = 'menu';
|
||||||
|
|
||||||
|
function getDataKey(date: Date, slot?: string): string {
|
||||||
|
const base = formatDate(date);
|
||||||
|
return slot === 'extra' ? `${base}_extra` : base;
|
||||||
|
}
|
||||||
|
|
||||||
/** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */
|
/** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */
|
||||||
export function getToday(): Date {
|
export function getToday(): Date {
|
||||||
if (process.env.MOCK_DATA === 'true') {
|
if (process.env.MOCK_DATA === 'true') {
|
||||||
@@ -43,15 +48,18 @@ function getEmptyData(date?: Date): ClientData {
|
|||||||
/**
|
/**
|
||||||
* Vrátí veškerá klientská data pro předaný den, nebo aktuální den, pokud není předán.
|
* Vrátí veškerá klientská data pro předaný den, nebo aktuální den, pokud není předán.
|
||||||
*/
|
*/
|
||||||
export async function getData(date?: Date): Promise<ClientData> {
|
export async function getData(date?: Date, slot?: string): Promise<ClientData> {
|
||||||
const clientData = await getClientData(date);
|
const clientData = await getClientData(date, slot);
|
||||||
clientData.menus = {
|
if (slot !== 'extra') {
|
||||||
SLADOVNICKA: await getRestaurantMenu('SLADOVNICKA', date),
|
clientData.menus = {
|
||||||
// UMOTLIKU: await getRestaurantMenu('UMOTLIKU', date),
|
SLADOVNICKA: await getRestaurantMenu('SLADOVNICKA', date),
|
||||||
TECHTOWER: await getRestaurantMenu('TECHTOWER', date),
|
// UMOTLIKU: await getRestaurantMenu('UMOTLIKU', date),
|
||||||
ZASTAVKAUMICHALA: await getRestaurantMenu('ZASTAVKAUMICHALA', date),
|
TECHTOWER: await getRestaurantMenu('TECHTOWER', date),
|
||||||
SENKSERIKOVA: await getRestaurantMenu('SENKSERIKOVA', date),
|
ZASTAVKAUMICHALA: await getRestaurantMenu('ZASTAVKAUMICHALA', date),
|
||||||
|
SENKSERIKOVA: await getRestaurantMenu('SENKSERIKOVA', date),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (slot === 'extra') clientData.slot = MealSlot.EXTRA;
|
||||||
return clientData;
|
return clientData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,8 +289,8 @@ function generateMenuWarnings(menu: RestaurantDayMenu, now: number): string[] {
|
|||||||
*
|
*
|
||||||
* @param date datum
|
* @param date datum
|
||||||
*/
|
*/
|
||||||
export async function initIfNeeded(date?: Date) {
|
export async function initIfNeeded(date?: Date, slot?: string) {
|
||||||
const usedDate = formatDate(date ?? getToday());
|
const usedDate = getDataKey(date ?? getToday(), slot);
|
||||||
const hasData = await storage.hasData(usedDate);
|
const hasData = await storage.hasData(usedDate);
|
||||||
if (!hasData) {
|
if (!hasData) {
|
||||||
await storage.setData(usedDate, getEmptyData(date || getToday()));
|
await storage.setData(usedDate, getEmptyData(date || getToday()));
|
||||||
@@ -298,9 +306,9 @@ export async function initIfNeeded(date?: Date) {
|
|||||||
* @param date datum, ke kterému se volba vztahuje
|
* @param date datum, ke kterému se volba vztahuje
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function removeChoices(login: string, trusted: boolean, locationKey: LunchChoice, date?: Date) {
|
export async function removeChoices(login: string, trusted: boolean, locationKey: LunchChoice, date?: Date, slot?: string) {
|
||||||
const selectedDay = formatDate(date ?? getToday());
|
const selectedDay = getDataKey(date ?? getToday(), slot);
|
||||||
let data = await getClientData(date);
|
let data = await getClientData(date, slot);
|
||||||
validateTrusted(data, login, trusted);
|
validateTrusted(data, login, trusted);
|
||||||
if (locationKey in data.choices) {
|
if (locationKey in data.choices) {
|
||||||
if (data.choices[locationKey] && login in data.choices[locationKey]) {
|
if (data.choices[locationKey] && login in data.choices[locationKey]) {
|
||||||
@@ -325,9 +333,9 @@ export async function removeChoices(login: string, trusted: boolean, locationKey
|
|||||||
* @param date datum, ke kterému se volba vztahuje
|
* @param date datum, ke kterému se volba vztahuje
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function removeChoice(login: string, trusted: boolean, locationKey: LunchChoice, foodIndex: number, date?: Date) {
|
export async function removeChoice(login: string, trusted: boolean, locationKey: LunchChoice, foodIndex: number, date?: Date, slot?: string) {
|
||||||
const selectedDay = formatDate(date ?? getToday());
|
const selectedDay = getDataKey(date ?? getToday(), slot);
|
||||||
let data = await getClientData(date);
|
let data = await getClientData(date, slot);
|
||||||
validateTrusted(data, login, trusted);
|
validateTrusted(data, login, trusted);
|
||||||
if (locationKey in data.choices) {
|
if (locationKey in data.choices) {
|
||||||
if (data.choices[locationKey] && login in data.choices[locationKey]) {
|
if (data.choices[locationKey] && login in data.choices[locationKey]) {
|
||||||
@@ -348,9 +356,9 @@ export async function removeChoice(login: string, trusted: boolean, locationKey:
|
|||||||
* @param date datum, ke kterému se volby vztahují
|
* @param date datum, ke kterému se volby vztahují
|
||||||
* @param ignoredLocationKey volba, která nebude odstraněna, pokud existuje
|
* @param ignoredLocationKey volba, která nebude odstraněna, pokud existuje
|
||||||
*/
|
*/
|
||||||
async function removeChoiceIfPresent(login: string, date?: Date, ignoredLocationKey?: LunchChoice) {
|
async function removeChoiceIfPresent(login: string, date?: Date, ignoredLocationKey?: LunchChoice, slot?: string) {
|
||||||
const usedDate = date ?? getToday();
|
const usedDate = date ?? getToday();
|
||||||
let data = await getClientData(usedDate);
|
let data = await getClientData(usedDate, slot);
|
||||||
for (const key of Object.keys(data.choices)) {
|
for (const key of Object.keys(data.choices)) {
|
||||||
const locationKey = key as LunchChoice;
|
const locationKey = key as LunchChoice;
|
||||||
if (ignoredLocationKey != null && ignoredLocationKey == locationKey) {
|
if (ignoredLocationKey != null && ignoredLocationKey == locationKey) {
|
||||||
@@ -361,7 +369,7 @@ async function removeChoiceIfPresent(login: string, date?: Date, ignoredLocation
|
|||||||
if (Object.keys(data.choices[locationKey]).length === 0) {
|
if (Object.keys(data.choices[locationKey]).length === 0) {
|
||||||
delete data.choices[locationKey];
|
delete data.choices[locationKey];
|
||||||
}
|
}
|
||||||
await storage.setData(formatDate(usedDate), data);
|
await storage.setData(getDataKey(usedDate, slot), data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
@@ -400,41 +408,43 @@ function validateTrusted(data: ClientData, login: string, trusted: boolean) {
|
|||||||
* @param date datum, ke kterému se volba vztahuje
|
* @param date datum, ke kterému se volba vztahuje
|
||||||
* @returns aktuální data
|
* @returns aktuální data
|
||||||
*/
|
*/
|
||||||
export async function addChoice(login: string, trusted: boolean, locationKey: LunchChoice, foodIndex?: number, date?: Date) {
|
export async function addChoice(login: string, trusted: boolean, locationKey: LunchChoice, foodIndex?: number, date?: Date, slot?: string) {
|
||||||
const usedDate = date ?? getToday();
|
const usedDate = date ?? getToday();
|
||||||
await initIfNeeded(usedDate);
|
await initIfNeeded(usedDate, slot);
|
||||||
let data = await getClientData(usedDate);
|
let data = await getClientData(usedDate, slot);
|
||||||
validateTrusted(data, login, trusted);
|
validateTrusted(data, login, trusted);
|
||||||
await validateFoodIndex(locationKey, foodIndex, date);
|
await validateFoodIndex(locationKey, foodIndex, date);
|
||||||
|
|
||||||
// Pokud uživatel měl vybranou PIZZA a mění na něco jiného
|
if (!slot || slot === 'obed') {
|
||||||
const hadPizzaChoice = data.choices.PIZZA && login in data.choices.PIZZA;
|
// Pokud uživatel měl vybranou PIZZA a mění na něco jiného
|
||||||
if (hadPizzaChoice && locationKey !== LunchChoice.PIZZA && foodIndex == null) {
|
const hadPizzaChoice = data.choices.PIZZA && login in data.choices.PIZZA;
|
||||||
// Kontrola, zda existuje Pizza day a uživatel je jeho zakladatel
|
if (hadPizzaChoice && locationKey !== LunchChoice.PIZZA && foodIndex == null) {
|
||||||
if (data.pizzaDay && data.pizzaDay.creator === login) {
|
// Kontrola, zda existuje Pizza day a uživatel je jeho zakladatel
|
||||||
// Pokud Pizza day není ve stavu CREATED, nelze změnit volbu
|
if (data.pizzaDay && data.pizzaDay.creator === login) {
|
||||||
if (data.pizzaDay.state !== PizzaDayState.CREATED) {
|
// Pokud Pizza day není ve stavu CREATED, nelze změnit volbu
|
||||||
throw new PizzaDayConflictError(
|
if (data.pizzaDay.state !== PizzaDayState.CREATED) {
|
||||||
`Nelze změnit volbu. Pizza day je ve stavu "${data.pizzaDay.state}" a musí být nejprve dokončen nebo smazán.`
|
throw new PizzaDayConflictError(
|
||||||
);
|
`Nelze změnit volbu. Pizza day je ve stavu "${data.pizzaDay.state}" a musí být nejprve dokončen nebo smazán.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Pizza day je ve stavu CREATED - bude smazán frontendem po potvrzení uživatelem
|
||||||
|
// (frontend volá nejprve deletePizzaDay, pak teprve addChoice)
|
||||||
}
|
}
|
||||||
// Pizza day je ve stavu CREATED - bude smazán frontendem po potvrzení uživatelem
|
|
||||||
// (frontend volá nejprve deletePizzaDay, pak teprve addChoice)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Smažeme pizzy uživatele (pokud Pizza day nebyl založen tímto uživatelem,
|
// Smažeme pizzy uživatele (pokud Pizza day nebyl založen tímto uživatelem,
|
||||||
// nebo byl již smazán frontendem)
|
// nebo byl již smazán frontendem)
|
||||||
await removeAllUserPizzas(login, usedDate);
|
await removeAllUserPizzas(login, usedDate);
|
||||||
// Znovu načteme data, protože removeAllUserPizzas je upravila
|
// Znovu načteme data, protože removeAllUserPizzas je upravila
|
||||||
data = await getClientData(usedDate);
|
data = await getClientData(usedDate, slot);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pokud měníme pouze lokaci, mažeme případné předchozí
|
// Pokud měníme pouze lokaci, mažeme případné předchozí
|
||||||
if (foodIndex == null) {
|
if (foodIndex == null) {
|
||||||
data = await removeChoiceIfPresent(login, usedDate);
|
data = await removeChoiceIfPresent(login, usedDate, undefined, slot);
|
||||||
} else {
|
} else {
|
||||||
// Mažeme případné ostatní volby (měla by být maximálně jedna)
|
// Mažeme případné ostatní volby (měla by být maximálně jedna)
|
||||||
data = await removeChoiceIfPresent(login, usedDate, locationKey);
|
data = await removeChoiceIfPresent(login, usedDate, locationKey, slot);
|
||||||
}
|
}
|
||||||
// TODO vytáhnout inicializaci "prázdné struktury" do vlastní funkce
|
// TODO vytáhnout inicializaci "prázdné struktury" do vlastní funkce
|
||||||
data.choices[locationKey] ??= {};
|
data.choices[locationKey] ??= {};
|
||||||
@@ -450,8 +460,7 @@ export async function addChoice(login: string, trusted: boolean, locationKey: Lu
|
|||||||
if (foodIndex != null && !data.choices[locationKey][login].selectedFoods?.includes(foodIndex)) {
|
if (foodIndex != null && !data.choices[locationKey][login].selectedFoods?.includes(foodIndex)) {
|
||||||
data.choices[locationKey][login].selectedFoods?.push(foodIndex);
|
data.choices[locationKey][login].selectedFoods?.push(foodIndex);
|
||||||
}
|
}
|
||||||
const selectedDate = formatDate(usedDate);
|
await storage.setData(getDataKey(usedDate, slot), data);
|
||||||
await storage.setData(selectedDate, data);
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,10 +498,10 @@ async function validateFoodIndex(locationKey: LunchChoice, foodIndex?: number, d
|
|||||||
* @param note poznámka
|
* @param note poznámka
|
||||||
* @param date datum, ke kterému se volba vztahuje
|
* @param date datum, ke kterému se volba vztahuje
|
||||||
*/
|
*/
|
||||||
export async function updateNote(login: string, trusted: boolean, note?: string, date?: Date) {
|
export async function updateNote(login: string, trusted: boolean, note?: string, date?: Date, slot?: string) {
|
||||||
const usedDate = date ?? getToday();
|
const usedDate = date ?? getToday();
|
||||||
await initIfNeeded(usedDate);
|
await initIfNeeded(usedDate, slot);
|
||||||
let data = await getClientData(usedDate);
|
let data = await getClientData(usedDate, slot);
|
||||||
validateTrusted(data, login, trusted);
|
validateTrusted(data, login, trusted);
|
||||||
const userEntry = data.choices != null && Object.entries(data.choices).find(entry => entry[1][login] != null);
|
const userEntry = data.choices != null && Object.entries(data.choices).find(entry => entry[1][login] != null);
|
||||||
if (userEntry) {
|
if (userEntry) {
|
||||||
@@ -501,8 +510,7 @@ export async function updateNote(login: string, trusted: boolean, note?: string,
|
|||||||
} else {
|
} else {
|
||||||
userEntry[1][login].note = note;
|
userEntry[1][login].note = note;
|
||||||
}
|
}
|
||||||
const selectedDate = formatDate(usedDate);
|
await storage.setData(getDataKey(usedDate, slot), data);
|
||||||
await storage.setData(selectedDate, data);
|
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -514,9 +522,9 @@ export async function updateNote(login: string, trusted: boolean, note?: string,
|
|||||||
* @param time preferovaný čas odchodu
|
* @param time preferovaný čas odchodu
|
||||||
* @param date datum, ke kterému se čas vztahuje
|
* @param date datum, ke kterému se čas vztahuje
|
||||||
*/
|
*/
|
||||||
export async function updateDepartureTime(login: string, time?: string, date?: Date) {
|
export async function updateDepartureTime(login: string, time?: string, date?: Date, slot?: string) {
|
||||||
const usedDate = date ?? getToday();
|
const usedDate = date ?? getToday();
|
||||||
let clientData = await getClientData(usedDate);
|
let clientData = await getClientData(usedDate, slot);
|
||||||
const found = Object.values(clientData.choices).find(location => login in location);
|
const found = Object.values(clientData.choices).find(location => login in location);
|
||||||
// TODO validace, že se jedná o restauraci
|
// TODO validace, že se jedná o restauraci
|
||||||
if (found) {
|
if (found) {
|
||||||
@@ -528,7 +536,7 @@ export async function updateDepartureTime(login: string, time?: string, date?: D
|
|||||||
}
|
}
|
||||||
found[login].departureTime = time;
|
found[login].departureTime = time;
|
||||||
}
|
}
|
||||||
await storage.setData(formatDate(usedDate), clientData);
|
await storage.setData(getDataKey(usedDate, slot), clientData);
|
||||||
}
|
}
|
||||||
return clientData;
|
return clientData;
|
||||||
}
|
}
|
||||||
@@ -539,15 +547,15 @@ export async function updateDepartureTime(login: string, time?: string, date?: D
|
|||||||
*
|
*
|
||||||
* @param login přihlašovací jméno uživatele
|
* @param login přihlašovací jméno uživatele
|
||||||
*/
|
*/
|
||||||
export async function updateBuyer(login: string) {
|
export async function updateBuyer(login: string, slot?: string) {
|
||||||
const usedDate = getToday();
|
const usedDate = getToday();
|
||||||
let clientData = await getClientData(usedDate);
|
let clientData = await getClientData(usedDate, slot);
|
||||||
const userEntry = clientData.choices?.['OBJEDNAVAM']?.[login];
|
const userEntry = clientData.choices?.['OBJEDNAVAM']?.[login];
|
||||||
if (!userEntry) {
|
if (!userEntry) {
|
||||||
throw new Error("Nelze nastavit objednatele pro uživatele s jinou volbou než \"Budu objednávat\"");
|
throw new Error("Nelze nastavit objednatele pro uživatele s jinou volbou než \"Budu objednávat\"");
|
||||||
}
|
}
|
||||||
userEntry.isBuyer = !(userEntry.isBuyer || false);
|
userEntry.isBuyer = !(userEntry.isBuyer || false);
|
||||||
await storage.setData(formatDate(usedDate), clientData);
|
await storage.setData(getDataKey(usedDate, slot), clientData);
|
||||||
return clientData;
|
return clientData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,9 +565,9 @@ export async function updateBuyer(login: string) {
|
|||||||
* @param date datum pro který vrátit data, pokud není vyplněno, je použit dnešní den
|
* @param date datum pro který vrátit data, pokud není vyplněno, je použit dnešní den
|
||||||
* @returns data pro klienta
|
* @returns data pro klienta
|
||||||
*/
|
*/
|
||||||
export async function getClientData(date?: Date): Promise<ClientData> {
|
export async function getClientData(date?: Date, slot?: string): Promise<ClientData> {
|
||||||
const targetDate = date ?? getToday();
|
const targetDate = date ?? getToday();
|
||||||
const dateString = formatDate(targetDate);
|
const dateString = getDataKey(targetDate, slot);
|
||||||
const clientData = await storage.getData<ClientData>(dateString) || getEmptyData(date);
|
const clientData = await storage.getData<ClientData>(dateString) || getEmptyData(date);
|
||||||
return {
|
return {
|
||||||
...clientData,
|
...clientData,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ post:
|
|||||||
$ref: "../../schemas/_index.yml#/DayIndex"
|
$ref: "../../schemas/_index.yml#/DayIndex"
|
||||||
foodIndex:
|
foodIndex:
|
||||||
$ref: "../../schemas/_index.yml#/FoodIndex"
|
$ref: "../../schemas/_index.yml#/FoodIndex"
|
||||||
|
slot:
|
||||||
|
$ref: "../../schemas/_index.yml#/MealSlot"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ post:
|
|||||||
$ref: "../../schemas/_index.yml#/LunchChoice"
|
$ref: "../../schemas/_index.yml#/LunchChoice"
|
||||||
dayIndex:
|
dayIndex:
|
||||||
$ref: "../../schemas/_index.yml#/DayIndex"
|
$ref: "../../schemas/_index.yml#/DayIndex"
|
||||||
|
slot:
|
||||||
|
$ref: "../../schemas/_index.yml#/MealSlot"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ post:
|
|||||||
$ref: "../../schemas/_index.yml#/LunchChoice"
|
$ref: "../../schemas/_index.yml#/LunchChoice"
|
||||||
dayIndex:
|
dayIndex:
|
||||||
$ref: "../../schemas/_index.yml#/DayIndex"
|
$ref: "../../schemas/_index.yml#/DayIndex"
|
||||||
|
slot:
|
||||||
|
$ref: "../../schemas/_index.yml#/MealSlot"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
post:
|
post:
|
||||||
operationId: setBuyer
|
operationId: setBuyer
|
||||||
summary: Nastavení/odnastavení aktuálně přihlášeného uživatele jako objednatele pro stav "Budu objednávat" pro aktuální den.
|
summary: Nastavení/odnastavení aktuálně přihlášeného uživatele jako objednatele pro stav "Budu objednávat" pro aktuální den.
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
slot:
|
||||||
|
$ref: "../../schemas/_index.yml#/MealSlot"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Stav byl úspěšně změněn.
|
description: Stav byl úspěšně změněn.
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ post:
|
|||||||
$ref: "../../schemas/_index.yml#/DayIndex"
|
$ref: "../../schemas/_index.yml#/DayIndex"
|
||||||
note:
|
note:
|
||||||
type: string
|
type: string
|
||||||
|
slot:
|
||||||
|
$ref: "../../schemas/_index.yml#/MealSlot"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ get:
|
|||||||
type: integer
|
type: integer
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 4
|
maximum: 4
|
||||||
|
- in: query
|
||||||
|
name: slot
|
||||||
|
description: Slot jídla. Pokud není předán, je použit výchozí slot (oběd).
|
||||||
|
schema:
|
||||||
|
$ref: "../schemas/_index.yml#/MealSlot"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
$ref: "../api.yml#/components/responses/ClientDataResponse"
|
$ref: "../api.yml#/components/responses/ClientDataResponse"
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ ClientData:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/PendingQr"
|
$ref: "#/PendingQr"
|
||||||
|
slot:
|
||||||
|
description: Slot jídla, ke kterému se tato data vztahují
|
||||||
|
$ref: "#/MealSlot"
|
||||||
|
|
||||||
# --- OBĚDY ---
|
# --- OBĚDY ---
|
||||||
UserLunchChoice:
|
UserLunchChoice:
|
||||||
@@ -130,6 +133,15 @@ LunchChoice:
|
|||||||
- OBJEDNAVAM
|
- OBJEDNAVAM
|
||||||
- NEOBEDVAM
|
- NEOBEDVAM
|
||||||
- ROZHODUJI
|
- ROZHODUJI
|
||||||
|
MealSlot:
|
||||||
|
description: Slot jídla - oběd nebo extra jídlo (večeře, pozdní oběd)
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- obed
|
||||||
|
- extra
|
||||||
|
x-enum-varnames:
|
||||||
|
- OBED
|
||||||
|
- EXTRA
|
||||||
DayIndex:
|
DayIndex:
|
||||||
description: Index dne v týdnu (0 = pondělí, 4 = pátek)
|
description: Index dne v týdnu (0 = pondělí, 4 = pátek)
|
||||||
type: integer
|
type: integer
|
||||||
|
|||||||
Reference in New Issue
Block a user