1599 lines
61 KiB
TypeScript
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>
|
|
),
|
|
})
|
|
}
|