feat: protected routes for clinic docs update

fix: doc upload after rejection
This commit is contained in:
deepvasoya 2025-05-21 11:25:07 +05:30
parent 3d91cee029
commit 35f21f2749
10 changed files with 404 additions and 100 deletions

View File

@ -117,9 +117,6 @@ const CustomFileUpload = forwardRef(function CustomFileUpload(
setIsLoading(true);
const response = await getPresignedUrl(filePayload);
// Debug the response structure
console.log('API Response:', response);
// Check if we have a valid response with the expected structure
if (response?.data?.data?.Key) {
// Use the Key from the response
@ -128,7 +125,6 @@ const CustomFileUpload = forwardRef(function CustomFileUpload(
} else {
// If the expected structure is not found, try to find the key in a different location
// or use a fallback value
console.log('Response structure is different than expected');
// Try different possible paths to find the key
const key = response?.data?.Key ||
@ -136,7 +132,6 @@ const CustomFileUpload = forwardRef(function CustomFileUpload(
response?.Key ||
value.name; // Fallback to the file name if key not found
console.log('Using key:', key);
onUploadDone(documentName, key);
// Try to find the API URL similarly

View File

@ -1,9 +1,9 @@
import axios from 'axios';
import { API_BASE_URL } from '../common/envVariables';
import { ERRORS, NOTIFICATION } from '../constants';
import { pushNotification } from '../utils/notification';
import { commonLogoutFunc } from '../utils/share';
import store from '../redux/store';
import axios from "axios";
import { API_BASE_URL } from "../common/envVariables";
import { ERRORS, NOTIFICATION } from "../constants";
import { pushNotification } from "../utils/notification";
import { commonLogoutFunc } from "../utils/share";
import store from "../redux/store";
export const axiosInstance = axios.create({
baseURL: API_BASE_URL,
@ -20,10 +20,15 @@ export const axiosInstance = axios.create({
axiosInstance.interceptors.request.use(
async function (config) {
try {
const token = JSON.parse(localStorage.getItem('redux'));
const token = JSON.parse(localStorage.getItem("redux"));
if (token?.login?.token) {
config.headers.Authorization = `Bearer ${token?.login?.token}`;
}
// if url is dev-ai-appointment.s3.amazonaws.com
if (config.url?.includes("dev-ai-appointment.s3.amazonaws.com")) {
config.headers.Authorization = null;
}
const state = store.getState();
const companyId = state?.loginAsCompanyAdmin?.companyId; // Extract companyId
if (companyId) {
@ -58,7 +63,7 @@ axiosInstance.interceptors.response.use(
console.log(error);
if (!error?.response?.data) {
pushNotification(error.message, NOTIFICATION.ERROR);
if (error.message !== 'Network Error') commonLogoutFunc();
if (error.message !== "Network Error") commonLogoutFunc();
} else if (
error?.response?.data?.error &&
error?.response?.data?.error[0]?.message
@ -91,10 +96,10 @@ axiosInstance.interceptors.response.use(
export const fileDownloadAxios = axios.create({
baseURL: API_BASE_URL,
responseType: 'blob',
responseType: "blob",
crossDomain: true,
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
withCredentials: true,
timeout: 300000,
@ -103,10 +108,16 @@ export const fileDownloadAxios = axios.create({
fileDownloadAxios.interceptors.request.use(
async function (config) {
try {
const token = JSON.parse(localStorage.getItem('redux'));
const token = JSON.parse(localStorage.getItem("redux"));
if (token?.data?.data) {
config.headers.Authorization = `${token?.data?.data}`;
}
// if url is dev-ai-appointment.s3.amazonaws.com
if (config.url?.includes("dev-ai-appointment.s3.amazonaws.com")) {
config.headers.Authorization = null;
}
const state = store.getState();
const companyId = state?.loginAsCompanyAdmin?.companyId; // Extract companyId
if (companyId) {
@ -131,7 +142,7 @@ fileDownloadAxios.interceptors.response.use(
async function (response) {
// Handle error response (when server responds with JSON instead of a file)
if (
response?.headers['content-type']?.includes('application/json') &&
response?.headers["content-type"]?.includes("application/json") &&
response?.data
) {
try {
@ -140,14 +151,14 @@ fileDownloadAxios.interceptors.response.use(
if (parsedError?.error) {
pushNotification(
parsedError?.message || 'Download failed',
parsedError?.message || "Download failed",
NOTIFICATION.ERROR
);
return Promise.reject(parsedError);
}
} catch (err) {
// eslint-disable-next-line no-console
console.error('Error parsing error response', err);
console.error("Error parsing error response", err);
}
}
@ -159,26 +170,26 @@ fileDownloadAxios.interceptors.response.use(
if (!error?.response?.data) {
pushNotification(error.message, NOTIFICATION.ERROR);
if (error.message !== 'Network Error') commonLogoutFunc();
if (error.message !== "Network Error") commonLogoutFunc();
} else {
// Handle error message from API
const contentType = error?.response?.headers?.['content-type'];
const contentType = error?.response?.headers?.["content-type"];
if (contentType?.includes('application/json')) {
if (contentType?.includes("application/json")) {
error.response.data.text().then((text) => {
try {
const parsedError = JSON.parse(text);
pushNotification(
parsedError.message || 'Something went wrong',
parsedError.message || "Something went wrong",
NOTIFICATION.ERROR
);
} catch (err) {
pushNotification('Unknown error occurred', NOTIFICATION.ERROR);
pushNotification("Unknown error occurred", NOTIFICATION.ERROR);
}
});
} else {
pushNotification(
error?.response?.data?.message || 'Download failed',
error?.response?.data?.message || "Download failed",
NOTIFICATION.ERROR
);
}

View File

@ -21,9 +21,12 @@ import { CLINIC_STATUS, HIDE_MODULES } from "../../../constants";
import { isBSPortal } from "../../../utils/share";
import { hideAndShowFunctionality } from "../../../views/Signup/signupAction";
import { useStyles } from "../mainLayoutStyles";
// Import the configuration from the separate file
import { SIDEBAR_CONFIG } from "./sideBarConfig"; // Adjust path if necessary
import {
selectClinicStatus,
selectUserRole,
} from "../../../redux/userRoleSlice";
// NOTE: Removed the internal SIDEBAR_ITEMS definition.
// We will now use the imported SIDEBAR_CONFIG.
@ -41,6 +44,7 @@ const Sidebar = ({ onClose, showCloseIcon }) => {
const [childRoute, setchildRoute] = useState("");
const [combinedRoute, setcombinedRoute] = useState("");
const dispatch = useDispatch();
const clinicStatus = useSelector(selectClinicStatus);
// Assuming companyStatus is fetched or defined elsewhere correctly
const companyStatus = "APPROVED"; // Example status, replace with actual logic if needed
@ -83,29 +87,25 @@ const Sidebar = ({ onClose, showCloseIcon }) => {
const userRole = useSelector((state) => state.userRole.role);
// Function to determine visibility and render a single sidebar item link
// The checkVisibility function with fixed logic
const checkVisibility = (item, i) => {
// Check if the user has the necessary role for this item
const hasRole = !item.roles || item.roles.includes(userRole);
// Determine if the feature related to this item should be hidden
let hideFeature =
(companyStatus === CLINIC_STATUS.APPROVED &&
HIDE_FUNCTIONALITY &&
HIDE_MODULES.includes(item?.path)) ||
(isSuperAdmin && HIDE_FUNCTIONALITY && HIDE_MODULES.includes(item?.path));
// Only render if user has the required role
if (hasRole) {
// Determine if the link should be disabled
const isDisabled =
(!isSuperAdmin && companyStatus !== CLINIC_STATUS.APPROVED) || hideFeature;
// FIXED LOGIC: If clinic status is rejected, only allow "/" and "/docs" paths
const isDisabled = (clinicStatus === "rejected" || clinicStatus === "under_review" || clinicStatus === "payment_due") && !(item.path == "" || item.path == "docs");
// Set the correct target path
const targetPath = isDisabled ? "#" : `/${item.path}`;
const isActive = activeLink === item.path; // Check if this link is the active one
return (
<Box
className={classes.marginBottom}
onClick={() => handleHideFeatures(hideFeature)} // This seems odd, clicking the box hides features? Review this logic.
onClick={() => handleHideFeatures(isDisabled)} // Pass isDisabled to handleHideFeatures
key={item.path || i} // Use a stable key like item.path
>
<NavLink
@ -113,7 +113,7 @@ const Sidebar = ({ onClose, showCloseIcon }) => {
classes.textDecorationNone
} ${isDisabled ? classes.disabledLink : ""}`}
to={targetPath}
// Prevent navigation for disabled links, although `to="#"` handles this
// Prevent navigation for disabled links
onClick={(e) => {
if (isDisabled) e.preventDefault();
handleLinkClick(item.path); // Set active link on click
@ -164,7 +164,8 @@ const Sidebar = ({ onClose, showCloseIcon }) => {
const visibleChildren = item.children
?.filter((childItem) => {
// Check if child has the necessary role
const hasChildRole = !childItem.roles || childItem.roles.includes(userRole);
const hasChildRole =
!childItem.roles || childItem.roles.includes(userRole);
return hasChildRole;
})
.filter(Boolean); // Filter out any null/undefined results

View File

@ -82,4 +82,13 @@ export const SIDEBAR_CONFIG = [
// Clinic admin can access call transcripts
roles: [USER_ROLES.CLINIC_ADMIN]
},
{
text: 'Clinic Documents',
path: 'docs',
requireSaprateApp: false,
icon: ArticleOutlinedIcon,
activeIcon: ArticleIcon,
// Clinic admin can access call transcripts
roles: [USER_ROLES.CLINIC_ADMIN]
},
];

View File

@ -1,19 +1,22 @@
// Initial state
const initialState = {
role: null,
clinicStatus: null,
};
// Action types
const SET_USER_ROLE = 'userRole/SET_USER_ROLE';
// Action creators
export const setUserRole = (role) => ({
export const setUserRole = (role, clinicStatus) => ({
type: SET_USER_ROLE,
payload: role,
clinicStatus: clinicStatus,
});
// Selector to get user role
export const selectUserRole = (state) => state.userRole.role;
export const selectClinicStatus = (state) => state.userRole.clinicStatus;
// Reducer
export default function userRoleReducer(state = initialState, action) {
@ -22,6 +25,7 @@ export default function userRoleReducer(state = initialState, action) {
return {
...state,
role: action.payload,
clinicStatus: action.clinicStatus,
};
default:
return state;

View File

@ -4,7 +4,7 @@ import ClinicsList from "../views/ClinicsList";
import Dashboard from "../views/Dashboard";
import StaffManagement from "../views/StaffManagement";
import ClinicDetails from "../views/ClinicDetails";
import Login from '../views/Login';
import Login from "../views/Login";
import YourDetailsForm from "../views/Signup/YourDetailsForm";
import MockPayment from "../views/MockPayment";
import Users from "../views/User";
@ -13,6 +13,7 @@ import ClinicTranscripts from "../views/ClinicTranscripts";
import ContractManagement from "../views/ContractManagement";
import MasterDataManagement from "../views/MasterData";
import PaymentManagement from "../views/PaymentManagement";
import ClinicDocUpdater from "../views/ClinicDocUpdate";
export const routesData = [
{
@ -28,6 +29,7 @@ export const routesData = [
{ path: "/transcripts", component: ClinicTranscripts },
{ path: "/masterData", component: MasterDataManagement },
{ path: "/payment-management", component: PaymentManagement },
{ path: "/docs", component: ClinicDocUpdater },
],
isProtected: true,
},
@ -38,7 +40,7 @@ export const routesData = [
{ path: "/auth/login", component: Login },
{ path: "/auth/signup/payment", component: MockPayment },
{
path: 'signup/your-details',
path: "signup/your-details",
component: YourDetailsForm,
},
],

View File

@ -0,0 +1,197 @@
import { Button, Grid, Paper, Typography } from "@mui/material";
import { Box } from "@mui/system";
import React, { useEffect, useState } from "react";
import CustomFileUpload from "../../components/CustomFileUpload";
import { useFormik } from "formik";
import * as Yup from "yup";
import { MAX_FILE_SIZE_IN_MB, MAX_FILES } from "../../constants";
import { updateClinicDocs } from "../../services/clinics.service";
import { useNavigate } from "react-router-dom";
const ClinicDocUpdater = () => {
const navigate = useNavigate();
// Use state instead of ref for initial form data
const [initialFormData, setInitialFormData] = useState({
companyABNImage: "",
contract: "",
logo: "",
clinicId: "",
});
// Track if data is loaded
const [isDataLoaded, setIsDataLoaded] = useState(false);
useEffect(() => {
// Load user data from localStorage
try {
const user = JSON.parse(localStorage.getItem("user"));
if (user && user.created_clinics ) {
setInitialFormData({
companyABNImage: user.created_clinics.abn_doc || "",
contract: user.created_clinics.contract_doc || "",
logo: user.created_clinics.logo || "",
clinicId: user.created_clinics.id || "",
});
}
} catch (error) {
console.error("Error loading user data:", error);
} finally {
setIsDataLoaded(true);
}
}, []);
const validationSchema = Yup.object().shape({
companyABNImage: Yup.string().required("Clinic ABN document is required"),
contract: Yup.string().required("Contract is required"),
logo: Yup.string().required("Logo is required"),
});
const handleFormSubmit = async () => {
console.log("Form values:", formik.values);
const payload = {
abn_doc: formik.values.companyABNImage,
contract_doc: formik.values.contract,
logo: formik.values.logo,
}
const resp = await updateClinicDocs(formik.values.clinicId, payload);
console.log(resp);
// clear localStorage
localStorage.removeItem("user");
// redirect to login
navigate("/");
};
const handleSubmit = async () => {
formik.handleSubmit();
};
// Initialize formik only after data is loaded
const formik = useFormik({
initialValues: initialFormData,
validationSchema,
onSubmit: handleFormSubmit,
enableReinitialize: true, // This is key - it will update form values when initialValues change
});
// Set uploaded file URL
const setUploadedFileUrl = (documentName, fileUrl) => {
console.log("Document Name:", documentName);
console.log("File URL:", fileUrl);
console.log("File URL Type:", typeof fileUrl);
if (documentName && fileUrl !== undefined) {
formik.setFieldValue(documentName, fileUrl);
console.log("After setting value:", formik.values[documentName]);
} else {
console.error("Invalid parameters for setUploadedFileUrl:", {
documentName,
fileUrl,
});
}
};
return (
<Box>
<Box sx={{ width: "100%" }}>
<Paper sx={{ width: "100%", mb: 2 }}>
<Box
sx={{
p: 2,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography variant="h6" component="div">
Clinic Docs
</Typography>
</Box>
{isDataLoaded ? (
<form onSubmit={formik.handleSubmit}>
<Box
sx={{
p: 2,
display: "flex",
gap: 2,
flexWrap: "wrap",
alignItems: "center",
}}
>
<Grid
container
spacing={5}
// padding={`0 ${theme.spacing(3.2)}`}
// className={classes.formRoot}
// sx={{ display: "flex" }}
>
<Grid item md={4} sm={12} xs={12}>
<CustomFileUpload
label="Add ABN/ACN Image*"
documentName="companyABNImage"
onUploadDone={setUploadedFileUrl}
maxFileSizeInMb={MAX_FILE_SIZE_IN_MB}
maxFiles={MAX_FILES}
uploadedFileUrl={formik.values.companyABNImage}
errorMessage={
formik.errors.companyABNImage &&
formik.touched.companyABNImage
? formik.errors.companyABNImage
: ""
}
/>
</Grid>
<Grid item md={4} sm={12} xs={12}>
<CustomFileUpload
label="Add Contract*"
documentName="contract"
onUploadDone={setUploadedFileUrl}
maxFileSizeInMb={MAX_FILE_SIZE_IN_MB}
maxFiles={MAX_FILES}
uploadedFileUrl={formik.values.contract}
errorMessage={
formik.errors.contract && formik.touched.contract
? formik.errors.contract
: ""
}
/>
</Grid>
<Grid item md={4} sm={12} xs={12}>
<CustomFileUpload
label="Add Logo*"
documentName="logo"
onUploadDone={setUploadedFileUrl}
maxFileSizeInMb={MAX_FILE_SIZE_IN_MB}
maxFiles={MAX_FILES}
uploadedFileUrl={formik.values.logo}
errorMessage={
formik.errors.logo && formik.touched.logo
? formik.errors.logo
: ""
}
/>
</Grid>
</Grid>
<Button
onClick={handleSubmit}
variant="contained"
color="primary"
>
Submit
</Button>
</Box>
</form>
) : (
<Box sx={{ p: 2 }}>
<Typography>Loading clinic documents...</Typography>
</Box>
)}
</Paper>
</Box>
</Box>
);
};
export default ClinicDocUpdater;

View File

@ -0,0 +1,30 @@
export const convertToSignupForm = (data) => {
return {
name: data.name,
email: data.email,
password: data.password,
mobileNumber: data.mobileNumber,
mobilePrefix: data.mobilePrefix,
companyName: data.companyName,
designation: data.designation,
businessPhonePrefix: data.businessPhonePrefix,
businessPhone: data.businessPhone,
emergencyBusinessPhone: data.emergencyBusinessPhone,
emergencyBusinessPhonePrefix: data.emergencyBusinessPhonePrefix,
businessFax: data.businessFax,
clinicLogo: data.clinicLogo,
businessEmail: data.businessEmail,
pincode: data.pincode,
state: data.state,
locality: data.locality,
country: data.country,
fullAddress: data.fullAddress,
companyABNImageNumber: data.companyABNImageNumber,
companyABNImage: data.companyABNImage,
termsAccepted: data.termsAccepted,
practiceManagementSystem: data.practiceManagementSystem,
practiceId: data.practiceId,
practiceName: data.practiceName,
contract: data.contract,
};
};

View File

@ -7,7 +7,12 @@ import SuperAdmin from "./components/SuperAdmin";
import { selectUserRole, setUserRole } from "../../redux/userRoleSlice";
import Totals from "./Tiles/Totals";
import { useStyles } from "./dashboardStyles";
import { USER_ROLES } from "../../constants";
import { NOTIFICATION, USER_ROLES } from "../../constants";
import { getClinicsById } from "../../services/clinics.service";
import { pushNotification } from "../../utils/notification";
import { useNavigate } from "react-router-dom";
import { updateFormDetails } from "../Signup/signupAction";
import { convertToSignupForm } from "./helper";
function Dashboard() {
const classes = useStyles();
@ -31,7 +36,51 @@ function Dashboard() {
const userRole = useSelector(selectUserRole);
const user = useSelector((state) => state.login.user);
const navigation = useNavigate();
const getClinic = async () => {
setIsLoading(true);
const resp = await getClinicsById(user.created_clinics[0].id);
// if(resp?.data?.data?.error){
// setIsActive(false);
// pushNotification("Error while fetching clinic data", NOTIFICATION.ERROR);
// navigation("/auth/signup/your-details?status=error");
// }
const status = resp?.data?.data?.clinic?.status;
if(status == "rejected"){
setIsActive(false);
// update user state with latest clinic
localStorage.setItem("user", JSON.stringify({
...user,
created_clinics: resp?.data?.data?.clinic
}))
dispatch(setUserRole(USER_ROLES.CLINIC_ADMIN, "rejected"));
pushNotification(resp?.data?.data?.fileStatus?.reason || "Your clinic registration has been rejected", NOTIFICATION.WARNING);
navigation("/docs");
}
// if(status=="payment_due"){
// setIsActive(false);
// pushNotification("Payment due", NOTIFICATION.WARNING);
// }
// if(status=="requested_doc"){
// setIsActive(false);
// pushNotification("Documents requested", NOTIFICATION.WARNING);
// }
setIsLoading(false);
};
useEffect(() => {
getClinic();
// Determine user role based on user data from login reducer
if (user) {
// Check if user is a super admin
@ -44,9 +93,9 @@ function Dashboard() {
setIsActive(clinicStatus == "active");
if (isSuperAdmin) {
dispatch(setUserRole(USER_ROLES.SUPER_ADMIN));
dispatch(setUserRole(USER_ROLES.SUPER_ADMIN, clinicStatus));
} else {
dispatch(setUserRole(USER_ROLES.CLINIC_ADMIN));
dispatch(setUserRole(USER_ROLES.CLINIC_ADMIN, clinicStatus));
}
}
setIsLoading(false);

View File

@ -94,6 +94,11 @@ function YourDetailsForm() {
const selectedLocalityRef = useRef();
const [testConnection, setTestConnDone] = useState(false);
const errorStatus = new URLSearchParams(window.location.search).get("status");
console.log(errorStatus)
useEffect(() => {
setIsLoading(true);
getLatestClinicId().then((res) => {
@ -209,6 +214,7 @@ function YourDetailsForm() {
if (yourDetailsFormData) {
defaultFormData.current = { ...yourDetailsFormData };
console.log(yourDetailsFormData)
// setLocalityOption([yourDetailsFormData?.locality])
selectedLocalityRef.current = yourDetailsFormData?.locality;
}