import { useMemo, useState } from 'react' import { m, mKey } from '@/i18n/messages' import { useLocale } from '@/i18n/config' import { asI18n, type I18nString } from '@pikku/react' import { ActionIcon, Badge, Box, Button, Group, Paper, Select, Stack, Text, TextInput, Tooltip, } from '@pikku/mantine/core' type SeverityKey = 'mild' | 'moderate' | 'severe' | 'standard' type Allergy = { allergen: string severity: string note?: string | null } type Participant = { participantId: string name: string kind: string dietaryTag: string | null roomId: string | null roomNumber: number | null allergies: Allergy[] } type DietaryTag = | 'omnivore' | 'vegetarian' | 'vegan' | 'gluten_free' | 'lactose_free' | 'pescatarian' | 'unknown' type Kind = 'overnight' | 'day_guest' const initials = (name: string) => name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((p) => p[0]?.toUpperCase() ?? '') .join('') const TINTS = [ '#88557F', '#A05C8E', '#BC7AA6', '#D3A7C4', '#FFBC7D', '#F37E15', '#FFCE00', '#806600', '#4E3149', '#583651', ] const tintFor = (id: string) => { let h = 0 for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) | 0 return TINTS[Math.abs(h) % TINTS.length] } const SEVERITY_STYLE: Record< SeverityKey, { bg: string; border: string; fg: string } > = { mild: { bg: '#F1EEF0', border: '#D9D2D7', fg: '#5A4F56' }, moderate: { bg: '#FFE7CF', border: '#FFBC7D', fg: '#7A4A18' }, severe: { bg: '#4E3149', border: '#4E3149', fg: '#FFFFFF' }, standard: { bg: '#F5EAF1', border: '#E0CCD9', fg: '#4E3149' }, } const severityKey = (s: string): SeverityKey => s === 'mild' || s === 'moderate' || s === 'severe' || s === 'standard' ? s : 'standard' const DIETARY_VALUES: DietaryTag[] = [ 'omnivore', 'vegetarian', 'vegan', 'gluten_free', 'lactose_free', 'pescatarian', 'unknown', ] export function ParticipantsTab({ participants, bookingId, addParticipant, updateParticipant, removeParticipant, addAllergy, removeAllergy, isAddingParticipant, }: { participants: Participant[] bookingId: string addParticipant: (args: { bookingId: string name: string kind: Kind email: null age: null notes: null }, opts?: { onSuccess?: () => void }) => void updateParticipant: (args: { participantId: string dietaryTag: DietaryTag | null }) => void removeParticipant: (args: { participantId: string }) => void addAllergy: ( args: { participantId: string allergen: string severity: 'standard' | 'separate_prep' | 'severe' }, opts?: { onSuccess?: () => void }, ) => void removeAllergy: (args: { participantId: string; allergen: string }) => void isAddingParticipant: boolean }) { useLocale() const [newName, setNewName] = useState('') const [newKind, setNewKind] = useState('overnight') const [newAllergyByPid, setNewAllergyByPid] = useState>({}) const [allergyOpenByPid, setAllergyOpenByPid] = useState>({}) const groups = useMemo(() => { const overnight = participants.filter((p) => p.kind === 'overnight') const day = participants.filter((p) => p.kind === 'day_guest') return [ { kind: 'overnight' as const, items: overnight }, { kind: 'day_guest' as const, items: day }, ].filter((g) => g.items.length > 0) }, [participants]) const dietaryOptions = DIETARY_VALUES.map((v) => ({ value: v, label: mKey(`common.pages.booking.participants.dietary.${v}` as any), })) const submitAdd = () => { const name = newName.trim() if (!name) return addParticipant( { bookingId, name, kind: newKind, email: null, age: null, notes: null }, { onSuccess: () => { setNewName('') setNewKind('overnight') }, }, ) } return ( {/* Inline compact add-participant bar -------------------------------- */} {m.common_pages_booking_participants_add_modal_title()} setNewName(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === 'Enter') submitAdd() }} style={{ flex: 1, minWidth: 160 }} aria-label={m.common_pages_booking_participants_add_modal_name()} /> onDietChange((v as DietaryTag | null) ?? null)} data={dietaryOptions} clearable placeholder={m.common_pages_booking_participants_dietary_unknown()} /> {/* Allergy summary chip */} {/* Delete */} × {/* Allergy chips + editor */} {p.allergies.length === 0 && !allergyEditorOpen ? ( ) : ( {p.allergies.map((a) => ( onRemoveAllergy(a.allergen)} testId={`remove-allergy-${p.participantId}-${a.allergen}`} removeLabel={m.common_pages_booking_participants_allergy_remove_aria({ allergen: a.allergen, })} /> ))} {!allergyEditorOpen ? ( ) : null} )} {allergyEditorOpen && ( onAllergyDraftChange(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === 'Enter') onAddAllergy() }} style={{ flex: 1, maxWidth: 280 }} /> )} ) } function AllergySummary({ count }: { count: number }) { useLocale() if (count === 0) { return ( {m.common_pages_booking_participants_allergy_none()} ) } return ( {m.common_pages_booking_participants_allergy_count({ count })} ) } function AllergyChip({ allergy, onRemove, testId, removeLabel, }: { allergy: Allergy onRemove: () => void testId: string removeLabel: I18nString }) { useLocale() const sev = severityKey(allergy.severity) const style = SEVERITY_STYLE[sev] const sevLabel = sev === 'standard' ? null : mKey(`common.pages.booking.participants.severity.${sev}` as const) return ( {mKey(`common.pages.allergens.${allergy.allergen}` as any)} {sevLabel ? ( · {sevLabel} ) : null} × ) }