import React, { useEffect, useRef, useState } from 'react'; import { IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonInput, IonList, IonModal, IonPage, IonTitle, IonToolbar, } from '@ionic/react'; import { cameraOutline, chevronBackOutline, closeOutline, } from 'ionicons/icons'; import { useHistory, useParams } from 'react-router-dom'; import { supabase } from '../supabase'; import { useAuth } from '../contexts/AuthContext'; import { buildCacheKey, removeCache } from '../utils/localCache'; import AvatarPicker from '../components/AvatarPicker'; import momImage from '../assets/mom.jpg'; import dadImage from '../assets/dad.jpg'; import '../styles/recipients.css'; type Params = { id?: string }; type RecipientRow = { id: string; first_name: string; last_name: string; relationship: string; country: string; city: string; mobile_number: string; photo_path: string | null; }; const countryOptions = [ { name: 'Zimbabwe', flag: 'πŸ‡ΏπŸ‡Ό', dialCode: '+263' }, { name: 'South Africa', flag: 'πŸ‡ΏπŸ‡¦', dialCode: '+27' }, { name: 'Zambia', flag: 'πŸ‡ΏπŸ‡²', dialCode: '+260' }, { name: 'Botswana', flag: 'πŸ‡§πŸ‡Ό', dialCode: '+267' }, { name: 'Mozambique', flag: 'πŸ‡²πŸ‡Ώ', dialCode: '+258' }, { name: 'United Kingdom', flag: 'πŸ‡¬πŸ‡§', dialCode: '+44' }, { name: 'United States', flag: 'πŸ‡ΊπŸ‡Έ', dialCode: '+1' }, { name: 'Canada', flag: 'πŸ‡¨πŸ‡¦', dialCode: '+1' }, { name: 'Australia', flag: 'πŸ‡¦πŸ‡Ί', dialCode: '+61' }, ]; const locationOptionsByCountry: Record< string, Array<{ group: string; options: Array<{ name: string; description: string }>; }> > = { Zimbabwe: [ { group: 'Major cities', options: [ { name: 'Harare', description: 'Capital city coverage' }, { name: 'Bulawayo', description: 'City merchants and pharmacies' }, { name: 'Mutare', description: 'Eastern Highlands coverage' }, { name: 'Gweru', description: 'Midlands city coverage' }, ], }, { group: 'Towns', options: [ { name: 'Chitungwiza', description: 'Harare metro support' }, { name: 'Masvingo', description: 'Town and surrounding areas' }, { name: 'Kwekwe', description: 'Supported collection points' }, { name: 'Kadoma', description: 'Supported collection points' }, { name: 'Victoria Falls', description: 'Town coverage' }, ], }, { group: 'Rural districts', options: [ { name: 'Murehwa District', description: 'Rural collection support' }, { name: 'Gokwe District', description: 'Rural collection support' }, { name: 'Buhera District', description: 'Rural collection support' }, { name: 'Zaka District', description: 'Rural collection support' }, { name: 'Guruve District', description: 'Rural collection support' }, { name: 'Tsholotsho District', description: 'Rural collection support', }, ], }, ], Zambia: [ { group: 'Major cities', options: [ { name: 'Lusaka', description: 'Capital city coverage' }, { name: 'Ndola', description: 'Copperbelt coverage' }, { name: 'Kitwe', description: 'Copperbelt coverage' }, { name: 'Livingstone', description: 'Town coverage' }, ], }, { group: 'Rural districts', options: [ { name: 'Chongwe District', description: 'Rural collection support' }, { name: 'Monze District', description: 'Rural collection support' }, ], }, ], Botswana: [ { group: 'Cities and towns', options: [ { name: 'Gaborone', description: 'Capital city coverage' }, { name: 'Francistown', description: 'Town coverage' }, { name: 'Maun', description: 'Town coverage' }, ], }, { group: 'Rural districts', options: [ { name: 'Kweneng District', description: 'Rural collection support' }, { name: 'Central District', description: 'Rural collection support' }, ], }, ], }; const fallbackLocationGroups = [ { group: 'Supported areas', options: [ { name: 'Main city', description: 'Available merchant coverage' }, { name: 'Nearby town', description: 'Supported collection points' }, { name: 'Rural district', description: 'Rural collection support' }, ], }, ]; const getCountryMeta = (countryName: string) => countryOptions.find((option) => option.name === countryName) ?? countryOptions[0]; const getLocationGroups = (countryName: string) => locationOptionsByCountry[countryName] ?? fallbackLocationGroups; const getSeededRecipientImage = ( firstName?: string | null, photoPath?: string | null ) => { const normalizedPath = photoPath?.trim().toLowerCase(); if (normalizedPath === 'mom.jpg' || normalizedPath === 'mum.jpg') { return momImage; } if (normalizedPath === 'dad.jpg' || normalizedPath === 'father.jpg') { return dadImage; } const normalizedName = firstName?.trim().toLowerCase(); if (normalizedName === 'mum' || normalizedName === 'mom') { return momImage; } if (normalizedName === 'dad' || normalizedName === 'father') { return dadImage; } return null; }; const stripDialCode = (value: string, dialCode: string) => { const normalizedValue = value.trim(); if (normalizedValue.startsWith(dialCode)) { return normalizedValue.slice(dialCode.length).trimStart(); } return normalizedValue; }; const sanitizePhoneInput = (value: string) => value.replace(/[^\d\s()-]/g, '').replace(/\s{2,}/g, ' '); const normalizePhoneInput = (value: string) => sanitizePhoneInput(value).replace(/^0+/, ''); const formatNameInput = (value: string) => value .toLowerCase() .replace( /(^|[\s'-])([a-z])/g, (_match, separator: string, letter: string) => `${separator}${letter.toUpperCase()}` ); const RecipientFormPage: React.FC = () => { const history = useHistory(); const { id } = useParams(); const { user } = useAuth(); const isEdit = Boolean(id); const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [relationship, setRelationship] = useState(''); const [country, setCountry] = useState('Zimbabwe'); const [city, setCity] = useState(''); const [mobileNumber, setMobileNumber] = useState(''); const [showCountrySheet, setShowCountrySheet] = useState(false); const [showLocationSheet, setShowLocationSheet] = useState(false); const selectedCountry = getCountryMeta(country); const locationGroups = getLocationGroups(country); const [photoFile, setPhotoFile] = useState(null); const [photoPreview, setPhotoPreview] = useState(null); const previewObjectUrlRef = useRef(null); const [existingPhotoPath, setExistingPhotoPath] = useState( null ); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const firstNameInputRef = useRef(null); const lastNameInputRef = useRef(null); const relationshipInputRef = useRef(null); const mobileInputRef = useRef(null); useEffect(() => { if (isEdit) { void loadRecipient(); } }, [id, isEdit]); useEffect(() => { return () => { if (previewObjectUrlRef.current) { URL.revokeObjectURL(previewObjectUrlRef.current); } }; }, []); const handleGoBack = () => { if (history.length > 1) { history.goBack(); } else { history.replace('/recipients'); } }; const showError = (message: string) => { setError(message); setTimeout(() => setError(null), 4000); }; const applyRecipientToForm = async (recipient: RecipientRow) => { setFirstName(recipient.first_name); setLastName(recipient.last_name); setRelationship(recipient.relationship); setCountry(recipient.country); setCity(recipient.city); const recipientCountry = getCountryMeta(recipient.country); setMobileNumber( stripDialCode(recipient.mobile_number, recipientCountry.dialCode) ); setExistingPhotoPath(recipient.photo_path); const seededImage = getSeededRecipientImage( recipient.first_name, recipient.photo_path ); if (seededImage) { setPhotoPreview(seededImage); return; } if (!recipient.photo_path) { setPhotoPreview(null); return; } const { data: signed } = await supabase.storage .from('recipient-photos') .createSignedUrl(recipient.photo_path, 3600); setPhotoPreview( signed?.signedUrl ? `${signed.signedUrl}&v=${encodeURIComponent(recipient.photo_path)}` : null ); }; const loadRecipient = async () => { if (!id) return; setLoading(true); const ownerId = user?.id ?? '00000000-0000-0000-0000-000000000000'; const { data, error: loadError } = await supabase .from('recipients') .select( 'id,first_name,last_name,relationship,country,city,mobile_number,photo_path' ) .eq('id', id) .eq('user_id', ownerId) .single(); if (loadError || !data) { showError(loadError?.message ?? 'Recipient not found'); setLoading(false); history.replace('/recipients'); return; } const recipient = data as RecipientRow; await applyRecipientToForm(recipient); setLoading(false); }; const handlePhotoChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; if (previewObjectUrlRef.current) { URL.revokeObjectURL(previewObjectUrlRef.current); } const objectUrl = URL.createObjectURL(file); previewObjectUrlRef.current = objectUrl; setPhotoFile(file); setPhotoPreview(objectUrl); }; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!user) { showError('Please sign in again to save this loved one'); return; } const payload = { first_name: firstName.trim(), last_name: lastName.trim(), relationship: relationship.trim(), country: country.trim(), city: city.trim(), mobile_number: `${selectedCountry.dialCode} ${mobileNumber.trim()}`.trim(), updated_at: new Date().toISOString(), }; if ( !payload.first_name || !payload.last_name || !payload.relationship || !payload.country || !payload.city || !payload.mobile_number ) { showError('Please complete all required fields'); return; } setSaving(true); setError(null); let photoPath = existingPhotoPath; if (photoFile) { const extension = photoFile.name.split('.').pop() || 'jpg'; photoPath = `${user.id}/${Date.now()}.${extension}`; const { error: uploadError } = await supabase.storage .from('recipient-photos') .upload(photoPath, photoFile); if (uploadError) { showError(uploadError.message); setSaving(false); return; } } const recipientsCacheKey = buildCacheKey(user.id, 'recipientsList'); if (isEdit && id) { const { error: updateError } = await supabase .from('recipients') .update({ ...payload, photo_path: photoPath }) .eq('id', id) .eq('user_id', user.id); if (updateError) { showError(updateError.message); setSaving(false); return; } await Promise.all([ removeCache(recipientsCacheKey), removeCache(buildCacheKey(user.id, `recipient_${id}`)), ]); setSaving(false); history.push(`/recipients/${id}`); return; } const { data, error: insertError } = await supabase .from('recipients') .insert({ user_id: user.id, ...payload, photo_path: photoPath, }) .select('id') .single(); if (insertError || !data) { showError(insertError?.message ?? 'Could not create recipient'); setSaving(false); return; } await removeCache(recipientsCacheKey); setSaving(false); history.replace(`/recipients/${data.id}`); }; const initials = `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); return ( {isEdit ? 'Edit loved one' : 'Add loved one'}
{error && (

{error}

)}
{saving ? 'Saving...' : isEdit ? 'Save changes' : 'Add loved one'}
setShowCountrySheet(false)} initialBreakpoint={1} breakpoints={[0, 1]} handle={true} className="country-sheet-modal" >

Select country

setShowCountrySheet(false)} aria-label="Close country selector" >
{countryOptions.map((option) => { const isSelected = option.name === country; return ( ); })}
setShowLocationSheet(false)} initialBreakpoint={1} breakpoints={[0, 1]} handle={true} className="country-sheet-modal" >

Select location

setShowLocationSheet(false)} aria-label="Close location selector" >
Choose a city, town, or rural district where support collection is available.
{locationGroups.map((group) => (

{group.group}

{group.options.map((option) => { const isSelected = option.name === city; return ( ); })}
))}
); }; export default RecipientFormPage;