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 ( { 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: ( ), }} /> <> setOpenDialog(false)} aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" > {"Confirm Disable AI Receptionist"} 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? ); case 1: // Greetings Setup return ( <>
{formData.voiceGender && ( )}
); case 2: return ( <> ); case 3: return ( <> ); case 4: // Integration Settings return ( {/* Error summary - show only if there are errors */} {/* {Object.values(errors).some((error) => error) && ( Please correct the following errors:
    {errors.practiceId && (
  • Practice ID is required when integration software is selected
  • )} {errors.practiceName && (
  • Practice Name is required when integration software is selected
  • )}
)} */} Integrate with
); default: return "Unknown step"; } }; return ( Clinic Setup {/* Stepper */} {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 ( {label} ); })} {/* Step Content */} {allStepsCompleted() ? ( All steps completed - Setup is ready to be saved. {/* */} ) : ( {renderStepContent(activeStep)} {/* Navigation Buttons */} {activeStep == 4 && ( )} {activeStep != 4 && ( )} {activeStep !== steps.length && ( )} )} ); }