Fix Edit navigation from menu + add avatar upload indicator

- BeneficiaryMenu: Navigate with ?edit=true param to open edit modal
- Beneficiary index: Auto-open edit modal when edit=true in URL
- Add loading indicator on Save button during edit save
- Add "Uploading..." overlay on avatar during image upload
This commit is contained in:
Sergei 2026-01-12 21:44:40 -08:00
parent 7105bb72f7
commit 429a18d1eb
2 changed files with 64 additions and 10 deletions

View File

@ -54,7 +54,7 @@ const getDashboardUrl = (deploymentId?: number) => {
const FERDINAND_DEPLOYMENT_ID = 21; const FERDINAND_DEPLOYMENT_ID = 21;
export default function BeneficiaryDetailScreen() { export default function BeneficiaryDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>(); const { id, edit } = useLocalSearchParams<{ id: string; edit?: string }>();
const { setCurrentBeneficiary } = useBeneficiary(); const { setCurrentBeneficiary } = useBeneficiary();
const toast = useToast(); const toast = useToast();
@ -74,6 +74,8 @@ export default function BeneficiaryDetailScreen() {
// Edit modal state // Edit modal state
const [isEditModalVisible, setIsEditModalVisible] = useState(false); const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [editForm, setEditForm] = useState({ name: '', address: '', avatar: undefined as string | undefined }); const [editForm, setEditForm] = useState({ name: '', address: '', avatar: undefined as string | undefined });
const [isSavingEdit, setIsSavingEdit] = useState(false);
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
// Avatar lightbox state // Avatar lightbox state
const [lightboxVisible, setLightboxVisible] = useState(false); const [lightboxVisible, setLightboxVisible] = useState(false);
@ -193,6 +195,15 @@ export default function BeneficiaryDetailScreen() {
loadBeneficiary(); loadBeneficiary();
}, [loadBeneficiary]); }, [loadBeneficiary]);
// Auto-open edit modal if navigated with ?edit=true parameter
useEffect(() => {
if (edit === 'true' && beneficiary && !isLoading && !isEditModalVisible) {
handleEditPress();
// Clear the edit param to prevent re-opening on future navigations
router.setParams({ edit: undefined });
}
}, [edit, beneficiary, isLoading, isEditModalVisible]);
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
setIsRefreshing(true); setIsRefreshing(true);
loadBeneficiary(false); loadBeneficiary(false);
@ -235,6 +246,7 @@ export default function BeneficiaryDetailScreen() {
} }
const beneficiaryId = parseInt(id, 10); const beneficiaryId = parseInt(id, 10);
setIsSavingEdit(true);
try { try {
// Update basic info // Update basic info
@ -245,12 +257,15 @@ export default function BeneficiaryDetailScreen() {
if (!response.ok) { if (!response.ok) {
toast.error('Error', response.error?.message || 'Failed to save changes.'); toast.error('Error', response.error?.message || 'Failed to save changes.');
setIsSavingEdit(false);
return; return;
} }
// Upload avatar if changed (new local file URI) // Upload avatar if changed (new local file URI)
if (editForm.avatar && editForm.avatar.startsWith('file://')) { if (editForm.avatar && editForm.avatar.startsWith('file://')) {
setIsUploadingAvatar(true);
const avatarResult = await api.updateBeneficiaryAvatar(beneficiaryId, editForm.avatar); const avatarResult = await api.updateBeneficiaryAvatar(beneficiaryId, editForm.avatar);
setIsUploadingAvatar(false);
if (!avatarResult.ok) { if (!avatarResult.ok) {
console.warn('[BeneficiaryDetail] Failed to upload avatar:', avatarResult.error?.message); console.warn('[BeneficiaryDetail] Failed to upload avatar:', avatarResult.error?.message);
// Show info but don't fail the whole operation // Show info but don't fail the whole operation
@ -263,6 +278,9 @@ export default function BeneficiaryDetailScreen() {
loadBeneficiary(false); loadBeneficiary(false);
} catch (err) { } catch (err) {
toast.error('Error', 'Failed to save changes.'); toast.error('Error', 'Failed to save changes.');
} finally {
setIsSavingEdit(false);
setIsUploadingAvatar(false);
} }
}; };
@ -475,7 +493,11 @@ export default function BeneficiaryDetailScreen() {
<ScrollView style={styles.modalContent}> <ScrollView style={styles.modalContent}>
{/* Avatar */} {/* Avatar */}
<TouchableOpacity style={styles.avatarPicker} onPress={handlePickAvatar}> <TouchableOpacity
style={styles.avatarPicker}
onPress={handlePickAvatar}
disabled={isSavingEdit}
>
{editForm.avatar ? ( {editForm.avatar ? (
<Image source={{ uri: editForm.avatar }} style={styles.avatarPickerImage} /> <Image source={{ uri: editForm.avatar }} style={styles.avatarPickerImage} />
) : ( ) : (
@ -483,9 +505,17 @@ export default function BeneficiaryDetailScreen() {
<Ionicons name="camera" size={32} color={AppColors.textMuted} /> <Ionicons name="camera" size={32} color={AppColors.textMuted} />
</View> </View>
)} )}
<View style={styles.avatarPickerBadge}> {isUploadingAvatar && (
<Ionicons name="pencil" size={12} color={AppColors.white} /> <View style={styles.avatarUploadOverlay}>
</View> <ActivityIndicator size="large" color={AppColors.white} />
<Text style={styles.avatarUploadText}>Uploading...</Text>
</View>
)}
{!isUploadingAvatar && (
<View style={styles.avatarPickerBadge}>
<Ionicons name="pencil" size={12} color={AppColors.white} />
</View>
)}
</TouchableOpacity> </TouchableOpacity>
{/* Name */} {/* Name */}
@ -517,13 +547,22 @@ export default function BeneficiaryDetailScreen() {
<View style={styles.modalFooter}> <View style={styles.modalFooter}>
<TouchableOpacity <TouchableOpacity
style={styles.cancelButton} style={[styles.cancelButton, isSavingEdit && styles.buttonDisabled]}
onPress={() => setIsEditModalVisible(false)} onPress={() => setIsEditModalVisible(false)}
disabled={isSavingEdit}
> >
<Text style={styles.cancelButtonText}>Cancel</Text> <Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.saveButton} onPress={handleSaveEdit}> <TouchableOpacity
<Text style={styles.saveButtonText}>Save</Text> style={[styles.saveButton, isSavingEdit && styles.buttonDisabled]}
onPress={handleSaveEdit}
disabled={isSavingEdit}
>
{isSavingEdit ? (
<ActivityIndicator size="small" color={AppColors.white} />
) : (
<Text style={styles.saveButtonText}>Save</Text>
)}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
@ -714,6 +753,18 @@ const styles = StyleSheet.create({
borderWidth: 2, borderWidth: 2,
borderColor: AppColors.surface, borderColor: AppColors.surface,
}, },
avatarUploadOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.6)',
borderRadius: AvatarSizes.lg / 2,
justifyContent: 'center',
alignItems: 'center',
},
avatarUploadText: {
color: AppColors.white,
fontSize: FontSizes.sm,
marginTop: Spacing.xs,
},
inputGroup: { inputGroup: {
marginBottom: Spacing.md, marginBottom: Spacing.md,
}, },
@ -765,4 +816,7 @@ const styles = StyleSheet.create({
fontWeight: FontWeights.semibold, fontWeight: FontWeights.semibold,
color: AppColors.white, color: AppColors.white,
}, },
buttonDisabled: {
opacity: 0.6,
},
}); });

View File

@ -68,8 +68,8 @@ export function BeneficiaryMenu({
if (onEdit) { if (onEdit) {
onEdit(); onEdit();
} else { } else {
// Navigate to main page with edit intent // Navigate to main page with edit=true param to open edit modal
router.push(`/(tabs)/beneficiaries/${beneficiaryId}`); router.push(`/(tabs)/beneficiaries/${beneficiaryId}?edit=true`);
} }
break; break;
case 'access': case 'access':