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;
export default function BeneficiaryDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const { id, edit } = useLocalSearchParams<{ id: string; edit?: string }>();
const { setCurrentBeneficiary } = useBeneficiary();
const toast = useToast();
@ -74,6 +74,8 @@ export default function BeneficiaryDetailScreen() {
// Edit modal state
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [editForm, setEditForm] = useState({ name: '', address: '', avatar: undefined as string | undefined });
const [isSavingEdit, setIsSavingEdit] = useState(false);
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
// Avatar lightbox state
const [lightboxVisible, setLightboxVisible] = useState(false);
@ -193,6 +195,15 @@ export default function BeneficiaryDetailScreen() {
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(() => {
setIsRefreshing(true);
loadBeneficiary(false);
@ -235,6 +246,7 @@ export default function BeneficiaryDetailScreen() {
}
const beneficiaryId = parseInt(id, 10);
setIsSavingEdit(true);
try {
// Update basic info
@ -245,12 +257,15 @@ export default function BeneficiaryDetailScreen() {
if (!response.ok) {
toast.error('Error', response.error?.message || 'Failed to save changes.');
setIsSavingEdit(false);
return;
}
// Upload avatar if changed (new local file URI)
if (editForm.avatar && editForm.avatar.startsWith('file://')) {
setIsUploadingAvatar(true);
const avatarResult = await api.updateBeneficiaryAvatar(beneficiaryId, editForm.avatar);
setIsUploadingAvatar(false);
if (!avatarResult.ok) {
console.warn('[BeneficiaryDetail] Failed to upload avatar:', avatarResult.error?.message);
// Show info but don't fail the whole operation
@ -263,6 +278,9 @@ export default function BeneficiaryDetailScreen() {
loadBeneficiary(false);
} catch (err) {
toast.error('Error', 'Failed to save changes.');
} finally {
setIsSavingEdit(false);
setIsUploadingAvatar(false);
}
};
@ -475,7 +493,11 @@ export default function BeneficiaryDetailScreen() {
<ScrollView style={styles.modalContent}>
{/* Avatar */}
<TouchableOpacity style={styles.avatarPicker} onPress={handlePickAvatar}>
<TouchableOpacity
style={styles.avatarPicker}
onPress={handlePickAvatar}
disabled={isSavingEdit}
>
{editForm.avatar ? (
<Image source={{ uri: editForm.avatar }} style={styles.avatarPickerImage} />
) : (
@ -483,9 +505,17 @@ export default function BeneficiaryDetailScreen() {
<Ionicons name="camera" size={32} color={AppColors.textMuted} />
</View>
)}
{isUploadingAvatar && (
<View style={styles.avatarUploadOverlay}>
<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>
{/* Name */}
@ -517,13 +547,22 @@ export default function BeneficiaryDetailScreen() {
<View style={styles.modalFooter}>
<TouchableOpacity
style={styles.cancelButton}
style={[styles.cancelButton, isSavingEdit && styles.buttonDisabled]}
onPress={() => setIsEditModalVisible(false)}
disabled={isSavingEdit}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.saveButton} onPress={handleSaveEdit}>
<TouchableOpacity
style={[styles.saveButton, isSavingEdit && styles.buttonDisabled]}
onPress={handleSaveEdit}
disabled={isSavingEdit}
>
{isSavingEdit ? (
<ActivityIndicator size="small" color={AppColors.white} />
) : (
<Text style={styles.saveButtonText}>Save</Text>
)}
</TouchableOpacity>
</View>
</View>
@ -714,6 +753,18 @@ const styles = StyleSheet.create({
borderWidth: 2,
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: {
marginBottom: Spacing.md,
},
@ -765,4 +816,7 @@ const styles = StyleSheet.create({
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
buttonDisabled: {
opacity: 0.6,
},
});

View File

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