Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
807 changes: 807 additions & 0 deletions app/mobile/app/admin/dashboard.tsx

Large diffs are not rendered by default.

214 changes: 127 additions & 87 deletions app/mobile/app/profile.tsx

Large diffs are not rendered by default.

121 changes: 71 additions & 50 deletions app/mobile/app/r-request-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { CategoryPicker } from '../components/forms/CategoryPicker';
import { DeadlinePicker } from '../components/forms/DeadlinePicker';
import { AddressFields } from '../components/forms/AddressFields';
import { AddressFieldsValue, emptyAddress, parseAddressString, formatAddress } from '../utils/address';
import { ReportModal } from '../components/ui/ReportModal';

export default function RequestDetails() {
const params = useLocalSearchParams();
Expand All @@ -43,6 +44,7 @@ export default function RequestDetails() {
const [cancellingTask, setCancellingTask] = useState(false);

const [modalVisible, setModalVisible] = useState(false);
const [reportModalVisible, setReportModalVisible] = useState(false);
const [isEdit, setIsEdit] = useState(false);
const [rating, setRating] = useState(0);
const [reviewText, setReviewText] = useState('');
Expand Down Expand Up @@ -75,8 +77,8 @@ export default function RequestDetails() {
(property === 'Text'
? themeColors.text
: property === 'Background'
? themeColors.labelDefaultBackground
: themeColors.labelDefaultBorder || themeColors.border)
? themeColors.labelDefaultBackground
: themeColors.labelDefaultBorder || themeColors.border)
);
};

Expand Down Expand Up @@ -175,20 +177,20 @@ export default function RequestDetails() {
}
setCurrentVolunteerIndex(0);
const currentVolunteer = assignedVolunteers[0];

// Check if review already exists for this volunteer
const existingReview = existingReviews.find(
(review) => review.reviewee.id === currentVolunteer.user.id && review.reviewer.id === user?.id
);

if (existingReview) {
setRating(existingReview.score);
setReviewText(existingReview.comment);
} else {
setRating(0);
setReviewText('');
}

setModalVisible(true);
setIsEdit(false);
};
Expand All @@ -201,8 +203,8 @@ export default function RequestDetails() {

const hasReviewedAllVolunteers = (): boolean => {
if (assignedVolunteers.length === 0) return false;
return assignedVolunteers.every((volunteer) =>
existingReviews.some((review) =>
return assignedVolunteers.every((volunteer) =>
existingReviews.some((review) =>
review.reviewee.id === volunteer.user.id && review.reviewer.id === user?.id
)
);
Expand Down Expand Up @@ -286,20 +288,20 @@ export default function RequestDetails() {
const nextIndex = currentVolunteerIndex + 1;
setCurrentVolunteerIndex(nextIndex);
const nextVolunteer = assignedVolunteers[nextIndex];

// Load existing review for next volunteer if it exists
const nextReview = updatedReviews.find(
(review) => review.reviewee.id === nextVolunteer.user.id && review.reviewer.id === user?.id
);

if (nextReview) {
setRating(nextReview.score);
setReviewText(nextReview.comment);
} else {
setRating(0);
setReviewText('');
}

Alert.alert('Success', `Review submitted for ${currentVolunteer.user.name}!`);
} else {
// All volunteers reviewed
Expand Down Expand Up @@ -340,8 +342,8 @@ export default function RequestDetails() {
return;
}

if (!addressFields.city.trim() || !addressFields.district.trim()) {
Alert.alert('Validation Error', 'Please select a city and district for the address.');
if (!addressFields.city.trim() || !addressFields.state.trim()) {
Alert.alert('Validation Error', 'Please select a city and state for the address.');
return;
}

Expand Down Expand Up @@ -405,12 +407,12 @@ export default function RequestDetails() {
style: 'destructive',
onPress: async () => {
if (!id || !request) return;

setCompletingTask(true);
try {
const response = await completeTask(id);
Alert.alert('Success', response.message || 'Request marked as completed successfully!');

// Refresh task data to get updated status
await fetchTaskData();
} catch (err: any) {
Expand Down Expand Up @@ -439,12 +441,12 @@ export default function RequestDetails() {
style: 'destructive',
onPress: async () => {
if (!id || !request) return;

setCancellingTask(true);
try {
const response = await cancelTask(id);
Alert.alert('Success', response.message || 'Request deleted successfully!');

// Navigate back to feed
router.back();
} catch (err: any) {
Expand Down Expand Up @@ -582,36 +584,36 @@ export default function RequestDetails() {
const firstPhoto = photos[0];
const photoUrl = firstPhoto.photo_url || firstPhoto.url || firstPhoto.image || '';
console.log(photoUrl);
const absoluteUrl = photoUrl.startsWith('http')
? photoUrl
const absoluteUrl = photoUrl.startsWith('http')
? photoUrl
: `${BACKEND_BASE_URL}${photoUrl}`;
return (
<Image
source={{ uri: absoluteUrl }}
<Image
source={{ uri: absoluteUrl }}
style={styles.heroImage}
resizeMode="cover"
/>
);
})()}

{/* Show remaining photos as thumbnails if there are more */}
{photos.length > 1 && (
<View style={[styles.thumbnailsContainer, { backgroundColor: themeColors.lightGray }]}>
<ScrollView
horizontal
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.thumbnailsScrollContent}
>
{photos.slice(1).map((photo) => {
const photoUrl = photo.photo_url || photo.url || photo.image || '';
const absoluteUrl = photoUrl.startsWith('http')
? photoUrl
const absoluteUrl = photoUrl.startsWith('http')
? photoUrl
: `${BACKEND_BASE_URL}${photoUrl}`;

return (
<TouchableOpacity key={photo.id} style={[styles.smallThumbnail, { borderColor: themeColors.card }]}>
<Image
source={{ uri: absoluteUrl }}
<Image
source={{ uri: absoluteUrl }}
style={styles.smallThumbnailImage}
resizeMode="cover"
/>
Expand All @@ -625,7 +627,7 @@ export default function RequestDetails() {
) : (
<Image source={{ uri: imageUrl }} style={styles.heroImage} />
)}

<View style={[styles.section, { backgroundColor: themeColors.card }]}>
<Text style={[styles.sectionTitle, { color: themeColors.text }]}>Requester</Text>
<View style={styles.requesterRow}>
Expand Down Expand Up @@ -726,17 +728,26 @@ export default function RequestDetails() {
)}

{!isCreator && (
<TouchableOpacity
style={[styles.primaryButton, { backgroundColor: themeColors.primary }]}
onPress={() =>
router.push({
pathname: '/select-volunteer',
params: { id },
})
}
>
<Text style={[styles.buttonText, { color: themeColors.card }]}>Volunteer for this Request</Text>
</TouchableOpacity>
<>
<TouchableOpacity
style={[styles.primaryButton, { backgroundColor: themeColors.primary }]}
onPress={() =>
router.push({
pathname: '/select-volunteer',
params: { id },
})
}
>
<Text style={[styles.buttonText, { color: themeColors.card }]}>Volunteer for this Request</Text>
</TouchableOpacity>

<TouchableOpacity
style={[styles.primaryButton, { backgroundColor: 'transparent', borderWidth: 1, borderColor: themeColors.error, marginTop: 12 }]}
onPress={() => setReportModalVisible(true)}
>
<Text style={[styles.buttonText, { color: themeColors.error }]}>Report Request</Text>
</TouchableOpacity>
</>
)}

{isCreator && isCompleted && (
Expand All @@ -756,7 +767,7 @@ export default function RequestDetails() {
onPress={handleOpenReviewModal}
>
<Text style={[styles.buttonText, { color: themeColors.card }]}>
{hasReviewedAllVolunteers()
{hasReviewedAllVolunteers()
? `Edit Rate & Review ${numAssigned === 1 ? 'Volunteer' : 'Volunteers'}`
: `Rate & Review ${numAssigned === 1 ? 'Volunteer' : 'Volunteers'}`
}
Expand All @@ -766,20 +777,30 @@ export default function RequestDetails() {
)}
</ScrollView>

{request && (
<ReportModal
visible={reportModalVisible}
onClose={() => setReportModalVisible(false)}
targetId={request.id}
targetType="task"
targetName={request.title}
/>
)}

<Modal visible={modalVisible} animationType="slide" transparent>
<View style={[styles.modalOverlay, { backgroundColor: themeColors.overlay }]}>
<View style={[styles.modalContent, { backgroundColor: themeColors.card }]}>
<Text style={[styles.modalTitle, { color: themeColors.text }]}>
{isEdit
? 'Edit Request'
: assignedVolunteers.length > 0
{isEdit
? 'Edit Request'
: assignedVolunteers.length > 0
? (() => {
const currentVolunteer = assignedVolunteers[currentVolunteerIndex];
const existingReview = getExistingReviewForVolunteer(currentVolunteer?.user?.id);
return existingReview
? `Edit Rate & Review ${currentVolunteer?.user?.name || 'Volunteer'}`
: `Rate & Review ${currentVolunteer?.user?.name || 'Volunteer'}`;
})()
const currentVolunteer = assignedVolunteers[currentVolunteerIndex];
const existingReview = getExistingReviewForVolunteer(currentVolunteer?.user?.id);
return existingReview
? `Edit Rate & Review ${currentVolunteer?.user?.name || 'Volunteer'}`
: `Rate & Review ${currentVolunteer?.user?.name || 'Volunteer'}`;
})()
: 'Rate Request'
}
</Text>
Expand Down
31 changes: 19 additions & 12 deletions app/mobile/app/signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
ScrollView,
Platform
} from 'react-native';
import { login } from '../lib/api';
import { login, checkIsAdmin } from '../lib/api';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useAuth } from '../lib/auth';

Expand All @@ -42,13 +42,19 @@ export default function SignIn() {
console.log('Attempting login with:', { email });
const response = await login(email, password);
console.log('Login successful:', response);

// The login function already fetches and stores the user profile
// Just set the user in auth context and navigate
if (response.data?.user_id) {
await setUser({ id: response.data.user_id, email });

// Check if user is admin
console.log('Checking admin status...');
const isAdmin = await checkIsAdmin();
console.log('Admin status:', isAdmin);
await AsyncStorage.setItem('isAdmin', JSON.stringify(isAdmin));
}

// Navigate to feed
router.replace('/feed');
} catch (error: any) {
Expand Down Expand Up @@ -79,9 +85,10 @@ export default function SignIn() {
if (router.canGoBack()) {
router.back();
} else {
router.replace('/');}
router.replace('/');
}
}>
}
}>
<Ionicons name="arrow-back" size={24} color={colors.primary} />
<Text style={[styles.backText, { color: colors.primary }]}>Back</Text>
</TouchableOpacity>
Expand Down Expand Up @@ -186,7 +193,7 @@ const styles = StyleSheet.create({
header: {
marginBottom: 20, textAlign: 'auto'
},
title: {
title: {
fontSize: 32,
fontWeight: '700',
},
Expand All @@ -203,12 +210,12 @@ const styles = StyleSheet.create({
marginBottom: 16,
paddingBottom: 4,
},
input: {
input: {
flex: 1,
marginLeft: 8,
height: 40,
},
rememberWrapper: {
rememberWrapper: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 24,
Expand All @@ -226,19 +233,19 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
fontSize: 16,
},
forgotText: {
forgotText: {
textAlign: 'center',
fontSize: 14,
marginBottom: 144,
},
signupPrompt: {
signupPrompt: {
flexDirection: 'row',
justifyContent: 'center',
},
promptText: {
promptText: {
fontSize: 14,
},
promptLink: {
promptLink: {
fontSize: 14,
fontWeight: '500',
},
Expand Down
Loading