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:
parent
7105bb72f7
commit
429a18d1eb
@ -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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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':
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user