718 lines
21 KiB
TypeScript
718 lines
21 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: {
|
|
en: String;
|
|
hi: String;
|
|
};
|
|
}
|
|
|
|
export default function ServiceFormScreen(): JSX.Element {
|
|
const { t } = useTranslation();
|
|
|
|
const validationSchema = Yup.object().shape({
|
|
serviceType: Yup.string().required(t("service.service-type-is-required")),
|
|
issues: Yup.array().when("serviceType", {
|
|
is: (val: string) => val !== "Regular",
|
|
then: (schema) =>
|
|
schema.min(1, t("service.atleast-one-issue-is-required")),
|
|
otherwise: (schema) => schema.notRequired(),
|
|
}),
|
|
date: Yup.date().required(t("service.date-and-time-is-required")),
|
|
photos: Yup.array().min(1, t("service.atleast-one-photo-is-required")),
|
|
comments: Yup.string(),
|
|
});
|
|
|
|
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 = [
|
|
{ label: "Regular", value: "Regular", disabled: !isAfterSixMonths },
|
|
{ label: "On-demand", value: "On-demand", disabled: false },
|
|
];
|
|
|
|
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("common.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 handleServiceTypeChange = async (
|
|
item: { label: string; value: string },
|
|
setFieldValue: (field: string, value: any) => void,
|
|
setFieldTouched: (field: string, touched: boolean) => void
|
|
) => {
|
|
if (item.value === "Regular") {
|
|
// Regular service can be selected immediately
|
|
setFieldValue("serviceType", item.value);
|
|
setFieldValue("issues", []);
|
|
setFieldTouched("issues", false);
|
|
setIssues([]);
|
|
setIsFocus(false);
|
|
} else if (item.value === "On-demand") {
|
|
try {
|
|
setFieldValue("serviceType", item.value);
|
|
setIsLoadingIssues(true);
|
|
const response = await api.get(`${BASE_URL}/api/v1/service-issue-list`);
|
|
|
|
if (response.data.success) {
|
|
setIssues(response.data.data);
|
|
setFieldValue("serviceType", item.value);
|
|
setFieldValue("issues", []);
|
|
setFieldTouched("issues", false);
|
|
setIsFocus(false);
|
|
} else {
|
|
setFieldValue("serviceType", null);
|
|
throw new Error(response.data?.message || "Failed to fetch issues");
|
|
}
|
|
} catch (error: any) {
|
|
console.error("Error fetching issues:", error);
|
|
showSnackbar(`${t("common.something-went-wrong")}`, "error");
|
|
setIssues([]);
|
|
setFieldValue("serviceType", null);
|
|
} finally {
|
|
setIsLoadingIssues(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("common.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) => {
|
|
if (item.disabled) {
|
|
showSnackbar(
|
|
t("service.regular-available-after-6-months"),
|
|
"error"
|
|
);
|
|
return;
|
|
}
|
|
handleServiceTypeChange(
|
|
item,
|
|
setFieldValue,
|
|
setFieldTouched
|
|
);
|
|
}}
|
|
renderItem={(item) => (
|
|
<View
|
|
style={{
|
|
padding: 12,
|
|
opacity: item.disabled ? 0.5 : 1,
|
|
}}
|
|
>
|
|
<Text
|
|
style={{ color: item.disabled ? "#949CAC" : "#252A34" }}
|
|
>
|
|
{item.label}
|
|
</Text>
|
|
</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",
|
|
},
|
|
});
|