diff --git a/src/api/households.ts b/src/api/households.ts index 60d0815..c9e165b 100644 --- a/src/api/households.ts +++ b/src/api/households.ts @@ -1,16 +1,15 @@ -import { api, fetchApi, getHouseholdSlug } from '@/api/client' -import type { components, paths } from '@/api/types' +import { api, getHouseholdSlug } from '@/api/client' +import type { components } from '@/api/types' -export type Member = components['schemas']['User'] +export type Member = components['schemas']['HouseholdMember'] // Prefer typed endpoint if exists, fallback to raw fetch for now export async function listMembers(): Promise { - // Temporary raw GET using household slug in query until typed endpoint is available - const slug = getHouseholdSlug() || '' - const path = slug ? `/api/v1/households/${encodeURIComponent(slug)}/members` : '/api/v1/households/members' - const resp = await fetchApi(path, { method: 'GET' }) - if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText || 'HTTP error'}`) - const data = await resp.json().catch(() => null) - const arr = Array.isArray(data) ? data : [] - return arr.filter((m): m is Member => typeof m === 'object' && m !== null && typeof (m as { id: unknown }).id === 'number') + const householdSlug = getHouseholdSlug() + if (!householdSlug) throw new Error('Missing household slug') + const { data, error, response } = await api.GET('/api/v1/households/{householdSlug}/members', { + params: { path: { householdSlug } }, + }) + if (!response.ok) throw new Error(`${response.status} ${response.statusText || 'HTTP error'}${error ? `: ${String(error)}` : ''}`) + return Array.isArray(data) ? data : [] } diff --git a/src/api/sdk.ts b/src/api/sdk.ts index 53299b2..41c734a 100644 --- a/src/api/sdk.ts +++ b/src/api/sdk.ts @@ -382,5 +382,17 @@ export async function unrequestMeal(mealId: number | string): Promise { if (!response.ok) throw httpError(response, error) } +export async function requestIngredient(ingredientId: number): Promise { + const householdSlug = requireSlug() + const { data, error, response } = await api.POST('/api/v1/households/{householdSlug}/shopping/current/ingredients', { + params: { path: { householdSlug } }, + body: { ingredientId }, + }) + if (!response.ok) throw httpError(response, error) + const [decoded] = decodeListIngredientItems([data as any]) + if (!decoded) throw new Error('Failed to decode requested ingredient item') + return decoded +} + // Re-export domain command types for convenience at SDK surface export type { PurchaseRequest } from '@/domain/commands' diff --git a/src/components/recipes/EditRecipePage.vue b/src/components/recipes/EditRecipePage.vue index 6b1f6b1..c6ed85f 100644 --- a/src/components/recipes/EditRecipePage.vue +++ b/src/components/recipes/EditRecipePage.vue @@ -153,6 +153,10 @@ function createFromScratch() { ingredients: [], imageUrls: [], serves: 1, + createdById: -1, + createdBy: null, + hiddenById: null, + hiddenBy: null, } } diff --git a/src/composables/useShopping.ts b/src/composables/useShopping.ts index e61b8f0..b866b5b 100644 --- a/src/composables/useShopping.ts +++ b/src/composables/useShopping.ts @@ -60,6 +60,7 @@ export function useShopping() { purchaseShoppingList: sdk.purchaseShoppingList, requestMeal: sdk.requestMeal, unrequestMeal: sdk.unrequestMeal, + requestIngredient: sdk.requestIngredient, getMyShoppingList: sdk.getMyShoppingList, saveMyShoppingList: sdk.saveMyShoppingList, // View-model helpers diff --git a/src/domain/decoders.ts b/src/domain/decoders.ts index bd47314..a599cc3 100644 --- a/src/domain/decoders.ts +++ b/src/domain/decoders.ts @@ -28,9 +28,23 @@ export function decodeRecipe( ): Recipe { if (!r) throw new Error('Invalid recipe payload') // Normalize arrays that may be optional - const imageUrls = r.imageUrls ?? [] - const ingredients = r.ingredients ?? [] - return { ...r, imageUrls, ingredients } + const imageUrls = (r as components['schemas']['RecipeOut']).imageUrls ?? (r as components['schemas']['Recipe']).imageUrls ?? [] + const ingredients = (r as components['schemas']['RecipeOut']).ingredients ?? (r as components['schemas']['Recipe']).ingredients ?? [] + // Normalize createdBy/hiddenBy to MemberRef shape when input is legacy Recipe (with Person) + const createdByRaw = (r as any).createdBy + const hiddenByRaw = (r as any).hiddenBy + const toMemberRef = (v: any) => { + if (!v) return v + if (typeof v === 'object' && v) { + const id = (v as any).id + const displayName = 'displayName' in v ? (v as any).displayName : ('name' in v ? (v as any).name : undefined) + return typeof id === 'number' && typeof displayName === 'string' ? { id, displayName } : v + } + return v + } + const createdBy = createdByRaw !== undefined ? toMemberRef(createdByRaw) : undefined + const hiddenBy = hiddenByRaw !== undefined ? toMemberRef(hiddenByRaw) : undefined + return { ...(r as any), imageUrls, ingredients, ...(createdBy !== undefined ? { createdBy } : {}), ...(hiddenBy !== undefined ? { hiddenBy } : {}) } } export function decodeMeal( diff --git a/src/views/HouseholdSettings.vue b/src/views/HouseholdSettings.vue index 7bc9856..af721ff 100644 --- a/src/views/HouseholdSettings.vue +++ b/src/views/HouseholdSettings.vue @@ -40,14 +40,14 @@ v-for="m in members" :key="m.id" > - {{ m.displayName }} ({{ m.email }}) + {{ m.displayName }} — role: {{ m.role }}

- Listing members will be added once the backend endpoint is available. + No members to show yet.

diff --git a/tests/shopping.api.test.js b/tests/shopping.api.test.js index 9c9c2e8..a8d93ec 100644 --- a/tests/shopping.api.test.js +++ b/tests/shopping.api.test.js @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' import { server, http, HttpResponse } from './test-setup' -import { getCurrentShoppingList, getShoppingList, requestMeal, unrequestMeal, purchaseShoppingList } from '@/api/sdk' +import { getCurrentShoppingList, getShoppingList, requestMeal, unrequestMeal, purchaseShoppingList, requestIngredient } from '@/api/sdk' import { setHouseholdSlugProvider } from '@/api/client' describe('shopping api (typed client)', () => { @@ -33,4 +33,18 @@ describe('shopping api (typed client)', () => { const list = await purchaseShoppingList([{ type: 'existing', id: 1, personId: 42 }]) expect(list.id).toBe(1) }) + + it('requests an ingredient by id for the current list', async () => { + server.use( + http.post('*/api/v1/households/:householdSlug/shopping/current/ingredients', async ({ request }) => { + const body = await request.json() + expect(body).toEqual({ ingredientId: 123 }) + return HttpResponse.json({ id: -1, ingredientId: 123, personId: 7, createdDate: new Date().toISOString(), kind: 'ingredient', listId: null, mealId: null, recipeId: null }) + }) + ) + setHouseholdSlugProvider(() => 'the-smiths') + const item = await requestIngredient(123) + expect(item.kind).toBe('ingredient') + expect(item.ingredientId).toBe(123) + }) })