health-apps-cms/src/views/ClinicSetup/index.jsx

1143 lines
36 KiB
JavaScript

import SendIcon from "@mui/icons-material/Send";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import FormControl from "@mui/material/FormControl";
import Grid from "@mui/material/Grid";
import InputLabel from "@mui/material/InputLabel";
import * as React from "react";
import { pushNotification } from "../../utils/notification";
import { NOTIFICATION_TYPE } from "../Notifications/notificationConstant";
import {
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
InputAdornment,
MenuItem,
Paper,
Select,
Step,
StepLabel,
Stepper,
TextField,
Typography,
} from "@mui/material";
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
import {
CLINIC_GREETINGS_LENGTH,
CLINIC_SCENARIOS_LENGTH,
NOTIFICATION,
} from "../../constants";
import { useSelector } from "react-redux";
import { getClinicsById, updateClinic } from "../../services/clinics.service";
import { useStyles } from "./clinicSetupStyles";
import { useNavigate } from "react-router-dom";
// Integration software options
const integrationOptions = [
{
id: "bp",
name: "BP Software",
},
{
id: "medicaldirector",
name: "Medical Director",
},
];
// Steps for the stepper
const steps = [
"General Information",
"AI Receptionist Setup",
"Scenarios Setup",
"General Info Setup",
"Integration Settings",
];
const voiceModels = [
{ id: "stream", name: "Stream", gender: "female" },
{ id: "sandy", name: "Sandy", gender: "female" },
{ id: "breeze", name: "Breeze", gender: "female" },
{ id: "wolf", name: "Wolf", gender: "male" },
{ id: "stan", name: "Sten", gender: "male" },
{ id: "blaze", name: "Blaze", gender: "male" },
];
const voiceModelGender = [
{ id: "male", name: "Male" },
{ id: "female", name: "Female" },
];
export default function ClinicSetup() {
const classes = useStyles();
// Active step state
const [activeStep, setActiveStep] = React.useState(0);
const [openDialog, setOpenDialog] = React.useState(false);
const [audioContext, setAudioContext] = React.useState(null);
const [testConnDone, setTestConnDone] = React.useState(true);
const [confirmPhoneNumber, setConfirmPhoneNumber] = React.useState(true);
const [audioListenDone, setAudioListenDone] = React.useState(true);
const navigate = useNavigate();
const user = useSelector((state) => state.login.user);
const clinic = user?.created_clinics[0];
if (!clinic) {
pushNotification("Clinic not found!", NOTIFICATION.ERROR);
return;
}
// Completed steps state
const [completed, setCompleted] = React.useState({});
// Form state
const [formData, setFormData] = React.useState({
// General Information
clinicId: clinic?.id,
clinicPhone: "",
clinicPonePrefix: "+61",
clinicAddress: "1 Wilkinson Road, Para Hills SA 5096",
otherInfo: "",
// Greetings Setup
clinicGreetings:
"Welcome to 365 days medical center group, para hills clinic. We are here to help you.",
clinicScenarios: `Booking for care plan /Care plan Review
Existing patients- Book with regular GP and Nurse on same day ( if not possible book different day, F2F with nurse for care plan and another days F2F consultation with Dr to sign of care plan )
New patients- At the moment we are not taking New patients on long term care but tell them they can see GP and can discuss with GP before booking for care plan .
Need to tell thee is not gap when booking for care plan and care plan review
Booking for MHCP/MHCP review
Do you have a Medicare card?
If No -Pt who do not have a Medicare card / Private patients with or without Insurance can have MHCP/MHCP review but have to pay if full and need to check with their insurance if they get back any amount. Need to tell Dressing fee ............but need toTell them to Visit the Patient Info tab at 365daysmedicalcentre.com.au to view our billing policy
`,
clinicOthers: `# Wellness Way Medical Centre
## Contact Information
**Address**: 83 Kesters Road, Para Hills, SA 5096, Australia
**Website**: www.wellnesswaymedical.com.au
## Accessibility Information
**Parking Facility**: On-site parking available with 20 general spaces and 4 dedicated accessible parking spots. First 2 hours free with validation, $3/hour thereafter.
**Wheelchair Accessibility**: Fully wheelchair accessible facility with ramps at all entrances, automatic doors, accessible toilets on each floor, and elevators to all levels. Wheelchairs available for patient use upon request at reception.
**Baby Care Facilities**: Dedicated parents' room located on Level 1 next to the pediatric wing, equipped with:
- Private feeding area with comfortable chairs
- Changing tables
- Bottle warming facilities
- Microwave
- Small play area for siblings
## Transportation
**Nearby Public Transport**:
- Bus: Routes 208, 209, and 580 stop directly outside (Para Hills Shopping Centre stop)
- Train: Parafield Station (3.2km away) with connecting buses
- O-Bahn Busway: Klemzig Interchange (7km) with connecting services
**Taxi/Rideshare Information**:
- Dedicated pickup/dropoff zone at main entrance
- Rideshare pickup point clearly marked with signage
- Reception can call taxis for patients upon request
## Hours of Operation
**Regular Hours**:
- Monday to Friday: 7:00 AM - 7:00 PM
- Saturday: 8:00 AM - 4:00 PM
- Sunday: 9:00 AM - 2:00 PM
**Holiday Schedule**:
- New Year's Day: 10:00 AM - 2:00 PM
- Australia Day: 10:00 AM - 2:00 PM
- Good Friday: Closed
- Easter Monday: 10:00 AM - 2:00 PM
- Anzac Day: 1:00 PM - 5:00 PM
- Queen's Birthday: 10:00 AM - 2:00 PM
- Labour Day: 10:00 AM - 2:00 PM
- Christmas Eve: 8:00 AM - 1:00 PM
- Christmas Day: Closed
- Boxing Day: 10:00 AM - 2:00 PM
- New Year's Eve: 8:00 AM - 1:00 PM
## Amenities
**Wi-Fi Availability**:
- Free Wi-Fi available throughout the facility
- Ask front desk for Wifi details
- Usage limit: 2GB per device per day
## Policies
**Pet Policy**:
- Service animals welcome throughout the facility
- Therapy animals permitted with prior arrangement
- Pet-friendly outdoor waiting area in courtyard garden
- Other pets must remain outside the building except for veterinary consultations (available Tuesdays and Thursdays)`,
// Integration Settings
integrationSoftware: "",
practiceId: "",
practiceName: "",
// Voice Configuration
voice: "",
voiceGender: voiceModelGender[1].id,
voice_model_provider: "rime",
});
const [prevFormData, setPrevFormData] = React.useState({
...formData,
});
const mobilePrefixOptions = [
{ id: "61", name: "+61" },
{ id: "91", name: "+91" },
];
const parsePhoneNumber = (phoneString) => {
// Split by space
const parts = phoneString.split(" ");
if (parts.length !== 2) {
return {
countryCode: null,
number: null,
error: "Phone number must contain exactly one space",
};
}
const countryCode = parts[0].replace("+", ""); // Everything before space (includes +)
const number = parts[1]; // Everything after space
return {
countryCode,
number,
};
};
const getClinic = async () => {
const resp = await getClinicsById(clinic.id);
if (resp?.data?.error) {
pushNotification(resp?.data?.message, NOTIFICATION.ERROR);
return;
}
const clinicResp = resp?.data?.data?.clinic;
const { countryCode, number } = parsePhoneNumber(clinicResp?.phone);
setFormData({
...formData,
clinicPhone: number,
clinicPonePrefix: countryCode,
clinicAddress: clinicResp?.address,
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,
voice: clinicResp?.voice_model ?? formData.voice,
voiceGender: clinicResp?.voice_model_gender ?? formData.voiceGender,
voice_model_provider: clinicResp?.voice_model_provider,
});
setPrevFormData({
...formData,
});
};
React.useEffect(() => {
getClinic();
}, []);
// Handle form reset
const handleReset = () => {
setActiveStep(0);
setCompleted({});
// reset form data to initial state
setFormData(
Object.keys(formData).reduce((acc, key) => {
acc[key] = "";
return acc;
}, {})
);
};
const handleConfirmPhoneNumber = () => {
setConfirmPhoneNumber(true);
};
React.useEffect(() => {
// Initialize audio context when component mounts
function initAudioContext() {
if (!window.AudioContext && window.webkitAudioContext) {
window.AudioContext = window.webkitAudioContext;
}
if (!window.AudioContext) {
pushNotification(
"Your browser does not support AudioContext and cannot play back audio.",
"error"
);
return null;
}
return new AudioContext();
}
const context = initAudioContext();
setAudioContext(context);
// initialize voice model
formData.voiceGender = voiceModelGender[0].id;
// Cleanup function
return () => {
if (context && context.state !== "closed") {
context.close();
}
};
}, []);
// Calculate total steps
const totalSteps = () => steps.length;
// Calculate completed steps
const completedSteps = () => Object.keys(completed).length;
// Check if all steps completed
const allStepsCompleted = () => completedSteps() === totalSteps();
// Check if current step is the last step
const isLastStep = () => activeStep === totalSteps() - 1;
// Handle next button click
const handleNext = () => {
if (
prevFormData.clinicPhone !== formData.clinicPhone &&
!confirmPhoneNumber
) {
pushNotification("Please confirm phone number", "error");
return;
}
if (!confirmPhoneNumber) {
pushNotification("Please confirm phone number", "error");
return;
}
if (activeStep == 1) {
if (!audioListenDone) {
pushNotification(
"Please listen to the audio before proceeding",
"error"
);
return;
}
}
// Only move to next step without marking as completed
if (!isLastStep()) {
setActiveStep(activeStep + 1);
} else if (!allStepsCompleted()) {
// If it's the last step and not all steps are completed,
// find the first uncompleted step
const firstUncompletedStep = steps.findIndex((step, i) => !(i in completed));
setActiveStep(firstUncompletedStep);
}
};
// Listen to voice using rime.ai API
const listenVoice = () => {
if (!audioContext) {
pushNotification("Audio context not initialized", "error");
return;
}
if (!formData.clinicGreetings || formData.clinicGreetings.length === 0) {
pushNotification(
"Please enter clinic greetings to listen preview",
"error"
);
return;
}
const options = {
method: "POST",
headers: {
Accept: "audio/mp3",
Authorization: "Bearer OFhm3M8ZJKhooRPbm_eoQoUAVDmWGO_9SwdU7mXuvYU",
"Content-Type": "application/json",
},
body: JSON.stringify({
speaker: formData.voice || "wolf",
text: formData.clinicGreetings,
modelId: "mist",
lang: "eng",
samplingRate: 22050,
speedAlpha: 1.0,
reduceLatency: false,
}),
};
// Show loading state
pushNotification("Generating audio...", "info");
fetch("https://users.rime.ai/v1/rime-tts", options)
.then((response) => {
// Check if the response is successful
if (!response.ok) {
throw new Error(`API responded with status ${response.status}`);
}
// Get the response as an ArrayBuffer instead of text
return response.arrayBuffer();
})
.then((arrayBuffer) => {
// Decode the audio data
audioContext.decodeAudioData(
arrayBuffer,
(buffer) => playAudioBuffer(buffer, audioContext),
(error) => {
pushNotification("Failed to decode audio data", "error");
}
);
})
.then(() => {
setAudioListenDone(true);
})
.catch((err) => {
pushNotification(`Error: ${err.message}`, "error");
});
};
// Play decoded audio buffer
function playAudioBuffer(audioBuffer, context) {
// Create a source node
const source = context.createBufferSource();
source.buffer = audioBuffer;
// Connect to the audio context destination (speakers)
source.connect(context.destination);
// Play the audio
source.start(0);
// Notify user
pushNotification("Playing audio...", "success");
}
// Handle back button click
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
// Handle step click
const handleStep = (step) => () => {
// First check if phone number is confirmed for any step navigation
if (!confirmPhoneNumber) {
pushNotification(
"Please confirm phone number before navigating between steps",
"error"
);
return;
}
// if voice model or voice gender is changed then must listen first
if (activeStep == 1) {
if (!audioListenDone) {
pushNotification(
"Please listen to the audio before proceeding",
"error"
);
return;
}
}
// Then check if the step is completed
if (completed[step]) {
setActiveStep(step);
} else {
// Alert user about validation errors
pushNotification(
"Please complete the current step before proceeding",
"error"
);
}
};
// test connection
const handleTestConnection = () => {
// logic here
setTestConnDone(true);
pushNotification("Test connection successful", "success");
};
// Form validation state
const [errors, setErrors] = React.useState({
clinicPhone: false,
clinicAddress: false,
});
// Validate the current step
const validateStep = () => {
switch (activeStep) {
case 0: // General Information
// eslint-disable-next-line no-case-declarations
// const stepErrors = {
// clinicPhone: formData.clinicPhone.trim() === '',
// clinicAddress: formData.clinicAddress.trim() === '',
// };
// setErrors(stepErrors);
// return !Object.values(stepErrors).some((isError) => isError);
return true;
case 1: // Clinic Timings
// No required fields in this step
return true;
case 2: // Greetings Setup
// No required fields in this step
return true;
case 3: // Integration Settings
// Validate only if software is selected
if (formData.integrationSoftware) {
const integrationErrors = {
practiceId: formData.practiceId.trim() === "",
practiceName: formData.practiceName.trim() === "",
};
setErrors(integrationErrors);
return !Object.values(integrationErrors).some((isError) => isError);
}
return true;
default:
return true;
}
};
// Handle complete step
const handleComplete = () => {
if (activeStep == 4 && !testConnDone) {
pushNotification("Please test connection before proceeding", NOTIFICATION.ERROR);
return;
}
// Mark current step as completed
const newCompleted = { ...completed };
newCompleted[activeStep] = true;
setCompleted(newCompleted);
// Move to next step or first uncompleted step
if (!isLastStep()) {
setActiveStep(activeStep + 1);
} else if (!allStepsCompleted()) {
const firstUncompletedStep = steps.findIndex((step, i) => !(i in newCompleted));
setActiveStep(firstUncompletedStep);
} else {
// All steps completed
setActiveStep(steps.length);
}
};
// Handle simple input changes
const handleChange = (e) => {
const { name, value } = e.target;
if (name === "clinicPhone" && value != formData.clinicPhone) {
setConfirmPhoneNumber(false);
}
if (name === "voice" || name === "voiceGender") {
setAudioListenDone(false);
}
if (name === "integrationSoftware" || name === "practiceId" || name === "practiceName") {
setTestConnDone(false);
}
setFormData((prev) => ({
...prev,
[name]: value,
}));
// Clear error for this field if it exists
if (errors[name]) {
setErrors((prev) => ({
...prev,
[name]: false,
}));
}
};
// Handle timing changes
const handleTimingChange = (timingType, day, timeType, value) => {
setFormData((prev) => ({
...prev,
[timingType]: {
...prev[timingType],
[day]: {
...prev[timingType][day],
[timeType]: value,
},
},
}));
};
// Validate the entire form
const validateForm = () => {
// Check all steps
const generalInfoValid =
formData.clinicPhone.trim() !== "" &&
formData.clinicAddress.trim() !== "";
let integrationValid = true;
if (formData.integrationSoftware) {
integrationValid =
formData.practiceId.trim() !== "" &&
formData.practiceName.trim() !== "";
}
return generalInfoValid && integrationValid;
};
// Handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
const payload = {
clinicId: formData.clinicId,
phone: `${formData.clinicPonePrefix} ${formData.clinicPhone}`,
address: formData.clinicAddress,
other_info: formData.otherInfo,
general_info: formData.clinicOthers,
greeting_msg: formData.clinicGreetings,
scenarios: formData.clinicScenarios,
integration: formData.integrationSoftware,
pms_id: formData.practiceId,
practice_name: formData.practiceName,
voice_model: formData.voice,
voice_model_gender: formData.voiceGender,
voice_model_provider:"rime"
};
if (validateForm()) {
const resp = await updateClinic(formData.clinicId, payload);
if(resp?.data?.error){
pushNotification(resp?.data?.message, NOTIFICATION.ERROR);
return;
}
// Here you would typically send the data to your backend
pushNotification("Clinic setup updated successfully", "success");
// Reset the form state manually
setActiveStep(0);
setCompleted({});
setConfirmPhoneNumber(true);
setTestConnDone(true);
setAudioListenDone(true);
// Refetch the clinic data to get the latest state
await getClinic();
}
};
// Render the current step content
const renderStepContent = (step) => {
switch (step) {
case 0: // General Information
return (
<Grid container spacing={3}>
<Grid
item
xs={12}
md={10}
alignItems="baseline"
flexDirection="row"
display="flex"
>
<TextField
className={classes.mobileNumberInput}
type="string"
style={{ width: "40%" }}
name="clinicPhone"
value={formData.clinicPhone}
onChange={(e) => {
if (e.target.value.length <= 10) {
const value = e.target.value?.match(/\d+/g) || "";
handleChange(e);
}
}}
helperText={
errors.clinicPhone
? "Phone number is required"
: "Unique single number for your clinic"
}
variant="outlined"
error={errors.clinicPhone}
InputProps={{
startAdornment: (
<Box sx={{ padding: "1px" }}>
<InputAdornment position="start">
<Select
name="clinicPonePrefix"
value={formData.clinicPonePrefix}
onChange={(e) => {
setFormData({
...formData,
clinicPonePrefix: e.target.value,
});
}}
>
{mobilePrefixOptions.map((option) => (
<MenuItem
sx={{
outline: "none",
border: "none",
}}
key={option.id}
value={option.id}
>
{option.name}
</MenuItem>
))}
</Select>
</InputAdornment>
</Box>
),
}}
/>
<Button
style={{ marginLeft: "10px" }}
variant="contained"
color="primary"
startIcon={<SendIcon />}
onClick={handleConfirmPhoneNumber}
>
Confirm
</Button>
<>
<Button
style={{ marginLeft: "10px" }}
variant="outlined"
color="error"
startIcon={<WarningAmberIcon />}
onClick={() => setOpenDialog(true)}
>
Disable 24x7 AI Receptionist
</Button>
<Dialog
open={openDialog}
onClose={() => setOpenDialog(false)}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Confirm Disable AI Receptionist"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Warning: This action will disable the 24x7 AI Receptionist
service for your clinic. This may affect your clinic's
ability to handle after-hours calls and appointments. You
have to manually disable the call forwarding from this
number. Are you sure you want to proceed?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => setOpenDialog(false)}
color="primary"
>
Cancel
</Button>
<Button
onClick={() => {
pushNotification(
"AI Receptionist disabled successfully",
NOTIFICATION_TYPE.SUCCESS
);
setOpenDialog(false);
}}
color="error"
autoFocus
>
Yes, Disable AI Receptionist
</Button>
</DialogActions>
</Dialog>
</>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Clinic Address"
name="clinicAddress"
value={formData.clinicAddress}
onChange={handleChange}
multiline
rows={3}
variant="outlined"
error={errors.clinicAddress}
helperText={
errors.clinicAddress ? "Clinic address is required" : ""
}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Other Information"
name="otherInfo"
value={formData.otherInfo}
onChange={handleChange}
multiline
rows={2}
variant="outlined"
/>
</Grid>
</Grid>
);
case 1: // Greetings Setup
return (
<>
<TextField
fullWidth
label="Clinic Greetings"
name="clinicGreetings"
value={formData.clinicGreetings}
onChange={handleChange}
multiline
rows={3}
variant="outlined"
helperText={`${formData.clinicGreetings.length}/${CLINIC_GREETINGS_LENGTH} characters`}
inputProps={{ maxLength: CLINIC_GREETINGS_LENGTH }}
/>
<div
style={{
display: "flex",
maxWidth: "40%",
// marginTop: '10px',
justifyContent: "space-between",
}}
>
<Button
onClick={listenVoice}
variant="contained"
endIcon={<SendIcon />}
>
Listen & Confirm
</Button>
<FormControl>
<Select
id="voice-model-gender"
onChange={handleChange}
value={formData.voiceGender || voiceModelGender[0].id}
displayEmpty
name="voiceGender"
>
{voiceModelGender.map((model) => (
<MenuItem key={model.id} value={model.id}>
{model.name}
</MenuItem>
))}
</Select>
</FormControl>
{formData.voiceGender && (
<FormControl>
<Select
id="voice-model-id"
onChange={handleChange}
value={
formData.voice ||
(formData.voiceGender === "male"
? voiceModels.find((model) => model.gender === "male")
.id
: voiceModels.find((model) => model.gender === "female")
.id)
}
displayEmpty
name="voice"
>
{voiceModels
.filter((model) => model.gender === formData.voiceGender)
.map((model) => (
<MenuItem key={model.id} value={model.id}>
{model.name}
</MenuItem>
))}
</Select>
</FormControl>
)}
</div>
</>
);
case 2:
return (
<>
<TextField
style={{ marginTop: "10px" }}
fullWidth
label="Scenarios"
name="clinicScenarios"
value={formData.clinicScenarios}
onChange={handleChange}
multiline
rows={8}
helperText={`${formData.clinicScenarios.length}/${CLINIC_SCENARIOS_LENGTH} characters`}
variant="outlined"
inputProps={{ maxLength: CLINIC_SCENARIOS_LENGTH }}
/>
</>
);
case 3:
return (
<>
<TextField
style={{ marginTop: "10px" }}
fullWidth
label="General Info"
name="clinicOthers"
value={formData.clinicOthers}
onChange={handleChange}
multiline
rows={8}
variant="outlined"
helperText={`${formData.clinicOthers.length}/3000 characters`}
inputProps={{ maxLength: 3000 }}
/>
</>
);
case 4: // Integration Settings
return (
<Grid container spacing={3}>
{/* Error summary - show only if there are errors */}
{/* {Object.values(errors).some((error) => error) && (
<Grid item xs={12}>
<Box
sx={{
backgroundColor: '#fdeded',
color: '#5f2120',
p: 2,
borderRadius: 1,
mb: 2,
}}
>
<Typography variant="subtitle1" sx={{ fontWeight: 'medium' }}>
Please correct the following errors:
</Typography>
<ul style={{ margin: '8px 0', paddingLeft: '24px' }}>
{errors.practiceId && (
<li>
Practice ID is required when integration software is
selected
</li>
)}
{errors.practiceName && (
<li>
Practice Name is required when integration software is
selected
</li>
)}
</ul>
</Box>
</Grid>
)} */}
<Grid item xs={12} md={6}>
<FormControl fullWidth variant="outlined">
<InputLabel id="integration-software-label">
Integrate with
</InputLabel>
<Select
labelId="integration-software-label"
label="Integrate with"
name="integrationSoftware"
value={formData.integrationSoftware}
onChange={handleChange}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{integrationOptions.map((option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="PMS ID"
name="practiceId"
value={formData.practiceId}
onChange={handleChange}
variant="outlined"
disabled={!formData.integrationSoftware}
error={!!formData.integrationSoftware && errors.practiceId}
helperText={
!!formData.integrationSoftware && errors.practiceId
? "Practice ID is required"
: ""
}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Practice Name"
name="practiceName"
value={formData.practiceName}
onChange={handleChange}
variant="outlined"
disabled={!formData.integrationSoftware}
error={!!formData.integrationSoftware && errors.practiceName}
helperText={
!!formData.integrationSoftware && errors.practiceName
? "Practice Name is required"
: ""
}
/>
</Grid>
</Grid>
);
default:
return "Unknown step";
}
};
return (
<Box component="form" onSubmit={handleSubmit} sx={{ width: "100%" }}>
<Paper elevation={0} sx={{ p: 2 }}>
<Typography
variant="h4"
component="h1"
sx={{ fontWeight: "bold", mb: 3 }}
>
Clinic Setup
</Typography>
{/* Stepper */}
<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 = {};
// Determine if this step is clickable
const isClickable = confirmPhoneNumber && (completed[index] || index < activeStep);
// Only show as completed if explicitly marked as completed
const isStepCompleted = completed[index] === true;
return (
<Step key={label} {...stepProps} completed={isStepCompleted}>
<StepLabel
{...labelProps}
onClick={isClickable ? handleStep(index) : undefined}
sx={{
cursor: isClickable ? "pointer" : "not-allowed",
opacity: isClickable ? 1 : 0.7,
"& .MuiStepLabel-label": {
color: isClickable ? "text.primary" : "text.disabled",
},
}}
>
{label}
</StepLabel>
</Step>
);
})}
</Stepper>
{/* Step Content */}
<Box sx={{ mt: 2, mb: 2, minHeight: "300px" }}>
{allStepsCompleted() ? (
<React.Fragment>
<Typography sx={{ mt: 2, mb: 1 }}>
All steps completed - Setup is ready to be saved.
</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">
Back
</Button> */}
<Button onClick={handleSubmit} variant="contained" color="primary" type="submit" disabled={activeStep == 4 && !testConnDone}>
Submit Setup
</Button>
</Box>
</React.Fragment>
) : (
<React.Fragment>
{renderStepContent(activeStep)}
{/* Navigation Buttons */}
<Box sx={{ display: "flex", flexDirection: "row", pt: 2, mt: 4 }}>
<Button
color="inherit"
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
variant="outlined"
>
Back
</Button>
<Box sx={{ flex: "1 1 auto" }} />
{activeStep == 4 && (
<Button
onClick={handleTestConnection}
sx={{ mr: 1 }}
variant="outlined"
>
Test Connection
</Button>
)}
{activeStep != 4 && (
<Button
disabled={activeStep == 4 && !testConnDone}
onClick={handleNext}
sx={{ mr: 1 }}
variant="outlined"
>
Next
</Button>
)}
{activeStep !== steps.length && (
<Button
variant="contained"
color="primary"
onClick={handleComplete}
>
{isLastStep() ? "Finish" : "Complete Step"}
</Button>
)}
</Box>
</React.Fragment>
)}
</Box>
</Paper>
</Box>
);
}