Commit 5240ff3a by krds-arun

partial migrated react native code

parent e3e089a6
import { Tabs } from 'expo-router';
import React from 'react';
import { HapticTab } from '@/components/haptic-tab';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
export default function TabLayout() {
const colorScheme = useColorScheme();
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
tabBarButton: HapticTab,
}}>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'Explore',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
}}
/>
</Tabs>
);
}
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
import { Collapsible } from '@/components/ui/collapsible';
import { ExternalLink } from '@/components/external-link';
import ParallaxScrollView from '@/components/parallax-scroll-view';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Fonts } from '@/constants/theme';
export default function TabTwoScreen() {
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
headerImage={
<IconSymbol
size={310}
color="#808080"
name="chevron.left.forwardslash.chevron.right"
style={styles.headerImage}
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText
type="title"
style={{
fontFamily: Fonts.rounded,
}}>
Explore
</ThemedText>
</ThemedView>
<ThemedText>This app includes example code to help you get started.</ThemedText>
<Collapsible title="File-based routing">
<ThemedText>
This app has two screens:{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
</ThemedText>
<ThemedText>
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
sets up the tab navigator.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/router/introduction">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Android, iOS, and web support">
<ThemedText>
You can open this project on Android, iOS, and the web. To open the web version, press{' '}
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
</ThemedText>
</Collapsible>
<Collapsible title="Images">
<ThemedText>
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
different screen densities
</ThemedText>
<Image
source={require('@/assets/images/react-logo.png')}
style={{ width: 100, height: 100, alignSelf: 'center' }}
/>
<ExternalLink href="https://reactnative.dev/docs/images">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Light and dark mode components">
<ThemedText>
This template has light and dark mode support. The{' '}
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
what the user&apos;s current color scheme is, and so you can adjust UI colors accordingly.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Animations">
<ThemedText>
This template includes an example of an animated component. The{' '}
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
the powerful{' '}
<ThemedText type="defaultSemiBold" style={{ fontFamily: Fonts.mono }}>
react-native-reanimated
</ThemedText>{' '}
library to create a waving hand animation.
</ThemedText>
{Platform.select({
ios: (
<ThemedText>
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
component provides a parallax effect for the header image.
</ThemedText>
),
})}
</Collapsible>
</ParallaxScrollView>
);
}
const styles = StyleSheet.create({
headerImage: {
color: '#808080',
bottom: -90,
left: -35,
position: 'absolute',
},
titleContainer: {
flexDirection: 'row',
gap: 8,
},
});
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
import { HelloWave } from '@/components/hello-wave';
import ParallaxScrollView from '@/components/parallax-scroll-view';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { Link } from 'expo-router';
export default function HomeScreen() {
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
headerImage={
<Image
source={require('@/assets/images/partial-react-logo.png')}
style={styles.reactLogo}
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Welcome!</ThemedText>
<HelloWave />
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
<ThemedText>
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
Press{' '}
<ThemedText type="defaultSemiBold">
{Platform.select({
ios: 'cmd + d',
android: 'cmd + m',
web: 'F12',
})}
</ThemedText>{' '}
to open developer tools.
</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<Link href="/modal">
<Link.Trigger>
<ThemedText type="subtitle">Step 2: Explore</ThemedText>
</Link.Trigger>
<Link.Preview />
<Link.Menu>
<Link.MenuAction title="Action" icon="cube" onPress={() => alert('Action pressed')} />
<Link.MenuAction
title="Share"
icon="square.and.arrow.up"
onPress={() => alert('Share pressed')}
/>
<Link.Menu title="More" icon="ellipsis">
<Link.MenuAction
title="Delete"
icon="trash"
destructive
onPress={() => alert('Delete pressed')}
/>
</Link.Menu>
</Link.Menu>
</Link>
<ThemedText>
{`Tap the Explore tab to learn more about what's included in this starter app.`}
</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
<ThemedText>
{`When you're ready, run `}
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
<ThemedText type="defaultSemiBold">app-example</ThemedText>.
</ThemedText>
</ThemedView>
</ParallaxScrollView>
);
}
const styles = StyleSheet.create({
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
stepContainer: {
gap: 8,
marginBottom: 8,
},
reactLogo: {
height: 178,
width: 290,
bottom: 0,
left: 0,
position: 'absolute',
},
});
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated';
import { useColorScheme } from '@/hooks/use-color-scheme';
export const unstable_settings = {
anchor: '(tabs)',
};
export default function RootLayout() {
const colorScheme = useColorScheme();
import React, { FC } from 'react';
import 'react-native-gesture-handler';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import Toast from 'react-native-toast-message';
import { Provider } from "react-redux";
import { PersistGate } from 'redux-persist/integration/react';
import createAppStore from '../store/store';
const { store, persistor } = createAppStore();
export { store };
const App: FC = () => {
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
</PersistGate>
</Provider>
<Toast />
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
};
export default App;
import { Link } from 'expo-router';
import { StyleSheet } from 'react-native';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
export default function ModalScreen() {
return (
<ThemedView style={styles.container}>
<ThemedText type="title">This is a modal</ThemedText>
<Link href="/" dismissTo style={styles.link}>
<ThemedText type="link">Go to home screen</ThemedText>
</Link>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
link: {
marginTop: 15,
paddingVertical: 15,
},
});
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import React, { useCallback, useEffect, useState } from "react";
import {
Dimensions,
KeyboardAvoidingView,
Platform,
StatusBar,
StyleSheet,
Text,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useDispatch, useSelector } from "react-redux";
import { FilterComponent } from "../../components/common";
import { QuestionsForm } from "../../components/forms/QuestionsForm";
import Colors from "../constants/Colors";
import { submitAudit } from "../store/actions/auditFormScreenActions";
import {
draftAnswer,
removeSubmittedFormDraft,
} from "../store/actions/qustionActions";
import { RootState } from "../store/store"; // ✅ adjust to your store type
import { getNaAnswerData } from "../utils/questionUtils";
const { width } = Dimensions.get("window");
const zero_1_2 = [
{ value: "0", color: Colors.color_00, label: "00" },
{ value: "1", color: Colors.color_01, label: "01" },
{ value: "2", color: Colors.color_02, label: "02" },
{ value: "na", color: Colors.color_NA, label: "NA" },
];
const yes_no_na = [
{ value: "yes", color: Colors.color_02, label: "Y" },
{ value: "no", color: Colors.color_00, label: "N" },
{ value: "na", color: Colors.color_NA, label: "NA" },
];
type RootStackParamList = {
AuditForm: {
auditFormId: string;
selectedEntity: string;
auditTypeId: string;
selectedGroup: string[];
selectedBUManager: string;
};
};
type Props = NativeStackScreenProps<RootStackParamList, "AuditForm">;
const AuditFormScreen: React.FC<Props> = ({ route, navigation }) => {
const dispatch = useDispatch();
const { auditFormId, selectedEntity, auditTypeId, selectedGroup, selectedBUManager } =
route.params;
const { auditFormData, userInfo, draftAnswerData, incidentReport } =
useSelector((state: RootState) => ({
auditFormData: state.auditFormData,
userInfo: state.userInfo,
draftAnswerData: state.draftAnswerData,
incidentReport: state.incidentReport,
}));
const [state, setState] = useState({
photo: [] as string[],
selectedGroup: "",
isActionplan: false,
filter: "ViewAll",
showFilters: false,
progress: 0,
});
const auditForm = auditFormData.data.audit_forms[auditFormId] || {};
const audits = auditFormData.data.audit_types[selectedEntity] || [];
// ✅ set dynamic header
useEffect(() => {
const formName =
audits.find((item: any) => item.form_id === auditFormId)?.form || "";
if (Platform.OS === "ios") {
navigation.setOptions({
headerTitle: () => (
<SafeAreaView style={{ flex: 1, paddingLeft: 8, marginRight: 8 }}>
<Text numberOfLines={2} ellipsizeMode="tail" style={{ textAlign: "center", paddingLeft: 8 }}>
{formName.toUpperCase()}
</Text>
</SafeAreaView>
),
});
} else {
navigation.setOptions({
headerBackground: () => (
<View
style={{
flex: 1,
marginLeft: 20,
marginRight: 20,
justifyContent: "center",
width: width - 30,
}}
>
<Text numberOfLines={2} ellipsizeMode="tail" style={{ textAlign: "center", paddingLeft: 8 }}>
{formName.toUpperCase()}
</Text>
</View>
),
title: "",
});
}
}, [auditFormId, audits, navigation]);
const getRadioOptions = (radio_type: string) =>
radio_type === "0_1_2" ? zero_1_2 : yes_no_na;
const calculateProgress = useCallback((percentage: number) => {
setState((prev) => ({ ...prev, progress: Number((percentage / 100).toFixed(2)) }));
}, []);
const submitAuditCall = (data: any) => {
const token = userInfo?.userInfo?.access_token ?? "";
const naAnswer = getNaAnswerData(auditForm, selectedGroup);
dispatch(
submitAudit(
{ ...data, ...naAnswer },
auditFormId,
selectedEntity,
auditTypeId,
token,
selectedBUManager
)
);
};
const draftAnswers = (draftValues: any, formId: string, score: number) => {
dispatch(
draftAnswer(
draftValues,
formId,
"Audit",
selectedEntity,
selectedGroup,
"",
score,
selectedBUManager
)
);
};
const removeSubmittedFormDrafts = (formId: string) => {
dispatch(removeSubmittedFormDraft(formId, selectedEntity));
};
const groupListData = getGroupListData(auditForm, selectedGroup);
let questions = formatQuestions(auditForm, [], selectedGroup);
if (state.selectedGroup) {
questions = filterQuestions(questions, groupListData, state.selectedGroup);
}
const radioButtonOptions = getRadioOptions(auditForm.radio_type);
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
{...(Platform.OS === "ios" ? { keyboardVerticalOffset: 88, behavior: "padding" } : {})}
>
<View style={{ flex: 1 }}>
<StatusBar backgroundColor={Colors.statusBarColor} barStyle="light-content" />
<FilterComponent
isActionplan={state.isActionplan}
groupData={groupListData}
selectedGroup={state.selectedGroup}
filter={state.filter}
setFilter={(value) => setState((prev) => ({ ...prev, filter: value }))}
actionPlanToggle={() => setState((prev) => ({ ...prev, isActionplan: !prev.isActionplan }))}
setGroup={(value) => setState((prev) => ({ ...prev, selectedGroup: value }))}
progress={state.progress}
/>
<View style={styles.questionsForm}>
<QuestionsForm
selectedGroup={state.selectedGroup}
userEmails={incidentReport.incidentReportDashboardForm.user_emails}
isActionplan={state.isActionplan}
draftAnswer={draftAnswers}
removeSubmittedFormDraft={removeSubmittedFormDrafts}
draftAnswerData={draftAnswerData}
type="Audit"
formId={auditFormId}
isUnAnswered={state.filter === "unanswered"}
data={questions}
clearFilter={() => setState((prev) => ({ ...prev, selectedGroup: "", filter: "unanswered" }))}
submitAnswer={submitAuditCall}
radioButtonOptions={radioButtonOptions}
navigation={navigation}
selectedEntity={selectedEntity}
triggerActionPlanFilter={() => setState((prev) => ({ ...prev, isActionplan: true }))}
calculateProgress={calculateProgress}
/>
</View>
</View>
</KeyboardAvoidingView>
);
};
// ✅ helper fns with TS
const formatQuestions = (auditForm: any, questions: any[], selectedGroup: string[]) => {
if (!auditForm) return [];
Object.entries(auditForm.groups || {}).forEach(([key, value]: [string, any]) => {
if (selectedGroup.includes(key)) {
const newQuestions = value.questions.map((q: any) => ({
...q,
group_title: value.group_title,
}));
questions = [...questions, ...newQuestions];
}
});
return questions;
};
const getGroupListData = (auditForm: any, selectedGroup: string[]) => {
const result: { title: string; key: string }[] = [];
if (!auditForm) return result;
Object.entries(auditForm.groups || {}).forEach(([key, value]: [string, any]) => {
if (selectedGroup.includes(key)) {
result.push({ title: value.group_title, key });
}
});
return result;
};
const filterQuestions = (questions: any[], groupListData: any[], selectedGroup: string) => {
const groupTitle = groupListData.find((item) => item.key === selectedGroup)?.title ?? "";
return questions.filter((q) => q.group_title === groupTitle);
};
const styles = StyleSheet.create({
questionsForm: {
backgroundColor: "#FFF",
flexGrow: 1,
},
});
export default AuditFormScreen;
import { AntDesign as Icon } from '@expo/vector-icons';
import * as DocumentPicker from 'expo-document-picker';
import * as ImagePicker from 'expo-image-picker';
import React, { useEffect, useState } from 'react';
import {
Dimensions,
Image,
ScrollView,
Text,
TouchableOpacity,
View
} from 'react-native';
import Colors from '../../constants/colors';
const { height } = Dimensions.get('window');
interface FileData {
key: number;
uri: string;
type: string;
data?: string;
name?: string;
size?: string;
}
interface ArrayComponentProps {
type: 'image' | 'video' | 'docs';
value?: FileData[];
updateFormikData: (data: FileData[]) => void;
}
const ArrayComponent: React.FC<ArrayComponentProps> = ({ type, value, updateFormikData }) => {
const [data, setData] = useState<FileData[]>([]);
const [error, setError] = useState<string>('');
useEffect(() => {
if (Array.isArray(value)) {
setData(value);
}
}, [value]);
const bytesToSize = (bytes: number) => {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) return '0 Byte';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${Math.round(bytes / Math.pow(1024, i))} ${sizes[i]}`;
};
const handleAddFile = async () => {
if (type !== 'docs') {
// Image or video picker
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: type === 'image' ? ImagePicker.MediaTypeOptions.Images : ImagePicker.MediaTypeOptions.Videos,
quality: 0.1,
});
if (!result.canceled) {
const asset = result.assets[0];
const newFile: FileData = {
key: data.length ? data[data.length - 1].key + 1 : 0,
uri: asset.uri,
type: type === 'image' ? asset.type ?? 'image/jpeg' : 'video/mp4',
data: asset.base64 ?? undefined, // convert null to undefined
name: asset.fileName ?? `file${data.length + 1}.${type === 'image' ? 'jpg' : 'mp4'}`,
size: asset.fileSize ? bytesToSize(asset.fileSize) : undefined,
};
setData(prev => [...prev, newFile]);
}
} else {
// Document picker
const result = await DocumentPicker.getDocumentAsync({ multiple: true });
if (!result.canceled && result.assets && result.assets.length > 0) {
result.assets.forEach((asset, idx) => {
const newFile: FileData = {
key: data.length ? data[data.length - 1].key + 1 + idx : idx,
uri: asset.uri,
type: asset.mimeType || '',
name: asset.name,
size: asset.size ? bytesToSize(asset.size) : undefined,
};
setData(prev => [...prev, newFile]);
});
}
}
};
const handleRemoveFile = (key: number) => {
setData(prev => prev.filter(d => d.key !== key));
};
const saveData = () => {
updateFormikData(data);
};
return (
<View style={{ width: '90%' }}>
{error.length > 0 && <Text style={{ color: 'red', marginBottom: 8 }}>{error}</Text>}
{type === 'docs' && (
<Text style={{ color: Colors.colorText, marginBottom: 8 }}>
Only <Text style={{ fontStyle: 'italic', fontWeight: '700' }}>".doc, .docx, .pdf, .xlsx, .xls"</Text> files are allowed
</Text>
)}
{data.length > 0 && (
<Text style={{ color: 'black' }}>
Attachments <Text style={{ color: Colors.buttonColor }}>({data.length} {data.length > 1 ? 'files' : 'file'})</Text>
</Text>
)}
<ScrollView style={{ height: data.length > 0 ? height / 2.5 : 0, marginTop: 10, marginBottom: 10 }}>
{data.map((item, index) => (
<View key={item.key} style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
<Image style={{ height: 50, width: 50 }} source={{ uri: item.data ? `data:image/jpeg;base64,${item.data}` : item.uri }} />
<View style={{ marginLeft: 10, flex: 2, marginRight: 32 }}>
<Text style={{ color: Colors.buttonColor, fontSize: 12 }} numberOfLines={1} ellipsizeMode="tail">
{item.name || `file00${index + 1}`}
</Text>
<Text style={{ color: 'black', fontSize: 12 }}>{item.size}</Text>
</View>
<Icon name="close-circle" size={20} color={Colors.colorPlaceholder} onPress={() => handleRemoveFile(item.key)} />
</View>
))}
</ScrollView>
<TouchableOpacity
disabled={data.length > 4}
style={{ padding: 8, backgroundColor: Colors.buttonColor, borderRadius: 5, marginBottom: 10 }}
onPress={handleAddFile}
>
<Text style={{ color: 'white', textAlign: 'center' }}>Add {type === 'docs' ? 'Document' : type}</Text>
</TouchableOpacity>
<TouchableOpacity style={{ padding: 8, backgroundColor: 'green', borderRadius: 5 }} onPress={saveData}>
<Text style={{ color: 'white', textAlign: 'center' }}>Save</Text>
</TouchableOpacity>
</View>
);
};
export { ArrayComponent };
import React from "react";
import { Image, ImageSourcePropType, StyleSheet, Text, View } from "react-native";
import Colors from '../../constants/colors';
interface BannerProps {
image: ImageSourcePropType;
title?: string;
isGroup?: boolean;
location?: string;
form?: string;
type?: string;
marginVertical?: number;
}
const Banner: React.FC<BannerProps> = ({
image,
title,
isGroup = false,
location,
form,
type,
marginVertical = 0,
}) => {
return (
<View style={[styles.container, { marginVertical }]}>
<Image source={image} style={styles.logo} />
{isGroup ? (
<View style={styles.groupInfo}>
<Text style={styles.label}>LOCATION :</Text>
<Text style={styles.value}>{location}</Text>
<Text style={styles.label}>{type} :</Text>
<Text style={styles.value}>{form}</Text>
</View>
) : (
<View style={{ flex: 1 }}>
<Text style={styles.title}>{title}</Text>
</View>
)}
</View>
);
};
export { Banner };
const styles = StyleSheet.create({
container: {
backgroundColor: Colors.colorWhite,
flexDirection: 'row',
alignItems: 'center',
borderRadius: 10,
padding: 8,
paddingLeft: 12,
marginBottom: 20,
marginTop: 20,
},
logo: {
resizeMode: 'contain',
height: 70,
width: 70,
margin: 8,
},
groupInfo: {
marginLeft: 15,
flex: 1,
justifyContent: 'center',
},
label: {
fontSize: 14,
color: Colors.colorTextInput,
},
value: {
fontSize: 14,
color: Colors.selectedColorSelect2,
},
title: {
fontSize: 24,
color: Colors.colorTextInput,
textAlign: 'left',
fontWeight: '200',
marginHorizontal: 10,
},
});
import React, { useState } from "react";
import { Dimensions, StyleSheet, Text, TouchableOpacity, View } from "react-native";
import * as Progress from "react-native-progress";
import Colors from "../../constants/colors";
const { width } = Dimensions.get("window");
interface BottomButtonProps {
pressCallBack: () => void;
buttonText?: string;
backgroundColor?: string;
draftPress?: () => void;
draft?: boolean;
progress?: number;
progressScore?: string | number;
isSubmitting?: boolean;
}
const BottomButton: React.FC<BottomButtonProps> = ({
pressCallBack,
buttonText = "SUBMIT",
backgroundColor,
draftPress,
draft = false,
progress = 0,
progressScore = "",
isSubmitting = false,
}) => {
const [active, setActive] = useState(false);
const bgColor = backgroundColor || "#C1AD84";
if (draft) {
return (
<View style={styles.draftContainer}>
{/* Floating Progress Button */}
<TouchableOpacity
style={styles.progressCircleContainer}
onPress={() => setActive(!active)}
>
<Progress.Circle
unfilledColor={Colors.appBackgroundColor}
color={Colors.buttonColor}
fill="white"
textStyle={{ fontSize: 14, fontWeight: "bold" }}
showsText
size={70}
progress={progress}
formatText={() => String(progressScore)}
thickness={5}
/>
</TouchableOpacity>
{!active && (
<TouchableOpacity
disabled={isSubmitting}
style={styles.submitButton}
onPress={pressCallBack}
>
<Text style={styles.submitText}>SUBMIT</Text>
</TouchableOpacity>
)}
{!active && draftPress && (
<TouchableOpacity style={styles.saveButton} onPress={draftPress}>
<Text style={styles.saveText}>SAVE</Text>
</TouchableOpacity>
)}
</View>
);
}
return (
<View style={{ flexDirection: "row", marginBottom: 0 }}>
<TouchableOpacity
disabled={isSubmitting}
style={[styles.container, { backgroundColor: bgColor, justifyContent: "flex-end", marginLeft: 0, borderRadius: 0 }]}
onPress={pressCallBack}
>
<Text style={styles.buttonText}>{buttonText}</Text>
</TouchableOpacity>
</View>
);
};
export { BottomButton };
const styles = StyleSheet.create({
container: {
flexDirection: "row",
height: 50,
alignContent: "center",
padding: 8,
flex: 1,
},
draftContainer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
position: "absolute",
bottom: 10,
left: 4,
right: 10,
backgroundColor: "transparent",
},
progressCircleContainer: {
width: 70,
height: 70,
borderRadius: 35,
backgroundColor: "#CCFF0000",
alignItems: "center",
justifyContent: "center",
},
submitButton: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 4.65,
borderRadius: width / 5,
height: 40,
elevation: 8,
backgroundColor: "#C1AD84",
width: width / 2 - 70,
marginLeft: 100,
marginRight: 20,
marginBottom: 25,
alignItems: "center",
justifyContent: "center",
},
saveButton: {
backgroundColor: "white",
width: width / 2 - 70,
borderWidth: 1,
borderColor: "#C1AD84",
borderRadius: width / 5,
height: 40,
marginRight: 20,
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 4.65,
elevation: 8,
marginBottom: 25,
alignItems: "center",
justifyContent: "center",
},
submitText: { color: "white", fontSize: 14, padding: 4, fontWeight: "400", paddingLeft: 10 },
saveText: { color: "#C1AD84", fontSize: 14, padding: 4, fontWeight: "400", paddingLeft: 10 },
buttonText: { fontSize: 20, color: "white", alignSelf: "center", fontWeight: "700", marginRight: 10 },
});
import React, { useEffect, useRef, useState } from 'react';
import { Animated, Easing, SafeAreaView, StyleSheet, Text, TouchableOpacity } from 'react-native';
import Modal from 'react-native-modal';
interface AlertOption {
label: string;
onPress: () => void;
}
interface CustomAlertProps {
visible?: boolean;
options: AlertOption[];
onClose?: () => void;
}
const CustomAlert: React.FC<CustomAlertProps> = ({ visible = true, options, onClose }) => {
const [show, setShow] = useState(visible);
const scaleAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (show) {
Animated.spring(scaleAnim, {
toValue: 1,
useNativeDriver: true,
friction: 5,
tension: 50,
}).start();
}
}, [show]);
const closeModal = () => {
Animated.timing(scaleAnim, {
toValue: 0,
duration: 200,
easing: Easing.out(Easing.ease),
useNativeDriver: true,
}).start(() => {
setShow(false);
if (onClose) onClose();
});
};
return (
<SafeAreaView>
<Modal
isVisible={show}
onBackdropPress={closeModal}
backdropOpacity={0.5}
animationIn="fadeIn"
animationOut="fadeOut"
>
<Animated.View style={[styles.mainContainer, { transform: [{ scale: scaleAnim }] }]}>
{options.map((option, index) => (
<TouchableOpacity
key={index}
style={styles.option}
onPress={() => {
option.onPress();
closeModal();
}}
>
<Text style={styles.optionText}>{option.label}</Text>
</TouchableOpacity>
))}
</Animated.View>
</Modal>
</SafeAreaView>
);
};
export default CustomAlert;
const styles = StyleSheet.create({
mainContainer: {
backgroundColor: 'white',
borderRadius: 12,
padding: 16,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 12 },
shadowOpacity: 0.58,
shadowRadius: 16,
elevation: 24,
},
option: {
paddingVertical: 12,
width: '100%',
alignItems: 'center',
},
optionText: {
fontSize: 16,
color: '#333',
},
});
import { Picker } from '@react-native-picker/picker';
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
interface GroupItem {
key: string;
title: string;
}
interface FilterComponentProps {
groupData: GroupItem[];
selectedGroup: string;
setGroup: (value: string) => void;
filter: string;
setFilter: (value: string) => void;
actionPlanToggle: () => void;
isActionplan: boolean;
}
const FilterComponent: React.FC<FilterComponentProps> = ({
groupData,
selectedGroup,
setGroup,
filter,
setFilter,
actionPlanToggle,
isActionplan,
}) => {
return (
<View style={{ backgroundColor: '#ECECEC', marginHorizontal: 8, height: 115 }}>
<View style={styles.header}>
<View style={styles.answerPicker}>
<Picker
selectedValue={filter}
onValueChange={(value) => setFilter(value)}
style={{ color: '#9B9B9B', fontSize: 12 }}
dropdownIconColor="#9B9B9B"
>
<Picker.Item label="View All" value="ViewAll" />
<Picker.Item label="Unanswered" value="unanswered" />
</Picker>
</View>
<TouchableOpacity
style={[styles.actionPlan, { backgroundColor: isActionplan ? '#1BD7A4' : 'white' }]}
onPress={actionPlanToggle}
>
<Text style={{ color: isActionplan ? 'white' : 'gray', fontSize: 12 }}>ACTION PLAN</Text>
</TouchableOpacity>
</View>
<View style={styles.groupPicker}>
<Picker
selectedValue={selectedGroup}
onValueChange={(value) => setGroup(value)}
style={{ color: '#9B9B9B', fontSize: 12 }}
dropdownIconColor="#9B9B9B"
>
<Picker.Item label="All" value="" />
{groupData.map((item, i) => (
<Picker.Item key={i} label={item.title} value={item.key} />
))}
</Picker>
</View>
</View>
);
};
export { FilterComponent };
const styles = StyleSheet.create({
answerPicker: {
borderWidth: 1,
width: '45%',
marginRight: 4,
backgroundColor: 'white',
borderColor: '#ECECEC',
borderRadius: 8,
justifyContent: 'center',
height: 40,
},
actionPlan: {
width: 150,
flex: 2,
marginLeft: 8,
alignItems: 'center',
borderRadius: 8,
justifyContent: 'center',
},
groupPicker: {
borderWidth: 1,
backgroundColor: 'white',
borderColor: '#ECECEC',
borderRadius: 8,
justifyContent: 'center',
marginHorizontal: 8,
height: 40,
},
header: {
flexDirection: 'row',
margin: 8,
marginTop: 14,
},
});
import { MaterialIcons } from '@expo/vector-icons';
import { Box, HStack, Icon, Pressable, Text } from 'native-base';
import React from 'react';
import { FlatList } from 'react-native';
import Colors from '../../constants/colors';
interface ListItemData {
form_id: string | number;
id: string | number;
technical_form_id?: string | number;
operational_form_id?: string | number;
selectedEntity?: string;
selectedGroup?: string;
type?: string;
checklistSubmissionId?: string | number;
selectedBUManager?: string;
title: string;
}
interface ListComponentProps {
data: ListItemData[];
listClick: (
form_id: string | number,
id: string | number,
technical_form_id?: string | number,
operational_form_id?: string | number,
selectedEntity?: string,
selectedGroup?: string,
type?: string,
checklistSubmissionId?: string | number,
selectedBUManager?: string
) => void;
onDelete: (
form_id: string | number,
id: string | number,
technical_form_id?: string | number,
operational_form_id?: string | number,
selectedEntity?: string,
selectedGroup?: string
) => void;
}
export const ListComponent: React.FC<ListComponentProps> = ({ data, listClick, onDelete }) => {
const renderItem = ({ item }: { item: ListItemData }) => {
const isDraft = item.title.includes(' [draft]');
const displayTitle = item.title.replace(' [draft]', '');
return (
<Box borderBottomWidth={1} borderColor="#ECECEC" py={2} px={3}>
<HStack alignItems="center" justifyContent="space-between">
<HStack alignItems="center">
{isDraft && (
<Pressable
onPress={() =>
onDelete(
item.form_id,
item.id,
item.technical_form_id,
item.operational_form_id,
item.selectedEntity,
item.selectedGroup
)
}
mr={3}
>
<Icon as={MaterialIcons} name="delete" size={5} color={Colors.colorTextInput} />
</Pressable>
)}
<Pressable
onPress={() =>
listClick(
item.form_id,
item.id,
item.technical_form_id,
item.operational_form_id,
item.selectedEntity,
item.selectedGroup,
item.type,
item.checklistSubmissionId,
item.selectedBUManager
)
}
>
<Text color={Colors.colorTextInput}>{displayTitle}</Text>
</Pressable>
</HStack>
<Icon as={MaterialIcons} name="arrow-forward-ios" size={5} color={Colors.colorTextInput} />
</HStack>
</Box>
);
};
return <FlatList data={data} renderItem={renderItem} keyExtractor={(item, index) => `${item.id}-${index}`} />;
};
import { Entypo as EntypoIcon } from '@expo/vector-icons';
import { Box, HStack, Icon, Modal, Pressable, ScrollView, Text, VStack } from 'native-base';
import React, { useEffect, useState } from 'react';
import { Alert, Image } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import Colors from '../../constants/colors';
import { getNotifications } from '../../store/actions/notificationActions';
import type { AppDispatch } from '../../store/store'; // Add this import - adjust path to your store
import { setAuthentication } from '../../utils/authentication';
interface MenuButtonProps {
navigation: any;
logout: () => void;
online: boolean;
syncData: () => void;
access_token?: string;
}
export const MenuButton: React.FC<MenuButtonProps> = ({ navigation, logout, online, syncData, access_token }) => {
const [visible, setVisible] = useState(false);
const state = useSelector((state: any) => state.notificationData);
const notifications = state.notifications || [];
const unread_count = state.unread_count || 0;
const dispatch = useDispatch<AppDispatch>(); // Add AppDispatch type here
const offline = useSelector((state: any) => state.offline);
if (access_token) {
setAuthentication(access_token);
}
useEffect(() => {
if (access_token && online) {
dispatch(getNotifications());
}
}, [online]);
const handleLogout = () => {
Alert.alert(
'Logout',
'Are you sure you want to logout?',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'OK', onPress: logout }
],
{ cancelable: false }
);
setVisible(false);
};
return (
<HStack alignItems="center">
{/* Notification Icon */}
<Pressable
onPress={() =>
notifications.length > 0
? navigation.navigate('Notifications', { online: offline.online })
: alert('You have no notifications')
}
>
<Image
source={require('../../assets/images/notification.png')}
style={{ width: 30, height: 35, margin: 10 }}
/>
{unread_count > 0 && (
<Box
position="absolute"
left={25}
bottom={27}
width={25}
height={25}
borderRadius={12.5}
bg="#8A1538"
alignItems="center"
justifyContent="center"
>
<Text color="white" fontSize={10} fontWeight="bold">
{unread_count}
</Text>
</Box>
)}
</Pressable>
{/* Menu Icon */}
<Pressable onPress={() => setVisible(true)}>
<Icon as={EntypoIcon} name="dots-three-vertical" size={8} color={Colors.buttonColor} mr={2} />
</Pressable>
{/* Modal */}
<Modal isOpen={visible} onClose={() => setVisible(false)} safeAreaTop>
<Modal.Content maxWidth="350px" alignSelf="flex-end">
<Modal.Body p={0}>
<ScrollView>
<VStack>
{online && (
<>
<Pressable
onPress={() => {
syncData();
setVisible(false);
}}
>
<HStack alignItems="center" justifyContent="space-between" p={3}>
<Text fontWeight="600">Update Forms</Text>
<Icon as={EntypoIcon} name="cycle" size={6} color={Colors.buttonColor} />
</HStack>
</Pressable>
<Box h={1} bg="#E6E6E6" />
</>
)}
<Pressable
onPress={() => {
navigation.navigate('OfflineLog');
setVisible(false);
}}
>
<HStack alignItems="center" justifyContent="space-between" p={3}>
<Text fontWeight="600">Pending Jobs</Text>
<Icon as={EntypoIcon} name="export" size={6} color={Colors.buttonColor} />
</HStack>
</Pressable>
<Box h={1} bg="#E6E6E6" />
<Pressable
onPress={() => {
navigation.navigate('FailedJobs');
setVisible(false);
}}
>
<HStack alignItems="center" justifyContent="space-between" p={3}>
<Text fontWeight="600">Retry Jobs</Text>
<Icon as={EntypoIcon} name="ccw" size={6} color={Colors.buttonColor} />
</HStack>
</Pressable>
<Box h={1} bg="#E6E6E6" />
<Pressable
onPress={() => {
navigation.navigate('History');
setVisible(false);
}}
>
<HStack alignItems="center" justifyContent="space-between" p={3}>
<Text fontWeight="600">History</Text>
<Icon as={EntypoIcon} name="back-in-time" size={6} color={Colors.buttonColor} />
</HStack>
</Pressable>
<Box h={1} bg="#E6E6E6" />
<Pressable
onPress={() => {
navigation.navigate('ResetPasswordScreen');
setVisible(false);
}}
>
<HStack alignItems="center" justifyContent="space-between" p={3}>
<Text fontWeight="600">Reset Password</Text>
<Icon as={EntypoIcon} name="retweet" size={6} color={Colors.buttonColor} />
</HStack>
</Pressable>
<Box h={1} bg="#E6E6E6" />
{online && (
<>
<Pressable onPress={handleLogout}>
<HStack alignItems="center" justifyContent="space-between" p={3}>
<Text fontWeight="600">Logout</Text>
<Icon as={EntypoIcon} name="log-out" size={6} color={Colors.buttonColor} />
</HStack>
</Pressable>
<Box h={1} bg="#E6E6E6" />
</>
)}
<Pressable
onPress={() => {
navigation.navigate('ContactUsPage');
setVisible(false);
}}
>
<HStack alignItems="center" justifyContent="space-between" p={3}>
<Text fontWeight="600">Contact Us</Text>
<Icon as={EntypoIcon} name="phone" size={6} color={Colors.buttonColor} />
</HStack>
</Pressable>
</VStack>
</ScrollView>
</Modal.Body>
</Modal.Content>
</Modal>
</HStack>
);
};
\ No newline at end of file
import React from 'react';
import { StyleSheet, TextStyle } from 'react-native';
import Spinner from 'react-native-loading-spinner-overlay';
import * as Progress from 'react-native-progress';
interface OverlaySpinnerProps {
visible: boolean;
textContent?: string;
customIndicator?: boolean;
progress?: number; // 0 to 1
}
const OverlaySpinner: React.FC<OverlaySpinnerProps> = ({
visible,
textContent = 'Loading...',
customIndicator = false,
progress = 0,
}) => {
const renderCustomIndicator = () => {
if (!customIndicator) return null;
return (
<Progress.Circle
size={100}
progress={progress}
animated
thickness={15}
showsText
indeterminate={progress === 0}
formatText={(p) => `${Math.round(progress * 100)}%`}
/>
);
};
return (
<Spinner
visible={visible}
textContent={textContent}
textStyle={styles.spinnerTextStyle}
customIndicator={renderCustomIndicator()}
/>
);
};
const styles = StyleSheet.create({
spinnerTextStyle: {
color: '#FFF',
fontSize: 16,
} as TextStyle,
});
export { OverlaySpinner };
import { MaterialCommunityIcons as Icon } from '@expo/vector-icons';
import React, { useEffect, useRef, useState } from 'react';
import {
Animated,
Dimensions,
FlatList,
Platform,
StyleSheet,
Text,
TextInput,
TextStyle,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native';
import Modal from 'react-native-modal';
import Colors from '../../constants/colors';
import Button from './lib/Button';
import TagItem from './lib/TagItem';
import utilities from './lib/utilities';
const { height } = Dimensions.get('window');
const INIT_HEIGHT = height * 0.6;
interface Option {
id: string;
name?: string;
title?: string;
form?: string;
checked?: boolean;
}
interface Select2Props {
data: Option[];
style?: ViewStyle;
modalStyle?: ViewStyle;
title?: string;
popupTitle?: string;
colorTheme?: string;
isSelectSingle?: boolean;
cancelButtonText?: string;
selectButtonText?: string;
listEmptyTitle ?: string;
searchPlaceHolderText?: string;
showSearchBox?: boolean;
selectedTitleStyle?: TextStyle;
selectedPlaceholderTextColor?: TextStyle;
buttonTextStyle?: TextStyle;
buttonStyle?: ViewStyle;
defaultFontName?: string;
isEnable?: boolean;
onSelect?: (selectedIds: string[], selectedItems: Option[]) => void;
onRemoveItem?: (selectedIds: string[], selectedItems: Option[]) => void;
}
const Select2: React.FC<Select2Props> = ({
data: propData,
style,
modalStyle,
title,
popupTitle,
colorTheme = '#16a45f',
isSelectSingle = false,
cancelButtonText = 'Cancel',
selectButtonText = 'Select',
showSearchBox = true,
selectedTitleStyle,
selectedPlaceholderTextColor,
buttonTextStyle,
buttonStyle,
defaultFontName,
isEnable = true,
onSelect,
onRemoveItem,
}) => {
const [show, setShow] = useState(false);
const [selectedItem, setSelectedItem] = useState<Option[]>([]);
const [preSelectedItem, setPreSelectedItem] = useState<Option[]>([]);
const [data, setData] = useState<Option[]>(propData);
const [keyword, setKeyword] = useState('');
const animatedHeight = useRef(new Animated.Value(INIT_HEIGHT)).current;
// Initialize preselected items
useEffect(() => {
const preSelected = propData.filter((item) => item.checked);
setData(propData);
setPreSelectedItem(preSelected);
}, [propData]);
const dataRender = data.filter((item) =>
utilities
.changeAlias(item.name || item.title || item.form || '')
.includes(utilities.changeAlias(keyword))
);
const defaultFont: TextStyle = defaultFontName ? { fontFamily: defaultFontName } : {};
const cancelSelection = () => {
const resetData = data.map((item) => ({
...item,
checked: preSelectedItem.some((pre) => pre.id === item.id),
}));
setData(resetData);
setSelectedItem(preSelectedItem);
setShow(false);
setKeyword('');
};
const onItemSelected = (item: Option) => {
let updatedData = [...data];
const newItem = { ...item, checked: !item.checked };
updatedData = updatedData.map((d) =>
d.id === newItem.id ? newItem : isSelectSingle ? { ...d, checked: false } : d
);
const selectedItems = updatedData.filter((d) => d.checked);
setData(updatedData);
setSelectedItem(selectedItems);
};
const handleConfirmSelection = () => {
const selectedIds = selectedItem.map((i) => i.id);
onSelect && onSelect(selectedIds, selectedItem);
setPreSelectedItem(selectedItem);
setShow(false);
setKeyword('');
};
const handleRemoveTag = (tag: Option) => {
const updatedData = data.map((item) =>
item.id === tag.id ? { ...item, checked: false } : item
);
const newSelected = updatedData.filter((d) => d.checked);
setData(updatedData);
setPreSelectedItem(newSelected);
const selectedIds = newSelected.map((i) => i.id);
onRemoveItem && onRemoveItem(selectedIds, newSelected);
};
const renderItem = ({ item }: { item: Option }) => (
<TouchableOpacity
onPress={() => onItemSelected(item)}
activeOpacity={0.7}
style={styles.itemWrapper}
>
<Text style={[styles.itemText, defaultFont]}>{item.name || item.form || item.title}</Text>
<Icon
style={styles.itemIcon}
name={item.checked ? 'check-circle-outline' : 'radiobox-blank'}
color={item.checked ? colorTheme : '#777777'}
size={20}
/>
</TouchableOpacity>
);
return (
<TouchableOpacity
onPress={isEnable ? () => setShow(true) : undefined}
activeOpacity={0.7}
style={[styles.container, style]}
>
<Modal
onBackdropPress={() => setShow(false)}
style={{ justifyContent: 'flex-end', margin: 0 }}
useNativeDriver
animationInTiming={300}
animationOutTiming={300}
hideModalContentWhileAnimating
isVisible={show}
>
<Animated.View style={[styles.modalContainer, modalStyle, { height: animatedHeight }]}>
<Text style={[styles.title, defaultFont, { color: colorTheme }]}>
{popupTitle || title}
</Text>
<View style={styles.line} />
{showSearchBox && (
<TextInput
style={[styles.inputKeyword, defaultFont]}
placeholder="Search..."
selectionColor={colorTheme}
onChangeText={setKeyword}
onFocus={() =>
Animated.spring(animatedHeight, {
toValue: INIT_HEIGHT + (Platform.OS === 'ios' ? height * 0.2 : 0),
friction: 7,
useNativeDriver: false,
}).start()
}
onBlur={() =>
Animated.spring(animatedHeight, {
toValue: INIT_HEIGHT,
friction: 7,
useNativeDriver: false,
}).start()
}
/>
)}
<FlatList
style={styles.listOption}
data={dataRender.sort((a, b) => (a.name || '').localeCompare(b.name || ''))}
keyExtractor={(item, index) => index.toString()}
renderItem={renderItem}
ListEmptyComponent={<Text style={[styles.empty, defaultFont]}>No options found</Text>}
/>
<View style={styles.buttonWrapper}>
<Button
defaultFont={defaultFont}
onPress={cancelSelection}
title={cancelButtonText}
textColor={colorTheme}
backgroundColor="#fff"
textStyle={buttonTextStyle}
style={[styles.button, { marginRight: 5, marginLeft: 10, borderWidth: 1, borderColor: colorTheme }]}
/>
<Button
defaultFont={defaultFont}
onPress={handleConfirmSelection}
title={selectButtonText}
backgroundColor={colorTheme}
textStyle={buttonTextStyle}
style={[styles.button, { marginLeft: 5, marginRight: 10 }]}
/>
</View>
</Animated.View>
</Modal>
{preSelectedItem.length > 0 ? (
isSelectSingle ? (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={[styles.selectedTitlte, defaultFont, selectedTitleStyle]}>
{preSelectedItem[0].name || preSelectedItem[0].form || preSelectedItem[0].title}
</Text>
<Icon style={styles.itemIcon} name="chevron-down" color={Colors.colorPlaceholder} size={30} />
</View>
) : (
<View style={styles.tagWrapper}>
{preSelectedItem.map((tag) => (
<TagItem key={tag.id} tagName={tag.name || ''} onRemoveTag={() => handleRemoveTag(tag)} />
))}
</View>
)
) : (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={[styles.selectedTitlte, defaultFont, selectedPlaceholderTextColor]}>{title}</Text>
<Icon style={styles.itemIcon} name="chevron-down" color={Colors.colorPlaceholder} size={30} />
</View>
)}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
width: '100%',
minHeight: 40,
borderRadius: 2,
paddingHorizontal: 16,
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
paddingVertical: 4,
},
modalContainer: {
paddingTop: 16,
backgroundColor: '#fff',
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
},
title: {
fontSize: 16,
marginBottom: 16,
width: '100%',
textAlign: 'center',
},
line: {
height: 1,
width: '100%',
backgroundColor: '#cacaca',
},
inputKeyword: {
height: 40,
borderRadius: 5,
borderWidth: 1,
paddingLeft: 8,
marginHorizontal: 24,
marginTop: 16,
},
buttonWrapper: {
marginVertical: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
button: {
height: 36,
flex: 1,
},
selectedTitlte: {
fontSize: 14,
color: 'gray',
flex: 1,
},
tagWrapper: {
flexDirection: 'row',
flexWrap: 'wrap',
},
listOption: {
paddingHorizontal: 24,
paddingTop: 1,
marginTop: 16,
},
itemWrapper: {
borderBottomWidth: 1,
borderBottomColor: '#eaeaea',
paddingVertical: 12,
flexDirection: 'row',
alignItems: 'center',
},
itemText: {
fontSize: 16,
color: '#333',
flex: 1,
},
itemIcon: {
width: 30,
textAlign: 'right',
},
empty: {
fontSize: 16,
color: 'gray',
alignSelf: 'center',
textAlign: 'center',
paddingTop: 16,
},
});
export default Select2;
\ No newline at end of file
import React from 'react';
import { ActivityIndicator, ActivityIndicatorProps, StyleSheet, View } from 'react-native';
interface SpinnerProps {
size?: ActivityIndicatorProps['size']; // 'small' | 'large' | number
}
const Spinner: React.FC<SpinnerProps> = ({ size = 'large' }) => {
return (
<View style={styles.spinnerStyle}>
<ActivityIndicator size={size} />
</View>
);
};
const styles = StyleSheet.create({
spinnerStyle: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
export { Spinner };
import React from 'react';
import { Dimensions, Image, ImageSourcePropType, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Colors from '../../constants/colors';
const { width } = Dimensions.get('window');
const tileHeight = width / 2 - 30;
interface TileProps {
title: string;
onSelectPress: () => void;
}
const Tile: React.FC<TileProps> = ({ title, onSelectPress }) => {
let image: ImageSourcePropType = require('@/assets/images/permit.png');
let titleText = title === 'Permit' ? 'CONTRACTORS & SUPPLIERS MANAGEMENT' : title;
switch (title) {
case 'CheckList':
image = require('@/assets/images/checklist.png');
titleText = 'OPERATIONAL & TECHNICAL CHECKLIST';
break;
case 'Inspection':
image = require('@/assets/images/inspection.png');
titleText = 'INSPECTION SYSTEM';
break;
case 'Auditing':
image = require('../../assets/images/audits.png');
titleText = 'AUDIT SYSTEM';
break;
case 'Suggestions':
image = require('../../assets/images/suggestions.png');
break;
case 'IncidentReport':
image = require('../../assets/images/incidents.png');
titleText = 'INCIDENT REPORTING';
break;
}
return (
<View style={styles.gridItem}>
<TouchableOpacity style={styles.container} onPress={onSelectPress}>
<Image source={image} style={styles.logo} />
<Text style={styles.text}>{titleText}</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
gridItem: {
flex: 1,
margin: 15,
height: tileHeight,
shadowColor: 'rgba(0,0,0,0.38)',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.37,
shadowRadius: 7.49,
elevation: 12,
marginTop: 8,
marginBottom: 8,
alignItems: 'center',
justifyContent: 'center',
},
container: {
flex: 1,
shadowColor: 'rgba(0,0,0,0.38)',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.22,
shadowRadius: 2.22,
elevation: 3,
padding: 15,
justifyContent: 'center',
alignItems: 'center',
width: tileHeight,
height: tileHeight,
borderRadius: tileHeight / 2,
backgroundColor: Colors.colorWhite,
borderColor: 'rgba(0,0,0,0.17)',
borderWidth: 1,
},
text: {
color: Colors.colorTextInput,
fontSize: 14,
fontWeight: 'bold',
textAlign: 'center',
marginTop: 5,
},
logo: {
resizeMode: 'contain',
height: width / 11,
width: width / 11,
},
});
export { Tile };
// src/components/common/index.ts
export { ArrayComponent } from './ArrayComponent';
export { Banner } from './Banner';
export { BottomButton } from './BottomButton';
export { FilterComponent } from './FilterComponent';
export { ListComponent } from './ListComponent';
export { MenuButton } from './MenuButton';
export { OverlaySpinner } from './OverlaySpinner';
export { Spinner } from './Spinner';
export { Tile } from './Tile';
import React from 'react';
import { StyleSheet, Text, TextStyle, TouchableOpacity, ViewStyle } from 'react-native';
interface ButtonProps {
title: string;
backgroundColor?: string;
textColor?: string;
style?: ViewStyle | ViewStyle[];
textStyle?: TextStyle | TextStyle[];
onPress?: () => void;
disabled?: boolean;
defaultFont?: TextStyle | TextStyle[];
}
const Button: React.FC<ButtonProps> = ({
title,
backgroundColor = '#2196F3',
textColor = '#fff',
style,
textStyle,
onPress,
disabled = false,
defaultFont,
}) => {
return (
<TouchableOpacity
activeOpacity={0.7}
disabled={disabled}
onPress={onPress}
style={[
styles.button,
{
backgroundColor: disabled ? 'gray' : backgroundColor,
alignSelf: 'center',
borderRadius: 25,
},
style, // only ViewStyle here
]}
>
<Text
style={[
styles.buttonText,
defaultFont, // applied safely to Text
{ color: textColor },
textStyle,
]}
>
{title}
</Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
height: 45,
paddingHorizontal: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
} as ViewStyle,
buttonText: {
fontSize: 16,
fontWeight: 'bold',
} as TextStyle,
});
export default Button;
import { MaterialCommunityIcons as Icon } from '@expo/vector-icons';
import React from 'react';
import { GestureResponderEvent, StyleSheet, Text, TouchableOpacity } from 'react-native';
interface TagItemProps {
tagName: string;
onRemoveTag: (event: GestureResponderEvent) => void;
}
const TagItem: React.FC<TagItemProps> = ({ tagName, onRemoveTag }) => {
return (
<TouchableOpacity onPress={onRemoveTag} style={styles.container}>
<Icon name="close" size={14} color="#333" />
<Text style={styles.text}>{tagName}</Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
paddingVertical: 4,
paddingHorizontal: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f5f6f5',
borderWidth: 1,
borderColor: '#e9e9e9',
margin: 4,
borderRadius: 3,
},
text: {
fontSize: 14,
color: '#333',
paddingLeft: 4,
},
});
export default TagItem;
const changeAlias = (alias?: string): string => {
let str = alias?.toLowerCase() || '';
str = str.replace(/à|á|ạ|ả|ã|â|ầ|ấ|ậ|ẩ|ẫ|ă|ằ|ắ|ặ|ẳ|ẵ/g, 'a');
str = str.replace(/è|é|ẹ|ẻ|ẽ|ê|ề|ế|ệ|ể|ễ/g, 'e');
str = str.replace(/ì|í|ị|ỉ|ĩ/g, 'i');
str = str.replace(/ò|ó|ọ|ỏ|õ|ô|ồ|ố|ộ|ổ|ỗ|ơ|ờ|ớ|ợ|ở|ỡ/g, 'o');
str = str.replace(/ù|ú|ụ|ủ|ũ|ư|ừ|ứ|ự|ử|ữ/g, 'u');
str = str.replace(/ỳ|ý|ỵ|ỷ|ỹ/g, 'y');
str = str.replace(/đ/g, 'd');
str = str.replace(/[!@%\^*\(\)\+=<>\?\/,;'"&#\[\]~$_`{}|\\]/g, ' ');
str = str.replace(/\s+/g, ' ');
str = str.trim();
return str;
};
export default { changeAlias };
import { FieldInputProps, FormikErrors } from 'formik';
import React, { useState } from 'react';
import {
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import DateTimePickerModal from 'react-native-modal-datetime-picker';
import Select2 from '../../components/common/Select2';
import Colors from '../../constants/colors';
interface Option {
id: string;
name: string;
checked?: boolean;
}
interface UserEmail {
id: string;
name: string;
checked?: boolean;
}
interface FormikActionsProps {
userEmails: UserEmail[];
value: Record<string, any>;
field: FieldInputProps<any>;
setFieldValue: (field: string, value: any) => void;
mode?: 'date' | 'time' | 'datetime';
placeholder?: string;
validationErrors: FormikErrors<any>;
}
const FormikActions: React.FC<FormikActionsProps> = ({
userEmails,
value = {},
field,
setFieldValue,
mode = 'date',
placeholder = 'Enter text',
validationErrors,
}) => {
const {
body = '',
field_critical_to_safety = '',
close_immediately = '0',
field_responsible_person = '',
} = value;
const [isDatePickerVisible, setIsDatePickerVisible] = useState(false);
const [date, setDate] = useState<string>('');
const criticalToSafetyOptions: Option[] = [
{ id: '1', name: 'Yes' },
{ id: '0', name: 'No' },
].map((item) => ({
...item,
checked: item.id === field_critical_to_safety,
}));
const closeImmediatelyOptions: Option[] = [
{ id: '0', name: 'No' },
{ id: '1', name: 'Yes' },
].map((item) => ({
...item,
checked: item.id === close_immediately,
}));
const responsiblePersonOptions: UserEmail[] = userEmails.map((item) => ({
...item,
checked: item.id === field_responsible_person,
}));
return (
<>
{/* Action Plan */}
<View style={styles.viewContainer}>
<Text style={[styles.text, { fontWeight: 'bold' }]}>Action Plan</Text>
<Text style={styles.text}>
Corrective Action{' '}
{validationErrors.body && <Text style={styles.errorTextStyle}>*</Text>}
</Text>
<TextInput
placeholder={placeholder}
placeholderTextColor={Colors.colorGray}
value={body}
onChangeText={(text) =>
setFieldValue(field.name, { ...value, body: text })
}
style={[
styles.textInput,
{ borderColor: validationErrors.body ? 'red' : '#ECECEC' },
]}
/>
</View>
{/* Critical To Safety */}
<View style={styles.viewContainer}>
<Text style={styles.text}>
Select Critical To Safety{' '}
{validationErrors.field_critical_to_safety && (
<Text style={styles.errorTextStyle}>*</Text>
)}
</Text>
<Select2
isSelectSingle
title="Select Critical To Safety"
colorTheme="#b69b30"
showSearchBox={false}
style={StyleSheet.flatten([
styles.textInput,
{ borderColor: validationErrors.field_critical_to_safety ? 'red' : '#ECECEC' },
])}
popupTitle="Select Critical To Safety"
cancelButtonText="Cancel"
selectButtonText="OK"
buttonTextStyle={{ fontSize: 11 }}
selectedTitleStyle={{
color:
field_critical_to_safety === ''
? Colors.colorGray
: field_critical_to_safety === '0'
? 'green'
: 'red',
}}
data={criticalToSafetyOptions}
onSelect={(data: string[]) => {
setFieldValue(
field.name,
data && data[0]
? { ...value, field_critical_to_safety: data[0] }
: { ...value, field_critical_to_safety: '' }
);
}}
onRemoveItem={() => {}}
/>
</View>
{/* Close Immediately */}
<View style={styles.viewContainer}>
<Text style={styles.text}>
Select Close Immediately{' '}
{validationErrors.close_immediately && (
<Text style={styles.errorTextStyle}>*</Text>
)}
</Text>
<Select2
isSelectSingle
title="Select Close Immediately"
colorTheme="#b69b30"
showSearchBox={false}
popupTitle="Select Close Immediately"
cancelButtonText="Cancel"
selectButtonText="OK"
style={StyleSheet.flatten([
styles.textInput,
{
borderColor: validationErrors.close_immediately ? 'red' : '#ECECEC',
},
])}
buttonTextStyle={{ fontSize: 11 }}
selectedTitleStyle={{
color:
close_immediately === ''
? Colors.colorGray
: close_immediately === '1'
? 'green'
: 'red',
}}
data={closeImmediatelyOptions}
onSelect={(data: string[]) => {
setFieldValue(
field.name,
data && data[0]
? { ...value, close_immediately: data[0] }
: { ...value, close_immediately: '' }
);
}}
onRemoveItem={() => {}}
/>
</View>
{/* Responsible Person & Target Date */}
{close_immediately === '0' && (
<>
{/* Responsible Person */}
<View style={styles.viewContainer}>
<Text style={styles.text}>
Select Responsible Person{' '}
{validationErrors.field_responsible_person && (
<Text style={styles.errorTextStyle}>*</Text>
)}
</Text>
<Select2
isSelectSingle
title="Select Responsible Person"
colorTheme="#b69b30"
popupTitle="Select Responsible Person"
cancelButtonText="Cancel"
selectButtonText="OK"
buttonTextStyle={{ fontSize: 11 }}
style={StyleSheet.flatten([
styles.textInput,
{ borderColor: validationErrors.field_critical_to_safety ? 'red' : '#ECECEC' },
])}
selectedTitleStyle={{ color: Colors.textColor }}
data={responsiblePersonOptions}
onSelect={(data: string[]) => {
setFieldValue(
field.name,
data && data[0]
? { ...value, field_responsible_person: data[0] }
: { ...value, field_responsible_person: '' }
);
}}
onRemoveItem={() => {}}
/>
</View>
{/* Target Date */}
<TouchableOpacity
onPress={() => setIsDatePickerVisible(true)}
style={styles.viewContainer}
>
<Text style={styles.text}>
Target Date{' '}
{validationErrors.field_target_date && (
<Text style={styles.errorTextStyle}>*</Text>
)}
</Text>
<TextInput
editable={false}
placeholderTextColor={Colors.textColor}
pointerEvents="none"
style={[
styles.textInput,
{
borderColor: validationErrors.field_target_date
? 'red'
: '#ECECEC',
},
]}
placeholder="Target Date"
value={date}
/>
</TouchableOpacity>
<DateTimePickerModal
isVisible={isDatePickerVisible}
textColor={Colors.textColor}
mode={mode}
minimumDate={new Date()}
onConfirm={(pickedDate: Date) => {
setIsDatePickerVisible(false);
const formatted = `${pickedDate.getFullYear()}-${pickedDate.getMonth() + 1}-${pickedDate.getDate()}`;
setDate(
`${pickedDate.getDate()}-${pickedDate.getMonth() + 1}-${pickedDate.getFullYear()}`
);
setFieldValue(field.name, { ...value, field_target_date: formatted });
}}
onCancel={() => setIsDatePickerVisible(false)}
/>
</>
)}
</>
);
};
const styles = StyleSheet.create({
textInput: {
borderColor: '#ECECEC',
borderWidth: 1,
borderRadius: 20,
paddingLeft: 15,
marginRight: 1,
marginTop: 8,
height: 40,
fontSize: 12,
width: '100%',
backgroundColor: '#ECECEC',
color: Colors.textColor,
},
viewContainer: {
justifyContent: 'center',
margin: 4,
},
text: {
marginRight: 10,
marginLeft: 5,
color: '#9B9B9B',
},
errorTextStyle: {
fontSize: 15,
alignSelf: 'center',
color: 'red',
marginVertical: 1,
},
});
export { FormikActions };
import React, { useEffect, useState } from 'react';
import { TextInput, TouchableOpacity } from 'react-native';
import DateTimePickerModal from 'react-native-modal-datetime-picker';
import Colors from '../../constants/colors';
import CommonStyles from '../../constants/CommonStyles';
interface FormikDatePickerProps {
field: { name: string; value?: any };
setFieldValue: (field: string, value: any) => void;
mode?: 'date' | 'time' | 'datetime';
placeholder?: string;
minDate?: Date;
maxDate?: Date;
startDate?: Date | 'false';
}
const FormikDatePicker: React.FC<FormikDatePickerProps> = ({
field,
setFieldValue,
mode = 'date',
placeholder = 'Select date',
minDate,
maxDate,
startDate,
}) => {
const [isPickerVisible, setPickerVisible] = useState(false);
const [displayDate, setDisplayDate] = useState<string>('');
useEffect(() => {
// If field already has value, format it for display
if (field.value) {
const dateObj = new Date(field.value);
setDisplayDate(formatDate(dateObj));
}
}, [field.value]);
const formatDate = (date: Date) => {
if (mode === 'time') {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`;
};
const handleConfirm = (selectedDate: Date) => {
setPickerVisible(false);
setDisplayDate(formatDate(selectedDate));
setFieldValue(field.name, selectedDate);
};
const handlePress = () => {
if (startDate === 'false') {
alert('Please Select Start Date');
} else {
setPickerVisible(true);
}
};
return (
<>
<TouchableOpacity onPress={handlePress}>
<TextInput
editable={false}
pointerEvents="none"
style={[CommonStyles.textInput]}
placeholder={placeholder}
placeholderTextColor={Colors.colorPlaceholder}
value={displayDate}
/>
</TouchableOpacity>
<DateTimePickerModal
isVisible={isPickerVisible}
mode={mode}
textColor={Colors.textColor}
maximumDate={maxDate}
minimumDate={
minDate
? minDate
: startDate && startDate !== 'false'
? new Date(startDate)
: undefined
}
onConfirm={handleConfirm}
onCancel={() => setPickerVisible(false)}
/>
</>
);
};
export { FormikDatePicker };
import { AntDesign as Icon } from '@expo/vector-icons';
import React, { useState } from 'react';
import {
Image,
SafeAreaView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import Modal from 'react-native-modal';
import Colors from '../../constants/colors';
import { ArrayComponent } from '../common';
interface FormikDocumentPickerProps {
field: { name: string; value?: any[] };
setFieldValue: (field: string, value: any) => void;
value?: any[];
backgroundColor?: string;
textColor?: string;
disabled?: boolean;
}
type DocumentType = 'image' | 'video' | 'docs';
const FormikDocumentPicker: React.FC<FormikDocumentPickerProps> = ({
field,
setFieldValue,
value = [],
backgroundColor = '#b69b30',
textColor = '#fff',
disabled = true,
}) => {
const [isAttachModalVisible, setAttachModalVisible] = useState(false);
const [isPickerModalVisible, setPickerModalVisible] = useState(false);
const [type, setType] = useState<DocumentType>('image');
const getImageByType = (docType: DocumentType) => {
switch (docType) {
case 'image':
return require('@/assets/images/camera.png');
case 'video':
return require('@/assets/images/video.png');
case 'docs':
return require('@/assets/images/upload_file.png');
default:
return require('@/assets/images/camera.png');
}
};
const openPicker = (docType: DocumentType) => {
setType(docType);
setPickerModalVisible(true);
};
return (
<>
<SafeAreaView>
{/* Attach Modal */}
<Modal
isVisible={isAttachModalVisible}
onBackButtonPress={() => setAttachModalVisible(false)}
onBackdropPress={() => setAttachModalVisible(false)}
>
<View style={styles.modalContent}>
<Icon
name="close-circle"
size={24}
color={Colors.colorPlaceholder}
style={styles.closeIcon}
onPress={() => setAttachModalVisible(false)}
/>
<View style={styles.attachOptions}>
<TouchableOpacity
style={styles.attachButton}
onPress={() => openPicker('image')}
>
<Text style={styles.attachButtonText}>Image</Text>
<Image
source={require('@/assets/images/camera.png')}
style={styles.logo}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.attachButton}
onPress={() => openPicker('docs')}
>
<Text style={styles.attachButtonText}>File</Text>
<Image
source={require('@/assets/images/upload_file.png')}
style={styles.logo}
/>
</TouchableOpacity>
</View>
</View>
</Modal>
{/* Document Picker Modal */}
<Modal
isVisible={isPickerModalVisible}
onBackButtonPress={() => setPickerModalVisible(false)}
onBackdropPress={() => setPickerModalVisible(false)}
>
<View style={styles.modalContent}>
<Icon
name="close-circle"
size={24}
color={Colors.colorPlaceholder}
style={styles.closeIcon}
onPress={() => setPickerModalVisible(false)}
/>
<ArrayComponent
value={value}
type={type}
updateFormikData={(data) => {
setFieldValue(field.name, data);
setPickerModalVisible(false);
setAttachModalVisible(false);
}}
/>
</View>
</Modal>
</SafeAreaView>
{/* Attach Button */}
<TouchableOpacity
style={[
styles.attachContainer,
{
backgroundColor: value.length > 0 ? backgroundColor : '#ECECEC',
},
]}
onPress={() => {
disabled && setAttachModalVisible(true);
}}
>
<Text
style={[
styles.attachText,
{ color: value.length > 0 ? textColor : Colors.textColor },
]}
>
Attach
</Text>
<Image source={getImageByType(type)} style={styles.logo} />
</TouchableOpacity>
</>
);
};
const styles = StyleSheet.create({
modalContent: {
backgroundColor: 'white',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
closeIcon: {
alignSelf: 'flex-end',
marginBottom: 12,
},
attachOptions: {
flexDirection: 'row',
justifyContent: 'space-around',
width: '100%',
},
attachButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: Colors.colorTextInput,
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 8,
},
attachButtonText: {
color: '#fff',
fontSize: 14,
fontWeight: '600',
marginRight: 6,
},
attachContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
borderWidth: 1,
borderColor: '#ECECEC',
padding: 10,
height: 40,
width: '30%',
marginLeft: 8,
},
attachText: {
fontSize: 12,
textAlign: 'center',
},
logo: {
width: 24,
height: 24,
resizeMode: 'contain',
marginLeft: 4,
tintColor: '#fff',
},
});
export { FormikDocumentPicker };
import { AntDesign as Icon } from '@expo/vector-icons';
import React, { useState } from 'react';
import {
Image,
SafeAreaView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import Modal from 'react-native-modal';
import Colors from '../../constants/colors';
import { ArrayComponent } from '../common';
interface FormikImagePickerProps {
field: { name: string; value?: any[] };
setFieldValue: (field: string, value: any) => void;
value?: any[];
type: 'image' | 'video' | 'docs';
}
const FormikImagePicker: React.FC<FormikImagePickerProps> = ({
field,
setFieldValue,
value = [],
type,
}) => {
const [isModalVisible, setModalVisible] = useState(false);
const getImageAndTitle = (docType: FormikImagePickerProps['type']) => {
switch (docType) {
case 'image':
return { image: require('@/assets/images/camera.png'), title: 'Image' };
case 'video':
return { image: require('@/assets/images/video.png'), title: 'Video' };
case 'docs':
return { image: require('@/assets/images/upload_file.png'), title: 'File' };
default:
return { image: require('@/assets/images/camera.png'), title: 'Attach' };
}
};
const { image, title } = getImageAndTitle(type);
return (
<>
<TouchableOpacity
style={styles.attach}
onPress={() => setModalVisible(true)}
>
<Text style={styles.attachText}>{title}</Text>
<Image source={image} style={styles.logo} />
</TouchableOpacity>
<SafeAreaView>
<Modal
isVisible={isModalVisible}
onBackButtonPress={() => setModalVisible(false)}
onBackdropPress={() => setModalVisible(false)}
>
<View style={styles.content}>
<Icon
name="close-circle" // updated icon name
size={24}
color={Colors.colorPlaceholder}
style={styles.closeIcon}
onPress={() => setModalVisible(false)}
/>
<ArrayComponent
value={value}
type={type}
updateFormikData={(data) => {
setFieldValue(field.name, data);
setModalVisible(false);
}}
/>
</View>
</Modal>
</SafeAreaView>
</>
);
};
const styles = StyleSheet.create({
content: {
backgroundColor: 'white',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
closeIcon: {
alignSelf: 'flex-end',
marginBottom: 12,
},
logo: {
width: 24,
height: 24,
resizeMode: 'contain',
marginLeft: 8,
tintColor: '#fff',
},
attach: {
flexDirection: 'row',
alignItems: 'center',
width: '25%',
borderRadius: 8,
justifyContent: 'center',
paddingVertical: 6,
paddingHorizontal: 8,
backgroundColor: Colors.colorTextInput,
},
attachText: {
color: '#fff',
fontSize: 14,
fontWeight: '600',
},
});
export { FormikImagePicker };
import React, { memo, useCallback } from 'react';
import { FlatList, StyleSheet, Text, TouchableOpacity, ViewStyle } from 'react-native';
interface RadioOption {
label: string;
value: string | number;
color?: string;
}
interface FormikQuestionAnswersProps {
field: { name: string; value?: string | number };
setFieldValue: (field: string, value: any) => void;
radioButtonOptions: RadioOption[];
style?: ViewStyle;
value?: string | number;
}
const FormikQuestionAnswers: React.FC<FormikQuestionAnswersProps> = memo(
({ field, setFieldValue, radioButtonOptions, style, value }) => {
const selectedIndex = radioButtonOptions.findIndex((item) => item.value === value);
const setFieldValues = useCallback(
(val: string | number) => {
setFieldValue(field.name, val);
},
[field.name, setFieldValue]
);
const renderAnswers = ({ item }: { item: RadioOption }) => (
<TouchableOpacity
style={[
styles.optionButton,
{ backgroundColor: item.color || '#ccc' },
value === item.value && styles.selectedOption,
]}
onPress={() => setFieldValues(item.value)}
>
<Text style={styles.optionLabel}>{item.label}</Text>
</TouchableOpacity>
);
return (
<FlatList
style={style}
keyExtractor={(item) => `${item.value}`}
data={radioButtonOptions}
renderItem={renderAnswers}
horizontal
showsHorizontalScrollIndicator={false}
/>
);
},
(prevProps, nextProps) => prevProps.value === nextProps.value
);
const styles = StyleSheet.create({
optionButton: {
height: 40,
width: 40,
marginHorizontal: 4,
borderRadius: 20,
alignItems: 'center',
justifyContent: 'center',
},
selectedOption: {
borderWidth: 2,
borderColor: '#000',
},
optionLabel: {
color: '#fff',
fontSize: 12,
textAlign: 'center',
},
});
export { FormikQuestionAnswers };
import React, { memo, useCallback } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import Colors from '../../constants/colors';
interface RadioOption {
label: string;
value: string | number;
}
interface FormikRadioButtonsProps {
field: { name: string; value?: string | number };
setFieldValue: (field: string, value: any) => void;
radioButtonOptions: RadioOption[];
value?: string | number;
label?: string;
}
const FormikRadioButtons: React.FC<FormikRadioButtonsProps> = memo(
({ field, setFieldValue, radioButtonOptions, value, label }) => {
const selectedValue = value ?? field.value;
const onSelect = useCallback(
(val: string | number) => {
setFieldValue(field.name, val);
},
[field.name, setFieldValue]
);
return (
<View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>}
<View style={styles.radioGroup}>
{radioButtonOptions.map((option) => {
const isSelected = option.value === selectedValue;
return (
<Pressable
key={option.value}
style={styles.radioButtonContainer}
onPress={() => onSelect(option.value)}
>
<View
style={[
styles.radioButtonOuter,
{ borderColor: Colors.radioButtonColor },
]}
>
{isSelected && (
<View
style={[
styles.radioButtonInner,
{ backgroundColor: Colors.radioButtonColor },
]}
/>
)}
</View>
<Text
style={[
styles.radioLabel,
{ color: isSelected ? Colors.radioButtonColor : '#000' },
]}
>
{option.label}
</Text>
</Pressable>
);
})}
</View>
</View>
);
},
(prevProps, nextProps) => prevProps.value === nextProps.value
);
const styles = StyleSheet.create({
container: {
flexDirection: 'column',
marginVertical: 4,
},
label: {
marginBottom: 4,
color: Colors.radioButtonColor,
fontSize: 14,
},
radioGroup: {
flexDirection: 'row',
flexWrap: 'wrap',
},
radioButtonContainer: {
flexDirection: 'row',
alignItems: 'center',
marginRight: 12,
marginBottom: 4,
},
radioButtonOuter: {
width: 18,
height: 18,
borderRadius: 9,
borderWidth: 2,
justifyContent: 'center',
alignItems: 'center',
},
radioButtonInner: {
width: 10,
height: 10,
borderRadius: 5,
},
radioLabel: {
marginLeft: 4,
fontSize: 14,
},
});
export { FormikRadioButtons };
import React, { useEffect, useState } from 'react';
import { StyleSheet, ViewStyle } from 'react-native';
import Select2 from '../../components/common/Select2';
import Colors from '../../constants/colors';
import CommonStyles from '../../constants/CommonStyles';
interface SelectOption {
id: string | number;
name: string;
}
interface FormikSelectProps {
field: { name: string; value?: any };
setFieldValue: (field: string, value: any) => void;
data: SelectOption[];
title?: string;
colorTheme?: string;
popupTitle?: string;
cancelButtonText?: string;
selectButtonText?: string;
searchPlaceHolderText?: string;
isSelectSingle?: boolean;
marginTop?: number;
}
const FormikSelect: React.FC<FormikSelectProps> = ({
field,
setFieldValue,
data,
title = 'Select an option',
colorTheme = Colors.primaryColor,
popupTitle = 'Select',
cancelButtonText = 'Cancel',
selectButtonText = 'Select',
searchPlaceHolderText = 'Search...',
isSelectSingle = true,
marginTop = 12,
}) => {
const [dataCopy, setDataCopy] = useState<{ id: string; name: string }[]>([]);
useEffect(() => {
const sortedData = [...data]
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
.map(item => ({ ...item, id: String(item.id) })); // Convert id to string
setDataCopy(sortedData);
}, [data]);
// Combine styles into a single object for Select2
const selectStyle: ViewStyle = {
...CommonStyles.textInput,
marginTop,
};
return (
<Select2
listEmptyTitle="No options available"
isSelectSingle={isSelectSingle}
title={title}
colorTheme={colorTheme}
popupTitle={popupTitle}
cancelButtonText={cancelButtonText}
selectButtonText={selectButtonText}
searchPlaceHolderText={searchPlaceHolderText}
selectedTitleStyle={{ color: Colors.selectedColorSelect2 }}
selectedPlaceholderTextColor={{ color: Colors.colorPlaceholder }}
data={dataCopy}
onSelect={(selected: any) => setFieldValue(field.name, selected)}
onRemoveItem={(removed: any) => setFieldValue(field.name, removed)}
style={selectStyle}
/>
);
};
const styles = StyleSheet.create({
item: {
marginVertical: 10,
paddingLeft: 15,
},
});
export { FormikSelect };
import React from 'react';
import { StyleProp, TextInput, TextInputProps, TextStyle } from 'react-native';
import Colors from '../../constants/colors';
interface FormikTextInputProps extends TextInputProps {
item: { questionId: string };
name: string;
value: string;
handleBlur: (field: string) => void;
handleChange: (field: string) => (text: string) => void;
style?: StyleProp<TextStyle>;
disabled?: boolean;
placeholder?: string;
}
const FormikTextInput: React.FC<FormikTextInputProps> = ({
item,
name,
value,
handleBlur,
handleChange,
style,
disabled = false,
placeholder,
...rest
}) => {
return (
<TextInput
editable={!disabled}
placeholder={placeholder}
placeholderTextColor={Colors.textColor}
value={value}
onBlur={() => handleBlur(`${item.questionId}_observation`)}
onChangeText={handleChange(`${item.questionId}_observation`)}
style={style}
{...rest}
/>
);
};
export { FormikTextInput };
import React, { useCallback, useEffect, useState } from 'react';
import { StyleSheet, Text, TextInput, View, ViewStyle } from 'react-native';
import Select2 from '../../components/common/Select2';
import Colors from '../../constants/colors';
import CommonStyles from '../../constants/CommonStyles';
interface Option {
id: string; // must be string for Select2
name: string;
checked?: boolean;
}
interface InvolvedPerson {
involvedPersonName: string;
phoneNumber: string;
address: string;
involvedUserRole: string[]; // always string[]
user: string[];
Nationality: string[];
}
interface InvolvedPeopleProps {
name: string;
value: { involvedPeople: InvolvedPerson[] };
index: number;
setFieldValue: (field: string, value: any) => void;
involvedUserRolesOptions: Option[];
usersOptions: Option[];
nationalityOptions: Option[];
contractor_emails: Option[];
arrayHelpers: { remove: (index: number) => void };
}
const InvolvedPeople: React.FC<InvolvedPeopleProps> = ({
name,
value,
index,
setFieldValue,
involvedUserRolesOptions,
usersOptions,
nationalityOptions,
contractor_emails,
arrayHelpers,
}) => {
const { involvedPeople } = value;
const person = involvedPeople[index];
// normalize options to string id for Select2
const normalizeOptions = (
options: (Option | { id: string | number; name: string })[]
): Option[] => options.map((o) => ({ id: o.id.toString(), name: o.name }));
const [userRoleOptions, setUserRoleOptions] = useState<Option[]>([]);
const [usersOptionCopy, setUsersOptionCopy] = useState<Option[]>([]);
const [nationalityOptionsCopy, setNationalityOptionsCopy] = useState<Option[]>([]);
const [contractorEmailsCopy, setContractorEmailsCopy] = useState<Option[]>([]);
useEffect(() => {
setUserRoleOptions(normalizeOptions(involvedUserRolesOptions));
setUsersOptionCopy(normalizeOptions(usersOptions));
setNationalityOptionsCopy(normalizeOptions(nationalityOptions));
setContractorEmailsCopy(normalizeOptions(contractor_emails));
}, [involvedUserRolesOptions, usersOptions, nationalityOptions, contractor_emails]);
const personRoleType = userRoleOptions.find(
(r) => r.id === person.involvedUserRole[0]
)?.name || '';
const handleSelect = useCallback(
(fieldKey: keyof InvolvedPerson, selected: string[]) => {
// Create a new person object with the updated field
const updatedPerson = { ...person, [fieldKey]: selected };
const updatedPeople = [...involvedPeople];
updatedPeople[index] = updatedPerson;
setFieldValue(name, updatedPeople);
},
[involvedPeople, name, person, setFieldValue, index]
);
const handleTextChange = useCallback(
(fieldKey: 'involvedPersonName' | 'phoneNumber' | 'address', text: string) => {
// Create a new person object with the updated field
const updatedPerson = { ...person, [fieldKey]: text };
const updatedPeople = [...involvedPeople];
updatedPeople[index] = updatedPerson;
setFieldValue(name, updatedPeople);
},
[involvedPeople, name, person, setFieldValue, index]
);
return (
<View key={index} style={styles.itemContainer}>
{/* Header Row */}
<View style={styles.headerRow}>
<Text style={styles.headerText}>Involved Person #{index + 1}</Text>
{index > 0 && (
<Text style={styles.removeText} onPress={() => arrayHelpers.remove(index)}>
Remove
</Text>
)}
</View>
{/* Role Select */}
<Select2
isSelectSingle
style={CommonStyles.incidentField as ViewStyle}
title="Select Person Role Type"
colorTheme="#b69b30"
popupTitle="Select Person Role Type"
cancelButtonText="Cancel"
selectButtonText="OK"
selectedTitleStyle={{ color: Colors.selectedColorSelect2 }}
selectedPlaceholderTextColor={{ color: Colors.colorPlaceholder }}
searchPlaceHolderText="Search..."
listEmptyTitle="Options Empty"
data={userRoleOptions}
onSelect={(data: string[]) => handleSelect('involvedUserRole', data)}
onRemoveItem={() => {}}
/>
{(personRoleType === '' || personRoleType === 'Staff' || personRoleType === 'Contractor') && (
<>
{(personRoleType === 'Staff' || personRoleType === 'Contractor') ? (
<Select2
isSelectSingle
style={CommonStyles.incidentField as ViewStyle}
title="User Email"
colorTheme="#b69b30"
popupTitle="Select User"
cancelButtonText="Cancel"
selectButtonText="OK"
selectedTitleStyle={{ color: Colors.selectedColorSelect2 }}
selectedPlaceholderTextColor={{ color: Colors.colorPlaceholder }}
searchPlaceHolderText="Search..."
listEmptyTitle="Options Empty"
data={personRoleType === 'Contractor' ? contractorEmailsCopy : usersOptionCopy}
onSelect={(data: string[]) => handleSelect('user', data)}
onRemoveItem={() => {}}
/>
) : (
<>
<TextInput
style={CommonStyles.incidentField}
placeholder="Involved Person Name"
placeholderTextColor={Colors.colorPlaceholder}
value={person.involvedPersonName}
onChangeText={(text: string) => handleTextChange('involvedPersonName', text)}
/>
<TextInput
style={CommonStyles.incidentField}
placeholder="Phone Number"
keyboardType="numeric"
placeholderTextColor={Colors.colorPlaceholder}
value={person.phoneNumber}
onChangeText={(text: string) => handleTextChange('phoneNumber', text)}
/>
<Select2
isSelectSingle
style={CommonStyles.incidentField as ViewStyle}
title="Nationality"
colorTheme="#b69b30"
popupTitle="Nationality"
cancelButtonText="Cancel"
selectButtonText="OK"
selectedTitleStyle={{ color: Colors.selectedColorSelect2 }}
selectedPlaceholderTextColor={{ color: Colors.colorPlaceholder }}
searchPlaceHolderText="Search..."
listEmptyTitle="Options Empty"
data={nationalityOptionsCopy}
onSelect={(data: string[]) => handleSelect('Nationality', data)}
onRemoveItem={() => {}}
/>
<TextInput
style={[CommonStyles.incidentField, styles.textArea]}
placeholder="Address"
placeholderTextColor={Colors.colorPlaceholder}
value={person.address}
onChangeText={(text: string) => handleTextChange('address', text)}
multiline
numberOfLines={4}
textAlignVertical="top"
/>
</>
)}
</>
)}
</View>
);
};
const styles = StyleSheet.create({
itemContainer: {
borderRadius: 5,
backgroundColor: Colors.appBackgroundColor,
borderWidth: 1,
padding: 8,
marginVertical: 5,
borderColor: Colors.appBackgroundColor,
},
headerRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 5,
},
headerText: {
color: '#606060',
fontSize: 14,
margin: 4,
flex: 2,
},
removeText: {
marginLeft: 5,
color: '#C1AD84',
},
textArea: {
minHeight: 80,
paddingTop: 10,
},
});
export { InvolvedPeople };
import React, { useCallback, useEffect, useState } from 'react';
import { StyleSheet, Text, TextInput, View, ViewStyle } from 'react-native';
import Select2 from '../../components/common/Select2';
import Colors from '../../constants/colors';
import CommonStyles from '../../constants/CommonStyles';
interface Option {
id: string;
name: string;
checked?: boolean;
}
interface Witness {
involvedPersonName: string;
phoneNumber: string;
address: string;
involvedUserRole: string[];
user: string[];
Nationality: string[];
}
interface WitnessesProps {
name: string;
value: { witnesses: Witness[] };
index: number;
setFieldValue: (field: string, value: any) => void;
involvedUserRolesOptions: Option[];
usersOptions: Option[];
nationalityOptions: Option[];
contractor_emails: Option[];
arrayHelpers: { remove: (index: number) => void };
}
const Witnesses: React.FC<WitnessesProps> = ({
name,
value,
index,
setFieldValue,
involvedUserRolesOptions,
usersOptions,
nationalityOptions,
contractor_emails,
arrayHelpers,
}) => {
// Normalize options to string id for Select2
const normalizeOptions = (
options: (Option | { id: string | number; name: string })[]
): Option[] => options.map((o) => ({ id: o.id.toString(), name: o.name }));
const [usersOptionsCopy, setUsersOptionsCopy] = useState<Option[]>([]);
const [contractorEmailsCopy, setContractorEmailsCopy] = useState<Option[]>([]);
const [nationalityOptionsCopy, setNationalityOptionsCopy] = useState<Option[]>([]);
const [involvedUserRolesOptionsCopy, setInvolvedUserRolesOptionsCopy] = useState<Option[]>([]);
const [personRoleType, setPersonRoleType] = useState('');
useEffect(() => {
setInvolvedUserRolesOptionsCopy(normalizeOptions(involvedUserRolesOptions));
setContractorEmailsCopy(normalizeOptions(contractor_emails));
setUsersOptionsCopy(normalizeOptions(usersOptions));
setNationalityOptionsCopy(normalizeOptions(nationalityOptions));
}, [involvedUserRolesOptions, usersOptions, nationalityOptions, contractor_emails]);
const { witnesses } = value;
const thisWitness = witnesses[index];
const handleRoleSelect = useCallback(
(data: string[]) => {
const result = involvedUserRolesOptionsCopy.find((item) => item.id === data[0]);
setPersonRoleType(result?.name || '');
const updatedWitness = { ...thisWitness, involvedUserRole: data };
const updatedWitnesses = [...witnesses];
updatedWitnesses[index] = updatedWitness;
setFieldValue(name, updatedWitnesses);
},
[involvedUserRolesOptionsCopy, thisWitness, witnesses, index, name, setFieldValue]
);
const handleSelect = useCallback(
(fieldKey: keyof Witness, selected: string[]) => {
const updatedWitness = { ...thisWitness, [fieldKey]: selected };
const updatedWitnesses = [...witnesses];
updatedWitnesses[index] = updatedWitness;
setFieldValue(name, updatedWitnesses);
},
[thisWitness, witnesses, index, name, setFieldValue]
);
const handleTextChange = useCallback(
(fieldKey: 'involvedPersonName' | 'phoneNumber' | 'address', text: string) => {
const updatedWitness = { ...thisWitness, [fieldKey]: text };
const updatedWitnesses = [...witnesses];
updatedWitnesses[index] = updatedWitness;
setFieldValue(name, updatedWitnesses);
},
[thisWitness, witnesses, index, name, setFieldValue]
);
return (
<View key={index} style={styles.itemContainer}>
<View style={styles.headerRow}>
<Text style={styles.headerText}>Witness #{index + 1}</Text>
{index > 0 && (
<Text style={styles.removeText} onPress={() => arrayHelpers.remove(index)}>
Remove
</Text>
)}
</View>
<Select2
isSelectSingle
style={CommonStyles.incidentField as ViewStyle}
title="Select Person Role Type"
colorTheme="#b69b30"
popupTitle="Select Person Role Type"
cancelButtonText="Cancel"
selectButtonText="OK"
selectedTitleStyle={{ color: Colors.selectedColorSelect2 }}
selectedPlaceholderTextColor={{ color: Colors.colorPlaceholder }}
searchPlaceHolderText="Search..."
listEmptyTitle="Options Empty"
data={involvedUserRolesOptionsCopy}
onSelect={handleRoleSelect}
onRemoveItem={() => {}}
/>
{personRoleType !== '' && (
<>
{personRoleType === '' || personRoleType === 'Staff' || personRoleType === 'Contractor' ? (
<Select2
isSelectSingle
style={CommonStyles.incidentField as ViewStyle}
title="User Email"
colorTheme="#b69b30"
popupTitle="Select User"
cancelButtonText="Cancel"
selectButtonText="OK"
listEmptyTitle="Options Empty"
searchPlaceHolderText="Search..."
selectedTitleStyle={{ color: Colors.selectedColorSelect2 }}
selectedPlaceholderTextColor={{ color: Colors.colorPlaceholder }}
data={personRoleType === 'Contractor' ? contractorEmailsCopy : usersOptionsCopy}
onSelect={(data: string[]) => handleSelect('user', data)}
onRemoveItem={() => {}}
/>
) : (
<>
<TextInput
style={CommonStyles.incidentField}
placeholder="Involved Person Name"
placeholderTextColor={Colors.colorPlaceholder}
onChangeText={(text: string) => handleTextChange('involvedPersonName', text)}
value={thisWitness.involvedPersonName}
/>
<TextInput
keyboardType="numeric"
style={CommonStyles.incidentField}
placeholder="Phone Number"
placeholderTextColor={Colors.colorPlaceholder}
onChangeText={(text: string) => handleTextChange('phoneNumber', text)}
value={thisWitness.phoneNumber}
/>
<Select2
isSelectSingle
style={CommonStyles.incidentField as ViewStyle}
title="Nationality"
colorTheme="#b69b30"
popupTitle="Nationality"
cancelButtonText="Cancel"
selectButtonText="OK"
listEmptyTitle="Options Empty"
searchPlaceHolderText="Search..."
selectedTitleStyle={{ color: Colors.selectedColorSelect2 }}
selectedPlaceholderTextColor={{ color: Colors.colorPlaceholder }}
data={nationalityOptionsCopy}
onSelect={(data: string[]) => handleSelect('Nationality', data)}
onRemoveItem={() => {}}
/>
<TextInput
style={[CommonStyles.incidentField, styles.textArea]}
placeholder="Address"
placeholderTextColor={Colors.colorPlaceholder}
onChangeText={(text: string) => handleTextChange('address', text)}
value={thisWitness.address}
multiline
numberOfLines={5}
textAlignVertical="top"
/>
</>
)}
</>
)}
</View>
);
};
const styles = StyleSheet.create({
itemContainer: {
borderColor: '#ddd',
borderRadius: 5,
padding: 8,
paddingVertical: 12,
marginVertical: 5,
backgroundColor: Colors.appBackgroundColor,
},
headerRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 5,
},
headerText: {
color: '#606060',
fontSize: 14,
margin: 4,
flex: 2,
},
removeText: {
marginLeft: 5,
color: '#C1AD84',
},
textArea: {
minHeight: 100,
paddingTop: 10,
},
});
export { Witnesses };
export * from './FormikActions'
export * from './FormikDatePicker'
export * from './FormikDocumentPicker'
export * from './FormikImagePicker'
export * from './FormikQuestionAnswers'
export * from './FormikRadioButtons'
export * from './FormikSelect'
export * from './FormikTextInput'
export * from './InvolvedPeople'
export * from './Witnesses'
import { Formik } from "formik";
import React from "react";
import {
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
import * as yup from "yup";
import { Banner, BottomButton } from "../../components/common";
import CommonStyles from "../../constants/CommonStyles";
import Colors from "../../constants/colors";
interface IncidentReportFormProps {
incidentReportPagedata: any;
submitIncident: (values: any) => void;
navigation: any;
}
const radioButtonOptions = [
{ label: "Yes", value: "yes" },
{ label: "No", value: "no" },
];
export const IncidentReportForm: React.FC<IncidentReportFormProps> = ({
incidentReportPagedata,
submitIncident,
navigation,
}) => {
if (!incidentReportPagedata?.incidentReportDashboardForm) {
alert("Network Error");
return null;
}
const {
incidentReportDashboardForm: {
entities,
incident_types,
incident_sub_types,
incident_groups,
areas,
people_role_types,
user_emails,
contractor_emails,
nationalities,
},
} = incidentReportPagedata;
const contractorRoleTypeId =
people_role_types.find((type: any) => type.name === "Contractor")?.id ?? "";
const staffRoleTypeId =
people_role_types.find((type: any) => type.name === "Staff")?.id ?? "";
const incidentReportFormSchema = yup.object().shape({
title: yup.string().required("Title is required"),
incidentGroup: yup.array().min(1, "Incident Group is required"),
incidentType: yup.array().min(1, "Incident Type is required"),
incidentSubType: yup.array(),
group: yup.array().min(1, "Location is required"),
date: yup.date().required("Date is required"),
time: yup.date().required("Time is required"),
area: yup.array().min(1, "Area is required"),
involvedPeople: yup.array().of(
yup.object().shape({
involvedUserRole: yup.array(),
user: yup.array(),
involvedPersonName: yup.string().when("involvedUserRole", ([involvedUserRole], schema) => {
if (!involvedUserRole?.length) return schema.nullable();
if (involvedUserRole[0] === contractorRoleTypeId || involvedUserRole[0] === staffRoleTypeId) {
return schema.nullable();
}
return schema.required("Involved Person Name is required");
}),
phoneNumber: yup.string(),
Nationality: yup.array(),
address: yup.string(),
})
),
witnesses: yup.array().of(
yup.object().shape({
involvedUserRole: yup.array(),
user: yup.array(),
involvedPersonName: yup.string().when("involvedUserRole", ([involvedUserRole], schema) => {
if (!involvedUserRole?.length) return schema.nullable();
if (involvedUserRole[0] === contractorRoleTypeId || involvedUserRole[0] === staffRoleTypeId) {
return schema.nullable();
}
return schema.required("Witness name is required");
}),
phoneNumber: yup.string(),
Nationality: yup.array(),
address: yup.string(),
})
),
body: yup.string().required("Description is required"),
field_firstaid: yup.string().nullable(),
field_staff_off_from_work: yup.string().nullable(),
field_staff_off_days: yup
.string()
.when("field_staff_off_from_work", ([field_staff_off_from_work], schema) => {
if (field_staff_off_from_work === "yes") {
return schema
.required("Staff off days required")
.test("isNumber", "Please provide valid number", (value) =>
!isNaN(Number(value))
);
}
return schema.nullable();
}),
field_taken_home: yup.string().nullable(),
field_hospitalized: yup.string().nullable(),
field_no_of_days_hospitalized: yup
.string()
.when("field_hospitalized", ([field_hospitalized], schema) => {
if (field_hospitalized === "yes") {
return schema
.required("Hospitalization days required")
.test("isNumber", "Please provide valid number", (value) =>
!isNaN(Number(value))
);
}
return schema.nullable();
}),
field_sent_or_taken_to_hospital: yup.string().nullable(),
field_hospital_name: yup.string().when("field_sent_or_taken_to_hospital", ([field_sent_or_taken_to_hospital], schema) => {
if (field_sent_or_taken_to_hospital === "yes") {
return schema.required("Hospital name is required");
}
return schema.nullable();
}),
});
return (
<Formik
key="incidentReportForm"
validateOnChange={false}
validateOnBlur={true}
initialValues={{
title: "",
incidentType: [],
incidentSubType: [],
group: [],
incidentGroup: [],
date: "",
time: "",
area: [],
involvedPeople: [
{
involvedUserRole: [],
user: [],
involvedPersonName: "",
phoneNumber: "",
Nationality: [],
address: "",
},
],
witnesses: [
{
involvedPersonName: "",
phoneNumber: "",
involvedUserRole: [],
user: [],
address: "",
Nationality: [],
},
],
body: "",
field_firstaid: "",
field_staff_off_from_work: "",
field_staff_off_days: "",
field_taken_home: "",
field_hospitalized: "",
field_no_of_days_hospitalized: "",
field_sent_or_taken_to_hospital: "",
field_hospital_name: "",
images: [],
videos: [],
docs: [],
}}
validationSchema={incidentReportFormSchema}
onSubmit={(values) => {
submitIncident(values);
navigation.navigate("Home");
}}
>
{({ handleChange, handleBlur, handleSubmit, values, setFieldValue, errors, touched, isSubmitting }) => (
<View style={CommonStyles.screen}>
<ScrollView>
<View style={{ padding: 15 }}>
<Banner title="Incident Reporting" image={require("../../assets/images/incidents.png")} />
{/* Example field */}
<Text style={[CommonStyles.subTitle, { marginTop: 30 }]}>Incident Title</Text>
<TextInput
placeholder="Incident Title"
placeholderTextColor={Colors.colorPlaceholder}
onChangeText={handleChange("title")}
onBlur={handleBlur("title")}
value={values.title}
style={[CommonStyles.textInput, { marginTop: 2 }]}
/>
{errors.title && <Text style={CommonStyles.errorTextStyle}>{errors.title}</Text>}
{/* Add all other Field components like FormikSelect, FormikDatePicker, etc. */}
{/* ... */}
</View>
</ScrollView>
<BottomButton
isSubmitting={isSubmitting}
buttonText="Submit"
pressCallBack={handleSubmit}
/>
</View>
)}
</Formik>
);
};
const styles = StyleSheet.create({
fieldArrayContainer: {
borderColor: Colors.colorTextField,
borderRadius: 5,
backgroundColor: Colors.colorTextField,
borderWidth: 1,
padding: 15,
},
addButton: {
borderColor: Colors.buttonColor,
alignSelf: "flex-end",
width: "25%",
justifyContent: "center",
right: 20,
marginTop: 10,
borderRadius: 5,
backgroundColor: Colors.buttonColor,
},
});
\ No newline at end of file
import messaging from "@react-native-firebase/messaging";
import axios from "axios";
import React, { useEffect, useRef, useState } from "react";
import {
Alert,
Linking,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import DeviceInfo from "react-native-device-info";
import { connect, ConnectedProps } from "react-redux";
import config from "../../config/config";
import Colors from "../../constants/colors";
import { getAuditFormData } from "../../store/actions/auditFormScreenActions";
import { getIncidentReportFormData } from "../../store/actions/incidentReportActions";
import { getInspectionFormData } from "../../store/actions/InspectionActions";
import { authenticate } from "../../store/actions/securityActions";
import { Spinner } from "../common";
interface RootState {
userInfo: {
loading: boolean;
error: string;
};
offline: {
online: boolean;
};
}
const mapStateToProps = (state: RootState) => {
return {
userInfo: state.userInfo,
offline: state.offline,
};
};
const mapDispatchToProps = {
authenticate,
getIncidentReportFormData,
getAuditFormData,
getInspectionFormData,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
type PropsFromRedux = ConnectedProps<typeof connector>;
interface LoginFormProps extends PropsFromRedux {
navigation: any;
}
async function registerAppWithFCM() {
await messaging().registerDeviceForRemoteMessages();
}
async function requestUserPermission() {
const settings = await messaging().requestPermission();
if (settings) {
// console.log('Permission settings:', settings);
}
}
const LoginForm: React.FC<LoginFormProps> = ({
navigation,
userInfo,
offline,
authenticate,
}) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const passwordRef = useRef<TextInput>(null);
useEffect(() => {
requestUserPermission();
}, []);
const postToken = async (token: string) => {
try {
// Await the promise to get the actual device ID string
const deviceId = await DeviceInfo.getUniqueId();
const formData = new FormData();
formData.append("device", deviceId); // Now you're passing a string
formData.append("token", token);
await axios.post(
`${config.apiserver}/api/v1/notifications/register-device-token`,
formData
);
} catch (error) {
console.log("token error=>", error);
}
};
const login = async () => {
if (!offline.online) {
Alert.alert(
"Offline Alert",
"App is offline in order to login you must have internet connection",
[
{
text: "OK",
onPress: () => {
// console.log('OK Pressed')
},
},
],
{ cancelable: true }
);
return;
}
const formData = new FormData();
formData.append("client_id", config.client_id);
formData.append("client_secret", config.client_secret);
formData.append("grant_type", "password");
formData.append("username", username);
formData.append("password", password);
try {
const data = await authenticate(formData, navigation);
if (data) {
const token = await messaging().getToken();
await postToken(token);
// navigation.replace('Home')
}
} catch (error) {
console.log("something went wrong while authenticating", error);
}
};
const handleForgotPassword = () => {
if (offline.online) {
Linking.openURL(`${config.apiserver}/user/password`);
} else {
alert("App is offline, you must have internet connection to reset password");
}
};
const { loading, error } = userInfo;
return (
<View style={styles.container}>
<TextInput
contextMenuHidden={true}
style={styles.input}
placeholder="Username"
keyboardType="email-address"
placeholderTextColor="#BFBFBF"
onChangeText={setUsername}
value={username}
onSubmitEditing={() => passwordRef.current?.focus()}
returnKeyType="next"
/>
<TextInput
contextMenuHidden={true}
ref={passwordRef}
secureTextEntry={true}
style={styles.input}
placeholder="Password"
placeholderTextColor="#BFBFBF"
onChangeText={setPassword}
value={password}
onSubmitEditing={login}
returnKeyType="done"
/>
{error && error.length > 0 && (
<Text numberOfLines={1} ellipsizeMode="tail" style={styles.errorTextStyle}>
{error}
</Text>
)}
{loading ? (
<Spinner size="small" />
) : (
<TouchableOpacity style={styles.buttonContainer} onPress={login}>
<Text style={styles.buttonText}>Login</Text>
</TouchableOpacity>
)}
<Text
style={styles.forgotPasswordText}
onPress={handleForgotPassword}
>
Forgot Password?
</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "white",
justifyContent: "center",
paddingHorizontal: 30,
},
input: {
height: 40,
borderColor: Colors.accentColor,
borderBottomWidth: 0.5,
marginBottom: 15,
color: Colors.colorTextInput,
},
buttonContainer: {
backgroundColor: Colors.accentColor,
borderRadius: 5,
height: 40,
justifyContent: "center",
marginTop: 10,
},
buttonText: {
textAlign: "center",
color: "#fff",
fontWeight: "bold",
},
errorTextStyle: {
fontSize: 12,
alignSelf: "center",
color: "red",
marginBottom: 4,
},
forgotPasswordText: {
textAlign: "right",
padding: 4,
textDecorationLine: "underline",
marginVertical: 8,
marginTop: 0,
marginBottom: 10,
},
});
export default connector(LoginForm);
\ No newline at end of file
import { Field, Formik, FormikHelpers } from 'formik';
import React from 'react';
import { Platform, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import * as yup from 'yup';
// Assuming these are your custom/internal components.
// Their props are inferred from usage in the original code.
import Colors from '../../constants/colors';
import CommonStyles from '../../constants/CommonStyles';
import { Banner, BottomButton } from '../common';
import { FormikDatePicker, FormikSelect } from '../formikComponents';
// --- Type Definitions ---
// A generic type for options used in select components
interface Option {
id: string | number;
name: string;
}
// Defines the shape of the form's values
interface PermitFormValues {
title: string;
field_start_date: Date | null;
field_end_date: Date | null;
field_job_location: (string | number)[]; // Assuming the value is an array of IDs
field_invited_contractor: (string | number)[];
field_job_details: string;
webform: (string | number)[];
}
// Defines the props passed to the PermitForm component
interface PermitFormProps {
submitPermit: (values: PermitFormValues) => void;
// Assuming the data props are arrays of Option objects
field_job_location: Option[];
webform: Option[]; // This might need a more complex structure if it's location-dependent
field_invited_contractor: Option[];
navigation: {
navigate: (screen: string) => void;
};
}
// --- Validation Schema ---
const PermitFormSchema = yup.object().shape({
title: yup.string().required('Project Name is required'),
field_start_date: yup.date().required("Start Date is required").nullable(),
field_end_date: yup
.date()
.required("End Date is required")
.nullable()
.min(yup.ref('field_start_date'), "End date can't be before start date"),
field_job_location: yup.array().min(1, "Job Location is required").required("Job Location is required"),
field_invited_contractor: yup.array().min(1, "Invited Contractor is required").required("Invited Contractor is required"),
field_job_details: yup.string().required('Job Details is required'),
webform: yup.array().min(1, "Permit Form is required").required("Permit Form is required"),
});
// --- Component ---
export const PermitForm: React.FC<PermitFormProps> = ({
submitPermit,
field_job_location,
webform,
field_invited_contractor,
navigation
}) => {
const initialValues: PermitFormValues = {
title: '',
field_start_date: null,
field_end_date: null,
field_job_location: [],
field_invited_contractor: [],
field_job_details: '',
webform: [],
};
// NOTE: This assumes webform and contractor options are NOT dependent on job location.
// If they ARE dependent, the logic in the commented-out block of the original
// code should be implemented using a useEffect hook.
const handleSubmitForm = (values: PermitFormValues, { setSubmitting }: FormikHelpers<PermitFormValues>) => {
submitPermit(values);
navigation.navigate('Home');
// Consider adding a toast message here using a modern library like 'react-native-toast-message'
setSubmitting(false);
};
return (
<Formik
key="PermitForm"
validateOnChange={true}
validateOnBlur={true}
initialValues={initialValues}
onSubmit={handleSubmitForm}
validationSchema={PermitFormSchema}
>
{({ handleChange, handleBlur, handleSubmit, values, setFieldValue, errors, touched, isSubmitting }) => (
<>
<ScrollView>
<View style={styles.container}>
<Banner title={'Contractors & \nSuppliers Management'} image={require('../../assets/images/permit.png')} />
<Text style={CommonStyles.subTitle}>Enter Details</Text>
{/* Project Name */}
<TextInput
placeholder="Project Name"
onChangeText={handleChange('title')}
placeholderTextColor={Colors.colorPlaceholder}
onBlur={handleBlur('title')}
value={values.title}
style={CommonStyles.textInput}
/>
{touched.title && errors.title && <Text style={CommonStyles.errorTextStyle}>{errors.title}</Text>}
{/* Job Location Select */}
<Field
name="field_job_location"
component={FormikSelect}
setFieldValue={setFieldValue}
isSelectSingle={true}
title='Job Location'
popupTitle="Select Job Location"
data={field_job_location || []}
/>
{touched.field_job_location && errors.field_job_location && <Text style={CommonStyles.errorTextStyle}>{errors.field_job_location}</Text>}
{/* Permit Form Select */}
<Field
name="webform"
component={FormikSelect}
setFieldValue={setFieldValue}
isSelectSingle={true}
title='Permit Form'
popupTitle="Select Permit WebForm"
data={webform || []}
/>
{touched.webform && errors.webform && <Text style={CommonStyles.errorTextStyle}>{errors.webform}</Text>}
{/* Invited Contractor Select */}
<Field
name="field_invited_contractor"
component={FormikSelect}
setFieldValue={setFieldValue}
isSelectSingle={false} // Allows multiple selections
title='Invited Contractor'
popupTitle="Select Invited Contractor"
data={field_invited_contractor || []}
/>
{touched.field_invited_contractor && errors.field_invited_contractor && <Text style={CommonStyles.errorTextStyle}>{errors.field_invited_contractor}</Text>}
{/* Start Date */}
<Field
name="field_start_date"
component={FormikDatePicker}
setFieldValue={setFieldValue}
placeholder="Select Start Date.."
minDate={new Date()}
/>
{touched.field_start_date && errors.field_start_date && <Text style={CommonStyles.errorTextStyle}>{errors.field_start_date}</Text>}
{/* End Date */}
<Field
name="field_end_date"
component={FormikDatePicker}
setFieldValue={setFieldValue}
placeholder="Select End Date.."
minDate={values.field_start_date || new Date()} // End date cannot be before start date
/>
{touched.field_end_date && errors.field_end_date && <Text style={CommonStyles.errorTextStyle}>{errors.field_end_date}</Text>}
{/* Job Details Textarea */}
<TextInput
multiline
numberOfLines={5}
placeholderTextColor={Colors.colorPlaceholder}
placeholder="Job Details"
onChangeText={handleChange('field_job_details')}
onBlur={handleBlur('field_job_details')}
value={values.field_job_details}
style={[CommonStyles.item, styles.textArea]}
/>
{touched.field_job_details && errors.field_job_details && <Text style={CommonStyles.errorTextStyle}>{errors.field_job_details}</Text>}
</View>
</ScrollView>
<BottomButton
isSubmitting={isSubmitting}
buttonText={'Submit'}
pressCallBack={handleSubmit}
/>
</>
)}
</Formik>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 15
},
textArea: {
height: 100, // Give a fixed height to the multiline input
textAlignVertical: 'top', // Crucial for Android
marginTop: 8,
borderWidth: Platform.OS === 'ios' ? 1 : 0.5, // Native-base 'bordered' style
borderColor: '#ddd',
padding: 10,
}
});
\ No newline at end of file
import React, { useState } from 'react'
import {
Dimensions,
Image,
ImageBackground,
ScrollView,
StyleSheet,
Text,
View
} from 'react-native'
import { ListComponent } from '../../components/common'
import Colors from '../../constants/colors'
import CommonStyles from '../../constants/CommonStyles'
import Select2 from '../common/Select2'
const { height } = Dimensions.get('window')
export interface SelectionFormProps {
auditForms: any[]
entities: any[]
selectedEntity: any
setLocation: (data: any) => void
setForm: (data: any, item: any) => void
draftsList: any[]
onDelete: (id: any) => void
navigation: any
listComponentNavigation: 'AuditFormScreen' | 'CheckListFormScreen' | 'InspectionFormScreen'
title: string
image: any
isPermission: boolean
setType: (data: any) => void
checkListType: any[]
buMangerList: any[]
setBUManager: (data: any) => void
}
export const SelectionForm: React.FC<SelectionFormProps> = ({
auditForms,
entities,
selectedEntity,
setLocation,
setForm,
draftsList,
onDelete,
navigation,
listComponentNavigation,
title,
image,
isPermission,
setType,
checkListType,
buMangerList,
setBUManager
}) => {
const [draft, setDraft] = useState<boolean>(false)
return (
<View style={{ flex: 1 }}>
{draft ? (
<View style={styles.listComponent}>
{draftsList.length > 0 ? (
<Text style={{ color: Colors.colorTextInput, margin: 10, fontSize: 14 }}>Drafts</Text>
) : null}
<ScrollView>
<ListComponent
data={draftsList || []}
onDelete={onDelete}
listClick={(
form_id,
id,
technical_form_id,
operational_form_id,
selectedEntity,
selectedGroup,
type,
checklistSubmissionId,
selectedBUManager
) => {
switch (listComponentNavigation) {
case 'AuditFormScreen':
navigation.navigate(listComponentNavigation, {
auditFormId: form_id,
selectedEntity,
auditTypeId: id,
selectedGroup,
type,
selectedBUManager
})
break
case 'CheckListFormScreen':
navigation.navigate(listComponentNavigation, {
operational_form_id: form_id,
technical_form_id: form_id,
selectedEntity,
checkListTypeId: id,
selectedGroup,
checkListType: type,
checklistSubmissionId,
selectedBUManager
})
break
case 'InspectionFormScreen':
navigation.navigate(listComponentNavigation, {
inspectionFormId: form_id,
selectedEntity,
inspectionTypeId: id,
selectedGroup,
selectedBUManager
})
break
}
}}
/>
</ScrollView>
</View>
) : (
<ImageBackground source={require('../../assets/images/cover.png')} style={styles.image}>
<View style={styles.container}>
<Image source={image} style={styles.logo} />
<Text style={styles.title}>{title}</Text>
{draftsList.length > 0 && (
<Text style={styles.draft} onPress={() => setDraft(true)}>
YOUR DRAFTS
</Text>
)}
</View>
</ImageBackground>
)}
<ScrollView>
<View style={{ backgroundColor: Colors.appBackgroundColor, flex: 1, paddingTop: 20, paddingHorizontal: 20 }}>
{isPermission && (
<>
<Text style={styles.subTitle}> Checklist Type </Text>
<Select2
isSelectSingle
title="Select Checklist"
colorTheme="#b69b30"
popupTitle="Select Checklist"
showSearchBox={false}
cancelButtonText="Cancel"
selectButtonText="OK"
buttonTextStyle={{ fontSize: 11 }}
listEmptyTitle="Options Empty"
selectedTitleStyle={{ color: Colors.selectedColorSelect2 }}
selectedPlaceholderTextColor={{ color: Colors.colorPlaceholder }} // fixed type
style={CommonStyles.textInput} // remove array brackets
isEnable={checkListType.length > 1}
data={checkListType || []}
onSelect={(data: any) => data.length > 0 && setType(data)}
onRemoveItem={() => {}}
/>
</>
)}
<Text style={styles.subTitle}> Location </Text>
<Select2
isSelectSingle
title="Select Location"
colorTheme="#b69b30"
popupTitle="Select Location"
cancelButtonText="Cancel"
selectButtonText="OK"
buttonTextStyle={{ fontSize: 11 }}
listEmptyTitle="Options Empty"
searchPlaceHolderText="Search.."
selectedTitleStyle={{ color: Colors.selectedColorSelect2 }}
selectedPlaceholderTextColor={{ color: Colors.colorPlaceholder }} // fixed type
style={CommonStyles.textInput} // remove array brackets
data={entities || []}
onSelect={(data: any) => setLocation(data)}
onRemoveItem={() => {}}
/>
{selectedEntity && (
<View style={{ marginTop: 5 }}>
<Text style={styles.subTitle}> BU Manager </Text>
<Select2
isSelectSingle
title="Select BU Manager"
colorTheme="#b69b30"
popupTitle="Select BU Manager"
cancelButtonText="Cancel"
selectButtonText="OK"
buttonTextStyle={{ fontSize: 11 }}
listEmptyTitle="Options Empty"
searchPlaceHolderText="Search.."
selectedTitleStyle={{ color: Colors.selectedColorSelect2 }}
selectedPlaceholderTextColor={{ color: Colors.colorPlaceholder }} // fixed type
style={CommonStyles.textInput} // remove array brackets
data={buMangerList || []}
onSelect={(data: any) => setBUManager(data)}
onRemoveItem={() => {}}
/>
<Text style={styles.subTitle}> {title} Forms </Text>
<Select2
isSelectSingle
title="Select Form"
colorTheme="#b69b30"
popupTitle="Select Form"
cancelButtonText="Cancel"
selectButtonText="OK"
buttonTextStyle={{ fontSize: 11 }}
listEmptyTitle="Options Empty"
searchPlaceHolderText="Search.."
selectedTitleStyle={{ color: Colors.selectedColorSelect2 }}
selectedPlaceholderTextColor={{ color: Colors.colorPlaceholder }} // fixed type
style={CommonStyles.textInput} // remove array brackets
data={auditForms || []}
onSelect={(_: any, item: any) => item.length > 0 && setForm(_, item)}
onRemoveItem={() => {}}
/>
</View>
)}
</View>
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
image: {
height: height / 1.8,
resizeMode: 'stretch',
justifyContent: 'flex-end'
},
listComponent: {
height: height / 1.8,
backgroundColor: Colors.colorWhite
},
text: {
color: 'grey',
fontSize: 30,
fontWeight: 'bold'
},
container: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'baseline',
backgroundColor: 'rgba(100, 100, 100, 0.5)'
},
logo: {
resizeMode: 'contain',
height: 54,
width: 54,
margin: 12
},
title: {
fontSize: 24,
color: Colors.colorWhite,
fontWeight: '200',
marginRight: 10,
marginLeft: 10,
flex: 2
},
draft: {
alignSelf: 'flex-end',
color: Colors.colorWhite,
fontSize: 12,
paddingRight: 10,
paddingBottom: 10
},
subTitle: {
color: Colors.colorTextInput,
marginTop: 15,
textAlign: 'left',
fontSize: 12
}
})
const config = {
apiserver: 'https://www.mafgateway.com',
client_id: '7f9ea978-6da7-411a-b4cf-acf7edd76d1f',
client_secret: '+?qKfR8e_gL:N#q',
};
export default config;
import { StyleSheet } from 'react-native';
import Colors from './colors';
const CommonStyles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: Colors.appBackgroundColor
},
select2: {
backgroundColor: Colors.colorTextField,
marginTop: 10
},
item: {
marginVertical: 10,
backgroundColor:Colors.colorTextField,
borderRadius:5,
borderColor:Colors.colorTextField,
color: Colors.colorTextInput
},
textInput: {
borderColor: Colors.colorTextField,
borderRadius: 5,
backgroundColor: Colors.colorTextField,
paddingLeft: 15,
borderWidth: .5,
minHeight: 40,
fontSize: 14,
marginTop:12,
color: Colors.colorTextInput
},
incidentField: {
height: 40,
borderColor: Colors.colorTextField,
borderWidth: 1,
paddingLeft: 10,
marginVertical: 10,
backgroundColor:Colors.colorTextField,
color: Colors.colorTextInput
},
errorTextStyle: {
fontSize: 12,
textAlign: 'right',
color: 'red',
marginBottom: 3,
marginTop: 3
},
subTitle:{
margin:4,
marginBottom:2,
color:Colors.colorTextInput,
fontSize:12
}
});
export default CommonStyles;
\ No newline at end of file
const Colors = {
primaryColor: '#4a148c',
accentColor: '#B49659',
headerBackgroundColor:'#FFF',
headerTintColor:'#6F6F6F',
buttonColor:'#C1AD84',
buttonText:'#FFFF',
primaryDarkColor:'#333',
appBackgroundColor:'#ECECEC',
color_02:'#1BD7A4',
color_01:'#FFC24B',
color_00:'#E64671',
color_NA:'#E6E6E6',
colorWhite:'#FFFF',
colorDark:'#1A1A1A',
colorGroup:'#535353',
textColor:'#939393',
colorPlaceholder:'#C7C7CD',
colorTextInput:'#606060',
selectedColorSelect2:'#B4965A',
statusBarColor:'#27292b',
colorTextField: '#FFF',
colorText:'#827979',
radioButtonColor:'#606060',
disableColor:'#f7ebd2',
colorGray:'gray',
darkGray:'#B2B0B0'
};
export default Colors;
\ No newline at end of file
import { StyleSheet, TextStyle, ViewStyle } from 'react-native';
import Colors from './colors';
// Define a type for our styles object for better type-checking
type CommonStyleSheet = {
screen: ViewStyle;
select2: ViewStyle;
item: ViewStyle;
textInput: TextStyle;
incidentField: TextStyle;
errorTextStyle: TextStyle;
subTitle: TextStyle;
};
const CommonStyles: CommonStyleSheet = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: Colors.appBackgroundColor
},
select2: {
backgroundColor: Colors.colorTextField,
marginTop: 10
},
item: {
marginVertical: 10,
backgroundColor:Colors.colorTextField,
borderRadius:5,
borderColor:Colors.colorTextField
},
textInput: {
borderColor: Colors.colorTextField,
borderRadius: 5,
backgroundColor: Colors.colorTextField,
paddingLeft: 15,
borderWidth: .5,
minHeight: 40,
fontSize: 14,
marginTop:12,
color: Colors.colorTextInput
},
incidentField: {
height: 40,
borderColor: Colors.colorTextField,
borderWidth: 1,
paddingLeft: 10,
marginVertical: 10,
backgroundColor:Colors.colorTextField,
color: Colors.colorTextInput
},
errorTextStyle: {
fontSize: 12,
textAlign: 'right',
color: 'red',
marginBottom: 3,
marginTop: 3
},
subTitle:{
margin:4,
marginBottom:2,
color:Colors.colorTextInput,
fontSize:12
}
});
export default CommonStyles;
\ No newline at end of file
......@@ -25,6 +25,7 @@ export const Colors = {
tabIconDefault: '#9BA1A6',
tabIconSelected: tintColorDark,
},
};
export const Fonts = Platform.select({
......
class Tile {
id: string;
title: string;
color: string;
constructor(id: string, title: string, color: string) {
this.id = id;
this.title = title;
this.color = color;
}
}
export default Tile;
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -11,16 +11,24 @@
"lint": "expo lint"
},
"dependencies": {
"@expo/vector-icons": "^15.0.2",
"@expo/metro-runtime": "~6.1.2",
"@expo/vector-icons": "^15.0.2",
"@react-native-firebase/app": "^23.4.0",
"@react-native-firebase/messaging": "^23.4.0",
"@react-native-picker/picker": "^2.11.2",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"expo": "~54.0.10",
"@redux-offline/redux-offline": "^2.6.0",
"@reduxjs/toolkit": "^2.9.0",
"axios": "^1.12.2",
"expo": "^54.0.10",
"expo-constants": "~18.0.9",
"expo-document-picker": "^14.0.7",
"expo-font": "~14.0.8",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.8",
"expo-image-picker": "^17.0.8",
"expo-linking": "~8.0.8",
"expo-router": "~6.0.8",
"expo-splash-screen": "~31.0.10",
......@@ -28,21 +36,41 @@
"expo-symbols": "~1.0.7",
"expo-system-ui": "~6.0.7",
"expo-web-browser": "~15.0.7",
"formik": "^2.4.6",
"jwt-decode": "^4.0.0",
"moment": "^2.30.1",
"native-base": "^3.4.28",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native": "^0.81.4",
"react-native-device-info": "^14.1.1",
"react-native-gesture-handler": "~2.28.0",
"react-native-worklets": "0.5.1",
"react-native-loading-spinner-overlay": "^3.0.1",
"react-native-modal": "^14.0.0-rc.1",
"react-native-modal-datetime-picker": "^18.0.0",
"react-native-progress": "^5.0.1",
"react-native-reanimated": "~4.1.0",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-web": "~0.21.0"
"react-native-simple-radio-button": "^2.7.4",
"react-native-svg": "^15.13.0",
"react-native-toast-message": "^2.3.3",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1",
"react-redux": "^9.2.0",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"redux-persist-filesystem-storage": "^4.2.0",
"redux-thunk": "^3.1.0",
"yup": "^1.7.1"
},
"devDependencies": {
"@types/lodash": "^4.17.20",
"@types/react": "~19.1.0",
"typescript": "~5.9.2",
"@types/redux-devtools-extension": "^2.13.0",
"@types/yup": "^0.29.14",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0"
"eslint-config-expo": "~10.0.0",
"typescript": "~5.9.2"
},
"private": true
}
import axios, { AxiosError, AxiosResponse } from "axios";
import moment from "moment";
import { AnyAction } from "redux";
import { ThunkAction, ThunkDispatch } from "redux-thunk";
import {
GET_INSPECTION_FORM_DATA_FAILED,
GET_INSPECTION_FORM_DATA_REQUEST,
GET_INSPECTION_FORM_DATA_SUCCESS,
SUBMIT_INSPECTION_FAILED,
SUBMIT_INSPECTION_REQUEST,
SUBMIT_INSPECTION_SUCCESS,
} from "./types";
import config from '../../config/config';
import { RootState } from "../store";
import { axiosErrorMessage } from "../../utils/helper";
import { addHistory } from "./historyScreenActions";
// Types
interface FormFile {
uri: string;
type: string;
name?: string;
key: string;
}
interface InspectionFormData {
[key: string]: any;
images?: FormFile[];
videos?: FormFile[];
docs?: FormFile[];
}
// Thunk Type
type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
AnyAction
>;
/**
* ✅ Fetch Inspection Form Data
*/
export const getInspectionFormData = (): AppThunk<Promise<void>> => {
return async (dispatch: ThunkDispatch<RootState, unknown, AnyAction>) => {
dispatch({ type: GET_INSPECTION_FORM_DATA_REQUEST });
try {
const res: AxiosResponse<any> = await axios.get(
`${config.apiserver}/api/v1/inspection/get-inspection-types-and-locations`
);
dispatch({
type: GET_INSPECTION_FORM_DATA_SUCCESS,
payload: res.data,
});
} catch (error) {
axiosErrorMessage(
"Something went wrong while fetching inspection form data. Try to sync again",
error as AxiosError
);
dispatch({
type: GET_INSPECTION_FORM_DATA_FAILED,
payload: error,
});
}
};
};
/**
* ✅ Submit Inspection Form
*/
export const submitInspection = (
data: InspectionFormData,
inspectionFormId: string,
selectedEntity: string,
inspectionTypeId: string,
selectedBUManager?: string
): AnyAction => {
const formData = new FormData();
const keys: string[] = [];
const allData: Record<string, any> = {};
allData[inspectionFormId] = {
id: inspectionTypeId,
entity_id: selectedEntity,
selectedBUManager,
actions: [],
};
const actions: any[] = [];
// Parse form data
Object.entries(data).forEach(([key, value]) => {
let questionId = "";
if (key.includes("_actions") && value?.body?.length > 0) {
actions.push(value);
}
if (key.includes("_answer")) {
questionId = key.replace("_answer", "");
allData[inspectionFormId][questionId] = {
...allData[inspectionFormId][questionId],
composite_radio_webform_custom_composite: value,
};
}
if (key.includes("_observation")) {
questionId = key.replace("_observation", "");
allData[inspectionFormId][questionId] = {
...allData[inspectionFormId][questionId],
composite_textarea_webform_custom_composite: value,
};
}
if (key.includes("_uri") && value?.length > 0) {
questionId = key.replace("_uri", "");
const filenames: string[] = [];
value.forEach((file: FormFile) => {
const extension = file.name
? "." + file.name.split(".").pop()
: file.type === "video/mp4"
? ".mp4"
: ".jpg";
const fileKey = `${questionId}image${file.key}${extension}`;
filenames.push(fileKey);
keys.push(`imageKey_${questionId}image${file.key}`);
formData.append(`imageKey_${questionId}image${file.key}`, {
uri: file.uri,
type: file.type,
name: file.name || fileKey,
} as any);
});
allData[inspectionFormId][questionId] = {
...allData[inspectionFormId][questionId],
composite_file_webform_custom_composite: filenames.join(),
};
}
});
allData[inspectionFormId]["actions"] = actions;
formData.append("submission_data", JSON.stringify(allData));
if (keys.length > 0) {
formData.append("image_keys", keys.join());
}
const formattedDate = moment().format("DD-MM-YYYY");
const formattedTime = moment().format("h:mm:ss a");
addHistory({
title: `${inspectionFormId} has submitted on ${formattedDate} at ${formattedTime}`,
data: formData,
url: `${config.apiserver}/api/v1/inspection/webform-submission`,
});
return {
type: SUBMIT_INSPECTION_REQUEST,
payload: formData,
meta: {
offline: {
effect: {
url: `${config.apiserver}/api/v1/inspection/webform-submission`,
method: "post",
data: formData,
headers: { Accept: "*/*" },
},
commit: { type: SUBMIT_INSPECTION_SUCCESS, meta: {} },
rollback: {
type: SUBMIT_INSPECTION_FAILED,
meta: {
title: inspectionFormId,
url: `${config.apiserver}/api/v1/inspection/webform-submission`,
data: formData,
},
},
},
},
};
};
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment