diff --git a/App.js b/App.js
index b26ba67..caf9a2c 100644
--- a/App.js
+++ b/App.js
@@ -9,7 +9,7 @@ import StackNavigator from './src/navigation/StackNavigator';
import { store } from './src/redux/store';
import { supabase } from './src/integrations/supabase/client';
-import messaging from '@react-native-firebase/messaging';
+import { getMessaging, getToken, onMessage } from '@react-native-firebase/messaging';
import notifee, { EventType } from '@notifee/react-native';
import RingtoneService from './src/notifications/RingtoneService';
@@ -19,7 +19,7 @@ import {
} from './src/notifications/notificationService';
import IncomingWorkOverlay from './src/components/IncomingWorkOverlay';
-import { clearIncomingWork } from './src/redux/workSlice';
+import { clearIncomingWork, showIncomingWork } from './src/redux/workSlice';
function AppContainer() {
const dispatch = useDispatch();
@@ -37,16 +37,22 @@ function AppContainer() {
}
// FCM Token (future backend)
- const token = await messaging().getToken();
- console.log('FCM TOKEN:', token);
+ try {
+ const token = await getToken(getMessaging());
+ console.log('FCM TOKEN SUCCESS:', token);
+ } catch (error) {
+ console.warn('FCM TOKEN ERROR (SERVICE_NOT_AVAILABLE probable):', error.message);
+ // We log it as a warning so it doesn't break the app flow
+ }
await setupNotificationChannel();
// Foreground FCM listener (future use)
- messaging().onMessage(async remoteMessage => {
+ onMessage(getMessaging(), async remoteMessage => {
if (remoteMessage.data?.type === 'NEW_WORK') {
await showIncomingWorkNotification(remoteMessage.data);
RingtoneService.startRingtone();
+ dispatch(showIncomingWork(remoteMessage.data));
}
});
@@ -59,6 +65,11 @@ function AppContainer() {
if (notificationId) {
await notifee.cancelNotification(notificationId);
}
+
+ // If detail.notification.data exists, show the overlay
+ if (detail.notification?.data) {
+ dispatch(showIncomingWork(detail.notification.data));
+ }
}
});
};
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 9a2ab13..f43d140 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -78,6 +78,7 @@ android {
compileSdk rootProject.ext.compileSdkVersion
namespace "com.workers"
+
defaultConfig {
applicationId "com.workers"
minSdkVersion rootProject.ext.minSdkVersion
@@ -85,7 +86,7 @@ android {
versionCode 4
versionName "1.0.3"
}
-
+
signingConfigs {
debug {
storeFile file('debug.keystore')
@@ -95,26 +96,33 @@ android {
}
release {
- storeFile file(GOBUILD_UPLOAD_STORE_FILE)
- storePassword GOBUILD_UPLOAD_STORE_PASSWORD
- keyAlias GOBUILD_UPLOAD_KEY_ALIAS
- keyPassword GOBUILD_UPLOAD_KEY_PASSWORD
- }
-
+ storeFile file(GOBUILD_UPLOAD_STORE_FILE)
+ storePassword GOBUILD_UPLOAD_STORE_PASSWORD
+ keyAlias GOBUILD_UPLOAD_KEY_ALIAS
+ keyPassword GOBUILD_UPLOAD_KEY_PASSWORD
+ }
}
- buildTypes {
- debug {
- signingConfig signingConfigs.debug
+
+ buildTypes {
+ debug {
+ signingConfig signingConfigs.debug
+ }
+ release {
+ signingConfig signingConfigs.release
+ minifyEnabled false
+ shrinkResources false
+ proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
+ }
}
- release {
- signingConfig signingConfigs.release
- minifyEnabled false
- shrinkResources false
- proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
+
+ packagingOptions {
+ pickFirst 'lib/arm64-v8a/libworklets.so'
+ pickFirst 'lib/armeabi-v7a/libworklets.so'
+ pickFirst 'lib/x86/libworklets.so'
+ pickFirst 'lib/x86_64/libworklets.so'
}
}
-}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
diff --git a/index.js b/index.js
index 759bf6e..c71f5a4 100644
--- a/index.js
+++ b/index.js
@@ -1,6 +1,6 @@
import 'react-native-gesture-handler';
import { AppRegistry } from 'react-native';
-import messaging from '@react-native-firebase/messaging';
+import { getMessaging, setBackgroundMessageHandler } from '@react-native-firebase/messaging';
import notifee, { EventType } from '@notifee/react-native';
import App from './App';
import { name as appName } from './app.json';
@@ -8,7 +8,7 @@ import { showIncomingWorkNotification } from './src/notifications/notificationSe
import RingtoneService from './src/notifications/RingtoneService';
// Background FCM handler
-messaging().setBackgroundMessageHandler(async remoteMessage => {
+setBackgroundMessageHandler(getMessaging(), async remoteMessage => {
if (remoteMessage.data?.type === 'NEW_WORK') {
console.log('Background FCM received', remoteMessage.data);
const notificationId = await showIncomingWorkNotification(remoteMessage.data);
diff --git a/package-lock.json b/package-lock.json
index ca9d4e4..597e899 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@notifee/react-native": "^9.1.8",
"@react-native-async-storage/async-storage": "^2.2.0",
+ "@react-native-community/geolocation": "^3.4.0",
"@react-native-firebase/app": "^23.8.3",
"@react-native-firebase/messaging": "^23.8.3",
"@react-native/new-app-screen": "0.82.1",
@@ -24,6 +25,7 @@
"buffer": "^6.0.3",
"react": "19.1.1",
"react-native": "0.82.1",
+ "react-native-calendars": "^1.1314.0",
"react-native-dotenv": "^3.4.11",
"react-native-flash-message": "^0.4.2",
"react-native-fs": "^2.20.0",
@@ -49,7 +51,7 @@
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
- "@react-native-community/cli": "20.0.0",
+ "@react-native-community/cli": "^20.0.0",
"@react-native-community/cli-platform-android": "20.0.0",
"@react-native-community/cli-platform-ios": "20.0.0",
"@react-native/babel-preset": "0.82.1",
@@ -3693,6 +3695,19 @@
"node": ">=10"
}
},
+ "node_modules/@react-native-community/geolocation": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@react-native-community/geolocation/-/geolocation-3.4.0.tgz",
+ "integrity": "sha512-bzZH89/cwmpkPMKKveoC72C4JH0yF4St5Ceg/ZM9pA1SqX9MlRIrIrrOGZ/+yi++xAvFDiYfihtn9TvXWU9/rA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/@react-native-firebase/app": {
"version": "23.8.3",
"resolved": "https://registry.npmjs.org/@react-native-firebase/app/-/app-23.8.3.tgz",
@@ -9678,7 +9693,6 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true,
"license": "MIT"
},
"node_modules/lodash.camelcase": {
@@ -9691,7 +9705,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
@@ -10408,6 +10421,16 @@
"node": ">=10"
}
},
+ "node_modules/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -11336,6 +11359,27 @@
}
}
},
+ "node_modules/react-native-calendars": {
+ "version": "1.1314.0",
+ "resolved": "https://registry.npmjs.org/react-native-calendars/-/react-native-calendars-1.1314.0.tgz",
+ "integrity": "sha512-4DLAVto8Qo9L3ggL2vsY9Gk8FFpJWtne8F/3wN8yUb7Xha9/SKS4B+vs7xlhWjKeqZUHws/Vi/q/6IZ8s60kcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "hoist-non-react-statics": "^3.3.1",
+ "lodash": "^4.17.15",
+ "memoize-one": "^5.2.1",
+ "prop-types": "^15.5.10",
+ "react-native-swipe-gestures": "^1.0.5",
+ "recyclerlistview": "^4.0.0",
+ "xdate": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "moment": "^2.29.4"
+ }
+ },
"node_modules/react-native-dotenv": {
"version": "3.4.11",
"resolved": "https://registry.npmjs.org/react-native-dotenv/-/react-native-dotenv-3.4.11.tgz",
@@ -11558,6 +11602,12 @@
"react-native": "*"
}
},
+ "node_modules/react-native-swipe-gestures": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.5.tgz",
+ "integrity": "sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw==",
+ "license": "MIT"
+ },
"node_modules/react-native-url-polyfill": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-3.0.0.tgz",
@@ -11813,6 +11863,21 @@
"node": ">= 6"
}
},
+ "node_modules/recyclerlistview": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/recyclerlistview/-/recyclerlistview-4.2.3.tgz",
+ "integrity": "sha512-STR/wj/FyT8EMsBzzhZ1l2goYirMkIgfV3gYEPxI3Kf3lOnu6f7Dryhyw7/IkQrgX5xtTcDrZMqytvteH9rL3g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash.debounce": "4.0.8",
+ "prop-types": "15.8.1",
+ "ts-object-utils": "0.0.5"
+ },
+ "peerDependencies": {
+ "react": ">= 15.2.1",
+ "react-native": ">= 0.30.0"
+ }
+ },
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
@@ -13080,6 +13145,12 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/ts-object-utils": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz",
+ "integrity": "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==",
+ "license": "ISC"
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -13695,6 +13766,12 @@
"async-limiter": "~1.0.0"
}
},
+ "node_modules/xdate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/xdate/-/xdate-0.8.3.tgz",
+ "integrity": "sha512-1NhJWPJwN+VjbkACT9XHbQK4o6exeSVtS2CxhMPwUE7xQakoEFTlwra9YcqV/uHQVyeEUYoYC46VGDJ+etnIiw==",
+ "license": "(MIT OR GPL-2.0)"
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/package.json b/package.json
index 4adea31..aff88e3 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"dependencies": {
"@notifee/react-native": "^9.1.8",
"@react-native-async-storage/async-storage": "^2.2.0",
+ "@react-native-community/geolocation": "^3.4.0",
"@react-native-firebase/app": "^23.8.3",
"@react-native-firebase/messaging": "^23.8.3",
"@react-native/new-app-screen": "0.82.1",
@@ -26,6 +27,7 @@
"buffer": "^6.0.3",
"react": "19.1.1",
"react-native": "0.82.1",
+ "react-native-calendars": "^1.1314.0",
"react-native-dotenv": "^3.4.11",
"react-native-flash-message": "^0.4.2",
"react-native-fs": "^2.20.0",
@@ -51,7 +53,7 @@
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
- "@react-native-community/cli": "20.0.0",
+ "@react-native-community/cli": "^20.0.0",
"@react-native-community/cli-platform-android": "20.0.0",
"@react-native-community/cli-platform-ios": "20.0.0",
"@react-native/babel-preset": "0.82.1",
diff --git a/src/api/Api.js b/src/api/Api.js
new file mode 100644
index 0000000..2f8c917
--- /dev/null
+++ b/src/api/Api.js
@@ -0,0 +1,7 @@
+import axios from "axios";
+
+const Api = axios.create({
+ baseURL: "http://192.168.29.153:3000",
+ timeout: 5000,
+});
+export default Api;
diff --git a/src/screens/AuthScreens/Login.js b/src/screens/AuthScreens/Login.js
index 1bd28d2..d38c48d 100644
--- a/src/screens/AuthScreens/Login.js
+++ b/src/screens/AuthScreens/Login.js
@@ -306,7 +306,6 @@ const styles = StyleSheet.create({
marginBottom: 20,
gap: 10,
top: 10,
- marginBottom: 30
},
loginText: {
diff --git a/src/screens/HomeScreen/HomeScreen.js b/src/screens/HomeScreen/HomeScreen.js
index 2d319f7..df29c70 100644
--- a/src/screens/HomeScreen/HomeScreen.js
+++ b/src/screens/HomeScreen/HomeScreen.js
@@ -7,24 +7,106 @@ import {
Image,
StatusBar,
FlatList,
- TouchableOpacity
+ TouchableOpacity,
+ Animated,
+ Easing,
+ Pressable,
+ RefreshControl,
+ PermissionsAndroid,
+ Platform
} from "react-native";
+import Geolocation from '@react-native-community/geolocation';
+import { ensureLocationPermission, checkLocationService } from "../../utils/permissionUtils";
import Icon from "react-native-vector-icons/Feather";
import LinearGradient from "react-native-linear-gradient";
import { useNavigation, DrawerActions } from "@react-navigation/native";
+import Api from "../../api/Api";
import { SafeAreaView } from "react-native-safe-area-context";
import YoutubePlayer from "react-native-youtube-iframe";
import TopWorkers from "../TopWorkers/TopWorkers";
import { SCREENS } from "../../navigation/StringNavigator";
import stringsoflanguages from "../../components/Language/ConnectLang";
import { useSelector } from "react-redux";
+import { styles } from "./homeScreenStyles";
+import { SafetyItem, TaskItem, TrainingItem } from "./homeScreenModals";
const HomeScreen = () => {
const navigation = useNavigation();
const [notificationCount] = useState(3);
-
const user = useSelector(state => state.auth.user);
+ const [isActive, setIsActive] = useState(user?.isActive || false);
+ const [isLoadingToggle, setIsLoadingToggle] = useState(false);
+ const [isRefreshing, setIsRefreshing] = useState(false); // Used for Pull-to-Refresh
+
+ // Animation specific state
+ const slideAnimation = React.useRef(new Animated.Value(user?.isActive ? 1 : 0)).current;
+
+ // Sync animated slide value directly when isActive initially loads or changes externally
+ React.useEffect(() => {
+ slideAnimation.setValue(isActive ? 1 : 0);
+ }, [isActive]);
+
+ const [isHovered, setIsHovered] = useState(false); // Used to expand for Text display
+
+ // Request permission for location on load
+ React.useEffect(() => {
+ requestLocationPermission();
+
+ // Setup the recurring 10-minute timer for geolocation updates
+ const locationInterval = setInterval(() => {
+ updateLiveLocation();
+ }, 10 * 60 * 1000); // 10 minutes
+
+ return () => clearInterval(locationInterval); // clear interval on unmount
+ }, []);
+
+ const requestLocationPermission = async () => {
+ const granted = await ensureLocationPermission();
+ if (granted) {
+ updateLiveLocation();
+ } else {
+ console.log('Location permission denied');
+ }
+ };
+
+ const updateLiveLocation = async () => {
+ if (!user?.id) return;
+
+ const hasPermission = await ensureLocationPermission();
+ if (!hasPermission) return;
+
+ // If permission is granted, verify the service is actually ON
+ const isServiceOn = await checkLocationService();
+ if (!isServiceOn) return;
+
+ Geolocation.getCurrentPosition(
+ async position => {
+ const { latitude, longitude } = position.coords;
+ try {
+ await Api.post('/workers/location', {
+ worker_id: user.id,
+ lat: latitude,
+ lng: longitude
+ });
+ console.log(`Live location updated: ${latitude}, ${longitude}`);
+ } catch (error) {
+ console.log('Error updating live location:', error);
+ }
+ },
+ error => console.log('Geolocation Error:', error.message),
+ { enableHighAccuracy: false, timeout: 30000, maximumAge: 10000 }
+ );
+ };
+
+ const onRefresh = React.useCallback(async () => {
+ setIsRefreshing(true);
+ // Force an immediate location update
+ await updateLiveLocation();
+ // Mock a slight delay so the spinner shows clearly for the user
+ setTimeout(() => setIsRefreshing(false), 1000);
+ }, [user?.id]);
+
/* DATA */
@@ -87,55 +169,70 @@ const HomeScreen = () => {
},
];
- /*RENDER FUNCTIONS*/
- const renderSafetyItem = ({ item }) => {
- return (
- navigation.navigate(item.screen)}
- >
-
- {item.title}
- {item.desc}
-
- );
+ const toggleStatus = async () => {
+ if (isLoadingToggle) return; // Prevent multiple requests
+
+ const newStatus = !isActive;
+ setIsLoadingToggle(true);
+ setIsActive(newStatus); // Optimistic update
+
+ // Animate toggle
+ Animated.timing(slideAnimation, {
+ toValue: newStatus ? 1 : 0,
+ duration: 250,
+ easing: Easing.bezier(0.25, 0.1, 0.25, 1),
+ useNativeDriver: false,
+ }).start();
+
+ try {
+ const response = await Api.post(`/workers/toggle-status/${user.id}`);
+
+ if (response.data && response.data.success !== undefined && !response.data.success) {
+ // Revert if explicit failure
+ setIsActive(!newStatus);
+ Animated.timing(slideAnimation, {
+ toValue: !newStatus ? 1 : 0,
+ duration: 250,
+ useNativeDriver: false,
+ }).start();
+ }
+ } catch (error) {
+ console.log("Error toggling status:", error);
+ // Properly revert the optimistic update
+ setIsActive(!newStatus);
+ Animated.timing(slideAnimation, {
+ toValue: !newStatus ? 1 : 0,
+ duration: 250,
+ useNativeDriver: false,
+ }).start();
+ } finally {
+ setIsLoadingToggle(false);
+ }
};
- const renderTaskItem = ({ item }) => (
-
-
-
-
-
-
- {item.title}
- Due: {item.date}
-
+ // Animation configurations
+ const trackColor = slideAnimation.interpolate({
+ inputRange: [0, 1],
+ outputRange: ["#E0E0E0", "#4CAF50"] // Gray -> Green
+ });
-
-
- );
+ const thumbTranslateX = slideAnimation.interpolate({
+ inputRange: [0, 1],
+ outputRange: [2, isHovered ? 78 : 32] // Moves according to width (Expanded vs Collapsed)
+ });
- const renderTrainingItem = ({ item }) => (
-
-
-
-
+ const containerWidth = isHovered ? 110 : 64; // Expanding width container
- {item.title}
- {item.time}
-
- );
return (
-
+
+ }
+ >
@@ -176,20 +273,54 @@ const HomeScreen = () => {
{/* Greeting */}
+
+ {user?.image ? (
+
+ ) : (
+
+ )}
- {user?.image ? (
-
- ) : (
-
- )}
-
-
- Good Morning,
- {user.Name}
+
+ Good Morning,
+ {user.Name}
+
+
+ !isLoadingToggle && setIsHovered(true)}
+ onPressOut={() => setIsHovered(false)}
+ style={[styles.customToggleOuterContainer, { opacity: isLoadingToggle ? 0.7 : 1 }]}
+ disabled={isLoadingToggle}
+ >
+
+
+ {isHovered && !isLoadingToggle && (
+
+ {isActive ? "ACTIVE" : "INACTIVE"}
+
+ )}
+
+
+
+
+
+
{/* My Tasks */}
@@ -199,7 +330,7 @@ const HomeScreen = () => {
}
keyExtractor={(item) => item.id}
scrollEnabled={false}
/>
@@ -215,7 +346,7 @@ const HomeScreen = () => {
}
keyExtractor={(item) => item.id}
horizontal
showsHorizontalScrollIndicator={false}
@@ -228,7 +359,7 @@ const HomeScreen = () => {
navigation.navigate(item.screen)} />}
keyExtractor={(item) => item.id}
numColumns={2}
columnWrapperStyle={{ justifyContent: "space-between" }}
@@ -241,197 +372,3 @@ const HomeScreen = () => {
};
export default HomeScreen;
-
-const styles = StyleSheet.create({
-
- container: {
- flex: 1,
- backgroundColor: "#FFF",
- },
-
- header: {
- paddingTop: 50,
- paddingBottom: 15,
- paddingHorizontal: 20,
- },
-
- headerRow: {
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "space-between",
- },
-
- logoText: {
- fontSize: 24,
- fontWeight: "bold",
- color: "#FFF",
- },
-
- logoHighlight: {
- color: "#FFD54F",
- },
-
- notificationWrapper: {
- position: "relative",
- },
-
- badge: {
- position: "absolute",
- top: -6,
- right: -6,
- backgroundColor: "red",
- minWidth: 18,
- height: 18,
- borderRadius: 9,
- justifyContent: "center",
- alignItems: "center",
- },
-
- badgeText: {
- color: "#fff",
- fontSize: 10,
- fontWeight: "bold",
- },
-
- whiteCard: {
- backgroundColor: "#fff",
- borderTopLeftRadius: 30,
- borderTopRightRadius: 30,
- padding: 20,
- paddingTop: 30,
- },
-
- greetingRow: {
- flexDirection: "row",
- alignItems: "center",
- marginBottom: 20,
- },
-
- goodMorning: {
- fontSize: 15,
- color: "#444",
- },
-
- username: {
- fontSize: 18,
- fontWeight: "bold",
- },
-
- sectionHeader: {
- flexDirection: "row",
- justifyContent: "space-between",
- marginTop: 20,
- },
-
- sectionTitle: {
- fontSize: 18,
- fontWeight: "bold",
- marginBottom: 10
- },
-
- taskCard: {
- flexDirection: "row",
- alignItems: "center",
- padding: 12,
- backgroundColor: "#F8F8F8",
- borderRadius: 12,
- marginTop: 12,
- },
-
- taskIconBox: {
- width: 40,
- height: 40,
- borderRadius: 10,
- backgroundColor: "#1E3C72",
- justifyContent: "center",
- alignItems: "center",
- marginRight: 12,
- },
-
- taskTitle: {
- fontSize: 15,
- fontWeight: "600",
- },
-
- taskDate: {
- fontSize: 12,
- color: "#777",
- },
-
- checkBox: {
- width: 20,
- height: 20,
- borderRadius: 6,
- borderWidth: 1.5,
- borderColor: "#aaa",
- },
-
- profileImage: {
- width: 50,
- height: 50,
- borderRadius: 30,
- },
-
- videoCard: {
- width: 180,
- marginRight: 15,
- marginTop: 15,
- },
-
- videoImage: {
- height: 110,
- width: "100%",
- borderRadius: 12,
- },
-
- playBtn: {
- position: "absolute",
- top: 35,
- left: 70,
- },
-
- videoTitle: {
- marginTop: 5,
- fontWeight: "600",
- },
-
- videoTime: {
- fontSize: 12,
- color: "#777",
- },
-
- trainingVideoCard: {
- width: 250,
- marginRight: 15,
- marginTop: 15,
- backgroundColor: "#F8F8F8",
- borderRadius: 14,
- padding: 8,
- },
-
- youtubeWrapper: {
- borderRadius: 12,
- overflow: "hidden",
- },
-
-
- gridCard: {
- width: "47%",
- backgroundColor: "#F8F8F8",
- padding: 15,
- borderRadius: 15,
- marginBottom: 15,
- },
-
- gridTitle: {
- marginTop: 10,
- fontWeight: "700",
- },
-
- gridDesc: {
- fontSize: 12,
- color: "#777",
- marginTop: 3,
- },
-
-});
diff --git a/src/screens/HomeScreen/homeScreenModals.js b/src/screens/HomeScreen/homeScreenModals.js
new file mode 100644
index 0000000..c7be34f
--- /dev/null
+++ b/src/screens/HomeScreen/homeScreenModals.js
@@ -0,0 +1,50 @@
+import React from "react";
+import { View, Text, TouchableOpacity } from "react-native";
+import Icon from "react-native-vector-icons/Feather";
+import YoutubePlayer from "react-native-youtube-iframe";
+import { styles } from "./homeScreenStyles";
+
+export const SafetyItem = ({ item, onPress }) => {
+ return (
+
+
+ {item.title}
+ {item.desc}
+
+ );
+};
+
+export const TaskItem = ({ item }) => (
+
+
+
+
+
+
+ {item.title}
+ Due: {item.date}
+
+
+
+
+);
+
+export const TrainingItem = ({ item }) => (
+
+
+
+
+
+ {item.title}
+ {item.time}
+
+);
diff --git a/src/screens/HomeScreen/homeScreenStyles.js b/src/screens/HomeScreen/homeScreenStyles.js
new file mode 100644
index 0000000..e9cf502
--- /dev/null
+++ b/src/screens/HomeScreen/homeScreenStyles.js
@@ -0,0 +1,205 @@
+import { StyleSheet } from 'react-native';
+
+export const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: "#FFF",
+ },
+ header: {
+ paddingTop: 50,
+ paddingBottom: 15,
+ paddingHorizontal: 20,
+ },
+ headerRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ },
+ logoText: {
+ fontSize: 24,
+ fontWeight: "bold",
+ color: "#FFF",
+ },
+ logoHighlight: {
+ color: "#FFD54F",
+ },
+ notificationWrapper: {
+ position: "relative",
+ },
+ badge: {
+ position: "absolute",
+ top: -6,
+ right: -6,
+ backgroundColor: "red",
+ minWidth: 18,
+ height: 18,
+ borderRadius: 9,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ badgeText: {
+ color: "#fff",
+ fontSize: 10,
+ fontWeight: "bold",
+ },
+ whiteCard: {
+ backgroundColor: "#fff",
+ borderTopLeftRadius: 30,
+ borderTopRightRadius: 30,
+ padding: 20,
+ paddingTop: 30,
+ },
+ greetingRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ marginBottom: 20,
+ },
+ userInfoWrapper: {
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ customToggleOuterContainer: {
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ customToggleTrack: {
+ height: 34,
+ borderRadius: 17,
+ padding: 2,
+ flexDirection: "row",
+ alignItems: "center",
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.15,
+ shadowRadius: 3,
+ elevation: 3,
+ overflow: 'hidden',
+ },
+ customToggleThumb: {
+ width: 30,
+ height: 30,
+ borderRadius: 15,
+ backgroundColor: "#FFFFFF",
+ position: "absolute",
+ justifyContent: "center",
+ alignItems: "center",
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.2,
+ shadowRadius: 2,
+ elevation: 4,
+ },
+ customToggleTextInner: {
+ position: 'absolute',
+ fontSize: 10,
+ fontWeight: "bold",
+ letterSpacing: 0.5,
+ },
+ goodMorning: {
+ fontSize: 15,
+ color: "#444",
+ },
+ username: {
+ fontSize: 18,
+ fontWeight: "bold",
+ },
+ sectionHeader: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ marginTop: 20,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: "bold",
+ marginBottom: 10
+ },
+ taskCard: {
+ flexDirection: "row",
+ alignItems: "center",
+ padding: 12,
+ backgroundColor: "#F8F8F8",
+ borderRadius: 12,
+ marginTop: 12,
+ },
+ taskIconBox: {
+ width: 40,
+ height: 40,
+ borderRadius: 10,
+ backgroundColor: "#1E3C72",
+ justifyContent: "center",
+ alignItems: "center",
+ marginRight: 12,
+ },
+ taskTitle: {
+ fontSize: 15,
+ fontWeight: "600",
+ },
+ taskDate: {
+ fontSize: 12,
+ color: "#777",
+ },
+ checkBox: {
+ width: 20,
+ height: 20,
+ borderRadius: 6,
+ borderWidth: 1.5,
+ borderColor: "#aaa",
+ },
+ profileImage: {
+ width: 50,
+ height: 50,
+ borderRadius: 30,
+ },
+ videoCard: {
+ width: 180,
+ marginRight: 15,
+ marginTop: 15,
+ },
+ videoImage: {
+ height: 110,
+ width: "100%",
+ borderRadius: 12,
+ },
+ playBtn: {
+ position: "absolute",
+ top: 35,
+ left: 70,
+ },
+ videoTitle: {
+ marginTop: 5,
+ fontWeight: "600",
+ },
+ videoTime: {
+ fontSize: 12,
+ color: "#777",
+ },
+ trainingVideoCard: {
+ width: 250,
+ marginRight: 15,
+ marginTop: 15,
+ backgroundColor: "#F8F8F8",
+ borderRadius: 14,
+ padding: 8,
+ },
+ youtubeWrapper: {
+ borderRadius: 12,
+ overflow: "hidden",
+ },
+ gridCard: {
+ width: "47%",
+ backgroundColor: "#F8F8F8",
+ padding: 15,
+ borderRadius: 15,
+ marginBottom: 15,
+ },
+ gridTitle: {
+ marginTop: 10,
+ fontWeight: "700",
+ },
+ gridDesc: {
+ fontSize: 12,
+ color: "#777",
+ marginTop: 3,
+ },
+});
diff --git a/src/screens/ProfileScreen/EditProfileModal.js b/src/screens/ProfileScreen/EditProfileModal.js
new file mode 100644
index 0000000..483218f
--- /dev/null
+++ b/src/screens/ProfileScreen/EditProfileModal.js
@@ -0,0 +1,251 @@
+import React, { useState } from "react";
+import { View, Text, Modal, TouchableOpacity, TextInput, StyleSheet, ScrollView, Alert, ActivityIndicator } from "react-native";
+import Icon from "react-native-vector-icons/Feather";
+import Api from "../../api/Api";
+import { useDispatch } from "react-redux";
+import { setUser } from "../../redux/authSlice";
+
+const EditProfileModal = ({ isVisible, onClose, user }) => {
+ const dispatch = useDispatch();
+ const [loading, setLoading] = useState(false);
+
+ const [formData, setFormData] = useState({
+ Name: user?.Name || "",
+ MobileNo: user?.MobileNo || "",
+ Skill: user?.Skill || "",
+ Area: user?.Area || "",
+ Age: user?.Age?.toString() || "",
+ Experience: user?.Experience?.toString() || "",
+ });
+
+ const handleChange = (name, value) => {
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSave = async () => {
+ if (!formData.Name || !formData.MobileNo || !formData.Skill) {
+ Alert.alert("Error", "Please fill in all mandatory fields (Name, Mobile No, Skill)");
+ return;
+ }
+
+ try {
+ setLoading(true);
+ const response = await Api.put(`/workers/update-details/${user.id}`, {
+ Name: formData.Name,
+ MobileNo: formData.MobileNo,
+ Skill: formData.Skill,
+ Area: formData.Area,
+ Age: formData.Age ? parseInt(formData.Age) : undefined,
+ Experience: formData.Experience ? parseInt(formData.Experience) : undefined,
+ });
+
+ if (response.status === 200) {
+ const updatedUser = { ...user, ...response.data.data };
+ dispatch(setUser(updatedUser));
+ Alert.alert("Success", "Profile updated successfully");
+ onClose();
+ }
+ } catch (error) {
+ console.error("Update profile error:", error.response?.data || error.message);
+ Alert.alert("Error", error.response?.data?.error || "Failed to update profile");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Edit Profile
+
+
+
+
+
+
+
+ Full Name
+
+
+ handleChange("Name", val)}
+ placeholder="Enter your name"
+ />
+
+
+
+
+ Mobile Number
+
+
+ handleChange("MobileNo", val)}
+ placeholder="Enter mobile number"
+ keyboardType="phone-pad"
+ />
+
+
+
+
+ Skill / Profession
+
+
+ handleChange("Skill", val)}
+ placeholder="e.g. Electrician, Plumber"
+ />
+
+
+
+
+ Area
+
+
+ handleChange("Area", val)}
+ placeholder="e.g. Srinagar, Jammu"
+ />
+
+
+
+
+
+ Age
+
+ handleChange("Age", val)}
+ placeholder="Age"
+ keyboardType="numeric"
+ />
+
+
+
+
+ Experience (Years)
+
+ handleChange("Experience", val)}
+ placeholder="Exp"
+ keyboardType="numeric"
+ />
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+ Save Changes
+ )}
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ modalOverlay: {
+ flex: 1,
+ backgroundColor: "rgba(0,0,0,0.5)",
+ justifyContent: "flex-end",
+ },
+ modalContainer: {
+ backgroundColor: "#fff",
+ borderTopLeftRadius: 30,
+ borderTopRightRadius: 30,
+ paddingHorizontal: 20,
+ paddingTop: 20,
+ maxHeight: "85%",
+ },
+ modalHeader: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginBottom: 20,
+ },
+ modalTitle: {
+ fontSize: 22,
+ fontWeight: "bold",
+ color: "#333",
+ },
+ formContent: {
+ paddingBottom: 40,
+ },
+ inputGroup: {
+ marginBottom: 15,
+ },
+ label: {
+ fontSize: 14,
+ color: "#666",
+ marginBottom: 8,
+ fontWeight: "500",
+ },
+ inputWrapper: {
+ flexDirection: "row",
+ alignItems: "center",
+ backgroundColor: "#F5F7FA",
+ borderRadius: 12,
+ paddingHorizontal: 15,
+ borderWidth: 1,
+ borderColor: "#E1E8ED",
+ },
+ inputIcon: {
+ marginRight: 10,
+ },
+ input: {
+ flex: 1,
+ height: 50,
+ color: "#333",
+ fontSize: 16,
+ },
+ row: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ },
+ saveButton: {
+ backgroundColor: "#00AFFF",
+ height: 55,
+ borderRadius: 15,
+ justifyContent: "center",
+ alignItems: "center",
+ marginTop: 20,
+ elevation: 3,
+ shadowColor: "#00AFFF",
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 5,
+ },
+ saveButtonText: {
+ color: "#fff",
+ fontSize: 18,
+ fontWeight: "bold",
+ },
+});
+
+export default EditProfileModal;
diff --git a/src/screens/ProfileScreen/ProfileScreen.js b/src/screens/ProfileScreen/ProfileScreen.js
index b301ada..093f0ee 100644
--- a/src/screens/ProfileScreen/ProfileScreen.js
+++ b/src/screens/ProfileScreen/ProfileScreen.js
@@ -1,317 +1,473 @@
-import React, { use } from "react";
-import { View, Text, StyleSheet, ScrollView } from "react-native";
+import React, { useEffect, useState, useRef } from "react";
+import { View, Text, StyleSheet, ScrollView, Modal, TouchableOpacity, Alert } from "react-native";
import Icon from "react-native-vector-icons/Feather";
import { useSelector } from "react-redux";
import { Image } from "react-native";
+import styles from "./profileStyle"
+import Api from "../../api/Api";
+import JobModal from "./jobModals";
+import { RefreshControl } from "react-native";
+import { Calendar } from "react-native-calendars";
+import EditProfileModal from "./EditProfileModal";
const ProfileScreen = () => {
const user = useSelector(state => state.auth.user);
- return (
-
-
-
-
+ const [selectedJob, setSelectedJob] = useState(null);
+ const [modalVisible, setModalVisible] = useState(false);
+ //const [seconds, setSeconds] = useState(0);
+ const [isRunning, setIsRunning] = useState(false);
+ const [jobs, setJobs] = useState([]);
+ const [activeJobId, setActiveJobId] = useState(null);
+ const [filter, setFilter] = useState("all");
+ const [loading, setLoading] = useState(false);
+ const [dropdownOpen, setDropdownOpen] = useState(false);
+ const [calendarVisible, setCalendarVisible] = useState(false);
+ const [editModalVisible, setEditModalVisible] = useState(false);
+ useEffect(() => {
+ if (user?.id) {
+ fetchJobs();
+ }
+ }, [user]);
+
+ useEffect(() => {
+ if (!user?.id) return;
+
+ const interval = setInterval(() => {
+ if (!isRunning) {
+ fetchJobs();
+ }
+ }, 30000);
+
+ return () => clearInterval(interval);
+ }, [user, isRunning]);
+
+ const jobsRef = useRef([]);
+
+ useEffect(() => {
+ jobsRef.current = jobs;
+ }, [jobs]);
+
+ useEffect(() => {
+ let interval = null;
+
+ if (isRunning && activeJobId) {
+ interval = setInterval(() => {
+ setJobs(prevJobs =>
+ prevJobs.map(job =>
+ job.worker_task_id === activeJobId
+ ? { ...job, elapsedTime: (job.elapsedTime || 0) + 1 }
+ : job
+ )
+ );
+ }, 1000);
+ }
+
+ return () => clearInterval(interval);
+ }, [isRunning, activeJobId]);
+
+ const mapStatus = (status) => {
+ if (!status) return "";
+
+ const normalized = status.toLowerCase().trim();
+
+ if (normalized === "assigned") return "pending";
+ if (normalized === "complete" || normalized === "completed") return "completed";
+ if (normalized === "in-progress" || normalized === "in progress") return "in-progress";
+
+
+ return normalized;
+ };
+
+ const fetchJobs = async () => {
+ try {
+ setLoading(true);
+
+ const response = await Api.get(`/workers/assigned-work/${user.id}`);
+
+ const formattedJobs = response.data.data.map(task => {
+ const requestData = task["User Request"] || task;
+ const previousJob = jobsRef.current.find(
+ j => j.worker_task_id === task.worker_task_id
+ );
+ return {
+ id: task.id,
+ worker_task_id: task.worker_task_id,
+ service_request_id: task.service_request_id,
+ status: mapStatus(task.status),
+ Name: requestData.Name,
+ DateOfService: requestData.DateOfService,
+ Location: requestData.Location,
+ ServiceType: requestData.ServiceType,
+ Phone: requestData.Phone,
+ latitude: requestData.latitude,
+ longitude: requestData.longitude,
+ elapsedTime:
+ previousJob?.elapsedTime ??
+ (task.worker_task_id === activeJobId ? 0 : 0)
+ };
+ });
+
+ setJobs(formattedJobs);
+
+
+ const runningJob = formattedJobs.find(j => j.status === "in-progress");
+
+ // only set active job if we don't already have one
+ if (runningJob && !activeJobId) {
+ setActiveJobId(runningJob.worker_task_id);
+ }
+
+ } catch (error) {
+ console.log("Error fetching jobs:", error.response?.data || error.message);
+ } finally {
+ setLoading(false);
+ }
+ };//to fetch individual user jobs
+
+
+ const currentJob = jobs.find(
+ j => j.worker_task_id === selectedJob?.worker_task_id
+ );
-
- {user.image ? (
-
- ) : (
-
- )}
-
- {/*Name + Profession */}
-
- {user.Name}
- {user.Skill}
-
+ //for sorting jobs based on status
+ const sortedAndFilteredJobs = jobs
+ .filter(job => {
+ if (filter === "all") return true;
+
+ return job.status === filter;
+ })
+ .sort((a, b) => {
+ // Always pin in-progress at top
+ if (a.status === "in-progress" && b.status !== "in-progress") return -1;
+ if (b.status === "in-progress" && a.status !== "in-progress") return 1;
+
+ return 0;
+ });
+
+ const formatTime = (totalSeconds) => {
+ const hrs = Math.floor(totalSeconds / 3600);
+ const mins = Math.floor((totalSeconds % 3600) / 60);
+ const secs = totalSeconds % 60;
+
+ return `${hrs.toString().padStart(2, "0")}:${mins
+ .toString()
+ .padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
+ };
+
+ // Calculate marked dates for the calendar based on jobs
+ const markedDates = jobs.reduce((acc, job) => {
+ if (job.DateOfService) {
+ // Attempt to parse DateOfService. Since its format might vary,
+ // a basic implementation is provided. Adjust parsing according to your date format.
+ // Assuming DateOfService is like "2023-10-15" or parsable by Date
+ try {
+ const dateObj = new Date(job.DateOfService);
+ if (!isNaN(dateObj)) {
+ const dateString = dateObj.toISOString().split('T')[0];
+ acc[dateString] = {
+ marked: true,
+ dotColor: job.status === "pending" ? "#9C7B00" :
+ job.status === "in-progress" ? "#2674FF" :
+ job.status === "completed" ? "#00994d" : "#00AFFF"
+ };
+ }
+ } catch (e) {
+ console.log("Error parsing date for calendar:", job.DateOfService);
+ }
+ }
+ return acc;
+ }, {});
+
+ const handleStart = async (jobId) => {
+
+ if (jobs.some(job => job.status === "in-progress")) {
+ Alert.alert("Task Conflict", "Only one task can be in progress at a time.");
+ return false;
+ }
+
+ try {
+ await Api.post(`/workers/button-calls/start/${jobId}`);
+
+ setActiveJobId(jobId);
+ setIsRunning(true);
+
+ setJobs(prev =>
+ prev.map(job =>
+ job.worker_task_id === jobId
+ ? { ...job, status: "in-progress" }
+ : job
+ )
+ );
+
+ return true; // 🔥 IMPORTANT
+ } catch (error) {
+ console.log(error);
+ return false;
+ }
+ };
+
+ const handleEnd = async (jobId) => {
+ try {
+ await Api.post(`/workers/button-calls/end/${jobId}`);
+
+ setIsRunning(false);
+ //setSeconds(0);
+ setSelectedJob(prev => ({ ...prev, status: "completed" }));
+
+ await fetchJobs(); // refresh UI
+ } catch (error) {
+ console.log("End error:", error.response?.data || error.message);
+ }
+ };
+ const modalJob = currentJob ? currentJob : selectedJob;
+ return (
+
+
+
+
+
+ {user.image ? () : ()}
-
- {/* BODY CONTENT */}
-
-
-
-
- {user.Area} J&K
-
+ {/*Name + Profession */}
+
+ {user.Name}
+ {user.Skill}
+
-
+ {/* Edit Button */}
+ setEditModalVisible(true)}
+ >
+
+
+
+
-
-
- {user.Experience} years experience
-
+ {/* BODY CONTENT */}
+
+ }
+ >
+
+
+
+ {user.Area} J&K
+
-
+
-
-
- {user.MobileNo}
-
+
+
+ {user.Experience} years experience
- {/* ---------- EARNINGS CARD ---------- */}
-
-
- ₹
-
+
-
- Total Earnings
- ₹12,450
-
+
+
+ {user.MobileNo}
+
- {/* ---------- RECENT JOBS ---------- */}
-
- Recent Jobs
-
+ {/* ---------- EARNINGS CARD ---------- */}
+
+
+ ₹
-
-
- {/* Jobs*/}
-
-
- Sarah Johnson
- Dec 3, 2025
-
-
- ₹3200
- pending
-
-
+
+ Total Earnings
+ ₹12,450
+
+
-
-
-
-
- John Smith
- Dec 1, 2025
-
-
- ₹2,500
- in progress
-
+ {/* ---------- RECENT JOBS ---------- */}
+
+ Recent Jobs
+
+
+ setCalendarVisible(true)}>
+
+
+
+
+ setDropdownOpen(!dropdownOpen)}
+ style={{ padding: 5 }}
+ >
+
+
+
+ {dropdownOpen && (
+
+ {["all", "pending", "in-progress", "completed"].map(type => (
+ {
+ setFilter(type);
+ setDropdownOpen(false);
+ }}
+ >
+
+ {type.toUpperCase()}
+
+
+ ))}
+
+ )}
-
-
-
-
-
- Mike Davis
- Nov 28, 2025
-
-
- ₹800
- completed
-
+
+
+
+ {jobs.length === 0 ? (
+
+ No jobs found
+
+ ) : (
+ sortedAndFilteredJobs.map((job) => (
+ {
+ /* setSelectedJob(job);
+ setIsRunning(false);
+ //setSeconds(0);
+ setModalVisible(true);*/
+ setSelectedJob(job);
+ setModalVisible(true);
+ if (job.status === "in-progress") {
+ if (job.status === "in-progress") {
+ if (!activeJobId) {
+ setActiveJobId(job.worker_task_id);
+ }
+ } else {
+ setIsRunning(false);
+ }
+ } else {
+ setIsRunning(false);
+ }
+
+ }}
+ >
+
+
+ {job.Name}
+
+
+
+ {job.DateOfService}
+
+
+
+
+
+ {job.ServiceType}
+
+
+
+ {job.status}
+
+
+
+ ))
+ )}
+
+
+
+
+
+ setCalendarVisible(false)}
+ >
+ setCalendarVisible(false)}
+ >
+ true}>
+
+ Your Schedule
+ setCalendarVisible(false)}>
+
+
-
+
+
+ {
+ // Find if any job is on this date
+ const jobOnDate = jobs.find((j) => {
+ if (!j.DateOfService) return false;
+
+ const jobDate = new Date(j.DateOfService)
+ .toISOString()
+ .split("T")[0];
+
+ return jobDate === day.dateString;
+ });
+ if (jobOnDate) {
+ setCalendarVisible(false);
+ setSelectedJob(jobOnDate);
+ setModalVisible(true);
+ } else {
+ Alert.alert("No Tasks", "You have no scheduled tasks on this date.");
+ }
+ }}
+ theme={{
+ selectedDayBackgroundColor: '#00AFFF',
+ todayTextColor: '#00AFFF',
+ arrowColor: '#00AFFF',
+ }}
+ />
-
-
- );
+
+
+
+ setEditModalVisible(false)}
+ user={user}
+ />
+ );
};
+
export default ProfileScreen;
-const styles = StyleSheet.create({
-
- screenContainer: {
- flex: 1,
- backgroundColor: "#F5F7FA"
- },
-
- Bar: {
- backgroundColor: "#00AFFF",
- paddingTop: 50,
- paddingBottom: 25,
- paddingHorizontal: 20,
- borderBottomLeftRadius: 30,
- borderBottomRightRadius: 30,
- elevation: 6,
- },
-
- Row: {
- flexDirection: "row",
- alignItems: "center",
- },
-
- avatarImage: {
- width: 70,
- height: 70,
- borderRadius: 45,
- backgroundColor: "#f1f1f1",
- justifyContent: "center",
- alignItems: "center",
- elevation: 5,
- },
-
- NameWrapper: {
- marginLeft: 15,
- },
-
- UserName: {
- fontSize: 22,
- color: "#fff",
- fontWeight: "700",
- },
-
- Profession: {
- fontSize: 15,
- color: "#EAF9FF",
- marginTop: 3,
- },
-
- bodyContent: {
- paddingHorizontal: 20,
- paddingTop: 20,
- paddingBottom: 30
- },
-
- userInfoCard: {
- backgroundColor: "#fff",
- padding: 18,
- borderRadius: 15,
- elevation: 3,
- marginBottom: 15,
- },
-
- infoRow: {
- flexDirection: "row",
- alignItems: "center",
- paddingVertical: 8,
- gap: 12
- },
-
- infoText: {
- fontSize: 15,
- color: "#333"
- },
-
- cardDivider: {
- height: 1,
- backgroundColor: "#eee",
- marginVertical: 5
- },
-
- earningsCard: {
- backgroundColor: "#fff",
- flexDirection: "row",
- alignItems: "center",
- padding: 18,
- borderRadius: 15,
- elevation: 3,
- marginBottom: 15,
- },
-
- earningsIconBox: {
- width: 45,
- height: 45,
- backgroundColor: "#2ECC71",
- borderRadius: 25,
- justifyContent: "center",
- alignItems: "center",
- marginRight: 15,
- },
-
- earningsDollarSymbol: {
- color: "#fff",
- fontWeight: "bold",
- fontSize: 18
- },
-
- earningsLabel: {
- color: "#555",
- fontSize: 14
- },
-
- earningsAmount: {
- fontSize: 24,
- fontWeight: "700",
- color: "#000"
- },
-
- recentJobsHeaderRow: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- marginBottom: 10,
- },
-
- recentJobsTitle: {
- fontSize: 20,
- fontWeight: "700"
- },
-
- recentJobsCard: {
- backgroundColor: "#fff",
- padding: 18,
- borderRadius: 15,
- elevation: 3,
- marginBottom: 20,
- },
-
- jobRow: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- paddingVertical: 10,
- },
-
- jobCustomerName: {
- fontSize: 16,
- fontWeight: "600",
- color: "#000"
- },
-
- jobDateText: {
- fontSize: 12,
- color: "#777",
- marginTop: 3
- },
-
- jobRightSection: {
- alignItems: "flex-end"
- },
-
- jobPayment: {
- fontSize: 16,
- fontWeight: "700",
- color: "#2ECC71"
- },
-
- jobStatusPending: {
- backgroundColor: "#FFE483",
- color: "#9C7B00",
- paddingHorizontal: 10,
- borderRadius: 8,
- marginTop: 3,
- fontSize: 12,
- },
-
- jobStatusProgress: {
- backgroundColor: "#DCE9FF",
- color: "#2674FF",
- paddingHorizontal: 10,
- borderRadius: 8,
- marginTop: 3,
- fontSize: 12,
- },
-
- jobStatusCompleted: {
- backgroundColor: "#C8FFE0",
- color: "#00994d",
- paddingHorizontal: 10,
- borderRadius: 8,
- marginTop: 3,
- fontSize: 12,
- },
-
-});
+/*Style sheet */
diff --git a/src/screens/ProfileScreen/jobModals.js b/src/screens/ProfileScreen/jobModals.js
new file mode 100644
index 0000000..38e207b
--- /dev/null
+++ b/src/screens/ProfileScreen/jobModals.js
@@ -0,0 +1,226 @@
+import React from "react";
+import { View, Text, Modal, TouchableOpacity, Linking, Platform, Alert } from "react-native";
+import Icon from "react-native-vector-icons/Feather";
+import Geolocation from '@react-native-community/geolocation';
+import styles from "./profileStyle";
+import { useEffect } from "react";
+import { ensureLocationPermission } from "../../utils/permissionUtils";
+
+const JobModal = ({
+ modalVisible,
+ setModalVisible,
+ selectedJob,
+ isRunning,
+ seconds,
+ formatTime,
+ setIsRunning,
+ setSelectedJob,
+ handleStart,
+ handleEnd,
+ setJobs
+}) => {
+
+ useEffect(() => {
+
+ if (!selectedJob) return;
+
+ if (selectedJob.status === "in-progress") {
+ setIsRunning(true);
+ } else {
+ setIsRunning(false);
+ }
+
+ }, [selectedJob?.worker_task_id, selectedJob?.status]);
+
+
+
+ const startJob = async () => {
+
+ const success = await handleStart(selectedJob.worker_task_id);
+
+ if (success) {
+
+ setSelectedJob(prev => ({
+ ...prev,
+ status: "in-progress"
+ }));
+
+ setIsRunning(true);
+ }
+ };
+
+ const endJob = async () => {
+ await handleEnd(selectedJob.worker_task_id);
+ };
+
+ const openMapRoute = async () => {
+ const hasPermission = await ensureLocationPermission();
+ if (!hasPermission) return;
+
+ const isServiceOn = await checkLocationService();
+ if (!isServiceOn) return;
+
+ Geolocation.getCurrentPosition(
+ (position) => {
+ const { latitude: workerLat, longitude: workerLng } = position.coords;
+ const { latitude: userLat, longitude: userLng } = selectedJob;
+
+ if (!userLat || !userLng) {
+ Alert.alert("Error", "User location not available");
+ return;
+ }
+
+ const url = Platform.select({
+ ios: `maps:0,0?saddr=${workerLat},${workerLng}&daddr=${userLat},${userLng}`,
+ android: `google.navigation:q=${userLat},${userLng}&mode=d`
+ });
+
+ Linking.openURL(url).catch((err) => console.error("An error occurred", err));
+ },
+ (error) => {
+ console.log("Geolocation Error:", error.message);
+ Alert.alert("Permission Error", "Could not get your current location. Please ensure location permissions are granted and GPS is enabled.");
+ },
+ { enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 }
+ );
+ };
+
+ return (
+ setModalVisible(false)}>
+
+
+
+ {/* HEADER */}
+
+
+ {selectedJob?.Name || "Job Details"}
+
+
+ setModalVisible(false)}>
+
+
+
+
+
+
+ {/* DATE */}
+
+ Date
+
+ {selectedJob?.DateOfService || "N/A"}
+
+
+
+ {/* ADDRESS */}
+
+ Address
+
+ {selectedJob?.Location || "No address provided"}
+
+
+
+ {/* SERVICE */}
+
+ Service Type
+
+
+ {selectedJob?.ServiceType || "Generic"}
+
+
+
+
+ {/* CONTACT */}
+
+ Contact no
+
+ {selectedJob?.Phone || "Hidden"}
+
+
+
+ {/* STATUS */}
+
+ Status
+
+ {selectedJob?.status || "Unknown"}
+
+
+
+ {/* -------- PENDING -------- */}
+ {selectedJob?.status === "pending" && (
+
+
+
+ View
+
+
+
+ Start
+
+
+
+ )}
+
+ {/* -------- IN PROGRESS -------- */}
+ {selectedJob?.status === "in-progress" && (
+ <>
+
+ {formatTime(seconds)}
+
+
+
+ View Map
+
+
+
+
+ {isRunning ? (
+ setIsRunning(false)}
+ >
+ Pause
+
+ ) : (
+ setIsRunning(true)}
+ >
+ Continue
+
+ )}
+
+
+ End
+
+
+
+ >
+ )}
+
+ {/* -------- COMPLETED -------- */}
+ {selectedJob?.status === "completed" && (
+
+ Job Completed ✅
+
+ )}
+
+
+
+
+ );
+};
+
+export default JobModal;
+
diff --git a/src/screens/ProfileScreen/profileStyle.js b/src/screens/ProfileScreen/profileStyle.js
new file mode 100644
index 0000000..8243fc5
--- /dev/null
+++ b/src/screens/ProfileScreen/profileStyle.js
@@ -0,0 +1,302 @@
+import {StyleSheet} from 'react-native';
+
+const styles = StyleSheet.create({
+
+ screenContainer: {
+ flex: 1, backgroundColor: "#F5F7FA"
+ },
+
+ Bar: {
+ backgroundColor: "#00AFFF",
+ paddingTop: 50,
+ paddingBottom: 25,
+ paddingHorizontal: 20,
+ borderBottomLeftRadius: 30,
+ borderBottomRightRadius: 30,
+ elevation: 6,
+ },
+
+ Row: {
+ flexDirection: "row", alignItems: "center",
+ },
+
+ avatarImage: {
+ width: 70,
+ height: 70,
+ borderRadius: 45,
+ backgroundColor: "#f1f1f1",
+ justifyContent: "center",
+ alignItems: "center",
+ elevation: 5,
+ },
+
+ NameWrapper: {
+ marginLeft: 15,
+ },
+
+ UserName: {
+ fontSize: 22, color: "#fff", fontWeight: "700",
+ },
+
+ Profession: {
+ fontSize: 15, color: "#EAF9FF", marginTop: 3,
+ },
+
+ bodyContent: {
+ paddingHorizontal: 20, paddingTop: 20, paddingBottom: 30
+ },
+
+ userInfoCard: {
+ backgroundColor: "#fff", padding: 18, borderRadius: 15, elevation: 3, marginBottom: 15,
+ },
+
+ infoRow: {
+ flexDirection: "row", alignItems: "center", paddingVertical: 8, gap: 12
+ },
+
+ infoText: {
+ fontSize: 15, color: "#333"
+ },
+
+ cardDivider: {
+ height: 1, backgroundColor: "#eee", marginVertical: 5
+ },
+
+ earningsCard: {
+ backgroundColor: "#fff",
+ flexDirection: "row",
+ alignItems: "center",
+ padding: 18,
+ borderRadius: 15,
+ elevation: 3,
+ marginBottom: 15,
+ },
+
+ earningsIconBox: {
+ width: 45,
+ height: 45,
+ backgroundColor: "#2ECC71",
+ borderRadius: 25,
+ justifyContent: "center",
+ alignItems: "center",
+ marginRight: 15,
+ },
+
+ earningsDollarSymbol: {
+ color: "#fff", fontWeight: "bold", fontSize: 18
+ },
+
+ earningsLabel: {
+ color: "#555", fontSize: 14
+ },
+
+ earningsAmount: {
+ fontSize: 24, fontWeight: "700", color: "#000"
+ },
+
+ recentJobsHeaderRow: {
+ flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 10,
+ },
+
+ recentJobsTitle: {
+ fontSize: 20, fontWeight: "700"
+ },
+
+ recentJobsCard: {
+ backgroundColor: "#fff", padding: 18, borderRadius: 15, elevation: 3, marginBottom: 20,
+ },
+
+ jobRow: {
+ flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingVertical: 10,
+ },
+
+ jobCustomerName: {
+ fontSize: 16, fontWeight: "600", color: "#000"
+ },
+
+ jobDateText: {
+ fontSize: 12, color: "#777", marginTop: 3
+ },
+
+ jobRightSection: {
+ alignItems: "flex-end"
+ },
+
+ jobPayment: {
+ fontSize: 16, fontWeight: "700", color: "#2ECC71"
+ },
+
+ jobStatusPending: {
+ backgroundColor: "#FFE483",
+ color: "#9C7B00",
+ paddingHorizontal: 10,
+ borderRadius: 8,
+ marginTop: 3,
+ fontSize: 12,
+ },
+
+ jobStatusProgress: {
+ backgroundColor: "#DCE9FF",
+ color: "#2674FF",
+ paddingHorizontal: 10,
+ borderRadius: 8,
+ marginTop: 3,
+ fontSize: 12,
+ },
+
+ jobStatusCompleted: {
+ backgroundColor: "#C8FFE0",
+ color: "#00994d",
+ paddingHorizontal: 10,
+ borderRadius: 8,
+ marginTop: 3,
+ fontSize: 12,
+ }, modalOverlay: {
+ flex: 1, backgroundColor: "rgba(0,0,0,0.45)", justifyContent: "center", alignItems: "center",
+ },
+
+ modalContainer: {
+ width: "90%", backgroundColor: "#fff", borderRadius: 20, padding: 20, elevation: 12,
+ },
+
+ modalHeader: {
+ flexDirection: "row", justifyContent: "space-between", alignItems: "center",
+ },
+
+ modalTitle: {
+ fontSize: 20, fontWeight: "700", color: "#111",
+ },
+
+ modalDivider: {
+ height: 1, backgroundColor: "#eee", marginVertical: 15,
+ },
+
+ detailRow: {
+ flexDirection: "row", justifyContent: "space-between", marginBottom: 12,
+ },
+
+ detailRowColumn: {
+ marginBottom: 12,
+ },
+
+ detailLabel: {
+ fontSize: 13, color: "#777",
+ },
+
+ detailValue: {
+ fontSize: 15, fontWeight: "500", color: "#222",
+ },
+
+ amountText: {
+ fontSize: 16, fontWeight: "700", color: "#2ECC71",
+ },
+
+ statusBadge: {
+ backgroundColor: "#FFE483", paddingHorizontal: 10, paddingVertical: 4, borderRadius: 20,
+ },
+
+ statusText: {
+ fontSize: 12, fontWeight: "600", color: "#9C7B00",
+ },
+
+ buttonRow: {
+ flexDirection: "row", justifyContent: "space-between", marginTop: 20,
+ },
+
+ acceptButton: {
+ flex: 1, backgroundColor: "#00AFFF", padding: 12, borderRadius: 10, alignItems: "center", marginLeft: 8,
+ },
+
+ rejectButton: {
+ flex: 1, backgroundColor: "#FF4D4D", padding: 12, borderRadius: 10, alignItems: "center", marginRight: 8,
+ },
+
+ buttonText: {
+ color: "#fff", fontWeight: "600",
+ }, viewButton: {
+ flex: 1, backgroundColor: "#555", padding: 12, borderRadius: 10, alignItems: "center", marginRight: 8,
+ },
+
+ startButton: {
+ flex: 1, backgroundColor: "#00C853", padding: 12, borderRadius: 10, alignItems: "center", marginLeft: 8,
+ },
+
+ pauseButton: {
+ flex: 1, backgroundColor: "#FF9800", padding: 12, borderRadius: 10, alignItems: "center", marginRight: 8,
+ },
+
+ endButton: {
+ flex: 1, backgroundColor: "#D32F2F", padding: 12, borderRadius: 10, alignItems: "center", marginLeft: 8,
+ },
+
+ timerText: {
+ fontSize: 28, fontWeight: "700", textAlign: "center", marginVertical: 15, color: "#111",
+ }, filterContainer: {
+ flexDirection: "row", justifyContent: "space-between", marginBottom: 10,
+ },
+
+ filterButton: {
+ flex: 1,
+ paddingVertical: 6,
+ marginHorizontal: 4,
+ borderRadius: 20,
+ backgroundColor: "#EAEAEA",
+ alignItems: "center",
+ },
+
+ activeFilter: {
+ backgroundColor: "#00AFFF",
+ },
+
+ filterText: {
+ fontSize: 12, fontWeight: "600", color: "#555",
+ },
+
+ activeFilterText: {
+ color: "#fff",
+ },
+
+ dropdownHeader: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ backgroundColor: "#fff",
+ padding: 15,
+ borderRadius: 10,
+ elevation: 2,
+ },
+ dropdownHeaderText: {
+ fontSize: 16,
+ fontWeight: "600",
+ color: "#333",
+ },
+ dropdownList: {
+ backgroundColor: "#fff",
+ borderRadius: 10,
+ marginTop: 5,
+ elevation: 3,
+ paddingVertical: 5,
+ },
+ dropdownItem: {
+ padding: 15,
+ borderBottomWidth: 1,
+ borderBottomColor: "#f0f0f0",
+ },
+ dropdownItemText: {
+ fontSize: 15,
+ color: "#555",
+ },
+ editButton: {
+ position: "absolute",
+ right: 0,
+ top: 0,
+ backgroundColor: "rgba(255, 255, 255, 0.2)",
+ padding: 8,
+ borderRadius: 10,
+ borderWidth: 1,
+ borderColor: "rgba(255, 255, 255, 0.3)",
+ },
+
+})
+
+export default styles;
diff --git a/src/utils/permissionUtils.js b/src/utils/permissionUtils.js
new file mode 100644
index 0000000..b79ce60
--- /dev/null
+++ b/src/utils/permissionUtils.js
@@ -0,0 +1,84 @@
+import { PermissionsAndroid, Platform, Alert, Linking } from 'react-native';
+import Geolocation from '@react-native-community/geolocation';
+
+/**
+ * Ensures location permission is granted.
+ * Requests permission if not already granted.
+ * @returns {Promise} True if granted, false otherwise.
+ */
+export const ensureLocationPermission = async () => {
+ if (Platform.OS === 'android') {
+ try {
+ // 1. Check if already granted
+ const isGranted = await PermissionsAndroid.check(
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
+ );
+
+ if (isGranted) {
+ // Check if location services are actually enabled by doing a quick ping
+ // However, PermissionsAndroid.check is usually enough for the "permission" part.
+ return true;
+ }
+
+ // 2. Request permission
+ const status = await PermissionsAndroid.request(
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
+ {
+ title: 'Location Permission',
+ message: 'This feature requires access to your location.',
+ buttonPositive: 'OK',
+ buttonNegative: 'Cancel',
+ }
+ );
+
+ if (status === PermissionsAndroid.RESULTS.GRANTED) {
+ return true;
+ }
+
+ if (status === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
+ Alert.alert(
+ 'Permission Required',
+ 'Location access is permanently denied. Please enable it in the app settings.',
+ [
+ { text: 'Cancel', style: 'cancel' },
+ { text: 'Settings', onPress: () => Linking.openSettings() },
+ ]
+ );
+ }
+ return false;
+ } catch (err) {
+ console.warn('Permission error:', err);
+ return false;
+ }
+ } else {
+ return true; // Simplified for iOS for now
+ }
+};
+
+/**
+ * Checks if location services (GPS) are enabled and prompts user if not.
+ * Note: Pure RN doesn't have a direct "isGPSEnabled" check without libraries,
+ * so we use getCurrentPosition with a short timeout.
+ */
+export const checkLocationService = async () => {
+ return new Promise((resolve) => {
+ Geolocation.getCurrentPosition(
+ () => resolve(true),
+ (error) => {
+ if (error.code === 2) { // 2 is POSITION_UNAVAILABLE / Services disabled
+ Alert.alert(
+ "Location Disabled",
+ "Please turn on your GPS (Location Services) to use this feature.",
+ [
+ { text: "Cancel", onPress: () => resolve(false), style: "cancel" },
+ { text: "OK", onPress: () => resolve(false) } // User has to turn it on manually in notification shade in standard RN
+ ]
+ );
+ } else {
+ resolve(true); // Other errors like timeout don't necessarily mean it's "off"
+ }
+ },
+ { enableHighAccuracy: false, timeout: 1000, maximumAge: 10000 }
+ );
+ });
+};