diff --git a/app/api/profileApi.ts b/app/api/profileApi.ts index 3e3f113..ff50581 100644 --- a/app/api/profileApi.ts +++ b/app/api/profileApi.ts @@ -11,7 +11,7 @@ export const updateProfileApi = async ( if (payload.firstname !== undefined) formData.append('firstname', payload.firstname); if (payload.lastname !== undefined) formData.append('lastname', payload.lastname); - if (payload.phone !== undefined) formData.append('phone', payload.phone); + if (payload.phonenumber !== undefined) formData.append('phonenumber', payload.phonenumber); if (payload.email_signature !== undefined) formData.append('email_signature', payload.email_signature); if (payload.password) formData.append('password', payload.password); if (payload.passwordr) formData.append('passwordr', payload.passwordr); diff --git a/app/components/projectActivityFeed/projectActivityFeed.tsx b/app/components/projectActivityFeed/projectActivityFeed.tsx index 69b21d7..621d201 100644 --- a/app/components/projectActivityFeed/projectActivityFeed.tsx +++ b/app/components/projectActivityFeed/projectActivityFeed.tsx @@ -3,7 +3,6 @@ import { View, Text, ActivityIndicator, - ScrollView, TouchableOpacity, } from 'react-native'; import Icon from 'react-native-vector-icons/Ionicons'; @@ -143,11 +142,7 @@ export const ProjectActivityFeed = ({ ) : ( <> - + {displayData.map((item, index) => ( ))} - + {hasMore && ( diff --git a/app/features/editProfile/editProfile.screen.tsx b/app/features/editProfile/editProfile.screen.tsx index 329dd2c..e95cc84 100644 --- a/app/features/editProfile/editProfile.screen.tsx +++ b/app/features/editProfile/editProfile.screen.tsx @@ -15,7 +15,7 @@ import Icon from 'react-native-vector-icons/Ionicons'; import { useNavigation } from '@react-navigation/native'; import { useTheme } from '@theme'; import { FormInput, Loader } from '@components'; -import { useAppDispatch, useAppSelector, RootState, updateUserData } from '@store'; +import { useAppDispatch, useAppSelector, RootState } from '@store'; import { getStyles } from './editProfile.styles'; import { updateProfile, getStaffDetails, resetEditProfileState } from './thunk'; import ImagePicker from 'react-native-image-crop-picker'; @@ -43,6 +43,9 @@ export const EditProfileScreen = () => { const [profileImageMime, setProfileImageMime] = useState('image/jpeg'); const [profileImageName, setProfileImageName] = useState('profile_image.jpg'); const [isImageChanged, setIsImageChanged] = useState(false); + // UI toggle state + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); const resolveServerUrl = async (path?: string | null) => { if (!path) return ''; @@ -62,36 +65,26 @@ export const EditProfileScreen = () => { return `${cleanBase}/${cleanRelPath}`; }; - useEffect(() => { - resolveServerUrl(userData?.profile_image).then((url) => { - setProfileImageUri(url); - }); - }, [userData?.profile_image]); - - // UI toggle state - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); - // Fetch staff details on mount - // useEffect(() => { - // if (userData?.staffid) { - // dispatch(getStaffDetails(userData.staffid)); - // } - // }, [dispatch, userData?.staffid]); + useEffect(() => { + if (userData?.staffid) { + dispatch(getStaffDetails(userData.staffid)); + } + }, [dispatch, userData?.staffid]); // Update local form state when details are loaded from the server - // useEffect(() => { - // if (staffDetails) { - // setFirstname(staffDetails.firstname || ''); - // setLastname(staffDetails.lastname || ''); - // setEmail(staffDetails.email || ''); - // setPhone(staffDetails.phonenumber || ''); - // setEmailSignature(staffDetails.email_signature || ''); - // resolveServerUrl(userData?.profile_image).then((url) => { - // setProfileImageUri(url); - // }); - // } - // }, [staffDetails, userData?.profile_image]); + useEffect(() => { + if (staffDetails) { + setFirstname(staffDetails.firstname || ''); + setLastname(staffDetails.lastname || ''); + setEmail(staffDetails.email || ''); + setPhone(staffDetails.phonenumber || ''); + setEmailSignature(staffDetails.email_signature || ''); + resolveServerUrl(userData?.profile_image).then((url) => { + setProfileImageUri(url); + }); + } + }, [staffDetails, userData?.profile_image]); // Reset edit profile state on unmount useEffect(() => { @@ -100,6 +93,34 @@ export const EditProfileScreen = () => { }; }, [dispatch]); + // Navigate back on success + useEffect(() => { + if (successMessage) { + Alert.alert('Success', successMessage, [ + { + text: 'OK', + onPress: () => { + dispatch(resetEditProfileState()); + navigation.goBack(); + }, + }, + ]); + } + }, [successMessage]); + + // Show error message from state + useEffect(() => { + if (error) { + Alert.alert('Error', error, [ + { + text: 'OK', + onPress: () => dispatch(resetEditProfileState()), + }, + ]); + } + }, [error]); + + const handleCameraLaunch = () => { ImagePicker.openCamera({ width: 400, @@ -203,40 +224,13 @@ export const EditProfileScreen = () => { email, firstname: firstname || undefined, lastname: lastname || undefined, - phone: phone || undefined, + phonenumber: phone || undefined, email_signature: emailSignature || undefined, password: password || undefined, passwordr: passwordr || undefined, profile_image: profileImagePayload, }), - ) - .unwrap() - .then((response) => { - // Success - const updatedImage = response.user_data?.profile_image || userData?.profile_image; - dispatch( - updateUserData({ - firstname, - lastname, - email, - phonenumber: phone, - email_signature: emailSignature, - profile_image: updatedImage, - }), - ); - Alert.alert('Success', 'Profile updated successfully.', [ - { - text: 'OK', - onPress: () => { - dispatch(resetEditProfileState()); - navigation.goBack(); - }, - }, - ]); - }) - .catch((err) => { - Alert.alert('Error', err || 'Failed to update profile'); - }); + ); }; const staffId = userData?.staffid || ''; diff --git a/app/features/leads/leads.screen.tsx b/app/features/leads/leads.screen.tsx index 9a8045b..a97ffb3 100644 --- a/app/features/leads/leads.screen.tsx +++ b/app/features/leads/leads.screen.tsx @@ -114,9 +114,9 @@ export const LeadsScreen = () => { placeholder="Search leads, companies, emails..." style={styles.searchInput} /> - + {/* - + */} {/* Status count cards */} diff --git a/app/features/login/login.screen.tsx b/app/features/login/login.screen.tsx index f96e0cf..51b423d 100644 --- a/app/features/login/login.screen.tsx +++ b/app/features/login/login.screen.tsx @@ -17,8 +17,8 @@ import { useTheme } from '@theme'; import { useAppDispatch, useAppSelector, RootState, login } from '@store'; import { LoginRequest } from '@interfaces'; import { NotificationService } from '@services'; -import { backGroundImage } from '@utils'; -import { FormInput } from '@components'; +import { backGroundImage, TENANCY_OPTIONS } from '@utils'; +import { FormInput, FormPicker } from '@components'; import AsyncStorage from '@react-native-async-storage/async-storage'; import DeviceInfo from 'react-native-device-info'; @@ -97,8 +97,14 @@ export const LoginScreen = () => { const savedPassword = await AsyncStorage.getItem('password'); if (savedBaseUrl) { - const cleanBaseUrl = savedBaseUrl.replace(/^(https?:\/\/)?/, ''); - setTenancy(cleanBaseUrl); + const matched = TENANCY_OPTIONS.find( + opt => + opt.value.trim().replace(/\/+$/, '').toLowerCase() === + savedBaseUrl.trim().replace(/\/+$/, '').toLowerCase(), + ); + setTenancy(matched ? matched.value : savedBaseUrl); + } else if (TENANCY_OPTIONS.length > 0) { + setTenancy(TENANCY_OPTIONS[0].value); } if (savedEmail) setEmail(savedEmail); if (savedPassword) setPassword(savedPassword); @@ -180,39 +186,13 @@ export const LoginScreen = () => { ) : null} - {/* Tenancy Field */} - { - const cleaned = text.replace(/^(https?:\/\/)?/, ''); - setTenancy(cleaned); - }} - prefix="https://" - placeholder="company-name" - autoCapitalize="none" - autoCorrect={false} - containerStyle={styles.fieldContainer} - inputContainerStyle={[ - styles.inputWrapper, - focusedField === 'tenancy' && styles.inputWrapperFocused, - ]} - onFocus={() => { - setFocusedField('tenancy'); - setTimeout(() => scrollViewRef.current?.scrollToEnd({ animated: true }), 100); - }} - onBlur={() => setFocusedField(null)} - leftIcon={ - - } + onValueChange={(val) => setTenancy(val)} + options={TENANCY_OPTIONS} + placeholder="Select Tenancy Environment" /> {/* Email Field */} diff --git a/app/interfaces/profile.ts b/app/interfaces/profile.ts index 0c65ab0..9f4a864 100644 --- a/app/interfaces/profile.ts +++ b/app/interfaces/profile.ts @@ -3,7 +3,7 @@ export interface UpdateProfilePayload { email: string; firstname?: string; lastname?: string; - phone?: string; + phonenumber?: string; email_signature?: string; password?: string; passwordr?: string; diff --git a/app/navigation/customDrawerContent.style.ts b/app/navigation/customDrawerContent.style.ts index 0440bff..de761eb 100644 --- a/app/navigation/customDrawerContent.style.ts +++ b/app/navigation/customDrawerContent.style.ts @@ -156,4 +156,10 @@ export const getStyles = (colors: ThemeColors) => fontWeight: '700', color: '#EF4444', }, + versionText: { + fontSize: 11, + color: colors.textMuted, + textAlign: 'center', + marginTop: 10, + }, }); diff --git a/app/navigation/customDrawerContent.tsx b/app/navigation/customDrawerContent.tsx index b82988d..f4bfc3f 100644 --- a/app/navigation/customDrawerContent.tsx +++ b/app/navigation/customDrawerContent.tsx @@ -222,6 +222,7 @@ export const CustomDrawerContent = (props: DrawerContentComponentProps) => { /> Sign Out + App Version: 1.0.0 ); diff --git a/app/store/commonReducers/auth/reducers.ts b/app/store/commonReducers/auth/reducers.ts index fe4f1cf..df47d54 100644 --- a/app/store/commonReducers/auth/reducers.ts +++ b/app/store/commonReducers/auth/reducers.ts @@ -1,9 +1,8 @@ -import { createAction, createReducer } from '@reduxjs/toolkit'; +import { createReducer } from '@reduxjs/toolkit'; import { login } from './thunk'; import { UserData } from '@interfaces'; +import { updateProfile } from '../../../features/editProfile/thunk'; -// Patch user_data in auth state after a successful profile update -export const updateUserData = createAction>('auth/updateUserData'); export interface LoginState { loginSuccess: boolean; @@ -50,9 +49,10 @@ export const authReducer = createReducer(initialState, builder => { acc.loginMessage = (payload as string) ?? error.message ?? 'Login failed'; acc.loginSuccess = false; }) - .addCase(updateUserData, (acc, action) => { - if (acc.user_data) { - acc.user_data = { ...acc.user_data, ...action.payload }; + // Sync auth user_data after a successful profile update + .addCase(updateProfile.fulfilled, (acc, action) => { + if (acc.user_data && action.payload.user_data) { + acc.user_data = { ...acc.user_data, ...action.payload.user_data }; } }); }); diff --git a/app/utils/api.ts b/app/utils/api.ts index 7dde230..2b8f3a4 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -1,22 +1,26 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; -import config from 'react-native-config'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { getAuthTokenForBaseUrl } from '@utils'; import {Store} from '@reduxjs/toolkit'; const axiosInstance: AxiosInstance = axios.create({ timeout: 15000, headers: { 'Content-Type': 'application/json', - authtoken: config.AUTH_TOKEN, }, }); axiosInstance.interceptors.request.use( async request => { - // Dynamically set baseURL from AsyncStorage on every request + // Dynamically set baseURL and authtoken from AsyncStorage on every request const storedBaseUrl = await AsyncStorage.getItem('base_url'); if (storedBaseUrl) { request.baseURL = storedBaseUrl; + + const authToken = getAuthTokenForBaseUrl(storedBaseUrl); + if (authToken) { + request.headers.authtoken = authToken; + } } if (request.data instanceof FormData) { diff --git a/app/utils/enviroment.ts b/app/utils/enviroment.ts new file mode 100644 index 0000000..f853a70 --- /dev/null +++ b/app/utils/enviroment.ts @@ -0,0 +1,33 @@ +import config from 'react-native-config'; + +export const normalizeUrl = (url?: string): string => { + if (!url) return ''; + return url.trim().replace(/\/+$/, '').toLowerCase(); +}; + +export const getAuthTokenForBaseUrl = (baseUrl: string | null): string => { + if (!baseUrl) return ''; + + const currentUrl = normalizeUrl(baseUrl); + + const envConfigs = [ + { + baseUrl: config.DEMO_CONVEXCRM_BASE_URL, + authToken: config.DEMO_CONVEXCRM_AUTH_TOKEN, + }, + { + baseUrl: config.CONVEXSOL_CONVEXCRM_BASE_URL, + authToken: config.CONVEXSOL_CONVEXCRM_AUTH_TOKEN, + }, + { + baseUrl: config.INTERNAL_CONVEXCRM_BASE_URL, + authToken: config.INTERNAL_CONVEXCRM_AUTH_TOKEN, + }, + ]; + + const matched = envConfigs.find( + item => item.baseUrl && normalizeUrl(item.baseUrl) === currentUrl, + ); + + return matched?.authToken || ''; +}; diff --git a/app/utils/index.ts b/app/utils/index.ts index 077b00a..1ae357a 100644 --- a/app/utils/index.ts +++ b/app/utils/index.ts @@ -3,3 +3,5 @@ export * from './assets'; export * from './api'; export * from './helper'; export * from './navigationRef'; +export * from './tenencyOptions'; +export * from './enviroment'; diff --git a/app/utils/tenencyOptions.ts b/app/utils/tenencyOptions.ts new file mode 100644 index 0000000..47434ba --- /dev/null +++ b/app/utils/tenencyOptions.ts @@ -0,0 +1,17 @@ +import config from 'react-native-config'; +import { FormPickerOption } from '@components'; + +export const TENANCY_OPTIONS: FormPickerOption[] = [ + { + label: 'Demo Convex CRM (demo-convexcrm.convexsol.co)', + value: config.DEMO_CONVEXCRM_BASE_URL || '', + }, + { + label: 'ConvexSol CRM (convexsol.convexcrm.com)', + value: config.CONVEXSOL_CONVEXCRM_BASE_URL || '', + }, + { + label: 'Internal CRM (internal.convexcrm.com)', + value: config.INTERNAL_CONVEXCRM_BASE_URL || '', + }, +];