/** * sv-dining-api · Cloudflare Worker * * Endpoints: * GET /api/orders → vsechny objednavky (recepce) * POST /api/orders → nova objednavka (host) * PUT /api/orders/:id → zmena statusu (recepce) * GET /api/orders/villa/:v → objednavky jedne vily (host - my orders) * GET /api/menu → aktualni menu * PUT /api/menu → ulozit menu (admin) * POST /api/auth → overit PIN (admin) */ const CORS_HEADERS = (origin) => ({ 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, X-PIN', 'Access-Control-Max-Age': '86400', }); export default { async fetch(request, env) { const origin = env.ALLOWED_ORIGIN || '*'; // CORS preflight if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: CORS_HEADERS(origin) }); } const url = new URL(request.url); const path = url.pathname; const method = request.method; try { // ── AUTH ────────────────────────────────────────────────────── if (path === '/api/auth' && method === 'POST') { return handleAuth(request, env, origin); } // ── MENU ────────────────────────────────────────────────────── if (path === '/api/menu') { if (method === 'GET') return handleMenuGet(env, origin); if (method === 'PUT') return handleMenuPut(request, env, origin); } // ── ORDERS ──────────────────────────────────────────────────── if (path === '/api/orders') { if (method === 'GET') return handleOrdersGet(url, env, origin); if (method === 'POST') return handleOrdersPost(request, env, origin); } // PUT /api/orders/:id const putMatch = path.match(/^\/api\/orders\/([^/]+)$/); if (putMatch && method === 'PUT') { return handleOrderPut(request, env, origin, putMatch[1]); } // GET /api/orders/villa/:villa const villaMatch = path.match(/^\/api\/orders\/villa\/([^/]+)$/); if (villaMatch && method === 'GET') { return handleOrdersVilla(env, origin, villaMatch[1]); } return json({ error: 'Not found' }, 404, origin); } catch (e) { console.error(e); return json({ error: 'Internal error', detail: e.message }, 500, origin); } } }; // ── AUTH ────────────────────────────────────────────────────────────── async function handleAuth(request, env, origin) { const body = await request.json().catch(() => ({})); if (!body.pin) return json({ ok: false }, 400, origin); const ok = body.pin === env.PIN; return json({ ok }, 200, origin); } // ── MENU ────────────────────────────────────────────────────────────── async function handleMenuGet(env, origin) { const row = await env.DB.prepare('SELECT data FROM menu WHERE id = 1').first(); if (!row) return json({ menu: null }, 200, origin); // frontend pouzije DEFAULT_MENU return json({ menu: JSON.parse(row.data) }, 200, origin); } async function handleMenuPut(request, env, origin) { // Vyzaduje platny PIN v hlavicce if (!checkPin(request, env)) return json({ error: 'Unauthorized' }, 401, origin); const body = await request.json().catch(() => null); if (!body?.menu) return json({ error: 'Invalid body' }, 400, origin); await env.DB.prepare( 'INSERT INTO menu (id, data, updated_at) VALUES (1, ?, datetime(\'now\')) ON CONFLICT(id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at' ).bind(JSON.stringify(body.menu)).run(); return json({ ok: true }, 200, origin); } // ── ORDERS GET (recepce) ────────────────────────────────────────────── async function handleOrdersGet(url, env, origin) { const status = url.searchParams.get('status'); // ?status=new|prog|done const limit = parseInt(url.searchParams.get('limit') || '100'); let query = 'SELECT * FROM orders'; const params = []; if (status) { query += ' WHERE status = ?'; params.push(status); } query += ' ORDER BY created_at DESC LIMIT ?'; params.push(limit); const { results } = await env.DB.prepare(query).bind(...params).all(); const orders = results.map(parseOrder); return json({ orders }, 200, origin); } // ── ORDERS GET (host - vlastni vila) ───────────────────────────────── async function handleOrdersVilla(env, origin, villa) { const { results } = await env.DB.prepare( "SELECT * FROM orders WHERE villa = ? AND status != 'done' ORDER BY created_at DESC" ).bind(villa).all(); return json({ orders: results.map(parseOrder) }, 200, origin); } // ── ORDERS POST (nova objednavka) ───────────────────────────────────── async function handleOrdersPost(request, env, origin) { const body = await request.json().catch(() => null); if (!body?.villa || !body?.name || !body?.items) { return json({ error: 'Missing fields' }, 400, origin); } const id = Date.now().toString(); await env.DB.prepare( 'INSERT INTO orders (id, villa, name, items, total, status) VALUES (?, ?, ?, ?, ?, ?)' ).bind( id, body.villa, body.name, JSON.stringify(body.items), body.total, 'new' ).run(); return json({ ok: true, id }, 201, origin); } // ── ORDER PUT (zmena statusu) ───────────────────────────────────────── async function handleOrderPut(request, env, origin, id) { const body = await request.json().catch(() => null); if (!body?.status || !['new','prog','done'].includes(body.status)) { return json({ error: 'Invalid status' }, 400, origin); } await env.DB.prepare('UPDATE orders SET status = ? WHERE id = ?').bind(body.status, id).run(); return json({ ok: true }, 200, origin); } // ── HELPERS ─────────────────────────────────────────────────────────── function parseOrder(row) { return { ...row, items: typeof row.items === 'string' ? JSON.parse(row.items) : row.items, }; } function checkPin(request, env) { return request.headers.get('X-PIN') === env.PIN; } function json(data, status = 200, origin = '*') { return new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json', ...CORS_HEADERS(origin), }, }); }