560 lines
16 KiB
TypeScript
560 lines
16 KiB
TypeScript
import React, { JSX, useState } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
Image,
|
|
StyleSheet,
|
|
GestureResponderEvent,
|
|
ScrollView,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
} 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";
|
|
import { uploadImage } from "@/utils/User";
|
|
import api from "@/services/axiosClient";
|
|
import { useSnackbar } from "@/contexts/Snackbar";
|
|
import { BASE_URL } 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";
|
|
|
|
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<boolean>(false);
|
|
const [isIssueSelectorVisible, setIssueSelectorVisible] = useState(false);
|
|
const { showSnackbar } = useSnackbar();
|
|
|
|
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: 0.5,
|
|
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: 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;
|
|
}
|
|
setFieldValue("date", combinedDate);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const { t } = useTranslation();
|
|
|
|
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>
|
|
) => {
|
|
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,
|
|
}) => (
|
|
<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={[
|
|
{ label: "Regular", value: "Regular" },
|
|
{ label: "On-demand", value: "On-demand" },
|
|
]}
|
|
maxHeight={200}
|
|
labelField="label"
|
|
valueField="value"
|
|
placeholder={`${t("service.select")}`}
|
|
value={values.serviceType}
|
|
onFocus={() => setIsFocus(true)}
|
|
onBlur={() => setIsFocus(false)}
|
|
onChange={(item) => {
|
|
setFieldValue("serviceType", item.value);
|
|
setIsFocus(false);
|
|
}}
|
|
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")} <Text style={styles.required}>*</Text>
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={styles.inputBox}
|
|
onPress={toggleIssueSelector}
|
|
>
|
|
<Text style={styles.issueText}>
|
|
{values.issues.length > 0
|
|
? values.issues.length + " issues selected"
|
|
: `${t("service.select-issue")}`}
|
|
</Text>
|
|
<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(values.date, 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}
|
|
/>
|
|
{isSubmitting && <Overlay isUploading={isSubmitting} />}
|
|
</View>
|
|
)}
|
|
</Formik>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
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",
|
|
},
|
|
});
|