diff --git a/client/src/components/modals/StoreAdminModal.tsx b/client/src/components/modals/StoreAdminModal.tsx index d1820a9..e416344 100644 --- a/client/src/components/modals/StoreAdminModal.tsx +++ b/client/src/components/modals/StoreAdminModal.tsx @@ -2,8 +2,9 @@ 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 { faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { faPlus, faUpRightFromSquare, faXmark } from "@fortawesome/free-solid-svg-icons"; import { addStore, deleteStore, Store } from "../../../../types"; +import { getStoreUrls, storeUrlLabel } from "../../utils/storeUrls"; type Props = { isOpen: boolean; @@ -14,23 +15,33 @@ type Props = { export default function StoreAdminModal({ isOpen, onClose, stores, onStoresChanged }: Readonly) { const [newName, setNewName] = useState(''); - const [newUrl, setNewUrl] = useState(''); + // Jeden podnik může být dostupný přes více dovozových služeb — proto seznam URL + const [newUrls, setNewUrls] = useState(['']); const [heslo, setHeslo] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const setUrlAt = (index: number, value: string) => + setNewUrls(prev => prev.map((u, i) => (i === index ? value : u))); + + const addUrlRow = () => setNewUrls(prev => [...prev, '']); + + const removeUrlRow = (index: number) => + setNewUrls(prev => (prev.length === 1 ? [''] : prev.filter((_, i) => i !== index))); + const handleAdd = async () => { if (!newName.trim()) return; setError(null); setLoading(true); try { - const res = await addStore({ body: { name: newName.trim(), url: newUrl.trim() || undefined, heslo } }); + const urls = newUrls.map(u => u.trim()).filter(Boolean); + const res = await addStore({ body: { name: newName.trim(), urls: urls.length > 0 ? urls : undefined, heslo } }); if (res.error) { setError((res.error as any).error || 'Nastala chyba'); } else if (res.data) { onStoresChanged(res.data as Store[]); setNewName(''); - setNewUrl(''); + setNewUrls(['']); } } catch (e: any) { setError(e.message || 'Nastala chyba'); @@ -89,14 +100,32 @@ export default function StoreAdminModal({ isOpen, onClose, stores, onStoresChang onChange={e => setNewName(e.target.value)} onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleAdd(); }} /> -
- setNewUrl(e.target.value)} - onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleAdd(); }} - /> + {newUrls.map((url, index) => ( +
+ setUrlAt(index, e.target.value)} + onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') handleAdd(); }} + /> + +
+ ))} +
+ @@ -107,25 +136,39 @@ export default function StoreAdminModal({ isOpen, onClose, stores, onStoresChang

Žádné obchody v seznamu

) : ( - {stores.map(s => ( - - - {s.name} - {s.url && /^https?:\/\//i.test(s.url) && ( - - - - )} - - handleRemove(s.name)} - style={{ cursor: 'pointer' }} - /> - - ))} + {stores.map(s => { + const urls = getStoreUrls(s); + return ( + +
+
{s.name}
+ {urls.length > 0 && ( + + {urls.map(url => ( + + {storeUrlLabel(url)} + + + ))} + + )} +
+ handleRemove(s.name)} + style={{ cursor: 'pointer' }} + /> +
+ ); + })}
)} diff --git a/client/src/pages/OrderGroupsPage.tsx b/client/src/pages/OrderGroupsPage.tsx index b9b3d5e..8e2ce27 100644 --- a/client/src/pages/OrderGroupsPage.tsx +++ b/client/src/pages/OrderGroupsPage.tsx @@ -11,6 +11,7 @@ import { getData, createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, setTracking, getOrderDates, } from '../../../types'; import { computeFeeShare, computeMemberTotal, countActiveMembers } from '../utils/groupFees'; +import { getStoreUrls, isHttpUrl, storeUrlLabel } from '../utils/storeUrls'; import { EVENT_MESSAGE, EVENT_PENDING_QR, SocketContext } from '../context/socket'; import { useAuth } from '../context/auth'; import { useSettings } from '../context/settings'; @@ -125,6 +126,8 @@ export default function OrderGroupsPage() { // ISO data dnů, ve kterých existuje aspoň jedna objednávka (pro zvýraznění v date pickeru) const [orderDates, setOrderDates] = useState([]); const [newGroupName, setNewGroupName] = useState(''); + // Vybraná URL na nabídku — podnik může být dostupný přes více dovozových služeb + const [newGroupUrl, setNewGroupUrl] = useState(''); const [creating, setCreating] = useState(false); const [adminModalOpen, setAdminModalOpen] = useState(false); const [editAmounts, setEditAmounts] = useState>({}); @@ -248,11 +251,18 @@ export default function OrderGroupsPage() { const handleCreate = async () => { if (!newGroupName || !auth?.login) return; setCreating(true); - const ok = await refresh(() => createGroup({ body: { name: newGroupName } })); - if (ok) setNewGroupName(''); + // Pojistka, kdyby se seznam obchodů mezitím změnil (socket) — vezmeme první URL podniku + const urls = getStoreUrls((data?.stores ?? []).find(s => s.name === newGroupName)); + const url = urls.includes(newGroupUrl) ? newGroupUrl : urls[0]; + const ok = await refresh(() => createGroup({ body: { name: newGroupName, url } })); + if (ok) { + setNewGroupName(''); + setNewGroupUrl(''); + } setCreating(false); }; + const handleJoin = (groupId: string) => refresh(() => addGroupMember({ body: { id: groupId } })); @@ -359,6 +369,8 @@ export default function OrderGroupsPage() { const stores = data.stores ?? []; const groups = data.groups ?? []; + // URL na nabídku vybíraného podniku — výběr nabízíme, jen když jich je víc + const newStoreUrls = getStoreUrls(stores.find(s => s.name === newGroupName)); // Zobrazené datum a režim historie (vše read-only, pokud nejde o aktuální den) const displayedIso = data.isoDate; @@ -456,12 +468,29 @@ export default function OrderGroupsPage() {
setNewGroupName(e.target.value)} + onChange={e => { + const name = e.target.value; + setNewGroupName(name); + // Výběr nabídky se resetuje na první URL nově vybraného podniku + setNewGroupUrl(getStoreUrls(stores.find(s => s.name === name))[0] ?? ''); + }} style={{ maxWidth: 260 }} > {stores.map(s => )} + {newStoreUrls.length > 1 && ( + setNewGroupUrl(e.target.value)} + style={{ maxWidth: 220 }} + title="Odkud se bude objednávat" + > + {newStoreUrls.map(url => ( + + ))} + + )} @@ -485,10 +514,13 @@ export default function OrderGroupsPage() { const isLocked = group.state === GroupState.LOCKED; const memberEntries = Object.entries(group.members) as [string, OrderGroupMember][]; const editingTimes = group.id in editTimes; - // URL na nabídku podniku (pokud ji má dohledatelný obchod vyplněnou). + // URL na nabídku podniku — přednostně ta, kterou zakladatel vybral při vytvoření + // skupiny. Skupiny z doby před výběrem URL ji nemají, pak zkusíme jedinou URL obchodu. // Povolíme jen http(s), aby odkaz nemohl být zneužit (např. javascript:). - const rawStoreUrl = stores.find(s => s.name === group.name)?.url; - const storeUrl = rawStoreUrl && /^https?:\/\//i.test(rawStoreUrl) ? rawStoreUrl : undefined; + const groupStoreUrls = getStoreUrls(stores.find(s => s.name === group.name)); + const storeUrl = isHttpUrl(group.url) + ? group.url + : (groupStoreUrls.length === 1 ? groupStoreUrls[0] : undefined); const totalFees = (group.fees ?? 0) + (group.shipping ?? 0) + (group.tip ?? 0); // Poplatky se dělí jen mezi aktivní strávníky (kdo si reálně něco objednal). diff --git a/client/src/utils/storeUrls.ts b/client/src/utils/storeUrls.ts new file mode 100644 index 0000000..0075922 --- /dev/null +++ b/client/src/utils/storeUrls.ts @@ -0,0 +1,37 @@ +import { Store } from '../../../types'; + +/** Známé dovozové služby — hostname (bez www) → čitelný název pro nabídku. */ +const KNOWN_SERVICES: { match: string; label: string }[] = [ + { match: 'bolt.eu', label: 'Bolt Food' }, + { match: 'wolt.com', label: 'Wolt' }, + { match: 'foodora.cz', label: 'Foodora' }, + { match: 'damejidlo.cz', label: 'Dáme jídlo' }, +]; + +/** + * Ověří, že jde o odkaz s protokolem http(s) — jiné protokoly (např. javascript:) + * by mohly být zneužity, proto je jako odkaz nezobrazujeme. + */ +export function isHttpUrl(url?: string): boolean { + return !!url && /^https?:\/\//i.test(url); +} + +/** Vrátí platné URL na nabídku daného obchodu. */ +export function getStoreUrls(store?: Store): string[] { + return (store?.urls ?? []).filter(isHttpUrl); +} + +/** + * Popisek URL pro výběr nabídky — u známých dovozových služeb jejich název, + * jinak hostname odkazu (např. "restaurace-u-nas.cz"). + */ +export function storeUrlLabel(url: string): string { + let hostname: string; + try { + hostname = new URL(url).hostname.toLowerCase().replace(/^www\./, ''); + } catch { + return url; + } + const known = KNOWN_SERVICES.find(s => hostname === s.match || hostname.endsWith(`.${s.match}`)); + return known ? known.label : hostname; +} diff --git a/server/changelogs/2026-08-25.json b/server/changelogs/2026-08-25.json index 6d2be4f..f2bca89 100644 --- a/server/changelogs/2026-08-25.json +++ b/server/changelogs/2026-08-25.json @@ -1,3 +1,5 @@ [ - "Statistiky se nově načtou i pro aktuální týden, který ještě neskončil" + "Statistiky se nově načtou i pro aktuální týden, který ještě neskončil", + "Podnik může mít více odkazů na nabídku (např. Bolt Food, Wolt i Foodora)", + "Při vytváření skupiny lze vybrat, přes kterou službu se bude objednávat — odkaz v záhlaví skupiny pak vede přímo na ni" ] diff --git a/server/src/groups.ts b/server/src/groups.ts index 6d713c5..c0fe18b 100644 --- a/server/src/groups.ts +++ b/server/src/groups.ts @@ -47,19 +47,41 @@ export async function getOrderDates(): Promise { return dates.sort(); } -export async function createGroup(creatorLogin: string, name: string, date?: Date): Promise { +/** + * Vytvoří novou skupinu objednávky. + * + * @param creatorLogin login zakladatele + * @param name název obchodu (musí být v seznamu povolených obchodů) + * @param date den, ke kterému skupina patří (výchozí dnešní) + * @param url vybraná URL na nabídku podniku — musí být jednou z URL obchodu. + * Pokud není předána a obchod má právě jednu URL, dosadí se automaticky. + */ +export async function createGroup(creatorLogin: string, name: string, date?: Date, url?: string): Promise { const stores = await getStores(); - if (!stores.some(s => s.name.toLowerCase() === name.trim().toLowerCase())) { + const store = stores.find(s => s.name.toLowerCase() === name.trim().toLowerCase()); + if (!store) { throw new Error('Obchod není v seznamu povolených obchodů'); } + const storeUrls = store.urls ?? []; + const requestedUrl = url?.trim(); + let selectedUrl: string | undefined; + if (requestedUrl) { + selectedUrl = storeUrls.find(u => u === requestedUrl); + if (!selectedUrl) { + throw new Error('Vybraná URL nepatří k tomuto obchodu'); + } + } else if (storeUrls.length === 1) { + // Obchod má jedinou nabídku — vybírat není z čeho + selectedUrl = storeUrls[0]; + } const data = await getExtraData(date); - const canonical = stores.find(s => s.name.toLowerCase() === name.trim().toLowerCase())!.name; const group: OrderGroup = { id: crypto.randomUUID(), - name: canonical, + name: store.name, creatorLogin, state: GroupState.OPEN, members: { [creatorLogin]: {} }, + ...(selectedUrl ? { url: selectedUrl } : {}), }; data.groups = [...(data.groups ?? []), group]; return saveExtraData(data, date); diff --git a/server/src/routes/groupRoutes.ts b/server/src/routes/groupRoutes.ts index 8038680..3787976 100644 --- a/server/src/routes/groupRoutes.ts +++ b/server/src/routes/groupRoutes.ts @@ -21,12 +21,15 @@ router.get("/dates", async (_req, res, next) => { router.post("/create", async (req: Request, res, next) => { const login = getLogin(parseToken(req)); - const { name } = req.body ?? {}; + const { name, url } = req.body ?? {}; if (!name || typeof name !== 'string') { return res.status(400).json({ error: 'Nebyl předán název skupiny' }); } + if (url != null && typeof url !== 'string') { + return res.status(400).json({ error: 'Neplatná URL nabídky' }); + } try { - const data = await createGroup(login, name); + const data = await createGroup(login, name, undefined, url); broadcastExtra(data); res.status(200).json(data); } catch (e: any) { next(e); } diff --git a/server/src/routes/storeRoutes.ts b/server/src/routes/storeRoutes.ts index b00c700..727a6d3 100644 --- a/server/src/routes/storeRoutes.ts +++ b/server/src/routes/storeRoutes.ts @@ -11,18 +11,18 @@ router.get("/", async (_req, res, next) => { }); router.post("/add", async (req, res, next) => { - const { name, heslo, url } = req.body ?? {}; + const { name, heslo, urls } = 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' }); } - if (url != null && typeof url !== 'string') { - return res.status(400).json({ error: 'Neplatná URL obchodu' }); + if (urls != null && (!Array.isArray(urls) || urls.some((u: unknown) => typeof u !== 'string'))) { + return res.status(400).json({ error: 'Neplatný seznam URL obchodu' }); } try { - const stores = await addStore(name, heslo, url); + const stores = await addStore(name, heslo, urls); res.status(200).json(stores); } catch (e: any) { if (e.message === 'UNAUTHORIZED') { diff --git a/server/src/stores.ts b/server/src/stores.ts index f725c17..2b1e2db 100644 --- a/server/src/stores.ts +++ b/server/src/stores.ts @@ -4,16 +4,68 @@ import getStorage from "./storage"; const storage = getStorage(); const STORES_KEY = 'stores'; +/** Maximální počet URL na jeden obchod (ochrana proti nafouknutí dat). */ +const MAX_URLS = 10; + /** - * Vrátí seznam povolených obchodů. Zachovává zpětnou kompatibilitu se starším - * formátem, kdy byly obchody uloženy jako pole řetězců (převede je na objekty). + * Podoby, v jakých mohou být obchody uloženy ve storage: + * - řetězec — nejstarší formát (jen název), + * - objekt s `url` — formát s jednou URL na nabídku, + * - objekt s `urls` — aktuální formát s více URL. + */ +type StoredStore = string | { name: string; url?: string; urls?: string[] }; + +/** + * Vrátí seznam povolených obchodů. Zachovává zpětnou kompatibilitu se staršími + * formáty uložených dat (pole řetězců, resp. jediná URL v poli `url`) — vše + * převede na aktuální podobu se seznamem `urls`. */ export async function getStores(): Promise { - const raw = await storage.getData<(string | Store)[]>(STORES_KEY); + const raw = await storage.getData(STORES_KEY); if (!raw) { return []; } - return raw.map(s => (typeof s === 'string' ? { name: s } : s)); + return raw.map(s => { + if (typeof s === 'string') { + return { name: s }; + } + const urls = s.urls ?? (s.url ? [s.url] : []); + return urls.length > 0 ? { name: s.name, urls } : { name: s.name }; + }); +} + +/** + * Ověří a upraví seznam URL obchodu — ořeže mezery, zahodí prázdné hodnoty, + * odstraní duplicity a zkontroluje, že jde o platné http(s) odkazy. + */ +function normalizeUrls(urls?: string[]): string[] { + if (!urls) { + return []; + } + const result: string[] = []; + for (const raw of urls) { + const trimmed = raw?.trim(); + if (!trimmed) { + continue; + } + // Povolíme pouze http(s), aby URL nemohla být zneužita (např. javascript: → XSS) + let parsed: URL; + try { + parsed = new URL(trimmed); + } catch { + throw new Error('Neplatná URL obchodu'); + } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + throw new Error('URL musí začínat http:// nebo https://'); + } + if (!result.some(u => u.toLowerCase() === trimmed.toLowerCase())) { + result.push(trimmed); + } + } + if (result.length > MAX_URLS) { + throw new Error(`Obchod může mít nejvýše ${MAX_URLS} URL`); + } + return result; } /** @@ -21,9 +73,9 @@ export async function getStores(): Promise { * * @param name název obchodu * @param heslo admin heslo - * @param url volitelná URL na nabídku podniku + * @param urls volitelný seznam URL na nabídku podniku (různé dovozové služby) */ -export async function addStore(name: string, heslo: string, url?: string): Promise { +export async function addStore(name: string, heslo: string, urls?: string[]): Promise { const adminPassword = process.env.ADMIN_PASSWORD; if (!adminPassword || heslo !== adminPassword) { throw new Error('UNAUTHORIZED'); @@ -36,20 +88,8 @@ export async function addStore(name: string, heslo: string, url?: string): Promi if (stores.some(s => s.name.toLowerCase() === trimmed.toLowerCase())) { throw new Error('Obchod s tímto názvem již existuje'); } - const trimmedUrl = url?.trim(); - if (trimmedUrl) { - // Povolíme pouze http(s), aby URL nemohla být zneužita (např. javascript: → XSS) - let parsed: URL; - try { - parsed = new URL(trimmedUrl); - } catch { - throw new Error('Neplatná URL obchodu'); - } - if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { - throw new Error('URL musí začínat http:// nebo https://'); - } - } - const store: Store = trimmedUrl ? { name: trimmed, url: trimmedUrl } : { name: trimmed }; + const normalizedUrls = normalizeUrls(urls); + const store: Store = normalizedUrls.length > 0 ? { name: trimmed, urls: normalizedUrls } : { name: trimmed }; const updated = [...stores, store]; await storage.setData(STORES_KEY, updated); return updated; diff --git a/server/src/tests/groups.test.ts b/server/src/tests/groups.test.ts index 4335e81..d991415 100644 --- a/server/src/tests/groups.test.ts +++ b/server/src/tests/groups.test.ts @@ -40,6 +40,42 @@ describe('createGroup', () => { expect(d2.groups).toHaveLength(2); expect(d2.groups![1].id).not.toBe(d2.groups![0].id); }); + + describe('výběr URL na nabídku', () => { + const MULTI_STORE = 'Bistro'; + const WOLT_URL = 'https://wolt.com/bistro'; + const BOLT_URL = 'https://food.bolt.eu/bistro'; + + beforeEach(async () => { + await addStore(MULTI_STORE, ADMIN_PW, [WOLT_URL, BOLT_URL]); + }); + + test('uloží vybranou URL do skupiny', async () => { + const data = await createGroup(CREATOR, MULTI_STORE, TODAY, BOLT_URL); + expect(data.groups![0].url).toBe(BOLT_URL); + }); + + test('odmítne URL, která k obchodu nepatří', async () => { + await expect(createGroup(CREATOR, MULTI_STORE, TODAY, 'https://wolt.com/jine')) + .rejects.toThrow('nepatří'); + }); + + test('bez výběru zůstane skupina bez URL, když má obchod více nabídek', async () => { + const data = await createGroup(CREATOR, MULTI_STORE, TODAY); + expect(data.groups![0].url).toBeUndefined(); + }); + + test('dosadí jedinou URL obchodu automaticky', async () => { + await addStore('Pizzerie', ADMIN_PW, [WOLT_URL]); + const data = await createGroup(CREATOR, 'Pizzerie', TODAY); + expect(data.groups![0].url).toBe(WOLT_URL); + }); + + test('obchod bez URL vytvoří skupinu bez URL', async () => { + const data = await createGroup(CREATOR, STORE, TODAY); + expect(data.groups![0].url).toBeUndefined(); + }); + }); }); describe('deleteGroup', () => { diff --git a/server/src/tests/stores.test.ts b/server/src/tests/stores.test.ts index b291a1d..8e180a0 100644 --- a/server/src/tests/stores.test.ts +++ b/server/src/tests/stores.test.ts @@ -28,6 +28,13 @@ describe('getStores', () => { const stores = await getStores(); expect(stores).toEqual([{ name: 'McDonald\'s' }, { name: 'KFC' }]); }); + + test('převede starý formát s jednou URL na seznam urls', async () => { + // Simulace dat uložených před zavedením více URL na jeden podnik + await getStorage().setData('stores', [{ name: 'Bistro', url: 'https://wolt.com/bistro' }]); + const stores = await getStores(); + expect(stores).toEqual([{ name: 'Bistro', urls: ['https://wolt.com/bistro'] }]); + }); }); describe('addStore', () => { @@ -37,21 +44,46 @@ describe('addStore', () => { }); test('uloží volitelnou URL a ořízne mezery', async () => { - const stores = await addStore('Bistro', ADMIN_PW, ' https://wolt.com/bistro '); - expect(stores).toContainEqual({ name: 'Bistro', url: 'https://wolt.com/bistro' }); + const stores = await addStore('Bistro', ADMIN_PW, [' https://wolt.com/bistro ']); + expect(stores).toContainEqual({ name: 'Bistro', urls: ['https://wolt.com/bistro'] }); }); - test('bez URL uloží obchod bez pole url', async () => { - const stores = await addStore('KFC', ADMIN_PW, ' '); + test('uloží více URL na jeden podnik (různé dovozové služby)', async () => { + const stores = await addStore('Bistro', ADMIN_PW, [ + 'https://wolt.com/bistro', + 'https://food.bolt.eu/bistro', + ]); + expect(stores).toContainEqual({ + name: 'Bistro', + urls: ['https://wolt.com/bistro', 'https://food.bolt.eu/bistro'], + }); + }); + + test('zahodí prázdné URL a duplicity (case-insensitive)', async () => { + const stores = await addStore('Bistro', ADMIN_PW, [ + 'https://wolt.com/bistro', + ' ', + 'https://WOLT.com/bistro', + ]); + expect(stores).toContainEqual({ name: 'Bistro', urls: ['https://wolt.com/bistro'] }); + }); + + test('bez URL uloží obchod bez pole urls', async () => { + const stores = await addStore('KFC', ADMIN_PW, [' ']); expect(stores).toContainEqual({ name: 'KFC' }); }); test('odmítne URL s nepovoleným schématem (javascript:)', async () => { - await expect(addStore('Zlo', ADMIN_PW, 'javascript:alert(1)')).rejects.toThrow('http'); + await expect(addStore('Zlo', ADMIN_PW, ['javascript:alert(1)'])).rejects.toThrow('http'); }); - test('odmítne nevalidní URL', async () => { - await expect(addStore('Zlo', ADMIN_PW, 'nfdjska')).rejects.toThrow('Neplatná URL'); + test('odmítne nevalidní URL i mezi platnými', async () => { + await expect(addStore('Zlo', ADMIN_PW, ['https://wolt.com/x', 'nfdjska'])).rejects.toThrow('Neplatná URL'); + }); + + test('odmítne příliš mnoho URL', async () => { + const urls = Array.from({ length: 11 }, (_, i) => `https://wolt.com/bistro${i}`); + await expect(addStore('Bistro', ADMIN_PW, urls)).rejects.toThrow('nejvýše'); }); test('vyhodí UNAUTHORIZED s nesprávným heslem', async () => { diff --git a/types/paths/groups/createGroup.yml b/types/paths/groups/createGroup.yml index 582fde2..c559f51 100644 --- a/types/paths/groups/createGroup.yml +++ b/types/paths/groups/createGroup.yml @@ -13,6 +13,9 @@ post: name: description: Název obchodu/restaurace (musí být v seznamu povolených obchodů) type: string + url: + description: Volitelná URL na nabídku podniku (musí být jednou z URL daného obchodu). Pokud má obchod právě jednu URL, dosadí se automaticky. + type: string responses: "200": $ref: "../../api.yml#/components/responses/ClientDataResponse" diff --git a/types/paths/stores/addStore.yml b/types/paths/stores/addStore.yml index cd69025..ba2d94d 100644 --- a/types/paths/stores/addStore.yml +++ b/types/paths/stores/addStore.yml @@ -14,9 +14,11 @@ post: name: description: Název obchodu/restaurace type: string - url: - description: Volitelná URL na nabídku podniku (např. Bolt Food/Wolt/Foodora) - type: string + urls: + description: Volitelný seznam URL na nabídku podniku (např. Bolt Food/Wolt/Foodora) + type: array + items: + type: string heslo: description: Admin heslo (ADMIN_PASSWORD) type: string diff --git a/types/schemas/_index.yml b/types/schemas/_index.yml index aaa57ad..a76cc68 100644 --- a/types/schemas/_index.yml +++ b/types/schemas/_index.yml @@ -886,9 +886,11 @@ Store: name: description: Název obchodu/restaurace type: string - url: - description: Volitelná URL na nabídku podniku (např. Bolt Food/Wolt/Foodora) - type: string + urls: + description: Seznam URL na nabídku podniku — jeden podnik může být dostupný přes více dovozových služeb (např. Bolt Food/Wolt/Foodora) + type: array + items: + type: string TrackingProvider: description: Rozvozová služba, přes kterou lze sledovat stav objednávky @@ -950,6 +952,9 @@ OrderGroup: name: description: Název obchodu/restaurace type: string + url: + description: URL na nabídku podniku vybraná při zakládání skupiny (jedna z URL obchodu) + type: string creatorLogin: description: Login zakladatele skupiny type: string