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 ForgotPassword from "../views/ForgotPassword";
import ResetPassword from "../views/ResetPassword"; import ResetPassword from "../views/ResetPassword";
import SignupCharges from "../views/SignupCharges"; import SignupCharges from "../views/SignupCharges";
import PaymentSuccessPage from "../views/WaitingPage";
export const routesData = [ export const routesData = [
{ {
@ -51,9 +52,10 @@ export const routesData = [
component: ForgotPassword, component: ForgotPassword,
}, },
{ {
path: 'reset-password', path: "reset-password",
component: ResetPassword, component: ResetPassword,
}, },
{ path: "waiting", component: PaymentSuccessPage },
], ],
}, },
]; ];

View File

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

View File

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

View File

@ -191,7 +191,7 @@ const ClinicDocUpdater = () => {
isDisabled={formik.values.isContractVerified} isDisabled={formik.values.isContractVerified}
/> />
</Grid> </Grid>
<Grid item md={4} sm={12} xs={12}> {/* <Grid item md={4} sm={12} xs={12}>
<CustomFileUpload <CustomFileUpload
label="Add Logo*" label="Add Logo*"
documentName="logo" documentName="logo"
@ -206,7 +206,7 @@ const ClinicDocUpdater = () => {
} }
isDisabled={formik.values.isLogoVerified} isDisabled={formik.values.isLogoVerified}
/> />
</Grid> </Grid> */}
</Grid> </Grid>
<Button <Button
onClick={handleSubmit} 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, clinicPhone: number,
clinicPonePrefix: countryCode, clinicPonePrefix: countryCode,
clinicAddress: clinicResp?.address, clinicAddress: clinicResp?.address,
clinicOthers: clinicResp?.general_info, clinicOthers: clinicResp?.general_info ?? "",
otherInfo: clinicResp?.other_info ?? formData.otherInfo, otherInfo: clinicResp?.other_info ?? "",
clinicGreetings: clinicResp?.greeting_msg ?? formData.clinicGreetings, clinicGreetings: clinicResp?.greeting_msg ?? "",
clinicScenarios: clinicResp?.scenarios ?? formData.clinicScenarios, clinicScenarios: clinicResp?.scenarios ?? "",
integrationSoftware: clinicResp?.integration, integrationSoftware: clinicResp?.integration,
practiceId: clinicResp?.pms_id, practiceId: clinicResp?.pms_id,
practiceName: clinicResp?.practice_name, 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} className={classes.mobileNumberInput}
type="string" type="string"
style={{ width: "40%" }} style={{ width: "40%" }}
label="Clinic Phone Number"
name="clinicPhone" name="clinicPhone"
value={formData.clinicPhone} value={formData.clinicPhone}
onChange={(e) => { onChange={(e) => {
@ -1018,7 +1017,27 @@ If No -Pt who do not have a Medicare card / Private patients with or without I
</Typography> </Typography>
{/* Stepper */} {/* 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) => { {steps.map((label, index) => {
const stepProps = {}; const stepProps = {};
const labelProps = {}; const labelProps = {};
@ -1054,9 +1073,9 @@ If No -Pt who do not have a Medicare card / Private patients with or without I
</Typography> </Typography>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}> <Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Box sx={{ flex: "1 1 auto" }} /> <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 Back
</Button> </Button> */}
<Button onClick={handleSubmit} variant="contained" color="primary" type="submit" disabled={activeStep == 4 && !testConnDone}> <Button onClick={handleSubmit} variant="contained" color="primary" type="submit" disabled={activeStep == 4 && !testConnDone}>
Submit Setup Submit Setup
</Button> </Button>

View File

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

View File

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

View File

@ -143,7 +143,7 @@ const MasterDataManagement = () => {
const handleEdit = async (row) => { const handleEdit = async (row) => {
setEditId(row.original.id); // Store the ID for update operation 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); setOpenDialog(true);
}; };

View File

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