251 lines
12 KiB
TypeScript
251 lines
12 KiB
TypeScript
import type {Route} from "../../../.react-router/types/app/pages/dashboard/+types/StaffManagement";
|
|
import {useEffect, useMemo, useState} from "react";
|
|
import {DashboardPageType, DoctorGrade, UserType} from "~/utils/hms_enums.ts";
|
|
import {useNavigate, useOutletContext} from "react-router";
|
|
import {
|
|
type DoctorDataWithPermission,
|
|
type DoctorListInfo,
|
|
type OutletContextType,
|
|
SORT_SYMBOLS
|
|
} from "~/utils/models.ts";
|
|
import {Accordion, ActionIcon, Button, Card, Group, Pagination, Stack, Table, Text, Tooltip} from "@mantine/core";
|
|
import {apiGetDoctorsList} from "~/utils/hms_api.ts";
|
|
import {showErrorMessage} from "~/utils/utils.ts";
|
|
import {iconMStyle, marginLeftRight, marginRightBottom, marginTopBottom} from "~/styles.ts";
|
|
import PencilIcon from "mdi-react/PencilIcon";
|
|
import {
|
|
confirmAdminAddUser,
|
|
confirmEditDoctor,
|
|
confirmResetPassword,
|
|
confirmSetResignedDoctor
|
|
} from "~/components/subs/confirms.tsx";
|
|
import AddIcon from "mdi-react/AddIcon";
|
|
import LogoutIcon from "mdi-react/LogoutIcon";
|
|
import LoginIcon from "mdi-react/LoginIcon";
|
|
import LockResetIcon from "mdi-react/LockResetIcon";
|
|
import {StaffTableFilter} from "~/components/subs/StaffManageTableFilter.tsx";
|
|
import HistoryIcon from "mdi-react/HistoryIcon";
|
|
import FilterIcon from "mdi-react/FilterIcon";
|
|
import {ResponsiveTableContainer} from "~/components/subs/ResponsiveTableContainer.tsx";
|
|
|
|
export function meta({}: Route.MetaArgs) {
|
|
return [
|
|
{ title: "Medical Teams Management" },
|
|
{ name: "description", content: "Medical Teams Management" },
|
|
];
|
|
}
|
|
|
|
export default function Component() {
|
|
const navigate = useNavigate();
|
|
const [refreshingStaffList, setRefreshingStaffList] = useState<boolean>(false)
|
|
const [staffInfo, setStaffInfo] = useState<DoctorListInfo>({doctors: [], total_pages: 1})
|
|
|
|
const [sortKey, setSortKey] = useState<string>("")
|
|
const [sortDesc, setSortDesc] = useState<boolean>(true)
|
|
const [currPage, setCurrPage] = useState<number>(1)
|
|
|
|
const [filterGrades, setFilterGrades] = useState<string[]>([])
|
|
const [filterGenders, setFilterGenders] = useState<string[]>([])
|
|
const [filterIsAdmin, setFilterIsAdmin] = useState<number>(-1)
|
|
const [filterIsTerminated, setFilterIsTerminated] = useState<number>(-1)
|
|
|
|
const { changePage } = useOutletContext<OutletContextType>();
|
|
|
|
useEffect(() => {
|
|
refreshStaffList()
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
refreshStaffList()
|
|
}, [currPage]);
|
|
|
|
const updateFilter = (grades: string[], genders: string[], isAdmin: string, isTerminated: string) => {
|
|
setFilterGrades(grades)
|
|
setFilterGenders(genders)
|
|
setFilterIsAdmin(Number(isAdmin))
|
|
setFilterIsTerminated(Number(isTerminated))
|
|
}
|
|
|
|
const refreshStaffList = () => {
|
|
setRefreshingStaffList(true)
|
|
apiGetDoctorsList(currPage).then(res => {
|
|
if (!res.success) {
|
|
showErrorMessage(res.message, "Get Ward List Failed")
|
|
return
|
|
}
|
|
setStaffInfo(res.data)
|
|
})
|
|
.catch(err => {})
|
|
.finally(() => { setRefreshingStaffList(false) })
|
|
}
|
|
|
|
const handleSort = (key: string) => {
|
|
if (sortKey === key) {
|
|
setSortDesc(!sortDesc)
|
|
} else {
|
|
setSortKey(key)
|
|
setSortDesc(false) // 默认升序
|
|
}
|
|
}
|
|
|
|
const sortedDoctors = useMemo<DoctorDataWithPermission[]>(() => {
|
|
let data = [...staffInfo.doctors]
|
|
|
|
data = data.filter((w) => {
|
|
const okType = filterGrades.length === 0 || filterGrades.includes(w.grade.toString())
|
|
const okGender = filterGenders.length === 0 || filterGenders.includes(w.gender)
|
|
const okAdmin = filterIsAdmin === -1 || (filterIsAdmin === 1 ? w.is_admin : !w.is_admin)
|
|
const okTerminated = filterIsTerminated === -1 || (filterIsTerminated === 1 ? w.is_resigned : !w.is_resigned)
|
|
return okType && okGender && okAdmin && okTerminated
|
|
})
|
|
|
|
if (!sortKey) return data
|
|
|
|
data.sort((a, b) => {
|
|
const valA = a[sortKey]
|
|
const valB = b[sortKey]
|
|
if (typeof valA === 'string' && typeof valB === 'string') {
|
|
const cmp = valA.localeCompare(valB)
|
|
return sortDesc ? -cmp : cmp
|
|
}
|
|
|
|
if (typeof valA === 'number' && typeof valB === 'number') {
|
|
return sortDesc ? valB - valA : valA - valB
|
|
}
|
|
|
|
if (typeof valA === 'boolean' && typeof valB === 'boolean') {
|
|
return sortDesc ? (valB ? 1 : -1) : (valA ? 1 : -1)
|
|
}
|
|
return 0
|
|
})
|
|
return data
|
|
}, [staffInfo.doctors, sortKey, sortDesc, filterGrades, filterGenders, filterIsAdmin, filterIsTerminated])
|
|
|
|
|
|
const rows = sortedDoctors.map((doctor) => (
|
|
<Table.Tr key={doctor.id}>
|
|
<Table.Td>{doctor.id}</Table.Td>
|
|
<Table.Td>{`${doctor.title} ${doctor.name}`}</Table.Td>
|
|
<Table.Td>{DoctorGrade[doctor.grade]}</Table.Td>
|
|
<Table.Td>{doctor.gender}</Table.Td>
|
|
<Table.Td>{doctor.birth_date}</Table.Td>
|
|
<Table.Td>{doctor.email}</Table.Td>
|
|
<Table.Td>{doctor.phone}</Table.Td>
|
|
<Table.Td>{doctor.is_admin ? "Yes" : "No"}</Table.Td>
|
|
<Table.Td>{doctor.is_resigned ? "Yes" : "No"}</Table.Td>
|
|
<Table.Td>
|
|
<Group>
|
|
<Tooltip label="Treatment Record" withArrow>
|
|
<ActionIcon onClick={() => { changePage(DashboardPageType.TreatmentRecord, `/dashboard/treatment_record/d${doctor.id}`) }}>
|
|
<HistoryIcon style={iconMStyle}/>
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
<ActionIcon onClick={() => { confirmEditDoctor(doctor, refreshStaffList) }}>
|
|
<PencilIcon style={iconMStyle}/>
|
|
</ActionIcon>
|
|
<Tooltip label="Reset Password" withArrow>
|
|
<ActionIcon onClick={() => { confirmResetPassword(doctor.id, UserType.DOCTOR) }}>
|
|
<LockResetIcon style={iconMStyle}/>
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
{doctor.is_resigned ?
|
|
<ActionIcon onClick={() => { confirmSetResignedDoctor(doctor.id, `${doctor.title} ${doctor.name}`, false, refreshStaffList) }}>
|
|
<LoginIcon style={iconMStyle}/>
|
|
</ActionIcon>
|
|
:
|
|
<ActionIcon color="red" onClick={() => { confirmSetResignedDoctor(doctor.id, `${doctor.title} ${doctor.name}`, true, refreshStaffList) }}>
|
|
<LogoutIcon style={iconMStyle}/>
|
|
</ActionIcon>
|
|
}
|
|
</Group>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))
|
|
|
|
return (
|
|
<Stack>
|
|
<Group justify="space-between" align="center" style={marginLeftRight}>
|
|
<Text size="1.5em" fw={700}>Staff Management</Text>
|
|
<Button
|
|
leftSection={<AddIcon style={iconMStyle}/>}
|
|
onClick={() => { confirmAdminAddUser(1, refreshStaffList) }}
|
|
>Add New Staff</Button>
|
|
</Group>
|
|
|
|
<Card padding="lg" radius="md" withBorder style={marginTopBottom}>
|
|
|
|
<Card.Section withBorder>
|
|
<Accordion variant="filled" chevronPosition="left" defaultValue="advanced-filter">
|
|
<Accordion.Item value="advanced-filter">
|
|
<Accordion.Control>
|
|
<Group>
|
|
<FilterIcon style={iconMStyle} />
|
|
<Text>Filters</Text>
|
|
</Group>
|
|
</Accordion.Control>
|
|
<Accordion.Panel>
|
|
<StaffTableFilter onChange={updateFilter}/>
|
|
</Accordion.Panel>
|
|
</Accordion.Item>
|
|
</Accordion>
|
|
</Card.Section>
|
|
|
|
<Card.Section>
|
|
<ResponsiveTableContainer minWidth={600}>
|
|
<Table striped highlightOnHover withColumnBorders withTableBorder>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th onClick={() => handleSort("id")}>
|
|
User ID{" "}
|
|
{sortKey === "id" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
|
|
</Table.Th>
|
|
<Table.Th onClick={() => handleSort("name")}>
|
|
Name{" "}
|
|
{sortKey === "name" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
|
|
</Table.Th>
|
|
<Table.Th onClick={() => handleSort("grade")}>
|
|
Grade{" "}
|
|
{sortKey === "grade" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
|
|
</Table.Th>
|
|
<Table.Th onClick={() => handleSort("gender")}>
|
|
Gender{" "}
|
|
{sortKey === "gender" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
|
|
</Table.Th>
|
|
<Table.Th onClick={() => handleSort("birth_date")}>
|
|
Birth Date{" "}
|
|
{sortKey === "birth_date" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
|
|
</Table.Th>
|
|
<Table.Th onClick={() => handleSort("email")}>
|
|
Email{" "}
|
|
{sortKey === "email" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
|
|
</Table.Th>
|
|
<Table.Th onClick={() => handleSort("phone")}>
|
|
Phone{" "}
|
|
{sortKey === "phone" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
|
|
</Table.Th>
|
|
<Table.Th onClick={() => handleSort("is_admin")}>
|
|
Admin{" "}
|
|
{sortKey === "is_admin" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
|
|
</Table.Th>
|
|
<Table.Th onClick={() => handleSort("is_resigned")}>
|
|
Terminated{" "}
|
|
{sortKey === "is_resigned" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
|
|
</Table.Th>
|
|
<Table.Th>
|
|
Operations
|
|
</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>{rows}</Table.Tbody>
|
|
</Table>
|
|
</ResponsiveTableContainer>
|
|
|
|
<Pagination withEdges total={staffInfo.total_pages} value={currPage} onChange={setCurrPage} mt="sm"
|
|
style={{justifyItems: "flex-end", ...marginRightBottom}}/>
|
|
</Card.Section>
|
|
</Card>
|
|
|
|
</Stack>
|
|
);
|
|
}
|