prepare("SELECT card_uid, title, body, position, updated_at_client, updated_at_server FROM shopping_state WHERE tenant_id = :t AND card_uid != '__inactive__' ORDER BY position, title"); $stmt->execute(['t' => $tenantId]); return $stmt->fetchAll() ?: []; } function shoppingLoadOne(PDO $db, int $tenantId, string $cardUid): ?array { $stmt = $db->prepare('SELECT card_uid, title, body, updated_at_client, updated_at_server FROM shopping_state WHERE tenant_id = :t AND card_uid = :u'); $stmt->execute(['t' => $tenantId, 'u' => $cardUid]); $row = $stmt->fetch(); return $row ?: null; } // ── POST /api/shopping ──────────────────────────────────────── if ($route === '/api/shopping' && $method === 'POST') { if (!$clientTenant) jsonResponse(['error' => 'unauthorized'], 401); $input = json_decode(file_get_contents('php://input'), true) ?: []; $uid = trim((string)($input['card_uid'] ?? '')); $title = (string)($input['title'] ?? ''); $body = (string)($input['body'] ?? ''); $pos = (int)($input['position'] ?? 0); $clientTs = trim((string)($input['updated_at'] ?? '')) ?: gmdate('Y-m-d H:i:s'); if ($uid === '') jsonResponse(['error' => 'card_uid required'], 400); $now = gmdate('Y-m-d H:i:s'); $db->prepare(' INSERT INTO shopping_state (tenant_id, card_uid, title, body, position, updated_at_client, updated_at_server) VALUES (:t, :u, :ti, :b, :p, :c, :s) ON CONFLICT(tenant_id, card_uid) DO UPDATE SET title = excluded.title, body = excluded.body, position = excluded.position, updated_at_client = excluded.updated_at_client, updated_at_server = excluded.updated_at_server ')->execute([ 't' => $clientTenant['id'], 'u' => $uid, 'ti' => $title, 'b' => $body, 'p' => $pos, 'c' => $clientTs, 's' => $now, ]); jsonResponse(['ok' => true, 'stored' => true, 'updated_at_server' => $now]); } // ── DELETE /api/shopping ───────────────────────────────────── if ($route === '/api/shopping' && $method === 'DELETE') { if (!$clientTenant) jsonResponse(['error' => 'unauthorized'], 401); $input = json_decode(file_get_contents('php://input'), true) ?: []; $uid = trim((string)($input['card_uid'] ?? '')); if ($uid === '') jsonResponse(['error' => 'card_uid required'], 400); $db->prepare('DELETE FROM shopping_state WHERE tenant_id = :t AND card_uid = :u') ->execute(['t' => $clientTenant['id'], 'u' => $uid]); jsonResponse(['ok' => true]); } // ── GET /api/shopping ──────────────────────────────────────── if ($route === '/api/shopping' && $method === 'GET') { if (!$tenant) jsonResponse(['error' => 'unauthorized'], 401); $lists = shoppingLoadAll($db, (int)$tenant['id']); jsonResponse(['ok' => true, 'lists' => $lists]); } // ── POST /api/shopping/check ───────────────────────────────── if ($route === '/api/shopping/check' && $method === 'POST') { if (!$sessionTenant) jsonResponse(['error' => 'unauthorized'], 401); $input = json_decode(file_get_contents('php://input'), true) ?: []; $uid = trim((string)($input['card_uid'] ?? '')); $line = (string)($input['line'] ?? ''); $checked = !empty($input['checked']); if ($uid === '' || $line === '') jsonResponse(['error' => 'card_uid and line required'], 400); $state = shoppingLoadOne($db, (int)$sessionTenant['id'], $uid); if (!$state) jsonResponse(['error' => 'list not found'], 404); $body = $state['body']; $lines = preg_split("/\r\n|\n|\r/", $body); $hit = false; foreach ($lines as $i => $l) { if ($l !== $line) continue; $toggled = $checked ? preg_replace('/^(\s*-\s*)\[\s\]/', '$1[x]', $l, 1) : preg_replace('/^(\s*-\s*)\[[xX]\]/', '$1[ ]', $l, 1); if ($toggled === $l) continue; $lines[$i] = $toggled; $hit = true; break; } if (!$hit) jsonResponse(['error' => 'line not found (list may have been edited — reload)'], 409); $newBody = implode("\n", $lines); $now = gmdate('Y-m-d H:i:s'); $db->prepare('UPDATE shopping_state SET body = :b, updated_at_server = :s WHERE tenant_id = :t AND card_uid = :u') ->execute(['b' => $newBody, 's' => $now, 't' => $sessionTenant['id'], 'u' => $uid]); jsonResponse(['ok' => true, 'updated_at_server' => $now]); }