diff --git a/backend/src/db/migrate.ts b/backend/src/db/migrate.ts index 782a2c1..e184adb 100644 --- a/backend/src/db/migrate.ts +++ b/backend/src/db/migrate.ts @@ -32,6 +32,7 @@ async function main() { name text NOT NULL, generic_name text, taken_by text, + taken_by_json text NOT NULL DEFAULT '[]', count integer NOT NULL DEFAULT 0, strips integer NOT NULL DEFAULT 0, pack_count integer NOT NULL DEFAULT 1, diff --git a/backend/src/db/migrations/0016_taken_by_json_array.sql b/backend/src/db/migrations/0016_taken_by_json_array.sql new file mode 100644 index 0000000..05a7c1e --- /dev/null +++ b/backend/src/db/migrations/0016_taken_by_json_array.sql @@ -0,0 +1,17 @@ +-- Convert taken_by from single string to JSON array +-- This allows multiple people to share the same medication + +-- Add new column for JSON array +ALTER TABLE medications ADD COLUMN taken_by_json TEXT NOT NULL DEFAULT '[]'; + +-- Migrate existing data: convert single string to JSON array +-- If taken_by is not null/empty, convert to ["value"], otherwise keep as [] +UPDATE medications +SET taken_by_json = CASE + WHEN taken_by IS NOT NULL AND taken_by != '' + THEN json_array(taken_by) + ELSE '[]' +END; + +-- Note: We keep the old taken_by column for backwards compatibility during migration +-- It can be dropped in a future migration once all code uses taken_by_json diff --git a/backend/src/db/migrations/meta/_journal.json b/backend/src/db/migrations/meta/_journal.json index d796e44..d926526 100644 --- a/backend/src/db/migrations/meta/_journal.json +++ b/backend/src/db/migrations/meta/_journal.json @@ -15,6 +15,7 @@ { "idx": 12, "version": 1, "when": 1735800000, "tag": "0012_add_user_avatar", "breakpoint": false }, { "idx": 13, "version": 1, "when": 1735900000, "tag": "0013_add_oidc_subject", "breakpoint": false }, { "idx": 14, "version": 1, "when": 1735400000, "tag": "0014_add_stock_calculation_mode", "breakpoint": false }, - { "idx": 15, "version": 1, "when": 1735400001, "tag": "0015_add_share_token_expiry", "breakpoint": false } + { "idx": 15, "version": 1, "when": 1735400001, "tag": "0015_add_share_token_expiry", "breakpoint": false }, + { "idx": 16, "version": 1, "when": 1735400002, "tag": "0016_taken_by_json_array", "breakpoint": false } ] } diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 2f192cc..8431886 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -25,7 +25,8 @@ export const medications = sqliteTable("medications", { userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), name: text("name", { length: 100 }).notNull(), genericName: text("generic_name", { length: 100 }), - takenBy: text("taken_by", { length: 100 }), + takenBy: text("taken_by", { length: 100 }), // Deprecated: use takenByJson + takenByJson: text("taken_by_json").notNull().default("[]"), // JSON array of person names count: integer("count").notNull().default(0), strips: integer("strips").notNull().default(0), packCount: integer("pack_count").notNull().default(1), diff --git a/backend/src/routes/medications.ts b/backend/src/routes/medications.ts index 2af7a5c..8a5441c 100644 --- a/backend/src/routes/medications.ts +++ b/backend/src/routes/medications.ts @@ -21,7 +21,7 @@ const blisterSchema = z.object({ const medicationSchema = z.object({ name: z.string().trim().min(1).max(100), genericName: z.string().trim().max(100).nullable().optional(), - takenBy: z.string().trim().max(100).nullable().optional(), + takenBy: z.array(z.string().trim().max(100)).default([]), // Array of person names packCount: z.number().int().min(0).default(1), stripsPerPack: z.number().int().min(1).default(1), tabsPerStrip: z.number().int().min(1).default(1), @@ -53,6 +53,16 @@ function parseBlisters(row: typeof medications.$inferSelect) { } } +function parseTakenByJson(takenByJson: string | null | undefined): string[] { + if (!takenByJson) return []; + try { + const parsed = JSON.parse(takenByJson); + return Array.isArray(parsed) ? parsed.filter((s: unknown) => typeof s === "string" && s.trim()) : []; + } catch { + return []; + } +} + export async function medicationRoutes(app: FastifyInstance) { // All medication routes require auth app.addHook("preHandler", requireAuth); @@ -81,7 +91,7 @@ export async function medicationRoutes(app: FastifyInstance) { id: row.id, name: row.name, genericName: row.genericName, - takenBy: row.takenBy, + takenBy: parseTakenByJson(row.takenByJson), count: row.count, strips: row.strips, stripSize: row.stripSize, @@ -108,6 +118,7 @@ export async function medicationRoutes(app: FastifyInstance) { const usageJson = JSON.stringify(blisters.map((s) => s.usage)); const everyJson = JSON.stringify(blisters.map((s) => s.every)); const startJson = JSON.stringify(blisters.map((s) => s.start)); + const takenByJson = JSON.stringify(takenBy || []); const derivedCount = deriveTotalTablets(packCount, stripsPerPack, tabsPerStrip, looseTablets); @@ -117,7 +128,8 @@ export async function medicationRoutes(app: FastifyInstance) { userId, name, genericName: genericName || null, - takenBy: takenBy || null, + takenBy: (takenBy && takenBy.length > 0) ? takenBy[0] : null, // Backwards compat + takenByJson, count: derivedCount, strips: stripsPerPack, stripSize: tabsPerStrip, @@ -139,7 +151,7 @@ export async function medicationRoutes(app: FastifyInstance) { id: inserted.id, name: inserted.name, genericName: inserted.genericName, - takenBy: inserted.takenBy, + takenBy: parseTakenByJson(inserted.takenByJson), count: inserted.count, strips: inserted.strips, stripSize: inserted.stripSize, @@ -173,6 +185,7 @@ export async function medicationRoutes(app: FastifyInstance) { const usageJson = JSON.stringify(blisters.map((s) => s.usage)); const everyJson = JSON.stringify(blisters.map((s) => s.every)); const startJson = JSON.stringify(blisters.map((s) => s.start)); + const takenByJson = JSON.stringify(takenBy || []); const derivedCount = deriveTotalTablets(packCount, stripsPerPack, tabsPerStrip, looseTablets); @@ -181,7 +194,8 @@ export async function medicationRoutes(app: FastifyInstance) { .set({ name, genericName: genericName || null, - takenBy: takenBy || null, + takenBy: (takenBy && takenBy.length > 0) ? takenBy[0] : null, // Backwards compat + takenByJson, count: derivedCount, strips: stripsPerPack, stripSize: tabsPerStrip, @@ -207,7 +221,7 @@ export async function medicationRoutes(app: FastifyInstance) { id: result[0].id, name: result[0].name, genericName: result[0].genericName, - takenBy: result[0].takenBy, + takenBy: parseTakenByJson(result[0].takenByJson), count: result[0].count, strips: result[0].strips, stripSize: result[0].stripSize, diff --git a/backend/src/routes/share.ts b/backend/src/routes/share.ts index 4c1f13a..705c5ce 100644 --- a/backend/src/routes/share.ts +++ b/backend/src/routes/share.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { randomBytes } from "crypto"; import { db } from "../db/client.js"; import { medications, shareTokens, userSettings, users } from "../db/schema.js"; -import { eq, and } from "drizzle-orm"; +import { eq, and, sql } from "drizzle-orm"; import { requireAuth, optionalAuth, getAnonymousUserId } from "../plugins/auth.js"; import { env } from "../plugins/env.js"; import type { AuthUser } from "../types/fastify.js"; @@ -35,6 +35,17 @@ async function getUserId(request: any, reply: any): Promise { return authUser.id; } +// Helper to parse takenByJson +function parseTakenByJson(takenByJson: string | null | undefined): string[] { + if (!takenByJson) return []; + try { + const parsed = JSON.parse(takenByJson); + return Array.isArray(parsed) ? parsed.filter((s: unknown) => typeof s === "string" && s.trim()) : []; + } catch { + return []; + } +} + // ============================================================================= // Share Routes // ============================================================================= @@ -70,13 +81,15 @@ export async function shareRoutes(app: FastifyInstance) { // Get user settings for stock thresholds const [settings] = await db.select().from(userSettings).where(eq(userSettings.userId, share.userId)); - // Get medications for this user filtered by takenBy - const meds = await db.select().from(medications).where( - and( - eq(medications.userId, share.userId), - eq(medications.takenBy, share.takenBy) - ) - ); + // Get medications for this user filtered by takenBy (search in JSON array) + // Use SQLite JSON function to check if takenBy is in the array + const allMeds = await db.select().from(medications).where(eq(medications.userId, share.userId)); + + // Filter medications where takenByJson array contains the share.takenBy value + const meds = allMeds.filter((med) => { + const takenByArray = parseTakenByJson(med.takenByJson); + return takenByArray.includes(share.takenBy); + }); // Parse blisters and build schedule data const medicationsWithBlisters = meds.map((med) => { @@ -135,15 +148,14 @@ export async function shareRoutes(app: FastifyInstance) { const { takenBy, scheduleDays } = parsed.data; - // Check if user has medications for this takenBy - const [existingMed] = await db.select().from(medications).where( - and( - eq(medications.userId, userId), - eq(medications.takenBy, takenBy) - ) - ); + // Check if user has medications for this takenBy (search in JSON array) + const allMeds = await db.select().from(medications).where(eq(medications.userId, userId)); + const medsForPerson = allMeds.filter((med) => { + const takenByArray = parseTakenByJson(med.takenByJson); + return takenByArray.includes(takenBy); + }); - if (!existingMed) { + if (medsForPerson.length === 0) { return reply.status(400).send({ error: "No medications found for this person", code: "NO_MEDICATIONS", @@ -182,14 +194,21 @@ export async function shareRoutes(app: FastifyInstance) { async (request, reply) => { const userId = await getUserId(request, reply); - // Get all unique takenBy values for this user - const meds = await db.select({ takenBy: medications.takenBy }) + // Get all unique takenBy values for this user (from JSON arrays) + const meds = await db.select({ takenByJson: medications.takenByJson }) .from(medications) .where(eq(medications.userId, userId)); - const uniquePeople = [...new Set(meds.map((m) => m.takenBy).filter(Boolean))] as string[]; + // Collect all unique person names from all takenByJson arrays + const allPeople = new Set(); + for (const med of meds) { + const takenByArray = parseTakenByJson(med.takenByJson); + for (const person of takenByArray) { + if (person) allPeople.add(person); + } + } - return { people: uniquePeople }; + return { people: [...allPeople].sort() }; } ); } diff --git a/backend/src/services/intake-reminder-scheduler.ts b/backend/src/services/intake-reminder-scheduler.ts index 8715b92..c1697fb 100644 --- a/backend/src/services/intake-reminder-scheduler.ts +++ b/backend/src/services/intake-reminder-scheduler.ts @@ -22,6 +22,17 @@ function getTimezone(): string { return process.env.TZ || "UTC"; } +// Parse takenByJson to array of strings +function parseTakenByJson(takenByJson: string | null | undefined): string[] { + if (!takenByJson) return []; + try { + const parsed = JSON.parse(takenByJson); + return Array.isArray(parsed) ? parsed.filter((s: unknown) => typeof s === "string" && s.trim()) : []; + } catch { + return []; + } +} + const intakeReminderStateFile = resolve(process.cwd(), "data", "intake-reminder-state.json"); function loadIntakeReminderState(): IntakeReminderState { @@ -63,11 +74,11 @@ type UpcomingIntake = { usage: number; intakeTime: Date; intakeTimeStr: string; - takenBy: string | null; + takenBy: string[]; // Changed to array pillWeightMg: number | null; }; -function getUpcomingIntakes(medName: string, blisters: Blister[], minutesBefore: number, takenBy: string | null, pillWeightMg: number | null, locale: string): UpcomingIntake[] { +function getUpcomingIntakes(medName: string, blisters: Blister[], minutesBefore: number, takenBy: string[], pillWeightMg: number | null, locale: string): UpcomingIntake[] { const now = Date.now(); // Window to detect if "now" is the right time to send reminder // We check if the notify time (intake - 15min) falls within current minute ±1 @@ -153,10 +164,11 @@ async function sendIntakeReminderEmail(email: string, intakes: UpcomingIntake[], return pillText; }; - // Helper to format medication name with takenBy + // Helper to format medication name with takenBy (array of names) const formatMedName = (intake: UpcomingIntake): string => { - if (intake.takenBy) { - return `${intake.medName} ${t(tr.intakeReminder.takenBy, { name: intake.takenBy })}`; + if (intake.takenBy.length > 0) { + const namesStr = intake.takenBy.join(", "); + return `${intake.medName} ${t(tr.intakeReminder.takenBy, { name: namesStr })}`; } return intake.medName; }; @@ -226,7 +238,7 @@ async function sendIntakeReminderEmail(email: string, intakes: UpcomingIntake[], ${t(tr.intakeReminder.description, { minutes: REMINDER_MINUTES_BEFORE })} ${intakes.map((i) => { - const takenByStr = i.takenBy ? ` ${t(tr.intakeReminder.takenBy, { name: i.takenBy })}` : ""; + const takenByStr = i.takenBy.length > 0 ? ` ${t(tr.intakeReminder.takenBy, { name: i.takenBy.join(", ") })}` : ""; return `${i.medName}${takenByStr}: ${formatDosagePlain(i)} - ${i.intakeTimeStr}`; }).join("\n")} @@ -304,7 +316,8 @@ async function checkAndSendIntakeRemindersForUser( // Find all upcoming intakes across all medications for this user for (const med of medsWithReminders) { const blisters = parseBlisters(med); - const upcoming = getUpcomingIntakes(med.name, blisters, REMINDER_MINUTES_BEFORE, med.takenBy, med.pillWeightMg, locale); + const takenByArray = parseTakenByJson(med.takenByJson); + const upcoming = getUpcomingIntakes(med.name, blisters, REMINDER_MINUTES_BEFORE, takenByArray, med.pillWeightMg, locale); allUpcoming.push(...upcoming); } @@ -343,7 +356,7 @@ async function checkAndSendIntakeRemindersForUser( const title = t(tr.push.intakeTitle, { minutes: REMINDER_MINUTES_BEFORE }); const message = newReminders .map((i) => { - const takenByStr = i.takenBy ? ` ${t(tr.intakeReminder.takenBy, { name: i.takenBy })}` : ""; + const takenByStr = i.takenBy.length > 0 ? ` ${t(tr.intakeReminder.takenBy, { name: i.takenBy.join(", ") })}` : ""; let dosage = `${i.usage} ${i.usage === 1 ? tr.common.pill : tr.common.pills}`; if (i.pillWeightMg) { const totalMg = i.usage * i.pillWeightMg; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 59a5f52..e0365db 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,7 +13,7 @@ type Medication = { id: number; name: string; genericName?: string | null; - takenBy?: string | null; + takenBy: string[]; // Changed from string | null to array count: number; strips: number; stripSize: number; @@ -47,7 +47,7 @@ type FormBlister = { usage: string; every: string; startDate: string; startTime: type FormState = { name: string; genericName: string; - takenBy: string; + takenBy: string[]; // Changed from string to array packCount: string; stripsPerPack: string; tabsPerStrip: string; @@ -69,7 +69,7 @@ const defaultBlister = (): FormBlister => { }; }; -const defaultForm = (): FormState => ({ name: "", genericName: "", takenBy: "", packCount: "1", stripsPerPack: "1", tabsPerStrip: "1", looseTablets: "0", pillWeightMg: "", expiryDate: "", notes: "", intakeRemindersEnabled: false, blisters: [defaultBlister()] }); +const defaultForm = (): FormState => ({ name: "", genericName: "", takenBy: [], packCount: "1", stripsPerPack: "1", tabsPerStrip: "1", looseTablets: "0", pillWeightMg: "", expiryDate: "", notes: "", intakeRemindersEnabled: false, blisters: [defaultBlister()] }); // Field validation limits (must match backend) const FIELD_LIMITS = { @@ -216,13 +216,16 @@ function AppContent() { }); // Validate form fields - const validateField = (field: keyof FieldErrors, value: string): string | undefined => { + const validateField = (field: keyof FieldErrors, value: string | string[]): string | undefined => { const limits = FIELD_LIMITS[field]; - if (field === 'name' && (!value || value.trim().length === 0)) { + // Skip validation for takenBy array (individual items validated on add) + if (field === 'takenBy') return undefined; + const strValue = typeof value === 'string' ? value : ''; + if (field === 'name' && (!strValue || strValue.trim().length === 0)) { return t('common.validation.required'); } - if ('max' in limits && value.length > limits.max) { - return t('common.validation.maxLength', { max: limits.max, current: value.length }); + if ('max' in limits && strValue.length > limits.max) { + return t('common.validation.maxLength', { max: limits.max, current: strValue.length }); } return undefined; }; @@ -235,12 +238,12 @@ function AppContent() { // Validate all fields when form changes useEffect(() => { const errors: FieldErrors = {}; - (['name', 'genericName', 'takenBy', 'notes'] as const).forEach(field => { + (['name', 'genericName', 'notes'] as const).forEach(field => { const error = validateField(field, form[field]); if (error) errors[field] = error; }); setFieldErrors(errors); - }, [form.name, form.genericName, form.takenBy, form.notes, t]); + }, [form.name, form.genericName, form.notes, t]); // Load user-specific planner data when user changes useEffect(() => { @@ -320,6 +323,8 @@ function AppContent() { const [scheduleDays, setScheduleDays] = useState(30); const [showPastDays, setShowPastDays] = useState(false); const [takenDoses, setTakenDoses] = useState>(new Set()); + // Tag input state for "Taken By" field + const [takenByInput, setTakenByInput] = useState(""); // Share dialog state const [showShareDialog, setShowShareDialog] = useState(false); const [sharePeople, setSharePeople] = useState([]); @@ -482,6 +487,12 @@ function AppContent() { const coverage = useMemo(() => calculateCoverage(meds, schedule.events, i18n.language, settings.reminderDaysBefore, settings.stockCalculationMode, takenDoses), [meds, schedule.events, i18n.language, settings.reminderDaysBefore, settings.stockCalculationMode, takenDoses]); const depletionByMed = useMemo(() => Object.fromEntries(coverage.all.map((c) => [c.name, c.depletionTime])), [coverage.all]); const coverageByMed = useMemo(() => Object.fromEntries(coverage.all.map((c) => [c.name, c])), [coverage.all]); + + // Get all unique people from medications for autocomplete suggestions + const existingPeople = useMemo(() => { + const allPeople = meds.flatMap(m => m.takenBy || []); + return [...new Set(allPeople)].filter(Boolean).sort(); + }, [meds]); // Get worst stock status for a day's medications (for coloring day blocks) const getDayStockStatus = (dayMeds: { medName: string; lastWhen: number }[]) => { @@ -536,7 +547,7 @@ function AppContent() { setLoading(true); fetch("/api/medications") .then((res) => res.json()) - .then((data: Medication[]) => setMeds(data)) + .then((data) => setMeds(Array.isArray(data) ? data : [])) .catch(() => setMeds([])) .finally(() => setLoading(false)); } @@ -771,10 +782,11 @@ function AppContent() { function startEdit(med: Medication) { setEditingId(med.id); + setTakenByInput(""); // Clear tag input when starting edit setForm({ name: med.name, genericName: med.genericName ?? "", - takenBy: med.takenBy ?? "", + takenBy: med.takenBy || [], // Already an array from API packCount: String(med.packCount ?? 1), stripsPerPack: String(med.stripsPerPack ?? med.strips ?? 1), tabsPerStrip: String(med.tabsPerStrip ?? med.stripSize ?? 1), @@ -801,6 +813,7 @@ function AppContent() { setShowEditModal(false); setPendingImage(null); setPendingImagePreview(null); + setTakenByInput(""); setForm(defaultForm()); } @@ -808,6 +821,29 @@ function AppContent() { setForm((prev) => ({ ...prev, [key]: value })); } + // Tag input helpers for "Taken By" field + function addTakenByPerson(name: string) { + const trimmed = name.trim(); + if (trimmed && trimmed.length <= FIELD_LIMITS.takenBy.max && !form.takenBy.includes(trimmed)) { + setForm(prev => ({ ...prev, takenBy: [...prev.takenBy, trimmed] })); + } + setTakenByInput(""); + } + + function removeTakenByPerson(name: string) { + setForm(prev => ({ ...prev, takenBy: prev.takenBy.filter(p => p !== name) })); + } + + function handleTakenByKeyDown(e: React.KeyboardEvent) { + if (e.key === 'Enter' || e.key === ',') { + e.preventDefault(); + addTakenByPerson(takenByInput); + } else if (e.key === 'Backspace' && !takenByInput && form.takenBy.length > 0) { + // Remove last tag on backspace when input is empty + removeTakenByPerson(form.takenBy[form.takenBy.length - 1]); + } + } + async function saveMedication(e: React.FormEvent) { e.preventDefault(); if (!form.name.trim()) return; @@ -816,7 +852,7 @@ function AppContent() { const payload = { name: form.name.trim(), genericName: form.genericName.trim() || null, - takenBy: form.takenBy.trim() || null, + takenBy: form.takenBy.filter(name => name.trim()), // Send array, filter empty strings packCount: Number(form.packCount) || 0, stripsPerPack: Math.max(1, Number(form.stripsPerPack) || 1), tabsPerStrip: Math.max(1, Number(form.tabsPerStrip) || 1), @@ -887,8 +923,9 @@ function AppContent() { setShareSelectedPerson(""); setShareSelectedDays(30); - // Get unique takenBy people from medications - const uniquePeople = [...new Set(meds.map(m => m.takenBy).filter(Boolean))] as string[]; + // Get unique takenBy people from all medications (flatten arrays) + const allPeople = meds.flatMap(m => m.takenBy || []); + const uniquePeople = [...new Set(allPeople)].filter(Boolean).sort(); setSharePeople(uniquePeople); if (uniquePeople.length > 0) { setShareSelectedPerson(uniquePeople[0]); @@ -1133,7 +1170,9 @@ function AppContent() { {row.name} - {med?.takenBy && { e.stopPropagation(); setSelectedUser(med.takenBy!); }}>{med.takenBy}} + {med?.takenBy && med.takenBy.length > 0 && med.takenBy.map((person) => ( + { e.stopPropagation(); setSelectedUser(person); }}>{person} + ))} {(med?.intakeRemindersEnabled || med?.notes) && ( {med?.intakeRemindersEnabled && 🔔} @@ -1201,7 +1240,9 @@ function AppContent() { {row.name} - {med?.takenBy && { e.stopPropagation(); setSelectedUser(med.takenBy!); }}>{med.takenBy}} + {med?.takenBy && med.takenBy.length > 0 && med.takenBy.map((person) => ( + { e.stopPropagation(); setSelectedUser(person); }}>{person} + ))} {(med?.intakeRemindersEnabled || med?.notes) && ( @@ -1315,7 +1356,7 @@ function AppContent() { return (
{dose.timeStr} - {dose.usage} {dose.usage !== 1 ? t('common.pills') : t('common.pill')}{med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} mg)`}{med?.takenBy && {t('dose.takenBy')} setSelectedUser(med.takenBy!)}>{med.takenBy}} + {dose.usage} {dose.usage !== 1 ? t('common.pills') : t('common.pill')}{med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} mg)`}{med?.takenBy && med.takenBy.length > 0 && {t('dose.takenBy')} {med.takenBy.map((person, i) => ({i > 0 && ", "} setSelectedUser(person)}>{person}))}} {isTaken ? ( ) : ( @@ -1414,7 +1455,7 @@ function AppContent() { return (
{dose.timeStr} - {dose.usage} {dose.usage !== 1 ? t('common.pills') : t('common.pill')}{med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} mg)`}{med?.takenBy && {t('dose.takenBy')} setSelectedUser(med.takenBy!)}>{med.takenBy}} + {dose.usage} {dose.usage !== 1 ? t('common.pills') : t('common.pill')}{med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} mg)`}{med?.takenBy && med.takenBy.length > 0 && {t('dose.takenBy')} {med.takenBy.map((person, i) => ({i > 0 && ", "} setSelectedUser(person)}>{person}))}} {isTaken ? ( ) : ( @@ -1504,12 +1545,28 @@ function AppContent() {