diff --git a/frontend-spec.md b/frontend-spec.md index 8507f00..5a37082 100644 --- a/frontend-spec.md +++ b/frontend-spec.md @@ -128,7 +128,7 @@ What exists now - `src/api/client.ts`: Authorization header provider only; household header injection removed. - Domain & UI - Member arrays (`chefs`, `consumers`, `cleanup`) normalized to MemberRef `{ id, displayName }` with decoders handling legacy shapes gracefully. - - Invitation Accept flow implemented; Household Settings supports sending invitations and listing members (members via temporary raw fetch path-scoped endpoint until typed spec lands). + - Invitation Accept flow implemented; Household Settings supports sending invitations and listing members using typed endpoints. Status of tests and typing - All tests pass: 27 files, 48 tests. @@ -201,13 +201,21 @@ API client SDK - `src/api/sdk.ts`: - - Recipes, meals, shopping fully path-scoped to households. Persons/parse remain via raw fetch until typed coverage. + - Recipes, meals, shopping fully path-scoped to households. + - Newly typed endpoints (post-latest codegen): + - `GET /api/v1/households/{householdSlug}/shopping/current` — current aggregated list. + - `GET /api/v1/households/{householdSlug}/shopping/{list_id}` — purchased list by id. + - `POST /api/v1/households/{householdSlug}/shopping` — purchase items (storeName + items). + - `POST /api/v1/households/{householdSlug}/shopping/current/meals/me` — request a meal. + - `DELETE /api/v1/households/{householdSlug}/shopping/current/meals/{meal_id}` — unrequest a meal. + - `POST /api/v1/households/{householdSlug}/shopping/current/ingredients` — request an ingredient (by id) for shopping. + - Persons and parse endpoints are still raw until they are added to the OpenAPI. UI - Login Page: Refactored to show email/password form when multitenant flag is enabled; legacy person list retained otherwise. Link to Create Account added. - Add `HouseholdSwitcher.vue` to app chrome and wire with router. - Update components that navigate using string paths to use named routes with slug. - - `src/views/HouseholdSettings.vue`: Invite members form wired to `sendInvitation(email)`. Members list rendered from `listMembers()`; guarded for backends that don’t yet support the endpoint. +- `src/views/HouseholdSettings.vue`: Invite members form wired to `sendInvitation(email)`. Members list rendered from `listMembers()` using the typed endpoint. - Adjust pages that call SDK/API to pass `{ householdSlug }` path params once client services are migrated. Tests @@ -223,9 +231,10 @@ Tests - Household scoping: use typed path params (`{ params: { path: { householdSlug } } }`); do not mutate path strings. - Auth refresh: returns `{ accessToken, tokenType }`. After refresh, call user/household endpoints to populate app state. Use `whoami` to validate the active route’s slug when needed. - Cleanup/migration tasks: - 1) Remove X-Household-Slug header injection in `api/client.ts` and refactor services to accept `householdSlug` via typed params. - 2) Replace temporary raw fetch for invitations (both accept and create) and members listing with generated typed endpoints. Invitations are now typed; members listing remains pending. - 3) Legacy identity: continue using `User` as the primary identity. Keep `Person` in meal-related UIs where the backend requires it, but remove Person as the login/identity concept. + 1) Remove X-Household-Slug header injection in `api/client.ts` and refactor services to accept `householdSlug` via typed params. (Completed) + 2) Replace temporary raw fetch calls with generated typed endpoints where available: invitations and members listing are now typed; migrate usages. Persons and parse remain raw for now. + 3) Integrate the new `POST /shopping/current/ingredients` endpoint into the SDK (`requestIngredient(ingredientId: number)`) and expose via `useShopping`; refactor `MyShoppingPage.vue` accordingly and remove legacy stubs. + 4) Legacy identity: continue using `User` as the primary identity. Keep `Person` in meal-related UIs where required by backend, but remove Person as the login/identity concept. --- @@ -252,6 +261,21 @@ Google OAuth is planned next. - Add a user profile endpoint and load it post-refresh to populate `currentUser()` with real data instead of a placeholder. - Replace temporary raw fetch calls (persons, parse, members listing) with typed endpoints when available. - Implement Google OAuth login and account creation flows. -- Review `MyShoppingPage` usage and remove legacy `getMyShoppingList/saveMyShoppingList` stubs when UI is refactored or removed. +- MyShopping ad-hoc item requests (planned): + - The v2 OpenAPI spec is being updated to include endpoints for creating, listing, updating, and deleting ad-hoc requested items on the current shopping list (independent of meals), scoped under `/api/v1/households/{householdSlug}/shopping/current/items`. + - Once available, add typed SDK methods: + - `createRequestedItem(input: { name: string; line?: string; quantity?: number; unit?: Unit })` + - `updateRequestedItem(id: number, patch: { name?: string; line?: string; quantity?: number; unit?: Unit })` + - `deleteRequestedItem(id: number)` + - `listRequestedItems()` (if provided separately; otherwise rely on `getCurrentShoppingList()`) + - Refactor `src/components/shopping/MyShoppingPage.vue` to use the above methods and remove legacy stubs `getMyShoppingList/saveMyShoppingList` from the SDK and `useShopping` composable. + - Update tests to drive TDD: + - MSW handlers for the new endpoints with path-scoped URLs and Authorization. + - Component tests to add, edit, and delete an ad-hoc requested item and verify it appears under outstanding items in `getCurrentShoppingList()`. + - Acceptance criteria: + - Users can add an item without associating it to a meal. + - Users can edit and delete such items. + - All calls use typed path-scoped endpoints; no header injection; Authorization still via provider. + - Note: No temporary measures required; proceed directly once backend ships endpoints. - Remove the legacy username login shim (`login(username: string)`) and any fallback UI; standardize on email/password (and Google) only. - Rollout flag: default `VUE_APP_MULTITENANT_ENABLED` to true across environments and plan removal of legacy flat routes and related tests once stable. diff --git a/src/api/types.ts b/src/api/types.ts index c7abc67..6d67828 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -365,6 +365,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/households/{householdSlug}/shopping/current/ingredients": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Request an ingredient for shopping (scoped) */ + post: operations["requestIngredientV2"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/healthz": { parameters: { query?: never; @@ -481,6 +498,11 @@ export interface components { mealId?: number | null; product?: components["schemas"]["Product"] | null; }; + /** IngredientIdWrapper */ + IngredientIdWrapper: { + /** Ingredientid */ + ingredientId: number; + }; /** IngredientPurchaseItemIn */ IngredientPurchaseItemIn: { /** Ingredientid */ @@ -822,6 +844,12 @@ export interface components { imageUrls: string[]; /** Ingredients */ ingredients: components["schemas"]["Ingredient"][]; + /** Createdbyid */ + createdById: number; + createdBy?: components["schemas"]["MemberRef"] | null; + /** Hiddenbyid */ + hiddenById?: number | null; + hiddenBy?: components["schemas"]["MemberRef"] | null; }; /** RefreshResponse */ RefreshResponse: { @@ -882,7 +910,7 @@ export interface components { storeName: "woolworths" | "coles" | "home"; /** Purchasedbyid */ purchasedById: number; - purchasedBy?: components["schemas"]["Person"] | null; + purchasedBy?: components["schemas"]["MemberRef"] | null; /** Items */ items: components["schemas"]["ListIngredientItem"][]; }; @@ -1812,6 +1840,42 @@ export interface operations { }; }; }; + requestIngredientV2: { + parameters: { + query?: never; + header?: never; + path: { + householdSlug: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["IngredientIdWrapper"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListIngredientItem"]; + }; + }; + 403: components["responses"]["Problem403"]; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; healthz_healthz_get: { parameters: { query?: never;