2025-04-30 17:28:58 +01:00

1599 lines
61 KiB
TypeScript

import {modals} from "@mantine/modals";
import React, {useMemo, useState, useEffect} from "react";
import {
ActionIcon,
Button, Checkbox,
type ComboboxItem, CopyButton,
Group,
NumberInput,
type OptionsFilter, Radio,
Select,
Stack,
Text,
TextInput, Tooltip,
Table,
MultiSelect,
Textarea
} from "@mantine/core";
import {
apiAdminRegister,
apiCreateWard,
apiDeleteWard,
apiEditDoctorInfo,
apiEditWard,
apiResetPasswordFromRoleID,
apiSetDoctorResigned,
apiDeleteTeam,
apiCreateTeam,
apiEditTeam,
apiPatientBooking,
apiEditPatientBooking,
apiDeletePatientBooking,
apiGetTeamList,
apiProcessAppointment,
apiPatientAdmission,
apiGetWardList,
apiEditPatientInfo,
apiPatientDischarge
} from "~/utils/hms_api.ts";
import {parseDmyToDate, showErrorMessage, showInfoMessage, timestampToDate} from "~/utils/utils.ts";
import {
type DoctorDataWithPermission,
type WardInfo,
WardTypes,
type DoctorTeamInfo,
type PatientBookingInfo,
type PatientDataWithWardAndAdmission, type PatientData, type DoctorData
} from "~/utils/models.ts";
import {useForm} from "@mantine/form";
import {iconMStyle} from "~/styles.ts";
import RenameIcon from "mdi-react/RenameIcon";
import HumanEditIcon from "mdi-react/HumanEditIcon";
import WardPatients from "~/components/subs/WardPatients.tsx";
import {DoctorGrade, Departments, BookingCategory} from "~/utils/hms_enums.ts";
import {DateInput, DateTimePicker} from "@mantine/dates";
import CalendarIcon from "mdi-react/CalendarIcon";
import PhoneIcon from "mdi-react/PhoneIcon";
import MailIcon from "mdi-react/MailIcon";
import GenderMaleFemaleIcon from "mdi-react/GenderMaleFemaleIcon";
import AlphaGIcon from "mdi-react/AlphaGIcon";
import HomeIcon from "mdi-react/HomeIcon";
import CheckIcon from "mdi-react/CheckIcon";
import ContentCopyIcon from "mdi-react/ContentCopyIcon";
export function confirmDeleteWard(wardId: number, wardName: String, onFinished: () => void = () => {}) {
const onClickConfirmDeleteWard = (wardId: number) => {
apiDeleteWard(wardId).then(res => {
if (res.success) {
showInfoMessage("", "Ward deleted successfully", 3000)
} else {
showErrorMessage(res.message, "Delete Ward Failed")
}
}).catch(err => {
showErrorMessage(err.toString(), "Delete Ward Failed")
}).finally(() => onFinished())
modals.closeAll()
}
modals.open({
title: "Delete Ward",
centered: true,
children: (
<Stack>
<Text>Are you sure you want to delete the ward: <Text span fw={700}>{wardName}</Text>?</Text>
<Group mt="md" justify="flex-end">
<Button onClick={() => modals.closeAll()}>Cancel</Button>
<Button color="red" onClick={() => onClickConfirmDeleteWard(wardId)}>Confirm</Button>
</Group>
</Stack>
)
})
}
export function confirmEditOrCreateWard(origWardInfo: WardInfo | null, onSucceed: () => void = () => {}) {
function ConfirmEditOrCreateWard({wardInfo}: {wardInfo: WardInfo | null}) {
const form = useForm({
initialValues: {
ward_id: wardInfo ? wardInfo.id : -1,
ward_name: wardInfo ? wardInfo.name : "",
total_capacity: wardInfo ? wardInfo.total_capacity : 0,
type: wardInfo ? wardInfo.type : "GENERAL"
},
validate: {
ward_name: (value) => (value.length == 0 ? "Input ward name." : null),
total_capacity: (value) => (value < 1 ? "Total capacity must be greater than 0." : null),
},
});
const optionsFilter: OptionsFilter = ({ options, search }) => {
const filtered = (options as ComboboxItem[]).filter((option) =>
option.label.toLowerCase().trim().includes(search.toLowerCase().trim())
);
filtered.sort((a, b) => a.label.localeCompare(b.label));
return filtered;
}
const wardTypesOptions = useMemo(() => {
return Object.entries(WardTypes).map(([key, value]) => ({ value: key, label: value }))
}, [WardTypes])
const onClickConfirmEditWard = (values: {ward_id: number, ward_name: string, total_capacity: number, type: string}) => {
if (origWardInfo) {
apiEditWard(values.ward_id, values.ward_name, values.total_capacity, values.type).then(res => {
if (res.success) {
modals.closeAll()
showInfoMessage("", "Ward edited successfully", 3000)
onSucceed()
} else {
showErrorMessage(res.message, "Edit Ward Failed")
}
}).catch(err => {
showErrorMessage(err.toString(), "Edit Ward Failed")
})
} else {
apiCreateWard(values.ward_name, values.total_capacity, values.type).then(res => {
if (res.success) {
modals.closeAll()
showInfoMessage("", "Ward created successfully", 3000)
onSucceed()
} else {
showErrorMessage(res.message, "Create Ward Failed")
}
}).catch(err => {
showErrorMessage(err.toString(), "Create Ward Failed")
})
}
}
return (
<form onSubmit={form.onSubmit((values) => onClickConfirmEditWard(values))}>
<Stack>
<TextInput
withAsterisk
label="Ward Name"
placeholder="Ward name"
leftSection={<RenameIcon style={iconMStyle}/>}
{...form.getInputProps('ward_name')}
/>
<NumberInput
withAsterisk
label="Total Capacity"
placeholder="Total capacity"
hideControls
leftSection={<HumanEditIcon style={iconMStyle}/>}
{...form.getInputProps('total_capacity')}
/>
<Select
label="Ward Type"
placeholder="Select a ward type"
data={wardTypesOptions}
filter={optionsFilter}
searchable
{...form.getInputProps("type")}
onChange={(e) => {
if (!e) e = form.values.type
form.getInputProps("type").onChange(e)
}}
/>
<Group mt="md" justify="flex-end">
<Button variant="outline" onClick={() => modals.closeAll()}>Cancel</Button>
<Button type="submit">Confirm</Button>
</Group>
</Stack>
</form>
)
}
modals.open({
title: origWardInfo ? "Edit Ward" : "Create Ward",
centered: true,
children: (
<ConfirmEditOrCreateWard wardInfo={origWardInfo} />
)
})
}
export function confirmCheckWardPatients(origWardId: number, onChanged: () => void = () => {}) {
modals.open({
title: "Ward Patients",
centered: true,
size: "70%",
children: (
<WardPatients ward_id={origWardId} onChanged={onChanged} />
)
})
}
export function confirmEditDoctor(
origDoctorInfo: DoctorDataWithPermission,
onSucceed: () => void = () => {},
) {
function ConfirmEditOrCreateDoctor({
doctorInfo,
}: {
doctorInfo: DoctorDataWithPermission;
}) {
const form = useForm({
initialValues: {
id: doctorInfo ? doctorInfo.id : -1,
name: doctorInfo ? doctorInfo.name : '',
email: doctorInfo ? doctorInfo.email : '',
phone: doctorInfo ? doctorInfo.phone : '',
gender: doctorInfo ? doctorInfo.gender : 'M',
birth_date: doctorInfo ? parseDmyToDate(doctorInfo.birth_date) : new Date(2000, 1, 1),
title: doctorInfo ? doctorInfo.title : '',
grade: doctorInfo ? doctorInfo.grade.toString() : DoctorGrade.Unknown.toString(),
is_admin: doctorInfo ? doctorInfo.is_admin : false
},
validate: {
name: (v) => (v.length === 0 ? 'Input doctor name.' : null),
email: (v) =>
/^\S+@\S+\.\S+$/.test(v) ? null : 'Incorrect email address.',
phone: (v) =>
v.length === 0 || /^\+?\d{6,15}$/.test(v)
? null
: 'Invalid phone number.',
birth_date: (v) => (v > new Date() ? 'Date of birth cannot be in the future' : null),
gender: (v) => (v.length === 0 ? "Select a gender" : null)
},
});
const genderOptions = useMemo(
() => [
{ value: 'M', label: 'Male' },
{ value: 'F', label: 'Female' },
{ value: 'Intersex', label: 'Intersex' },
],
[],
);
const gradeOptions = useMemo(
() =>
Object.entries(DoctorGrade)
.filter(([k]) => isNaN(Number(k)))
.map(([k, v]) => ({
value: v.toString(),
label: k,
})),
[],
);
const optionsFilter: OptionsFilter = ({ options, search }) => {
const res = (options as ComboboxItem[]).filter((o) =>
o.label.toLowerCase().includes(search.toLowerCase().trim()),
);
res.sort((a, b) => a.label.localeCompare(b.label));
return res;
};
const onClickConfirmEditDoctor = (values: typeof form.values) => {
apiEditDoctorInfo(values).then((res) => {
if (res.success) {
modals.closeAll();
showInfoMessage('', 'Doctor edited successfully', 3000);
onSucceed();
} else showErrorMessage(res.message, 'Edit Doctor Failed');
}).catch((err) =>
showErrorMessage(err.toString(), 'Edit Doctor Failed'),
);
};
return (
<form onSubmit={form.onSubmit(onClickConfirmEditDoctor)}>
<Stack>
<Group grow>
<TextInput
withAsterisk
label="Name"
placeholder="Doctor name"
leftSection={<RenameIcon style={iconMStyle} />}
{...form.getInputProps('name')}
/>
<TextInput
label="Title"
placeholder="e.g. Dr., Prof."
leftSection={<RenameIcon style={iconMStyle} />}
{...form.getInputProps('title')}
/>
</Group>
<Group grow>
<Select
label="Gender"
placeholder="Select gender"
data={genderOptions}
filter={optionsFilter}
leftSection={<GenderMaleFemaleIcon style={iconMStyle} />}
searchable
withAsterisk
{...form.getInputProps('gender')}
onChange={(v) => {
if (!v) v = form.values.gender;
form.getInputProps('gender').onChange(v);
}}
/>
<DateInput
withAsterisk
label="Birth Date"
valueFormat="DD/MM/YYYY"
leftSection={<CalendarIcon style={iconMStyle} />}
{...form.getInputProps('birth_date')}
/>
</Group>
<Group grow>
<Select
label="Grade"
placeholder="Select grade"
data={gradeOptions}
filter={optionsFilter}
leftSection={<AlphaGIcon style={iconMStyle} />}
{...form.getInputProps('grade')}
onChange={(v) => {
if (!v) v = form.values.grade.toString();
form.getInputProps('grade').onChange(v);
}}
/>
<TextInput
withAsterisk
label="Email"
placeholder="doctor@example.com"
leftSection={<MailIcon style={iconMStyle} />}
{...form.getInputProps('email')}
/>
</Group>
<Group grow>
<TextInput
label="Phone"
placeholder="+441234567890"
leftSection={<PhoneIcon style={iconMStyle} />}
{...form.getInputProps('phone')}
/>
<Checkbox
label="Admin"
key={form.key("is_admin")}
{...form.getInputProps('is_admin', { type: 'checkbox' })}
/>
</Group>
<Group mt="md" justify="flex-end">
<Button variant="outline" onClick={() => modals.closeAll()}>
Cancel
</Button>
<Button type="submit">Confirm</Button>
</Group>
</Stack>
</form>
);
}
modals.open({
title: origDoctorInfo ? 'Edit Doctor' : 'Create Doctor',
centered: true,
size: "xl",
children: <ConfirmEditOrCreateDoctor doctorInfo={origDoctorInfo} />,
});
}
export function confirmSetResignedDoctor(doctor_id: number, doctor_name: string, is_resigned: boolean, onFinished: () => void = () => {}) {
const onClickConfirmSetResigned = (doctor: number, resigned: boolean) => {
apiSetDoctorResigned(doctor, resigned).then(res => {
if (res.success) {
showInfoMessage("", "The operation has been completed successfully.", 3000)
}
else {
showErrorMessage(res.message, "The doctor's resignation failed.")
}
}).catch(err => {
showErrorMessage(err.toString(), "The doctor's resignation failed.")
}).finally(() => onFinished())
modals.closeAll()
}
modals.open({
title: is_resigned ? "Termination" : "Reinstatement",
centered: true,
children: (
<Stack>
{ is_resigned ?
<Text>Are you certain you want to proceed with the termination of <Text span fw={700}>{doctor_name}</Text>?</Text>
:
<Text>Are you certain you want to proceed with the reinstatement of <Text span fw={700}>{doctor_name}</Text>?</Text>
}
<Group mt="md" justify="flex-end">
<Button onClick={() => modals.closeAll()}>Cancel</Button>
<Button color="red" onClick={() => onClickConfirmSetResigned(doctor_id, is_resigned)}>Confirm</Button>
</Group>
</Stack>
)
})
}
export function confirmAdminAddUser(
initRegType: number = 1, // Patient: 0, Doctor: 1, Receptionist: 99
onSucceed: () => void = () => {},
) {
function ConfirmAddUser() {
const form = useForm({
initialValues: {
reg_type: initRegType.toString(),
username: '',
name: '',
email: '',
phone: '',
gender: '',
birth_date: new Date(2000, 1, 1),
title: '',
address: '',
postcode: '',
grade: DoctorGrade.Unknown.toString(),
is_admin: false
},
validate: {
name: (v) => (v.length === 0 ? 'Input name.' : null),
email: (v) =>
/^\S+@\S+\.\S+$/.test(v) ? null : 'Incorrect email address.',
phone: (v) =>
v.length === 0 || /^\+?\d{6,15}$/.test(v)
? null
: 'Invalid phone number.',
birth_date: (v) => (v > new Date() ? 'Date of birth cannot be in the future' : null),
gender: (v) => (v.length === 0 ? "Select a gender" : null)
},
});
const genderOptions = useMemo(
() => [
{ value: 'M', label: 'Male' },
{ value: 'F', label: 'Female' },
{ value: 'Intersex', label: 'Intersex' },
],
[],
);
const gradeOptions = useMemo(
() =>
Object.entries(DoctorGrade)
.filter(([k]) => isNaN(Number(k)))
.map(([k, v]) => ({
value: v.toString(),
label: k,
})),
[],
);
const optionsFilter: OptionsFilter = ({ options, search }) => {
const res = (options as ComboboxItem[]).filter((o) =>
o.label.toLowerCase().includes(search.toLowerCase().trim()),
);
res.sort((a, b) => a.label.localeCompare(b.label));
return res;
};
const onClickRegSubmit = (values: typeof form.values) => {
apiAdminRegister(values).then((res) => {
if (res.success) {
// modals.closeAll();
showRegAccountPassword("Added User Succeed", res.data?.username, res.data?.password)
onSucceed()
} else showErrorMessage(res.message, 'Add User Failed');
}).catch((err) =>
showErrorMessage(err.toString(), 'Add User Failed'),
);
};
return (
<form onSubmit={form.onSubmit(onClickRegSubmit)}>
<Stack>
<Radio.Group
name="User Type"
label="Select user type"
withAsterisk
{...form.getInputProps('reg_type')}
>
<Group>
<Radio value="0" label="Patient" />
<Radio value="1" label="Doctor" />
<Radio value="99" label="Receptionist" />
</Group>
</Radio.Group>
<TextInput
withAsterisk
label="Login Name"
placeholder="Name used for system login"
leftSection={<RenameIcon style={iconMStyle} />}
{...form.getInputProps('username')}
/>
<TextInput
withAsterisk
label="Name"
placeholder="Real name"
leftSection={<RenameIcon style={iconMStyle} />}
{...form.getInputProps('name')}
/>
<TextInput
label="Title"
placeholder="e.g. Dr., Prof."
leftSection={<RenameIcon style={iconMStyle} />}
{...form.getInputProps('title')}
/>
<Select
label="Gender"
placeholder="Select gender"
data={genderOptions}
filter={optionsFilter}
leftSection={<GenderMaleFemaleIcon style={iconMStyle} />}
searchable
withAsterisk
{...form.getInputProps('gender')}
onChange={(v) => {
if (!v) v = form.values.gender;
form.getInputProps('gender').onChange(v);
}}
/>
<DateInput
withAsterisk
label="Birth Date"
valueFormat="DD/MM/YYYY"
leftSection={<CalendarIcon style={iconMStyle} />}
{...form.getInputProps('birth_date')}
/>
<TextInput
withAsterisk
label="Email"
placeholder="doctor@example.com"
leftSection={<MailIcon style={iconMStyle} />}
{...form.getInputProps('email')}
/>
<TextInput
label="Phone"
placeholder="+441234567890"
leftSection={<PhoneIcon style={iconMStyle} />}
{...form.getInputProps('phone')}
/>
{
form.values.reg_type == "0" ?
<>
<TextInput
withAsterisk
label="Address"
placeholder="Your home address"
leftSection={<HomeIcon style={iconMStyle}/>}
{...form.getInputProps('address')}
/>
<TextInput
withAsterisk
label="Postcode"
placeholder="Your postcode. eg. BS16 1QY"
leftSection={<HomeIcon style={iconMStyle}/>}
{...form.getInputProps('postcode')}
/>
</>
:
<>
<Select
label="Grade"
placeholder="Select grade"
data={gradeOptions}
filter={optionsFilter}
leftSection={<AlphaGIcon style={iconMStyle} />}
{...form.getInputProps('grade')}
onChange={(v) => {
if (!v) v = form.values.grade.toString();
form.getInputProps('grade').onChange(v);
}}
/>
<Checkbox
label="Admin"
key={form.key("is_admin")}
{...form.getInputProps('is_admin', { type: 'checkbox' })}
/>
</>
}
<Group mt="md" justify="flex-end">
<Button variant="outline" onClick={() => modals.closeAll()}>
Cancel
</Button>
<Button type="submit">Confirm</Button>
</Group>
</Stack>
</form>
);
}
modals.open({
title: "Add User",
centered: true,
size: "70%",
children: <ConfirmAddUser />,
});
}
export function showRegAccountPassword(title: string, username?: string, password?: string) {
modals.open({
title: title,
centered: true,
closeOnEscape: false,
closeOnClickOutside: false,
withCloseButton: false,
children: (
<Stack>
<Group>
<Text>Username: <Text span fw={700}>{username}</Text></Text>
{ username &&
<CopyButton value={username}>
{({ copied, copy }) => (
<Tooltip label={copied ? 'Copied' : 'Copy'} withArrow position="right">
<ActionIcon color={copied ? 'teal' : 'gray'} variant="subtle" onClick={copy}>
{copied ? <CheckIcon style={iconMStyle} /> : <ContentCopyIcon style={iconMStyle} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
}
</Group>
<Group>
<Text>Password: <Text span fw={700}>{password}</Text></Text>
{ password &&
<CopyButton value={password}>
{({ copied, copy }) => (
<Tooltip label={copied ? 'Copied' : 'Copy'} withArrow position="right">
<ActionIcon color={copied ? 'teal' : 'gray'} variant="subtle" onClick={copy}>
{copied ? <CheckIcon style={iconMStyle} /> : <ContentCopyIcon style={iconMStyle} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
}
</Group>
<Group mt="md" justify="flex-end">
<CopyButton value={`Username: ${username}\nPassword: ${password}`}>
{({ copied, copy }) => (
<Button color={copied ? 'teal' : 'blue'} variant="outline" onClick={copy}>
{copied ? 'Copied All' : 'Copy All'}
</Button>
)}
</CopyButton>
<Button onClick={() => modals.closeAll()}>OK</Button>
</Group>
</Stack>
),
});
}
export function confirmResetPassword(user_id: number, user_type: number) {
function ConfirmResetPWD() {
const [resetting, setResetting] = useState(false)
return(
<Stack>
<Text>Are you sure you want to reset the password?</Text>
<Group mt="md" justify="flex-end">
<Button onClick={() => modals.closeAll()}>Cancel</Button>
<Button color="red" disabled={resetting} onClick={() => {
setResetting(true)
apiResetPasswordFromRoleID(user_id, user_type).then(res => {
if (res.success) {
showRegAccountPassword("Reset Password Succeed", res.data?.username, res.data?.password)
} else {
showErrorMessage(res.message, "Reset Password Failed")
modals.closeAll()
}
}).catch(err => {
showErrorMessage(err.toString(), "Reset Password Failed")
modals.closeAll()
}).finally(() => setResetting(false))
}}>Confirm</Button>
</Group>
</Stack>
)
}
modals.open({
title: "Reset Password",
centered: true,
children: <ConfirmResetPWD/>
});
}
export function confirmDeleteTeam(teamId: number, department: String, onFinished: () => void = () => {}) {
const onClickConfirmDeleteTeam = (teamId: number) => {
apiDeleteTeam(teamId).then(res => {
if (res.success) {
showInfoMessage("", "Medical team deleted successfully", 3000)
} else {
showErrorMessage(res.message, "Failed to delete medical team")
}
}).catch(err => {
showErrorMessage(err.toString(), "Failed to delete medical team")
}).finally(() => onFinished())
modals.closeAll()
}
modals.open({
title: "Delete Medical Team",
centered: true,
children: (
<Stack>
<Text>Are you sure you want to delete this medical team: <Text span fw={700}>{department}</Text>?</Text>
<Group mt="md" justify="flex-end">
<Button onClick={() => modals.closeAll()}>Cancel</Button>
<Button color="red" onClick={() => onClickConfirmDeleteTeam(teamId)}>Confirm</Button>
</Group>
</Stack>
)
})
}
export function confirmEditOrCreateTeam(
origTeamInfo: DoctorTeamInfo | null,
doctorsList: DoctorDataWithPermission[] = [],
onSucceed: () => void = () => {}
) {
function ConfirmEditOrCreateTeam({
teamInfo,
doctorsList
}: {
teamInfo: DoctorTeamInfo | null;
doctorsList: DoctorDataWithPermission[];
}) {
const [loading, setLoading] = useState(false);
const [availableDoctors, setAvailableDoctors] = useState<DoctorDataWithPermission[]>([]);
useEffect(() => {
// Filter out resigned doctors
const activeDoctors = doctorsList.filter(d => !d.is_resigned);
setAvailableDoctors(activeDoctors);
}, [doctorsList]);
const form = useForm({
initialValues: {
team_id: teamInfo ? teamInfo.id : -1,
department: teamInfo ? teamInfo.department : '',
consultant_id: teamInfo ? teamInfo.consultant_id.toString() : '-1',
is_admin_team: teamInfo ? teamInfo.is_admin_team : false,
team_members: teamInfo
? teamInfo.members.map(member => member.id)
: []
},
validate: {
department: (value) => (!value ? 'Please select a department' : null),
consultant_id: (value) => (value === '-1' || !value ? 'Please select a consultant' : null),
},
});
const departmentOptions = useMemo(() => {
return Object.entries(Departments).map(([key, value]) => ({
value: value as string,
label: key.replace(/_/g, ' ')
}));
}, []);
const consultantOptions = useMemo(() => {
return availableDoctors
.map(d => ({
value: d.id.toString(),
label: `${d.title} ${d.name}`
}));
}, [availableDoctors]);
const teamMemberOptions = useMemo(() => {
return availableDoctors.map(d => ({
value: d.id.toString(),
label: `${d.title} ${d.name} (${DoctorGrade[d.grade]})`
}));
}, [availableDoctors]);
const handleFormSubmit = (values: typeof form.values) => {
setLoading(true);
const teamData = {
...values,
consultant_id: Number(values.consultant_id),
team_members: values.team_members.map(Number)
};
const apiCall = teamInfo
? apiEditTeam(teamData)
: apiCreateTeam(teamData);
apiCall.then(res => {
if (res.success) {
modals.closeAll();
showInfoMessage("", teamInfo ? "Medical team updated successfully" : "Medical team created successfully", 3000);
onSucceed();
} else {
showErrorMessage(res.message, teamInfo ? "Failed to update medical team" : "Failed to create medical team");
}
}).catch(err => {
showErrorMessage(err.toString(), teamInfo ? "Failed to update medical team" : "Failed to create medical team");
}).finally(() => {
setLoading(false);
});
};
return (
<form onSubmit={form.onSubmit(handleFormSubmit)}>
<Stack>
<Select
withAsterisk
label="Department"
placeholder="Select department"
data={departmentOptions}
searchable
nothingFoundMessage="No results found"
{...form.getInputProps('department')}
onChange={(value) => {
if (!value) return;
form.setFieldValue('department', value);
}}
/>
<Select
withAsterisk
label="Consultant"
placeholder="Select consultant"
data={consultantOptions}
searchable
nothingFoundMessage="No results found"
{...form.getInputProps('consultant_id')}
onChange={(value) => {
if (!value) return;
form.setFieldValue('consultant_id', value);
}}
/>
<MultiSelect
label="Team Members"
placeholder="Select team members"
data={teamMemberOptions}
searchable
nothingFoundMessage="No results found"
value={form.values.team_members.map(id => id.toString())}
onChange={(values) => {
form.setFieldValue('team_members', values.map(Number));
}}
/>
<Checkbox
label="Admin Team"
{...form.getInputProps('is_admin_team', { type: 'checkbox' })}
/>
<Group mt="md" justify="flex-end">
<Button variant="outline" onClick={() => modals.closeAll()}>
Cancel
</Button>
<Button type="submit" loading={loading}>
Confirm
</Button>
</Group>
</Stack>
</form>
);
}
modals.open({
title: origTeamInfo ? "Edit Medical Team" : "Create Medical Team",
centered: true,
size: "lg",
children: (
<ConfirmEditOrCreateTeam
teamInfo={origTeamInfo}
doctorsList={doctorsList}
/>
)
});
}
export function confirmViewTeamMembers(teamInfo: DoctorTeamInfo) {
modals.open({
title: `Medical Team: ${teamInfo.department.replace(/_/g, ' ')}`,
centered: true,
size: "lg",
children: (
<Stack>
<Text fw={700}>Team Information</Text>
<Text>Department: {teamInfo.department.replace(/_/g, ' ')}</Text>
<Text>
Consultant: {
teamInfo.members.find(m => m.id === teamInfo.consultant_id)
? `${teamInfo.members.find(m => m.id === teamInfo.consultant_id)?.title || ''} ${teamInfo.members.find(m => m.id === teamInfo.consultant_id)?.name || ''}`
: `ID: ${teamInfo.consultant_id}`
}
</Text>
<Text>Admin Team: {teamInfo.is_admin_team ? 'Yes' : 'No'}</Text>
<Text fw={700} mt="md">Team Members ({teamInfo.members.length})</Text>
{teamInfo.members.length > 0 ? (
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>ID</Table.Th>
<Table.Th>Name</Table.Th>
<Table.Th>Grade</Table.Th>
<Table.Th>Gender</Table.Th>
<Table.Th>Contact</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{teamInfo.members.map(member => (
<Table.Tr key={member.id}>
<Table.Td>{member.id}</Table.Td>
<Table.Td>{`${member.title} ${member.name}`}</Table.Td>
<Table.Td>{DoctorGrade[member.grade]}</Table.Td>
<Table.Td>{member.gender}</Table.Td>
<Table.Td>{member.email}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
) : (
<Text c="dimmed">No team members</Text>
)}
<Group mt="md" justify="flex-end">
<Button onClick={() => modals.closeAll()}>Close</Button>
</Group>
</Stack>
)
});
}
export function confirmDeleteBooking(appointmentId: number, category: string, onFinished: () => void = () => {}) {
const onClickConfirmDeleteBooking = (appointmentId: number) => {
apiDeletePatientBooking(appointmentId).then(res => {
if (res.success) {
showInfoMessage("", "Appointment deleted successfully", 3000)
} else {
showErrorMessage(res.message, "Failed to delete appointment")
}
}).catch(err => {
showErrorMessage(err.toString(), "Failed to delete appointment")
}).finally(() => onFinished())
modals.closeAll()
}
modals.open({
title: "Delete Appointment",
centered: true,
children: (
<Stack>
<Text>Are you sure you want to delete this appointment: <Text span fw={700}>{category}</Text>?</Text>
<Group mt="md" justify="flex-end">
<Button onClick={() => modals.closeAll()}>Cancel</Button>
<Button color="red" onClick={() => onClickConfirmDeleteBooking(appointmentId)}>Confirm</Button>
</Group>
</Stack>
)
})
}
export function confirmEditOrCreateBooking(
origBookingInfo: PatientBookingInfo | null,
onSucceed: () => void = () => {}
) {
function ConfirmEditOrCreateBooking({
bookingInfo
}: {
bookingInfo: PatientBookingInfo | null;
}) {
const [loading, setLoading] = useState(false);
const form = useForm({
initialValues: {
appointment_id: bookingInfo ? bookingInfo.id : -1,
appointment_time: bookingInfo ? new Date(bookingInfo.appointment_time * 1000) : new Date(),
category: bookingInfo ? bookingInfo.category : BookingCategory.Consultation,
description: bookingInfo ? bookingInfo.description : '',
},
validate: {
appointment_time: (value) => (!value ? 'Please select appointment time' : null),
category: (value) => (!value ? 'Please select category' : null),
description: (value) => (!value || value.length < 5 ? 'Description must be at least 5 characters' : null),
},
});
const categoryOptions = useMemo(() => {
return Object.entries(BookingCategory).map(([key, value]) => ({
value: value,
label: key.replace(/_/g, ' ')
}));
}, []);
const handleFormSubmit = (values: typeof form.values) => {
setLoading(true);
const appointmentTime = Math.floor(values.appointment_time.getTime() / 1000);
const bookingData = {
...values,
appointment_time: appointmentTime,
};
const apiCall = bookingInfo
? apiEditPatientBooking(bookingData)
: apiPatientBooking(bookingData);
apiCall.then(res => {
if (res.success) {
modals.closeAll();
showInfoMessage("", bookingInfo ? "Appointment updated successfully" : "Appointment created successfully", 3000);
onSucceed();
} else {
showErrorMessage(res.message, bookingInfo ? "Failed to update appointment" : "Failed to create appointment");
}
}).catch(err => {
showErrorMessage(err.toString(), bookingInfo ? "Failed to update appointment" : "Failed to create appointment");
}).finally(() => {
setLoading(false);
});
};
return (
<form onSubmit={form.onSubmit(handleFormSubmit)}>
<Stack>
<Select
withAsterisk
label="Category"
placeholder="Select appointment category"
data={categoryOptions}
searchable
nothingFoundMessage="No results found"
{...form.getInputProps('category')}
onChange={(value) => {
if (!value) return;
form.setFieldValue('category', value);
}}
/>
<DateTimePicker
withAsterisk
label="Appointment Time"
placeholder="Select date and time"
valueFormat="DD/MM/YYYY HH:mm"
leftSection={<CalendarIcon style={iconMStyle} />}
{...form.getInputProps('appointment_time')}
/>
<Textarea
withAsterisk
label="Description"
placeholder="Describe your appointment request"
minRows={3}
{...form.getInputProps('description')}
/>
<Group mt="md" justify="flex-end">
<Button variant="outline" onClick={() => modals.closeAll()}>
Cancel
</Button>
<Button type="submit" loading={loading}>
Confirm
</Button>
</Group>
</Stack>
</form>
);
}
modals.open({
title: origBookingInfo ? "Edit Appointment" : "Create Appointment",
centered: true,
size: "lg",
children: (
<ConfirmEditOrCreateBooking bookingInfo={origBookingInfo} />
)
});
}
export function confirmApproveAppointment(
appointmentId: number,
onSucceed: () => void = () => {}
) {
function ConfirmApproveAppointment() {
const [loading, setLoading] = useState(false);
const [teams, setTeams] = useState<DoctorTeamInfo[]>([]);
const [loadingTeams, setLoadingTeams] = useState(true);
const form = useForm({
initialValues: {
appointment_id: appointmentId,
approved: true,
feedback: '',
assigned_team: '',
},
validate: {
feedback: (value) => (!value ? 'Please provide feedback' : null),
assigned_team: (value) => (!value ? 'Please select a medical team' : null),
},
});
useEffect(() => {
// Load teams for selection
apiGetTeamList(1).then(res => {
if (res.success) {
setTeams(res.data.teams);
} else {
showErrorMessage(res.message, "Failed to load medical teams");
}
})
.catch(err => {
showErrorMessage("Failed to load medical teams", "Error");
})
.finally(() => {
setLoadingTeams(false);
});
}, []);
const teamOptions = useMemo(() => {
return teams.map(team => ({
value: team.id.toString(),
label: `${team.department.replace(/_/g, ' ')} (ID: ${team.id})`
}));
}, [teams]);
const handleFormSubmit = (values: typeof form.values) => {
setLoading(true);
apiProcessAppointment(
values.appointment_id,
values.approved,
values.feedback,
parseInt(values.assigned_team)
).then(res => {
if (res.success) {
modals.closeAll();
showInfoMessage("", "Appointment approved successfully", 3000);
onSucceed();
} else {
showErrorMessage(res.message, "Failed to approve appointment");
}
}).catch(err => {
showErrorMessage("Failed to approve appointment", "Error");
}).finally(() => {
setLoading(false);
});
};
return (
<form onSubmit={form.onSubmit(handleFormSubmit)}>
<Stack>
<Textarea
withAsterisk
label="Feedback"
placeholder="Provide feedback for the patient"
minRows={3}
{...form.getInputProps('feedback')}
/>
<Select
withAsterisk
label="Assign Medical Team"
placeholder="Select a medical team"
data={teamOptions}
searchable
nothingFoundMessage="No medical teams found"
disabled={loadingTeams}
{...form.getInputProps('assigned_team')}
onChange={(value) => {
if (!value) return;
form.setFieldValue('assigned_team', value);
}}
/>
<Group mt="md" justify="flex-end">
<Button variant="outline" onClick={() => modals.closeAll()}>
Cancel
</Button>
<Button
type="submit"
loading={loading || loadingTeams}
color="green"
>
Approve
</Button>
</Group>
</Stack>
</form>
);
}
modals.open({
title: "Approve Appointment",
centered: true,
size: "md",
children: <ConfirmApproveAppointment />
});
}
export function confirmRejectAppointment(
appointmentId: number,
onSucceed: () => void = () => {}
) {
function ConfirmRejectAppointment() {
const [loading, setLoading] = useState(false);
const form = useForm({
initialValues: {
appointment_id: appointmentId,
approved: true,
feedback: '',
assigned_team: null,
},
validate: {
feedback: (value) => (!value ? 'Please provide rejection reason' : null),
},
});
const handleFormSubmit = (values: typeof form.values) => {
setLoading(true);
apiProcessAppointment(
values.appointment_id,
values.approved,
values.feedback,
values.assigned_team
).then(res => {
if (res.success) {
modals.closeAll();
showInfoMessage("", "Appointment rejected successfully", 3000);
onSucceed();
} else {
showErrorMessage(res.message, "Failed to reject appointment");
}
}).catch(err => {
showErrorMessage("Failed to reject appointment", "Error");
}).finally(() => {
setLoading(false);
});
};
return (
<form onSubmit={form.onSubmit(handleFormSubmit)}>
<Stack>
<Textarea
withAsterisk
label="Rejection Reason"
placeholder="Provide reason for rejection"
minRows={3}
{...form.getInputProps('feedback')}
/>
<Group mt="md" justify="flex-end">
<Button variant="outline" onClick={() => modals.closeAll()}>
Cancel
</Button>
<Button
type="submit"
loading={loading}
color="red"
>
Reject
</Button>
</Group>
</Stack>
</form>
);
}
modals.open({
title: "Reject Appointment",
centered: true,
size: "md",
children: <ConfirmRejectAppointment />
});
}
export function confirmPatientAdmission(
appointmentId: number,
onSucceed: () => void = () => {}
) {
function ConfirmPatientAdmission() {
const [loading, setLoading] = useState(false);
const [wards, setWards] = useState<WardInfo[]>([]);
const [loadingWards, setLoadingWards] = useState(true);
const form = useForm({
initialValues: {
appointment_id: appointmentId,
ward_id: '',
},
validate: {
ward_id: (value) => (!value ? 'Please select a ward' : null),
},
});
useEffect(() => {
// Load wards for selection
apiGetWardList(1, true).then(res => {
if (res.success) {
setWards(res.data.wards);
} else {
showErrorMessage(res.message, "Failed to load wards");
}
})
.catch(err => {
showErrorMessage("Failed to load wards", "Error");
})
.finally(() => {
setLoadingWards(false);
});
}, []);
const wardOptions = useMemo(() => {
return wards
.filter(ward => ward.current_occupancy < ward.total_capacity) // Only show wards with available capacity
.map(ward => ({
value: ward.id.toString(),
label: `${ward.name} (${ward.current_occupancy}/${ward.total_capacity}) - ${WardTypes[ward.type]}`
}));
}, [wards]);
const handleFormSubmit = (values: typeof form.values) => {
setLoading(true);
apiPatientAdmission(
values.appointment_id,
parseInt(values.ward_id)
).then(res => {
if (res.success) {
modals.closeAll();
showInfoMessage("", "Patient admitted successfully", 3000);
onSucceed();
} else {
showErrorMessage(res.message, "Failed to admit patient");
}
}).catch(err => {
showErrorMessage("Failed to admit patient", "Error");
}).finally(() => {
setLoading(false);
});
};
return (
<form onSubmit={form.onSubmit(handleFormSubmit)}>
<Stack>
<Select
withAsterisk
label="Select Ward"
placeholder="Choose a ward for patient"
data={wardOptions}
searchable
nothingFoundMessage={wardOptions.length === 0 ? "No wards with available capacity" : "No matching wards found"}
disabled={loadingWards || wardOptions.length === 0}
{...form.getInputProps('ward_id')}
onChange={(value) => {
if (!value) return;
form.setFieldValue('ward_id', value);
}}
/>
{wardOptions.length === 0 && !loadingWards && (
<Text c="red" size="sm">
No wards with available capacity. Please free up space in a ward first.
</Text>
)}
<Group mt="md" justify="flex-end">
<Button variant="outline" onClick={() => modals.closeAll()}>
Cancel
</Button>
<Button
type="submit"
loading={loading || loadingWards}
disabled={wardOptions.length === 0}
color="blue"
>
Admit Patient
</Button>
</Group>
</Stack>
</form>
);
}
modals.open({
title: "Patient Admission",
centered: true,
size: "md",
children: <ConfirmPatientAdmission />
});
}
export function confirmEditPatient(
origPatientInfo: PatientData,
onSucceed: () => void = () => {},
) {
function ConfirmEditPatient({
patientInfo,
}: {
patientInfo: PatientData;
}) {
const form = useForm({
initialValues: {
patient_id: patientInfo.id,
title: patientInfo.title,
name: patientInfo.name,
gender: patientInfo.gender,
birth_date: parseDmyToDate(patientInfo.birth_date),
email: patientInfo.email,
phone: patientInfo.phone,
address: patientInfo.address,
postcode: patientInfo.postcode,
},
validate: {
name: (value) => (!value || value.trim() === "" ? "Name cannot be empty" : null),
title: (value) => (!value || value.trim() === "" ? "Title cannot be empty" : null),
email: (value) => (!/^\S+@\S+$/.test(value) ? "Please enter a valid email" : null),
phone: (value) => (!value || value.length < 6 ? "Please enter a valid phone number" : null),
address: (value) => (!value || value.trim() === "" ? "Address cannot be empty" : null),
postcode: (value) => (!value || value.trim() === "" ? "Postcode cannot be empty" : null),
},
});
const onClickConfirmEditPatient = (values: typeof form.values) => {
apiEditPatientInfo(values).then(res => {
if (res.success) {
modals.closeAll()
showInfoMessage("", "Patient information updated successfully", 3000)
onSucceed()
} else {
showErrorMessage(res.message, "Failed to update patient information")
}
}).catch(err => {
showErrorMessage(err.toString(), "Failed to update patient information")
})
}
return (
<form onSubmit={form.onSubmit(onClickConfirmEditPatient)}>
<Stack>
<Group grow>
<TextInput
withAsterisk
label="Title"
placeholder="Mr, Mrs, Dr, etc."
{...form.getInputProps("title")}
/>
<TextInput
withAsterisk
label="Name"
placeholder="Full name"
leftSection={<HumanEditIcon style={iconMStyle}/>}
{...form.getInputProps("name")}
/>
</Group>
<Group grow>
<Select
withAsterisk
label="Gender"
placeholder="Select gender"
data={[
{ value: "M", label: "Male" },
{ value: "F", label: "Female" },
{ value: "Intersex", label: "Intersex" },
]}
leftSection={<GenderMaleFemaleIcon style={iconMStyle}/>}
{...form.getInputProps("gender")}
/>
<DateInput
withAsterisk
label="Birth Date"
placeholder="Select birth date"
value={form.values.birth_date || null}
onChange={(value) => {
form.setFieldValue("birth_date", value || new Date());
}}
leftSection={<CalendarIcon style={iconMStyle}/>}
error={form.errors.birth_date}
/>
</Group>
<Group grow>
<TextInput
withAsterisk
label="Email"
placeholder="example@example.com"
leftSection={<MailIcon style={iconMStyle}/>}
{...form.getInputProps("email")}
/>
<TextInput
withAsterisk
label="Phone"
placeholder="Phone number"
leftSection={<PhoneIcon style={iconMStyle}/>}
{...form.getInputProps("phone")}
/>
</Group>
<Group grow>
<TextInput
withAsterisk
label="Address"
placeholder="Full address"
leftSection={<HomeIcon style={iconMStyle}/>}
{...form.getInputProps("address")}
/>
<TextInput
withAsterisk
label="Postcode"
placeholder="Postal code"
{...form.getInputProps("postcode")}
/>
</Group>
<Group justify="flex-end" mt="md">
<Button variant="outline" onClick={() => modals.closeAll()}>Cancel</Button>
<Button type="submit">Save Changes</Button>
</Group>
</Stack>
</form>
);
}
modals.open({
title: "Edit Patient Information",
centered: true,
size: "xl",
children: <ConfirmEditPatient patientInfo={origPatientInfo} />,
});
}
export function confirmPatientDischarge(admissionId: number, patientName: string, onFinished: () => void = () => {}) {
const onClickConfirmDischarge = (admissionId: number) => {
apiPatientDischarge(admissionId).then(res => {
if (res.success) {
modals.closeAll()
showInfoMessage("", "Patient discharged successfully", 3000)
onFinished()
} else {
showErrorMessage(res.message, "Failed to discharge patient")
}
}).catch(err => {
showErrorMessage(err.toString(), "Failed to discharge patient")
})
}
modals.open({
title: "Confirm Patient Discharge",
centered: true,
children: (
<Stack>
<Text>Are you sure you want to discharge {patientName}?</Text>
<Group justify="flex-end" mt="md">
<Button variant="outline" onClick={() => modals.closeAll()}>Cancel</Button>
<Button color="red" onClick={() => onClickConfirmDischarge(admissionId)}>Confirm Discharge</Button>
</Group>
</Stack>
),
})
}