feat!: remove Person entity usage, adopt MemberRef across meals; update SDK and UI

This commit is contained in:
jableader 2025-11-01 20:16:54 +11:00
parent 166dc04948
commit a9f2b6f5a8
7 changed files with 39 additions and 103 deletions

View file

@ -224,29 +224,7 @@ export async function parseProduct(
return (await res.json()) ?? null
}
// Persons
async function listPersons(params?: { q?: string | null; cursor?: string | null; limit?: number }): Promise<Page<components['schemas']['Person']>> {
const query: Record<string, unknown> = {}
if (params) {
if (params.q !== undefined) query.q = params.q
if (params.cursor !== undefined) query.cursor = params.cursor
if (typeof params.limit === 'number') query.limit = params.limit
}
const url = '/api/v1/persons' + (Object.keys(query).length ? ('?' + new URLSearchParams(query as Record<string, string>)) : '')
const res = await fetchApi(url, { method: 'GET' })
if (!res.ok) throw httpError(res, null)
const data = await res.json()
const normalized = Array.isArray(data) ? { items: data } : (data ?? null)
return fromOpenApiPage<components['schemas']['Person'], components['schemas']['Person']>(normalized, (p) => p)
}
export async function getPersonsInHome(): Promise<Page<components['schemas']['Person']>> {
return listPersons()
}
export async function searchPersons(name: string): Promise<Page<components['schemas']['Person']>> {
return listPersons({ q: name })
}
// Person-related functions are removed as the entity is no longer in use.
// Meals
export async function getUpcomingMeals(from: Date, to: Date): Promise<Meal[]> {

View file

@ -58,7 +58,7 @@ const router = useRouter()
const route = useRoute()
const email = ref('')
const password = ref('')
const isMultitenant = computed(() => typeof process !== 'undefined' && process.env?.VUE_APP_MULTITENANT_ENABLED === 'true')
const isMultitenant = computed(() => true)
const { user } = useAuth()
onMounted(async () => {})

View file

@ -7,21 +7,21 @@
/>
<div class="persons-list">
Cooked by
<person-list
<member-list
:people="meal.chefs"
@remove-person="(p) => removePerson('chefs', p)"
@add-person="(p) => addPerson('chefs', p)"
@remove="(p) => removePerson('chefs', p)"
@add="(p) => addPerson('chefs', p)"
/>
for
<person-list
<member-list
:people="meal.consumers"
@remove-person="(p) => removePerson('consumers', p)"
@add-person="(p) => addPerson('consumers', p)"
@remove="(p) => removePerson('consumers', p)"
@add="(p) => addPerson('consumers', p)"
/>, with
<person-list
<member-list
:people="meal.cleanup"
@remove-person="(p) => removePerson('cleanup', p)"
@add-person="(p) => addPerson('cleanup', p)"
@remove="(p) => removePerson('cleanup', p)"
@add="(p) => addPerson('cleanup', p)"
/>
on cleanup.
</div>
@ -131,11 +131,13 @@ import RecipeSearchBox from '@/components/recipes/RecipeSearchBox.vue'
import RecipeCard from '@/components/recipes/RecipeCard.vue'
import DatePicker from './DatePicker.vue'
import EditableIngredientsPanel from '../ingredients/EditableIngredientsPanel.vue'
import PersonList from './PersonList.vue'
import MemberList from './MemberList.vue'
import CompactParsedIngredient from '../ingredients/CompactParsedIngredient.vue'
const showIngredientsIcon = new URL('@/assets/show-ingredients.svg', import.meta.url).toString()
const trash = new URL('@/assets/trash.svg', import.meta.url).toString()
import type { MemberRef } from '@/domain/types'
function addPersonIfNotExists<T extends { id: number }>(list: T[], person: T | null | undefined) {
if (!person) return
if (!list.find((p) => p.id === person.id)) {
@ -203,14 +205,12 @@ function updateIngredient(ingredient: Ingredient, newIngredient: Ingredient) {
meal.extraIngredients = meal.extraIngredients.map((i) => (i === ingredient ? newIngredient : i))
}
function removePerson(list: PeopleKey, person: { id: number }) {
function removePerson(list: PeopleKey, person: MemberRef) {
meal[list] = meal[list].filter((p) => p.id !== person.id)
}
function addPerson(list: PeopleKey, person: { id: number; name?: string; displayName?: string }) {
// Normalize person into MemberRef shape
const normalized = { id: person.id, displayName: (person as any).displayName ?? (person as any).name ?? '' }
addPersonIfNotExists(meal[list], normalized)
function addPerson(list: PeopleKey, person: MemberRef) {
addPersonIfNotExists(meal[list], person)
}
async function selectRecipe(recipe: { id: number | string }) {

View file

@ -8,7 +8,7 @@
class="person-circle remove-person"
@click="removePerson(person)"
>
{{ person.displayName || person.name || '' }}
{{ person.displayName || '' }}
</button>
</span>
<span>
@ -40,7 +40,7 @@
class="person-circle add-person"
@mousedown="addPerson(person)"
>
{{ person.displayName || person.name || '' }}
{{ person.displayName || '' }}
</button>
</li>
</ul>
@ -50,36 +50,35 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { searchPersons } from '@/api/sdk'
// Accept either legacy Person shape or new MemberRef
type PersonLike = { id: number; name?: string; displayName?: string }
const props = withDefaults(defineProps<{ people?: PersonLike[] }>(), { people: () => [] })
import { listMembers } from '@/api/households'
import type { MemberRef } from '@/domain/types'
const props = withDefaults(defineProps<{ people?: MemberRef[] }>(), { people: () => [] })
const emit = defineEmits<{
(e: 'add-person', person: PersonLike): void
(e: 'remove-person', person: PersonLike): void
(e: 'add', person: MemberRef): void
(e: 'remove', person: MemberRef): void
}>()
const isAddingPerson = ref(false)
const searchName = ref('')
const searchResults = ref<PersonLike[]>([])
const searchResults = ref<MemberRef[]>([])
// Template refs for DOM elements
const searchNameInput = ref<HTMLInputElement | null>(null)
const persondroplist = ref<HTMLUListElement | null>(null)
async function updateSearchResults() {
const q = searchName.value.trim()
if (!q) {
searchResults.value = []
return
}
const page = await searchPersons(q)
const results: PersonLike[] = page.items.map((p) => ({ id: p.id, name: p.name }))
const q = searchName.value.trim().toLowerCase()
const allMembers = await listMembers()
const idSet = new Set(props.people.map((p) => p.id))
searchResults.value = results.filter((p: PersonLike) => !idSet.has(p.id))
let results: MemberRef[] = allMembers.filter((p) => !idSet.has(p.id))
if (q) {
results = results.filter((p) => p.displayName.toLowerCase().includes(q))
}
searchResults.value = results
}
function addPerson(person?: PersonLike) {
function addPerson(person?: MemberRef) {
if (!person && searchResults.value.length > 0) {
person = searchResults.value[0]
}
@ -90,15 +89,15 @@ function addPerson(person?: PersonLike) {
return
}
if (!props.people.find((p) => p.id === person.id)) {
emit('add-person', person)
emit('add', person)
}
searchName.value = ''
searchResults.value = []
isAddingPerson.value = false
}
function removePerson(person: PersonLike) {
emit('remove-person', person)
function removePerson(person: MemberRef) {
emit('remove', person)
}
// Watchers

View file

@ -20,7 +20,8 @@ export type Ingredient = components['schemas']['Ingredient']
export type Product = components['schemas']['Product']
export type User = components['schemas']['User']
export type Household = components['schemas']['HouseholdResponse']
export type Person = components['schemas']['Person']
// Member reference used across meals and lookups
export type MemberRef = components['schemas']['MemberRef']
// v2 no longer exposes Recipe-Input; use RecipeCreate at boundary when creating
export type MealInput = components['schemas']['MealIn']
export type MealRecipeOut = components['schemas']['MealRecipe']

View file

@ -1,12 +0,0 @@
import { describe, it, expect } from 'vitest'
import { server, http, HttpResponse } from './test-setup'
import { getPersonsInHome } from '@/api/sdk'
describe('persons api errors', () => {
it('propagates non-2xx errors', async () => {
server.use(
http.get('*/api/v1/persons', () => new HttpResponse(null, { status: 422 }))
)
await expect(getPersonsInHome()).rejects.toBeTruthy()
})
})

View file

@ -1,30 +0,0 @@
import { describe, it, expect } from 'vitest'
import { server, http, HttpResponse } from './test-setup'
import { getPersonsInHome, searchPersons } from '@/api/sdk'
// Persons API tests
describe('persons api (typed client)', () => {
it('lists persons in home', async () => {
server.use(
http.get('*/api/v1/persons', () => {
return HttpResponse.json([{ id: 1, name: 'Ada Lovelace' }])
})
)
const page = await getPersonsInHome()
expect(Array.isArray(page.items)).toBe(true)
expect(page.items[0].name).toBe('Ada Lovelace')
})
it('searches persons by name', async () => {
server.use(
http.get('*/api/v1/persons', ({ request }) => {
const url = new URL(request.url)
const q = url.searchParams.get('q')
return HttpResponse.json(q ? [{ id: 2, name: 'Alan Turing' }] : [])
})
)
const page = await searchPersons('alan')
expect(page.items[0].name.toLowerCase()).toContain('alan')
})
})