feat: clinic offers

This commit is contained in:
deepvasoya 2025-05-22 11:43:48 +05:30
parent f949253b77
commit a17366037b
10 changed files with 827 additions and 632 deletions

View File

@ -24,7 +24,7 @@ function Header() {
{user?.userType == USER_ROLES.SUPER_ADMIN.toLowerCase() ? null : `Welcome to ${user?.created_clinics?.[0]?.name}`} {user?.userType == USER_ROLES.SUPER_ADMIN.toLowerCase() ? null : `Welcome to ${user?.created_clinics?.[0]?.name}`}
</Typography> </Typography>
<Box className={classes.profilDiv}> <Box className={classes.profilDiv}>
<NotificationSection /> {/* <NotificationSection /> */}
<Box <Box
sx={{ sx={{
zIndex: zIndex:

View File

@ -84,3 +84,49 @@ export const getClinicsDashboardStatsById = () => {
.catch((err) => reject(err)); .catch((err) => reject(err));
}); });
}; };
export const createClinicOffer = (data) => {
const url = `/admin/clinic/offer`;
return new Promise((resolve, reject) => {
axiosInstance
.post(url, data)
.then((response) => resolve(response))
.catch((err) => reject(err));
});
};
export const getClinicOffer = (params) => {
let searchParams = new URLSearchParams();
searchParams.append("size", params?.pagination?.pageSize ?? 10);
searchParams.append("page", params?.pagination.pageIndex ?? 0);
searchParams.append("search", params?.globalFilter ?? "");
const url = `/admin/clinic/offers?${searchParams.toString()}`;
return new Promise((resolve, reject) => {
axiosInstance
.get(url)
.then((response) => resolve(response))
.catch((err) => reject(err));
});
};
export const updateClinicOffer = (data) => {
const url = `/admin/clinic/offer`;
return new Promise((resolve, reject) => {
axiosInstance
.put(url, data)
.then((response) => resolve(response))
.catch((err) => reject(err));
});
};
export const deleteClinicOffer = (id) => {
const url = `/admin/clinic/offer/${id}`;
return new Promise((resolve, reject) => {
axiosInstance
.delete(url)
.then((response) => resolve(response))
.catch((err) => reject(err));
});
};

View File

@ -0,0 +1,35 @@
import { axiosInstance } from "../config/api";
export const getMasterData = (params) => {
let searchParams = new URLSearchParams();
searchParams.append("search", params?.globalFilter ?? "");
const url = `/admin/master-data?${searchParams.toString()}`;
return new Promise((resolve, reject) => {
axiosInstance
.get(url)
.then((response) => resolve(response))
.catch((err) => reject(err));
});
};
export const setMasterData = (data) => {
const url = "/admin/master-data";
return new Promise((resolve, reject) => {
axiosInstance
.post(url, data)
.then((response) => resolve(response))
.catch((err) => reject(err));
});
};
export const updateMasterData = (data) => {
const url = "/admin/master-data";
return new Promise((resolve, reject) => {
axiosInstance
.put(url, data)
.then((response) => resolve(response))
.catch((err) => reject(err));
});
};

View File

@ -9,3 +9,5 @@ export const passwordNumberRegex = /\d/;
// passwordSpacesRegex.js // passwordSpacesRegex.js
export const passwordSpacesRegex = /^\S.*\S$/; export const passwordSpacesRegex = /^\S.*\S$/;
export const emailRegex = /^\S+@\S+\.\S+$/;

View File

@ -249,7 +249,7 @@ const ClinicsList = () => {
[ [
// ..............sro number column...................... // ..............sro number column......................
{ {
size: 60, size: 100,
header: "Sr. No.", header: "Sr. No.",
Cell: (props) => { Cell: (props) => {
const tableState = props?.table?.getState(); const tableState = props?.table?.getState();
@ -330,68 +330,6 @@ const ClinicsList = () => {
); );
}, },
}, },
// ........................... sent email column.............................
// {
// enableSorting: true,
// accessorKey: 'hasActiveCustomPlan',
// minSize: 250,
// header: 'Sent Mail',
// Cell: ({ row }) => (
// <>
// {row?.original?.plans?.[0]?.status ===
// PLAN_STATUS_TYPE.ACTIVATED && (
// <div>
// <Link
// onClick={() => handleRegisterEmailSent(row)}
// className={classes.sendEmailStatus}
// >
// <u>Resend Subscription Mail</u>
// </Link>
// <div className={classes.sendEmailLastSentMailDate}>
// Last Sent On:{' '}
// {format(
// new Date(row?.original?.lastCustomPlanEmailSent),
// 'dd MMM yyyy'
// )}
// </div>
// </div>
// )}
// </>
// ),
// },
// ................add plan column..............................
// {
// enableSorting: true,
// isBold: true,
// size: 120,
// accessorKey: 'hasActiveCustomPlan',
// header: 'Custom Plan',
// Cell: ({ row }) => (
// <>
// {row?.original?.plans?.[0]?.status ===
// PLAN_STATUS_TYPE.ACTIVATED ? (
// <Box>
// <div className={classes.planAddedText}>
// Plan Added <VerifiedIcon className={classes.verifyIcon} />
// </div>
// </Box>
// ) : (
// <Tooltip>
// <Link
// to={{
// pathname: `/plans/${row.original.id}/custom-create`,
// search: `?tab=${type}`,
// }}
// className={classes.addDiscountCodeLink}
// >
// <u>ADD PLAN</u>
// </Link>
// </Tooltip>
// )}
// </>
// ),
// },
].filter(Boolean), ].filter(Boolean),
[type] [type]
); );

View File

@ -1,61 +1,38 @@
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; import DescriptionIcon from '@mui/icons-material/Description';
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import PersonAddIcon from "@mui/icons-material/PersonAdd"; import PersonAddIcon from "@mui/icons-material/PersonAdd";
import SearchIcon from "@mui/icons-material/Search";
import { import {
Alert,
Box, Box,
Button, Button,
Container,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Divider, Divider,
IconButton, IconButton,
InputAdornment,
Paper,
Snackbar,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField, TextField,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import React, { useState } from "react"; import React, { useMemo, useRef, useState } from "react";
import CustomBreadcrumbs from "../../components/CustomBreadcrumbs"; import CustomBreadcrumbs from "../../components/CustomBreadcrumbs";
import PageHeader from "../../components/PageHeader"; import PageHeader from "../../components/PageHeader";
import { pushNotification } from "../../utils/notification";
import { NOTIFICATION } from "../../constants";
import { useStyles } from "./masterDataStyles";
import Table from "../../components/Table";
import { getMasterData, setMasterData } from "../../services/masterData.services";
const MasterDataManagement = () => { const MasterDataManagement = () => {
const classes = useStyles();
const ref = useRef(null);
// State for form fields // State for form fields
const [appointmentType, setAppointmentType] = useState(""); const [type, setType] = useState("");
const queryParams = new URLSearchParams(location.search);
// State for staff list // State for dialog
const [staffList, setStaffList] = useState([]);
// State for staff dialog
const [openDialog, setOpenDialog] = useState(false); const [openDialog, setOpenDialog] = useState(false);
// State for search // Handle dialog open/close
const [searchQuery, setSearchQuery] = useState("");
// State for pagination
const [page, setPage] = useState(1);
const rowsPerPage = 10;
// State for notification
const [notification, setNotification] = useState({
open: false,
message: "",
severity: "success",
});
// Handle staff dialog open/close
const handleOpenDialog = () => { const handleOpenDialog = () => {
setOpenDialog(true); setOpenDialog(true);
}; };
@ -63,38 +40,50 @@ const MasterDataManagement = () => {
const handleCloseDialog = () => { const handleCloseDialog = () => {
setOpenDialog(false); setOpenDialog(false);
// Clear form // Clear form
setAppointmentType(""); setType("");
}; };
// Handle form submission // Handle form submission
const handleSubmit = (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
// Add new staff member const payload = {
const newStaff = { type,
id: staffList.length + 1,
appointmentType,
}; };
setStaffList([...staffList, newStaff]); const response = await setMasterData(payload);
if (response?.data?.data) {
pushNotification("Master Data created successfully!", NOTIFICATION.SUCCESS);
} else {
pushNotification(response?.data?.message, NOTIFICATION.ERROR);
}
// Close dialog // Close dialog
handleCloseDialog(); handleCloseDialog();
// Show success notification
setNotification({
open: true,
message: "Staff member added successfully!",
severity: "success",
});
}; };
// Handle notification close const getData = async (filters) => {
const handleCloseNotification = () => { try {
setNotification({ // Remove the type parameter since it's not defined
...notification, let params = {
open: false, ...filters
}); };
const resp = await getMasterData(params);
console.log('API Response:', resp);
return {
data: resp?.data?.data?.data,
rowCount: resp?.data?.data?.total || 0,
};
} catch (error) {
console.error('Error fetching admins:', error);
return {
data: [],
rowCount: 0,
};
}
}; };
// ...................breadcrumbs array........................ // ...................breadcrumbs array........................
@ -109,244 +98,144 @@ const MasterDataManagement = () => {
}, },
]; ];
const columns = useMemo(() => [
{
size: 100,
header: "Sr. No.",
Cell: (props) => {
const tableState = props?.table?.getState();
const serialNumber = (
props?.row?.index +
1 +
tableState?.pagination?.pageIndex * tableState?.pagination?.pageSize
)
?.toString()
?.padStart(2, "0");
return <span>{serialNumber}</span>;
},
enableSorting: false,
},
{
id: "type", // Add an id to match the accessorKey
size: 280,
accessorKey: "type",
header: "Type",
enableColumnFilter: false,
}
// Removed the empty object that was causing the error
]);
return ( return (
<Container maxWidth="lg" sx={{ py: 4 }}> <Box>
<PageHeader pageTitle="Master Data Management" hideAddButton /> <PageHeader
pageTitle="Master Data Management"
// hideAddButton
addButtonIcon={<AddIcon />}
onAddButtonClick={handleOpenDialog}
/>
<CustomBreadcrumbs breadcrumbs={breadcrumbs} /> <CustomBreadcrumbs breadcrumbs={breadcrumbs} />
<Box className={classes.tableMainDiv}>
<Paper elevation={3} sx={{ p: 0, mb: 4, overflow: "hidden" }}> <Box>
{/* Staff List Header with Add Button */} <Table
<Box hideTopToolbar
sx={{ columns={columns}
display: "flex", getData={getData}
justifyContent: "space-between", options={{
alignItems: "center", enableRowSelection: true,
p: 3, showTopBar: false,
}} showFilters: true,
>
<Typography variant="h6" component="h2" sx={{ fontWeight: "bold" }}>
Master Appointment Type List
</Typography>
<Button
variant="contained"
color="error"
startIcon={<AddIcon />}
onClick={handleOpenDialog}
sx={{
borderRadius: 50,
textTransform: "none",
backgroundColor: "#ff3366",
"&:hover": {
backgroundColor: "#e61653",
},
}}
>
Add Appointment Type
</Button>
</Box>
{/* Search Box */}
<Box sx={{ px: 3, pb: 2 }}>
<TextField
placeholder="Search Appointment Type here"
fullWidth
size="small"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon color="action" />
</InputAdornment>
),
}}
sx={{
backgroundColor: "#fff",
"& .MuiOutlinedInput-root": {
borderRadius: 2,
},
}} }}
searchText="Master Data"
showSearchBox={true}
ref={ref}
// actions={[
// {
// onClick: (row) => {
// },
// text: "Remove",
// icon: (
// <DescriptionIcon />
// // <img
// // src={TransactionHistoryIcon}
// // className={classes.tableActionIcons}
// // alt="transaction history"
// // />
// ),
// },
// ]}
/> />
</Box> </Box>
</Box>
{/* Staff List Table */} <Box className={classes.tableMainDiv}>
<TableContainer> <Dialog
<Table> open={openDialog}
<TableHead> onClose={handleCloseDialog}
<TableRow sx={{ backgroundColor: "#f5f5f5" }}> maxWidth="sm"
<TableCell sx={{ fontWeight: "bold" }}>Sr. No.</TableCell> fullWidth
<TableCell sx={{ fontWeight: "bold" }}> >
Appointment Type <DialogTitle
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{staffList.length > 0 ? (
staffList
.filter((staff) =>
`${staff.appointmentType}`
.toLowerCase()
.includes(searchQuery.toLowerCase())
)
.slice((page - 1) * rowsPerPage, page * rowsPerPage)
.map((staff, index) => (
<TableRow key={staff.id}>
<TableCell>
{(page - 1) * rowsPerPage + index + 1}
</TableCell>
<TableCell>{staff.appointmentType}</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={2}
align="center"
sx={{ py: 5, color: "#666" }}
>
No Appointment Type added yet
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
{/* Pagination */}
{staffList.length > 0 && (
<Box
sx={{ sx={{
display: "flex", display: "flex",
justifyContent: "center", alignItems: "center",
p: 2, justifyContent: "space-between",
borderTop: "1px solid #eee", pb: 1,
}} }}
> >
<Button <Box sx={{ display: "flex", alignItems: "center" }}>
onClick={() => setPage((prev) => Math.max(prev - 1, 1))} <PersonAddIcon sx={{ mr: 1, color: "#0a2d6b" }} />
disabled={page === 1} <Typography
startIcon={<ArrowBackIosNewIcon fontSize="small" />} variant="h6"
sx={{ mx: 1, color: "#666" }} component="span"
> sx={{ fontWeight: "bold", color: "#0a2d6b" }}
Previous >
</Button> Add Appointment Type
</Typography>
</Box>
<IconButton onClick={handleCloseDialog} size="small">
<CloseIcon />
</IconButton>
</DialogTitle>
<Divider />
<DialogContent>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<TextField
label="Appointment Type"
fullWidth
margin="normal"
value={type}
onChange={(e) => setType(e.target.value)}
placeholder="Appointment Type"
required
InputLabelProps={{
shrink: true,
}}
/>
</Box>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 3 }}>
<Button <Button
variant="contained" variant="contained"
disableElevation color="error"
fullWidth
onClick={handleSubmit}
sx={{ sx={{
mx: 1, py: 1.5,
minWidth: "36px", backgroundColor: "#ff3366",
backgroundColor: "#f0f0f0",
color: "#333",
"&:hover": { "&:hover": {
backgroundColor: "#e0e0e0", backgroundColor: "#e61653",
}, },
}} }}
> >
{page} Add Appointment Type
</Button> </Button>
</DialogActions>
<Button </Dialog>
onClick={() => setPage((prev) => prev + 1)} </Box>
disabled={page * rowsPerPage >= staffList.length} </Box>
endIcon={<ArrowForwardIosIcon fontSize="small" />}
sx={{ mx: 1, color: "#666" }}
>
Next
</Button>
</Box>
)}
</Paper>
{/* Add Staff Dialog */}
<Dialog
open={openDialog}
onClose={handleCloseDialog}
maxWidth="sm"
fullWidth
>
<DialogTitle
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
pb: 1,
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<PersonAddIcon sx={{ mr: 1, color: "#0a2d6b" }} />
<Typography
variant="h6"
component="span"
sx={{ fontWeight: "bold", color: "#0a2d6b" }}
>
Add New Appointment Type
</Typography>
</Box>
<IconButton onClick={handleCloseDialog} size="small">
<CloseIcon />
</IconButton>
</DialogTitle>
<Divider />
<DialogContent>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<TextField
label="Appointment Type"
fullWidth
margin="normal"
value={appointmentType}
onChange={(e) => setAppointmentType(e.target.value)}
placeholder="Appointment Type"
required
InputLabelProps={{
shrink: true,
}}
/>
</Box>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 3 }}>
<Button
variant="contained"
color="error"
fullWidth
onClick={handleSubmit}
sx={{
py: 1.5,
backgroundColor: "#ff3366",
"&:hover": {
backgroundColor: "#e61653",
},
}}
>
Add Appointment Type
</Button>
</DialogActions>
</Dialog>
{/* Notification */}
<Snackbar
open={notification.open}
autoHideDuration={4000}
onClose={handleCloseNotification}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
>
<Alert
onClose={handleCloseNotification}
severity={notification.severity}
sx={{ width: "100%" }}
>
{notification.message}
</Alert>
</Snackbar>
</Container>
); );
}; };

View File

@ -0,0 +1,119 @@
import makeStyles from '@mui/styles/makeStyles';
import { pxToRem } from '../../theme/typography';
export const useStyles = makeStyles((theme) => ({
chipClass: {
height: 'fit-content',
minHeight: '30px',
padding: '2px',
alignItems: 'center',
},
statusColor: {
color: theme.palette.primary.main,
fontSize: pxToRem(10),
},
tabsBox: {
display: 'flex',
justifyContent: ' space-around',
// width: '55%',
marginTop: theme.spacing(0.5),
marginRight: theme.spacing(5.0),
alignItems: 'center',
},
secondaryButton: {
width: '200px',
height: '46px',
borderRadius: '8px',
justifyContent: 'space-evenly',
fontSize: pxToRem(16),
},
tableActionIcons: {
marginRight: theme.spacing(1.4),
width: '15px',
},
companyNameTableColumn: {
display: 'flex',
alignItems: 'center',
},
companyName: {
marginLeft: theme.spacing(1),
fontSize: pxToRem(14),
objectFit: 'contain',
width: '260px',
},
companyNameLink: {
textDecoration: 'none',
color: theme.palette.grey[10],
'&:hover': {
color: theme.palette.info.main,
textDecoration: 'underline',
},
},
companyWebsiteLabel: {
fontSize: pxToRem(12),
},
companyNameLogo: {
height: '40px',
width: '40px',
borderRadius: theme.shape.borderRadiusComponent,
objectFit: 'contain',
},
sendEmailStatus: {
fontSize: pxToRem(14),
color: theme.palette.primary.main,
},
sendEmailLastSentMailDate: {
fontSize: pxToRem(12),
},
addDiscountCodeLink: {
fontSize: pxToRem(12),
color: theme.palette.primary.main,
},
addDiscountCodeLabel: {
fontSize: pxToRem(14),
backgroundColor: theme.palette.common.white,
color: theme.palette.common.black,
},
customModel: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
},
customModelBox: {
paddingLeft: theme.spacing(5),
paddingRight: theme.spacing(5),
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
// height: '100%',
border: 'none',
},
newPlanTitleText: {
fontSize: pxToRem(28),
fontFamily: theme.fontFamily.bold,
},
newPlanSubTitleText: {
fontSize: pxToRem(18),
padding: theme.spacing(1),
},
addPlanSuccessIcon: {
padding: theme.spacing(2),
paddingTop: theme.spacing(1),
},
planAddedText: {
fontSize: pxToRem(12),
color: theme.palette.grey[54],
display: 'flex',
alignItems: 'center',
alignSelf: 'center',
},
verifyIcon: {
marginLeft: theme.spacing(0.3),
fontSize: pxToRem(12),
color: theme.palette.grey[54],
},
}));

View File

@ -1,10 +1,10 @@
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import EditIcon from "@mui/icons-material/Edit";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { import {
Box, Box,
Button, Button,
Checkbox, Checkbox,
Container,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
@ -13,113 +13,173 @@ import {
IconButton, IconButton,
MenuItem, MenuItem,
Paper, Paper,
Snackbar,
Alert,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField, TextField,
Typography, Typography,
Divider,
} from "@mui/material"; } from "@mui/material";
import React, { useState } from "react"; import React, { useMemo, useRef, useState } from "react";
import CustomBreadcrumbs from "../../components/CustomBreadcrumbs"; import CustomBreadcrumbs from "../../components/CustomBreadcrumbs";
import PageHeader from "../../components/PageHeader"; import PageHeader from "../../components/PageHeader";
import { useStyles } from "./paymentStyles";
import Table from "../../components/Table";
import { getAdmins } from "../../services/auth.services";
import { pushNotification } from "../../utils/notification";
import { NOTIFICATION } from "../../constants";
import { emailRegex } from "../../utils/regex";
import {
createClinicOffer,
getClinicOffer,
} from "../../services/clinics.service";
const PaymentManagement = () => { const PaymentManagement = () => {
const classes = useStyles();
const ref = useRef(null);
const tableRef = useRef(null);
// State for payment dialog // State for payment dialog
const [paymentDialogOpen, setPaymentDialogOpen] = useState(false); const [paymentDialogOpen, setPaymentDialogOpen] = useState(false);
const [paymentData, setPaymentData] = useState({ const [paymentData, setPaymentData] = useState({
clinicName: "", clinicEmail: "",
setupFeeWaived: false, setupFeeWaived: false,
specialOffer: false, specialOffer: false,
configurationMonth: 3 configurationMonth: 3,
}); });
// State for payments list // State for payments list
const [paymentsList, setPaymentsList] = useState([]); const [paymentsList, setPaymentsList] = useState([]);
const [editPaymentIndex, setEditPaymentIndex] = useState(null); const [editPaymentIndex, setEditPaymentIndex] = useState(null);
// State for notification const handleSubmit = () => {};
const [notification, setNotification] = useState({
open: false, const columns = useMemo(() => [
message: "", {
severity: "success", size: 100,
}); header: "Sr. No.",
Cell: (props) => {
const tableState = props?.table?.getState();
const serialNumber = (
props?.row?.index +
1 +
tableState?.pagination?.pageIndex * tableState?.pagination?.pageSize
)
?.toString()
?.padStart(2, "0");
return <span>{serialNumber}</span>;
},
enableSorting: false,
},
{
size: 260,
accessorKey: "clinic_email",
header: "Clinic Email",
enableColumnFilter: false,
},
{
size: 200,
accessorKey: "setup_fees_waived",
header: "Setup Fees Waived",
Cell: (props) => (props.renderedCellValue ? "Yes" : "No"),
enableColumnFilter: false,
enableSorting: false,
},
{
size: 200,
accessorKey: "special_offer_for_month",
header: "Special Offer For Month",
enableColumnFilter: false,
enableSorting: false,
},
]);
const getData = async (filters) => {
try {
let params = {
...filters,
};
const resp = await getClinicOffer(params);
console.log("API Response:", resp);
return {
data: resp?.data?.data?.data,
rowCount: resp?.data?.total || 0,
};
} catch (error) {
console.error("Error fetching admins:", error);
return {
data: [],
rowCount: 0,
};
}
};
// Handle payment dialog submission // Handle payment dialog submission
const handleAddPayment = () => { const handleAddPayment = async () => {
// Validate input // Validate input
if (!paymentData.clinicName.trim()) { if (!paymentData.clinicEmail.trim()) {
setNotification({ pushNotification("Please enter a clinic email", NOTIFICATION.ERROR);
open: true,
message: "Please enter a clinic name",
severity: "error",
});
return; return;
} }
if (editPaymentIndex !== null) { if (!emailRegex.test(paymentData.clinicEmail)) {
// Update existing payment pushNotification("Please enter a valid clinic email", NOTIFICATION.ERROR);
const updatedPayments = [...paymentsList]; return;
updatedPayments[editPaymentIndex] = {...paymentData};
setPaymentsList(updatedPayments);
setNotification({
open: true,
message: "Payment configuration updated successfully",
severity: "success",
});
} else {
// Add new payment
setPaymentsList([...paymentsList, {...paymentData}]);
setNotification({
open: true,
message: "Payment configuration added successfully",
severity: "success",
});
} }
// either setup fees waived or special offer for month should be true
if (!paymentData.setupFeeWaived && !paymentData.specialOffer) {
pushNotification(
"Please select either setup fees waived or special offer for month",
NOTIFICATION.ERROR
);
return;
}
const payload = {
clinic_email: paymentData.clinicEmail,
setup_fees_waived: paymentData.setupFeeWaived,
special_offer_for_month: paymentData.specialOffer
? paymentData.configurationMonth.toString()
: "0",
};
const resp = await createClinicOffer(payload);
console.log("API Response:", resp);
// Reset form and close dialog // Reset form and close dialog
setPaymentData({ setPaymentData({
clinicName: "", clinicEmail: "",
setupFeeWaived: false, setupFeeWaived: false,
specialOffer: false, specialOffer: false,
configurationMonth: 3 configurationMonth: 3,
}); });
setEditPaymentIndex(null); setEditPaymentIndex(null);
setPaymentDialogOpen(false); setPaymentDialogOpen(false);
// Refresh the table data
if (tableRef.current) {
tableRef.current.reFetchData();
pushNotification("Payment configuration added successfully", NOTIFICATION.SUCCESS);
}
}; };
// Handle edit payment // Handle edit payment
const handleEditPayment = (index) => { const handleEditPayment = (index) => {
setEditPaymentIndex(index); setEditPaymentIndex(index);
setPaymentData({...paymentsList[index]}); setPaymentData({ ...paymentsList[index] });
setPaymentDialogOpen(true); setPaymentDialogOpen(true);
}; };
// Handle delete payment // Handle delete payment
const handleDeletePayment = (index) => { const handleDeletePayment = (index) => {
const updatedPayments = [...paymentsList]; const updatedPayments = [...paymentsList];
updatedPayments.splice(index, 1); updatedPayments.splice(index, 1);
setPaymentsList(updatedPayments); setPaymentsList(updatedPayments);
setNotification({
open: true,
message: "Payment configuration deleted successfully",
severity: "success",
});
};
// Handle notification close pushNotification(
const handleCloseNotification = () => { "Payment configuration deleted successfully",
setNotification({ NOTIFICATION.SUCCESS
...notification, );
open: false,
});
}; };
// Breadcrumbs // Breadcrumbs
@ -129,202 +189,229 @@ const PaymentManagement = () => {
]; ];
return ( return (
<Container maxWidth="lg" sx={{ py: 4 }}> <>
<PageHeader pageTitle="Payment Management" hideAddButton /> <Box>
<PageHeader
<CustomBreadcrumbs breadcrumbs={breadcrumbs} /> pageTitle="Payment Management"
addButtonIcon={<AddIcon />}
<Paper elevation={3} sx={{ p: 0, mb: 4, overflow: "hidden" }}> onAddButtonClick={() => setPaymentDialogOpen(true)}
{/* Payment Management Header with Add Button */} />
<Box <CustomBreadcrumbs breadcrumbs={breadcrumbs} />
sx={{ <Box className={classes.tableMainDiv}>
display: "flex", <Box>
justifyContent: "space-between", <Table
alignItems: "center", ref={tableRef}
p: 3, hideTopToolbar
}} columns={columns}
> getData={getData}
<Typography variant="h6" component="h2" sx={{ fontWeight: "bold" }}> options={{
Payment Configurations enableRowSelection: true,
</Typography> showTopBar: false,
showFilters: true,
<Button }}
variant="contained" searchText="Staff"
color="error" showSearchBox={true}
startIcon={<AddIcon />} actions={[
onClick={() => setPaymentDialogOpen(true)} {
sx={{ onClick: (row) => handleEditPayment(row),
borderRadius: 50, text: "Edit",
textTransform: "none", icon: <EditIcon />,
backgroundColor: "#ff3366", },
"&:hover": { ]}
backgroundColor: "#e61653", />
}, </Box>
}}
>
Add Payment
</Button>
</Box> </Box>
{/* Payment List Table */} {/* Improved Dialog Box */}
<TableContainer> <Dialog
<Table sx={{ minWidth: 650 }} aria-label="payment table"> open={paymentDialogOpen}
<TableHead sx={{ backgroundColor: "#f5f5f5" }}> onClose={() => setPaymentDialogOpen(false)}
<TableRow> maxWidth="sm"
<TableCell sx={{ fontWeight: "bold" }}>Clinic Name</TableCell> fullWidth
<TableCell sx={{ fontWeight: "bold" }}>Setup Fee Waived</TableCell> PaperProps={{
<TableCell sx={{ fontWeight: "bold" }}>Special Offer</TableCell> sx: {
<TableCell sx={{ fontWeight: "bold" }}>Configuration Month</TableCell> borderRadius: "12px",
<TableCell sx={{ fontWeight: "bold" }}>Actions</TableCell> overflow: "hidden",
</TableRow> },
</TableHead> }}
<TableBody> >
{paymentsList.length > 0 ? ( <DialogTitle
paymentsList.map((payment, index) => (
<TableRow key={index}>
<TableCell>{payment.clinicName}</TableCell>
<TableCell>{payment.setupFeeWaived ? "Yes" : "No"}</TableCell>
<TableCell>{payment.specialOffer ? "Yes" : "No"}</TableCell>
<TableCell>
{payment.specialOffer ? payment.configurationMonth : "-"}
</TableCell>
<TableCell>
<Button
size="small"
color="primary"
onClick={() => handleEditPayment(index)}
>
Edit
</Button>
<Button
size="small"
color="error"
onClick={() => handleDeletePayment(index)}
>
Delete
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} align="center">
No payment configurations found
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</Paper>
{/* Payment Dialog */}
<Dialog
open={paymentDialogOpen}
onClose={() => setPaymentDialogOpen(false)}
maxWidth="sm"
fullWidth
>
<DialogTitle>
{editPaymentIndex !== null ? "Edit Payment" : "Add New Payment"}
<IconButton
aria-label="close"
onClick={() => setPaymentDialogOpen(false)}
sx={{ sx={{
position: 'absolute', padding: "16px 24px",
right: 8, backgroundColor: (theme) => theme.palette.primary.main,
top: 8, color: "white",
color: (theme) => theme.palette.grey[500], fontWeight: "bold",
position: "relative",
}} }}
> >
<CloseIcon /> {editPaymentIndex !== null
</IconButton> ? "Edit Payment Configuration"
</DialogTitle> : "Add New Payment"}
<DialogContent> <IconButton
<Box sx={{ mt: 2 }}> aria-label="close"
<TextField onClick={() => setPaymentDialogOpen(false)}
autoFocus sx={{
margin="dense" position: "absolute",
id="clinicName" right: 8,
label="Clinic Name" top: 8,
type="text" color: "white",
fullWidth }}
variant="outlined" >
value={paymentData.clinicName} <CloseIcon />
onChange={(e) => setPaymentData({...paymentData, clinicName: e.target.value})} </IconButton>
sx={{ mb: 2 }} </DialogTitle>
/>
<DialogContent sx={{ padding: "24px 24px 16px" }}>
<FormControlLabel <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
control={ <Typography variant="subtitle2" gutterBottom sx={{ mb: 1 }}>
<Checkbox Clinic Information
checked={paymentData.setupFeeWaived} </Typography>
onChange={(e) => setPaymentData({...paymentData, setupFeeWaived: e.target.checked})}
/>
}
label="Setup fee waived"
sx={{ mb: 1 }}
/>
<FormControlLabel
control={
<Checkbox
checked={paymentData.specialOffer}
onChange={(e) => setPaymentData({...paymentData, specialOffer: e.target.checked})}
/>
}
label="Special Offer: First 3 months free"
sx={{ mb: 1 }}
/>
{paymentData.specialOffer && (
<TextField <TextField
select autoFocus
margin="dense" margin="dense"
id="configurationMonth" id="clinicEmail"
label="Configuration Month" label="Clinic Email"
type="text"
fullWidth fullWidth
variant="outlined" variant="outlined"
value={paymentData.configurationMonth} value={paymentData.clinicEmail}
onChange={(e) => setPaymentData({...paymentData, configurationMonth: e.target.value})} onChange={(e) =>
sx={{ mt: 1 }} setPaymentData({
> ...paymentData,
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((month) => ( clinicEmail: e.target.value,
<MenuItem key={month} value={month}> })
{month} }
</MenuItem> sx={{ mb: 3 }}
))} />
</TextField>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setPaymentDialogOpen(false)} color="primary">
Cancel
</Button>
<Button onClick={handleAddPayment} variant="contained" color="primary">
Save
</Button>
</DialogActions>
</Dialog>
{/* Notification Snackbar */} <Typography variant="subtitle2" gutterBottom sx={{ mb: 1 }}>
<Snackbar Payment Options
open={notification.open} </Typography>
autoHideDuration={6000}
onClose={handleCloseNotification} <Paper
anchorOrigin={{ vertical: "top", horizontal: "right" }} elevation={0}
> sx={{
<Alert backgroundColor: (theme) => theme.palette.grey[50],
onClose={handleCloseNotification} p: 2,
severity={notification.severity} mb: 2,
sx={{ width: "100%" }} border: "1px solid",
> borderColor: (theme) => theme.palette.grey[200],
{notification.message} borderRadius: "8px",
</Alert> }}
</Snackbar> >
</Container> <FormControlLabel
control={
<Checkbox
checked={paymentData.setupFeeWaived}
onChange={(e) =>
setPaymentData({
...paymentData,
setupFeeWaived: e.target.checked,
})
}
color="primary"
/>
}
label={
<Box>
<Typography variant="body1">Setup fee waived</Typography>
<Typography variant="caption" color="text.secondary">
No initial setup fee will be charged
</Typography>
</Box>
}
sx={{ mb: 1, display: "flex", alignItems: "flex-start" }}
/>
<Divider sx={{ my: 1.5 }} />
<FormControlLabel
control={
<Checkbox
checked={paymentData.specialOffer}
onChange={(e) =>
setPaymentData({
...paymentData,
specialOffer: e.target.checked,
})
}
color="primary"
/>
}
label={
<Box>
<Typography variant="body1">Special Offer</Typography>
<Typography variant="caption" color="text.secondary">
First 3 months free of charge
</Typography>
</Box>
}
sx={{ display: "flex", alignItems: "flex-start" }}
/>
</Paper>
{paymentData.specialOffer && (
<TextField
select
margin="dense"
id="configurationMonth"
label="Configuration Month"
fullWidth
variant="outlined"
value={paymentData.configurationMonth}
onChange={(e) =>
setPaymentData({
...paymentData,
configurationMonth: parseInt(e.target.value),
})
}
sx={{ mt: 1 }}
helperText="Number of months for special configuration"
>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((month) => (
<MenuItem key={month} value={month}>
{month} {month === 1 ? "month" : "months"}
</MenuItem>
))}
</TextField>
)}
</Box>
</DialogContent>
<DialogActions
sx={{
padding: "16px 24px",
borderTop: "1px solid",
borderColor: (theme) => theme.palette.grey[200],
justifyContent: "space-between",
}}
>
<Button
onClick={() => setPaymentDialogOpen(false)}
sx={{
color: (theme) => theme.palette.grey[700],
fontWeight: "medium",
}}
>
Cancel
</Button>
<Button
onClick={handleAddPayment}
variant="contained"
color="primary"
sx={{
borderRadius: "8px",
px: 3,
}}
>
{editPaymentIndex !== null ? "Update" : "Save"}
</Button>
</DialogActions>
</Dialog>
</Box>
</>
); );
}; };
export default PaymentManagement; export default PaymentManagement;

View File

@ -0,0 +1,119 @@
import makeStyles from '@mui/styles/makeStyles';
import { pxToRem } from '../../theme/typography';
export const useStyles = makeStyles((theme) => ({
chipClass: {
height: 'fit-content',
minHeight: '30px',
padding: '2px',
alignItems: 'center',
},
statusColor: {
color: theme.palette.primary.main,
fontSize: pxToRem(10),
},
tabsBox: {
display: 'flex',
justifyContent: ' space-around',
// width: '55%',
marginTop: theme.spacing(0.5),
marginRight: theme.spacing(5.0),
alignItems: 'center',
},
secondaryButton: {
width: '200px',
height: '46px',
borderRadius: '8px',
justifyContent: 'space-evenly',
fontSize: pxToRem(16),
},
tableActionIcons: {
marginRight: theme.spacing(1.4),
width: '15px',
},
companyNameTableColumn: {
display: 'flex',
alignItems: 'center',
},
companyName: {
marginLeft: theme.spacing(1),
fontSize: pxToRem(14),
objectFit: 'contain',
width: '260px',
},
companyNameLink: {
textDecoration: 'none',
color: theme.palette.grey[10],
'&:hover': {
color: theme.palette.info.main,
textDecoration: 'underline',
},
},
companyWebsiteLabel: {
fontSize: pxToRem(12),
},
companyNameLogo: {
height: '40px',
width: '40px',
borderRadius: theme.shape.borderRadiusComponent,
objectFit: 'contain',
},
sendEmailStatus: {
fontSize: pxToRem(14),
color: theme.palette.primary.main,
},
sendEmailLastSentMailDate: {
fontSize: pxToRem(12),
},
addDiscountCodeLink: {
fontSize: pxToRem(12),
color: theme.palette.primary.main,
},
addDiscountCodeLabel: {
fontSize: pxToRem(14),
backgroundColor: theme.palette.common.white,
color: theme.palette.common.black,
},
customModel: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
},
customModelBox: {
paddingLeft: theme.spacing(5),
paddingRight: theme.spacing(5),
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
// height: '100%',
border: 'none',
},
newPlanTitleText: {
fontSize: pxToRem(28),
fontFamily: theme.fontFamily.bold,
},
newPlanSubTitleText: {
fontSize: pxToRem(18),
padding: theme.spacing(1),
},
addPlanSuccessIcon: {
padding: theme.spacing(2),
paddingTop: theme.spacing(1),
},
planAddedText: {
fontSize: pxToRem(12),
color: theme.palette.grey[54],
display: 'flex',
alignItems: 'center',
alignSelf: 'center',
},
verifyIcon: {
marginLeft: theme.spacing(0.3),
fontSize: pxToRem(12),
color: theme.palette.grey[54],
},
}));

View File

@ -1,28 +1,15 @@
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import PersonAddIcon from "@mui/icons-material/PersonAdd"; import PersonAddIcon from "@mui/icons-material/PersonAdd";
import SearchIcon from "@mui/icons-material/Search";
import { import {
Alert,
Box, Box,
Button, Button,
Container,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Divider, Divider,
IconButton, IconButton,
InputAdornment,
Paper,
Snackbar,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField, TextField,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
@ -32,12 +19,10 @@ import PageHeader from "../../components/PageHeader";
import { createAdmin, getAdmins } from "../../services/auth.services"; import { createAdmin, getAdmins } from "../../services/auth.services";
import { pushNotification } from "../../utils/notification"; import { pushNotification } from "../../utils/notification";
import { NOTIFICATION } from "../../constants"; import { NOTIFICATION } from "../../constants";
import { useTheme } from "@emotion/react";
import { useStyles } from "./staffStyles"; import { useStyles } from "./staffStyles";
import Table from "../../components/Table"; import Table from "../../components/Table";
const StaffManagement = () => { const StaffManagement = () => {
const theme = useTheme();
const classes = useStyles(); const classes = useStyles();
const ref = useRef(null); const ref = useRef(null);
// State for form fields // State for form fields
@ -46,26 +31,9 @@ const StaffManagement = () => {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [emailError, setEmailError] = useState(""); const [emailError, setEmailError] = useState("");
// State for staff list
const [staffList, setStaffList] = useState([]);
// State for dialog // State for dialog
const [openDialog, setOpenDialog] = useState(false); const [openDialog, setOpenDialog] = useState(false);
// State for search
const [searchQuery, setSearchQuery] = useState("");
// State for pagination
const [page, setPage] = useState(1);
const rowsPerPage = 10;
// State for notification
const [notification, setNotification] = useState({
open: false,
message: "",
severity: "success",
});
// Email validation function // Email validation function
const validateEmail = (email) => { const validateEmail = (email) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@ -113,14 +81,6 @@ const StaffManagement = () => {
handleCloseDialog(); handleCloseDialog();
}; };
// Handle notification close
const handleCloseNotification = () => {
setNotification({
...notification,
open: false,
});
};
const getData = async (filters) => { const getData = async (filters) => {
try { try {
// Remove the type parameter since it's not defined // Remove the type parameter since it's not defined