Add cancel payment confirmation modal

rename-package
vinay kumar 2025-09-16 19:14:23 +05:30
parent d44677a034
commit 4eb06631b4
13 changed files with 339 additions and 202 deletions

View File

@ -38,6 +38,7 @@ import { displayValue } from "@/utils/Common";
import RefreshIcon from "@/assets/icons/refresh.svg";
import CustomerSupport from "@/components/home/CustomerSupportModal";
import { useTranslation } from "react-i18next";
import { Image } from "expo-image";
export interface MyPlan {
no_of_emi: number;
@ -231,8 +232,16 @@ export default function PaymentsTabScreen() {
},
headerTitle: () => (
<View style={styles.headerTitleContainer}>
<Text style={styles.title}>{model}</Text>
<Text style={styles.subtitle}>{chasisNumber}</Text>
<View style={styles.logoContainer}>
<Image
source={require("../../assets/images/lio_logo.png")}
style={styles.logo}
/>
</View>
<View>
<Text style={styles.title}>{model}</Text>
<Text style={styles.subtitle}>{chasisNumber}</Text>
</View>
</View>
),
headerRight: () => (
@ -270,7 +279,6 @@ export default function PaymentsTabScreen() {
// Format currency
const formatCurrency = (amount: number) => {
console.log(amount, "amount format current");
return `${amount.toLocaleString()}`;
};
@ -590,6 +598,22 @@ const StatusBadge = ({
};
const styles = StyleSheet.create({
logo: {
width: "100%",
height: "100%",
resizeMode: "contain",
},
logoContainer: {
padding: 8,
borderRadius: 44 / 2, // make it perfectly round
borderWidth: 2,
borderColor: "#E5E9F0",
width: 44,
height: 44,
alignItems: "center",
justifyContent: "center",
overflow: "hidden", // ensures image doesn't overflow
},
loadingContainer: {
padding: 16,
alignItems: "center",
@ -611,8 +635,9 @@ const styles = StyleSheet.create({
width: "80%",
},
headerTitleContainer: {
flexDirection: "column",
flexDirection: "row",
backgroundColor: "#F3F5F8",
gap: 8,
},
subtitle: {
fontSize: 18,

View File

@ -9,6 +9,8 @@ import Pending from "@/assets/icons/pending.svg";
import Failed from "@/assets/icons/cancel.svg";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useTranslation } from "react-i18next";
import { payments } from "@/constants/config";
import { formatDate } from "@/utils/Payments";
const PaymentConfirmationScreen = () => {
const router = useRouter();
@ -22,35 +24,11 @@ const PaymentConfirmationScreen = () => {
const paymentStatus = paymentOrder?.status || "confirmed";
// Format amount with currency
const formatAmount = (amount: number | null) => {
const formatAmount = (amount: number | null | undefined) => {
if (!amount) return "₹0";
return `${amount.toLocaleString("en-IN")}`;
};
// Format date
const formatDate = (dateString?: string) => {
if (!dateString)
return new Date().toLocaleString("en-IN", {
day: "numeric",
month: "long",
year: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
weekday: "long",
});
return new Date(dateString).toLocaleString("en-IN", {
day: "numeric",
month: "long",
year: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
weekday: "long",
});
};
// Get icon and text based on status
const getStatusDisplay = () => {
switch (paymentStatus) {
@ -95,15 +73,15 @@ const PaymentConfirmationScreen = () => {
<View style={styles.contentFrame}>
<View style={styles.qrFrame}>
<View style={styles.paymentStatusContainer}>
<Text style={styles.statusText}>{statusDisplay.text}</Text>
<View style={statusDisplay.iconContainerStyle}>
{statusDisplay.icon}
</View>
<Text style={styles.amountText}>
{formatAmount(paymentOrder?.amount || due_amount)}
{formatAmount(paymentOrder?.amount)}
</Text>
<Text>Payment to {payments.AMOUNT_PAID_TO}</Text>
<View style={styles.statusContainer}>
<Text style={styles.statusText}>{statusDisplay.text}</Text>
<Text style={styles.dateText}>
{formatDate(paymentOrder?.transaction_date)}
</Text>
@ -200,7 +178,6 @@ const styles = StyleSheet.create({
backgroundColor: "#fcfcfc",
borderRadius: 8,
padding: 16,
paddingVertical: 32,
},
paymentStatusContainer: {
alignItems: "center",
@ -220,6 +197,13 @@ const styles = StyleSheet.create({
successIcon: {
// Icon styling handled by Ionicons
},
paidTo: {
fontSize: 12,
fontWeight: "700",
color: "#253342",
textAlign: "center",
lineHeight: 20,
},
amountText: {
fontSize: 20,
fontWeight: "600",

View File

@ -9,16 +9,15 @@ import {
import { useNavigation, useRoute } from "@react-navigation/native";
import { useRouter } from "expo-router";
import api from "@/services/axiosClient";
import { BASE_URL } from "@/constants/config";
import { BASE_URL, payments } from "@/constants/config";
import { useSnackbar } from "@/contexts/Snackbar";
import { SafeAreaView } from "react-native-safe-area-context";
// Import your success/failure icons
import SuccessIcon from "@/assets/icons/check_circle.svg";
import FailureIcon from "@/assets/icons/cancel.svg";
import PendingIcon from "@/assets/icons/pending.svg";
import Header from "@/components/common/Header";
import { useTranslation } from "react-i18next";
import { formatDate } from "@/utils/Payments";
interface TransactionDetailData {
id: number;
@ -152,18 +151,18 @@ export default function TransactionDetailScreen() {
) : (
<View style={styles.container}>
<View style={styles.content}>
<Text style={styles.statusText}>
{getStatusText(transactionData.status)}
</Text>
<View style={styles.iconContainer}>
{getStatusIcon(transactionData.status)}
</View>
<Text style={styles.amount}>
{formatCurrency(transactionData.amount)}
</Text>
<Text style={styles.statusText}>
{getStatusText(transactionData.status)}
</Text>
<Text>Payment to {payments.AMOUNT_PAID_TO}</Text>
<Text style={styles.dateTime}>
{formatDateTime(transactionData.transaction_date)}
{formatDate(transactionData?.transaction_date)}
</Text>
<View style={styles.divider} />
@ -305,7 +304,6 @@ const styles = StyleSheet.create({
fontSize: 14,
fontWeight: "400",
color: "#252A34",
marginBottom: 4,
},
dateTime: {
fontSize: 14,

View File

@ -27,6 +27,7 @@ import { payments } from "@/constants/config";
import { useFocusEffect, useRouter } from "expo-router";
import { useSocket } from "@/contexts/SocketContext";
import { useTranslation } from "react-i18next";
import CancelPayment from "@/components/Payments/CancelPaymentModal";
const { height: screenHeight } = Dimensions.get("window");
@ -34,9 +35,6 @@ const UpiPaymentScreen = () => {
const dispatch = useDispatch();
//paymentorder.amount is undefined
console.log("inside payemi ✅✅");
useEffect(() => {
console.log("inside pay emi useeffect 🔥🔥🔥🔥🔥");
}, []);
const paymentOrder = useSelector(
(state: RootState) => state.payments.paymentOrder
);
@ -46,36 +44,36 @@ const UpiPaymentScreen = () => {
const { showSnackbar } = useSnackbar();
const insets = useSafeAreaInsets();
const [isCancelModalVisible, setIsCancelModalVisible] =
useState<boolean>(false);
function toggleCancelModal() {
setIsCancelModalVisible(!isCancelModalVisible);
}
function handleCancelPayment() {
offPaymentConfirmation();
disconnect();
router.back();
}
useFocusEffect(
React.useCallback(() => {
let backPressCount = 0;
let backPressTimer: NodeJS.Timeout | null = null;
const backAction = () => {
if (backPressCount === 0) {
backPressCount++;
showSnackbar("Press back again to cancel payment", "info");
backPressTimer = setTimeout(() => {
backPressCount = 0;
}, 2000);
return true;
} else {
if (backPressTimer) clearTimeout(backPressTimer);
offPaymentConfirmation();
disconnect();
router.back();
return true;
}
setIsCancelModalVisible(true);
return true;
};
const backHandler = BackHandler.addEventListener(
"hardwareBackPress",
backAction
);
// ✅ Cleanup when screen loses focus
return () => {
if (backPressTimer) clearTimeout(backPressTimer);
backHandler.remove();
};
}, [offPaymentConfirmation, disconnect, router])
}, [])
);
useFocusEffect(
@ -99,7 +97,7 @@ const UpiPaymentScreen = () => {
])
);
const formatAmount = (amount: number): string => {
const formatAmount = (amount: number | null | undefined): string => {
if (amount == null || amount == undefined) return `${0}`;
return `${amount.toLocaleString("en-IN")}`;
};
@ -143,18 +141,28 @@ const UpiPaymentScreen = () => {
const shareQR = async (): Promise<void> => {
try {
const fileUri =
FileSystem.documentDirectory + `qr-${paymentOrder?.order_id}.png`;
// Download QR code image to local file
const downloadResult = await FileSystem.downloadAsync(
paymentOrder?.qr_code_url,
fileUri
);
// Share using expo-sharing
if (await Sharing.isAvailableAsync()) {
await Share.share({
message: `Pay ${formatAmount(
paymentOrder.amount
)} using this QR code`,
url: paymentOrder.qr_code_url,
await Sharing.shareAsync(downloadResult.uri, {
mimeType: "image/png",
dialogTitle: "Share QR Code",
UTI: "public.png",
});
} else {
Alert.alert("Error", "Sharing is not available on this device");
showSnackbar("Sharing is not available on this device.", "error");
}
} catch (error) {
Alert.alert("Error", "Failed to share QR code");
console.error("Error sharing QR code:", error);
showSnackbar("Failed to share QR code", "error");
}
};
@ -190,74 +198,82 @@ const UpiPaymentScreen = () => {
const { t } = useTranslation();
return (
<View style={styles.container}>
<Header title={t("payment.pay-emi")} showBackButton={false} />
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.content}>
<View style={styles.qrFrame}>
<View style={styles.amountSection}>
<Text style={styles.amountLabel}>
{t("payment.amount-to-be-paid")}
</Text>
<Text style={styles.amount}>
{formatAmount(paymentOrder?.amount)}
</Text>
</View>
<View style={styles.qrCodeContainer}>
<Image
source={{ uri: paymentOrder?.qr_code_url }}
style={styles.qrCode}
contentFit="contain"
/>
</View>
<View style={styles.buttonsContainer}>
<TouchableOpacity
onPress={shareQR}
style={styles.secondaryButton}
>
<ShareIcon />
<Text style={styles.secondaryButtonText}>
{t("payment.share-qr")}
<>
<View style={styles.container}>
<Header title={t("payment.pay-emi")} showBackButton={false} />
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.content}>
<View style={styles.qrFrame}>
<View style={styles.amountSection}>
<Text style={styles.amountLabel}>
{t("payment.amount-to-be-paid")}
</Text>
</TouchableOpacity>
<Text style={styles.paidTo}>{payments.AMOUNT_PAID_TO}</Text>
<Text style={styles.amount}>
{formatAmount(paymentOrder?.amount)}
</Text>
</View>
<View style={styles.qrCodeContainer}>
<Image
source={{ uri: paymentOrder?.qr_code_url }}
style={styles.qrCode}
contentFit="contain"
/>
</View>
<View style={styles.buttonsContainer}>
<TouchableOpacity
onPress={shareQR}
style={styles.secondaryButton}
>
<ShareIcon />
<Text style={styles.secondaryButtonText}>
{t("payment.share-qr")}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={downloadQR}
style={styles.secondaryButton}
>
<DownloadIcon />
<Text style={styles.secondaryButtonText}>
{t("payment.download-qr")}
</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
onPress={downloadQR}
style={styles.secondaryButton}
onPress={payUsingUpiApp}
style={[styles.primaryButton]}
>
<DownloadIcon />
<Text style={styles.secondaryButtonText}>
{t("payment.download-qr")}
<Text style={[styles.primaryButtonText]}>
{t("payment.pay-using-upi")}
</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
onPress={payUsingUpiApp}
style={[styles.primaryButton]}
>
<Text style={[styles.primaryButtonText]}>
{t("payment.pay-using-upi")}
</Text>
</TouchableOpacity>
</View>
<View style={[styles.confirm, { marginBottom: insets.bottom }]}>
<Text style={styles.confirmTitle}>
{t("payment.confirm-payment")}
</Text>
<TouchableOpacity
onPress={() => handlePaymentDone()}
style={styles.paymentDone}
>
<Text style={styles.doneText}>{t("payment.payment-done")}</Text>
</TouchableOpacity>
<View style={[styles.confirm, { marginBottom: insets.bottom }]}>
<Text style={styles.confirmTitle}>
{t("payment.confirm-payment")}
</Text>
<TouchableOpacity
onPress={() => handlePaymentDone()}
style={styles.paymentDone}
>
<Text style={styles.doneText}>{t("payment.payment-done")}</Text>
</TouchableOpacity>
</View>
</View>
</View>
</ScrollView>
</View>
</ScrollView>
</View>
<CancelPayment
visible={isCancelModalVisible}
onClose={toggleCancelModal}
onCancel={handleCancelPayment}
/>
</>
);
};
@ -305,6 +321,7 @@ const styles = StyleSheet.create({
borderRadius: 4,
},
confirmTitle: {
padding: 2,
fontWeight: "400",
fontSize: 14,
lineHeight: 14,
@ -346,6 +363,13 @@ const styles = StyleSheet.create({
textAlign: "center",
marginBottom: 4,
},
paidTo: {
fontSize: 12,
fontWeight: "700",
color: "#253342",
textAlign: "center",
lineHeight: 20,
},
amount: {
fontSize: 20,
fontWeight: "600",

View File

@ -23,6 +23,7 @@ import { useSocket } from "@/contexts/SocketContext";
import { useSnackbar } from "@/contexts/Snackbar";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useTranslation } from "react-i18next";
import { displayValue, formatCurrency } from "@/utils/Common";
const validationSchema = Yup.object().shape({
paymentType: Yup.string().required("Please select a payment option"),
@ -169,34 +170,41 @@ const SelectAmountScreen = () => {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
setFieldValue,
isValid,
validateField,
dirty,
}) => {
const handleQuickAmountPress = (amount: number) => {
const currentAmount = values.customAmount
? parseFloat(values.customAmount)
: 0;
const newAmount = currentAmount + amount;
let newAmount = values.customAmount
? parseFloat(values.customAmount) + amount
: amount;
if (newAmount > payments.MAX_AMOUNT) {
newAmount = payments.MAX_AMOUNT;
}
setFieldValue("paymentType", "custom");
setFieldValue("customAmount", newAmount.toString());
validateField("customAmount");
};
const handleCustomAmountChange = (text: string) => {
const numericText = text.replace(/[^0-9.]/g, "");
let numericText = text.replace(/[^0-9.]/g, "");
const parts = numericText.split(".");
const formattedText =
parts.length > 2
? parts[0] + "." + parts.slice(1).join("")
: numericText;
setFieldValue("customAmount", formattedText, true);
if (parts.length > 2) {
numericText = parts[0] + "." + parts[1];
}
if (parts[1]?.length > 2) {
numericText = parts[0] + "." + parts[1].slice(0, 2);
}
const numValue = parseFloat(numericText);
if (!isNaN(numValue) && numValue > payments.MAX_AMOUNT) {
numericText = payments.MAX_AMOUNT.toString();
}
setFieldValue("customAmount", numericText, true);
setFieldValue("paymentType", "custom");
validateField("customAmount"); // Trigger validation after change
};
const getPaymentAmount = () => {
@ -214,34 +222,15 @@ const SelectAmountScreen = () => {
!isNaN(paymentAmount) &&
paymentAmount >= payments.MIN_AMOUNT);
// Button text logic
let buttonText = "";
if (values.paymentType === "due") {
buttonText = `Pay ₹${paymentAmount.toFixed(2)}`;
} else {
buttonText =
paymentAmount >= payments.MIN_AMOUNT
? `Pay ₹${paymentAmount.toFixed(2)}`
: "Select Amount";
}
return (
<KeyboardAvoidingView
style={styles.container}
behavior={"height"}
// Improved keyboard offset
// keyboardVerticalOffset={Platform.OS === "ios" ? 88 : 0}
>
<View style={styles.container}>
<Header
title={t("payment.select-amount")}
showBackButton={true}
/>
<ScrollView
style={styles.content}
showsVerticalScrollIndicator={false}
// Add keyboard dismiss on scroll
keyboardShouldPersistTaps="handled"
contentContainerStyle={styles.scrollContent}
>
<View style={styles.selectAmountContainer}>
@ -269,7 +258,7 @@ const SelectAmountScreen = () => {
</Text>
</View>
<Text style={styles.amountText}>
{dueAmount?.toFixed(2)}
{displayValue(dueAmount, formatCurrency)}
</Text>
</TouchableOpacity>
@ -317,7 +306,6 @@ const SelectAmountScreen = () => {
placeholderTextColor="#94A3B8"
keyboardType="numeric"
onFocus={() => setFieldValue("paymentType", "custom")}
// Add return key handling
returnKeyType="done"
/>
<View style={styles.helperContainer}>
@ -358,34 +346,28 @@ const SelectAmountScreen = () => {
</Text>
)}
</View>
<View style={[styles.buttonContainer]}>
<TouchableOpacity
style={[
styles.payButton,
!isButtonEnabled && styles.disabledButton,
]}
onPress={() => handleSubmit()}
disabled={!isButtonEnabled}
>
{getPaymentAmount() < payments.MIN_AMOUNT ? (
<Text style={styles.payButtonText}>
{t("payment.select-amount")}
</Text>
) : (
<Text style={styles.payButtonText}>
Pay {displayValue(getPaymentAmount(), formatCurrency)}
</Text>
)}
</TouchableOpacity>
</View>
</ScrollView>
<View
style={[
styles.buttonContainer,
!keyboardVisible && { marginBottom: insets.bottom },
]}
>
<TouchableOpacity
style={[
styles.payButton,
!isButtonEnabled && styles.disabledButton,
]}
onPress={() => handleSubmit()}
disabled={!isButtonEnabled}
>
{getPaymentAmount() < payments.MIN_AMOUNT ? (
<Text style={styles.payButtonText}>
{t("payment.select-amount")}
</Text>
) : (
<Text style={styles.payButtonText}>
Pay {getPaymentAmount().toFixed(2)}
</Text>
)}
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</View>
);
}}
</Formik>

View File

@ -148,7 +148,6 @@ const styles = StyleSheet.create({
},
formContainer: {
flex: 1,
justifyContent: "space-between",
},
inputContainer: {
marginBottom: 24,

View File

@ -0,0 +1,91 @@
import BottomSheetModal from "@/components/common/BottomSheetModal";
import { useTranslation } from "react-i18next";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
interface CancelPaymentProps {
visible: boolean;
onClose: () => void;
onCancel: () => void;
}
export default function CancelPayment({
visible,
onClose,
onCancel,
}: CancelPaymentProps) {
const { t } = useTranslation();
return (
<BottomSheetModal
visible={visible}
onClose={onClose}
heading={t("payment.cancel-payment")}
>
<View style={styles.row}>
<Text>{t("payment.do-you-want-to-cancel-payment")}</Text>
</View>
<View style={styles.buttonsContainer}>
<TouchableOpacity onPress={onCancel} style={styles.button}>
<Text style={styles.bottomButtonText}>
{t("payment.cancel-payment")}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={onClose} style={styles.button}>
<Text style={styles.bottomButtonText}>
{t("payment.continue-payment")}
</Text>
</TouchableOpacity>
</View>
</BottomSheetModal>
);
}
const styles = StyleSheet.create({
buttonsContainer: {
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: 8,
borderTopWidth: 1,
borderBottomWidth: 1,
borderColor: "#D8DDE7",
},
bottomButtonText: {
textAlign: "center",
},
button: {
width: "45%",
height: 44,
paddingHorizontal: 16,
paddingVertical: 8,
borderWidth: 1,
borderColor: "#E5E9F0",
borderRadius: 4,
backgroundColor: "#F3F5F8",
},
row: {
flexDirection: "row",
gap: 16,
justifyContent: "space-between",
},
secondaryButton: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingVertical: 8,
paddingHorizontal: 16,
height: 40,
borderRadius: 4,
backgroundColor: "#F3F5F8",
borderWidth: 1,
borderColor: "#D8DDE7",
width: 156,
gap: 8,
},
fullButton: {
width: "100%",
},
buttonText: {
fontSize: 14,
fontWeight: "500",
color: "#252A34",
},
});

View File

@ -124,7 +124,6 @@ const styles = StyleSheet.create({
textAlign: "left",
},
paymentAmount: {
width: 69,
height: 20,
fontFamily: "Inter-SemiBold",
fontSize: 14,
@ -140,7 +139,6 @@ const styles = StyleSheet.create({
alignItems: "center",
},
paymentDetails: {
width: 237,
height: 16,
fontFamily: "Inter-Regular",
fontSize: 12,
@ -156,7 +154,6 @@ const styles = StyleSheet.create({
alignItems: "center",
paddingHorizontal: 8,
paddingVertical: 2,
marginLeft: 4, // simulate gap
},
statusText: {
fontFamily: "Inter-Medium",

View File

@ -185,11 +185,13 @@ export const issueConfig = [
];
export const payments = {
MIN_AMOUNT: 200,
MIN_AMOUNT: 1,
MAX_AMOUNT: 500000,
LINK_EXPIRED: "Payment link expired",
SOCKET_CONNECTION_TIMEOUT_IN_SECS: 5,
REGISTER_TRANSACTION_EMIT_EVENT_NAME: "register-transaction",
REGISTER_TRANSACTION_LISTEN_EVENT_NAME: "registration-ack",
PAYMENT_CONFIRMATION_EVENT_NAME: "register-transaction",
EMI_WARNING_DAYS_THRESHOLD: 14,
AMOUNT_PAID_TO: "Nav Shakti Lithium Pvt. Ltd.",
};

View File

@ -113,7 +113,10 @@
"pay-using-upi": "Pay using UPI App",
"confirm-payment": "Confirm once your payment is completed",
"payment-done": "Payment Done",
"view-plan": "View Plan"
"view-plan": "View Plan",
"cancel-payment": "Cancel Payment",
"continue-payment": "Continue Payment",
"do-you-want-to-cancel-payment": "Are you sure you want to cancel the payment?"
},
"service": {
"schedule-maintenance": "Schedule Maintenance",

View File

@ -113,7 +113,10 @@
"amount-to-be-paid": "भुगतान की जाने वाली राशि",
"pay-using-upi": "UPI ऐप का उपयोग करके भुगतान करें",
"confirm-payment": "भुगतान पूरा होने पर पुष्टि करें।",
"payment-done": "भुगतान हो गया"
"payment-done": "भुगतान हो गया",
"cancel-payment": "भुगतान रद्द करें",
"continue-payment": "भुगतान जारी रखें",
"do-you-want-to-cancel-payment": "क्या आप वाकई भुगतान रद्द करना चाहते हैं?"
},
"service": {
"schedule-maintenance": "शेड्यूल मेंटेनेंस",

View File

@ -4,3 +4,7 @@ export const displayValue = (value: any, formatter?: (val: any) => string) => {
}
return formatter ? formatter(value) : value;
};
export const formatCurrency = (amount: number) => {
return `${amount.toLocaleString()}`;
};

View File

@ -10,3 +10,28 @@ export const toCamel = (obj: any): any => {
}
return obj;
};
export const formatDate = (dateString?: string | null) => {
if (!dateString) {
return "--";
}
const date = dateString ? new Date(dateString) : new Date();
const optionsDate = {
day: "numeric",
month: "long",
year: "numeric",
} as const;
const optionsTime = {
hour: "numeric",
minute: "2-digit",
hour12: true,
} as const;
const optionsWeekday = { weekday: "long" } as const;
const formattedDate = date.toLocaleDateString("en-IN", optionsDate);
const formattedTime = date.toLocaleTimeString("en-IN", optionsTime);
const formattedWeekday = date.toLocaleDateString("en-IN", optionsWeekday);
return `${formattedDate}, ${formattedTime}, ${formattedWeekday}`;
};