Compare commits
22 Commits
774be3df6d
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 916766450a | |||
| 5e596c3b99 | |||
| 3ba5fdd086 | |||
| 03f4e438a3 | |||
| b591411d10 | |||
|
8a588cf486
|
|||
|
0e4dc061b8
|
|||
|
7fd3ba0fc4
|
|||
| 94b8f0a452 | |||
| 3e6ecd4e6a | |||
| f12dc7b562 | |||
| 8aef00ab05 | |||
|
d91c8db49c
|
|||
| d8714b2086 | |||
| c7f78cf2c9 | |||
| 1efe2b8f7d | |||
| 5f03471541 | |||
| 21d7224fb4 | |||
| abc3d070cc | |||
| cca751752d | |||
| d2f45be2d3 | |||
| 936b33cc80 |
Vendored
+67
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "types: openapi-ts",
|
||||
"type": "shell",
|
||||
"command": "yarn openapi-ts",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/types"
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "silent",
|
||||
"panel": "dedicated"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "server: startReload",
|
||||
"type": "shell",
|
||||
"command": "yarn startReload",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/server",
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"group": "dev"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "client: vite",
|
||||
"type": "shell",
|
||||
"command": "yarn start",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/client"
|
||||
},
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"group": "dev"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "dev: server+client",
|
||||
"dependsOn": [
|
||||
"server: startReload",
|
||||
"client: vite"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "dev: all",
|
||||
"dependsOrder": "sequence",
|
||||
"dependsOn": [
|
||||
"types: openapi-ts",
|
||||
"dev: server+client"
|
||||
],
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
# TODO
|
||||
- [ ] HTTP_REMOTE_TRUSTED_IPS se nikde nevalidují, hlavičky jsou přijímány odkudkoli
|
||||
- [ ] V případě zapnutí přihlašování přes trusted headers nefunguje standardní přihlášení (nevrátí žádnou odpověď)
|
||||
- [ ] Nemělo by se jít dostat na přihlašovací formulář (měla by tam být nanejvýš hláška nebo přesměrování)
|
||||
- [ ] Možnost úhrady celé útraty jednou osobou
|
||||
- Základní myšlenka: jedna osoba uhradí celou útratu (v zájmu rychlosti odbavení), ostatním se automaticky vygeneruje QR kód, kterým následně uhradí svoji část útraty
|
||||
- Obecně to bude problém např. pokud si někdo objedná něco navíc (pití apod.)
|
||||
- [ ] Tlačítko "Uhradit" u každého řádku podniku - platí ten, kdo kliknul
|
||||
- [ ] Zobrazeno bude pouze, pokud má daný uživatel nastaveno číslo účtu
|
||||
- [ ] Dialog pro zadání spropitného, které se následně rozpočte rovnoměrně všem strávníkům
|
||||
- [ ] Generování a zobrazení QR kódů ostatním strávníkům
|
||||
- [ ] Umožnit u každého strávníka připočíst vlastní částku (např. za pití)
|
||||
- [ ] Umožnit (např. zaškrtávátky) vybrat, za koho bude zaplaceno (pokud někdo bude platit zvlášť)
|
||||
- [ ] Podpora pro notifikace v externích systémech (Gotify, Discord, MS Teams)
|
||||
- [ ] Umožnit zadat URL/tokeny uživatelem
|
||||
- [ ] Umožnit uživatelsky konfigurovat typy notifikací, které se budou odesílat
|
||||
- [ ] Zavést notifikace typu "Jdeme na oběd"
|
||||
- [ ] Notifikaci dostanou pouze uživatelé, kteří mají vybranou stejnou lokalitu
|
||||
- [ ] Vylepšit parsery restaurací
|
||||
- [ ] Sladovnická
|
||||
- [ ] Zbytečná prvotní validace indexu, datum konkrétního dne je i v samotné tabulce s jídly, viz TODO v parseru
|
||||
- [ ] U Motlíků
|
||||
- [ ] Validovat, že vstupní datum je zahrnuto v rozsahu uvedeném nad tabulkou (např. '12.6.-16.6.')
|
||||
- [ ] Jídelní lístek se stahuje jednou každý den, teoreticky by stačilo jednou týdně (za předpokladu, že se během týdne nemění)
|
||||
- [ ] TechTower
|
||||
- [ ] Validovat, že vstupní datum je zahrnuto v rozsahu uvedeném nad tabulkou (typicky 'Obědy 12. 6. - 16. 6. 2023 (každý den vždy i obědový bufet)')
|
||||
- [ ] Jídelní lístek se stahuje v rámci prvního požadavku daný den, ale často se jídelní lístek na stránkách aktualizuje až v průběhu pondělního dopoledne a ten zobrazený je proto neaktuální
|
||||
- Stránka neposílá hlavičku o času poslední modifikace, takže o to se nelze opřít
|
||||
- Nevím aktuálně jak řešit jinak, než častějším scrapováním celé stránky
|
||||
- [X] Někdy jsou v názvech jídel přebytečné mezery kolem čárek ( , )
|
||||
- [ ] Nasazení nové verze v Docker smaže veškerá data (protože data.json není vystrčený ven z kontejneru)
|
||||
- [ ] Zavést složku /data
|
||||
- [ ] Mazat z databáze data z minulosti, aktuálně je to k ničemu
|
||||
- [ ] Skripty pro snadné spuštění vývoje na Windows (ekvivalent ./run_dev.sh)
|
||||
- [ ] Implementovat Pizza day
|
||||
- [ ] Zobrazit upozornění před smazáním/zamknutím/odemknutím pizza day
|
||||
- [ ] Pizzy se samy budou při naklikání přidávat do košíku
|
||||
- [ ] Nutno nejprve vyřešit předávání PHPSESSIONID cookie na pizzachefie.cz pomocí fetch()
|
||||
- [ ] Ceny krabic za pizzu jsou napevno v kódu - problém, pokud se někdy změní
|
||||
- [X] Umožnit u Pizza day ručně připočíst cenu za přísady
|
||||
- [X] Prvotní načtení pizz při založení Pizza Day trvá a nic se během toho nezobrazuje (např. loader)
|
||||
- [X] Po doručení zobrazit komu zaplatit (kdo objednával)
|
||||
- [x] Zbytečně nescrapovat každý den pizzy z Pizza Chefie, dokud není založen Pizza Day
|
||||
- [x] Umožnit uzamčení objednávek zakladatelem
|
||||
- [x] Možnost uložení čísla účtu
|
||||
- [x] Automatické generování a zobrazení QR kódů
|
||||
- [x] https://qr-platba.cz/pro-vyvojare/restful-api/
|
||||
- [x] Zobrazovat celkovou cenu objednávky pod tabulkou objednávek
|
||||
- [x] Umožnit přidat k objednávce poznámku (např. "bez oliv")
|
||||
- [x] Negenerovat QR kód pro objednávajícího
|
||||
- [X] Možnost náhledu na ostatní dny v týdnu (např. pomocí šipek)
|
||||
- [X] Možnost výběru oběda na následující dny v týdnu
|
||||
- [X] Umožnit vybrat libovolný čas odchodu
|
||||
- [X] Validace zadání smysluplného času (ideálně i klientská)
|
||||
- [x] Umožnit smazání aktuální volby "popelnicí", místo nutnosti vybrat prázdnou položku v selectu
|
||||
- [x] Přívětivější možnost odhlašování
|
||||
- [x] Vyřešit responzivní design pro použití na mobilu
|
||||
- [x] Vyndat URL na Food API do .env
|
||||
- [x] Neselhat při nedostupnosti nebo chybě z Food API
|
||||
- [x] Dokončit docker-compose pro kompletní funkčnost
|
||||
- [x] Vylepšit dokumentaci projektu
|
||||
- [x] Popsat závislosti, co je nutné provést před vývojem a postup spuštění pro vývoj
|
||||
- [x] Popsat dostupné env
|
||||
- [x] Přesunout autentizaci na server (JWT?)
|
||||
- [x] Zavést .env.template a přidat .env do .gitignore
|
||||
- [x] Zkrášlit dialog pro vyplnění čísla účtu, vypadá mizerně
|
||||
- [x] Zbavit se Food API, potřebnou funkcionalitu zahrnout do serveru
|
||||
- [x] Vyřešit API mezi serverem a klientem, aby nebyl v obou projektech duplicitní kód (viz types.ts a Types.tsx)
|
||||
- [X] Vybraná jídla strávníků zobrazovat v samostatném sloupci
|
||||
- [X] Umožnit výběr/zadání preferovaného času odchodu na oběd
|
||||
- Hodí se např. pokud má někdo schůzky
|
||||
- [X] Ukládat dostupné pizzy do DB místo souborů
|
||||
- [X] Ukládat jídla do DB místo souborů
|
||||
+4
-4
@@ -7,7 +7,7 @@ self.addEventListener('push', (event) => {
|
||||
body: data.body,
|
||||
icon: '/favicon.ico',
|
||||
tag: 'lunch-reminder',
|
||||
data: { login: data.login },
|
||||
data: { login: data.login, token: data.token },
|
||||
actions: [
|
||||
{ action: 'neobedvam', title: 'Mám vlastní/neobědvám' },
|
||||
],
|
||||
@@ -19,13 +19,13 @@ self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === 'neobedvam') {
|
||||
const login = event.notification.data?.login;
|
||||
if (login) {
|
||||
const { login, token } = event.notification.data ?? {};
|
||||
if (login && token) {
|
||||
event.waitUntil(
|
||||
fetch('/api/notifications/push/quickChoice', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ login }),
|
||||
body: JSON.stringify({ login, token }),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
+50
-20
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { EVENT_DISCONNECT, EVENT_MESSAGE, SocketContext } from './context/socket';
|
||||
import { EVENT_DISCONNECT, EVENT_MESSAGE, EVENT_PENDING_QR, SocketContext } from './context/socket';
|
||||
import { useAuth } from './context/auth';
|
||||
import Login from './Login';
|
||||
import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap';
|
||||
@@ -13,13 +13,15 @@ import './App.scss';
|
||||
import { faCircleCheck, faNoteSticky, faTrashCan, faComment } from '@fortawesome/free-regular-svg-icons';
|
||||
import { useSettings } from './context/settings';
|
||||
import Footer from './components/Footer';
|
||||
import { faBasketShopping, faChainBroken, faChevronLeft, faChevronRight, faGear, faMoneyBillTransfer, faSatelliteDish, faSearch, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faArrowUpRightFromSquare, faBasketShopping, faChainBroken, faChevronLeft, faChevronRight, faGear, faMoneyBillTransfer, faSatelliteDish, faSearch, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Loader from './components/Loader';
|
||||
import { getHumanDateTime, isInTheFuture, formatDateString } from './Utils';
|
||||
import NoteModal from './components/modals/NoteModal';
|
||||
import ConfirmModal from './components/modals/ConfirmModal';
|
||||
import PayForAllModal from './components/modals/PayForAllModal';
|
||||
import { useEasterEgg } from './context/eggs';
|
||||
import { ClientData, Food, MealSlot, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, LocationLunchChoicesMap, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr, generateQr } from '../../types';
|
||||
import { ClientData, Food, MealSlot, PendingQr, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, LocationLunchChoicesMap, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime, setBuyer, dismissQr, generateQr } from '../../types';
|
||||
import { getLunchChoiceName } from './enums';
|
||||
// import FallingLeaves, { LEAF_PRESETS, LEAF_COLOR_THEMES } from './FallingLeaves';
|
||||
// import './FallingLeaves.scss';
|
||||
@@ -59,6 +61,7 @@ const EASTER_EGG_DEFAULT_DURATION = 0.75;
|
||||
function App() {
|
||||
const auth = useAuth();
|
||||
const settings = useSettings();
|
||||
const navigate = useNavigate();
|
||||
const [easterEgg, _] = useEasterEgg(auth);
|
||||
const [isConnected, setIsConnected] = useState<boolean>(false);
|
||||
const [data, setData] = useState<ClientData>();
|
||||
@@ -75,6 +78,7 @@ function App() {
|
||||
const [dayIndex, setDayIndex] = useState<number>();
|
||||
const [loadingPizzaDay, setLoadingPizzaDay] = useState<boolean>(false);
|
||||
const [noteModalOpen, setNoteModalOpen] = useState<boolean>(false);
|
||||
const [dismissQrId, setDismissQrId] = useState<string | null>(null);
|
||||
const [payForAllLocationKey, setPayForAllLocationKey] = useState<LunchChoice | null>(null);
|
||||
const [eggImage, setEggImage] = useState<Blob>();
|
||||
const eggRef = useRef<HTMLImageElement>(null);
|
||||
@@ -132,14 +136,25 @@ function App() {
|
||||
setData(newData);
|
||||
}
|
||||
});
|
||||
socket.on(EVENT_PENDING_QR, (pendingQr: PendingQr) => {
|
||||
setData(prev => prev ? { ...prev, pendingQrs: [...(prev.pendingQrs ?? []), pendingQr] } : prev);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off(EVENT_CONNECT);
|
||||
socket.off(EVENT_DISCONNECT);
|
||||
socket.off(EVENT_MESSAGE);
|
||||
socket.off(EVENT_PENDING_QR);
|
||||
}
|
||||
}, [socket]);
|
||||
|
||||
// Připojení do osobní socket místnosti po přihlášení
|
||||
useEffect(() => {
|
||||
if (auth?.login) {
|
||||
socket.emit('join', auth.login);
|
||||
}
|
||||
}, [auth?.login, socket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!auth?.login || !data?.choices) {
|
||||
return
|
||||
@@ -685,15 +700,15 @@ function App() {
|
||||
{locationPickCount >= 2 && auth.login && loginObject[auth.login] !== undefined
|
||||
&& locationKey !== LunchChoice.PIZZA && locationKey !== LunchChoice.NEOBEDVAM && locationKey !== LunchChoice.ROZHODUJI
|
||||
&& settings?.bankAccount && settings?.holderName && (
|
||||
<span title='Zaplatit za všechny a vygenerovat QR kódy ostatním' className="ms-2">
|
||||
<FontAwesomeIcon
|
||||
icon={faMoneyBillTransfer}
|
||||
onClick={() => setPayForAllLocationKey(locationKey)}
|
||||
className='action-icon'
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span title='Zaplatit za všechny a vygenerovat QR kódy ostatním' className="ms-2">
|
||||
<FontAwesomeIcon
|
||||
icon={faMoneyBillTransfer}
|
||||
onClick={() => setPayForAllLocationKey(locationKey)}
|
||||
className='action-icon'
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className='p-0'>
|
||||
<Table className="nested-table">
|
||||
@@ -721,6 +736,9 @@ function App() {
|
||||
markAsBuyer();
|
||||
}} icon={faBasketShopping} className={isBuyer ? 'buyer-icon' : 'action-icon'} style={{ cursor: 'pointer' }} />
|
||||
</span>}
|
||||
{login === auth.login && locationKey === LunchChoice.OBJEDNAVAM && <span title='Přejít na stránku objednávek'>
|
||||
<FontAwesomeIcon onClick={() => navigate('/objednani')} icon={faArrowUpRightFromSquare} className='action-icon' style={{ cursor: 'pointer' }} />
|
||||
</span>}
|
||||
{login !== auth.login && locationKey === LunchChoice.OBJEDNAVAM && isBuyer && <span title='Objednávající'>
|
||||
<FontAwesomeIcon onClick={() => {
|
||||
copyNote(userPayload.note!);
|
||||
@@ -893,18 +911,12 @@ function App() {
|
||||
{data.pendingQrs.map(qr => (
|
||||
<div key={qr.id} className='qr-code mb-3'>
|
||||
<p>
|
||||
<strong>{formatDateString(qr.date)}</strong> — {qr.creator} ({qr.totalPrice} Kč)
|
||||
<strong>{formatDateString(qr.date)}</strong> — {qr.creator} ({qr.totalPrice / 100} Kč)
|
||||
{qr.purpose && <><br /><span className="text-muted">{qr.purpose}</span></>}
|
||||
</p>
|
||||
<img src={`/api/qr?login=${auth.login}&id=${qr.id}`} alt='QR kód' />
|
||||
<div className='mt-2'>
|
||||
<Button variant="success" onClick={async () => {
|
||||
await dismissQr({ body: { id: qr.id } });
|
||||
const response = await getData({ query: { dayIndex } });
|
||||
if (response.data) {
|
||||
setData(response.data);
|
||||
}
|
||||
}}>
|
||||
<Button variant="success" onClick={() => setDismissQrId(qr.id)}>
|
||||
Zaplatil jsem
|
||||
</Button>
|
||||
</div>
|
||||
@@ -920,6 +932,24 @@ function App() {
|
||||
/> */}
|
||||
<Footer />
|
||||
<NoteModal isOpen={noteModalOpen} onClose={() => setNoteModalOpen(false)} onSave={saveNote} />
|
||||
<ConfirmModal
|
||||
isOpen={dismissQrId !== null}
|
||||
title="Potvrzení platby"
|
||||
message="Opravdu jste zaplatili? QR kód bude odstraněn."
|
||||
confirmLabel="Zaplatil jsem"
|
||||
confirmVariant="success"
|
||||
onClose={() => setDismissQrId(null)}
|
||||
onConfirm={async () => {
|
||||
if (!dismissQrId) return;
|
||||
const id = dismissQrId;
|
||||
setDismissQrId(null);
|
||||
await dismissQr({ body: { id } });
|
||||
const response = await getData({ query: { dayIndex } });
|
||||
if (response.data) {
|
||||
setData(response.data);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{payForAllLocationKey && data && (
|
||||
<PayForAllModal
|
||||
isOpen
|
||||
|
||||
@@ -5,20 +5,20 @@ import { SnowOverlay } from 'react-snow-overlay';
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { SocketContext, socket } from "./context/socket";
|
||||
import StatsPage from "./pages/StatsPage";
|
||||
import ExtraPage from "./pages/ExtraPage";
|
||||
import OrderGroupsPage from "./pages/OrderGroupsPage";
|
||||
import App from "./App";
|
||||
|
||||
export const STATS_URL = '/stats';
|
||||
export const VECERE_URL = '/vecere';
|
||||
export const OBJEDNANI_URL = '/objednani';
|
||||
|
||||
export default function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path={STATS_URL} element={<StatsPage />} />
|
||||
<Route path={VECERE_URL} element={
|
||||
<Route path={OBJEDNANI_URL} element={
|
||||
<ProvideSettings>
|
||||
<SocketContext.Provider value={socket}>
|
||||
<ExtraPage />
|
||||
<OrderGroupsPage />
|
||||
<ToastContainer />
|
||||
</SocketContext.Provider>
|
||||
</ProvideSettings>
|
||||
|
||||
@@ -10,7 +10,7 @@ import GenerateQrModal from "./modals/GenerateQrModal";
|
||||
import GenerateMockDataModal from "./modals/GenerateMockDataModal";
|
||||
import ClearMockDataModal from "./modals/ClearMockDataModal";
|
||||
import { useNavigate } from "react-router";
|
||||
import { STATS_URL, VECERE_URL } from "../AppRoutes";
|
||||
import { STATS_URL, OBJEDNANI_URL } from "../AppRoutes";
|
||||
import { FeatureRequest, getVotes, updateVote, LunchChoices, getChangelogs } from "../../../types";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -207,7 +207,7 @@ export default function Header({ choices, dayIndex }: Props) {
|
||||
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</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(VECERE_URL)}>Večeře</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => navigate(OBJEDNANI_URL)}>Objednání</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => {
|
||||
getChangelogs().then(response => {
|
||||
const entries = response.data ?? {};
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function PizzaOrderList({ state, orders, onDelete, creator }: Rea
|
||||
borderTop: '2px solid var(--luncher-border)'
|
||||
}}>
|
||||
<td colSpan={4} style={{ padding: '16px 20px', border: 'none' }}>Celkem</td>
|
||||
<td style={{ padding: '16px 20px', border: 'none', textAlign: 'right', color: 'var(--luncher-primary)' }}>{`${total} Kč`}</td>
|
||||
<td style={{ padding: '16px 20px', border: 'none', textAlign: 'right', color: 'var(--luncher-primary)' }}>{`${total / 100} Kč`}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function PizzaOrderRow({ creator, order, state, onDelete, onFeeMo
|
||||
<td>{order.customer}</td>
|
||||
<td>{order.pizzaList!.map<React.ReactNode>(pizzaOrder =>
|
||||
<span key={pizzaOrder.name}>
|
||||
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price} Kč)`}
|
||||
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price / 100} Kč)`}
|
||||
{auth?.login === order.customer && state === PizzaDayState.CREATED &&
|
||||
<span title='Odstranit'>
|
||||
<FontAwesomeIcon onClick={() => {
|
||||
@@ -38,10 +38,10 @@ export default function PizzaOrderRow({ creator, order, state, onDelete, onFeeMo
|
||||
.reduce((prev, curr, index) => [prev, <br key={`br-${index}`} />, curr])}
|
||||
</td>
|
||||
<td style={{ maxWidth: "200px" }}>{order.note ?? '-'}</td>
|
||||
<td style={{ maxWidth: "200px" }}>{order.fee?.price ? `${order.fee.price} Kč${order.fee.text ? ` (${order.fee.text})` : ''}` : '-'}</td>
|
||||
<td style={{ maxWidth: "200px" }}>{order.fee?.price ? `${order.fee.price / 100} Kč${order.fee.text ? ` (${order.fee.text})` : ''}` : '-'}</td>
|
||||
<td>
|
||||
{order.totalPrice} Kč{auth?.login === creator && state === PizzaDayState.CREATED && <span title='Nastavit příplatek'><FontAwesomeIcon onClick={() => { setIsFeeModalOpen(true) }} className='action-icon' icon={faMoneyBill1} /></span>}
|
||||
{order.totalPrice / 100} Kč{auth?.login === creator && state === PizzaDayState.CREATED && <span title='Nastavit příplatek'><FontAwesomeIcon onClick={() => { setIsFeeModalOpen(true) }} className='action-icon' icon={faMoneyBill1} /></span>}
|
||||
</td>
|
||||
<PizzaAdditionalFeeModal customerName={order.customer} isOpen={isFeeModalOpen} onClose={() => setIsFeeModalOpen(false)} onSave={saveFees} initialValues={{ text: order.fee?.text, price: order.fee?.price?.toString() }} />
|
||||
<PizzaAdditionalFeeModal customerName={order.customer} isOpen={isFeeModalOpen} onClose={() => setIsFeeModalOpen(false)} onSave={saveFees} initialValues={{ text: order.fee?.text, price: order.fee?.price != null ? String(order.fee.price / 100) : undefined }} />
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Modal, Button } from "react-bootstrap";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
confirmLabel?: string;
|
||||
confirmVariant?: string;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function ConfirmModal({ isOpen, title, message, confirmLabel = "Potvrdit", confirmVariant = "primary", onConfirm, onClose }: Readonly<Props>) {
|
||||
return (
|
||||
<Modal show={isOpen} onHide={onClose}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>{message}</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={onClose}>Zrušit</Button>
|
||||
<Button variant={confirmVariant} onClick={onConfirm}>{confirmLabel}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Modal, Button, Form, Table, Alert } from "react-bootstrap";
|
||||
import { updateGroupFees, OrderGroup, OrderGroupMember } from "../../../../types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
group: OrderGroup;
|
||||
onSaved: (data: any) => void;
|
||||
};
|
||||
|
||||
function parseHal(s: string): number {
|
||||
const n = parseFloat(s.replace(',', '.'));
|
||||
return isNaN(n) || n < 0 ? 0 : Math.round(n * 100);
|
||||
}
|
||||
|
||||
function parsePercent(s: string): number {
|
||||
const n = parseFloat(s.replace(',', '.'));
|
||||
return isNaN(n) || n < 0 ? 0 : Math.round(n);
|
||||
}
|
||||
|
||||
function computeMemberTotal(member: OrderGroupMember, feeShare: number, discountType: string, discountValue: number, memberCount: number): number {
|
||||
const base = member.amount ?? 0;
|
||||
const surcharge = member.surchargeAmount ?? 0;
|
||||
const discount = discountType === 'percent'
|
||||
? Math.round((base + surcharge) * discountValue / 100)
|
||||
: Math.round(discountValue / memberCount);
|
||||
return base + surcharge + feeShare - discount;
|
||||
}
|
||||
|
||||
export default function EditGroupFeesModal({ isOpen, onClose, group, onSaved }: Readonly<Props>) {
|
||||
const [fees, setFees] = useState('');
|
||||
const [shipping, setShipping] = useState('');
|
||||
const [tip, setTip] = useState('');
|
||||
const [discountType, setDiscountType] = useState<'percent' | 'fixed'>('percent');
|
||||
const [discountValue, setDiscountValue] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setFees(group.fees ? String(group.fees / 100) : '');
|
||||
setShipping(group.shipping ? String(group.shipping / 100) : '');
|
||||
setTip(group.tip ? String(group.tip / 100) : '');
|
||||
setDiscountType((group.discountType as 'percent' | 'fixed') ?? 'percent');
|
||||
setDiscountValue(group.discountValue
|
||||
? ((group.discountType as string) === 'fixed' ? String(group.discountValue / 100) : String(group.discountValue))
|
||||
: '');
|
||||
setError(null);
|
||||
}, [isOpen, group]);
|
||||
|
||||
const memberEntries = Object.entries(group.members) as [string, OrderGroupMember][];
|
||||
const memberCount = memberEntries.length;
|
||||
|
||||
const feesNum = parseHal(fees);
|
||||
const shippingNum = parseHal(shipping);
|
||||
const tipNum = parseHal(tip);
|
||||
const discountNum = discountType === 'percent' ? parsePercent(discountValue) : parseHal(discountValue);
|
||||
const totalFees = feesNum + shippingNum + tipNum;
|
||||
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount) : 0;
|
||||
|
||||
const handleSave = async () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await updateGroupFees({
|
||||
body: {
|
||||
id: group.id,
|
||||
fees: feesNum,
|
||||
shipping: shippingNum,
|
||||
tip: tipNum,
|
||||
discountType: discountNum > 0 ? discountType : undefined,
|
||||
discountValue: discountNum > 0 ? discountNum : undefined,
|
||||
}
|
||||
});
|
||||
if (res.error) {
|
||||
setError((res.error as any).error || 'Nastala chyba');
|
||||
} else {
|
||||
onSaved(res.data);
|
||||
onClose();
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e.message || 'Nastala chyba');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={isOpen} onHide={onClose} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title><h2>Poplatky skupiny — {group.name}</h2></Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{error && (
|
||||
<Alert variant="danger" onClose={() => setError(null)} dismissible>{error}</Alert>
|
||||
)}
|
||||
|
||||
<div className="d-flex gap-3 flex-wrap mb-3">
|
||||
<Form.Group>
|
||||
<Form.Label>Poplatky (Kč)</Form.Label>
|
||||
<Form.Control
|
||||
type="number" min={0} step={0.01}
|
||||
value={fees} onChange={e => setFees(e.target.value)}
|
||||
placeholder="0" style={{ width: 110 }}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Doprava (Kč)</Form.Label>
|
||||
<Form.Control
|
||||
type="number" min={0} step={0.01}
|
||||
value={shipping} onChange={e => setShipping(e.target.value)}
|
||||
placeholder="0" style={{ width: 110 }}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Spropitné (Kč)</Form.Label>
|
||||
<Form.Control
|
||||
type="number" min={0} step={0.01}
|
||||
value={tip} onChange={e => setTip(e.target.value)}
|
||||
placeholder="0" style={{ width: 110 }}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
</Form.Group>
|
||||
</div>
|
||||
|
||||
<div className="d-flex gap-3 align-items-end flex-wrap mb-3">
|
||||
<Form.Group>
|
||||
<Form.Label>Sleva</Form.Label>
|
||||
<div className="d-flex gap-2 align-items-center">
|
||||
<Form.Select
|
||||
value={discountType}
|
||||
onChange={e => setDiscountType(e.target.value as 'percent' | 'fixed')}
|
||||
style={{ width: 160 }}
|
||||
>
|
||||
<option value="percent">Procentuální (%)</option>
|
||||
<option value="fixed">Pevná částka (Kč)</option>
|
||||
</Form.Select>
|
||||
<Form.Control
|
||||
type="number" min={0} step={discountType === 'percent' ? 1 : 0.01}
|
||||
value={discountValue} onChange={e => setDiscountValue(e.target.value)}
|
||||
placeholder="0" style={{ width: 100 }}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
<span className="text-muted">{discountType === 'percent' ? '%' : 'Kč'}</span>
|
||||
</div>
|
||||
</Form.Group>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<h6>Náhled celkových částek ({memberCount} členů, {feeShare > 0 ? `poplatek ${feeShare / 100} Kč/os.` : 'bez poplatku'})</h6>
|
||||
<Table size="sm" bordered>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Člen</th>
|
||||
<th className="text-end">Základ</th>
|
||||
<th className="text-end">Příplatek</th>
|
||||
<th className="text-end">Poplatek</th>
|
||||
<th className="text-end">Sleva</th>
|
||||
<th className="text-end fw-bold">Celkem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{memberEntries.map(([login, member]) => {
|
||||
const base = member.amount ?? 0;
|
||||
const surcharge = member.surchargeAmount ?? 0;
|
||||
const discount = discountNum > 0
|
||||
? (discountType === 'percent'
|
||||
? Math.round((base + surcharge) * discountNum / 100)
|
||||
: Math.round(discountNum / memberCount))
|
||||
: 0;
|
||||
const total = computeMemberTotal(member, feeShare, discountType, discountNum, memberCount);
|
||||
return (
|
||||
<tr key={login}>
|
||||
<td><strong>{login}</strong></td>
|
||||
<td className="text-end">{base > 0 ? `${base / 100} Kč` : '—'}</td>
|
||||
<td className="text-end">{surcharge > 0 ? `${surcharge / 100} Kč` : '—'}</td>
|
||||
<td className="text-end">{feeShare > 0 ? `${feeShare / 100} Kč` : '—'}</td>
|
||||
<td className="text-end text-danger">{discount > 0 ? `-${discount / 100} Kč` : '—'}</td>
|
||||
<td className="text-end fw-bold">{total > 0 ? `${total / 100} Kč` : '—'}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={onClose} disabled={loading}>Storno</Button>
|
||||
<Button variant="primary" onClick={handleSave} disabled={loading}>
|
||||
{loading ? 'Ukládám...' : 'Uložit'}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -33,9 +33,7 @@ function parseAmount(s: string): number | null {
|
||||
if (!s || s.trim().length === 0) return null;
|
||||
const n = parseFloat(s);
|
||||
if (isNaN(n) || n < 0) return null;
|
||||
const parts = s.split('.');
|
||||
if (parts.length === 2 && parts[1].length > 2) return null;
|
||||
return Math.round(n * 100) / 100;
|
||||
return Math.round(n * 100);
|
||||
}
|
||||
|
||||
export default function PayForAllModal({ isOpen, onClose, locationName, locationChoices, menu, payerLogin, bankAccount, bankAccountHolder }: Readonly<Props>) {
|
||||
@@ -55,11 +53,11 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
let baseAmountParseFailed = false;
|
||||
if (menu) {
|
||||
for (const idx of selectedFoods) {
|
||||
const price = parsePriceCzk(menu.food?.[idx]?.price);
|
||||
if (price === null) {
|
||||
const priceKc = parsePriceCzk(menu.food?.[idx]?.price);
|
||||
if (priceKc === null) {
|
||||
baseAmountParseFailed = true;
|
||||
} else {
|
||||
baseAmount += price;
|
||||
baseAmount += Math.round(priceKc * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,13 +82,19 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
if (includedDiners.length === 0) return 0;
|
||||
const tip = parseAmount(tipTotal);
|
||||
if (tip === null || tip === 0) return 0;
|
||||
return Math.round((tip / includedDiners.length) * 100) / 100;
|
||||
const totalPeople = includedDiners.length + 1;
|
||||
return Math.round(tip / totalPeople);
|
||||
})();
|
||||
const payerTipShare = (() => {
|
||||
const tip = parseAmount(tipTotal);
|
||||
if (!tip) return 0;
|
||||
return tip - tipPerPerson * includedDiners.length;
|
||||
})();
|
||||
|
||||
const getTotal = (d: DinerEntry): number => {
|
||||
const surcharge = parseAmount(d.surchargeAmount) ?? 0;
|
||||
const tip = d.included && d.login !== payerLogin ? tipPerPerson : 0;
|
||||
return Math.round((d.baseAmount + surcharge + tip) * 100) / 100;
|
||||
const tip = d.login === payerLogin ? payerTipShare : tipPerPerson;
|
||||
return d.baseAmount + surcharge + tip;
|
||||
};
|
||||
|
||||
const handleInclude = useCallback((login: string, checked: boolean) => {
|
||||
@@ -116,11 +120,6 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
setError(`Celková částka pro ${d.login} musí být kladná`);
|
||||
return;
|
||||
}
|
||||
const amountStr = total.toString();
|
||||
if (amountStr.includes('.') && amountStr.split('.')[1].length > 2) {
|
||||
setError(`Částka pro ${d.login} má více než 2 desetinná místa`);
|
||||
return;
|
||||
}
|
||||
const foods = d.selectedFoods.map(i => menu?.food?.[i]?.name).filter(Boolean).join(', ');
|
||||
const purposeBase = `Oběd ${locationName}${foods ? ` — ${foods}` : ''}`;
|
||||
recipients.push({
|
||||
@@ -167,7 +166,7 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<p>Zaplatili jste za skupinu v restauraci. Nastavte příplatky a dýško, poté vygenerujte QR kódy pro ostatní.</p>
|
||||
<p>Zaplatili jste za skupinu v restauraci. Nastavte příplatky a společné poplatky, poté vygenerujte QR kódy pro ostatní.</p>
|
||||
|
||||
{!hasMenu && (
|
||||
<Alert variant="info">
|
||||
@@ -194,7 +193,7 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
<th>Strávník</th>
|
||||
<th>Jídla</th>
|
||||
<th style={{ width: 220 }}>Příplatek</th>
|
||||
<th style={{ width: 90 }}>Dýško</th>
|
||||
<th style={{ width: 90 }}>Poplatek</th>
|
||||
<th style={{ width: 90 }}>Celkem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -220,40 +219,38 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
<td>
|
||||
<small>
|
||||
{foodNames || <span className="text-muted">—</span>}
|
||||
{hasMenu && d.baseAmount > 0 && <span className="text-muted"> ({d.baseAmount} Kč)</span>}
|
||||
{hasMenu && d.baseAmount > 0 && <span className="text-muted"> ({d.baseAmount / 100} Kč)</span>}
|
||||
{d.baseAmountParseFailed && <span className="text-warning"> ⚠</span>}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{!isPayer && (
|
||||
<div className="d-flex gap-1">
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="popis"
|
||||
value={d.surchargeText}
|
||||
onChange={e => handleSurchargeText(d.login, e.target.value)}
|
||||
disabled={!d.included}
|
||||
size="sm"
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Kč"
|
||||
value={d.surchargeAmount}
|
||||
onChange={e => handleSurchargeAmount(d.login, e.target.value)}
|
||||
disabled={!d.included}
|
||||
size="sm"
|
||||
style={{ width: 70 }}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="d-flex gap-1">
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="popis"
|
||||
value={d.surchargeText}
|
||||
onChange={e => handleSurchargeText(d.login, e.target.value)}
|
||||
disabled={!isPayer && !d.included}
|
||||
size="sm"
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Kč"
|
||||
value={d.surchargeAmount}
|
||||
onChange={e => handleSurchargeAmount(d.login, e.target.value)}
|
||||
disabled={!isPayer && !d.included}
|
||||
size="sm"
|
||||
style={{ width: 70 }}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-end">
|
||||
{!isPayer && d.included ? `${tipPerPerson} Kč` : '—'}
|
||||
{(() => { const s = isPayer ? payerTipShare : tipPerPerson; return s > 0 ? `${s / 100} Kč` : '—'; })()}
|
||||
</td>
|
||||
<td className="text-end fw-bold">
|
||||
{!isPayer ? `${total} Kč` : '—'}
|
||||
{`${total / 100} Kč`}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -262,7 +259,7 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
</Table>
|
||||
|
||||
<div className="d-flex align-items-center gap-2 mt-2">
|
||||
<label className="mb-0 text-nowrap">Dýško celkem (Kč):</label>
|
||||
<label className="mb-0 text-nowrap">Poplatky celkem (Kč):</label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="0"
|
||||
@@ -274,7 +271,7 @@ export default function PayForAllModal({ isOpen, onClose, locationName, location
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{includedDiners.length > 0 && tipPerPerson > 0
|
||||
? `(${tipPerPerson} Kč / osoba)`
|
||||
? `(${tipPerPerson / 100} Kč / osoba)`
|
||||
: ''}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Modal, Button, Form, Table, Alert } from "react-bootstrap";
|
||||
import { generateQr, OrderGroup, OrderGroupMember, QrRecipient } from "../../../../types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess?: () => void;
|
||||
group: OrderGroup;
|
||||
payerLogin: string;
|
||||
bankAccount: string;
|
||||
bankAccountHolder: string;
|
||||
groupId?: string;
|
||||
};
|
||||
|
||||
type DinerEntry = {
|
||||
login: string;
|
||||
member: OrderGroupMember;
|
||||
included: boolean;
|
||||
};
|
||||
|
||||
export default function PayForGroupModal({ isOpen, onClose, onSuccess, group, payerLogin, bankAccount, bankAccountHolder, groupId }: Readonly<Props>) {
|
||||
const [diners, setDiners] = useState<DinerEntry[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const entries: DinerEntry[] = (Object.entries(group.members) as [string, OrderGroupMember][]).map(([login, member]) => ({
|
||||
login,
|
||||
member,
|
||||
included: login !== payerLogin,
|
||||
}));
|
||||
setDiners(entries);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
}, [isOpen, group, payerLogin]);
|
||||
|
||||
const memberCount = diners.length;
|
||||
const fees = group.fees ?? 0;
|
||||
const shipping = group.shipping ?? 0;
|
||||
const tip = group.tip ?? 0;
|
||||
const totalFees = fees + shipping + tip;
|
||||
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount) : 0;
|
||||
|
||||
const getMemberTotal = (entry: DinerEntry): number => {
|
||||
const base = entry.member.amount ?? 0;
|
||||
const surcharge = entry.member.surchargeAmount ?? 0;
|
||||
const discountType = group.discountType;
|
||||
const discountValue = group.discountValue ?? 0;
|
||||
const discount = discountValue > 0
|
||||
? (discountType === 'percent'
|
||||
? Math.round((base + surcharge) * discountValue / 100)
|
||||
: Math.round(discountValue / memberCount))
|
||||
: 0;
|
||||
return base + surcharge + feeShare - discount;
|
||||
};
|
||||
|
||||
const includedNonPayers = diners.filter(d => d.included && d.login !== payerLogin);
|
||||
|
||||
const handleInclude = (login: string, checked: boolean) => {
|
||||
setDiners(prev => prev.map(d => d.login === login ? { ...d, included: checked } : d));
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setError(null);
|
||||
const recipients: QrRecipient[] = [];
|
||||
|
||||
for (const d of diners) {
|
||||
if (!d.included || d.login === payerLogin) continue;
|
||||
const total = getMemberTotal(d);
|
||||
if (total <= 0) {
|
||||
setError(`Celková částka pro ${d.login} musí být kladná`);
|
||||
return;
|
||||
}
|
||||
const note = d.member.note?.trim();
|
||||
recipients.push({
|
||||
login: d.login,
|
||||
purpose: note ? note.replace(/[^\x00-\xff*]/g, '').replace(/\*/g, '').substring(0, 60) : `Objednávka ${group.name}`.substring(0, 60),
|
||||
amount: total,
|
||||
});
|
||||
}
|
||||
|
||||
if (recipients.length === 0) {
|
||||
setError("Nebyl vybrán žádný příjemce");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await generateQr({
|
||||
body: { recipients, bankAccount, bankAccountHolder, ...(groupId ? { groupId } : {}) },
|
||||
});
|
||||
if (response.error) {
|
||||
setError((response.error as any).error || 'Nastala chyba při generování QR kódů');
|
||||
} else {
|
||||
setSuccess(true);
|
||||
onSuccess?.();
|
||||
setTimeout(() => onClose(), 2000);
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e.message || 'Nastala chyba při generování QR kódů');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hasFees = totalFees > 0;
|
||||
|
||||
return (
|
||||
<Modal show={isOpen} onHide={onClose} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title><h2>Generovat QR — {group.name}</h2></Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{success ? (
|
||||
<Alert variant="success">
|
||||
QR kódy byly úspěšně vygenerovány! Uživatelé je uvidí v sekci „Nevyřízené platby".
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<p>Zaplatili jste za skupinu. Vyberte, komu vygenerovat QR kód k úhradě.</p>
|
||||
|
||||
{error && (
|
||||
<Alert variant="danger" onClose={() => setError(null)} dismissible>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{hasFees && (
|
||||
<div className="d-flex gap-3 mb-2 text-muted" style={{ fontSize: '0.9em' }}>
|
||||
{fees > 0 && <span>Poplatky: <strong>{fees / 100} Kč</strong></span>}
|
||||
{shipping > 0 && <span>Doprava: <strong>{shipping / 100} Kč</strong></span>}
|
||||
{tip > 0 && <span>Spropitné: <strong>{tip / 100} Kč</strong></span>}
|
||||
<span>→ {feeShare / 100} Kč/os.</span>
|
||||
</div>
|
||||
)}
|
||||
{group.discountValue != null && group.discountValue > 0 && (
|
||||
<div className="mb-2 text-success" style={{ fontSize: '0.9em' }}>
|
||||
Sleva: {group.discountType === 'percent' ? `${group.discountValue}%` : `${group.discountValue / 100} Kč`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Table striped bordered hover responsive size="sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 40 }}></th>
|
||||
<th>Člen</th>
|
||||
<th style={{ width: 90 }} className="text-end">Základ</th>
|
||||
<th style={{ width: 90 }} className="text-end">Příplatek</th>
|
||||
{hasFees && <th style={{ width: 90 }} className="text-end">Poplatek</th>}
|
||||
<th style={{ width: 90 }} className="text-end fw-bold">Celkem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{diners.map(d => {
|
||||
const isPayer = d.login === payerLogin;
|
||||
const total = getMemberTotal(d);
|
||||
const surcharge = d.member.surchargeAmount ?? 0;
|
||||
return (
|
||||
<tr key={d.login} className={!d.included && !isPayer ? 'text-muted' : ''}>
|
||||
<td className="text-center">
|
||||
{isPayer ? (
|
||||
<small className="text-muted">plátce</small>
|
||||
) : (
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
checked={d.included}
|
||||
onChange={e => handleInclude(d.login, e.target.checked)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<strong>{d.login}</strong>
|
||||
{d.member.surchargeText && (
|
||||
<small className="text-muted ms-1">({d.member.surchargeText})</small>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-end">
|
||||
{(d.member.amount ?? 0) > 0 ? `${d.member.amount! / 100} Kč` : <span className="text-muted">—</span>}
|
||||
</td>
|
||||
<td className="text-end">
|
||||
{surcharge > 0 ? `${surcharge / 100} Kč` : <span className="text-muted">—</span>}
|
||||
</td>
|
||||
{hasFees && (
|
||||
<td className="text-end">
|
||||
{feeShare > 0 ? `${feeShare / 100} Kč` : '—'}
|
||||
</td>
|
||||
)}
|
||||
<td className="text-end fw-bold">
|
||||
{total > 0 ? `${total / 100} Kč` : <span className="text-muted">—</span>}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{!success && (
|
||||
<>
|
||||
<span className="me-auto text-muted">Příjemci: {includedNonPayers.length}</span>
|
||||
<Button variant="secondary" onClick={onClose} disabled={loading}>Storno</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleGenerate}
|
||||
disabled={loading || includedNonPayers.length === 0}
|
||||
>
|
||||
{loading ? 'Generuji...' : 'Vygenerovat QR'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{success && (
|
||||
<Button variant="secondary" onClick={onClose}>Zavřít</Button>
|
||||
)}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -15,12 +15,12 @@ export default function PizzaAdditionalFeeModal({ customerName, isOpen, onClose,
|
||||
const priceRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const doSubmit = () => {
|
||||
onSave(customerName, textRef.current?.value, Number.parseInt(priceRef.current?.value ?? "0"));
|
||||
onSave(customerName, textRef.current?.value, Math.round(Number.parseFloat(priceRef.current?.value ?? "0") * 100));
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onSave(customerName, textRef.current?.value, Number.parseInt(priceRef.current?.value ?? "0"));
|
||||
onSave(customerName, textRef.current?.value, Math.round(Number.parseFloat(priceRef.current?.value ?? "0") * 100));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { useState } from "react";
|
||||
import { Modal, Button, Form, ListGroup, Alert } from "react-bootstrap";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTrashCan } from "@fortawesome/free-regular-svg-icons";
|
||||
import { addStore, deleteStore } from "../../../../types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
stores: string[];
|
||||
onStoresChanged: (stores: string[]) => void;
|
||||
};
|
||||
|
||||
export default function StoreAdminModal({ isOpen, onClose, stores, onStoresChanged }: Readonly<Props>) {
|
||||
const [newName, setNewName] = useState('');
|
||||
const [heslo, setHeslo] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!newName.trim()) return;
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await addStore({ body: { name: newName.trim(), heslo } });
|
||||
if (res.error) {
|
||||
setError((res.error as any).error || 'Nastala chyba');
|
||||
} else if (res.data) {
|
||||
onStoresChanged(res.data as string[]);
|
||||
setNewName('');
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e.message || 'Nastala chyba');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = async (name: string) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await deleteStore({ body: { name, heslo } });
|
||||
if (res.error) {
|
||||
setError((res.error as any).error || 'Nastala chyba');
|
||||
} else if (res.data) {
|
||||
onStoresChanged(res.data as string[]);
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e.message || 'Nastala chyba');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={isOpen} onHide={onClose}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title><h2>Správa obchodů</h2></Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{error && (
|
||||
<Alert variant="danger" onClose={() => setError(null)} dismissible>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Admin heslo</Form.Label>
|
||||
<Form.Control
|
||||
type="password"
|
||||
placeholder="Heslo"
|
||||
value={heslo}
|
||||
onChange={e => setHeslo(e.target.value)}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
<h6>Přidat obchod</h6>
|
||||
<div className="d-flex gap-2 mb-3">
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Název obchodu"
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleAdd(); }}
|
||||
/>
|
||||
<Button variant="primary" onClick={handleAdd} disabled={loading || !newName.trim() || !heslo}>
|
||||
Přidat
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<h6>Aktuální seznam</h6>
|
||||
{stores.length === 0 ? (
|
||||
<p className="text-muted">Žádné obchody v seznamu</p>
|
||||
) : (
|
||||
<ListGroup>
|
||||
{stores.map(s => (
|
||||
<ListGroup.Item key={s} className="d-flex justify-content-between align-items-center">
|
||||
{s}
|
||||
<FontAwesomeIcon
|
||||
icon={faTrashCan}
|
||||
className="action-icon"
|
||||
title="Odebrat"
|
||||
onClick={() => handleRemove(s)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={onClose}>Zavřít</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -18,3 +18,4 @@ export const SocketContext = React.createContext();
|
||||
export const EVENT_CONNECT = 'connect';
|
||||
export const EVENT_DISCONNECT = 'disconnect';
|
||||
export const EVENT_MESSAGE = 'message';
|
||||
export const EVENT_PENDING_QR = 'pendingQr';
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { Alert, Badge, Button, Card, Form, Modal, OverlayTrigger, Table, Tooltip } from 'react-bootstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faBasketShopping, faCircleCheck, faGear, faLock, faLockOpen, faSearch, faUserPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
ClientData, GroupState, MealSlot, OrderGroup, OrderGroupMember,
|
||||
getData, createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes,
|
||||
} from '../../../types';
|
||||
import { EVENT_MESSAGE, SocketContext } from '../context/socket';
|
||||
import { useAuth } from '../context/auth';
|
||||
import { useSettings } from '../context/settings';
|
||||
import Login from '../Login';
|
||||
import Header from '../components/Header';
|
||||
import Footer from '../components/Footer';
|
||||
import Loader from '../components/Loader';
|
||||
import StoreAdminModal from '../components/modals/StoreAdminModal';
|
||||
import PayForGroupModal from '../components/modals/PayForGroupModal';
|
||||
import EditGroupFeesModal from '../components/modals/EditGroupFeesModal';
|
||||
|
||||
const SLOT = MealSlot.EXTRA;
|
||||
const TIME_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/;
|
||||
|
||||
function stateBadge(state: GroupState) {
|
||||
const map: Record<GroupState, { bg: string; label: string }> = {
|
||||
[GroupState.OPEN]: { bg: 'success', label: 'Otevřeno' },
|
||||
[GroupState.LOCKED]: { bg: 'warning', label: 'Uzamčeno' },
|
||||
[GroupState.ORDERED]: { bg: 'secondary', label: 'Objednáno' },
|
||||
};
|
||||
const { bg, label } = map[state] ?? { bg: 'light', label: state };
|
||||
return <Badge bg={bg}>{label}</Badge>;
|
||||
}
|
||||
|
||||
export default function OrderGroupsPage() {
|
||||
const auth = useAuth();
|
||||
const settings = useSettings();
|
||||
const socket = useContext(SocketContext);
|
||||
const [data, setData] = useState<ClientData | undefined>();
|
||||
const [failure, setFailure] = useState(false);
|
||||
const [newGroupName, setNewGroupName] = useState('');
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [adminModalOpen, setAdminModalOpen] = useState(false);
|
||||
const [editAmounts, setEditAmounts] = useState<Record<string, string>>({});
|
||||
const [editNotes, setEditNotes] = useState<Record<string, string>>({});
|
||||
const [editSurcharges, setEditSurcharges] = useState<Record<string, { text: string; amount: string }>>({});
|
||||
const [editTimes, setEditTimes] = useState<Record<string, { orderedAt: string; deliveryAt: string }>>({});
|
||||
const [payModal, setPayModal] = useState<OrderGroup | null>(null);
|
||||
const [feesModal, setFeesModal] = useState<OrderGroup | null>(null);
|
||||
const [confirmOrderGroup, setConfirmOrderGroup] = useState<OrderGroup | null>(null);
|
||||
const [pageError, setPageError] = useState<string | null>(null);
|
||||
|
||||
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(prev => ({
|
||||
...newData,
|
||||
stores: newData.stores ?? prev?.stores,
|
||||
}));
|
||||
});
|
||||
return () => { socket.off(EVENT_MESSAGE); };
|
||||
}, [socket]);
|
||||
|
||||
const refresh = async (fn: () => Promise<any>): Promise<boolean> => {
|
||||
setPageError(null);
|
||||
const result = await fn();
|
||||
if (result?.error) {
|
||||
setPageError((result.error as any).error || 'Nastala chyba');
|
||||
await fetchData();
|
||||
return false;
|
||||
}
|
||||
if (result?.data) {
|
||||
setData(result.data);
|
||||
socket.emit?.('message', result.data as ClientData);
|
||||
}
|
||||
await fetchData();
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!newGroupName || !auth?.login) return;
|
||||
setCreating(true);
|
||||
const ok = await refresh(() => createGroup({ body: { name: newGroupName } }));
|
||||
if (ok) setNewGroupName('');
|
||||
setCreating(false);
|
||||
};
|
||||
|
||||
const handleJoin = (groupId: string) =>
|
||||
refresh(() => addGroupMember({ body: { id: groupId } }));
|
||||
|
||||
const handleToggleLock = (group: OrderGroup) => {
|
||||
const next = group.state === GroupState.OPEN ? GroupState.LOCKED : GroupState.OPEN;
|
||||
return refresh(() => setGroupState({ body: { id: group.id, state: next } }));
|
||||
};
|
||||
|
||||
const handleConfirmOrdered = async (group: OrderGroup) => {
|
||||
setConfirmOrderGroup(null);
|
||||
await refresh(() => setGroupState({ body: { id: group.id, state: GroupState.ORDERED } }));
|
||||
};
|
||||
|
||||
const handleRevertOrdered = (group: OrderGroup) =>
|
||||
refresh(() => setGroupState({ body: { id: group.id, state: GroupState.LOCKED } }));
|
||||
|
||||
const handleDelete = (groupId: string) =>
|
||||
refresh(() => deleteGroup({ body: { id: groupId } }));
|
||||
|
||||
const handleSaveAmount = async (groupId: string, login: string) => {
|
||||
const key = `${groupId}:${login}`;
|
||||
const raw = editAmounts[key];
|
||||
const n = parseFloat(raw ?? '');
|
||||
if (!raw || isNaN(n) || n < 0) {
|
||||
setPageError('Zadejte platnou kladnou částku');
|
||||
return;
|
||||
}
|
||||
const ok = await refresh(() => updateGroupMember({ body: { id: groupId, login, amount: Math.round(n * 100) } }));
|
||||
if (ok) setEditAmounts(prev => { const next = { ...prev }; delete next[key]; return next; });
|
||||
};
|
||||
|
||||
const handleSaveNote = async (groupId: string, login: string) => {
|
||||
const key = `${groupId}:${login}`;
|
||||
const note = editNotes[key] ?? '';
|
||||
const ok = await refresh(() => updateGroupMember({ body: { id: groupId, login, note } }));
|
||||
if (ok) setEditNotes(prev => { const next = { ...prev }; delete next[key]; return next; });
|
||||
};
|
||||
|
||||
const handleSaveSurcharge = async (groupId: string, login: string) => {
|
||||
const key = `${groupId}:${login}`;
|
||||
const surchargeText = editSurcharges[key]?.text ?? '';
|
||||
const rawAmount = editSurcharges[key]?.amount ?? '';
|
||||
const surchargeAmount = rawAmount === '' ? 0 : parseFloat(rawAmount.replace(',', '.'));
|
||||
if (rawAmount !== '' && (isNaN(surchargeAmount) || surchargeAmount < 0)) {
|
||||
setPageError('Zadejte platnou výši příplatku');
|
||||
return;
|
||||
}
|
||||
const ok = await refresh(() => updateGroupMember({ body: { id: groupId, login, surchargeText, surchargeAmount: rawAmount === '' ? 0 : Math.round(surchargeAmount * 100) } }));
|
||||
if (ok) setEditSurcharges(prev => { const next = { ...prev }; delete next[key]; return next; });
|
||||
};
|
||||
|
||||
const handleSaveTimes = async (group: OrderGroup) => {
|
||||
const times = editTimes[group.id];
|
||||
if (!times) return;
|
||||
const { orderedAt, deliveryAt } = times;
|
||||
if (orderedAt && !TIME_REGEX.test(orderedAt)) {
|
||||
setPageError('Čas objednání musí být ve formátu HH:MM');
|
||||
return;
|
||||
}
|
||||
if (deliveryAt && !TIME_REGEX.test(deliveryAt)) {
|
||||
setPageError('Čas doručení musí být ve formátu HH:MM');
|
||||
return;
|
||||
}
|
||||
const ok = await refresh(() => updateGroupTimes({ body: { id: group.id, orderedAt, deliveryAt } }));
|
||||
if (ok) setEditTimes(prev => { const next = { ...prev }; delete next[group.id]; return next; });
|
||||
};
|
||||
|
||||
const canEditMember = (group: OrderGroup, targetLogin: string) => {
|
||||
if (group.state === GroupState.ORDERED) return false;
|
||||
if (auth?.login === group.creatorLogin) return true;
|
||||
if (auth?.login === targetLogin && group.state === GroupState.OPEN) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const canManageMembers = (group: OrderGroup) => {
|
||||
if (group.state === GroupState.ORDERED) return false;
|
||||
if (auth?.login === group.creatorLogin) return true;
|
||||
return group.state === GroupState.OPEN;
|
||||
};
|
||||
|
||||
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 stores = data.stores ?? [];
|
||||
const groups = data.groups ?? [];
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<Header choices={data.choices} />
|
||||
<div className="wrapper">
|
||||
<div className="d-flex align-items-center justify-content-between mb-1">
|
||||
<h1 className="title mb-0">Objednání</h1>
|
||||
<Button variant="outline-secondary" size="sm" onClick={() => setAdminModalOpen(true)} title="Správa obchodů">
|
||||
<FontAwesomeIcon icon={faGear} />
|
||||
</Button>
|
||||
</div>
|
||||
<p style={{ color: 'var(--luncher-text-muted)' }}>Skupinové objednávky z obchodů a restaurací</p>
|
||||
|
||||
{pageError && (
|
||||
<Alert variant="danger" dismissible onClose={() => setPageError(null)} className="mt-2">
|
||||
{pageError}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="content-wrapper">
|
||||
<div className="content" style={{ maxWidth: 1200 }}>
|
||||
{/* Vytvoření nové skupiny */}
|
||||
<div className="choice-section fade-in mb-4">
|
||||
<h5>Vytvořit skupinu</h5>
|
||||
{stores.length === 0 ? (
|
||||
<p className="text-muted">
|
||||
Nejsou přidány žádné obchody.{' '}
|
||||
<Button variant="link" size="sm" className="p-0" onClick={() => setAdminModalOpen(true)}>
|
||||
Přidat obchod
|
||||
</Button>
|
||||
</p>
|
||||
) : (
|
||||
<div className="d-flex gap-2 align-items-end flex-wrap">
|
||||
<Form.Select
|
||||
value={newGroupName}
|
||||
onChange={e => setNewGroupName(e.target.value)}
|
||||
style={{ maxWidth: 260 }}
|
||||
>
|
||||
<option value="">— vyberte obchod —</option>
|
||||
{stores.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</Form.Select>
|
||||
<Button variant="primary" onClick={handleCreate} disabled={creating || !newGroupName}>
|
||||
Vytvořit skupinu
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Seznam skupin */}
|
||||
{groups.length === 0 && (
|
||||
<p className="text-muted fade-in">Zatím žádné skupiny pro dnešní den.</p>
|
||||
)}
|
||||
|
||||
{groups.map(group => {
|
||||
const login = auth!.login ?? '';
|
||||
const isCreator = login === group.creatorLogin;
|
||||
const isMember = login in group.members;
|
||||
const isOrdered = group.state === GroupState.ORDERED;
|
||||
const isLocked = group.state === GroupState.LOCKED;
|
||||
const memberEntries = Object.entries(group.members) as [string, OrderGroupMember][];
|
||||
const memberCount = memberEntries.length;
|
||||
const editingTimes = group.id in editTimes;
|
||||
|
||||
const totalFees = (group.fees ?? 0) + (group.shipping ?? 0) + (group.tip ?? 0);
|
||||
const feeShare = memberCount > 0 ? Math.round(totalFees / memberCount) : 0;
|
||||
const getMemberTotal = (m: OrderGroupMember) => {
|
||||
const base = m.amount ?? 0;
|
||||
const surcharge = m.surchargeAmount ?? 0;
|
||||
const dv = group.discountValue ?? 0;
|
||||
const discount = dv > 0
|
||||
? (group.discountType === 'percent'
|
||||
? Math.round((base + surcharge) * dv / 100)
|
||||
: Math.round(dv / memberCount))
|
||||
: 0;
|
||||
return base + surcharge + feeShare - discount;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card key={group.id} className="mb-3 fade-in">
|
||||
<Card.Header className="d-flex justify-content-between align-items-center">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<strong>{group.name}</strong>
|
||||
{stateBadge(group.state)}
|
||||
<small className="text-muted">zakladatel: {group.creatorLogin}</small>
|
||||
</div>
|
||||
<div className="d-flex gap-2">
|
||||
{isCreator && !isOrdered && (
|
||||
<>
|
||||
<Button variant="outline-info" size="sm" onClick={() => setFeesModal(group)} title="Upravit poplatky a slevu">
|
||||
Poplatky
|
||||
</Button>
|
||||
<Button variant="outline-secondary" size="sm" onClick={() => handleToggleLock(group)} title={isLocked ? 'Odemknout' : 'Uzamknout'}>
|
||||
<FontAwesomeIcon icon={isLocked ? faLockOpen : faLock} />
|
||||
</Button>
|
||||
{isLocked && (
|
||||
<Button variant="outline-primary" size="sm" onClick={() => setConfirmOrderGroup(group)}>
|
||||
Objednáno
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline-danger" size="sm" onClick={() => handleDelete(group.id)} title="Smazat skupinu">
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{isCreator && isOrdered && (
|
||||
<>
|
||||
{settings?.bankAccount && settings?.holderName && !group.qrGenerated && (
|
||||
<Button variant="primary" size="sm" onClick={() => setPayModal(group)}>
|
||||
<FontAwesomeIcon icon={faBasketShopping} className="me-1" />
|
||||
Generovat QR
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline-warning" size="sm" onClick={() => handleRevertOrdered(group)} title="Vrátit na Uzamčeno (smaže QR kódy)">
|
||||
<FontAwesomeIcon icon={faLockOpen} />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!isMember && !isOrdered && !isLocked && (
|
||||
<Button variant="outline-success" size="sm" onClick={() => handleJoin(group.id)}>
|
||||
<FontAwesomeIcon icon={faUserPlus} className="me-1" />
|
||||
Přidat se
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Body className="p-0">
|
||||
<Table className="mb-0" size="sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Člen</th>
|
||||
<th style={{ width: 180 }}>Částka (bez slev)</th>
|
||||
<th style={{ width: 220 }}>Příplatek</th>
|
||||
<th>Poznámka</th>
|
||||
<th style={{ width: 160 }}>Celkem (s poplatky)</th>
|
||||
<th style={{ width: 40 }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{memberEntries.map(([memberLogin, member]) => {
|
||||
const key = `${group.id}:${memberLogin}`;
|
||||
const editingAmount = key in editAmounts;
|
||||
const editingNote = key in editNotes;
|
||||
const editingSurcharge = key in editSurcharges;
|
||||
const canEdit = canEditMember(group, memberLogin);
|
||||
const memberTotal = getMemberTotal(member);
|
||||
return (
|
||||
<tr key={memberLogin}>
|
||||
<td>
|
||||
<span className="user-info">
|
||||
<strong>{memberLogin}</strong>
|
||||
{memberLogin === group.creatorLogin && (
|
||||
<OverlayTrigger placement="top" overlay={<Tooltip>Zakladatel / objednávající</Tooltip>}>
|
||||
<span className="ms-1"><FontAwesomeIcon icon={faBasketShopping} className="buyer-icon" /></span>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{member.paid && (
|
||||
<OverlayTrigger placement="top" overlay={<Tooltip>Zaplaceno</Tooltip>}>
|
||||
<span className="ms-1"><FontAwesomeIcon icon={faCircleCheck} className="text-success" /></span>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{canEdit && editingAmount ? (
|
||||
<div className="d-flex gap-1">
|
||||
<Form.Control
|
||||
type="number"
|
||||
size="sm"
|
||||
value={editAmounts[key]}
|
||||
onChange={e => setEditAmounts(prev => ({ ...prev, [key]: e.target.value }))}
|
||||
onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveAmount(group.id, memberLogin); if (e.key === 'Escape') setEditAmounts(prev => { const n = { ...prev }; delete n[key]; return n; }); }}
|
||||
style={{ width: 95 }}
|
||||
autoFocus
|
||||
/>
|
||||
<Button size="sm" variant="outline-success" onClick={() => handleSaveAmount(group.id, memberLogin)}>✓</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
style={{ cursor: canEdit ? 'pointer' : undefined }}
|
||||
onClick={() => canEdit && setEditAmounts(prev => ({ ...prev, [key]: member.amount != null ? String(member.amount / 100) : '' }))}
|
||||
title={canEdit ? 'Klikněte pro úpravu' : undefined}
|
||||
>
|
||||
{member.amount != null ? `${member.amount / 100} Kč` : <span className="text-muted">—</span>}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{canEdit && editingSurcharge ? (
|
||||
<div className="d-flex gap-1">
|
||||
<Form.Control
|
||||
type="text"
|
||||
size="sm"
|
||||
placeholder="popis"
|
||||
value={editSurcharges[key]?.text ?? ''}
|
||||
onChange={e => setEditSurcharges(prev => ({ ...prev, [key]: { ...prev[key], text: e.target.value } }))}
|
||||
onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveSurcharge(group.id, memberLogin); if (e.key === 'Escape') setEditSurcharges(prev => { const n = { ...prev }; delete n[key]; return n; }); }}
|
||||
style={{ width: 80 }}
|
||||
autoFocus
|
||||
/>
|
||||
<Form.Control
|
||||
type="number"
|
||||
size="sm"
|
||||
placeholder="Kč"
|
||||
value={editSurcharges[key]?.amount ?? ''}
|
||||
onChange={e => setEditSurcharges(prev => ({ ...prev, [key]: { ...prev[key], amount: e.target.value } }))}
|
||||
onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveSurcharge(group.id, memberLogin); if (e.key === 'Escape') setEditSurcharges(prev => { const n = { ...prev }; delete n[key]; return n; }); }}
|
||||
style={{ width: 60 }}
|
||||
/>
|
||||
<Button size="sm" variant="outline-success" onClick={() => handleSaveSurcharge(group.id, memberLogin)}>✓</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
style={{ cursor: canEdit ? 'pointer' : undefined }}
|
||||
onClick={() => canEdit && setEditSurcharges(prev => ({ ...prev, [key]: { text: member.surchargeText ?? '', amount: member.surchargeAmount != null ? String(member.surchargeAmount / 100) : '' } }))}
|
||||
title={canEdit ? 'Klikněte pro úpravu příplatku' : undefined}
|
||||
>
|
||||
{member.surchargeAmount != null && member.surchargeAmount > 0 ? (
|
||||
<small>{member.surchargeText ? `${member.surchargeText}: ` : ''}<strong>{member.surchargeAmount / 100} Kč</strong></small>
|
||||
) : (
|
||||
<small className="text-muted">—</small>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{canEdit && editingNote ? (
|
||||
<div className="d-flex gap-1">
|
||||
<Form.Control
|
||||
type="text"
|
||||
size="sm"
|
||||
value={editNotes[key]}
|
||||
onChange={e => setEditNotes(prev => ({ ...prev, [key]: e.target.value }))}
|
||||
onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveNote(group.id, memberLogin); if (e.key === 'Escape') setEditNotes(prev => { const n = { ...prev }; delete n[key]; return n; }); }}
|
||||
autoFocus
|
||||
/>
|
||||
<Button size="sm" variant="outline-success" onClick={() => handleSaveNote(group.id, memberLogin)}>✓</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
style={{ cursor: canEdit ? 'pointer' : undefined }}
|
||||
onClick={() => canEdit && setEditNotes(prev => ({ ...prev, [key]: member.note ?? '' }))}
|
||||
title={canEdit ? 'Klikněte pro úpravu poznámky' : undefined}
|
||||
>
|
||||
<small className="text-muted">{member.note || '—'}</small>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-end">
|
||||
<small className={memberTotal > 0 ? 'fw-bold' : 'text-muted'}>
|
||||
{memberTotal > 0 ? `${memberTotal / 100} Kč` : '—'}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div className="d-flex gap-1 justify-content-end">
|
||||
{canManageMembers(group) && (isCreator || memberLogin === login) && (memberLogin !== group.creatorLogin) && (
|
||||
<FontAwesomeIcon
|
||||
icon={faTrashCan}
|
||||
className="action-icon"
|
||||
title={memberLogin === login ? 'Odhlásit se' : 'Odebrat z skupiny'}
|
||||
onClick={() => refresh(() => removeGroupMember({ body: { id: group.id, login: memberLogin } }))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
{(() => {
|
||||
const sumBase = memberEntries.reduce((sum, [, m]) => sum + (m.amount ?? 0) + (m.surchargeAmount ?? 0), 0);
|
||||
const dv = group.discountValue ?? 0;
|
||||
const totalDiscount = dv > 0
|
||||
? (group.discountType === 'percent' ? Math.round(sumBase * dv / 100) : dv)
|
||||
: 0;
|
||||
const groupTotal = sumBase + totalFees - totalDiscount;
|
||||
return groupTotal > 0 ? (
|
||||
<tfoot>
|
||||
<tr style={{ fontWeight: 700, borderTop: '2px solid var(--luncher-border)' }}>
|
||||
<td colSpan={4} className="text-end" style={{ fontSize: '0.9em' }}>Celkem za skupinu:</td>
|
||||
<td className="text-end">{groupTotal / 100} Kč</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
) : null;
|
||||
})()}
|
||||
</Table>
|
||||
|
||||
{/* Souhrn poplatků a slevy */}
|
||||
{(totalFees > 0 || (group.discountValue != null && group.discountValue > 0)) && (
|
||||
<div className="px-3 py-2 border-top d-flex gap-3 flex-wrap" style={{ fontSize: '0.85em', color: 'var(--luncher-text-muted)' }}>
|
||||
{group.fees != null && group.fees > 0 && <span>Poplatky: <strong>{group.fees / 100} Kč</strong></span>}
|
||||
{group.shipping != null && group.shipping > 0 && <span>Doprava: <strong>{group.shipping / 100} Kč</strong></span>}
|
||||
{group.tip != null && group.tip > 0 && <span>Spropitné: <strong>{group.tip / 100} Kč</strong></span>}
|
||||
{feeShare > 0 && <span>→ <strong>{feeShare / 100} Kč</strong>/os.</span>}
|
||||
{group.discountValue != null && group.discountValue > 0 && (
|
||||
<span className="text-success">
|
||||
Sleva: <strong>{group.discountType === 'percent' ? `${group.discountValue}%` : `${group.discountValue / 100} Kč`}</strong>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Časy objednání a doručení */}
|
||||
{isOrdered && (
|
||||
<div className="px-3 py-2 border-top">
|
||||
{isCreator && editingTimes ? (
|
||||
<div className="d-flex align-items-center gap-3 flex-wrap">
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<small className="text-muted text-nowrap">Objednáno v:</small>
|
||||
<Form.Control
|
||||
type="text"
|
||||
size="sm"
|
||||
placeholder="HH:MM"
|
||||
value={editTimes[group.id]?.orderedAt ?? ''}
|
||||
onChange={e => setEditTimes(prev => ({ ...prev, [group.id]: { ...prev[group.id], orderedAt: e.target.value } }))}
|
||||
onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveTimes(group); }}
|
||||
style={{ width: 75 }}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<small className="text-muted text-nowrap">Doručení v:</small>
|
||||
<Form.Control
|
||||
type="text"
|
||||
size="sm"
|
||||
placeholder="HH:MM"
|
||||
value={editTimes[group.id]?.deliveryAt ?? ''}
|
||||
onChange={e => setEditTimes(prev => ({ ...prev, [group.id]: { ...prev[group.id], deliveryAt: e.target.value } }))}
|
||||
onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleSaveTimes(group); }}
|
||||
style={{ width: 75 }}
|
||||
/>
|
||||
</div>
|
||||
<Button size="sm" variant="outline-success" onClick={() => handleSaveTimes(group)}>Uložit</Button>
|
||||
<Button size="sm" variant="outline-secondary" onClick={() => setEditTimes(prev => { const n = { ...prev }; delete n[group.id]; return n; })}>Zrušit</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="d-flex align-items-center gap-3 flex-wrap"
|
||||
style={{ cursor: isCreator ? 'pointer' : undefined }}
|
||||
onClick={() => isCreator && setEditTimes(prev => ({ ...prev, [group.id]: { orderedAt: group.orderedAt ?? '', deliveryAt: group.deliveryAt ?? '' } }))}
|
||||
title={isCreator ? 'Klikněte pro úpravu časů' : undefined}
|
||||
>
|
||||
<small className="text-muted">
|
||||
Objednáno v: <strong>{group.orderedAt ?? '—'}</strong>
|
||||
</small>
|
||||
<small className="text-muted">
|
||||
Doručení v: <strong>{group.deliveryAt ?? '—'}</strong>
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
{/* Potvrzovací dialog pro přechod do stavu Objednáno */}
|
||||
<Modal show={!!confirmOrderGroup} onHide={() => setConfirmOrderGroup(null)} centered>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Potvrdit objednání</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
Opravdu chcete označit skupinu <strong>{confirmOrderGroup?.name}</strong> jako objednanou?
|
||||
Tato akce uzavře skupinu a zaznamená čas objednání.
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => setConfirmOrderGroup(null)}>Zrušit</Button>
|
||||
<Button variant="primary" onClick={() => confirmOrderGroup && handleConfirmOrdered(confirmOrderGroup)}>
|
||||
Objednáno
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<StoreAdminModal
|
||||
isOpen={adminModalOpen}
|
||||
onClose={() => setAdminModalOpen(false)}
|
||||
stores={stores}
|
||||
onStoresChanged={updated => setData(prev => prev ? { ...prev, stores: updated } : prev)}
|
||||
/>
|
||||
|
||||
{payModal && settings?.bankAccount && settings?.holderName && (
|
||||
<PayForGroupModal
|
||||
isOpen={!!payModal}
|
||||
onClose={() => setPayModal(null)}
|
||||
onSuccess={fetchData}
|
||||
group={payModal}
|
||||
groupId={payModal.id}
|
||||
payerLogin={auth.login}
|
||||
bankAccount={settings.bankAccount}
|
||||
bankAccountHolder={settings.holderName}
|
||||
/>
|
||||
)}
|
||||
|
||||
{feesModal && (
|
||||
<EditGroupFeesModal
|
||||
isOpen={!!feesModal}
|
||||
onClose={() => setFeesModal(null)}
|
||||
group={feesModal}
|
||||
onSaved={newData => {
|
||||
if (newData) {
|
||||
setData(newData);
|
||||
socket.emit?.('message', newData as ClientData);
|
||||
}
|
||||
setFeesModal(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,10 @@ import path from 'path';
|
||||
// Use 127.0.0.1 explicitly — on Node.js 18+/Windows, `localhost` may resolve to ::1
|
||||
// (IPv6) while the HTTP server only binds to 0.0.0.0 (IPv4), causing the webServer
|
||||
// readiness poll to time out even though the server is listening.
|
||||
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://127.0.0.1:3001';
|
||||
// Port 3099 avoids conflicts with locally running Docker containers on 3001-3003.
|
||||
// Override with E2E_PORT env var if needed.
|
||||
const E2E_PORT = process.env.E2E_PORT ?? '3099';
|
||||
const BASE_URL = process.env.E2E_BASE_URL ?? `http://127.0.0.1:${E2E_PORT}`;
|
||||
|
||||
// Server env vars injected for local runs. In CI these are set at the step level.
|
||||
const serverEnv: Record<string, string> = {
|
||||
@@ -15,6 +18,7 @@ const serverEnv: Record<string, string> = {
|
||||
HTTP_REMOTE_USER_ENABLED: 'true',
|
||||
HTTP_REMOTE_USER_HEADER_NAME: 'remote-user',
|
||||
HTTP_REMOTE_TRUSTED_IPS: process.env.HTTP_REMOTE_TRUSTED_IPS ?? '127.0.0.1,::1,::ffff:127.0.0.1',
|
||||
PORT: E2E_PORT,
|
||||
};
|
||||
if (process.env.REDIS_HOST) {
|
||||
serverEnv.REDIS_HOST = process.env.REDIS_HOST;
|
||||
@@ -50,7 +54,7 @@ export default defineConfig({
|
||||
cwd: path.resolve(__dirname, '../server'),
|
||||
// Poll a dedicated health endpoint — polling '/' can stall in Express 5 when
|
||||
// server/public/ doesn't exist in the working directory (no finalhandler match).
|
||||
url: `http://127.0.0.1:3001/api/health`,
|
||||
url: `http://127.0.0.1:${E2E_PORT}/api/health`,
|
||||
timeout: 15_000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
env: serverEnv,
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# Spustí server a klienta v samostatných panelech jednoho okna Windows Terminalu.
|
||||
# Vyžaduje Windows Terminal (wt.exe) — výchozí součást Windows 11.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ScriptDir = $PSScriptRoot
|
||||
|
||||
Push-Location (Join-Path $ScriptDir 'types')
|
||||
try { yarn openapi-ts } finally { Pop-Location }
|
||||
|
||||
if (-not (Get-Command wt.exe -ErrorAction SilentlyContinue)) {
|
||||
Write-Error "wt.exe (Windows Terminal) nebyl nalezen. Nainstalujte z Microsoft Store nebo použijte run_dev.sh v WSL."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$serverDir = Join-Path $ScriptDir 'server'
|
||||
$clientDir = Join-Path $ScriptDir 'client'
|
||||
|
||||
# wt splits on ';' before respecting quoting, so encode the compound server command to avoid it
|
||||
$serverCmd = '$env:NODE_ENV = ''development''; yarn startReload'
|
||||
$serverCmdB64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($serverCmd))
|
||||
|
||||
wt -w 0 new-tab --title 'luncher-server' -d $serverDir pwsh -NoExit -EncodedCommand $serverCmdB64 `; `
|
||||
split-pane -H --title 'luncher-client' -d $clientDir pwsh -NoExit -Command "yarn start"
|
||||
@@ -47,4 +47,8 @@
|
||||
|
||||
# Heslo pro bypass rate limitu na endpointu /api/food/refresh (pro skripty/admin).
|
||||
# Bez hesla může refresh volat každý přihlášený uživatel (podléhá rate limitu).
|
||||
# REFRESH_BYPASS_PASSWORD=
|
||||
# REFRESH_BYPASS_PASSWORD=
|
||||
|
||||
# Admin heslo pro správu seznamu obchodů na stránce /objednani.
|
||||
# Bez hesla nelze přidávat ani odebírat obchody ze seznamu (POST/DELETE na /api/stores vrátí 403).
|
||||
# ADMIN_PASSWORD=
|
||||
@@ -1,3 +0,0 @@
|
||||
[
|
||||
"Evidence večeří a pozdních obědů na samostatné stránce (/vecere)"
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"Skupinové objednávky s QR platbou — stránka /objednani (více skupin, každá z jiného obchodu, stavový automat open/locked/ordered)"
|
||||
]
|
||||
+8
-8
@@ -9,13 +9,13 @@ import jwt from 'jsonwebtoken';
|
||||
*/
|
||||
export function generateToken(login?: string, trusted?: boolean): string {
|
||||
if (!process.env.JWT_SECRET) {
|
||||
throw Error("Není vyplněna proměnná prostředí JWT_SECRET");
|
||||
throw new Error("Není vyplněna proměnná prostředí JWT_SECRET");
|
||||
}
|
||||
if (process.env.JWT_SECRET.length < 32) {
|
||||
throw Error("Proměnná prostředí JWT_SECRET musí být minimálně 32 znaků");
|
||||
throw new Error("Proměnná prostředí JWT_SECRET musí být minimálně 32 znaků");
|
||||
}
|
||||
if (!login || login.trim().length === 0) {
|
||||
throw Error("Nebyl předán login");
|
||||
throw new Error("Nebyl předán login");
|
||||
}
|
||||
const payload = { login, trusted: trusted || false, logoutUrl: process.env.LOGOUT_URL };
|
||||
return jwt.sign(payload, process.env.JWT_SECRET);
|
||||
@@ -28,7 +28,7 @@ export function generateToken(login?: string, trusted?: boolean): string {
|
||||
*/
|
||||
export function verify(token: string): boolean {
|
||||
if (!process.env.JWT_SECRET) {
|
||||
throw Error("Není vyplněna proměnná prostředí JWT_SECRET");
|
||||
throw new Error("Není vyplněna proměnná prostředí JWT_SECRET");
|
||||
}
|
||||
try {
|
||||
jwt.verify(token, process.env.JWT_SECRET);
|
||||
@@ -45,10 +45,10 @@ export function verify(token: string): boolean {
|
||||
*/
|
||||
export function getLogin(token?: string): string {
|
||||
if (!process.env.JWT_SECRET) {
|
||||
throw Error("Není vyplněna proměnná prostředí JWT_SECRET");
|
||||
throw new Error("Není vyplněna proměnná prostředí JWT_SECRET");
|
||||
}
|
||||
if (!token) {
|
||||
throw Error("Nebyl předán token");
|
||||
throw new Error("Nebyl předán token");
|
||||
}
|
||||
const payload: any = jwt.verify(token, process.env.JWT_SECRET);
|
||||
return payload.login;
|
||||
@@ -61,10 +61,10 @@ export function getLogin(token?: string): string {
|
||||
*/
|
||||
export function getTrusted(token?: string): boolean {
|
||||
if (!process.env.JWT_SECRET) {
|
||||
throw Error("Není vyplněna proměnná prostředí JWT_SECRET");
|
||||
throw new Error("Není vyplněna proměnná prostředí JWT_SECRET");
|
||||
}
|
||||
if (!token) {
|
||||
throw Error("Nebyl předán token");
|
||||
throw new Error("Nebyl předán token");
|
||||
}
|
||||
const payload: any = jwt.verify(token, process.env.JWT_SECRET);
|
||||
return payload.trusted || false;
|
||||
|
||||
@@ -28,16 +28,16 @@ const buildPizzaUrl = (pizzaUrl: string) => {
|
||||
return `${baseUrl}/${pizzaUrl}`;
|
||||
}
|
||||
|
||||
// Ceny krabic dle velikosti
|
||||
// Ceny krabic dle velikosti v haléřích
|
||||
const boxPrices: { [key: string]: number } = {
|
||||
"30cm": 13,
|
||||
"35cm": 15,
|
||||
"40cm": 18,
|
||||
"50cm": 25
|
||||
"30cm": 1300,
|
||||
"35cm": 1500,
|
||||
"40cm": 1800,
|
||||
"50cm": 2500
|
||||
}
|
||||
|
||||
// Cena obalu pro salát
|
||||
const SALAT_BOX_PRICE = 13;
|
||||
// Cena obalu pro salát v haléřích
|
||||
const SALAT_BOX_PRICE = 1300;
|
||||
|
||||
/**
|
||||
* Stáhne a scrapne aktuální pizzy ze stránek Pizza Chefie.
|
||||
@@ -79,7 +79,7 @@ export async function downloadPizzy(mock: boolean): Promise<Pizza[]> {
|
||||
a.each((i, elm) => {
|
||||
const varId = Number.parseInt(elm.attribs.href.split('?varianta=')[1].trim());
|
||||
const size = $($(elm).contents().get(0)).text().trim();
|
||||
const price = Number.parseInt($($(elm).contents().get(1)).text().trim().split(" Kč")[0]);
|
||||
const price = Number.parseInt($($(elm).contents().get(1)).text().trim().split(" Kč")[0]) * 100;
|
||||
sizes.push({ varId, size, pizzaPrice: price, boxPrice: boxPrices[size], price: price + boxPrices[size] });
|
||||
})
|
||||
result.push({
|
||||
@@ -119,7 +119,7 @@ export async function downloadSalaty(mock: boolean): Promise<Salat[]> {
|
||||
ingredients.push($(elm).text());
|
||||
});
|
||||
const priceText = $('.cena > span', salatHtml).first().text().trim();
|
||||
const price = Number.parseInt(priceText.split(' Kč')[0]);
|
||||
const price = Number.parseInt(priceText.split(' Kč')[0]) * 100;
|
||||
result.push({ name, ingredients, price: price + SALAT_BOX_PRICE });
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import crypto from "crypto";
|
||||
import getStorage from "./storage";
|
||||
import { getClientData, getToday, initIfNeeded } from "./service";
|
||||
import { getStores } from "./stores";
|
||||
import { removePendingQrsByGroupId } from "./pizza";
|
||||
import { ClientData, GroupState, MealSlot, OrderGroup, OrderGroupMember } from "../../types/gen/types.gen";
|
||||
import { formatDate } from "./utils";
|
||||
|
||||
const storage = getStorage();
|
||||
|
||||
async function getExtraData(date?: Date): Promise<ClientData> {
|
||||
await initIfNeeded(date, MealSlot.EXTRA);
|
||||
const data = await getClientData(date, MealSlot.EXTRA);
|
||||
data.stores = await getStores();
|
||||
return data;
|
||||
}
|
||||
|
||||
function getExtraKey(date?: Date): string {
|
||||
return `${formatDate(date ?? getToday())}_extra`;
|
||||
}
|
||||
|
||||
async function saveExtraData(data: ClientData, date?: Date): Promise<ClientData> {
|
||||
await storage.setData(getExtraKey(date), data);
|
||||
return data;
|
||||
}
|
||||
|
||||
function findGroup(data: ClientData, id: string): OrderGroup | undefined {
|
||||
return data.groups?.find(g => g.id === id);
|
||||
}
|
||||
|
||||
export async function createGroup(creatorLogin: string, name: string, date?: Date): Promise<ClientData> {
|
||||
const stores = await getStores();
|
||||
if (!stores.some(s => s.toLowerCase() === name.trim().toLowerCase())) {
|
||||
throw new Error('Obchod není v seznamu povolených obchodů');
|
||||
}
|
||||
const data = await getExtraData(date);
|
||||
const canonical = stores.find(s => s.toLowerCase() === name.trim().toLowerCase())!;
|
||||
const group: OrderGroup = {
|
||||
id: crypto.randomUUID(),
|
||||
name: canonical,
|
||||
creatorLogin,
|
||||
state: GroupState.OPEN,
|
||||
members: { [creatorLogin]: {} },
|
||||
};
|
||||
data.groups = [...(data.groups ?? []), group];
|
||||
return saveExtraData(data, date);
|
||||
}
|
||||
|
||||
export async function deleteGroup(login: string, groupId: string, date?: Date): Promise<ClientData> {
|
||||
const data = await getExtraData(date);
|
||||
const group = findGroup(data, groupId);
|
||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||
if (group.creatorLogin !== login) throw new Error('Skupinu může smazat pouze zakladatel');
|
||||
data.groups = (data.groups ?? []).filter(g => g.id !== groupId);
|
||||
return saveExtraData(data, date);
|
||||
}
|
||||
|
||||
export async function addGroupMember(login: string, groupId: string, targetLogin: string, date?: Date): Promise<ClientData> {
|
||||
const data = await getExtraData(date);
|
||||
const group = findGroup(data, groupId);
|
||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||
if (group.state === GroupState.ORDERED) throw new Error('Skupinu ve stavu "objednáno" nelze upravovat');
|
||||
if (login !== group.creatorLogin && login !== targetLogin) {
|
||||
throw new Error('Přidat jiného uživatele může pouze zakladatel');
|
||||
}
|
||||
if (group.state === GroupState.LOCKED && login !== group.creatorLogin) {
|
||||
throw new Error('Skupinu ve stavu "uzamčeno" může upravovat pouze zakladatel');
|
||||
}
|
||||
if (group.members[targetLogin]) throw new Error('Uživatel je již ve skupině');
|
||||
group.members[targetLogin] = {};
|
||||
return saveExtraData(data, date);
|
||||
}
|
||||
|
||||
export async function removeGroupMember(login: string, groupId: string, targetLogin: string, date?: Date): Promise<ClientData> {
|
||||
const data = await getExtraData(date);
|
||||
const group = findGroup(data, groupId);
|
||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||
if (group.state === GroupState.ORDERED) throw new Error('Skupinu ve stavu "objednáno" nelze upravovat');
|
||||
if (login !== group.creatorLogin && login !== targetLogin) {
|
||||
throw new Error('Odebrat jiného uživatele může pouze zakladatel');
|
||||
}
|
||||
if (group.state === GroupState.LOCKED && login !== group.creatorLogin) {
|
||||
throw new Error('Skupinu ve stavu "uzamčeno" může upravovat pouze zakladatel');
|
||||
}
|
||||
if (targetLogin === group.creatorLogin) throw new Error('Zakladatel skupiny nemůže být odebrán');
|
||||
if (!group.members[targetLogin]) throw new Error('Uživatel není ve skupině');
|
||||
delete group.members[targetLogin];
|
||||
return saveExtraData(data, date);
|
||||
}
|
||||
|
||||
export async function updateGroupMember(login: string, groupId: string, targetLogin: string, patch: Partial<OrderGroupMember>, date?: Date): Promise<ClientData> {
|
||||
const data = await getExtraData(date);
|
||||
const group = findGroup(data, groupId);
|
||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||
if (group.state === GroupState.ORDERED) throw new Error('Skupinu ve stavu "objednáno" nelze upravovat');
|
||||
const isSelf = login === targetLogin;
|
||||
const isCreator = login === group.creatorLogin;
|
||||
if (!isSelf && !isCreator) throw new Error('Upravit jiného uživatele může pouze zakladatel');
|
||||
if (!isCreator && group.state === GroupState.LOCKED) {
|
||||
throw new Error('Skupinu ve stavu "uzamčeno" může upravovat pouze zakladatel');
|
||||
}
|
||||
if (!group.members[targetLogin]) throw new Error('Uživatel není ve skupině');
|
||||
group.members[targetLogin] = { ...group.members[targetLogin], ...patch };
|
||||
return saveExtraData(data, date);
|
||||
}
|
||||
|
||||
const VALID_TRANSITIONS: Record<GroupState, GroupState[]> = {
|
||||
[GroupState.OPEN]: [GroupState.LOCKED],
|
||||
[GroupState.LOCKED]: [GroupState.OPEN, GroupState.ORDERED],
|
||||
[GroupState.ORDERED]: [GroupState.LOCKED],
|
||||
};
|
||||
|
||||
function getCurrentHHMM(): string {
|
||||
const now = new Date();
|
||||
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export async function setGroupState(login: string, groupId: string, newState: GroupState, date?: Date): Promise<ClientData> {
|
||||
const data = await getExtraData(date);
|
||||
const group = findGroup(data, groupId);
|
||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||
if (group.creatorLogin !== login) throw new Error('Stav může měnit pouze zakladatel');
|
||||
if (!VALID_TRANSITIONS[group.state].includes(newState)) {
|
||||
throw new Error(`Nelze přejít ze stavu "${group.state}" do stavu "${newState}"`);
|
||||
}
|
||||
if (newState === GroupState.ORDERED) {
|
||||
group.orderedAt = getCurrentHHMM();
|
||||
}
|
||||
if (group.state === GroupState.ORDERED && newState === GroupState.LOCKED) {
|
||||
const memberLogins = Object.keys(group.members);
|
||||
await removePendingQrsByGroupId(memberLogins, groupId);
|
||||
group.orderedAt = undefined;
|
||||
group.deliveryAt = undefined;
|
||||
group.qrGenerated = undefined;
|
||||
for (const ml of memberLogins) {
|
||||
group.members[ml] = { ...group.members[ml], paid: undefined };
|
||||
}
|
||||
}
|
||||
group.state = newState;
|
||||
return saveExtraData(data, date);
|
||||
}
|
||||
|
||||
export async function markGroupQrGenerated(login: string, groupId: string, date?: Date): Promise<void> {
|
||||
const data = await getExtraData(date);
|
||||
const group = findGroup(data, groupId);
|
||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||
if (group.creatorLogin !== login) throw new Error('QR kódy může generovat pouze zakladatel');
|
||||
if (group.state !== GroupState.ORDERED) throw new Error('QR kódy lze generovat pouze ve stavu "objednáno"');
|
||||
group.qrGenerated = true;
|
||||
await saveExtraData(data, date);
|
||||
}
|
||||
|
||||
export async function markGroupMemberPaid(login: string, groupId: string, date?: Date): Promise<ClientData | null> {
|
||||
const data = await getExtraData(date);
|
||||
const group = findGroup(data, groupId);
|
||||
if (!group || !group.members[login]) return null;
|
||||
group.members[login] = { ...group.members[login], paid: true };
|
||||
return saveExtraData(data, date);
|
||||
}
|
||||
|
||||
export async function updateGroupFees(login: string, groupId: string, fees?: number, shipping?: number, tip?: number, discountType?: string, discountValue?: number, date?: Date): Promise<ClientData> {
|
||||
const data = await getExtraData(date);
|
||||
const group = findGroup(data, groupId);
|
||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||
if (group.creatorLogin !== login) throw new Error('Poplatky může měnit pouze zakladatel');
|
||||
if (group.state === GroupState.ORDERED) throw new Error('Skupinu ve stavu "objednáno" nelze upravovat');
|
||||
if (fees !== undefined) group.fees = fees > 0 ? fees : undefined;
|
||||
if (shipping !== undefined) group.shipping = shipping > 0 ? shipping : undefined;
|
||||
if (tip !== undefined) group.tip = tip > 0 ? tip : undefined;
|
||||
if (discountType !== undefined) group.discountType = (discountType as any) || undefined;
|
||||
if (discountValue !== undefined) group.discountValue = discountValue > 0 ? discountValue : undefined;
|
||||
return saveExtraData(data, date);
|
||||
}
|
||||
|
||||
export async function updateGroupTimes(login: string, groupId: string, orderedAt?: string, deliveryAt?: string, date?: Date): Promise<ClientData> {
|
||||
const data = await getExtraData(date);
|
||||
const group = findGroup(data, groupId);
|
||||
if (!group) throw new Error('Skupina nebyla nalezena');
|
||||
if (group.creatorLogin !== login) throw new Error('Časy může měnit pouze zakladatel');
|
||||
if (orderedAt !== undefined) group.orderedAt = orderedAt || undefined;
|
||||
if (deliveryAt !== undefined) group.deliveryAt = deliveryAt || undefined;
|
||||
return saveExtraData(data, date);
|
||||
}
|
||||
+30
-8
@@ -1,7 +1,7 @@
|
||||
import express from "express";
|
||||
import bodyParser from "body-parser";
|
||||
import cors from 'cors';
|
||||
import { getData, getDateForWeekIndex, getToday } from "./service";
|
||||
import { getData, addChoice, getDateForWeekIndex, getToday } from "./service";
|
||||
import { MealSlot } from "../../types/gen/types.gen";
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
@@ -9,8 +9,8 @@ import { getQr } from "./qr";
|
||||
import { generateToken, getLogin, verify } from "./auth";
|
||||
import { getIsWeekend, InsufficientPermissions, PizzaDayConflictError, parseToken } from "./utils";
|
||||
import { getPendingQrs } from "./pizza";
|
||||
import { initWebsocket } from "./websocket";
|
||||
import { startReminderScheduler } from "./pushReminder";
|
||||
import { initWebsocket, getWebsocket } from "./websocket";
|
||||
import { startReminderScheduler, verifyQuickChoiceToken } from "./pushReminder";
|
||||
import { storageReady } from "./storage";
|
||||
import pizzaDayRoutes from "./routes/pizzaDayRoutes";
|
||||
import foodRoutes, { refreshMetoda } from "./routes/foodRoutes";
|
||||
@@ -21,13 +21,15 @@ import notificationRoutes from "./routes/notificationRoutes";
|
||||
import qrRoutes from "./routes/qrRoutes";
|
||||
import devRoutes from "./routes/devRoutes";
|
||||
import changelogRoutes from "./routes/changelogRoutes";
|
||||
import groupRoutes from "./routes/groupRoutes";
|
||||
import storeRoutes from "./routes/storeRoutes";
|
||||
|
||||
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
|
||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
||||
|
||||
// Validace nastavení JWT tokenu - nemá bez něj smysl vůbec povolit server spustit
|
||||
if (!process.env.JWT_SECRET) {
|
||||
throw Error("Není vyplněna proměnná prostředí JWT_SECRET");
|
||||
throw new Error("Není vyplněna proměnná prostředí JWT_SECRET");
|
||||
}
|
||||
|
||||
const app = express();
|
||||
@@ -81,12 +83,12 @@ app.post("/api/login", (req, res) => {
|
||||
if (remoteUser && remoteUser.length > 0) {
|
||||
res.status(200).json(generateToken(Buffer.from(remoteUser, 'latin1').toString(), true));
|
||||
} else {
|
||||
throw Error("Je zapnuto přihlášení přes hlavičky, ale nepřišla hlavička nebo ??");
|
||||
throw new Error("Je zapnuto přihlášení přes hlavičky, ale nepřišla hlavička nebo ??");
|
||||
}
|
||||
} else {
|
||||
// Klasická autentizace loginem
|
||||
if (!req.body?.login || req.body.login.trim().length === 0) {
|
||||
throw Error("Nebyl předán login");
|
||||
throw new Error("Nebyl předán login");
|
||||
}
|
||||
// TODO zavést podmínky pro délku loginu (min i max)
|
||||
res.status(200).json(generateToken(req.body.login, false));
|
||||
@@ -114,6 +116,22 @@ app.get("/api/qr", async (req, res) => {
|
||||
// Přeskočení auth pro refresh dat xd
|
||||
app.use("/api/food/refresh", refreshMetoda);
|
||||
|
||||
// Rychlá akce z push notifikace — autentizace pomocí HMAC tokenu z push payloadu (SW nemá přístup k JWT)
|
||||
app.post("/api/notifications/push/quickChoice", async (req, res, next) => {
|
||||
try {
|
||||
const { login, token } = req.body ?? {};
|
||||
if (!login || typeof login !== 'string' || !token || typeof token !== 'string') {
|
||||
return res.status(400).json({ error: 'Chybí login nebo token' });
|
||||
}
|
||||
if (!verifyQuickChoiceToken(login, token)) {
|
||||
return res.status(403).json({ error: 'Neplatný token' });
|
||||
}
|
||||
const updatedData = await addChoice(login, false, 'NEOBEDVAM', undefined, undefined);
|
||||
getWebsocket().emit("message", updatedData);
|
||||
res.status(200).json({});
|
||||
} catch (e: any) { next(e); }
|
||||
});
|
||||
|
||||
/** Middleware ověřující JWT token */
|
||||
app.use("/api/", (req, res, next) => {
|
||||
if (HTTP_REMOTE_USER_ENABLED) {
|
||||
@@ -180,9 +198,13 @@ app.use("/api/notifications", notificationRoutes);
|
||||
app.use("/api/qr", qrRoutes);
|
||||
app.use("/api/dev", devRoutes);
|
||||
app.use("/api/changelogs", changelogRoutes);
|
||||
app.use("/api/groups", groupRoutes);
|
||||
app.use("/api/stores", storeRoutes);
|
||||
|
||||
app.use('/stats', express.static('public'));
|
||||
app.use(express.static('public'));
|
||||
app.use(express.static(path.join(process.cwd(), 'public')));
|
||||
app.get('*splat', (_req, res) => {
|
||||
res.sendFile(path.join(process.cwd(), 'public', 'index.html'));
|
||||
});
|
||||
|
||||
// Middleware pro zpracování chyb
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
|
||||
+220
-220
@@ -661,30 +661,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 1,
|
||||
size: "30cm",
|
||||
pizzaPrice: 138,
|
||||
boxPrice: 13,
|
||||
price: 151
|
||||
pizzaPrice: 13800,
|
||||
boxPrice: 1300,
|
||||
price: 15100
|
||||
},
|
||||
{
|
||||
varId: 2,
|
||||
size: "35cm",
|
||||
pizzaPrice: 166,
|
||||
boxPrice: 15,
|
||||
price: 181
|
||||
pizzaPrice: 16600,
|
||||
boxPrice: 1500,
|
||||
price: 18100
|
||||
},
|
||||
{
|
||||
varId: 3,
|
||||
size: "40cm",
|
||||
pizzaPrice: 223,
|
||||
boxPrice: 18,
|
||||
price: 241
|
||||
pizzaPrice: 22300,
|
||||
boxPrice: 1800,
|
||||
price: 24100
|
||||
},
|
||||
{
|
||||
varId: 4,
|
||||
size: "50cm",
|
||||
pizzaPrice: 306,
|
||||
boxPrice: 25,
|
||||
price: 331
|
||||
pizzaPrice: 30600,
|
||||
boxPrice: 2500,
|
||||
price: 33100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -700,30 +700,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 6,
|
||||
size: "30cm",
|
||||
pizzaPrice: 142,
|
||||
boxPrice: 13,
|
||||
price: 155
|
||||
pizzaPrice: 14200,
|
||||
boxPrice: 1300,
|
||||
price: 15500
|
||||
},
|
||||
{
|
||||
varId: 7,
|
||||
size: "35cm",
|
||||
pizzaPrice: 172,
|
||||
boxPrice: 15,
|
||||
price: 187
|
||||
pizzaPrice: 17200,
|
||||
boxPrice: 1500,
|
||||
price: 18700
|
||||
},
|
||||
{
|
||||
varId: 8,
|
||||
size: "40cm",
|
||||
pizzaPrice: 233,
|
||||
boxPrice: 18,
|
||||
price: 251
|
||||
pizzaPrice: 23300,
|
||||
boxPrice: 1800,
|
||||
price: 25100
|
||||
},
|
||||
{
|
||||
varId: 9,
|
||||
size: "50cm",
|
||||
pizzaPrice: 316,
|
||||
boxPrice: 25,
|
||||
price: 341
|
||||
pizzaPrice: 31600,
|
||||
boxPrice: 2500,
|
||||
price: 34100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -741,30 +741,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 10,
|
||||
size: "30cm",
|
||||
pizzaPrice: 142,
|
||||
boxPrice: 13,
|
||||
price: 155
|
||||
pizzaPrice: 14200,
|
||||
boxPrice: 1300,
|
||||
price: 15500
|
||||
},
|
||||
{
|
||||
varId: 11,
|
||||
size: "35cm",
|
||||
pizzaPrice: 172,
|
||||
boxPrice: 15,
|
||||
price: 187
|
||||
pizzaPrice: 17200,
|
||||
boxPrice: 1500,
|
||||
price: 18700
|
||||
},
|
||||
{
|
||||
varId: 12,
|
||||
size: "40cm",
|
||||
pizzaPrice: 233,
|
||||
boxPrice: 18,
|
||||
price: 251
|
||||
pizzaPrice: 23300,
|
||||
boxPrice: 1800,
|
||||
price: 25100
|
||||
},
|
||||
{
|
||||
varId: 13,
|
||||
size: "50cm",
|
||||
pizzaPrice: 316,
|
||||
boxPrice: 25,
|
||||
price: 341
|
||||
pizzaPrice: 31600,
|
||||
boxPrice: 2500,
|
||||
price: 34100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -780,30 +780,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 14,
|
||||
size: "30cm",
|
||||
pizzaPrice: 142,
|
||||
boxPrice: 13,
|
||||
price: 155
|
||||
pizzaPrice: 14200,
|
||||
boxPrice: 1300,
|
||||
price: 15500
|
||||
},
|
||||
{
|
||||
varId: 15,
|
||||
size: "35cm",
|
||||
pizzaPrice: 172,
|
||||
boxPrice: 15,
|
||||
price: 187
|
||||
pizzaPrice: 17200,
|
||||
boxPrice: 1500,
|
||||
price: 18700
|
||||
},
|
||||
{
|
||||
varId: 16,
|
||||
size: "40cm",
|
||||
pizzaPrice: 233,
|
||||
boxPrice: 18,
|
||||
price: 251
|
||||
pizzaPrice: 23300,
|
||||
boxPrice: 1800,
|
||||
price: 25100
|
||||
},
|
||||
{
|
||||
varId: 17,
|
||||
size: "50cm",
|
||||
pizzaPrice: 294,
|
||||
boxPrice: 25,
|
||||
price: 319
|
||||
pizzaPrice: 29400,
|
||||
boxPrice: 2500,
|
||||
price: 31900
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -821,30 +821,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 22,
|
||||
size: "30cm",
|
||||
pizzaPrice: 162,
|
||||
boxPrice: 13,
|
||||
price: 175
|
||||
pizzaPrice: 16200,
|
||||
boxPrice: 1300,
|
||||
price: 17500
|
||||
},
|
||||
{
|
||||
varId: 23,
|
||||
size: "35cm",
|
||||
pizzaPrice: 186,
|
||||
boxPrice: 15,
|
||||
price: 201
|
||||
pizzaPrice: 18600,
|
||||
boxPrice: 1500,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 24,
|
||||
size: "40cm",
|
||||
pizzaPrice: 263,
|
||||
boxPrice: 18,
|
||||
price: 281
|
||||
pizzaPrice: 26300,
|
||||
boxPrice: 1800,
|
||||
price: 28100
|
||||
},
|
||||
{
|
||||
varId: 25,
|
||||
size: "50cm",
|
||||
pizzaPrice: 346,
|
||||
boxPrice: 25,
|
||||
price: 371
|
||||
pizzaPrice: 34600,
|
||||
boxPrice: 2500,
|
||||
price: 37100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -861,30 +861,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 26,
|
||||
size: "30cm",
|
||||
pizzaPrice: 162,
|
||||
boxPrice: 13,
|
||||
price: 175
|
||||
pizzaPrice: 16200,
|
||||
boxPrice: 1300,
|
||||
price: 17500
|
||||
},
|
||||
{
|
||||
varId: 27,
|
||||
size: "35cm",
|
||||
pizzaPrice: 186,
|
||||
boxPrice: 15,
|
||||
price: 201
|
||||
pizzaPrice: 18600,
|
||||
boxPrice: 1500,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 28,
|
||||
size: "40cm",
|
||||
pizzaPrice: 263,
|
||||
boxPrice: 18,
|
||||
price: 281
|
||||
pizzaPrice: 26300,
|
||||
boxPrice: 1800,
|
||||
price: 28100
|
||||
},
|
||||
{
|
||||
varId: 29,
|
||||
size: "50cm",
|
||||
pizzaPrice: 346,
|
||||
boxPrice: 25,
|
||||
price: 371
|
||||
pizzaPrice: 34600,
|
||||
boxPrice: 2500,
|
||||
price: 37100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -902,30 +902,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 30,
|
||||
size: "30cm",
|
||||
pizzaPrice: 162,
|
||||
boxPrice: 13,
|
||||
price: 175
|
||||
pizzaPrice: 16200,
|
||||
boxPrice: 1300,
|
||||
price: 17500
|
||||
},
|
||||
{
|
||||
varId: 31,
|
||||
size: "35cm",
|
||||
pizzaPrice: 186,
|
||||
boxPrice: 15,
|
||||
price: 201
|
||||
pizzaPrice: 18600,
|
||||
boxPrice: 1500,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 32,
|
||||
size: "40cm",
|
||||
pizzaPrice: 263,
|
||||
boxPrice: 18,
|
||||
price: 281
|
||||
pizzaPrice: 26300,
|
||||
boxPrice: 1800,
|
||||
price: 28100
|
||||
},
|
||||
{
|
||||
varId: 33,
|
||||
size: "50cm",
|
||||
pizzaPrice: 346,
|
||||
boxPrice: 25,
|
||||
price: 371
|
||||
pizzaPrice: 34600,
|
||||
boxPrice: 2500,
|
||||
price: 37100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -946,30 +946,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 34,
|
||||
size: "30cm",
|
||||
pizzaPrice: 162,
|
||||
boxPrice: 13,
|
||||
price: 175
|
||||
pizzaPrice: 16200,
|
||||
boxPrice: 1300,
|
||||
price: 17500
|
||||
},
|
||||
{
|
||||
varId: 35,
|
||||
size: "35cm",
|
||||
pizzaPrice: 186,
|
||||
boxPrice: 15,
|
||||
price: 201
|
||||
pizzaPrice: 18600,
|
||||
boxPrice: 1500,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 36,
|
||||
size: "40cm",
|
||||
pizzaPrice: 263,
|
||||
boxPrice: 18,
|
||||
price: 281
|
||||
pizzaPrice: 26300,
|
||||
boxPrice: 1800,
|
||||
price: 28100
|
||||
},
|
||||
{
|
||||
varId: 37,
|
||||
size: "50cm",
|
||||
pizzaPrice: 346,
|
||||
boxPrice: 25,
|
||||
price: 371
|
||||
pizzaPrice: 34600,
|
||||
boxPrice: 2500,
|
||||
price: 37100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -987,30 +987,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 38,
|
||||
size: "30cm",
|
||||
pizzaPrice: 162,
|
||||
boxPrice: 13,
|
||||
price: 175
|
||||
pizzaPrice: 16200,
|
||||
boxPrice: 1300,
|
||||
price: 17500
|
||||
},
|
||||
{
|
||||
varId: 39,
|
||||
size: "35cm",
|
||||
pizzaPrice: 186,
|
||||
boxPrice: 15,
|
||||
price: 201
|
||||
pizzaPrice: 18600,
|
||||
boxPrice: 1500,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 40,
|
||||
size: "40cm",
|
||||
pizzaPrice: 263,
|
||||
boxPrice: 18,
|
||||
price: 281
|
||||
pizzaPrice: 26300,
|
||||
boxPrice: 1800,
|
||||
price: 28100
|
||||
},
|
||||
{
|
||||
varId: 41,
|
||||
size: "50cm",
|
||||
pizzaPrice: 346,
|
||||
boxPrice: 25,
|
||||
price: 371
|
||||
pizzaPrice: 34600,
|
||||
boxPrice: 2500,
|
||||
price: 37100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1028,30 +1028,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 42,
|
||||
size: "30cm",
|
||||
pizzaPrice: 172,
|
||||
boxPrice: 13,
|
||||
price: 185
|
||||
pizzaPrice: 17200,
|
||||
boxPrice: 1300,
|
||||
price: 18500
|
||||
},
|
||||
{
|
||||
varId: 43,
|
||||
size: "35cm",
|
||||
pizzaPrice: 212,
|
||||
boxPrice: 15,
|
||||
price: 227
|
||||
pizzaPrice: 21200,
|
||||
boxPrice: 1500,
|
||||
price: 22700
|
||||
},
|
||||
{
|
||||
varId: 44,
|
||||
size: "40cm",
|
||||
pizzaPrice: 293,
|
||||
boxPrice: 18,
|
||||
price: 311
|
||||
pizzaPrice: 29300,
|
||||
boxPrice: 1800,
|
||||
price: 31100
|
||||
},
|
||||
{
|
||||
varId: 45,
|
||||
size: "50cm",
|
||||
pizzaPrice: 376,
|
||||
boxPrice: 25,
|
||||
price: 401
|
||||
pizzaPrice: 37600,
|
||||
boxPrice: 2500,
|
||||
price: 40100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1069,30 +1069,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 46,
|
||||
size: "30cm",
|
||||
pizzaPrice: 182,
|
||||
boxPrice: 13,
|
||||
price: 195
|
||||
pizzaPrice: 18200,
|
||||
boxPrice: 1300,
|
||||
price: 19500
|
||||
},
|
||||
{
|
||||
varId: 47,
|
||||
size: "35cm",
|
||||
pizzaPrice: 222,
|
||||
boxPrice: 15,
|
||||
price: 237
|
||||
pizzaPrice: 22200,
|
||||
boxPrice: 1500,
|
||||
price: 23700
|
||||
},
|
||||
{
|
||||
varId: 48,
|
||||
size: "40cm",
|
||||
pizzaPrice: 303,
|
||||
boxPrice: 18,
|
||||
price: 321
|
||||
pizzaPrice: 30300,
|
||||
boxPrice: 1800,
|
||||
price: 32100
|
||||
},
|
||||
{
|
||||
varId: 49,
|
||||
size: "50cm",
|
||||
pizzaPrice: 386,
|
||||
boxPrice: 25,
|
||||
price: 411
|
||||
pizzaPrice: 38600,
|
||||
boxPrice: 2500,
|
||||
price: 41100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1114,30 +1114,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 50,
|
||||
size: "30cm",
|
||||
pizzaPrice: 182,
|
||||
boxPrice: 13,
|
||||
price: 195
|
||||
pizzaPrice: 18200,
|
||||
boxPrice: 1300,
|
||||
price: 19500
|
||||
},
|
||||
{
|
||||
varId: 51,
|
||||
size: "35cm",
|
||||
pizzaPrice: 222,
|
||||
boxPrice: 15,
|
||||
price: 237
|
||||
pizzaPrice: 22200,
|
||||
boxPrice: 1500,
|
||||
price: 23700
|
||||
},
|
||||
{
|
||||
varId: 52,
|
||||
size: "40cm",
|
||||
pizzaPrice: 303,
|
||||
boxPrice: 18,
|
||||
price: 321
|
||||
pizzaPrice: 30300,
|
||||
boxPrice: 1800,
|
||||
price: 32100
|
||||
},
|
||||
{
|
||||
varId: 53,
|
||||
size: "50cm",
|
||||
pizzaPrice: 396,
|
||||
boxPrice: 25,
|
||||
price: 421
|
||||
pizzaPrice: 39600,
|
||||
boxPrice: 2500,
|
||||
price: 42100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1156,30 +1156,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 54,
|
||||
size: "30cm",
|
||||
pizzaPrice: 182,
|
||||
boxPrice: 13,
|
||||
price: 195
|
||||
pizzaPrice: 18200,
|
||||
boxPrice: 1300,
|
||||
price: 19500
|
||||
},
|
||||
{
|
||||
varId: 55,
|
||||
size: "35cm",
|
||||
pizzaPrice: 222,
|
||||
boxPrice: 15,
|
||||
price: 237
|
||||
pizzaPrice: 22200,
|
||||
boxPrice: 1500,
|
||||
price: 23700
|
||||
},
|
||||
{
|
||||
varId: 56,
|
||||
size: "40cm",
|
||||
pizzaPrice: 303,
|
||||
boxPrice: 18,
|
||||
price: 321
|
||||
pizzaPrice: 30300,
|
||||
boxPrice: 1800,
|
||||
price: 32100
|
||||
},
|
||||
{
|
||||
varId: 57,
|
||||
size: "50cm",
|
||||
pizzaPrice: 396,
|
||||
boxPrice: 25,
|
||||
price: 421
|
||||
pizzaPrice: 39600,
|
||||
boxPrice: 2500,
|
||||
price: 42100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1199,30 +1199,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 58,
|
||||
size: "30cm",
|
||||
pizzaPrice: 182,
|
||||
boxPrice: 13,
|
||||
price: 195
|
||||
pizzaPrice: 18200,
|
||||
boxPrice: 1300,
|
||||
price: 19500
|
||||
},
|
||||
{
|
||||
varId: 59,
|
||||
size: "35cm",
|
||||
pizzaPrice: 222,
|
||||
boxPrice: 15,
|
||||
price: 237
|
||||
pizzaPrice: 22200,
|
||||
boxPrice: 1500,
|
||||
price: 23700
|
||||
},
|
||||
{
|
||||
varId: 60,
|
||||
size: "40cm",
|
||||
pizzaPrice: 303,
|
||||
boxPrice: 18,
|
||||
price: 321
|
||||
pizzaPrice: 30300,
|
||||
boxPrice: 1800,
|
||||
price: 32100
|
||||
},
|
||||
{
|
||||
varId: 61,
|
||||
size: "50cm",
|
||||
pizzaPrice: 396,
|
||||
boxPrice: 25,
|
||||
price: 421
|
||||
pizzaPrice: 39600,
|
||||
boxPrice: 2500,
|
||||
price: 42100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1241,30 +1241,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 62,
|
||||
size: "30cm",
|
||||
pizzaPrice: 188,
|
||||
boxPrice: 13,
|
||||
price: 201
|
||||
pizzaPrice: 18800,
|
||||
boxPrice: 1300,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 63,
|
||||
size: "35cm",
|
||||
pizzaPrice: 226,
|
||||
boxPrice: 15,
|
||||
price: 241
|
||||
pizzaPrice: 22600,
|
||||
boxPrice: 1500,
|
||||
price: 24100
|
||||
},
|
||||
{
|
||||
varId: 64,
|
||||
size: "40cm",
|
||||
pizzaPrice: 313,
|
||||
boxPrice: 18,
|
||||
price: 331
|
||||
pizzaPrice: 31300,
|
||||
boxPrice: 1800,
|
||||
price: 33100
|
||||
},
|
||||
{
|
||||
varId: 65,
|
||||
size: "50cm",
|
||||
pizzaPrice: 426,
|
||||
boxPrice: 25,
|
||||
price: 451
|
||||
pizzaPrice: 42600,
|
||||
boxPrice: 2500,
|
||||
price: 45100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1283,30 +1283,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 66,
|
||||
size: "30cm",
|
||||
pizzaPrice: 188,
|
||||
boxPrice: 13,
|
||||
price: 201
|
||||
pizzaPrice: 18800,
|
||||
boxPrice: 1300,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 67,
|
||||
size: "35cm",
|
||||
pizzaPrice: 226,
|
||||
boxPrice: 15,
|
||||
price: 241
|
||||
pizzaPrice: 22600,
|
||||
boxPrice: 1500,
|
||||
price: 24100
|
||||
},
|
||||
{
|
||||
varId: 68,
|
||||
size: "40cm",
|
||||
pizzaPrice: 313,
|
||||
boxPrice: 18,
|
||||
price: 331
|
||||
pizzaPrice: 31300,
|
||||
boxPrice: 1800,
|
||||
price: 33100
|
||||
},
|
||||
{
|
||||
varId: 69,
|
||||
size: "50cm",
|
||||
pizzaPrice: 426,
|
||||
boxPrice: 25,
|
||||
price: 451
|
||||
pizzaPrice: 42600,
|
||||
boxPrice: 2500,
|
||||
price: 45100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1327,30 +1327,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 309,
|
||||
size: "30cm",
|
||||
pizzaPrice: 182,
|
||||
boxPrice: 13,
|
||||
price: 195
|
||||
pizzaPrice: 18200,
|
||||
boxPrice: 1300,
|
||||
price: 19500
|
||||
},
|
||||
{
|
||||
varId: 310,
|
||||
size: "35cm",
|
||||
pizzaPrice: 222,
|
||||
boxPrice: 15,
|
||||
price: 237
|
||||
pizzaPrice: 22200,
|
||||
boxPrice: 1500,
|
||||
price: 23700
|
||||
},
|
||||
{
|
||||
varId: 311,
|
||||
size: "40cm",
|
||||
pizzaPrice: 303,
|
||||
boxPrice: 18,
|
||||
price: 321
|
||||
pizzaPrice: 30300,
|
||||
boxPrice: 1800,
|
||||
price: 32100
|
||||
},
|
||||
{
|
||||
varId: 312,
|
||||
size: "50cm",
|
||||
pizzaPrice: 396,
|
||||
boxPrice: 25,
|
||||
price: 421
|
||||
pizzaPrice: 39600,
|
||||
boxPrice: 2500,
|
||||
price: 42100
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1369,30 +1369,30 @@ const MOCK_PIZZA_LIST = [
|
||||
{
|
||||
varId: 394,
|
||||
size: "30cm",
|
||||
pizzaPrice: 188,
|
||||
boxPrice: 13,
|
||||
price: 201
|
||||
pizzaPrice: 18800,
|
||||
boxPrice: 1300,
|
||||
price: 20100
|
||||
},
|
||||
{
|
||||
varId: 395,
|
||||
size: "35cm",
|
||||
pizzaPrice: 226,
|
||||
boxPrice: 15,
|
||||
price: 241
|
||||
pizzaPrice: 22600,
|
||||
boxPrice: 1500,
|
||||
price: 24100
|
||||
},
|
||||
{
|
||||
varId: 396,
|
||||
size: "40cm",
|
||||
pizzaPrice: 313,
|
||||
boxPrice: 18,
|
||||
price: 331
|
||||
pizzaPrice: 31300,
|
||||
boxPrice: 1800,
|
||||
price: 33100
|
||||
},
|
||||
{
|
||||
varId: 397,
|
||||
size: "50cm",
|
||||
pizzaPrice: 426,
|
||||
boxPrice: 25,
|
||||
price: 451
|
||||
pizzaPrice: 42600,
|
||||
boxPrice: 2500,
|
||||
price: 45100
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1434,22 +1434,22 @@ const MOCK_SALAT_LIST = [
|
||||
{
|
||||
name: "Greek",
|
||||
ingredients: ["Salát", "Černé olivy", "Paprika mix", "Červená cibule", "Rajčata", "Okurka salátová", "Jogurtový dresing"],
|
||||
price: 174 + 13,
|
||||
price: (174 + 13) * 100,
|
||||
},
|
||||
{
|
||||
name: "Caesar",
|
||||
ingredients: ["Salát", "Rajčata", "Kuřecí maso", "Krutony", "Parmazán", "Caesar dresing", "Olivový olej"],
|
||||
price: 184 + 13,
|
||||
price: (184 + 13) * 100,
|
||||
},
|
||||
{
|
||||
name: "Šopský salát",
|
||||
ingredients: ["Salátová okurka", "Rajčata", "Paprika mix", "Červená cibule", "Balkánský sýr"],
|
||||
price: 164 + 13,
|
||||
price: (164 + 13) * 100,
|
||||
},
|
||||
{
|
||||
name: "Těstovinový salát",
|
||||
ingredients: ["Penne", "Okurka", "Rajčata", "Paprika mix", "Kuřecí maso", "Jogurtový dresing"],
|
||||
price: 184 + 13,
|
||||
price: (184 + 13) * 100,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
+48
-31
@@ -74,7 +74,7 @@ export async function createPizzaDay(creator: string): Promise<ClientData> {
|
||||
await initIfNeeded();
|
||||
const clientData = await getClientData(getToday());
|
||||
if (clientData.pizzaDay) {
|
||||
throw Error("Pizza day pro dnešní den již existuje");
|
||||
throw new Error("Pizza day pro dnešní den již existuje");
|
||||
}
|
||||
// TODO berka rychlooprava, vyřešit lépe - stahovat jednou, na jediném místě!
|
||||
const [pizzaList, salatList] = await Promise.all([getPizzaList(), getSalatList()]);
|
||||
@@ -91,10 +91,10 @@ export async function createPizzaDay(creator: string): Promise<ClientData> {
|
||||
export async function deletePizzaDay(login: string): Promise<ClientData> {
|
||||
const clientData = await getClientData(getToday());
|
||||
if (!clientData.pizzaDay) {
|
||||
throw Error("Pizza day pro dnešní den neexistuje");
|
||||
throw new Error("Pizza day pro dnešní den neexistuje");
|
||||
}
|
||||
if (clientData.pizzaDay.creator !== login) {
|
||||
throw Error("Login uživatele se neshoduje se zakladatelem Pizza Day");
|
||||
throw new Error("Login uživatele se neshoduje se zakladatelem Pizza Day");
|
||||
}
|
||||
delete clientData.pizzaDay;
|
||||
const today = formatDate(getToday());
|
||||
@@ -113,10 +113,10 @@ export async function addPizzaOrder(login: string, pizza: Pizza, size: PizzaSize
|
||||
const today = formatDate(getToday());
|
||||
const clientData = await getClientData(getToday());
|
||||
if (!clientData.pizzaDay) {
|
||||
throw Error("Pizza day pro dnešní den neexistuje");
|
||||
throw new Error("Pizza day pro dnešní den neexistuje");
|
||||
}
|
||||
if (clientData.pizzaDay.state !== PizzaDayState.CREATED) {
|
||||
throw Error("Pizza day není ve stavu " + PizzaDayState.CREATED);
|
||||
throw new Error("Pizza day není ve stavu " + PizzaDayState.CREATED);
|
||||
}
|
||||
let order: PizzaOrder | undefined = clientData.pizzaDay?.orders?.find(o => o.customer === login);
|
||||
if (!order) {
|
||||
@@ -152,10 +152,10 @@ export async function addSalatOrder(login: string, salat: Salat) {
|
||||
const today = formatDate(getToday());
|
||||
const clientData = await getClientData(getToday());
|
||||
if (!clientData.pizzaDay) {
|
||||
throw Error("Pizza day pro dnešní den neexistuje");
|
||||
throw new Error("Pizza day pro dnešní den neexistuje");
|
||||
}
|
||||
if (clientData.pizzaDay.state !== PizzaDayState.CREATED) {
|
||||
throw Error("Pizza day není ve stavu " + PizzaDayState.CREATED);
|
||||
throw new Error("Pizza day není ve stavu " + PizzaDayState.CREATED);
|
||||
}
|
||||
let order: PizzaOrder | undefined = clientData.pizzaDay?.orders?.find(o => o.customer === login);
|
||||
if (!order) {
|
||||
@@ -222,16 +222,16 @@ export async function removePizzaOrder(login: string, pizzaOrder: PizzaVariant)
|
||||
const today = formatDate(getToday());
|
||||
const clientData = await getClientData(getToday());
|
||||
if (!clientData.pizzaDay) {
|
||||
throw Error("Pizza day pro dnešní den neexistuje");
|
||||
throw new Error("Pizza day pro dnešní den neexistuje");
|
||||
}
|
||||
const orderIndex = clientData.pizzaDay.orders!.findIndex(o => o.customer === login);
|
||||
if (orderIndex < 0) {
|
||||
throw Error("Nebyly nalezeny žádné objednávky pro uživatele " + login);
|
||||
throw new Error("Nebyly nalezeny žádné objednávky pro uživatele " + login);
|
||||
}
|
||||
const order = clientData.pizzaDay.orders![orderIndex];
|
||||
const index = order.pizzaList!.findIndex(o => o.name === pizzaOrder.name && o.size === pizzaOrder.size);
|
||||
if (index < 0) {
|
||||
throw Error("Objednávka s danými parametry nebyla nalezena");
|
||||
throw new Error("Objednávka s danými parametry nebyla nalezena");
|
||||
}
|
||||
const price = order.pizzaList![index].price;
|
||||
order.pizzaList!.splice(index, 1);
|
||||
@@ -253,13 +253,13 @@ export async function lockPizzaDay(login: string) {
|
||||
const today = formatDate(getToday());
|
||||
const clientData = await getClientData(getToday());
|
||||
if (!clientData.pizzaDay) {
|
||||
throw Error("Pizza day pro dnešní den neexistuje");
|
||||
throw new Error("Pizza day pro dnešní den neexistuje");
|
||||
}
|
||||
if (clientData.pizzaDay.creator !== login) {
|
||||
throw Error("Pizza day není spravován uživatelem " + login);
|
||||
throw new Error("Pizza day není spravován uživatelem " + login);
|
||||
}
|
||||
if (clientData.pizzaDay.state !== PizzaDayState.CREATED && clientData.pizzaDay.state !== PizzaDayState.ORDERED) {
|
||||
throw Error("Pizza day není ve stavu " + PizzaDayState.CREATED + " nebo " + PizzaDayState.ORDERED);
|
||||
throw new Error("Pizza day není ve stavu " + PizzaDayState.CREATED + " nebo " + PizzaDayState.ORDERED);
|
||||
}
|
||||
clientData.pizzaDay.state = PizzaDayState.LOCKED;
|
||||
await storage.setData(today, clientData);
|
||||
@@ -276,13 +276,13 @@ export async function unlockPizzaDay(login: string) {
|
||||
const today = formatDate(getToday());
|
||||
const clientData = await getClientData(getToday());
|
||||
if (!clientData.pizzaDay) {
|
||||
throw Error("Pizza day pro dnešní den neexistuje");
|
||||
throw new Error("Pizza day pro dnešní den neexistuje");
|
||||
}
|
||||
if (clientData.pizzaDay.creator !== login) {
|
||||
throw Error("Pizza day není spravován uživatelem " + login);
|
||||
throw new Error("Pizza day není spravován uživatelem " + login);
|
||||
}
|
||||
if (clientData.pizzaDay.state !== PizzaDayState.LOCKED) {
|
||||
throw Error("Pizza day není ve stavu " + PizzaDayState.LOCKED);
|
||||
throw new Error("Pizza day není ve stavu " + PizzaDayState.LOCKED);
|
||||
}
|
||||
clientData.pizzaDay.state = PizzaDayState.CREATED;
|
||||
await storage.setData(today, clientData);
|
||||
@@ -299,13 +299,13 @@ export async function finishPizzaOrder(login: string) {
|
||||
const today = formatDate(getToday());
|
||||
const clientData = await getClientData(getToday());
|
||||
if (!clientData.pizzaDay) {
|
||||
throw Error("Pizza day pro dnešní den neexistuje");
|
||||
throw new Error("Pizza day pro dnešní den neexistuje");
|
||||
}
|
||||
if (clientData.pizzaDay.creator !== login) {
|
||||
throw Error("Pizza day není spravován uživatelem " + login);
|
||||
throw new Error("Pizza day není spravován uživatelem " + login);
|
||||
}
|
||||
if (clientData.pizzaDay.state !== PizzaDayState.LOCKED) {
|
||||
throw Error("Pizza day není ve stavu " + PizzaDayState.LOCKED);
|
||||
throw new Error("Pizza day není ve stavu " + PizzaDayState.LOCKED);
|
||||
}
|
||||
clientData.pizzaDay.state = PizzaDayState.ORDERED;
|
||||
await storage.setData(today, clientData);
|
||||
@@ -324,13 +324,13 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b
|
||||
const today = formatDate(getToday());
|
||||
const clientData = await getClientData(getToday());
|
||||
if (!clientData.pizzaDay) {
|
||||
throw Error("Pizza day pro dnešní den neexistuje");
|
||||
throw new Error("Pizza day pro dnešní den neexistuje");
|
||||
}
|
||||
if (clientData.pizzaDay.creator !== login) {
|
||||
throw Error("Pizza day není spravován uživatelem " + login);
|
||||
throw new Error("Pizza day není spravován uživatelem " + login);
|
||||
}
|
||||
if (clientData.pizzaDay.state !== PizzaDayState.ORDERED) {
|
||||
throw Error("Pizza day není ve stavu " + PizzaDayState.ORDERED);
|
||||
throw new Error("Pizza day není ve stavu " + PizzaDayState.ORDERED);
|
||||
}
|
||||
clientData.pizzaDay.state = PizzaDayState.DELIVERED;
|
||||
|
||||
@@ -342,7 +342,7 @@ export async function finishPizzaDelivery(login: string, bankAccount?: string, b
|
||||
let message = order.pizzaList!.map(item =>
|
||||
item.category === 'salat' ? `Salát ${item.name}` : `Pizza ${item.name} (${item.size})`
|
||||
).join(', ');
|
||||
await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice, message, id);
|
||||
await generateQr(order.customer, bankAccount, bankAccountHolder, order.totalPrice / 100, message, id);
|
||||
order.hasQr = true;
|
||||
// Uložíme nevyřízený QR kód pro persistentní zobrazení
|
||||
await addPendingQr(order.customer, {
|
||||
@@ -370,14 +370,14 @@ export async function updatePizzaDayNote(login: string, note?: string) {
|
||||
const today = formatDate(getToday());
|
||||
let clientData = await getClientData(getToday());
|
||||
if (!clientData.pizzaDay) {
|
||||
throw Error("Pizza day pro dnešní den neexistuje");
|
||||
throw new Error("Pizza day pro dnešní den neexistuje");
|
||||
}
|
||||
if (clientData.pizzaDay.state !== PizzaDayState.CREATED) {
|
||||
throw Error("Pizza day není ve stavu " + PizzaDayState.CREATED);
|
||||
throw new Error("Pizza day není ve stavu " + PizzaDayState.CREATED);
|
||||
}
|
||||
const myOrder = clientData.pizzaDay.orders!.find(o => o.customer === login);
|
||||
if (!myOrder?.pizzaList?.length) {
|
||||
throw Error("Pizza day neobsahuje žádné objednávky uživatele " + login);
|
||||
throw new Error("Pizza day neobsahuje žádné objednávky uživatele " + login);
|
||||
}
|
||||
myOrder.note = note;
|
||||
await storage.setData(today, clientData);
|
||||
@@ -397,17 +397,17 @@ export async function updatePizzaFee(login: string, targetLogin: string, text?:
|
||||
const today = formatDate(getToday());
|
||||
let clientData = await getClientData(getToday());
|
||||
if (!clientData.pizzaDay) {
|
||||
throw Error("Pizza day pro dnešní den neexistuje");
|
||||
throw new Error("Pizza day pro dnešní den neexistuje");
|
||||
}
|
||||
if (clientData.pizzaDay.state !== PizzaDayState.CREATED) {
|
||||
throw Error(`Pizza day není ve stavu ${PizzaDayState.CREATED}`);
|
||||
throw new Error(`Pizza day není ve stavu ${PizzaDayState.CREATED}`);
|
||||
}
|
||||
if (clientData.pizzaDay.creator !== login) {
|
||||
throw Error("Příplatky může měnit pouze zakladatel Pizza day");
|
||||
throw new Error("Příplatky může měnit pouze zakladatel Pizza day");
|
||||
}
|
||||
const targetOrder = clientData.pizzaDay.orders!.find(o => o.customer === targetLogin);
|
||||
if (!targetOrder?.pizzaList?.length) {
|
||||
throw Error(`Pizza day neobsahuje žádné objednávky uživatele ${targetLogin}`);
|
||||
throw new Error(`Pizza day neobsahuje žádné objednávky uživatele ${targetLogin}`);
|
||||
}
|
||||
if (!price) {
|
||||
delete targetOrder.fee;
|
||||
@@ -449,10 +449,27 @@ export async function getPendingQrs(login: string): Promise<PendingQr[]> {
|
||||
|
||||
/**
|
||||
* Označí QR kód jako uhrazený (odstraní ho ze seznamu nevyřízených).
|
||||
* Vrátí odstraněný QR kód, pokud byl nalezen.
|
||||
*/
|
||||
export async function dismissPendingQr(login: string, id: string): Promise<void> {
|
||||
export async function dismissPendingQr(login: string, id: string): Promise<PendingQr | undefined> {
|
||||
const key = getPendingQrKey(login);
|
||||
const existing = await storage.getData<PendingQr[]>(key) ?? [];
|
||||
const dismissed = existing.find(qr => qr.id === id);
|
||||
const filtered = existing.filter(qr => qr.id !== id);
|
||||
await storage.setData(key, filtered);
|
||||
return dismissed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Odstraní všechny nevyřízené QR kódy patřící dané skupině objednávky.
|
||||
*/
|
||||
export async function removePendingQrsByGroupId(logins: string[], groupId: string): Promise<void> {
|
||||
for (const login of logins) {
|
||||
const key = getPendingQrKey(login);
|
||||
const existing = await storage.getData<PendingQr[]>(key) ?? [];
|
||||
const filtered = existing.filter(qr => qr.groupId !== groupId);
|
||||
if (filtered.length !== existing.length) {
|
||||
await storage.setData(key, filtered);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import webpush from 'web-push';
|
||||
import crypto from 'crypto';
|
||||
import getStorage from './storage';
|
||||
import { getClientData, getToday } from './service';
|
||||
import { getIsWeekend } from './utils';
|
||||
@@ -65,6 +66,19 @@ export function getVapidPublicKey(): string | undefined {
|
||||
return process.env.VAPID_PUBLIC_KEY;
|
||||
}
|
||||
|
||||
function generateQuickChoiceToken(login: string): string {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const secret = process.env.JWT_SECRET ?? '';
|
||||
return crypto.createHmac('sha256', secret).update(`${login}:${today}`).digest('hex');
|
||||
}
|
||||
|
||||
/** Ověří jednorázový token z push notifikace. */
|
||||
export function verifyQuickChoiceToken(login: string, token: string): boolean {
|
||||
if (!login || !token || token.length !== 64) return false;
|
||||
const expected = generateQuickChoiceToken(login);
|
||||
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(token, 'hex'));
|
||||
}
|
||||
|
||||
|
||||
/** Zkontroluje a odešle připomínky uživatelům, kteří si nezvolili oběd. */
|
||||
async function checkAndSendReminders(): Promise<void> {
|
||||
@@ -115,6 +129,7 @@ async function checkAndSendReminders(): Promise<void> {
|
||||
title: 'Luncher',
|
||||
body: 'Ještě nemáte zvolený oběd!',
|
||||
login,
|
||||
token: generateQuickChoiceToken(login),
|
||||
})
|
||||
);
|
||||
lastReminded.set(login, Date.now());
|
||||
|
||||
+5
-7
@@ -31,10 +31,10 @@ export function convertBbanToIban(bankAccountNumber: string): string {
|
||||
// Zatím napevno, nemá smysl řešit nic jiného než CZ
|
||||
iban = iban.replace('C', '12').replace('Z', '35');
|
||||
const remainder = BigInt(iban) % BigInt(97);
|
||||
const checkDigits = BigInt(98) - remainder;
|
||||
iban = `${COUNTRY_CODE}${checkDigits.toString()}${bankCode}${prefix}${accountNumber}`;
|
||||
const checkDigits = (BigInt(98) - remainder).toString().padStart(2, '0');
|
||||
iban = `${COUNTRY_CODE}${checkDigits}${bankCode}${prefix}${accountNumber}`;
|
||||
if (iban.length !== 24) {
|
||||
throw Error("Neplatná délka sestaveného IBAN: " + iban.length + ", očekáváno 24");
|
||||
throw new Error("Neplatná délka sestaveného IBAN: " + iban.length + ", očekáváno 24");
|
||||
}
|
||||
return iban;
|
||||
}
|
||||
@@ -56,10 +56,8 @@ function createStorageKey(customerName: string, id: string): string {
|
||||
* @param id unikátní identifikátor (UUID) tohoto QR kódu
|
||||
*/
|
||||
export async function generateQr(customerName: string, bankAccountNumber: string, bankAccountHolder: string, amount: number, message: string, id: string): Promise<void> {
|
||||
// Zpráva pro příjemce nesmí dle standardu obsahovat '*' a být delší než 60 znaků
|
||||
if (message.indexOf('*') >= 0) {
|
||||
message = message.replace(/\*/g, '');
|
||||
}
|
||||
// Zpráva nesmí obsahovat '*' a znaky mimo ISO 8859-1; délka max. 60 znaků
|
||||
message = message.replace(/[^\x00-\xff]/g, '').replace(/\*/g, '');
|
||||
if (message.length > 60) {
|
||||
message = message.substring(0, 60);
|
||||
}
|
||||
|
||||
@@ -56,23 +56,23 @@ function checkRateLimit(key: string, limit: number = RATE_LIMIT): boolean {
|
||||
*/
|
||||
const parseValidateFutureDayIndex = (req: Request<{}, any, AddChoiceData["body"] | UpdateNoteData["body"]>) => {
|
||||
if (req.body.dayIndex == null) {
|
||||
throw Error(`Nebyl předán index dne v týdnu.`);
|
||||
throw new Error(`Nebyl předán index dne v týdnu.`);
|
||||
}
|
||||
const todayDayIndex = getDayOfWeekIndex(getToday());
|
||||
const dayIndex = req.body.dayIndex;
|
||||
if (isNaN(dayIndex)) {
|
||||
throw Error(`Neplatný index dne v týdnu: ${req.body.dayIndex}`);
|
||||
throw new Error(`Neplatný index dne v týdnu: ${req.body.dayIndex}`);
|
||||
}
|
||||
if (dayIndex < todayDayIndex) {
|
||||
throw Error(`Předaný index dne v týdnu (${dayIndex}) nesmí být nižší než dnešní den (${todayDayIndex})`);
|
||||
throw new Error(`Předaný index dne v týdnu (${dayIndex}) nesmí být nižší než dnešní den (${todayDayIndex})`);
|
||||
}
|
||||
return dayIndex;
|
||||
}
|
||||
|
||||
const parseSlot = (body: Record<string, any>): MealSlot | undefined => {
|
||||
const slot = body?.slot;
|
||||
if (slot != null && slot !== MealSlot.OBED && slot !== MealSlot.EXTRA) {
|
||||
throw Error(`Neplatný slot: ${slot}`);
|
||||
if (slot != null && slot !== MealSlot.OBED) {
|
||||
throw new Error(`Neplatný slot: ${slot}`);
|
||||
}
|
||||
return slot ?? undefined;
|
||||
};
|
||||
@@ -153,7 +153,7 @@ router.post("/updateNote", async (req: Request<{}, any, UpdateNoteData["body"]>,
|
||||
try { slot = parseSlot(req.body); } catch (e: any) { return res.status(400).json({ error: e.message }); }
|
||||
try {
|
||||
if (note && note.length > 70) {
|
||||
throw Error("Poznámka může mít maximálně 70 znaků");
|
||||
throw new Error("Poznámka může mít maximálně 70 znaků");
|
||||
}
|
||||
let date = undefined;
|
||||
if (req.body.dayIndex != null) {
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import express, { Request } from "express";
|
||||
import { getLogin } from "../auth";
|
||||
import { parseToken } from "../utils";
|
||||
import { getWebsocket } from "../websocket";
|
||||
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, updateGroupFees } from "../groups";
|
||||
import { GroupState } from "../../../types/gen/types.gen";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function broadcastExtra(data: any) {
|
||||
getWebsocket().emit("message", data);
|
||||
}
|
||||
|
||||
router.post("/create", async (req: Request, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
const { name } = req.body ?? {};
|
||||
if (!name || typeof name !== 'string') {
|
||||
return res.status(400).json({ error: 'Nebyl předán název skupiny' });
|
||||
}
|
||||
try {
|
||||
const data = await createGroup(login, name);
|
||||
broadcastExtra(data);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) { next(e); }
|
||||
});
|
||||
|
||||
router.post("/delete", async (req: Request, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
const { id } = req.body ?? {};
|
||||
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
|
||||
try {
|
||||
const data = await deleteGroup(login, id);
|
||||
broadcastExtra(data);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) { next(e); }
|
||||
});
|
||||
|
||||
router.post("/addMember", async (req: Request, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
const { id, login: targetLogin } = req.body ?? {};
|
||||
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
|
||||
if (targetLogin !== undefined && (typeof targetLogin !== 'string' || targetLogin.trim() === '')) {
|
||||
return res.status(400).json({ error: 'Neplatný login uživatele' });
|
||||
}
|
||||
const target = targetLogin ?? login;
|
||||
try {
|
||||
const data = await addGroupMember(login, id, target);
|
||||
broadcastExtra(data);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) { next(e); }
|
||||
});
|
||||
|
||||
router.post("/removeMember", async (req: Request, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
const { id, login: targetLogin } = req.body ?? {};
|
||||
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
|
||||
if (!targetLogin) return res.status(400).json({ error: 'Nebyl předán login uživatele' });
|
||||
try {
|
||||
const data = await removeGroupMember(login, id, targetLogin);
|
||||
broadcastExtra(data);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) { next(e); }
|
||||
});
|
||||
|
||||
router.post("/updateMember", async (req: Request, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
const { id, login: targetLogin, amount, note, surchargeText, surchargeAmount } = req.body ?? {};
|
||||
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
|
||||
if (!targetLogin) return res.status(400).json({ error: 'Nebyl předán login uživatele' });
|
||||
const patch: Record<string, any> = {};
|
||||
if (amount !== undefined) {
|
||||
if (!Number.isInteger(amount) || amount < 0) {
|
||||
return res.status(400).json({ error: 'Neplatná částka' });
|
||||
}
|
||||
patch.amount = amount;
|
||||
}
|
||||
if (note !== undefined) {
|
||||
if (typeof note !== 'string') return res.status(400).json({ error: 'Neplatná poznámka' });
|
||||
patch.note = note;
|
||||
}
|
||||
if (surchargeText !== undefined) {
|
||||
if (typeof surchargeText !== 'string') return res.status(400).json({ error: 'Neplatný text příplatku' });
|
||||
patch.surchargeText = surchargeText;
|
||||
}
|
||||
if (surchargeAmount !== undefined) {
|
||||
if (!Number.isInteger(surchargeAmount) || surchargeAmount < 0) {
|
||||
return res.status(400).json({ error: 'Neplatná výše příplatku' });
|
||||
}
|
||||
patch.surchargeAmount = surchargeAmount;
|
||||
}
|
||||
try {
|
||||
const data = await updateGroupMember(login, id, targetLogin, patch);
|
||||
broadcastExtra(data);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) { next(e); }
|
||||
});
|
||||
|
||||
router.post("/setState", async (req: Request, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
const { id, state } = req.body ?? {};
|
||||
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
|
||||
if (!state || !Object.values(GroupState).includes(state)) {
|
||||
return res.status(400).json({ error: 'Neplatný stav skupiny' });
|
||||
}
|
||||
try {
|
||||
const data = await setGroupState(login, id, state as GroupState);
|
||||
broadcastExtra(data);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) { next(e); }
|
||||
});
|
||||
|
||||
router.post("/updateFees", async (req: Request, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
const { id, fees, shipping, tip, discountType, discountValue } = req.body ?? {};
|
||||
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
|
||||
if (fees !== undefined && (!Number.isInteger(fees) || fees < 0)) {
|
||||
return res.status(400).json({ error: 'Neplatná výše poplatků' });
|
||||
}
|
||||
if (shipping !== undefined && (!Number.isInteger(shipping) || shipping < 0)) {
|
||||
return res.status(400).json({ error: 'Neplatná výše dopravy' });
|
||||
}
|
||||
if (tip !== undefined && (!Number.isInteger(tip) || tip < 0)) {
|
||||
return res.status(400).json({ error: 'Neplatná výše spropitného' });
|
||||
}
|
||||
if (discountType !== undefined && discountType !== '' && !['percent', 'fixed'].includes(discountType)) {
|
||||
return res.status(400).json({ error: 'Neplatný typ slevy' });
|
||||
}
|
||||
if (discountValue !== undefined && (!Number.isInteger(discountValue) || discountValue < 0)) {
|
||||
return res.status(400).json({ error: 'Neplatná výše slevy' });
|
||||
}
|
||||
try {
|
||||
const data = await updateGroupFees(login, id, fees, shipping, tip, discountType, discountValue);
|
||||
broadcastExtra(data);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) { next(e); }
|
||||
});
|
||||
|
||||
router.post("/updateTimes", async (req: Request, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
const { id, orderedAt, deliveryAt } = req.body ?? {};
|
||||
if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' });
|
||||
const timeRegex = /^([01]\d|2[0-3]):[0-5]\d$/;
|
||||
if (orderedAt !== undefined && orderedAt !== '' && !timeRegex.test(orderedAt)) {
|
||||
return res.status(400).json({ error: 'Neplatný formát času objednání (očekáváno HH:MM)' });
|
||||
}
|
||||
if (deliveryAt !== undefined && deliveryAt !== '' && !timeRegex.test(deliveryAt)) {
|
||||
return res.status(400).json({ error: 'Neplatný formát času doručení (očekáváno HH:MM)' });
|
||||
}
|
||||
try {
|
||||
const data = await updateGroupTimes(login, id, orderedAt, deliveryAt);
|
||||
broadcastExtra(data);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) { next(e); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -3,8 +3,6 @@ import { getLogin } from "../auth";
|
||||
import { parseToken } from "../utils";
|
||||
import { getNotificationSettings, saveNotificationSettings } from "../notifikace";
|
||||
import { subscribePush, unsubscribePush, getVapidPublicKey } from "../pushReminder";
|
||||
import { addChoice } from "../service";
|
||||
import { getWebsocket } from "../websocket";
|
||||
import { UpdateNotificationSettingsData } from "../../../types";
|
||||
|
||||
const router = express.Router();
|
||||
@@ -66,14 +64,4 @@ router.post("/push/unsubscribe", async (req, res, next) => {
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
/** Rychlá akce z push notifikace — nastaví volbu NEOBEDVAM pro přihlášeného uživatele. */
|
||||
router.post("/push/quickChoice", async (req, res, next) => {
|
||||
try {
|
||||
const login = getLogin(parseToken(req));
|
||||
const data = await addChoice(login, false, 'NEOBEDVAM', undefined, undefined);
|
||||
getWebsocket().emit("message", data);
|
||||
res.status(200).json({});
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import express, { Request } from "express";
|
||||
import { getLogin } from "../auth";
|
||||
import { createPizzaDay, deletePizzaDay, getPizzaList, getSalatList, addPizzaOrder, addSalatOrder, removePizzaOrder, lockPizzaDay, unlockPizzaDay, finishPizzaOrder, finishPizzaDelivery, updatePizzaDayNote, updatePizzaFee, dismissPendingQr } from "../pizza";
|
||||
import { markGroupMemberPaid } from "../groups";
|
||||
import { parseToken } from "../utils";
|
||||
import { getWebsocket } from "../websocket";
|
||||
import { AddPizzaData, DismissQrData, FinishDeliveryData, RemovePizzaData, UpdatePizzaDayNoteData, UpdatePizzaFeeData } from "../../../types";
|
||||
@@ -29,10 +30,10 @@ router.post("/add", async (req: Request<{}, any, AddPizzaData["body"]>, res) =>
|
||||
const salatIndex = req.body.salatIndex;
|
||||
const salaty = await getSalatList();
|
||||
if (!salaty) {
|
||||
throw Error("Selhalo získání seznamu dostupných salátů.");
|
||||
throw new Error("Selhalo získání seznamu dostupných salátů.");
|
||||
}
|
||||
if (!salaty[salatIndex]) {
|
||||
throw Error("Neplatný index salátu: " + salatIndex);
|
||||
throw new Error("Neplatný index salátu: " + salatIndex);
|
||||
}
|
||||
const data = await addSalatOrder(login, salaty[salatIndex]);
|
||||
getWebsocket().emit("message", data);
|
||||
@@ -40,22 +41,22 @@ router.post("/add", async (req: Request<{}, any, AddPizzaData["body"]>, res) =>
|
||||
} else {
|
||||
// Přidání pizzy
|
||||
if (req.body?.pizzaIndex === undefined || isNaN(req.body.pizzaIndex)) {
|
||||
throw Error("Nebyl předán index pizzy ani salátu");
|
||||
throw new Error("Nebyl předán index pizzy ani salátu");
|
||||
}
|
||||
const pizzaIndex = req.body.pizzaIndex;
|
||||
if (req.body?.pizzaSizeIndex === undefined || isNaN(req.body.pizzaSizeIndex)) {
|
||||
throw Error("Nebyl předán index velikosti pizzy");
|
||||
throw new Error("Nebyl předán index velikosti pizzy");
|
||||
}
|
||||
const pizzaSizeIndex = req.body.pizzaSizeIndex;
|
||||
let pizzy = await getPizzaList();
|
||||
if (!pizzy) {
|
||||
throw Error("Selhalo získání seznamu dostupných pizz.");
|
||||
throw new Error("Selhalo získání seznamu dostupných pizz.");
|
||||
}
|
||||
if (!pizzy[pizzaIndex]) {
|
||||
throw Error("Neplatný index pizzy: " + pizzaIndex);
|
||||
throw new Error("Neplatný index pizzy: " + pizzaIndex);
|
||||
}
|
||||
if (!pizzy[pizzaIndex].sizes[pizzaSizeIndex]) {
|
||||
throw Error("Neplatný index velikosti pizzy: " + pizzaSizeIndex);
|
||||
throw new Error("Neplatný index velikosti pizzy: " + pizzaSizeIndex);
|
||||
}
|
||||
const data = await addPizzaOrder(login, pizzy[pizzaIndex], pizzy[pizzaIndex].sizes[pizzaSizeIndex]);
|
||||
getWebsocket().emit("message", data);
|
||||
@@ -66,7 +67,7 @@ router.post("/add", async (req: Request<{}, any, AddPizzaData["body"]>, res) =>
|
||||
router.post("/remove", async (req: Request<{}, any, RemovePizzaData["body"]>, res) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
if (!req.body?.pizzaOrder) {
|
||||
throw Error("Nebyla předána objednávka");
|
||||
throw new Error("Nebyla předána objednávka");
|
||||
}
|
||||
const data = await removePizzaOrder(login, req.body?.pizzaOrder);
|
||||
getWebsocket().emit("message", data);
|
||||
@@ -105,7 +106,7 @@ router.post("/updatePizzaDayNote", async (req: Request<{}, any, UpdatePizzaDayNo
|
||||
const login = getLogin(parseToken(req));
|
||||
try {
|
||||
if (req.body.note && req.body.note.length > 70) {
|
||||
throw Error("Poznámka může mít maximálně 70 znaků");
|
||||
throw new Error("Poznámka může mít maximálně 70 znaků");
|
||||
}
|
||||
const data = await updatePizzaDayNote(login, req.body.note);
|
||||
getWebsocket().emit("message", data);
|
||||
@@ -132,7 +133,11 @@ router.post("/dismissQr", async (req: Request<{}, any, DismissQrData["body"]>, r
|
||||
return res.status(400).json({ error: "Nebyl předán identifikátor QR kódu" });
|
||||
}
|
||||
try {
|
||||
await dismissPendingQr(login, req.body.id);
|
||||
const dismissed = await dismissPendingQr(login, req.body.id);
|
||||
if (dismissed?.groupId) {
|
||||
const updatedExtra = await markGroupMemberPaid(login, dismissed.groupId);
|
||||
if (updatedExtra) getWebsocket().emit("message", updatedExtra);
|
||||
}
|
||||
res.status(200).json({});
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ import { getLogin } from "../auth";
|
||||
import { parseToken, formatDate } from "../utils";
|
||||
import { generateQr } from "../qr";
|
||||
import { addPendingQr } from "../pizza";
|
||||
import { markGroupQrGenerated } from "../groups";
|
||||
import { emitToUser } from "../websocket";
|
||||
import { GenerateQrData } from "../../../types";
|
||||
import crypto from "crypto";
|
||||
|
||||
@@ -14,7 +16,7 @@ const router = express.Router();
|
||||
router.post("/generate", async (req: Request<{}, any, GenerateQrData["body"]>, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
try {
|
||||
const { recipients, bankAccount, bankAccountHolder } = req.body;
|
||||
const { recipients, bankAccount, bankAccountHolder, groupId } = req.body;
|
||||
|
||||
if (!recipients || !Array.isArray(recipients) || recipients.length === 0) {
|
||||
return res.status(400).json({ error: "Nebyl předán seznam příjemců" });
|
||||
@@ -35,27 +37,29 @@ router.post("/generate", async (req: Request<{}, any, GenerateQrData["body"]>, r
|
||||
if (!recipient.purpose || recipient.purpose.trim().length === 0) {
|
||||
return res.status(400).json({ error: `Příjemce ${recipient.login} nemá vyplněný účel platby` });
|
||||
}
|
||||
if (typeof recipient.amount !== 'number' || recipient.amount <= 0) {
|
||||
if (!Number.isInteger(recipient.amount) || recipient.amount <= 0) {
|
||||
return res.status(400).json({ error: `Příjemce ${recipient.login} má neplatnou částku` });
|
||||
}
|
||||
// Validace max 2 desetinná místa
|
||||
const amountStr = recipient.amount.toString();
|
||||
if (amountStr.includes('.') && amountStr.split('.')[1].length > 2) {
|
||||
return res.status(400).json({ error: `Částka pro ${recipient.login} má více než 2 desetinná místa` });
|
||||
}
|
||||
|
||||
// Vygenerovat QR kód
|
||||
const id = crypto.randomUUID();
|
||||
await generateQr(recipient.login, bankAccount, bankAccountHolder, recipient.amount, recipient.purpose, id);
|
||||
await generateQr(recipient.login, bankAccount, bankAccountHolder, recipient.amount / 100, recipient.purpose, id);
|
||||
|
||||
// Uložit jako nevyřízený QR kód
|
||||
await addPendingQr(recipient.login, {
|
||||
// Uložit jako nevyřízený QR kód a okamžitě doručit příjemci
|
||||
const pendingQr = {
|
||||
id,
|
||||
date: today,
|
||||
creator: login,
|
||||
totalPrice: recipient.amount,
|
||||
purpose: recipient.purpose,
|
||||
});
|
||||
...(groupId ? { groupId } : {}),
|
||||
};
|
||||
await addPendingQr(recipient.login, pendingQr);
|
||||
emitToUser(recipient.login, 'pendingQr', pendingQr);
|
||||
}
|
||||
|
||||
if (groupId) {
|
||||
await markGroupQrGenerated(login, groupId);
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true, count: recipients.length });
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import express from "express";
|
||||
import { getStores, addStore, removeStore } from "../stores";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", async (_req, res, next) => {
|
||||
try {
|
||||
const stores = await getStores();
|
||||
res.status(200).json(stores);
|
||||
} catch (e: any) { next(e); }
|
||||
});
|
||||
|
||||
router.post("/add", async (req, res, next) => {
|
||||
const { name, heslo } = req.body ?? {};
|
||||
if (!name || typeof name !== 'string') {
|
||||
return res.status(400).json({ error: 'Nebyl předán název obchodu' });
|
||||
}
|
||||
if (!heslo || typeof heslo !== 'string') {
|
||||
return res.status(400).json({ error: 'Nebylo předáno heslo' });
|
||||
}
|
||||
try {
|
||||
const stores = await addStore(name, heslo);
|
||||
res.status(200).json(stores);
|
||||
} catch (e: any) {
|
||||
if (e.message === 'UNAUTHORIZED') {
|
||||
return res.status(403).json({ error: 'Nesprávné heslo' });
|
||||
}
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/delete", async (req, res, next) => {
|
||||
const { name, heslo } = req.body ?? {};
|
||||
if (!name || typeof name !== 'string') {
|
||||
return res.status(400).json({ error: 'Nebyl předán název obchodu' });
|
||||
}
|
||||
if (!heslo || typeof heslo !== 'string') {
|
||||
return res.status(400).json({ error: 'Nebylo předáno heslo' });
|
||||
}
|
||||
try {
|
||||
const stores = await removeStore(name, heslo);
|
||||
res.status(200).json(stores);
|
||||
} catch (e: any) {
|
||||
if (e.message === 'UNAUTHORIZED') {
|
||||
return res.status(403).json({ error: 'Nesprávné heslo' });
|
||||
}
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -3,6 +3,7 @@ import getStorage from "./storage";
|
||||
import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova, StaleWeekError } from "./restaurants";
|
||||
import { getTodayMock } from "./mock";
|
||||
import { removeAllUserPizzas } from "./pizza";
|
||||
import { getStores } from "./stores";
|
||||
import { ClientData, DepartureTime, LunchChoice, MealSlot, PizzaDayState, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types/gen/types.gen";
|
||||
|
||||
const storage = getStorage();
|
||||
@@ -50,7 +51,9 @@ export function getEmptyData(date?: Date): ClientData {
|
||||
*/
|
||||
export async function getData(date?: Date, slot?: MealSlot): Promise<ClientData> {
|
||||
const clientData = await getClientData(date, slot);
|
||||
if (slot !== MealSlot.EXTRA) {
|
||||
if (slot === MealSlot.EXTRA) {
|
||||
clientData.stores = await getStores();
|
||||
} else {
|
||||
clientData.menus = {
|
||||
SLADOVNICKA: await getRestaurantMenu('SLADOVNICKA', date),
|
||||
// UMOTLIKU: await getRestaurantMenu('UMOTLIKU', date),
|
||||
@@ -482,13 +485,13 @@ export async function addChoice(login: string, trusted: boolean, locationKey: Lu
|
||||
async function validateFoodIndex(locationKey: LunchChoice, foodIndex?: number, date?: Date) {
|
||||
if (foodIndex != null) {
|
||||
if (typeof foodIndex !== 'number') {
|
||||
throw Error(`Neplatný index ${foodIndex} typu ${typeof foodIndex}`);
|
||||
throw new Error(`Neplatný index ${foodIndex} typu ${typeof foodIndex}`);
|
||||
}
|
||||
if (foodIndex < 0) {
|
||||
throw Error(`Neplatný index ${foodIndex}`);
|
||||
throw new Error(`Neplatný index ${foodIndex}`);
|
||||
}
|
||||
if (!Object.keys(Restaurant).includes(locationKey)) {
|
||||
throw Error(`Neplatný index ${foodIndex} pro lokalitu ${locationKey} nepodporující indexy`);
|
||||
throw new Error(`Neplatný index ${foodIndex} pro lokalitu ${locationKey} nepodporující indexy`);
|
||||
}
|
||||
const usedDate = date ?? getToday();
|
||||
const menu = await getRestaurantMenu(locationKey as Restaurant, usedDate);
|
||||
@@ -540,7 +543,7 @@ export async function updateDepartureTime(login: string, time?: string, date?: D
|
||||
delete found[login].departureTime;
|
||||
} else {
|
||||
if (!Object.values<string>(DepartureTime).includes(time)) {
|
||||
throw Error(`Neplatný čas odchodu ${time}`);
|
||||
throw new Error(`Neplatný čas odchodu ${time}`);
|
||||
}
|
||||
found[login].departureTime = time;
|
||||
}
|
||||
|
||||
+2
-2
@@ -22,13 +22,13 @@ export async function getStats(startDate: string, endDate: string): Promise<Week
|
||||
// Dočasná validace, aby to někdo ručně neshodil
|
||||
const daysDiff = ((end as any) - (start as any)) / (1000 * 60 * 60 * 24);
|
||||
if (daysDiff > 4) {
|
||||
throw Error('Neplatný rozsah');
|
||||
throw new Error('Neplatný rozsah');
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(23, 59, 59, 999);
|
||||
if (end > today) {
|
||||
throw Error('Nelze načíst statistiky pro budoucí datum');
|
||||
throw new Error('Nelze načíst statistiky pro budoucí datum');
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
@@ -20,7 +20,7 @@ if (!process.env.STORAGE || process.env.STORAGE?.toLowerCase() === JSON_KEY) {
|
||||
} else if (process.env.STORAGE?.toLowerCase() === MEMORY_KEY) {
|
||||
storage = new MemoryStorage();
|
||||
} else {
|
||||
throw Error("Nepodporovaná hodnota proměnné STORAGE: " + process.env.STORAGE + ", podporované jsou 'json', 'redis' nebo 'memory'");
|
||||
throw new Error("Nepodporovaná hodnota proměnné STORAGE: " + process.env.STORAGE + ", podporované jsou 'json', 'redis' nebo 'memory'");
|
||||
}
|
||||
|
||||
export const storageReady: Promise<void> = storage.initialize
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import getStorage from "./storage";
|
||||
|
||||
const storage = getStorage();
|
||||
const STORES_KEY = 'stores';
|
||||
|
||||
export async function getStores(): Promise<string[]> {
|
||||
return (await storage.getData<string[]>(STORES_KEY)) ?? [];
|
||||
}
|
||||
|
||||
export async function addStore(name: string, heslo: string): Promise<string[]> {
|
||||
const adminPassword = process.env.ADMIN_PASSWORD;
|
||||
if (!adminPassword || heslo !== adminPassword) {
|
||||
throw new Error('UNAUTHORIZED');
|
||||
}
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error('Název obchodu nesmí být prázdný');
|
||||
}
|
||||
const stores = await getStores();
|
||||
if (stores.some(s => s.toLowerCase() === trimmed.toLowerCase())) {
|
||||
throw new Error('Obchod s tímto názvem již existuje');
|
||||
}
|
||||
const updated = [...stores, trimmed];
|
||||
await storage.setData(STORES_KEY, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function removeStore(name: string, heslo: string): Promise<string[]> {
|
||||
const adminPassword = process.env.ADMIN_PASSWORD;
|
||||
if (!adminPassword || heslo !== adminPassword) {
|
||||
throw new Error('UNAUTHORIZED');
|
||||
}
|
||||
const stores = await getStores();
|
||||
const updated = stores.filter(s => s.toLowerCase() !== name.trim().toLowerCase());
|
||||
await storage.setData(STORES_KEY, updated);
|
||||
return updated;
|
||||
}
|
||||
@@ -31,8 +31,8 @@ test('saláty mají name a ingredients', async () => {
|
||||
|
||||
test('cena salátu zahrnuje pevný příplatek 13 Kč za obal', async () => {
|
||||
const salaty = await downloadSalaty(false);
|
||||
// Caesar sticker price = 129, box = 13
|
||||
expect(salaty[0].price).toBe(129 + 13);
|
||||
// Řecký sticker price = 119, box = 13
|
||||
expect(salaty[1].price).toBe(119 + 13);
|
||||
// Caesar sticker price = 129, box = 13 → (129 + 13) * 100 haléřů
|
||||
expect(salaty[0].price).toBe((129 + 13) * 100);
|
||||
// Řecký sticker price = 119, box = 13 → (119 + 13) * 100 haléřů
|
||||
expect(salaty[1].price).toBe((119 + 13) * 100);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import { getStores, addStore } from '../stores';
|
||||
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState } from '../groups';
|
||||
import { GroupState } from '../../../types/gen/types.gen';
|
||||
|
||||
const CREATOR = 'tomas';
|
||||
const USER = 'petr';
|
||||
const ADMIN_PW = 'testadmin';
|
||||
const STORE = 'McDonald\'s';
|
||||
const TODAY = new Date('2025-01-10');
|
||||
|
||||
beforeEach(async () => {
|
||||
resetMemoryStorage();
|
||||
process.env.ADMIN_PASSWORD = ADMIN_PW;
|
||||
await addStore(STORE, ADMIN_PW);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.ADMIN_PASSWORD;
|
||||
});
|
||||
|
||||
describe('createGroup', () => {
|
||||
test('vytvoří skupinu, creator je člen', async () => {
|
||||
const data = await createGroup(CREATOR, STORE, TODAY);
|
||||
expect(data.groups).toHaveLength(1);
|
||||
const group = data.groups![0];
|
||||
expect(group.name).toBe(STORE);
|
||||
expect(group.creatorLogin).toBe(CREATOR);
|
||||
expect(group.state).toBe(GroupState.OPEN);
|
||||
expect(group.members[CREATOR]).toBeDefined();
|
||||
});
|
||||
|
||||
test('odmítne název mimo seznam obchodů', async () => {
|
||||
await expect(createGroup(CREATOR, 'Neznámý obchod', TODAY)).rejects.toThrow('seznam');
|
||||
});
|
||||
|
||||
test('vygeneruje unikátní ID', async () => {
|
||||
const d1 = await createGroup(CREATOR, STORE, TODAY);
|
||||
const d2 = await createGroup(USER, STORE, TODAY);
|
||||
expect(d2.groups).toHaveLength(2);
|
||||
expect(d2.groups![1].id).not.toBe(d2.groups![0].id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteGroup', () => {
|
||||
test('creator může smazat skupinu', async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
const groupId = d.groups![0].id;
|
||||
const result = await deleteGroup(CREATOR, groupId, TODAY);
|
||||
expect(result.groups).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('nečlen nemůže smazat skupinu', async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
const groupId = d.groups![0].id;
|
||||
await expect(deleteGroup(USER, groupId, TODAY)).rejects.toThrow('zakladatel');
|
||||
});
|
||||
|
||||
test('smazání neexistující skupiny vyhodí chybu', async () => {
|
||||
await expect(deleteGroup(CREATOR, 'nonexistent', TODAY)).rejects.toThrow('nalezena');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addGroupMember', () => {
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
groupId = d.groups![0].id;
|
||||
});
|
||||
|
||||
test('uživatel se může přidat sám (open)', async () => {
|
||||
const d = await addGroupMember(USER, groupId, USER, TODAY);
|
||||
expect(d.groups![0].members[USER]).toBeDefined();
|
||||
});
|
||||
|
||||
test('creator může přidat jiného uživatele', async () => {
|
||||
const d = await addGroupMember(CREATOR, groupId, USER, TODAY);
|
||||
expect(d.groups![0].members[USER]).toBeDefined();
|
||||
});
|
||||
|
||||
test('nečlen nemůže přidat jiného uživatele', async () => {
|
||||
await expect(addGroupMember(USER, groupId, 'dalsi', TODAY)).rejects.toThrow('zakladatel');
|
||||
});
|
||||
|
||||
test('nelze přidat do skupiny ve stavu ordered', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||
await expect(addGroupMember(USER, groupId, USER, TODAY)).rejects.toThrow('objednáno');
|
||||
});
|
||||
|
||||
test('nelze přidat existujícího člena', async () => {
|
||||
await expect(addGroupMember(CREATOR, groupId, CREATOR, TODAY)).rejects.toThrow('již');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeGroupMember', () => {
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
groupId = d.groups![0].id;
|
||||
await addGroupMember(CREATOR, groupId, USER, TODAY);
|
||||
});
|
||||
|
||||
test('člen se může odhlásit sám', async () => {
|
||||
const d = await removeGroupMember(USER, groupId, USER, TODAY);
|
||||
expect(d.groups![0].members[USER]).toBeUndefined();
|
||||
});
|
||||
|
||||
test('creator může odebrat jiného člena', async () => {
|
||||
const d = await removeGroupMember(CREATOR, groupId, USER, TODAY);
|
||||
expect(d.groups![0].members[USER]).toBeUndefined();
|
||||
});
|
||||
|
||||
test('nelze odebrat zakladatele', async () => {
|
||||
await expect(removeGroupMember(CREATOR, groupId, CREATOR, TODAY)).rejects.toThrow('Zakladatel');
|
||||
});
|
||||
|
||||
test('nečlen nemůže odebrat jiného', async () => {
|
||||
await expect(removeGroupMember(USER, groupId, CREATOR, TODAY)).rejects.toThrow('zakladatel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateGroupMember', () => {
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
groupId = d.groups![0].id;
|
||||
await addGroupMember(CREATOR, groupId, USER, TODAY);
|
||||
});
|
||||
|
||||
test('člen může upravit svá data (open)', async () => {
|
||||
const d = await updateGroupMember(USER, groupId, USER, { amount: 150, note: 'Big Mac' }, TODAY);
|
||||
expect(d.groups![0].members[USER].amount).toBe(150);
|
||||
expect(d.groups![0].members[USER].note).toBe('Big Mac');
|
||||
});
|
||||
|
||||
test('creator může upravit data jiného člena', async () => {
|
||||
const d = await updateGroupMember(CREATOR, groupId, USER, { amount: 200 }, TODAY);
|
||||
expect(d.groups![0].members[USER].amount).toBe(200);
|
||||
});
|
||||
|
||||
test('člen nemůže upravit data jiného (locked)', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
await expect(updateGroupMember(USER, groupId, USER, { amount: 100 }, TODAY)).rejects.toThrow('uzamčeno');
|
||||
});
|
||||
|
||||
test('nikdo nemůže upravit při stavu ordered', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||
await expect(updateGroupMember(CREATOR, groupId, USER, { amount: 100 }, TODAY)).rejects.toThrow('objednáno');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setGroupState', () => {
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE, TODAY);
|
||||
groupId = d.groups![0].id;
|
||||
});
|
||||
|
||||
test('open → locked', async () => {
|
||||
const d = await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
expect(d.groups![0].state).toBe(GroupState.LOCKED);
|
||||
});
|
||||
|
||||
test('locked → open (odemčení)', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
const d = await setGroupState(CREATOR, groupId, GroupState.OPEN, TODAY);
|
||||
expect(d.groups![0].state).toBe(GroupState.OPEN);
|
||||
});
|
||||
|
||||
test('locked → ordered', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
const d = await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||
expect(d.groups![0].state).toBe(GroupState.ORDERED);
|
||||
});
|
||||
|
||||
test('open → ordered není povoleno', async () => {
|
||||
await expect(setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY)).rejects.toThrow('Nelze přejít');
|
||||
});
|
||||
|
||||
test('ordered je terminální stav', async () => {
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED, TODAY);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED, TODAY);
|
||||
await expect(setGroupState(CREATOR, groupId, GroupState.OPEN, TODAY)).rejects.toThrow('Nelze přejít');
|
||||
});
|
||||
|
||||
test('nečlen nemůže měnit stav', async () => {
|
||||
await expect(setGroupState(USER, groupId, GroupState.LOCKED, TODAY)).rejects.toThrow('zakladatel');
|
||||
});
|
||||
});
|
||||
@@ -28,8 +28,8 @@ beforeEach(() => {
|
||||
|
||||
const VALID_BODY = {
|
||||
recipients: [
|
||||
{ login: 'uzivatel1', purpose: 'Pizza Margherita', amount: 149 },
|
||||
{ login: 'uzivatel2', purpose: 'Pizza Diavola', amount: 179 },
|
||||
{ login: 'uzivatel1', purpose: 'Pizza Margherita', amount: 14900 },
|
||||
{ login: 'uzivatel2', purpose: 'Pizza Diavola', amount: 17900 },
|
||||
],
|
||||
bankAccount: '19-2000145399/0800',
|
||||
bankAccountHolder: 'Jan Novák',
|
||||
@@ -76,17 +76,17 @@ test('POST /generate vrátí 400 pro zápornou částku', async () => {
|
||||
expect(res.body.error).toContain('částku');
|
||||
});
|
||||
|
||||
test('POST /generate vrátí 400 pro částku s více než 2 desetinnými místy', async () => {
|
||||
test('POST /generate vrátí 400 pro necelou částku', async () => {
|
||||
const body = {
|
||||
...VALID_BODY,
|
||||
recipients: [{ login: 'uzivatel1', purpose: 'Pizza', amount: 149.999 }],
|
||||
recipients: [{ login: 'uzivatel1', purpose: 'Pizza', amount: 149.5 }],
|
||||
};
|
||||
const res = await request(buildApp())
|
||||
.post('/api/qr/generate')
|
||||
.set('Authorization', TOKEN)
|
||||
.send(body);
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain('desetinná');
|
||||
expect(res.body.error).toContain('částku');
|
||||
});
|
||||
|
||||
test('POST /generate vrátí 400 pro příjemce bez login', async () => {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import { getStores, addStore, removeStore } from '../stores';
|
||||
|
||||
const ADMIN_PW = 'testadmin';
|
||||
|
||||
beforeEach(() => {
|
||||
resetMemoryStorage();
|
||||
process.env.ADMIN_PASSWORD = ADMIN_PW;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.ADMIN_PASSWORD;
|
||||
});
|
||||
|
||||
describe('getStores', () => {
|
||||
test('vrátí prázdné pole, pokud seznam neexistuje', async () => {
|
||||
const stores = await getStores();
|
||||
expect(stores).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addStore', () => {
|
||||
test('přidá obchod se správným heslem', async () => {
|
||||
const stores = await addStore('McDonald\'s', ADMIN_PW);
|
||||
expect(stores).toContain('McDonald\'s');
|
||||
});
|
||||
|
||||
test('vyhodí UNAUTHORIZED s nesprávným heslem', async () => {
|
||||
await expect(addStore('KFC', 'spatne')).rejects.toThrow('UNAUTHORIZED');
|
||||
});
|
||||
|
||||
test('vyhodí UNAUTHORIZED pokud ADMIN_PASSWORD není nastaven', async () => {
|
||||
delete process.env.ADMIN_PASSWORD;
|
||||
await expect(addStore('KFC', '')).rejects.toThrow('UNAUTHORIZED');
|
||||
});
|
||||
|
||||
test('odmítne prázdný název', async () => {
|
||||
await expect(addStore(' ', ADMIN_PW)).rejects.toThrow('prázdný');
|
||||
});
|
||||
|
||||
test('odmítne duplikát (case-insensitive)', async () => {
|
||||
await addStore('McDonald\'s', ADMIN_PW);
|
||||
await expect(addStore('mcdonald\'s', ADMIN_PW)).rejects.toThrow('existuje');
|
||||
});
|
||||
|
||||
test('vrátí aktualizovaný seznam', async () => {
|
||||
await addStore('McDonald\'s', ADMIN_PW);
|
||||
const stores = await addStore('KFC', ADMIN_PW);
|
||||
expect(stores).toHaveLength(2);
|
||||
expect(stores).toContain('McDonald\'s');
|
||||
expect(stores).toContain('KFC');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeStore', () => {
|
||||
beforeEach(async () => {
|
||||
await addStore('McDonald\'s', ADMIN_PW);
|
||||
});
|
||||
|
||||
test('odebere obchod se správným heslem', async () => {
|
||||
const stores = await removeStore('McDonald\'s', ADMIN_PW);
|
||||
expect(stores).not.toContain('McDonald\'s');
|
||||
});
|
||||
|
||||
test('case-insensitive odebrání', async () => {
|
||||
const stores = await removeStore('MCDONALD\'S', ADMIN_PW);
|
||||
expect(stores).not.toContain('McDonald\'s');
|
||||
});
|
||||
|
||||
test('vyhodí UNAUTHORIZED s nesprávným heslem', async () => {
|
||||
await expect(removeStore('McDonald\'s', 'spatne')).rejects.toThrow('UNAUTHORIZED');
|
||||
});
|
||||
|
||||
test('odebrání neexistujícího obchodu nic nerozbije', async () => {
|
||||
const stores = await removeStore('Neexistuje', ADMIN_PW);
|
||||
expect(stores).toContain('McDonald\'s');
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -90,7 +90,7 @@ export const parseToken = (req: any) => {
|
||||
export const checkQueryParams = (req: any, paramNames: string[]) => {
|
||||
for (const name of paramNames) {
|
||||
if (req.query[name] == null) {
|
||||
throw Error(`Nebyl předán parametr '${name}' v query požadavku`);
|
||||
throw new Error(`Nebyl předán parametr '${name}' v query požadavku`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export const checkQueryParams = (req: any, paramNames: string[]) => {
|
||||
export const checkBodyParams = (req: any, paramNames: string[]) => {
|
||||
for (const name of paramNames) {
|
||||
if (req.body[name] == null) {
|
||||
throw Error(`Nebyl předán parametr '${name}' v těle požadavku`);
|
||||
throw new Error(`Nebyl předán parametr '${name}' v těle požadavku`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function updateFeatureVote(login: string, option: FeatureRequest, a
|
||||
const index = data[login].indexOf(option);
|
||||
if (index > -1) {
|
||||
if (active) {
|
||||
throw Error('Pro tuto možnost jste již hlasovali');
|
||||
throw new Error('Pro tuto možnost jste již hlasovali');
|
||||
} else {
|
||||
data[login].splice(index, 1);
|
||||
if (data[login].length === 0) {
|
||||
@@ -49,7 +49,7 @@ export async function updateFeatureVote(login: string, option: FeatureRequest, a
|
||||
}
|
||||
} else if (active) {
|
||||
if (data[login].length == 4) {
|
||||
throw Error('Je možné hlasovat pro maximálně 4 možnosti');
|
||||
throw new Error('Je možné hlasovat pro maximálně 4 možnosti');
|
||||
}
|
||||
data[login].push(option);
|
||||
}
|
||||
|
||||
+12
-2
@@ -11,6 +11,12 @@ export const initWebsocket = (server: any) => {
|
||||
io.on("connection", (socket) => {
|
||||
console.log(`New client connected: ${socket.id}`);
|
||||
|
||||
socket.on("join", (login: string) => {
|
||||
if (login && typeof login === "string") {
|
||||
socket.join(`user:${login}`);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("message", (message) => {
|
||||
io.emit("message", message);
|
||||
});
|
||||
@@ -22,6 +28,10 @@ export const initWebsocket = (server: any) => {
|
||||
return io;
|
||||
}
|
||||
|
||||
export const getWebsocket = () => {
|
||||
return io;
|
||||
export const getWebsocket = () => io;
|
||||
|
||||
/** Pošle event konkrétnímu přihlášenému uživateli (pokud je připojen). */
|
||||
export const emitToUser = (login: string, event: string, data: unknown) => {
|
||||
if (!io) return;
|
||||
io.to(`user:${login}`).emit(event, data);
|
||||
}
|
||||
@@ -81,6 +81,32 @@ paths:
|
||||
/changelogs:
|
||||
$ref: "./paths/changelogs/getChangelogs.yml"
|
||||
|
||||
# Skupiny objednávek (/api/groups)
|
||||
/groups/create:
|
||||
$ref: "./paths/groups/createGroup.yml"
|
||||
/groups/delete:
|
||||
$ref: "./paths/groups/deleteGroup.yml"
|
||||
/groups/addMember:
|
||||
$ref: "./paths/groups/addMember.yml"
|
||||
/groups/removeMember:
|
||||
$ref: "./paths/groups/removeMember.yml"
|
||||
/groups/updateMember:
|
||||
$ref: "./paths/groups/updateMember.yml"
|
||||
/groups/setState:
|
||||
$ref: "./paths/groups/setState.yml"
|
||||
/groups/updateTimes:
|
||||
$ref: "./paths/groups/updateTimes.yml"
|
||||
/groups/updateFees:
|
||||
$ref: "./paths/groups/updateFees.yml"
|
||||
|
||||
# Správa obchodů (/api/stores)
|
||||
/stores:
|
||||
$ref: "./paths/stores/listStores.yml"
|
||||
/stores/add:
|
||||
$ref: "./paths/stores/addStore.yml"
|
||||
/stores/delete:
|
||||
$ref: "./paths/stores/deleteStore.yml"
|
||||
|
||||
# DEV endpointy (/api/dev)
|
||||
/dev/generate:
|
||||
$ref: "./paths/dev/generate.yml"
|
||||
|
||||
Generated
+594
@@ -0,0 +1,594 @@
|
||||
{
|
||||
"name": "@luncher/types",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@luncher/types",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@hey-api/client-fetch": "^0.8.2",
|
||||
"@hey-api/openapi-ts": "^0.64.7",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/client-fetch": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.8.2.tgz",
|
||||
"integrity": "sha512-61T4UGfAzY5345vMxWDX8qnSTNRJcOpWuZyvNu3vNebCTLPwMQAM85mhEuBoACdWeRtLhNoUjU0UR5liRyD1bA==",
|
||||
"deprecated": "Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/hey-api"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/json-schema-ref-parser": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.0.2.tgz",
|
||||
"integrity": "sha512-F6LSkttZcT/XiX3ydeDqTY3uRN3BLJMwyMTk4kg/ichZlKUp3+3Odv0WokSmXGSoZGTW/N66FROMYAm5NPdJlA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/hey-api"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/openapi-ts": {
|
||||
"version": "0.64.7",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.64.7.tgz",
|
||||
"integrity": "sha512-xpaBzdGAKz7cPuGah1GZWl3zTZquOXRnwmpVCQKEUpvDtSYWBLjSxtAYIqDBjwJkuNHcakZBIWLcappx7slc2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hey-api/json-schema-ref-parser": "1.0.2",
|
||||
"c12": "2.0.1",
|
||||
"commander": "13.0.0",
|
||||
"handlebars": "4.7.8"
|
||||
},
|
||||
"bin": {
|
||||
"openapi-ts": "bin/index.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=22.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/hey-api"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdevtools/ono": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/c12": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/c12/-/c12-2.0.1.tgz",
|
||||
"integrity": "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.1",
|
||||
"confbox": "^0.1.7",
|
||||
"defu": "^6.1.4",
|
||||
"dotenv": "^16.4.5",
|
||||
"giget": "^1.2.3",
|
||||
"jiti": "^2.3.0",
|
||||
"mlly": "^1.7.1",
|
||||
"ohash": "^1.1.4",
|
||||
"pathe": "^1.1.2",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"pkg-types": "^1.2.0",
|
||||
"rc9": "^2.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"magicast": "^0.3.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"magicast": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/citty": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"consola": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz",
|
||||
"integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
||||
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz",
|
||||
"integrity": "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/defu": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/destr": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz",
|
||||
"integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/giget": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/giget/-/giget-1.2.5.tgz",
|
||||
"integrity": "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
"consola": "^3.4.0",
|
||||
"defu": "^6.1.4",
|
||||
"node-fetch-native": "^1.6.6",
|
||||
"nypm": "^0.5.4",
|
||||
"pathe": "^2.0.3",
|
||||
"tar": "^6.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"giget": "dist/cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/giget/node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.8",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
||||
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5",
|
||||
"neo-async": "^2.6.2",
|
||||
"source-map": "^0.6.1",
|
||||
"wordwrap": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"handlebars": "bin/handlebars"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.7"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"uglify-js": "^3.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mlly": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
|
||||
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"pathe": "^2.0.1",
|
||||
"pkg-types": "^1.3.0",
|
||||
"ufo": "^1.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mlly/node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch-native": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz",
|
||||
"integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.5.4.tgz",
|
||||
"integrity": "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
"consola": "^3.4.0",
|
||||
"pathe": "^2.0.3",
|
||||
"pkg-types": "^1.3.1",
|
||||
"tinyexec": "^0.3.2",
|
||||
"ufo": "^1.5.4"
|
||||
},
|
||||
"bin": {
|
||||
"nypm": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.16.0 || >=16.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nypm/node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ohash": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz",
|
||||
"integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
||||
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
|
||||
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.1.8",
|
||||
"mlly": "^1.7.4",
|
||||
"pathe": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types/node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rc9": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^5.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/ufo": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
|
||||
"integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
"version": "3.19.3",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"uglifyjs": "bin/uglifyjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
post:
|
||||
operationId: addGroupMember
|
||||
summary: Přidá uživatele do skupiny (sebe, nebo jiného jako zakladatel).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
properties:
|
||||
id:
|
||||
description: ID skupiny
|
||||
type: string
|
||||
login:
|
||||
description: Login uživatele (volitelné — pokud není zadán, přidá přihlášeného uživatele)
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||
@@ -0,0 +1,18 @@
|
||||
post:
|
||||
operationId: createGroup
|
||||
summary: Vytvoří novou skupinu objednávky pro aktuální den.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
description: Název obchodu/restaurace (musí být v seznamu povolených obchodů)
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||
@@ -0,0 +1,18 @@
|
||||
post:
|
||||
operationId: deleteGroup
|
||||
summary: Smaže skupinu objednávky (pouze zakladatel).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
properties:
|
||||
id:
|
||||
description: ID skupiny
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||
@@ -0,0 +1,22 @@
|
||||
post:
|
||||
operationId: removeGroupMember
|
||||
summary: Odebere uživatele ze skupiny (sebe, nebo jiného jako zakladatel).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- login
|
||||
properties:
|
||||
id:
|
||||
description: ID skupiny
|
||||
type: string
|
||||
login:
|
||||
description: Login uživatele k odebrání
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||
@@ -0,0 +1,21 @@
|
||||
post:
|
||||
operationId: setGroupState
|
||||
summary: Změní stav skupiny (pouze zakladatel).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- state
|
||||
properties:
|
||||
id:
|
||||
description: ID skupiny
|
||||
type: string
|
||||
state:
|
||||
$ref: "../../schemas/_index.yml#/GroupState"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||
@@ -0,0 +1,34 @@
|
||||
post:
|
||||
operationId: updateGroupFees
|
||||
summary: Aktualizuje skupinové poplatky a slevu (pouze zakladatel, pouze otevřená skupina).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
properties:
|
||||
id:
|
||||
description: ID skupiny
|
||||
type: string
|
||||
fees:
|
||||
description: Poplatky (haléře)
|
||||
type: integer
|
||||
shipping:
|
||||
description: Doprava (haléře)
|
||||
type: integer
|
||||
tip:
|
||||
description: Spropitné (haléře)
|
||||
type: integer
|
||||
discountType:
|
||||
description: Typ slevy
|
||||
type: string
|
||||
enum: [percent, fixed]
|
||||
discountValue:
|
||||
description: Hodnota slevy (procenta nebo haléře pro fixed)
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||
@@ -0,0 +1,34 @@
|
||||
post:
|
||||
operationId: updateGroupMember
|
||||
summary: Aktualizuje data člena skupiny (částka, poznámka, příplatek).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- login
|
||||
properties:
|
||||
id:
|
||||
description: ID skupiny
|
||||
type: string
|
||||
login:
|
||||
description: Login člena ke změně
|
||||
type: string
|
||||
amount:
|
||||
description: Částka k úhradě v haléřích
|
||||
type: integer
|
||||
note:
|
||||
description: Poznámka
|
||||
type: string
|
||||
surchargeText:
|
||||
description: Popis příplatku
|
||||
type: string
|
||||
surchargeAmount:
|
||||
description: Výše příplatku v haléřích
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||
@@ -0,0 +1,24 @@
|
||||
post:
|
||||
operationId: updateGroupTimes
|
||||
summary: Aktualizuje časy objednání a doručení skupiny (pouze zakladatel).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
properties:
|
||||
id:
|
||||
description: ID skupiny
|
||||
type: string
|
||||
orderedAt:
|
||||
description: Čas objednání ve formátu HH:MM
|
||||
type: string
|
||||
deliveryAt:
|
||||
description: Očekávaný čas doručení ve formátu HH:MM
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
$ref: "../../api.yml#/components/responses/ClientDataResponse"
|
||||
@@ -16,8 +16,8 @@ post:
|
||||
type: string
|
||||
description: Textový popis přirážky/slevy
|
||||
price:
|
||||
type: number
|
||||
description: Částka přirážky/slevy v Kč
|
||||
type: integer
|
||||
description: Částka přirážky/slevy v haléřích
|
||||
responses:
|
||||
"200":
|
||||
description: Nastavení přirážky/slevy proběhlo úspěšně.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
post:
|
||||
operationId: addStore
|
||||
summary: Přidá obchod do seznamu povolených (vyžaduje admin heslo).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- heslo
|
||||
properties:
|
||||
name:
|
||||
description: Název obchodu/restaurace
|
||||
type: string
|
||||
heslo:
|
||||
description: Admin heslo (ADMIN_PASSWORD)
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Obchod byl přidán
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
@@ -0,0 +1,28 @@
|
||||
post:
|
||||
operationId: deleteStore
|
||||
summary: Odebere obchod ze seznamu povolených (vyžaduje admin heslo).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- heslo
|
||||
properties:
|
||||
name:
|
||||
description: Název obchodu/restaurace k odebrání
|
||||
type: string
|
||||
heslo:
|
||||
description: Admin heslo (ADMIN_PASSWORD)
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Obchod byl odebrán
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
@@ -0,0 +1,12 @@
|
||||
get:
|
||||
operationId: listStores
|
||||
summary: Vrátí seznam povolených obchodů/restaurací.
|
||||
responses:
|
||||
"200":
|
||||
description: Seznam obchodů
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
+122
-21
@@ -66,6 +66,16 @@ ClientData:
|
||||
slot:
|
||||
description: Slot jídla, ke kterému se tato data vztahují
|
||||
$ref: "#/MealSlot"
|
||||
groups:
|
||||
description: Skupiny objednávajících pro extra slot
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/OrderGroup"
|
||||
stores:
|
||||
description: Seznam povolených obchodů/restaurací pro extra objednávky
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
# --- OBĚDY ---
|
||||
UserLunchChoice:
|
||||
@@ -262,7 +272,6 @@ DepartureTime:
|
||||
FeatureRequest:
|
||||
type: string
|
||||
enum:
|
||||
- Ruční generování QR kódů mimo Pizza day (např. při objednávání)
|
||||
- Možnost označovat si jídla jako oblíbená (taková jídla by se uživateli následně zvýrazňovala)
|
||||
- Možnost úhrady v podniku za všechny jednou osobou a následné generování QR ostatním
|
||||
- Zrušení \"užívejte víkend\", místo toho umožnit zpětně náhled na uplynulý týden
|
||||
@@ -275,7 +284,6 @@ FeatureRequest:
|
||||
- Celkové vylepšení UI/UX
|
||||
- Zlepšení dokumentace/postupů pro ostatní vývojáře
|
||||
x-enum-varnames:
|
||||
- CUSTOM_QR
|
||||
- FAVORITES
|
||||
- SINGLE_PAYMENT
|
||||
- NO_WEEKENDS
|
||||
@@ -412,14 +420,14 @@ PizzaSize:
|
||||
description: Velikost pizzy, např. "30cm"
|
||||
type: string
|
||||
pizzaPrice:
|
||||
description: Cena samotné pizzy v Kč
|
||||
type: number
|
||||
description: Cena samotné pizzy v haléřích
|
||||
type: integer
|
||||
boxPrice:
|
||||
description: Cena krabice pizzy v Kč
|
||||
type: number
|
||||
description: Cena krabice pizzy v haléřích
|
||||
type: integer
|
||||
price:
|
||||
description: Celková cena (pizza + krabice)
|
||||
type: number
|
||||
description: Celková cena (pizza + krabice) v haléřích
|
||||
type: integer
|
||||
Pizza:
|
||||
description: Údaje o konkrétní pizze.
|
||||
type: object
|
||||
@@ -462,8 +470,8 @@ PizzaVariant:
|
||||
description: Velikost pizzy (např. "30cm"), nebo "1 porce" pro salát
|
||||
type: string
|
||||
price:
|
||||
description: Cena v Kč, včetně krabice/obalu
|
||||
type: number
|
||||
description: Cena v haléřích, včetně krabice/obalu
|
||||
type: integer
|
||||
category:
|
||||
description: Kategorie položky (pizza nebo salat)
|
||||
type: string
|
||||
@@ -486,8 +494,8 @@ Salat:
|
||||
items:
|
||||
type: string
|
||||
price:
|
||||
description: Cena salátu v Kč (bez obalu)
|
||||
type: number
|
||||
description: Cena salátu v haléřích (bez obalu)
|
||||
type: integer
|
||||
PizzaOrder:
|
||||
description: Údaje o objednávce pizzy jednoho uživatele.
|
||||
type: object
|
||||
@@ -513,11 +521,11 @@ PizzaOrder:
|
||||
description: Popis příplatku (např. "kuřecí maso navíc")
|
||||
type: string
|
||||
price:
|
||||
description: Cena příplatku v Kč
|
||||
type: number
|
||||
description: Cena příplatku v haléřích
|
||||
type: integer
|
||||
totalPrice:
|
||||
description: Celková cena všech objednaných pizz daného uživatele, včetně krabic a příplatků
|
||||
type: number
|
||||
description: Celková cena všech objednaných pizz daného uživatele v haléřích, včetně krabic a příplatků
|
||||
type: integer
|
||||
hasQr:
|
||||
description: |
|
||||
Příznak, pokud je k této objednávce vygenerován QR kód pro platbu. To je typicky pravda, pokud:
|
||||
@@ -627,9 +635,9 @@ QrRecipient:
|
||||
description: Účel platby (např. "Pizza prosciutto")
|
||||
type: string
|
||||
amount:
|
||||
description: Částka v Kč (kladné číslo, max 2 desetinná místa)
|
||||
type: number
|
||||
minimum: 0.01
|
||||
description: Částka v haléřích (kladné celé číslo)
|
||||
type: integer
|
||||
minimum: 1
|
||||
GenerateQrRequest:
|
||||
description: Request pro generování QR kódů
|
||||
type: object
|
||||
@@ -650,6 +658,9 @@ GenerateQrRequest:
|
||||
bankAccountHolder:
|
||||
description: Jméno držitele bankovního účtu
|
||||
type: string
|
||||
groupId:
|
||||
description: ID skupiny objednávky (pro propojení QR kódů se skupinou)
|
||||
type: string
|
||||
|
||||
# --- DEV MOCK DATA ---
|
||||
GenerateMockDataRequest:
|
||||
@@ -674,6 +685,93 @@ ClearMockDataRequest:
|
||||
description: Index dne v týdnu (0 = pondělí, 4 = pátek). Pokud není zadán, použije se aktuální den.
|
||||
$ref: "#/DayIndex"
|
||||
|
||||
# --- SKUPINOVÉ OBJEDNÁVKY ---
|
||||
GroupState:
|
||||
description: Stav skupiny objednávky
|
||||
type: string
|
||||
enum:
|
||||
- open
|
||||
- locked
|
||||
- ordered
|
||||
x-enum-varnames:
|
||||
- OPEN
|
||||
- LOCKED
|
||||
- ORDERED
|
||||
|
||||
OrderGroupMember:
|
||||
description: Data člena skupiny objednávky
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
amount:
|
||||
description: Částka k úhradě v haléřích
|
||||
type: integer
|
||||
note:
|
||||
description: Volitelná poznámka (např. co si objednává)
|
||||
type: string
|
||||
surchargeText:
|
||||
description: Popis příplatku
|
||||
type: string
|
||||
surchargeAmount:
|
||||
description: Výše příplatku v haléřích
|
||||
type: integer
|
||||
paid:
|
||||
description: Příznak, zda člen uhradil svůj podíl objednávky
|
||||
type: boolean
|
||||
|
||||
OrderGroup:
|
||||
description: Skupina uživatelů objednávajících z jednoho místa
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- creatorLogin
|
||||
- state
|
||||
- members
|
||||
properties:
|
||||
id:
|
||||
description: Unikátní identifikátor skupiny
|
||||
type: string
|
||||
name:
|
||||
description: Název obchodu/restaurace
|
||||
type: string
|
||||
creatorLogin:
|
||||
description: Login zakladatele skupiny
|
||||
type: string
|
||||
state:
|
||||
$ref: "#/GroupState"
|
||||
members:
|
||||
description: Členové skupiny
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: "#/OrderGroupMember"
|
||||
orderedAt:
|
||||
description: Čas objednání ve formátu HH:MM
|
||||
type: string
|
||||
deliveryAt:
|
||||
description: Očekávaný čas doručení ve formátu HH:MM
|
||||
type: string
|
||||
fees:
|
||||
description: Poplatky (balení apod.) celkem v haléřích
|
||||
type: integer
|
||||
shipping:
|
||||
description: Doprava v haléřích
|
||||
type: integer
|
||||
tip:
|
||||
description: Spropitné v haléřích
|
||||
type: integer
|
||||
discountType:
|
||||
description: Typ slevy aplikované na objednávku
|
||||
type: string
|
||||
enum: [percent, fixed]
|
||||
discountValue:
|
||||
description: Hodnota slevy (procenta pro typ 'percent', haléře pro typ 'fixed')
|
||||
type: integer
|
||||
qrGenerated:
|
||||
description: Příznak, zda byly pro skupinu vygenerovány QR kódy (blokuje opakované generování)
|
||||
type: boolean
|
||||
|
||||
# --- NEVYŘÍZENÉ QR KÓDY ---
|
||||
PendingQr:
|
||||
description: Nevyřízený QR kód pro platbu
|
||||
@@ -695,8 +793,11 @@ PendingQr:
|
||||
description: Jméno uživatele, který QR vygeneroval (příjemce platby)
|
||||
type: string
|
||||
totalPrice:
|
||||
description: Celková cena objednávky v Kč
|
||||
type: number
|
||||
description: Celková cena objednávky v haléřích
|
||||
type: integer
|
||||
purpose:
|
||||
description: Účel platby (např. "Pizza prosciutto")
|
||||
type: string
|
||||
groupId:
|
||||
description: ID skupiny objednávky, ke které QR patří
|
||||
type: string
|
||||
|
||||
+53
-48
@@ -4,12 +4,12 @@
|
||||
|
||||
"@hey-api/client-fetch@^0.8.2":
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@hey-api/client-fetch/-/client-fetch-0.8.2.tgz#675aadfbc9478bb8eef5679f11a9334258dff4c8"
|
||||
resolved "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.8.2.tgz"
|
||||
integrity sha512-61T4UGfAzY5345vMxWDX8qnSTNRJcOpWuZyvNu3vNebCTLPwMQAM85mhEuBoACdWeRtLhNoUjU0UR5liRyD1bA==
|
||||
|
||||
"@hey-api/json-schema-ref-parser@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.0.2.tgz#c3824c5d9d531eeb5c2b2557857a8ad20b5c75a7"
|
||||
resolved "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.0.2.tgz"
|
||||
integrity sha512-F6LSkttZcT/XiX3ydeDqTY3uRN3BLJMwyMTk4kg/ichZlKUp3+3Odv0WokSmXGSoZGTW/N66FROMYAm5NPdJlA==
|
||||
dependencies:
|
||||
"@jsdevtools/ono" "^7.1.3"
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
"@hey-api/openapi-ts@^0.64.7":
|
||||
version "0.64.7"
|
||||
resolved "https://registry.yarnpkg.com/@hey-api/openapi-ts/-/openapi-ts-0.64.7.tgz#f239d268b4a35b91f5ff25479d15578feb01f365"
|
||||
resolved "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.64.7.tgz"
|
||||
integrity sha512-xpaBzdGAKz7cPuGah1GZWl3zTZquOXRnwmpVCQKEUpvDtSYWBLjSxtAYIqDBjwJkuNHcakZBIWLcappx7slc2g==
|
||||
dependencies:
|
||||
"@hey-api/json-schema-ref-parser" "1.0.2"
|
||||
@@ -28,27 +28,27 @@
|
||||
|
||||
"@jsdevtools/ono@^7.1.3":
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
|
||||
resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz"
|
||||
integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==
|
||||
|
||||
"@types/json-schema@^7.0.15":
|
||||
version "7.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
|
||||
acorn@^8.14.0:
|
||||
version "8.14.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz"
|
||||
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
|
||||
|
||||
argparse@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
|
||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||
|
||||
c12@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/c12/-/c12-2.0.1.tgz#5702d280b31a08abba39833494c9b1202f0f5aec"
|
||||
resolved "https://registry.npmjs.org/c12/-/c12-2.0.1.tgz"
|
||||
integrity sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==
|
||||
dependencies:
|
||||
chokidar "^4.0.1"
|
||||
@@ -66,63 +66,63 @@ c12@2.0.1:
|
||||
|
||||
chokidar@^4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
|
||||
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
|
||||
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
|
||||
dependencies:
|
||||
readdirp "^4.0.1"
|
||||
|
||||
chownr@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
|
||||
resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz"
|
||||
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
|
||||
|
||||
citty@^0.1.6:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.6.tgz#0f7904da1ed4625e1a9ea7e0fa780981aab7c5e4"
|
||||
resolved "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz"
|
||||
integrity sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==
|
||||
dependencies:
|
||||
consola "^3.2.3"
|
||||
|
||||
commander@13.0.0:
|
||||
version "13.0.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-13.0.0.tgz#1b161f60ee3ceb8074583a0f95359a4f8701845c"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz"
|
||||
integrity sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==
|
||||
|
||||
confbox@^0.1.7, confbox@^0.1.8:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06"
|
||||
resolved "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz"
|
||||
integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==
|
||||
|
||||
consola@^3.2.3, consola@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.0.tgz#4cfc9348fd85ed16a17940b3032765e31061ab88"
|
||||
resolved "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz"
|
||||
integrity sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==
|
||||
|
||||
defu@^6.1.4:
|
||||
version "6.1.4"
|
||||
resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479"
|
||||
resolved "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz"
|
||||
integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==
|
||||
|
||||
destr@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449"
|
||||
resolved "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz"
|
||||
integrity sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==
|
||||
|
||||
dotenv@^16.4.5:
|
||||
version "16.4.7"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26"
|
||||
resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz"
|
||||
integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==
|
||||
|
||||
fs-minipass@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
|
||||
resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz"
|
||||
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
|
||||
dependencies:
|
||||
minipass "^3.0.0"
|
||||
|
||||
giget@^1.2.3:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/giget/-/giget-1.2.5.tgz#0bd4909356a0da75cc1f2b33538f93adec0d202f"
|
||||
resolved "https://registry.npmjs.org/giget/-/giget-1.2.5.tgz"
|
||||
integrity sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug==
|
||||
dependencies:
|
||||
citty "^0.1.6"
|
||||
@@ -135,7 +135,7 @@ giget@^1.2.3:
|
||||
|
||||
handlebars@4.7.8:
|
||||
version "4.7.8"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9"
|
||||
resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz"
|
||||
integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
@@ -147,36 +147,36 @@ handlebars@4.7.8:
|
||||
|
||||
jiti@^2.3.0:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.2.tgz#d19b7732ebb6116b06e2038da74a55366faef560"
|
||||
resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz"
|
||||
integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==
|
||||
|
||||
js-yaml@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||
resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz"
|
||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
minimist@^1.2.5:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
minipass@^3.0.0:
|
||||
version "3.3.6"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a"
|
||||
resolved "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz"
|
||||
integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
minipass@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
|
||||
resolved "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz"
|
||||
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
|
||||
|
||||
minizlib@^2.1.1:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
|
||||
resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz"
|
||||
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
|
||||
dependencies:
|
||||
minipass "^3.0.0"
|
||||
@@ -184,12 +184,12 @@ minizlib@^2.1.1:
|
||||
|
||||
mkdirp@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
mlly@^1.7.1, mlly@^1.7.4:
|
||||
version "1.7.4"
|
||||
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.4.tgz#3d7295ea2358ec7a271eaa5d000a0f84febe100f"
|
||||
resolved "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz"
|
||||
integrity sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==
|
||||
dependencies:
|
||||
acorn "^8.14.0"
|
||||
@@ -199,17 +199,17 @@ mlly@^1.7.1, mlly@^1.7.4:
|
||||
|
||||
neo-async@^2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||
resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz"
|
||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||
|
||||
node-fetch-native@^1.6.6:
|
||||
version "1.6.6"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.6.tgz#ae1d0e537af35c2c0b0de81cbff37eedd410aa37"
|
||||
resolved "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz"
|
||||
integrity sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==
|
||||
|
||||
nypm@^0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.5.4.tgz#a5ab0d8d37f96342328479f88ef58699f29b3051"
|
||||
resolved "https://registry.npmjs.org/nypm/-/nypm-0.5.4.tgz"
|
||||
integrity sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA==
|
||||
dependencies:
|
||||
citty "^0.1.6"
|
||||
@@ -221,27 +221,32 @@ nypm@^0.5.4:
|
||||
|
||||
ohash@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.4.tgz#ae8d83014ab81157d2c285abf7792e2995fadd72"
|
||||
resolved "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz"
|
||||
integrity sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==
|
||||
|
||||
pathe@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec"
|
||||
resolved "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz"
|
||||
integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==
|
||||
|
||||
pathe@^2.0.1, pathe@^2.0.3:
|
||||
pathe@^2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
|
||||
resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz"
|
||||
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
|
||||
|
||||
pathe@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz"
|
||||
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
|
||||
|
||||
perfect-debounce@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
|
||||
resolved "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz"
|
||||
integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==
|
||||
|
||||
pkg-types@^1.2.0, pkg-types@^1.3.0, pkg-types@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.3.1.tgz#bd7cc70881192777eef5326c19deb46e890917df"
|
||||
resolved "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz"
|
||||
integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==
|
||||
dependencies:
|
||||
confbox "^0.1.8"
|
||||
@@ -250,7 +255,7 @@ pkg-types@^1.2.0, pkg-types@^1.3.0, pkg-types@^1.3.1:
|
||||
|
||||
rc9@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/rc9/-/rc9-2.1.2.tgz#6282ff638a50caa0a91a31d76af4a0b9cbd1080d"
|
||||
resolved "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz"
|
||||
integrity sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==
|
||||
dependencies:
|
||||
defu "^6.1.4"
|
||||
@@ -258,17 +263,17 @@ rc9@^2.1.2:
|
||||
|
||||
readdirp@^4.0.1:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
|
||||
resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz"
|
||||
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
|
||||
|
||||
source-map@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
tar@^6.2.1:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
|
||||
resolved "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz"
|
||||
integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
|
||||
dependencies:
|
||||
chownr "^2.0.0"
|
||||
@@ -280,30 +285,30 @@ tar@^6.2.1:
|
||||
|
||||
tinyexec@^0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2"
|
||||
resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz"
|
||||
integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==
|
||||
|
||||
typescript@^5.9.3:
|
||||
typescript@^5.5.3, typescript@^5.9.3:
|
||||
version "5.9.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz"
|
||||
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
||||
|
||||
ufo@^1.5.4:
|
||||
version "1.5.4"
|
||||
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754"
|
||||
resolved "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz"
|
||||
integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.19.3"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f"
|
||||
resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz"
|
||||
integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==
|
||||
|
||||
wordwrap@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
|
||||
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
Reference in New Issue
Block a user