feat!: remove Person entity usage, adopt MemberRef across meals; update SDK and UI
This commit is contained in:
parent
166dc04948
commit
a9f2b6f5a8
7 changed files with 39 additions and 103 deletions
|
|
@ -224,29 +224,7 @@ export async function parseProduct(
|
||||||
return (await res.json()) ?? null
|
return (await res.json()) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persons
|
// Person-related functions are removed as the entity is no longer in use.
|
||||||
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 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meals
|
// Meals
|
||||||
export async function getUpcomingMeals(from: Date, to: Date): Promise<Meal[]> {
|
export async function getUpcomingMeals(from: Date, to: Date): Promise<Meal[]> {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const email = ref('')
|
const email = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const isMultitenant = computed(() => typeof process !== 'undefined' && process.env?.VUE_APP_MULTITENANT_ENABLED === 'true')
|
const isMultitenant = computed(() => true)
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
|
||||||
onMounted(async () => {})
|
onMounted(async () => {})
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,21 @@
|
||||||
/>
|
/>
|
||||||
<div class="persons-list">
|
<div class="persons-list">
|
||||||
Cooked by
|
Cooked by
|
||||||
<person-list
|
<member-list
|
||||||
:people="meal.chefs"
|
:people="meal.chefs"
|
||||||
@remove-person="(p) => removePerson('chefs', p)"
|
@remove="(p) => removePerson('chefs', p)"
|
||||||
@add-person="(p) => addPerson('chefs', p)"
|
@add="(p) => addPerson('chefs', p)"
|
||||||
/>
|
/>
|
||||||
for
|
for
|
||||||
<person-list
|
<member-list
|
||||||
:people="meal.consumers"
|
:people="meal.consumers"
|
||||||
@remove-person="(p) => removePerson('consumers', p)"
|
@remove="(p) => removePerson('consumers', p)"
|
||||||
@add-person="(p) => addPerson('consumers', p)"
|
@add="(p) => addPerson('consumers', p)"
|
||||||
/>, with
|
/>, with
|
||||||
<person-list
|
<member-list
|
||||||
:people="meal.cleanup"
|
:people="meal.cleanup"
|
||||||
@remove-person="(p) => removePerson('cleanup', p)"
|
@remove="(p) => removePerson('cleanup', p)"
|
||||||
@add-person="(p) => addPerson('cleanup', p)"
|
@add="(p) => addPerson('cleanup', p)"
|
||||||
/>
|
/>
|
||||||
on cleanup.
|
on cleanup.
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -131,11 +131,13 @@ import RecipeSearchBox from '@/components/recipes/RecipeSearchBox.vue'
|
||||||
import RecipeCard from '@/components/recipes/RecipeCard.vue'
|
import RecipeCard from '@/components/recipes/RecipeCard.vue'
|
||||||
import DatePicker from './DatePicker.vue'
|
import DatePicker from './DatePicker.vue'
|
||||||
import EditableIngredientsPanel from '../ingredients/EditableIngredientsPanel.vue'
|
import EditableIngredientsPanel from '../ingredients/EditableIngredientsPanel.vue'
|
||||||
import PersonList from './PersonList.vue'
|
import MemberList from './MemberList.vue'
|
||||||
import CompactParsedIngredient from '../ingredients/CompactParsedIngredient.vue'
|
import CompactParsedIngredient from '../ingredients/CompactParsedIngredient.vue'
|
||||||
const showIngredientsIcon = new URL('@/assets/show-ingredients.svg', import.meta.url).toString()
|
const showIngredientsIcon = new URL('@/assets/show-ingredients.svg', import.meta.url).toString()
|
||||||
const trash = new URL('@/assets/trash.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) {
|
function addPersonIfNotExists<T extends { id: number }>(list: T[], person: T | null | undefined) {
|
||||||
if (!person) return
|
if (!person) return
|
||||||
if (!list.find((p) => p.id === person.id)) {
|
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))
|
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)
|
meal[list] = meal[list].filter((p) => p.id !== person.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPerson(list: PeopleKey, person: { id: number; name?: string; displayName?: string }) {
|
function addPerson(list: PeopleKey, person: MemberRef) {
|
||||||
// Normalize person into MemberRef shape
|
addPersonIfNotExists(meal[list], person)
|
||||||
const normalized = { id: person.id, displayName: (person as any).displayName ?? (person as any).name ?? '' }
|
|
||||||
addPersonIfNotExists(meal[list], normalized)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectRecipe(recipe: { id: number | string }) {
|
async function selectRecipe(recipe: { id: number | string }) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
class="person-circle remove-person"
|
class="person-circle remove-person"
|
||||||
@click="removePerson(person)"
|
@click="removePerson(person)"
|
||||||
>
|
>
|
||||||
{{ person.displayName || person.name || '' }}
|
{{ person.displayName || '' }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
class="person-circle add-person"
|
class="person-circle add-person"
|
||||||
@mousedown="addPerson(person)"
|
@mousedown="addPerson(person)"
|
||||||
>
|
>
|
||||||
{{ person.displayName || person.name || '' }}
|
{{ person.displayName || '' }}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -50,36 +50,35 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { searchPersons } from '@/api/sdk'
|
import { listMembers } from '@/api/households'
|
||||||
// Accept either legacy Person shape or new MemberRef
|
import type { MemberRef } from '@/domain/types'
|
||||||
type PersonLike = { id: number; name?: string; displayName?: string }
|
|
||||||
const props = withDefaults(defineProps<{ people?: PersonLike[] }>(), { people: () => [] })
|
const props = withDefaults(defineProps<{ people?: MemberRef[] }>(), { people: () => [] })
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'add-person', person: PersonLike): void
|
(e: 'add', person: MemberRef): void
|
||||||
(e: 'remove-person', person: PersonLike): void
|
(e: 'remove', person: MemberRef): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isAddingPerson = ref(false)
|
const isAddingPerson = ref(false)
|
||||||
const searchName = ref('')
|
const searchName = ref('')
|
||||||
const searchResults = ref<PersonLike[]>([])
|
const searchResults = ref<MemberRef[]>([])
|
||||||
|
|
||||||
// Template refs for DOM elements
|
// Template refs for DOM elements
|
||||||
const searchNameInput = ref<HTMLInputElement | null>(null)
|
const searchNameInput = ref<HTMLInputElement | null>(null)
|
||||||
const persondroplist = ref<HTMLUListElement | null>(null)
|
const persondroplist = ref<HTMLUListElement | null>(null)
|
||||||
|
|
||||||
async function updateSearchResults() {
|
async function updateSearchResults() {
|
||||||
const q = searchName.value.trim()
|
const q = searchName.value.trim().toLowerCase()
|
||||||
if (!q) {
|
const allMembers = await listMembers()
|
||||||
searchResults.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const page = await searchPersons(q)
|
|
||||||
const results: PersonLike[] = page.items.map((p) => ({ id: p.id, name: p.name }))
|
|
||||||
const idSet = new Set(props.people.map((p) => p.id))
|
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) {
|
if (!person && searchResults.value.length > 0) {
|
||||||
person = searchResults.value[0]
|
person = searchResults.value[0]
|
||||||
}
|
}
|
||||||
|
|
@ -90,15 +89,15 @@ function addPerson(person?: PersonLike) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!props.people.find((p) => p.id === person.id)) {
|
if (!props.people.find((p) => p.id === person.id)) {
|
||||||
emit('add-person', person)
|
emit('add', person)
|
||||||
}
|
}
|
||||||
searchName.value = ''
|
searchName.value = ''
|
||||||
searchResults.value = []
|
searchResults.value = []
|
||||||
isAddingPerson.value = false
|
isAddingPerson.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePerson(person: PersonLike) {
|
function removePerson(person: MemberRef) {
|
||||||
emit('remove-person', person)
|
emit('remove', person)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watchers
|
// Watchers
|
||||||
|
|
@ -20,7 +20,8 @@ export type Ingredient = components['schemas']['Ingredient']
|
||||||
export type Product = components['schemas']['Product']
|
export type Product = components['schemas']['Product']
|
||||||
export type User = components['schemas']['User']
|
export type User = components['schemas']['User']
|
||||||
export type Household = components['schemas']['HouseholdResponse']
|
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
|
// v2 no longer exposes Recipe-Input; use RecipeCreate at boundary when creating
|
||||||
export type MealInput = components['schemas']['MealIn']
|
export type MealInput = components['schemas']['MealIn']
|
||||||
export type MealRecipeOut = components['schemas']['MealRecipe']
|
export type MealRecipeOut = components['schemas']['MealRecipe']
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -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')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Loading…
Reference in a new issue