Skip to content

Commit c31be7a

Browse files
committed
feat(core): add service health context, stabilize report flow, and introduce report analytics modal
1 parent 292ae78 commit c31be7a

14 files changed

Lines changed: 676 additions & 278 deletions

File tree

mobile/App.jsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Popup from './src/components/Popup.jsx';
1818
import GlobalLoader from './src/components/Loader.jsx';
1919
import CameraView from './src/components/CameraView.jsx';
2020
import { CameraProvier, useCamera } from './src/context/Camera.context.js';
21+
import { HealthProvider } from './src/context/Health.context.js';
2122

2223
const MainApp = () => {
2324
const { user, setUser } = useAuth();
@@ -64,9 +65,11 @@ const App = () => {
6465
<PopupProvider>
6566
<AuthProvider>
6667
<LoaderProvider>
67-
<CameraProvier>
68-
<MainApp />
69-
</CameraProvier>
68+
<HealthProvider>
69+
<CameraProvier>
70+
<MainApp />
71+
</CameraProvier>
72+
</HealthProvider>
7073
</LoaderProvider>
7174
</AuthProvider>
7275
</PopupProvider>

mobile/android/.kotlin/sessions/kotlin-compiler-16608453380749593125.salive

Whitespace-only changes.
Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
22

3-
<uses-permission android:name="android.permission.INTERNET" />
4-
<uses-permission android:name="android.permission.CAMERA" />
3+
<uses-permission android:name="android.permission.INTERNET" />
4+
<uses-permission android:name="android.permission.CAMERA" />
55

6-
<application
7-
android:name=".MainApplication"
6+
<application
7+
android:name=".MainApplication"
8+
android:label="@string/app_name"
9+
android:icon="@mipmap/ic_launcher"
10+
android:roundIcon="@mipmap/ic_launcher_round"
11+
android:allowBackup="false"
12+
android:theme="@style/AppTheme"
13+
android:requestLegacyExternalStorage="true"
14+
android:usesCleartextTraffic="true"
15+
android:supportsRtl="true">
16+
<activity
17+
android:name=".MainActivity"
818
android:label="@string/app_name"
9-
android:icon="@mipmap/ic_launcher"
10-
android:roundIcon="@mipmap/ic_launcher_round"
11-
android:allowBackup="false"
12-
android:theme="@style/AppTheme"
13-
android:usesCleartextTraffic="true"
14-
android:supportsRtl="true">
15-
<activity
16-
android:name=".MainActivity"
17-
android:label="@string/app_name"
18-
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
19-
android:launchMode="singleTask"
20-
android:windowSoftInputMode="adjustResize"
21-
android:exported="true">
22-
<intent-filter>
23-
<action android:name="android.intent.action.MAIN" />
24-
<category android:name="android.intent.category.LAUNCHER" />
25-
</intent-filter>
26-
</activity>
27-
</application>
28-
</manifest>
19+
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
20+
android:launchMode="singleTask"
21+
android:windowSoftInputMode="adjustResize"
22+
android:exported="true">
23+
<intent-filter>
24+
<action android:name="android.intent.action.MAIN" />
25+
<category android:name="android.intent.category.LAUNCHER" />
26+
</intent-filter>
27+
</activity>
28+
</application>
29+
</manifest>

mobile/src/apis/api.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import axios from 'axios';
2+
import AsyncStorage from '@react-native-async-storage/async-storage';
3+
import { FARM_API_URL } from '@env';
4+
5+
const api = axios.create({
6+
baseURL: FARM_API_URL,
7+
});
8+
9+
api.interceptors.request.use(
10+
async config => {
11+
try {
12+
const token = await AsyncStorage.getItem('accessToken');
13+
if (token) {
14+
config.headers.Authorization = `Bearer ${token}`;
15+
}
16+
} catch (error) {
17+
console.error('Token retrieval failed', error);
18+
}
19+
return config;
20+
},
21+
error => {
22+
return Promise.reject(error);
23+
},
24+
);
25+
26+
export default api;

mobile/src/apis/farm.js

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,33 @@
1-
import axios from 'axios';
2-
import AsyncStorage from '@react-native-async-storage/async-storage';
3-
import { FARM_API_URL } from '@env';
1+
import api from './api';
42

5-
const fetchFarms = async showPopup => {
3+
export const fetchFarms = async (showPopup) => {
64
try {
7-
const token = await AsyncStorage.getItem('accessToken');
8-
const response = await axios.get(FARM_API_URL, {
9-
headers: {
10-
Authorization: `Bearer ${token}`,
11-
},
12-
});
13-
return response.data.data.docs;
5+
const res = await api.get('/');
6+
return res.data.data.docs;
147
} catch (error) {
15-
console.error('Failed to fetch farms:', error);
16-
showPopup({ success: false, msg: 'failed to fetch Farms' });
8+
showPopup({ success: false, msg: 'Failed to fetch farms' });
179
return [];
1810
}
1911
};
2012

21-
const addFarm = async (form, showPopup) => {
13+
export const addFarm = async (form, showPopup) => {
2214
try {
23-
const token = await AsyncStorage.getItem('accessToken');
24-
const response = await axios.post(FARM_API_URL, form, {
25-
headers: {
26-
Authorization: `Bearer ${token}`,
27-
},
28-
});
29-
showPopup({ success: true, msg: 'farm added successfully' });
30-
return response.data.data;
15+
const res = await api.post('/', form);
16+
showPopup({ success: true, msg: 'Farm added successfully' });
17+
return res.data.data;
3118
} catch (error) {
32-
showPopup({ success: false, msg: 'Failed to add Farm' });
19+
showPopup({ success: false, msg: 'Failed to add farm' });
3320
throw error;
3421
}
3522
};
3623

37-
const deleteFarm = async (farmId, showPopup) => {
24+
export const deleteFarm = async (farmId, showPopup) => {
3825
try {
39-
const token = await AsyncStorage.getItem('accessToken');
40-
const response = await axios.delete(`${FARM_API_URL}/${farmId}`, {
41-
headers: {
42-
Authorization: `Bearer ${token}`,
43-
},
44-
});
26+
const res = await api.delete(`/${farmId}`);
4527
showPopup({ success: true, msg: 'Farm deleted' });
46-
return response.data.data;
28+
return res.data.data;
4729
} catch (err) {
4830
showPopup({ success: false, msg: 'Failed to delete' });
4931
throw err;
5032
}
51-
};
52-
53-
export { fetchFarms, addFarm, deleteFarm };
33+
};

mobile/src/apis/report.js

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,34 @@
1-
import axios from 'axios';
2-
import AsyncStorage from '@react-native-async-storage/async-storage';
3-
import { FARM_API_URL } from '@env';
1+
import api from './api';
42

53
const fetchReports = async farmId => {
64
try {
7-
const token = await AsyncStorage.getItem('accessToken');
8-
const res = await axios.get(`${FARM_API_URL}/${farmId}/reports`, {
9-
headers: {
10-
Authorization: `Bearer ${token}`,
11-
},
12-
});
5+
const res = await api.get(`/${farmId}/reports`);
136
return res.data.data.docs;
147
} catch (err) {
15-
console.error('Failed to fetch farms:', err);
16-
showPopup({ success: false, msg: 'failed to fetch Farms' });
8+
console.error('Fetch error:', err.message);
179
return [];
1810
}
1911
};
2012

2113
const createReport = async (farmId, photoPath) => {
2214
const formData = new FormData();
15+
2316
formData.append('symptomsImg', {
2417
uri: photoPath,
2518
type: 'image/jpeg',
26-
fileName: 'symptoms.jpg',
19+
name: 'symptoms.jpg',
2720
});
2821

29-
console.log(formData);
30-
console.log(photoPath);
31-
console.log(farmId);
32-
3322
try {
34-
const token = await AsyncStorage.getItem('accessToken');
35-
const res = await axios.post(
36-
`${FARM_API_URL}/${farmId}/reports`,
37-
formData,
38-
{
39-
headers: {
40-
Authorization: `Bearer ${token}`,
41-
},
42-
transformRequest: x => x,
23+
const res = await api.post(`/${farmId}/reports`, formData, {
24+
headers: {
25+
'Content-Type': 'multipart/form-data',
4326
},
44-
);
45-
console.log(res);
46-
27+
});
4728
return res.data.data;
4829
} catch (err) {
49-
console.log(err);
50-
console.error('Upload error:', err.response?.data || err.message);
30+
console.log('Error Source:', err.response?.headers['server'] || 'Unknown');
31+
console.log('Error Data:', err.response?.data);
5132
throw err;
5233
}
5334
};

mobile/src/components/CameraView.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Camera,
1010
useCameraDevice,
1111
useCameraPermission,
12+
useCameraFormat,
1213
} from 'react-native-vision-camera';
1314
import { useEffect, useRef, useState } from 'react';
1415
import Ionicons from 'react-native-vector-icons/Ionicons';
@@ -21,6 +22,8 @@ const CameraView = ({ onPhotoTaken, onClose }) => {
2122
const device = useCameraDevice('back');
2223
const { hasPermission, requestPermission } = useCameraPermission();
2324

25+
const format = useCameraFormat(device, [{photoResolution: {width: 1280, height: 720}}])
26+
2427
useEffect(() => {
2528
if (hasPermission === 'not-determined' || 'denied') {
2629
requestPermission();
@@ -30,7 +33,7 @@ const CameraView = ({ onPhotoTaken, onClose }) => {
3033
const capturePicture = async () => {
3134
if (camera.current) {
3235
try {
33-
const photo = await camera.current.takePhoto({ flash });
36+
const photo = await camera.current.takeSnapshot({ flash, quality: 85 });
3437
if (onPhotoTaken) {
3538
onPhotoTaken(photo);
3639
}
@@ -85,6 +88,7 @@ const CameraView = ({ onPhotoTaken, onClose }) => {
8588
device={device}
8689
isActive={true}
8790
photo={true}
91+
format={format}
8892
/>
8993
<TouchableOpacity style={styles.captureBtn} onPress={capturePicture}>
9094
<Ionicons size={36} name="camera" />

mobile/src/components/Card.jsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import FeatherIcon from 'react-native-vector-icons/Feather.js';
55

66
const Card = ({ item, handleDelete, navigation }) => {
77
const colors = useTheme();
8-
8+
99
return (
1010
<TouchableOpacity
1111
activeOpacity={0.9}
@@ -20,28 +20,37 @@ const Card = ({ item, handleDelete, navigation }) => {
2020
</Text>
2121

2222
<View style={[styles.badge, { backgroundColor: colors.appBg }]}>
23-
<Text style={styles.badgeText}>
24-
{item.type === 'pig' ? '🐷 Swine' : '🐔 Poultry'}
23+
<Text>{item.type === 'pig' ? '🐷' : '🐔'}</Text>
24+
<Text style={[styles.badgeText, { color: colors.textPrimary }]}>
25+
{item.type}
2526
</Text>
2627
</View>
2728
</View>
2829

2930
<View style={styles.statsRow}>
3031
<Text style={[styles.infoText, { color: colors.textSecondary }]}>
31-
Population: <Text style={{ color: colors.textPrimary }}>{item.size}</Text>
32+
Population:{' '}
33+
<Text style={{ color: colors.textPrimary }}>{item.size}</Text>
3234
</Text>
3335
</View>
3436

3537
<View style={styles.locationRow}>
36-
<FeatherIcon name="map-pin" size={14} color={colors.textSecondary} />
37-
<Text numberOfLines={1} style={[styles.locationText, { color: colors.textSecondary }]}>
38+
<FeatherIcon
39+
name="map-pin"
40+
size={14}
41+
color={colors.textSecondary}
42+
/>
43+
<Text
44+
numberOfLines={1}
45+
style={[styles.locationText, { color: colors.textSecondary }]}
46+
>
3847
{item.location}
3948
</Text>
4049
</View>
4150
</View>
4251

4352
<TouchableOpacity
44-
style={[styles.deleteBtn, { backgroundColor : colors.appBg }]}
53+
style={[styles.deleteBtn, { backgroundColor: colors.appBg }]}
4554
onPress={() => handleDelete(item)}
4655
>
4756
<FeatherIcon name="trash-2" size={18} color={colors.textPrimary} />
@@ -62,7 +71,7 @@ const styles = StyleSheet.create({
6271
shadowOpacity: 0.1,
6372
shadowRadius: 6,
6473
marginHorizontal: 2,
65-
overflow: 'hidden',
74+
overflow: 'hidden',
6675
},
6776
cardContent: {
6877
padding: 16,
@@ -77,17 +86,24 @@ const styles = StyleSheet.create({
7786
headerRow: {
7887
flexDirection: 'row',
7988
alignItems: 'center',
89+
justifyContent: 'space-between',
8090
gap: 10,
8191
marginBottom: 12,
92+
paddingRight: 60,
8293
},
8394
farmName: {
8495
fontSize: 18,
8596
fontFamily: 'Lato-Bold',
97+
flex: 1,
8698
},
8799
badge: {
88100
paddingHorizontal: 8,
89101
paddingVertical: 2,
90102
borderRadius: 6,
103+
flexShrink: 0,
104+
flexDirection: 'row',
105+
alignItems: 'center',
106+
gap: 6,
91107
},
92108
badgeText: {
93109
fontSize: 12,
@@ -119,4 +135,4 @@ const styles = StyleSheet.create({
119135
fontSize: 14,
120136
fontFamily: 'Lato-Regular',
121137
},
122-
});
138+
});

mobile/src/components/FarmInput.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export default FarmInput;
151151
const styles = StyleSheet.create({
152152
overlay: {
153153
...StyleSheet.absoluteFillObject,
154-
justifyContent: 'center',
154+
paddingTop: 26,
155155
alignItems: 'center',
156156
zIndex: 20,
157157
backgroundColor: 'rgba(0,0,0,0.2)',

mobile/src/components/ProfileForm.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ export default ProfileForm;
123123
const styles = StyleSheet.create({
124124
overlay: {
125125
...StyleSheet.absoluteFillObject,
126-
justifyContent: 'center',
126+
// justifyContent: 'center',
127+
paddingTop: 26,
127128
alignItems: 'center',
128129
zIndex: 10,
129130
backgroundColor: 'rgba(0,0,0,0.1)',

0 commit comments

Comments
 (0)