Compare commits

...

2 Commits

Author SHA1 Message Date
2e8774900f
feat: Základ generování QR kódů 2025-12-02 11:46:52 +01:00
0179afca75
feat: část refaktoru databáze 2025-11-25 13:42:06 +01:00
13 changed files with 457 additions and 36 deletions

View File

@ -15,7 +15,7 @@ import { useSettings } from './context/settings';
import Footer from './components/Footer';
import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons';
import Loader from './components/Loader';
import { getHumanDateTime, isInTheFuture } from './Utils';
import { getDayOfWeekIndex, getHumanDate, getHumanDateTime, getIsWeekend, isInTheFuture } from './Utils';
import NoteModal from './components/modals/NoteModal';
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 } from '../../types';
@ -71,7 +71,10 @@ function App() {
const departureChoiceRef = useRef<HTMLSelectElement>(null);
const pizzaPoznamkaRef = useRef<HTMLInputElement>(null);
const [failure, setFailure] = useState<boolean>(false);
const [dayIndex, setDayIndex] = useState<number>();
const [dayIndex, setDayIndex] = useState<number>(); // Index zobrazovaného dne
// TODO berka zde je nutné dořešit mocking pro testování
const [todayDayIndex, setTodayDayIndex] = useState<number>(getDayOfWeekIndex(new Date())); // Index dnešního dne
const [isTodayWeekend, setIsTodayWeekend] = useState<boolean>(getIsWeekend(new Date()));
const [loadingPizzaDay, setLoadingPizzaDay] = useState<boolean>(false);
const [noteModalOpen, setNoteModalOpen] = useState<boolean>(false);
const [eggImage, setEggImage] = useState<Blob>();
@ -89,8 +92,9 @@ function App() {
const data = response.data
if (data) {
setData(data);
setDayIndex(data.dayIndex);
dayIndexRef.current = data.dayIndex;
const dayIndex = getDayOfWeekIndex(new Date(data.date));
setDayIndex(dayIndex);
dayIndexRef.current = dayIndex;
setFood(data.menus);
}
}).catch(e => {
@ -103,6 +107,8 @@ function App() {
if (!auth?.login) {
return
}
setTodayDayIndex(getDayOfWeekIndex(new Date()));
setIsTodayWeekend(getIsWeekend(new Date()));
getData({ query: { dayIndex: dayIndex } }).then(response => {
const data = response.data;
setData(data);
@ -125,7 +131,7 @@ function App() {
socket.on(EVENT_MESSAGE, (newData: ClientData) => {
// console.log("Přijata nová data ze socketu", newData);
// 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 || getDayOfWeekIndex(new Date(newData.date)) === dayIndexRef.current) {
setData(newData);
}
});
@ -439,16 +445,16 @@ function App() {
}
const noOrders = data?.pizzaDay?.orders?.length === 0;
const canChangeChoice = dayIndex == null || data.todayDayIndex == null || dayIndex >= data.todayDayIndex;
const canChangeChoice = dayIndex == null || dayIndex >= todayDayIndex;
const { path, url, startOffset, endOffset, duration, ...style } = easterEgg || {};
return (
<div className="app-container">
{easterEgg && eggImage && <img ref={eggRef} alt='' src={URL.createObjectURL(eggImage)} style={{ position: 'absolute', ...EASTER_EGG_STYLE, ...style, animationDuration: `${duration ?? EASTER_EGG_DEFAULT_DURATION}s` }} />}
<Header />
<Header dayIndex={dayIndex} />
<div className='wrapper'>
{data.isWeekend ? <h4>Užívejte víkend :)</h4> : <>
{isTodayWeekend ? <h4>Užívejte víkend :)</h4> : <>
<Alert variant={'primary'}>
{/* <img alt="" src='hat.png' style={{ position: "absolute", width: "70px", rotate: "-45deg", left: -40, top: -58 }} />
<img alt="" src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} /> */}
@ -464,7 +470,7 @@ function App() {
<span title='Předchozí den'>
<FontAwesomeIcon icon={faChevronLeft} style={{ cursor: "pointer", visibility: dayIndex > 0 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex - 1)} />
</span>
<h1 className='title' style={{ color: dayIndex === data.todayDayIndex ? 'black' : 'gray' }}>{data.date}</h1>
<h1 className='title' style={{ color: dayIndex === todayDayIndex ? 'black' : 'gray' }}>{getHumanDate(new Date(data.date))}</h1>
<span title="Následující den">
<FontAwesomeIcon icon={faChevronRight} style={{ cursor: "pointer", visibility: dayIndex < 4 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex + 1)} />
</span>
@ -480,7 +486,7 @@ function App() {
<div className='content-wrapper'>
<div className='content'>
{canChangeChoice && <>
<p>{`Jak to ${dayIndex == null || dayIndex === data.todayDayIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}</p>
<p>{`Jak to ${dayIndex == null || dayIndex === todayDayIndex ? 'dnes' : 'tento den'} vidíš s obědem?`}</p>
<Form.Select ref={choiceRef} onChange={doAddChoice}>
<option></option>
{Object.entries(LunchChoice)
@ -589,7 +595,7 @@ function App() {
: <div className='mt-5'><i>Zatím nikdo nehlasoval...</i></div>
}
</div>
{dayIndex === data.todayDayIndex &&
{dayIndex === todayDayIndex &&
<div className='mt-5'>
{!data.pizzaDay &&
<div style={{ textAlign: 'center' }}>

View File

@ -73,6 +73,12 @@ export const getDayOfWeekIndex = (date: Date) => {
return (((date.getDay() - 1) % 7) + 7) % 7;
}
/** Vrátí true, pokud je předané datum o víkendu. */
export function getIsWeekend(date: Date) {
const index = getDayOfWeekIndex(date);
return index == 5 || index == 6;
}
/** Vrátí první pracovní den v týdnu předaného data. */
export function getFirstWorkDayOfWeek(date: Date) {
const firstDay = new Date(date.getTime());

View File

@ -6,11 +6,16 @@ import { useSettings } from "../context/settings";
import FeaturesVotingModal from "./modals/FeaturesVotingModal";
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
import RefreshMenuModal from "./modals/RefreshMenuModal";
import GenerateQRModal from "./modals/GenerateQRModal";
import { useNavigate } from "react-router";
import { STATS_URL } from "../AppRoutes";
import { FeatureRequest, getVotes, updateVote } from "../../../types";
export default function Header() {
type Props = {
dayIndex?: number;
}
export default function Header({ dayIndex }: Readonly<Props>) {
const auth = useAuth();
const settings = useSettings();
const navigate = useNavigate();
@ -18,6 +23,7 @@ export default function Header() {
const [votingModalOpen, setVotingModalOpen] = useState<boolean>(false);
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
const [refreshMenuModalOpen, setRefreshMenuModalOpen] = useState<boolean>(false);
const [generateQRModalOpen, setGenerateQRModalOpen] = useState<boolean>(false);
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[] | undefined>([]);
useEffect(() => {
@ -44,6 +50,10 @@ export default function Header() {
setRefreshMenuModalOpen(false);
}
const closeGenerateQRModal = () => {
setGenerateQRModalOpen(false);
}
const isValidInteger = (str: string) => {
str = str.trim();
if (!str) {
@ -121,6 +131,7 @@ export default function Header() {
<NavDropdown align="end" title={auth?.login} id="basic-nav-dropdown">
<NavDropdown.Item onClick={() => setSettingsModalOpen(true)}>Nastavení</NavDropdown.Item>
<NavDropdown.Item onClick={() => setRefreshMenuModalOpen(true)}>Přenačtení menu</NavDropdown.Item>
<NavDropdown.Item onClick={() => setGenerateQRModalOpen(true)}>Generování QR</NavDropdown.Item>
<NavDropdown.Item onClick={() => setVotingModalOpen(true)}>Hlasovat o nových funkcích</NavDropdown.Item>
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
@ -131,6 +142,7 @@ export default function Header() {
</Navbar.Collapse>
<SettingsModal isOpen={settingsModalOpen} onClose={closeSettingsModal} onSave={saveSettings} />
<RefreshMenuModal isOpen={refreshMenuModalOpen} onClose={closeRefreshMenuModal} />
<GenerateQRModal isOpen={generateQRModalOpen} onClose={closeGenerateQRModal} dayIndex={dayIndex} bankAccount={settings?.bankAccount} bankAccountHolder={settings?.holderName} />
<FeaturesVotingModal isOpen={votingModalOpen} onClose={closeVotingModal} onChange={saveFeatureVote} initialValues={featureVotes} />
<PizzaCalculatorModal isOpen={pizzaModalOpen} onClose={closePizzaModal} />
</Navbar>

View File

@ -0,0 +1,176 @@
import { useEffect, useState } from "react";
import { Modal, Button, Table, Form, Alert } from "react-bootstrap";
import { ClientData, generateQr, getData } from "../../../../types";
type Props = {
isOpen: boolean,
onClose: () => void,
dayIndex?: number,
bankAccount?: string,
bankAccountHolder?: string,
}
type UserQRData = {
login: string;
selected: boolean;
note: string;
amount: string;
}
/** Modální dialog pro generování QR kódů. */
export default function GenerateQRModal({ isOpen, onClose, dayIndex, bankAccount, bankAccountHolder }: Readonly<Props>) {
const [users, setUsers] = useState<UserQRData[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const isBankDataValid = bankAccount && bankAccountHolder;
useEffect(() => {
if (isOpen) {
setLoading(true);
getData({ query: { dayIndex } }).then(response => {
const data: ClientData = response.data;
const userList: UserQRData[] = [];
// Projdeme všechny volby stravování a získáme uživatele
if (data.choices) {
Object.entries(data.choices).forEach(([locationKey, locationUsers]) => {
Object.keys(locationUsers).forEach(login => {
// Přidáme uživatele pouze pokud tam ještě není
if (!userList.find(u => u.login === login)) {
userList.push({
login,
selected: false,
note: '',
amount: ''
});
}
});
});
}
setUsers(userList);
setLoading(false);
}).catch(() => {
setLoading(false);
});
}
}, [isOpen, dayIndex]);
const handleCheckboxChange = (login: string) => {
setUsers(users.map(u =>
u.login === login ? { ...u, selected: !u.selected } : u
));
};
const handleNoteChange = (login: string, note: string) => {
setUsers(users.map(u =>
u.login === login ? { ...u, note } : u
));
};
const handleAmountChange = (login: string, amount: string) => {
setUsers(users.map(u =>
u.login === login ? { ...u, amount } : u
));
};
const handleGenerate = async () => {
const selectedUsers = users.filter(u => u.selected);
// TODO: Implementovat generování QR kódů
console.log('Generování QR pro:', selectedUsers);
alert('Funkce generování QR bude implementována');
await generateQr({
body: {
bankAccount: bankAccount!,
bankAccountHolder: bankAccountHolder!,
qrCodes: selectedUsers.map(u => ({
login: u.login,
des: u.note,
amount: Number.parseFloat(u.amount)
}))
},
})
};
const handleClose = () => {
setUsers([]);
onClose();
};
return (
<Modal show={isOpen} onHide={handleClose} size="lg">
<Modal.Header closeButton>
<Modal.Title><h2>Generování QR kódů</h2></Modal.Title>
</Modal.Header>
<Modal.Body>
{!isBankDataValid && (
<Alert variant="warning">
<strong>Upozornění:</strong> Pro generování QR kódů je nutné mít v nastavení vyplněné číslo bankovního účtu a jméno majitele účtu.
</Alert>
)}
{loading ? (
<p>Načítání uživatelů...</p>
) : users.length === 0 ? (
<p>Pro aktuální den nemá žádný uživatel vybranou volbu stravování.</p>
) : (
<Table striped bordered hover>
<thead>
<tr>
<th style={{ width: '50px' }}></th>
<th>Uživatel</th>
<th>Poznámka</th>
<th style={{ width: '120px' }}>Částka ()</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.login}>
<td className="text-center">
<Form.Check
type="checkbox"
checked={user.selected}
onChange={() => handleCheckboxChange(user.login)}
/>
</td>
<td>{user.login}</td>
<td>
<Form.Control
type="text"
value={user.note}
onChange={(e) => handleNoteChange(user.login, e.target.value)}
placeholder="Poznámka"
disabled={!user.selected}
/>
</td>
<td>
<Form.Control
type="number"
step="0.01"
min="0"
value={user.amount}
onChange={(e) => handleAmountChange(user.login, e.target.value)}
placeholder="0.00"
disabled={!user.selected}
/>
</td>
</tr>
))}
</tbody>
</Table>
)}
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}>
Zavřít
</Button>
<Button
variant="primary"
onClick={handleGenerate}
disabled={users.filter(u => u.selected).length === 0 || !isBankDataValid}
>
Generovat
</Button>
</Modal.Footer>
</Modal>
);
}

View File

@ -103,7 +103,7 @@ export default function StatsPage() {
return (
<>
<Header />
<Header dayIndex={undefined} />
<div className="stats-page">
<h1>Statistiky</h1>
<div className="week-navigator">

View File

@ -13,6 +13,8 @@ import foodRoutes, { refreshMetoda } from "./routes/foodRoutes";
import votingRoutes from "./routes/votingRoutes";
import easterEggRoutes from "./routes/easterEggRoutes";
import statsRoutes from "./routes/statsRoutes";
import debugRoutes from "./routes/debugRoutes";
import qrRoutes from "./routes/qrRoutes";
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
@ -99,6 +101,8 @@ app.get("/api/qr", (req, res) => {
// Přeskočení auth pro refresh dat xd
app.use("/api/food/refresh", refreshMetoda);
app.use("/api/debug", debugRoutes);
/** Middleware ověřující JWT token */
app.use("/api/", (req, res, next) => {
if (HTTP_REMOTE_USER_ENABLED) {
@ -143,6 +147,7 @@ app.use("/api/food", foodRoutes);
app.use("/api/voting", votingRoutes);
app.use("/api/easterEggs", easterEggRoutes);
app.use("/api/stats", statsRoutes);
app.use("/api/qr", qrRoutes);
app.use('/stats', express.static('public'));
app.use(express.static('public'));

View File

@ -517,6 +517,24 @@ const MOCK_DATA = {
name: "Pečené vepřové koleno, křen, hořčice, chléb",
price: "320\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Slovácké strapačky s uzenou slaninou, zelím, mletým pepřem & sekanou petrželkou",
price: "140\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Hovězí guláš s vejcem, zeleninovou garniturkou & žemlovými knedlíky",
price: "145\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Kuřecí roláda s kaštanovou nádivkou, demi-glace & smetanovou bramborovou kaší",
price: "150\xA0Kč",
isSoup: false,
}
],
[
@ -531,6 +549,24 @@ const MOCK_DATA = {
name: "Poutine (trhané vepřové, hranolky, sýr, čalamáda, pikantní omáčka)",
price: "190\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Slovácké strapačky s uzenou slaninou, zelím, mletým pepřem & sekanou petrželkou",
price: "140\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Hovězí guláš s vejcem, zeleninovou garniturkou & žemlovými knedlíky",
price: "145\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Kuřecí roláda s kaštanovou nádivkou, demi-glace & smetanovou bramborovou kaší",
price: "150\xA0Kč",
isSoup: false,
}
],
[
@ -545,6 +581,24 @@ const MOCK_DATA = {
name: "Vepřový řízek z kotlety, domácí bramborový salát",
price: "170\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Slovácké strapačky s uzenou slaninou, zelím, mletým pepřem & sekanou petrželkou",
price: "140\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Hovězí guláš s vejcem, zeleninovou garniturkou & žemlovými knedlíky",
price: "145\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Kuřecí roláda s kaštanovou nádivkou, demi-glace & smetanovou bramborovou kaší",
price: "150\xA0Kč",
isSoup: false,
}
],
[
@ -559,6 +613,24 @@ const MOCK_DATA = {
name: "Burger z Chuck rollu, hranolky, tatarská omáčka",
price: "200\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Slovácké strapačky s uzenou slaninou, zelím, mletým pepřem & sekanou petrželkou",
price: "140\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Hovězí guláš s vejcem, zeleninovou garniturkou & žemlovými knedlíky",
price: "145\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Kuřecí roláda s kaštanovou nádivkou, demi-glace & smetanovou bramborovou kaší",
price: "150\xA0Kč",
isSoup: false,
}
],
],
@ -601,6 +673,18 @@ const MOCK_DATA = {
name: "Hovězí po Burgundsku, bramborová kaše",
price: "155\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Špagety s kuřecím masem, špenátem a smetanou",
price: "145\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Medailonky z vepřové panenky s fazolkami se slaninou, šťouchané brambory",
price: "185\xA0Kč",
isSoup: false,
}
],
[
@ -615,6 +699,18 @@ const MOCK_DATA = {
name: "Kuřecí plátky na sušených rajčatech, bylinkách a česneku, bramborová kaše",
price: "155\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Špagety s kuřecím masem, špenátem a smetanou",
price: "145\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Medailonky z vepřové panenky s fazolkami se slaninou, šťouchané brambory",
price: "185\xA0Kč",
isSoup: false,
}
],
[
@ -629,6 +725,18 @@ const MOCK_DATA = {
name: "Rajská s plněnou paprikou, knedlík",
price: "170\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Špagety s kuřecím masem, špenátem a smetanou",
price: "145\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Medailonky z vepřové panenky s fazolkami se slaninou, šťouchané brambory",
price: "185\xA0Kč",
isSoup: false,
}
],
[
@ -643,6 +751,18 @@ const MOCK_DATA = {
name: "Ragú z trhané kachny, onsen vejce, soté ze špenátu a ředkvičky, bramborové pyré, lanýžová sůl, zelený olej",
price: "189\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Špagety s kuřecím masem, špenátem a smetanou",
price: "145\xA0Kč",
isSoup: false,
},
{
amount: "-",
name: "Medailonky z vepřové panenky s fazolkami se slaninou, šťouchané brambory",
price: "185\xA0Kč",
isSoup: false,
}
],
],
@ -1402,7 +1522,7 @@ const MOCK_PIZZA_LIST = [
* Funkce vrací mock datu ve formátu YYYY-MM-DD
*/
export const getTodayMock = (): Date => {
return new Date('2025-01-10'); // pátek
return new Date('2025-01-08'); // středa
}
export const getMenuSladovnickaMock = () => {

View File

@ -0,0 +1,45 @@
import express, { Request } from "express";
import { addChoice, getData, removeChoices } from "../service";
import { ClientData, LunchChoice } from "../../../types";
const NAMES = ["alice", "bob", "carol", "dave", "eve", "frank", "grace", "heidi", "ivan", "judy"];
const DATES = ["2025-01-06", "2025-01-07", "2025-01-08", "2025-01-09", "2025-01-10"];
const router = express.Router();
router.get("/createUsers", async (req: Request<{}, any, any>, res) => {
for (const element of NAMES) {
for (const dateStr of DATES) {
// Se šancí 50 % přidat pro tohoto uživatele tento den náhodnou volbu
if (Math.random() > 0.5) {
const foodIndex = Math.floor(Math.random() * 3); // Předpokládáme, že jsou 3 možnosti jídla
const date = new Date(dateStr);
// Náhodná volba z LunchChoice
const lunchChoices = [
"SLADOVNICKA",
"TECHTOWER",
"ZASTAVKAUMICHALA",
"SENKSERIKOVA",
];
const randomLunchChoice = lunchChoices[Math.floor(Math.random() * lunchChoices.length)];
await addChoice(element, true, randomLunchChoice as LunchChoice, foodIndex, date);
}
}
}
res.status(200).json({});
});
router.get("/clearUsers", async (req: Request<{}, any, any>, res) => {
for (const dateStr of DATES) {
const date = new Date(dateStr);
const data: ClientData = await getData(date);
for (const user of NAMES) {
for (const locationKey in data.choices) {
await removeChoices(user, true, locationKey as keyof ClientData["choices"], date);
}
}
}
res.status(200).json({});
});
export default router;

View File

@ -0,0 +1,15 @@
import express, { Request, Response } from "express";
import { getLogin } from "../auth";
import { parseToken } from "../utils";
import { GenerateQrData } from "../../../types";
const router = express.Router();
router.post("/generate", async (req: Request<{}, any, GenerateQrData["body"]>, res: Response<any>) => {
getLogin(parseToken(req));
console.log("Bank account for QR codes:", req.body.bankAccount);
console.log("Bank account holder for QR codes:", req.body.bankAccountHolder);
console.log("Requested QR codes for users:", req.body.qrCodes);
});
export default router;

View File

@ -1,4 +1,4 @@
import { InsufficientPermissions, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getIsWeekend, getWeekNumber } from "./utils";
import { InsufficientPermissions, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getIsWeekend, getWeekNumber } from "./utils";
import getStorage from "./storage";
import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova } from "./restaurants";
import { getTodayMock } from "./mock";
@ -31,10 +31,7 @@ export const getDateForWeekIndex = (index: number) => {
function getEmptyData(date?: Date): ClientData {
const usedDate = date || getToday();
return {
todayDayIndex: getDayOfWeekIndex(getToday()),
date: getHumanDate(usedDate),
isWeekend: getIsWeekend(usedDate),
dayIndex: getDayOfWeekIndex(usedDate),
date: usedDate.toISOString().split('T')[0],
choices: {},
};
}
@ -486,9 +483,5 @@ export async function updateDepartureTime(login: string, time?: string, date?: D
export async function getClientData(date?: Date): Promise<ClientData> {
const targetDate = date ?? getToday();
const dateString = formatDate(targetDate);
const clientData = await storage.getData<ClientData>(dateString) || getEmptyData(date);
return {
...clientData,
todayDayIndex: getDayOfWeekIndex(getToday()),
}
return await storage.getData<ClientData>(dateString) || getEmptyData(date);
}

View File

@ -65,6 +65,10 @@ paths:
/voting/updateVote:
$ref: "./paths/voting/updateVote.yml"
# QR kódy (/api/qr)
/qr/generate:
$ref: "./paths/qr/generateQr.yml"
components:
schemas:
$ref: "./schemas/_index.yml"

View File

@ -0,0 +1,12 @@
post:
operationId: generateQr
summary: Generování QR kódů.
requestBody:
required: true
content:
application/json:
schema:
$ref: "../../schemas/_index.yml#/GenerateQrCodesRequest"
responses:
"200":
description: QR kódy byly úspěšně vygenerovány.

View File

@ -21,23 +21,13 @@ ClientData:
type: object
additionalProperties: false
required:
- todayDayIndex
- date
- isWeekend
- choices
properties:
todayDayIndex:
description: Index dnešního dne v týdnu
$ref: "#/DayIndex"
date:
description: Human-readable datum dne
description: Datum konkrétního dne
type: string
isWeekend:
description: Příznak, zda je tento den víkend
type: boolean
dayIndex:
description: Index dne v týdnu, ke kterému se vztahují tato data
$ref: "#/DayIndex"
format: date
choices:
$ref: "#/LunchChoices"
menus:
@ -479,6 +469,43 @@ PizzaDay:
items:
$ref: "#/PizzaOrder"
# --- QR KÓDY ---
QrCodeRequest:
description: Data potřebná pro vygenerování jednoho QR kódu pro platbu
type: object
required:
- login
- note
- amount
properties:
login:
description: Přihlašovací jméno uživatele, pro kterého bude QR kód vygenerován
type: string
note:
description: Popis platby
type: string
amount:
description: Částka platby v Kč
type: number
GenerateQrCodesRequest:
description: Data potřebná pro vygenerování QR kódů pro platbu
type: object
required:
- bankAccount
- bankAccountHolder
properties:
bankAccount:
description: Číslo bankovního účtu objednávajícího
type: string
bankAccountHolder:
description: Jméno majitele bankovního účtu
type: string
qrCodes:
description: Pole požadavků na vygenerování QR kódů
type: array
items:
$ref: "#/QrCodeRequest"
# --- NOTIFIKACE ---
UdalostEnum:
type: string