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/ProfileScreen.js b/src/screens/ProfileScreen/ProfileScreen.js index b301ada..b215931 100644 --- a/src/screens/ProfileScreen/ProfileScreen.js +++ b/src/screens/ProfileScreen/ProfileScreen.js @@ -1,317 +1,456 @@ -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"; 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); + 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 ? ( - - ) : ( - - )} - + //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 ? () : ()} + - {/*Name + Profession */} - - {user.Name} - {user.Skill} - + {/*Name + Profession */} + + {user.Name} + {user.Skill} + - {/* BODY CONTENT */} - - - - - {user.Area} J&K - - - - - - - {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', + }} + /> - - - ); + + + ); }; + 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..6d841f1 --- /dev/null +++ b/src/screens/ProfileScreen/profileStyle.js @@ -0,0 +1,293 @@ +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", + }, + +}) + +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 } + ); + }); +};