feat: možnost editace existujících podniků u objednávání

This commit is contained in:
2026-08-25 11:43:35 +02:00
parent ce051447e6
commit 6f6567a2a9
7 changed files with 327 additions and 51 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
[
"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"
"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",
"Existující podnik lze upravit — přejmenovat i změnit odkazy na nabídku, bez nutnosti smazat a znovu přidat"
]
+26 -1
View File
@@ -1,5 +1,5 @@
import express from "express";
import { getStores, addStore, removeStore } from "../stores";
import { getStores, addStore, updateStore, removeStore } from "../stores";
const router = express.Router();
@@ -32,6 +32,31 @@ router.post("/add", async (req, res, next) => {
}
});
router.post("/update", async (req, res, next) => {
const { name, newName, 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 (newName != null && typeof newName !== 'string') {
return res.status(400).json({ error: 'Neplatný název 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 updateStore(name, heslo, newName ?? undefined, urls ?? undefined);
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') {
+40
View File
@@ -95,6 +95,46 @@ export async function addStore(name: string, heslo: string, urls?: string[]): Pr
return updated;
}
/**
* Upraví existující obchod — název a/nebo seznam URL na nabídku.
*
* @param name aktuální název obchodu (identifikuje upravovaný obchod)
* @param heslo admin heslo
* @param newName nový název; pokud není předán, název zůstane nezměněn
* @param urls nový seznam URL (nahradí stávající); pokud není předán, URL zůstanou nezměněné
*/
export async function updateStore(name: string, heslo: string, newName?: string, urls?: string[]): Promise<Store[]> {
const adminPassword = process.env.ADMIN_PASSWORD;
if (!adminPassword || heslo !== adminPassword) {
throw new Error('UNAUTHORIZED');
}
const stores = await getStores();
const index = stores.findIndex(s => s.name.toLowerCase() === name.trim().toLowerCase());
if (index < 0) {
throw new Error('Obchod nebyl nalezen');
}
const current = stores[index];
let finalName = current.name;
if (newName !== undefined) {
const trimmedName = newName.trim();
if (!trimmedName) {
throw new Error('Název obchodu nesmí být prázdný');
}
// Nový název nesmí kolidovat s jiným obchodem (shoda se sebou samým je v pořádku)
if (stores.some((s, i) => i !== index && s.name.toLowerCase() === trimmedName.toLowerCase())) {
throw new Error('Obchod s tímto názvem již existuje');
}
finalName = trimmedName;
}
const finalUrls = urls !== undefined ? normalizeUrls(urls) : (current.urls ?? []);
const updatedStore: Store = finalUrls.length > 0 ? { name: finalName, urls: finalUrls } : { name: finalName };
const updated = stores.map((s, i) => (i === index ? updatedStore : s));
await storage.setData(STORES_KEY, updated);
return updated;
}
/**
* Odebere obchod ze seznamu povolených (dle názvu).
*
+77 -1
View File
@@ -1,6 +1,6 @@
import { resetMemoryStorage } from '../storage/memory';
import getStorage from '../storage';
import { getStores, addStore, removeStore } from '../stores';
import { getStores, addStore, updateStore, removeStore } from '../stores';
const ADMIN_PW = 'testadmin';
@@ -113,6 +113,82 @@ describe('addStore', () => {
});
});
describe('updateStore', () => {
const WOLT_URL = 'https://wolt.com/bistro';
const BOLT_URL = 'https://food.bolt.eu/bistro';
beforeEach(async () => {
await addStore('Bistro', ADMIN_PW, [WOLT_URL]);
});
test('přidá další URL k existujícímu obchodu', async () => {
const stores = await updateStore('Bistro', ADMIN_PW, undefined, [WOLT_URL, BOLT_URL]);
expect(stores).toContainEqual({ name: 'Bistro', urls: [WOLT_URL, BOLT_URL] });
});
test('přejmenuje obchod a zachová URL', async () => {
const stores = await updateStore('Bistro', ADMIN_PW, ' Bistro U Nás ');
expect(stores).toContainEqual({ name: 'Bistro U Nás', urls: [WOLT_URL] });
});
test('prázdné pole URL odstraní všechny URL', async () => {
const stores = await updateStore('Bistro', ADMIN_PW, undefined, []);
expect(stores).toContainEqual({ name: 'Bistro' });
});
test('bez předaných URL zůstanou URL nezměněné', async () => {
const stores = await updateStore('Bistro', ADMIN_PW, 'Bistro 2');
expect(stores).toContainEqual({ name: 'Bistro 2', urls: [WOLT_URL] });
});
test('zachová pozici obchodu v seznamu', async () => {
await addStore('KFC', ADMIN_PW);
const stores = await updateStore('Bistro', ADMIN_PW, 'Bistro 2');
expect(names(stores)).toEqual(['Bistro 2', 'KFC']);
});
test('funguje case-insensitive dle názvu', async () => {
const stores = await updateStore('BISTRO', ADMIN_PW, undefined, [BOLT_URL]);
expect(stores).toContainEqual({ name: 'Bistro', urls: [BOLT_URL] });
});
test('ponechání stejného názvu není bráno jako duplikát', async () => {
const stores = await updateStore('Bistro', ADMIN_PW, 'bistro');
expect(names(stores)).toEqual(['bistro']);
});
test('odmítne název kolidující s jiným obchodem', async () => {
await addStore('KFC', ADMIN_PW);
await expect(updateStore('Bistro', ADMIN_PW, 'kfc')).rejects.toThrow('existuje');
});
test('odmítne prázdný název', async () => {
await expect(updateStore('Bistro', ADMIN_PW, ' ')).rejects.toThrow('prázdný');
});
test('odmítne nevalidní URL', async () => {
await expect(updateStore('Bistro', ADMIN_PW, undefined, ['javascript:alert(1)'])).rejects.toThrow('http');
});
test('vyhodí chybu u neexistujícího obchodu', async () => {
await expect(updateStore('Neexistuje', ADMIN_PW, 'Nový')).rejects.toThrow('nebyl nalezen');
});
test('vyhodí UNAUTHORIZED s nesprávným heslem', async () => {
await expect(updateStore('Bistro', 'spatne', 'Hacknuto')).rejects.toThrow('UNAUTHORIZED');
});
test('vyhodí UNAUTHORIZED pokud ADMIN_PASSWORD není nastaven', async () => {
delete process.env.ADMIN_PASSWORD;
await expect(updateStore('Bistro', '', 'Hacknuto')).rejects.toThrow('UNAUTHORIZED');
});
test('při nesprávném heslu se obchod nezmění', async () => {
await expect(updateStore('Bistro', 'spatne', 'Hacknuto')).rejects.toThrow('UNAUTHORIZED');
expect(names(await getStores())).toEqual(['Bistro']);
});
});
describe('removeStore', () => {
beforeEach(async () => {
await addStore('McDonald\'s', ADMIN_PW);