fix: minor changes

feat: invoice waiting page
This commit is contained in:
deepvasoya 2025-06-02 12:22:10 +05:30
parent 9e39a1f9a1
commit 344d5792ed
10 changed files with 522 additions and 79 deletions

View File

@ -16,6 +16,7 @@ import ClinicDocUpdater from "../views/ClinicDocUpdate";
import ForgotPassword from "../views/ForgotPassword";
import ResetPassword from "../views/ResetPassword";
import SignupCharges from "../views/SignupCharges";
import PaymentSuccessPage from "../views/WaitingPage";
export const routesData = [
{
@ -51,9 +52,10 @@ export const routesData = [
component: ForgotPassword,
},
{
path: 'reset-password',
path: "reset-password",
component: ResetPassword,
},
{ path: "waiting", component: PaymentSuccessPage },
],
},
];

View File

@ -137,7 +137,7 @@ export const deleteClinicOffer = (id) => {
export const getClinicDoctors = (params) => {
let searchParams = new URLSearchParams();
searchParams.append("limit", params?.pagination?.pageSize ?? 10);
searchParams.append("page", params?.pagination.pageIndex + 1 ?? 1);
searchParams.append("page", params?.pagination?.pageIndex + 1 ?? 1);
searchParams.append("search", params?.globalFilter ?? "");
const url = `/clinic-doctors?${searchParams.toString()}`;

View File

@ -2,8 +2,8 @@ import { axiosInstance } from "../config/api";
export const getMasterData = (params) => {
let searchParams = new URLSearchParams();
searchParams.append("limit", params?.pagination?.pageSize ?? 10);
searchParams.append("page", params?.pagination.pageIndex + 1 ?? 1);
searchParams.append("limit", 100);
searchParams.append("page", 1);
searchParams.append("search", params?.globalFilter ?? "");
const url = `/admin/master-data?${searchParams.toString()}`;

View File

@ -191,7 +191,7 @@ const ClinicDocUpdater = () => {
isDisabled={formik.values.isContractVerified}
/>
</Grid>
<Grid item md={4} sm={12} xs={12}>
{/* <Grid item md={4} sm={12} xs={12}>
<CustomFileUpload
label="Add Logo*"
documentName="logo"
@ -206,7 +206,7 @@ const ClinicDocUpdater = () => {
}
isDisabled={formik.values.isLogoVerified}
/>
</Grid>
</Grid> */}
</Grid>
<Button
onClick={handleSubmit}

View File

@ -229,10 +229,10 @@ If No -Pt who do not have a Medicare card / Private patients with or without I
clinicPhone: number,
clinicPonePrefix: countryCode,
clinicAddress: clinicResp?.address,
clinicOthers: clinicResp?.general_info,
otherInfo: clinicResp?.other_info ?? formData.otherInfo,
clinicGreetings: clinicResp?.greeting_msg ?? formData.clinicGreetings,
clinicScenarios: clinicResp?.scenarios ?? formData.clinicScenarios,
clinicOthers: clinicResp?.general_info ?? "",
otherInfo: clinicResp?.other_info ?? "",
clinicGreetings: clinicResp?.greeting_msg ?? "",
clinicScenarios: clinicResp?.scenarios ?? "",
integrationSoftware: clinicResp?.integration,
practiceId: clinicResp?.pms_id,
practiceName: clinicResp?.practice_name,
@ -652,7 +652,6 @@ If No -Pt who do not have a Medicare card / Private patients with or without I
className={classes.mobileNumberInput}
type="string"
style={{ width: "40%" }}
label="Clinic Phone Number"
name="clinicPhone"
value={formData.clinicPhone}
onChange={(e) => {
@ -1018,7 +1017,27 @@ If No -Pt who do not have a Medicare card / Private patients with or without I
</Typography>
{/* Stepper */}
<Stepper activeStep={activeStep} sx={{ mb: 4 }}>
<Stepper
activeStep={activeStep}
sx={{
mb: 4,
'& .Mui-completed': {
'& .MuiStepIcon-root': {
color: 'success.main',
},
'& .MuiStepLabel-label.Mui-completed': {
color: 'success.main',
fontWeight: 'bold',
},
},
'& .MuiStepIcon-active': {
color: 'primary.main',
},
'& .MuiStepIcon-completed': {
color: 'success.main',
},
}}
>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
@ -1054,9 +1073,9 @@ If No -Pt who do not have a Medicare card / Private patients with or without I
</Typography>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Box sx={{ flex: "1 1 auto" }} />
<Button onClick={handleBack} sx={{ mr: 1 }} color="primary" variant="outlined">
{/* <Button onClick={handleBack} sx={{ mr: 1 }} color="primary" variant="outlined">
Back
</Button>
</Button> */}
<Button onClick={handleSubmit} variant="contained" color="primary" type="submit" disabled={activeStep == 4 && !testConnDone}>
Submit Setup
</Button>

View File

@ -84,18 +84,13 @@ const ClinicsList = () => {
{
enableColumnFilter: false,
enableSorting: true,
size: "220px",
size: 100,
accessorKey: "name",
header: "Clinic Name",
Cell: ({ row }) => (
<>
<div className={classes.companyNameTableColumn}>
{/* <img
className={classes.companyNameLogo}
src="https://dev-ai-appointment.s3.ap-southeast-2.amazonaws.com/common/clinic_logo.jpg"
alt="logo"
/> */}
<div className={classes.companyName}>
<div>
<div>
<Link
to={{
pathname: `/clinics/${row?.original?.id}`,
@ -105,10 +100,6 @@ const ClinicsList = () => {
>
<>{row?.original.name}</>
</Link>
<div className={classes.companyWebsiteLabel}>
{row.original.website}
</div>
</div>
</div>
</>
@ -118,7 +109,7 @@ const ClinicsList = () => {
{
enableColumnFilter: false,
enableSorting: true,
size: 100,
size: 150,
accessorKey: "update_time",
header: "Req.Raised On",
Cell: ({ row }) => (
@ -132,43 +123,43 @@ const ClinicsList = () => {
),
},
// .................send email column..................
{
enableColumnFilter: false,
enableSorting: true,
accessorKey: "lastEmailSent",
size: 190,
header: "Send Mail",
Cell: ({ row }) => (
<>
{(row.original.status === CLINIC_STATUS.REJECTED ||
row.original.status === CLINIC_STATUS.ON_HOLD) && (
<div>
<Link
onClick={() => handleEmailSent(row)}
className={classes.sendEmailStatus}
>
<u>
{row.original.status === CLINIC_STATUS.ON_HOLD &&
"Resend On Hold Mail"}
{row.original.status === CLINIC_STATUS.REJECTED &&
"Resend Rejection Mail"}
</u>
</Link>
<div className={classes.sendEmailLastSentMailDate}>
Last Sent On:
{format(
new Date(row?.original?.lastEmailSent),
"dd MMM yyyy"
)}
</div>
</div>
)}
</>
),
},
// {
// enableColumnFilter: false,
// enableSorting: true,
// accessorKey: "lastEmailSent",
// size: 190,
// header: "Send Mail",
// Cell: ({ row }) => (
// <>
// {(row.original.status === CLINIC_STATUS.REJECTED ||
// row.original.status === CLINIC_STATUS.ON_HOLD) && (
// <div>
// <Link
// onClick={() => handleEmailSent(row)}
// className={classes.sendEmailStatus}
// >
// <u>
// {row.original.status === CLINIC_STATUS.ON_HOLD &&
// "Resend On Hold Mail"}
// {row.original.status === CLINIC_STATUS.REJECTED &&
// "Resend Rejection Mail"}
// </u>
// </Link>
// <div className={classes.sendEmailLastSentMailDate}>
// Last Sent On:
// {format(
// new Date(row?.original?.lastEmailSent),
// "dd MMM yyyy"
// )}
// </div>
// </div>
// )}
// </>
// ),
// },
// .................location column..................
{
size: 100,
size: 150,
enableColumnFilter: false,
enableSorting: true,
accessorKey: "city",
@ -196,7 +187,7 @@ const ClinicsList = () => {
accessorFn: ({ status }) => (
<>
<Chip
className={classes.chipClass}
// className={classes.chipClass}
label={
status === CLINIC_STATUS.UNDER_REVIEW.toLowerCase() ? (
"Under Review"
@ -275,14 +266,8 @@ const ClinicsList = () => {
header: "Clinic Name",
Cell: ({ row }) => (
<>
<div className={classes.companyNameTableColumn}>
{/* <img
className={classes.companyNameLogo}
src="https://dev-ai-appointment.s3.ap-southeast-2.amazonaws.com/common/clinic_logo.jpg"
alt="logo"
/> */}
<div className={classes.companyName}>
<div>
<div>
<Link
to={{
pathname: `/clinics/${row?.original?.id}`,
@ -292,9 +277,6 @@ const ClinicsList = () => {
>
<>{row?.original.name}</>
</Link>
<div className={classes.companyWebsiteLabel}>
{row.original.website}
</div>
</div>
</div>
</>

View File

@ -146,13 +146,13 @@ const PaymentConfig = () => {
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="Per Minute Charges"
label="Per Call Charges"
name="per_call_charges"
value={paymentConfig.per_call_charges}
onChange={handleInputChange}
InputProps={{
startAdornment: <InputAdornment position="start">$</InputAdornment>,
endAdornment: <InputAdornment position="end">/min</InputAdornment>,
endAdornment: <InputAdornment position="end">/calls</InputAdornment>,
}}
variant="outlined"
/>

View File

@ -143,7 +143,7 @@ const MasterDataManagement = () => {
const handleEdit = async (row) => {
setEditId(row.original.id); // Store the ID for update operation
setType(row.original.type);
setType(row.original.type.charAt(0).toUpperCase() + row.original.type.slice(1));
setOpenDialog(true);
};

View File

@ -346,7 +346,10 @@ function YourDetailsForm() {
pushNotification(response?.data?.message, NOTIFICATION.ERROR);
return;
}
pushNotification("OTP sent successfully to your email", NOTIFICATION.SUCCESS);
pushNotification(
"OTP sent successfully to your email",
NOTIFICATION.SUCCESS
);
};
const verifyOTP = async (e) => {
@ -393,8 +396,9 @@ function YourDetailsForm() {
return;
}
pushNotification(response?.data?.message, NOTIFICATION.SUCCESS);
localStorage.setItem("temp_signup_token", response?.data?.data?.token);
// open new page for stripe payment from url
const newWindow = window.open(response?.data?.data, "_blank", "noopener,noreferrer");
window.open(response?.data?.data?.url, "_blank", "noopener,noreferrer");
// if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') {
// // Fallback in case popup is blocked
// window.location.href = response?.data?.data;

View File

@ -0,0 +1,436 @@
import React, { useState, useEffect, useRef } from "react";
import {
Box,
Card,
CardContent,
Typography,
Button,
CircularProgress,
Avatar,
LinearProgress,
Chip,
Stack,
Paper,
Divider,
Alert,
} from "@mui/material";
import {
CheckCircle,
Schedule,
ArrowBack,
OpenInNew,
Error,
Refresh,
} from "@mui/icons-material";
import { green, blue, red, grey } from "@mui/material/colors";
import { API_BASE_URL } from "../../common/envVariables";
import { useNavigate } from "react-router-dom";
const PaymentSuccessPage = () => {
const [status, setStatus] = useState("processing"); // 'processing', 'success', 'error'
const [invoiceUrl, setInvoiceUrl] = useState("");
const [timeElapsed, setTimeElapsed] = useState(0);
const [token, setToken] = useState("");
const navigate = useNavigate();
// Use refs to track active polling and timer
const pollingRef = useRef(null);
const timerRef = useRef(null);
const isPollingActiveRef = useRef(false);
const checkPaymentStatus = async () => {
try {
if (!token || !isPollingActiveRef.current) {
return;
}
const response = await fetch(`${API_BASE_URL}/stripe/get-invoice`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
if (data.data && isPollingActiveRef.current) {
// Success - stop polling and timer
isPollingActiveRef.current = false;
if (pollingRef.current) {
clearTimeout(pollingRef.current);
pollingRef.current = null;
}
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
setStatus("success");
setInvoiceUrl(data.data);
} else if (isPollingActiveRef.current) {
// Still processing, check again in 10 seconds
pollingRef.current = setTimeout(() => checkPaymentStatus(), 10000);
}
} catch (error) {
console.error("Error checking payment status:", error);
if (isPollingActiveRef.current) {
// Retry after 10 seconds on error
pollingRef.current = setTimeout(() => checkPaymentStatus(), 10000);
}
}
};
const startTimer = () => {
timerRef.current = setInterval(() => {
setTimeElapsed(prev => prev + 1);
}, 1000);
};
const stopPollingAndTimer = () => {
isPollingActiveRef.current = false;
if (pollingRef.current) {
clearTimeout(pollingRef.current);
pollingRef.current = null;
}
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
const openInvoice = () => {
// Stop all polling and timer before navigation
stopPollingAndTimer();
if (invoiceUrl) {
window.open(invoiceUrl, "_blank");
}
// Clean up and navigate
localStorage.removeItem("temp_signup_token");
setToken("");
navigate("/auth/login");
};
const goToLogin = () => {
stopPollingAndTimer();
localStorage.removeItem("temp_signup_token");
navigate("/auth/login");
};
const goToDashboard = () => {
stopPollingAndTimer();
navigate("/dashboard");
};
const handleRetry = () => {
stopPollingAndTimer();
setStatus("processing");
setTimeElapsed(0);
isPollingActiveRef.current = true;
startTimer();
checkPaymentStatus();
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, "0")}`;
};
useEffect(() => {
const storedToken = localStorage.getItem("temp_signup_token");
if (!storedToken) {
navigate("/auth/login");
return;
}
setToken(storedToken);
isPollingActiveRef.current = true;
startTimer();
// Start checking payment status once token is set
const initializeChecking = async () => {
if (storedToken) {
checkPaymentStatus();
}
};
initializeChecking();
// Cleanup function
return () => {
stopPollingAndTimer();
};
}, []); // Remove token from dependency array to avoid infinite loop
// Separate useEffect for when token changes
useEffect(() => {
if (token && status === "processing" && !isPollingActiveRef.current) {
isPollingActiveRef.current = true;
checkPaymentStatus();
}
}, [token]);
if (status === "processing") {
return (
<Box
sx={{
minHeight: "100vh",
background: "linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
p: 2,
}}
>
<Card sx={{ maxWidth: 400, width: "100%", textAlign: "center" }}>
<CardContent sx={{ p: 4 }}>
{/* Header */}
<Avatar
sx={{
width: 64,
height: 64,
bgcolor: blue[100],
color: blue[600],
mx: "auto",
mb: 2,
}}
>
<Schedule />
</Avatar>
<Typography variant="h5" fontWeight="bold" gutterBottom>
Processing Your Payment
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Please wait while we confirm your subscription and prepare your
invoice...
</Typography>
{/* Loading Animation */}
<Box sx={{ mb: 3 }}>
<CircularProgress size={40} sx={{ mb: 2 }} />
<Typography variant="body2" color="text.secondary">
Time elapsed: {formatTime(timeElapsed)}
</Typography>
</Box>
{/* Progress Steps */}
<Box sx={{ mb: 4 }}>
<Stack spacing={1}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<CheckCircle
sx={{ color: green[500], mr: 1, fontSize: 18 }}
/>
<Typography variant="body2" sx={{ color: green[600] }}>
Payment Submitted
</Typography>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<CircularProgress
size={16}
sx={{ mr: 1, color: blue[500] }}
/>
<Typography variant="body2" sx={{ color: blue[600] }}>
Confirming Payment
</Typography>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<Schedule sx={{ color: grey[400], mr: 1, fontSize: 18 }} />
<Typography variant="body2" color="text.disabled">
Generating Invoice
</Typography>
</Box>
</Stack>
</Box>
{/* Back Button */}
<Button
fullWidth
variant="outlined"
startIcon={<ArrowBack />}
onClick={goToLogin}
sx={{ mt: 2 }}
>
Back to Login
</Button>
</CardContent>
</Card>
</Box>
);
}
if (status === "success") {
return (
<Box
sx={{
minHeight: "100vh",
background: "linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
p: 2,
}}
>
<Card sx={{ maxWidth: 400, width: "100%", textAlign: "center" }}>
<CardContent sx={{ p: 4 }}>
{/* Success Header */}
<Avatar
sx={{
width: 64,
height: 64,
bgcolor: green[100],
color: green[600],
mx: "auto",
mb: 2,
}}
>
<CheckCircle />
</Avatar>
<Typography variant="h5" fontWeight="bold" gutterBottom>
Payment Successful!
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Your subscription has been activated and your invoice is ready.
</Typography>
{/* Processing Time */}
<Chip
label={`Processed in ${formatTime(timeElapsed)}`}
color="success"
variant="outlined"
sx={{ mb: 3 }}
/>
{/* Action Buttons */}
<Stack spacing={2}>
<Button
fullWidth
variant="contained"
startIcon={<OpenInNew />}
onClick={openInvoice}
sx={{ py: 1.5 }}
>
View Invoice
</Button>
<Button
fullWidth
variant="outlined"
onClick={goToDashboard}
sx={{ py: 1.5 }}
>
Continue to Dashboard
</Button>
<Divider sx={{ my: 1 }} />
<Button
fullWidth
color="inherit"
startIcon={<ArrowBack />}
onClick={goToLogin}
>
Back to Login
</Button>
</Stack>
</CardContent>
</Card>
</Box>
);
}
if (status === "error") {
return (
<Box
sx={{
minHeight: "100vh",
background: "linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
p: 2,
}}
>
<Card sx={{ maxWidth: 400, width: "100%", textAlign: "center" }}>
<CardContent sx={{ p: 4 }}>
{/* Error Header */}
<Avatar
sx={{
width: 64,
height: 64,
bgcolor: red[100],
color: red[600],
mx: "auto",
mb: 2,
}}
>
<Error />
</Avatar>
<Typography variant="h5" fontWeight="bold" gutterBottom>
Payment Processing Error
</Typography>
<Alert severity="error" sx={{ mb: 3, textAlign: "left" }}>
We encountered an issue while processing your payment. Please try
again or contact support.
</Alert>
{/* Action Buttons */}
<Stack spacing={2}>
<Button
fullWidth
variant="contained"
startIcon={<Refresh />}
onClick={handleRetry}
sx={{ py: 1.5 }}
>
Try Again
</Button>
<Button
fullWidth
variant="outlined"
startIcon={<ArrowBack />}
onClick={goToLogin}
sx={{ py: 1.5 }}
>
Back to Login
</Button>
</Stack>
</CardContent>
</Card>
</Box>
);
}
};
export default PaymentSuccessPage;