BaaS_Driver_Android_App/app/(tabs)/service.tsx

680 lines
20 KiB
TypeScript

import React, { JSX, useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
Image,
StyleSheet,
GestureResponderEvent,
ScrollView,
KeyboardAvoidingView,
Platform,
ActivityIndicator,
Keyboard,
} 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 { FastField, 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";
import { uploadImage } from "@/utils/User";
import api from "@/services/axiosClient";
import { useSnackbar } from "@/contexts/Snackbar";
import { BASE_URL, SERVICE } from "@/constants/config";
import { Overlay } from "@/components/common/Overlay";
import CrossIcon from "@/assets/icons/close_white.svg";
import { useTranslation } from "react-i18next";
import CalendarIcon from "@/assets/icons/calendar.svg";
import { useSelector } from "react-redux";
import { RootState } from "@/store";
interface FormValues {
serviceType: string | null;
issues: number[];
comments: string | null;
date: Date | null;
photos: string[];
}
interface Issue {
id: number;
name: string;
}
const validationSchema = Yup.object().shape({
serviceType: Yup.string().required("Service Type is required"),
issues: Yup.array().when("serviceType", {
is: (val: string) => val !== "Regular",
then: (schema) => schema.min(1, "At least one issue is required"),
otherwise: (schema) => schema.notRequired(),
}),
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<boolean>(false);
const [isIssueSelectorVisible, setIssueSelectorVisible] = useState(false);
const { data } = useSelector((state: RootState) => state.user);
const [issues, setIssues] = useState<Issue[]>([]);
const [isLoadingIssues, setIsLoadingIssues] = useState(false);
const { showSnackbar } = useSnackbar();
function toggleIssueSelector() {
setIssueSelectorVisible(!isIssueSelectorVisible);
}
function checkSixMonthsCondition(warrantyStartDate: string | undefined) {
if (warrantyStartDate) {
const startDate = new Date(warrantyStartDate);
const monthsLater = new Date(startDate);
monthsLater.setMonth(
monthsLater.getMonth() + SERVICE.ENABLE_REGULAR_SERVICE_AFTER_IN_MONTHS
);
const today = new Date();
if (today >= monthsLater) {
return true;
} else {
return false;
}
}
return false;
}
const warrantyStartDay = data?.batteries[0]?.warranty_start_date;
const isAfterSixMonths = checkSixMonthsCondition(warrantyStartDay);
const dropdownData = isAfterSixMonths
? [
{ label: "Regular", value: "Regular" },
{ label: "On-demand", value: "On-demand" },
]
: [{ label: "On-demand", value: "On-demand" }];
const fetchIssues = async () => {
try {
setIsLoadingIssues(true);
const response = await api.get(`${BASE_URL}/api/v1/service-issue-list`);
if (response.data.success) {
setIssues(response.data.data);
} else {
throw new Error(response.data?.message || "Failed to fetch issues");
}
} catch (error: any) {
console.error("Error fetching issues:", error);
showSnackbar(`${t("service.something-went-wrong")}`, "error");
setIssues([]);
} finally {
setIsLoadingIssues(false);
}
};
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: 0.5,
allowsMultipleSelection: true,
});
if (!result.canceled) {
const newPhotos = result.assets.map((asset) => asset.uri);
const allPhotos = [...currentPhotos, ...newPhotos];
setFieldValue("photos", allPhotos);
}
};
const showPicker = (setFieldValue: (field: string, value: any) => void) => {
const now = new Date();
DateTimePickerAndroid.open({
value: now,
mode: "date",
is24Hour: false,
display: "default",
minimumDate: now,
onChange: (event, selectedDate) => {
if (event.type === "set" && selectedDate) {
// When date is selected, show time picker next
DateTimePickerAndroid.open({
value: selectedDate,
mode: "time",
is24Hour: false,
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()
);
if (combinedDate < now) {
showSnackbar(`${t("service.select-valid-time")}`, "error");
return;
}
//(10AM - 5PM)
const hours = combinedDate.getHours();
if (hours < 10 || hours >= 17) {
showSnackbar(
t("service.time-must-be-between-10-and-5"),
"error"
);
return;
}
setFieldValue("date", combinedDate);
}
},
});
}
},
});
};
const { t } = useTranslation();
const handleServiceTypeChange = async (
item: { label: string; value: string },
setFieldValue: (field: string, value: any) => void,
setFieldTouched: (field: string, touched: boolean) => void
) => {
setFieldValue("serviceType", item.value);
if (item.value === "Regular") {
setFieldValue("issues", []);
setFieldTouched("issues", false);
setIssues([]);
} else if (item.value === "On-demand") {
await fetchIssues();
setFieldValue("issues", []);
setFieldTouched("issues", false);
}
setIsFocus(false);
};
const getSelectedIssuesText = (selectedIssueIds: number[]) => {
if (selectedIssueIds.length === 0) return `${t("service.select-issue")}`;
if (selectedIssueIds.length === 1) return "1 issue selected";
return `${selectedIssueIds.length} issues selected`;
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 100 : 0}
>
<ScrollView
style={styles.screen}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={async (
values: FormValues,
actions: FormikHelpers<FormValues>
) => {
Keyboard.dismiss();
try {
const uploadedPhotoUrls: string[] = [];
for (const uri of values.photos) {
const uploadedUrl = await uploadImage(uri);
uploadedPhotoUrls.push(uploadedUrl);
}
console.log("IMAGES UPLOADED");
const payload = {
service_type: values.serviceType,
issue_types: values.issues,
scheduled_time: values.date?.toISOString(),
photos: uploadedPhotoUrls,
comments: values.comments,
};
const response = await api.post(
`${BASE_URL}/api/v1/schedule-maintenance`,
payload
);
if (!response.data.success) {
console.log(response.data?.message || "Submission failed");
throw new Error(response.data?.message || "Submission failed");
}
console.log("Submission successful:", response.data);
showSnackbar(
`${t("service.service-request-success")}`,
"success"
);
actions.resetForm();
} catch (error: any) {
console.error("Error during submission:", error);
showSnackbar(`${t("service.something-went-wrong")}`, "error");
} finally {
actions.setSubmitting(false);
}
}}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
setFieldValue,
errors,
touched,
isSubmitting,
setFieldTouched,
}) => (
<View style={styles.formContainer}>
<View style={styles.inputContainer}>
<Text style={styles.label}>
{t("service.service-type")}{" "}
<Text style={styles.required}>*</Text>
</Text>
<Dropdown
style={[
styles.dropdown,
isFocus && { borderColor: "#00be88" },
]}
placeholderStyle={styles.placeholderStyle}
selectedTextStyle={styles.selectedTextStyle}
inputSearchStyle={styles.inputSearchStyle}
iconStyle={styles.iconStyle}
data={dropdownData}
maxHeight={200}
labelField="label"
valueField="value"
placeholder={`${t("service.select")}`}
value={values.serviceType}
onFocus={() => setIsFocus(true)}
onBlur={() => setIsFocus(false)}
onChange={(item) =>
handleServiceTypeChange(
item,
setFieldValue,
setFieldTouched
)
}
renderLeftIcon={() => (
<View style={{ marginRight: 10 }}>
{/* Add your icon component here if needed */}
</View>
)}
/>
{touched.serviceType && errors.serviceType && (
<Text style={styles.error}>{errors.serviceType}</Text>
)}
</View>
<View style={{ marginTop: 8 }}>
<Text style={styles.label}>
{t("service.issue")}{" "}
{values.serviceType == "On-demand" && (
<Text style={styles.required}>*</Text>
)}
</Text>
<TouchableOpacity
style={[styles.inputBox]}
onPress={toggleIssueSelector}
disabled={
values.serviceType === "Regular" ||
isLoadingIssues ||
values.serviceType == null
}
>
<Text
style={[
styles.issueText,
(values.serviceType === "Regular" ||
values.serviceType == null ||
isLoadingIssues) &&
styles.issueTextDisabled,
]}
>
{getSelectedIssuesText(values.issues)}
</Text>
{isLoadingIssues ? <ActivityIndicator /> : <ChevronRight />}
</TouchableOpacity>
{touched.issues && errors.issues && (
<Text style={styles.error}>{errors.issues}</Text>
)}
</View>
<View style={{ marginTop: 8 }}>
<Text style={styles.label}>
{t("service.select-datetime")}{" "}
<Text style={styles.required}>*</Text>
</Text>
<TouchableOpacity
onPress={() => showPicker(setFieldValue)}
style={styles.inputBoxDate}
>
<Text style={styles.dateText}>
{values.date
? values.date.toLocaleString()
: `${t("service.select")}`}
</Text>
<CalendarIcon width={20} height={20} />
</TouchableOpacity>
{touched.date && errors.date && (
<Text style={styles.error}>{`${errors.date}`}</Text>
)}
</View>
<TouchableOpacity
style={styles.photoBox}
onPress={() => handlePhotoPick(setFieldValue, values.photos)}
>
<AddPhoto />
<Text style={styles.addPhotoText}>
{t("service.add-photos")}{" "}
</Text>
</TouchableOpacity>
<Text style={styles.helperText}>
{t("service.supported-formats")}
</Text>
{/* Selected Images Preview */}
<View style={styles.photosContainer}>
{values.photos.map((uri, index) => (
<View key={index} style={styles.photoPreviewContainer}>
<Image source={{ uri }} style={styles.photoPreview} />
<TouchableOpacity
style={styles.removePhotoButton}
onPress={() => {
const updatedPhotos = [...values.photos];
updatedPhotos.splice(index, 1);
setFieldValue("photos", updatedPhotos);
}}
>
<CrossIcon />
</TouchableOpacity>
</View>
))}
</View>
{touched.photos && errors.photos && (
<Text style={styles.error}>{errors.photos}</Text>
)}
<View style={{ marginTop: 16 }}>
<Text style={styles.label}>{t("service.comments")} </Text>
<TextInput
style={styles.commentInput}
multiline
maxLength={100}
onChangeText={handleChange("comments")}
onBlur={handleBlur("comments")}
value={values.comments}
/>
<Text style={styles.wordCount}>
{values.comments?.length || 0}/100 {t("service.words")}
</Text>
</View>
<TouchableOpacity
style={styles.submitButton}
onPress={handleSubmit as (e?: GestureResponderEvent) => void}
>
<Text style={styles.submitText}>{t("service.submit")} </Text>
</TouchableOpacity>
<IssueSelectorModal
visible={isIssueSelectorVisible}
onClose={toggleIssueSelector}
onSelect={(selectedIssues) => {
setFieldValue("issues", selectedIssues);
}}
initialSelectedValues={values.issues}
issues={issues}
/>
{isSubmitting && <Overlay isUploading={isSubmitting} />}
</View>
)}
</Formik>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
issueTextDisabled: {
color: "#949CAC",
},
container: {
flex: 1,
},
inputContainer: {},
screen: {
flex: 1,
backgroundColor: "#F3F5F8",
},
scrollContent: {
paddingBottom: 116,
},
topBar: {
height: 56,
backgroundColor: "#F3F5F8",
justifyContent: "center",
paddingHorizontal: 16,
},
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",
marginTop: 8,
},
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: 4,
right: 4,
backgroundColor: "#252A345C",
borderRadius: 12,
width: 24,
height: 24,
justifyContent: "center",
alignItems: "center",
},
removePhotoText: {
color: "white",
fontSize: 18,
fontWeight: "bold",
lineHeight: 20,
},
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",
},
});