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",