diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 8496bee..2af0294 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -8,6 +8,7 @@ import { useSnackbar } from "@/contexts/Snackbar"; import NetInfo from "@react-native-community/netinfo"; import { getUserDetails } from "@/store/userSlice"; import { useDispatch } from "react-redux"; +import { View } from "react-native"; export default function TabLayout() { const { isLoggedIn } = useSelector((state: RootState) => state.auth); @@ -70,6 +71,9 @@ export default function TabLayout() { const IconComponent = focused ? IconFilled : Icon; return ; }, + headerBackground: () => ( + + ), }} /> ))} diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 5d2f5f2..6ac1ec5 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -357,11 +357,11 @@ const styles = StyleSheet.create({ paddingBottom: 110, }, iconContainer: { - backgroundColor: "#fff", + backgroundColor: "#F3F5F8", }, headerTitleContainer: { flexDirection: "column", - backgroundColor: "#fff", + backgroundColor: "#F3F5F8", }, title: { fontSize: 14, @@ -378,7 +378,7 @@ const styles = StyleSheet.create({ alignItems: "center", paddingRight: 16, gap: 8, - backgroundColor: "#fff", + backgroundColor: "#F3F5F8", }, badge: { backgroundColor: "#FEE2E2", diff --git a/app/(tabs)/service.tsx b/app/(tabs)/service.tsx index a810b5f..c3bd639 100644 --- a/app/(tabs)/service.tsx +++ b/app/(tabs)/service.tsx @@ -1,27 +1,477 @@ -import { StyleSheet } from "react-native"; -import { Text, View } from "react-native"; +import React, { JSX, useState } from "react"; +import { + View, + Text, + TextInput, + TouchableOpacity, + Image, + StyleSheet, + GestureResponderEvent, + ScrollView, +} from "react-native"; +import { Dropdown } from "react-native-element-dropdown"; +import { DateTimePickerAndroid } from "@react-native-community/datetimepicker"; +import * as ImagePicker from "expo-image-picker"; +import { Formik, FormikHelpers } from "formik"; +import * as Yup from "yup"; +import ChevronRight from "../../assets/icons/chevron_rightside.svg"; +import AddPhoto from "../../assets/icons/add_photo_alternate.svg"; +import IssueSelectorModal from "@/components/service/IssueSelectorModal"; + +interface FormValues { + serviceType: string | null; + issues: string[]; + comments: string | null; + date: Date | null; + photos: string[]; +} + +const validationSchema = Yup.object().shape({ + serviceType: Yup.string().required("Service Type is required"), + issues: Yup.array().min(1, "At least one issue is required"), + date: Yup.date().required("Date and Time is required"), + photos: Yup.array().min(1, "At least one photo is required"), + comments: Yup.string(), +}); + +export default function ServiceFormScreen(): JSX.Element { + const [isFocus, setIsFocus] = useState(false); + const [isIssueSelectorVisible, setIssueSelectorVisible] = useState(false); + + function toggleIssueSelector() { + setIssueSelectorVisible(!isIssueSelectorVisible); + } + + const initialValues: FormValues = { + serviceType: null, + issues: [], + comments: "", + date: null, + photos: [], + }; + + const handlePhotoPick = async ( + setFieldValue: (field: string, value: any) => void, + currentPhotos: string[] + ) => { + let result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + quality: 1, + allowsMultipleSelection: true, + }); + + if (!result.canceled) { + const newPhotos = result.assets.map((asset) => asset.uri); + const allPhotos = [...currentPhotos, ...newPhotos]; + setFieldValue("photos", allPhotos); + } + }; + + const showPicker = ( + currentDate: Date | null, + setFieldValue: (field: string, value: any) => void + ) => { + const now = currentDate || new Date(); + + // First, show the date picker + DateTimePickerAndroid.open({ + value: now, + mode: "date", + is24Hour: true, + display: "default", + onChange: (event, selectedDate) => { + if (event.type === "set" && selectedDate) { + // When date is selected, show time picker next + DateTimePickerAndroid.open({ + value: selectedDate, + mode: "time", + is24Hour: true, + display: "default", + onChange: (timeEvent, selectedTime) => { + if (timeEvent.type === "set" && selectedTime) { + // Combine date and time into one Date object + const combinedDate = new Date( + selectedDate.getFullYear(), + selectedDate.getMonth(), + selectedDate.getDate(), + selectedTime.getHours(), + selectedTime.getMinutes() + ); + setFieldValue("date", combinedDate); + } + }, + }); + } + }, + }); + }; -export default function ServiceTabScreen() { return ( - - Coming Soon - + <> + + + ) => { + console.log(values); + }} + > + {({ + handleChange, + handleBlur, + handleSubmit, + values, + setFieldValue, + errors, + touched, + }) => ( + + + + Service Type * + + setIsFocus(true)} + onBlur={() => setIsFocus(false)} + onChange={(item) => { + setFieldValue("serviceType", item.value); + setIsFocus(false); + }} + renderLeftIcon={() => ( + + {/* Add your icon component here if needed */} + + )} + /> + {touched.serviceType && errors.serviceType && ( + {errors.serviceType} + )} + + + + Issues * + + + + {values.issues.length > 0 + ? values.issues.length + " issues selected" + : "Select Issue"} + + + + {touched.issues && errors.issues && ( + {errors.issues} + )} + + + + + Select Date and Time * + + showPicker(values.date, setFieldValue)} + style={styles.inputBoxDate} + > + + {values.date && values.date.toLocaleString()} + + + {touched.date && errors.date && ( + {`${errors.date}`} + )} + + handlePhotoPick(setFieldValue, values.photos)} + > + + Add photos + + + Supported formats include JPG, JPEG and PNG. + + + {/* Selected Images Preview */} + + {values.photos.map((uri, index) => ( + + + { + const updatedPhotos = [...values.photos]; + updatedPhotos.splice(index, 1); + setFieldValue("photos", updatedPhotos); + }} + > + × + + + ))} + + + {touched.photos && errors.photos && ( + {errors.photos} + )} + + Comments + + + {values.comments.length}/100 words + + + void} + > + Submit + + + { + setFieldValue("issues", selectedIssues); + }} + /> + + )} + + + ); } +// styles stay unchanged + const styles = StyleSheet.create({ - container: { + inputContainer: {}, + screen: { flex: 1, - alignItems: "center", + backgroundColor: "#F3F5F8", + marginBottom: 116, + }, + topBar: { + height: 56, + backgroundColor: "#F3F5F8", justifyContent: "center", + paddingHorizontal: 16, }, - title: { - fontSize: 20, + topBarText: { + fontSize: 16, + fontWeight: "600", + color: "#252A34", + }, + formContainer: { + paddingHorizontal: 16, + }, + label: { + fontSize: 14, + fontWeight: "500", + color: "#252A34", + marginBottom: 4, + }, + required: { + color: "#D42210", + }, + inputBox: { + backgroundColor: "#FFFFFF", + borderColor: "#D8DDE7", + borderRadius: 4, + paddingHorizontal: 8, + height: 40, + marginTop: 8, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + inputBoxDate: { + backgroundColor: "#FFFFFF", + borderColor: "#D8DDE7", + borderWidth: 1, + borderRadius: 4, + paddingHorizontal: 8, + height: 40, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginBottom: 4, + }, + picker: { + height: 40, + width: "100%", + }, + issueText: { + color: "#006C4D", + }, + dateText: { + color: "#949CAC", + }, + photoBox: { + backgroundColor: "#FFFFFF", + borderColor: "#949CAC", + borderWidth: 1, + borderRadius: 4, + height: 64, + justifyContent: "center", + alignItems: "center", + marginBottom: 8, + borderStyle: "dashed", + marginTop: 8, + }, + addPhotoText: { + fontSize: 14, + color: "#252A34", + fontWeight: "500", + }, + helperText: { + fontSize: 12, + color: "#717B8F", + marginBottom: 8, + }, + commentInput: { + backgroundColor: "#FFFFFF", + borderColor: "#D8DDE7", + borderWidth: 1, + borderRadius: 4, + padding: 10, + height: 80, + textAlignVertical: "top", + }, + wordCount: { + textAlign: "right", + color: "#717B8F", + fontSize: 14, + marginTop: 4, + }, + submitButton: { + marginTop: 24, + backgroundColor: "#00875F", + borderRadius: 4, + height: 48, + justifyContent: "center", + alignItems: "center", + }, + submitText: { + color: "#FCFCFC", + fontSize: 14, + fontWeight: "500", + }, + photo: { + width: 80, + height: 80, + resizeMode: "cover", + }, + error: { + color: "#D42210", + fontSize: 12, + marginBottom: 8, + }, + photosContainer: { + flexDirection: "row", + flexWrap: "wrap", + marginTop: 8, + gap: 8, + }, + photoPreviewContainer: { + position: "relative", + }, + photoPreview: { + width: 80, + height: 80, + borderRadius: 4, + }, + removePhotoButton: { + position: "absolute", + top: -8, + right: -8, + backgroundColor: "#D42210", + borderRadius: 12, + width: 24, + height: 24, + justifyContent: "center", + alignItems: "center", + }, + removePhotoText: { + color: "white", + fontSize: 18, fontWeight: "bold", + lineHeight: 20, }, - separator: { - marginVertical: 30, - height: 1, - width: "80%", + dropdown: { + height: 40, + borderColor: "#D8DDE7", + borderWidth: 1, + borderRadius: 4, + paddingHorizontal: 8, + backgroundColor: "#FFFFFF", + marginTop: 8, + }, + placeholderStyle: { + fontSize: 14, + color: "#252A34", + }, + selectedTextStyle: { + fontSize: 14, + color: "#252A34", + }, + iconStyle: { + width: 20, + height: 20, + }, + inputSearchStyle: { + height: 40, + fontSize: 16, + }, + dropdownMenu: { + backgroundColor: "#FCFCFC", + borderRadius: 4, + shadowColor: "#0E1118", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.1, + shadowRadius: 32, + elevation: 4, + }, + dropdownItem: { + padding: 16, + height: 36, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + dropdownItemText: { + fontSize: 14, + color: "#252A34", }, }); diff --git a/assets/icons/add_photo_alternate.svg b/assets/icons/add_photo_alternate.svg new file mode 100644 index 0000000..51bac8e --- /dev/null +++ b/assets/icons/add_photo_alternate.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/components/service/IssueSelectorModal.tsx b/components/service/IssueSelectorModal.tsx new file mode 100644 index 0000000..c9c2542 --- /dev/null +++ b/components/service/IssueSelectorModal.tsx @@ -0,0 +1,170 @@ +import React, { useState } from "react"; +import { + View, + Text, + TextInput, + ScrollView, + TouchableOpacity, + Modal, + StyleSheet, +} from "react-native"; +import Checkbox from "expo-checkbox"; +import { issueConfig } from "@/constants/config"; + +interface IssueSelectorModalProps { + visible: boolean; + onClose: () => void; + onSelect: (selectedValues: string[]) => void; +} + +export default function IssueSelectorModal({ + visible, + onClose, + onSelect, +}: IssueSelectorModalProps) { + const [selectedValues, setSelectedValues] = useState([]); + const [search, setSearch] = useState(""); + + const toggleValue = (value: string) => { + setSelectedValues((prev) => + prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value] + ); + }; + + const filteredConfig = issueConfig + .map((group) => ({ + ...group, + options: group.options.filter((option) => + option.label.toLowerCase().includes(search.toLowerCase()) + ), + })) + .filter((group) => group.options.length > 0); + + const clearSelection = () => setSelectedValues([]); + + return ( + + + {/* Header */} + + + + + {/* Selection Counter and Clear Button */} + + {`${selectedValues.length}/23 Selected`} + + Clear + + + + + {filteredConfig.map((group) => ( + + {group.category} + {group.options.map((option) => ( + toggleValue(option.value)} + > + toggleValue(option.value)} + color={ + selectedValues.includes(option.value) + ? "#252A34" + : undefined + } + /> + {option.label} + + ))} + + ))} + + + {/* Done Button */} + { + onSelect(selectedValues); + onClose(); + }} + > + Done + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: "#fff" }, + header: { + paddingHorizontal: 16, + paddingVertical: 12, + }, + searchBar: { + backgroundColor: "#fff", + borderColor: "#D8DDE6", + borderWidth: 1, + borderRadius: 4, + paddingHorizontal: 12, + height: 36, + fontSize: 14, + color: "#252A34", + }, + counterBar: { + flexDirection: "row", + justifyContent: "space-between", + paddingHorizontal: 20, + paddingVertical: 12, + }, + counterText: { + fontSize: 12, + color: "#555", + }, + clearText: { + fontSize: 14, + color: "#ADB4BD", + }, + scrollArea: { paddingHorizontal: 0 }, + category: { + fontSize: 14, + color: "#252A34", + paddingVertical: 12, + paddingHorizontal: 16, + backgroundColor: "#F9F9F9", + }, + itemRow: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 8, + paddingHorizontal: 16, + backgroundColor: "#FCFCFC", + }, + itemLabel: { + marginLeft: 12, + fontSize: 12, + color: "#252A34", + }, + doneButton: { + backgroundColor: "#00875F", + padding: 16, + alignItems: "center", + }, + doneText: { + color: "#fff", + fontSize: 14, + fontWeight: "bold", + }, +}); diff --git a/constants/config.ts b/constants/config.ts index 3e4fd03..05fa20f 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -109,3 +109,69 @@ export const AWS = { BUCKET_NAME: "battery-as-a-service", REGION: "us-east-1", }; + +export const issueConfig = [ + { + category: "Physical Damage", + options: [ + { label: "Top cover crack/damage", value: "top_cover_damage" }, + { label: "Chogori connector damage", value: "chogori_connector_damage" }, + { label: "Flip handle damage/missing", value: "flip_handle_damage" }, + { + label: "Handle gripper damage/missing", + value: "handle_gripper_damage", + }, + { label: "TCU cover damage", value: "tcu_cover_damage" }, + { label: "Gore Vent damage", value: "gore_vent_damage" }, + ], + }, + { + category: "Water Ingress Issues", + options: [ + { label: "BMS water ingress", value: "bms_water_ingress" }, + { label: "SD card water ingress", value: "sd_card_water_ingress" }, + { label: "Cell pack water ingress", value: "cell_pack_water_ingress" }, + { + label: "Battery completely submerged in water", + value: "battery_submerged", + }, + ], + }, + { + category: "Electrical & Functional Failures", + options: [ + { label: "BMS failure/card burnt", value: "bms_failure" }, + { + label: "SD card failure/data unable to copy/SD card not detected", + value: "sd_card_failure", + }, + { label: "Deep discharge (DD)", value: "deep_discharge" }, + { label: "Cell imbalance", value: "cell_imbalance" }, + { label: "CT sensing faulty", value: "ct_sensing_faulty" }, + { label: "TCU hang", value: "tcu_hang" }, + ], + }, + { + category: "Alarms & Warnings", + options: [ + { label: "UV1, UV2 alarms (Under Voltage)", value: "uv1_uv2_alarm" }, + { label: "EOD (End of Discharge)", value: "eod_alarm" }, + { + label: "Charge & discharge OT (Over Temperature) alarm", + value: "ot_alarm", + }, + ], + }, + { + category: "Other Issues", + options: [ + { label: "BMS full dust formed", value: "bms_dust" }, + { label: "NFF (No Fault Found)", value: "nff" }, + { + label: "Under warranty (not an issue, but listed in data)", + value: "under_warranty", + }, + { label: "NA (Not Applicable)", value: "not_applicable" }, + ], + }, +]; diff --git a/package-lock.json b/package-lock.json index 3792bbe..4a567b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,15 @@ "dependencies": { "@expo/vector-icons": "^14.1.0", "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-community/datetimepicker": "^8.4.2", "@react-native-community/netinfo": "^11.4.1", + "@react-native-picker/picker": "^2.11.1", "@react-navigation/native": "^7.1.6", "@react-navigation/stack": "^7.4.2", "@reduxjs/toolkit": "^2.8.2", "axios": "^1.10.0", "expo": "^53.0.13", + "expo-checkbox": "^4.1.4", "expo-constants": "~17.1.6", "expo-font": "~13.3.1", "expo-image-picker": "~16.1.4", @@ -3349,6 +3352,29 @@ "node": ">=10" } }, + "node_modules/@react-native-community/datetimepicker": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.4.2.tgz", + "integrity": "sha512-V/s+foBfjlWGV8MKdMhxugq0SPMtYqUEYlf+sMrKUUm5Gx3pA9Qoum2ZQUqBfI4A8kgaEPIGyG/YsNX7ycnNSA==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": ">=52.0.0", + "react": "*", + "react-native": "*", + "react-native-windows": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "react-native-windows": { + "optional": true + } + } + }, "node_modules/@react-native-community/netinfo": { "version": "11.4.1", "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz", @@ -3358,6 +3384,19 @@ "react-native": ">=0.59" } }, + "node_modules/@react-native-picker/picker": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.1.tgz", + "integrity": "sha512-ThklnkK4fV3yynnIIRBkxxjxR4IFbdMNJVF6tlLdOJ/zEFUEFUEdXY0KmH0iYzMwY8W4/InWsLiA7AkpAbnexA==", + "license": "MIT", + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.79.4", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.4.tgz", @@ -7003,6 +7042,22 @@ "react-native": "*" } }, + "node_modules/expo-checkbox": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/expo-checkbox/-/expo-checkbox-4.1.4.tgz", + "integrity": "sha512-sahBTVble5/6EnHgLyGvX6fAytkZ7vmllHUbX5ko1kTQ59qTdiVmCznxqaT5DNWfxRZ0gdQVlao46dGQ3hbmeQ==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native-web": { + "optional": true + } + } + }, "node_modules/expo-constants": { "version": "17.1.6", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.1.6.tgz", diff --git a/package.json b/package.json index 84838ac..f4b3719 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,15 @@ "dependencies": { "@expo/vector-icons": "^14.1.0", "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-community/datetimepicker": "^8.4.2", "@react-native-community/netinfo": "^11.4.1", + "@react-native-picker/picker": "^2.11.1", "@react-navigation/native": "^7.1.6", "@react-navigation/stack": "^7.4.2", "@reduxjs/toolkit": "^2.8.2", "axios": "^1.10.0", "expo": "^53.0.13", + "expo-checkbox": "^4.1.4", "expo-constants": "~17.1.6", "expo-font": "~13.3.1", "expo-image-picker": "~16.1.4",