переделал страницу с пациентами для внедрения redux и вынесения логикки компонента в отдельный хук

This commit is contained in:
Андрей Дувакин 2025-03-24 13:31:30 +05:00
parent 3dec01804a
commit f840a0ab34
5 changed files with 205 additions and 186 deletions

View File

@ -55,7 +55,7 @@ const PatientFormModal = ({visible, onCancel, onSubmit, patient}) => {
cancelText={"Отмена"} cancelText={"Отмена"}
maskClosable={false} maskClosable={false}
forceRender={true} forceRender={true}
style={{top: 20}} style={{marginTop: 20, marginBottom: 50}}
centered centered
> >
<Form form={form} layout={"vertical"}> <Form form={form} layout={"vertical"}>

View File

@ -0,0 +1,120 @@
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { notification } from "antd";
import {
useAddPatientMutation,
useDeletePatientMutation,
useGetPatientsQuery,
useUpdatePatientMutation
} from "../redux/services/patientsApi";
import {
openModal,
closeModal,
selectPatient,
setSearchText,
setSortOrder,
setViewMode,
setCurrentPage,
setPageSize
} from "../redux/slices/patientsSlice";
import { getCachedInfo } from "../utils/cachedInfoUtils";
const usePatients = () => {
const dispatch = useDispatch();
const {
searchText,
sortOrder,
viewMode,
selectedPatient,
isModalVisible
} = useSelector(state => state.patientsUI);
const { data: patients = [], isLoading, isError } = useGetPatientsQuery(undefined, {
pollingInterval: 20000,
});
const [addPatient] = useAddPatientMutation();
const [updatePatient] = useUpdatePatientMutation();
const [deletePatient] = useDeletePatientMutation();
useEffect(() => {
document.title = "Пациенты";
const cachedViewMode = getCachedInfo("viewModePatients");
if (cachedViewMode) dispatch(setViewMode(cachedViewMode));
}, [dispatch]);
const handleAddPatient = () => {
dispatch(selectPatient(null));
dispatch(openModal());
};
const handleEditPatient = (patient) => {
dispatch(selectPatient(patient));
dispatch(openModal());
};
const handleDeletePatient = async (patientId) => {
try {
await deletePatient(patientId).unwrap();
notification.success({
message: "Пациент удалён",
description: "Пациент успешно удалён из базы.",
placement: "topRight",
});
} catch (error) {
notification.error({
message: "Ошибка удаления",
description: error.data?.message || "Не удалось удалить пациента",
placement: "topRight",
});
}
};
const handleModalPatientSubmit = async (patientData) => {
try {
if (selectedPatient) {
await updatePatient({ id: selectedPatient.id, ...patientData }).unwrap();
notification.success({
message: "Пациент обновлён",
description: `Данные пациента ${patientData.first_name} ${patientData.last_name} успешно обновлены.`,
placement: "topRight",
});
} else {
await addPatient(patientData).unwrap();
notification.success({
message: "Пациент добавлен",
description: `Пациент ${patientData.first_name} ${patientData.last_name} успешно добавлен.`,
placement: "topRight",
});
}
dispatch(closeModal());
} catch (error) {
notification.error({
message: "Ошибка",
description: error.data?.message || "Произошла ошибка при сохранении",
placement: "topRight",
});
}
};
return {
patients,
isLoading,
isError,
searchText,
sortOrder,
viewMode,
isModalVisible,
selectedPatient,
handleAddPatient,
handleEditPatient,
handleDeletePatient,
handleModalPatientSubmit,
setSearchText: (text) => dispatch(setSearchText(text)),
setSortOrder: (order) => dispatch(setSortOrder(order)),
setViewMode: (mode) => dispatch(setViewMode(mode)),
setCurrentPage: (page) => dispatch(setCurrentPage(page)),
setPageSize: (size) => dispatch(setPageSize(size)),
};
};
export default usePatients;

View File

@ -1,4 +1,4 @@
import {useEffect} from 'react'; import {useEffect, useMemo} from 'react';
import { import {
Input, Input,
Select, Select,
@ -6,147 +6,77 @@ import {
FloatButton, FloatButton,
Row, Row,
Col, Col,
notification,
Tooltip,
Table, Table,
Button, Button,
Popconfirm, Popconfirm,
Typography, Result Typography,
Result, Tooltip
} from "antd"; } from "antd";
import { import {
BuildOutlined, BuildOutlined,
PlusOutlined, PlusOutlined,
SortAscendingOutlined, SortAscendingOutlined,
SortDescendingOutlined, TableOutlined, SortDescendingOutlined,
TableOutlined,
TeamOutlined TeamOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import { import {
useAddPatientMutation,
useDeletePatientMutation,
useGetPatientsQuery,
useUpdatePatientMutation
} from "../redux/services/patientsApi.js";
import {
openModal,
selectPatient,
setSearchText, setSearchText,
setSortOrder, setSortOrder,
setViewMode, setViewMode,
closeModal, setCurrentPage, setPageSize closeModal,
setCurrentPage,
setPageSize
} from "../redux/slices/patientsSlice.js"; } from "../redux/slices/patientsSlice.js";
import PatientListCard from "../components/patients/PatientListCard.jsx"; import PatientListCard from "../components/patients/PatientListCard.jsx";
import PatientFormModal from "../components/patients/PatientFormModal.jsx"; import PatientFormModal from "../components/patients/PatientFormModal.jsx";
import SelectViewMode from "../components/SelectViewMode.jsx"; import SelectViewMode from "../components/SelectViewMode.jsx";
import LoadingIndicator from "../components/LoadingIndicator.jsx"; import LoadingIndicator from "../components/LoadingIndicator.jsx";
import {getCachedInfo} from "../utils/cachedInfoUtils.js"; import usePatients from "../hooks/usePatients.js";
const {Option} = Select; const {Option} = Select;
const {Title} = Typography const {Title} = Typography;
const PatientsPage = () => { const PatientsPage = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const {
patients,
isLoading,
isError,
handleAddPatient,
handleEditPatient,
handleDeletePatient,
handleModalPatientSubmit
} = usePatients();
const { const {
searchText, searchText,
sortOrder, sortOrder,
viewMode, viewMode,
selectedPatient, selectedPatient,
isModalVisible isModalVisible,
currentPage,
pageSize
} = useSelector(state => state.patientsUI); } = useSelector(state => state.patientsUI);
const {data: patients = [], isLoading, isError} = useGetPatientsQuery();
const [addPatient] = useAddPatientMutation();
const [updatePatient] = useUpdatePatientMutation();
const [deletePatient] = useDeletePatientMutation();
useEffect(() => { useEffect(() => {
document.title = "Пациенты" document.title = "Пациенты";
const cachedViewMode = getCachedInfo("viewModePatients");
if (cachedViewMode) dispatch(setViewMode(cachedViewMode));
}, []); }, []);
const handleAddPatient = () => { const filteredPatients = useMemo(() => {
dispatch(selectPatient(null)); return patients
dispatch(openModal()); .filter(patient =>
}; Object.values(patient)
.filter(value => typeof value === "string")
const handleEditPatient = (patient) => { .some(value => value.toLowerCase().includes(searchText.toLowerCase()))
dispatch(selectPatient(patient)); )
dispatch(openModal()); .sort((a, b) => {
}; const fullNameA = `${a.last_name} ${a.first_name}`;
const fullNameB = `${b.last_name} ${b.first_name}`;
const handleDeletePatient = async (patientId) => { return sortOrder === "asc" ? fullNameA.localeCompare(fullNameB) : fullNameB.localeCompare(fullNameA);
try {
await deletePatient(patientId).unwrap()
notification.success({
message: "Пациент удалён",
description: "Пациент успешно удалён из базы.",
placement: "topRight",
}); });
} catch (error) { }, [patients, searchText, sortOrder]);
notification.error({
message: "Ошибка удаления",
description: error.data?.message || "Не удалось удалить пациента",
placement: "topRight",
});
}
};
const handleModalPatientSubmit = async (patientData) => {
try {
if (selectedPatient) {
await updatePatient({id: selectedPatient.id, ...patientData}).unwrap()
notification.success({
message: "Пациент обновлён",
description: `Данные пациента ${patientData.first_name} ${patientData.last_name} успешно обновлены.`,
placement: "topRight",
})
} else {
await addPatient(patientData).unwrap()
notification.success({
message: "Пациент добавлен",
description: `Пациент ${patientData.first_name} ${patientData.last_name} успешно добавлен.`,
placement: "topRight",
});
}
dispatch(closeModal());
} catch (error) {
notification.error({
message: "Ошибка",
description: error.data?.message || "Произошла ошибка при сохранении",
placement: "topRight",
});
}
};
const filteredPatients = patients
.filter(patient => {
const searchLower = searchText.toLowerCase();
return Object.values(patient)
.filter(value => typeof value === "string")
.some(value => value.toLowerCase().includes(searchLower));
})
.sort((a, b) => {
const fullNameA = `${a.last_name} ${a.first_name}`;
const fullNameB = `${b.last_name} ${b.first_name}`;
return sortOrder === "asc"
? fullNameA.localeCompare(fullNameB)
: fullNameB.localeCompare(fullNameA);
});
const viewModes = [
{
value: "tile",
label: "Плитка",
icon: <BuildOutlined style={{marginRight: 8}}/>
},
{
value: "table",
label: "Таблица",
icon: <TableOutlined style={{marginRight: 8}}/>
}
];
const columns = [ const columns = [
{ {
@ -192,7 +122,7 @@ const PatientsPage = () => {
title: "Действия", title: "Действия",
key: "actions", key: "actions",
fixed: 'right', fixed: 'right',
render: (text, record) => ( render: (_, record) => (
<Row gutter={[8, 8]}> <Row gutter={[8, 8]}>
<Col xs={24} xl={12}> <Col xs={24} xl={12}>
<Button block onClick={() => handleEditPatient(record)}>Изменить</Button> <Button block onClick={() => handleEditPatient(record)}>Изменить</Button>
@ -213,70 +143,20 @@ const PatientsPage = () => {
}, },
]; ];
const TableView = () => ( const viewModes = [
<Table {
columns={columns} value: "tile",
dataSource={filteredPatients.map(patient => ({...patient, key: patient.id}))} label: "Плитка",
scroll={{ icon: <BuildOutlined style={{marginRight: 8}}/>
x: "max-content" },
}} {
showSorterTooltip={false} value: "table",
pagination={{ label: "Таблица",
current: 1, icon: <TableOutlined style={{marginRight: 8}}/>
pageSize: 10, }
showSizeChanger: true, ];
pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => {
dispatch(setCurrentPage(page));
dispatch(setPageSize(newPageSize));
},
}}
/>
);
const TileView = () => ( if (isError) return <Result status="error" title="Ошибка" subTitle="Произошла ошибка в работе страницы"/>;
<List
grid={{
gutter: 16,
xs: 1,
sm: 1,
md: 2,
lg: 2,
xl: 3,
xxl: 3,
}}
dataSource={filteredPatients}
renderItem={(patient) => (
<List.Item>
<PatientListCard
patient={patient}
handleEditPatient={handleEditPatient}
handleDeletePatient={handleDeletePatient}
/>
</List.Item>
)}
pagination={{
current: 1,
pageSize: 10,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => {
dispatch(setCurrentPage(page));
dispatch(setPageSize(newPageSize));
},
}}
/>
);
if (isError) {
return (
<Result
status="error"
title="Ошибка"
subTitle="Произошла ошибка в работе страницы"
/>
);
}
return ( return (
<div style={{padding: 20}}> <div style={{padding: 20}}>
@ -320,7 +200,7 @@ const PatientsPage = () => {
}> }>
<SelectViewMode <SelectViewMode
viewMode={viewMode} viewMode={viewMode}
setViewMode={(value) => dispatch(setViewMode(value))} setViewMode={value => dispatch(setViewMode(value))}
localStorageKey={"viewModePatients"} localStorageKey={"viewModePatients"}
toolTipText={"Формат отображения пациентов"} toolTipText={"Формат отображения пациентов"}
viewModes={viewModes} viewModes={viewModes}
@ -328,22 +208,40 @@ const PatientsPage = () => {
</Col> </Col>
</Row> </Row>
{isLoading ? ( {isLoading ? <LoadingIndicator/> : viewMode === "tile" ? (
<LoadingIndicator/> <List
) : viewMode === "tile" ? ( grid={{gutter: 16, column: 3}}
<TileView/> dataSource={filteredPatients}
renderItem={patient => (
<List.Item>
<PatientListCard
patient={patient}
handleEditPatient={handleEditPatient}
handleDeletePatient={handleDeletePatient}
/>
</List.Item>
)}
pagination={{
current: currentPage, pageSize, onChange: (p, s) => {
dispatch(setCurrentPage(p));
dispatch(setPageSize(s));
}
}}
/>
) : ( ) : (
<TableView/> <Table
columns={columns}
dataSource={filteredPatients.map(p => ({...p, key: p.id}))}
pagination={{
current: currentPage, pageSize, onChange: (p, s) => {
dispatch(setCurrentPage(p));
dispatch(setPageSize(s));
}
}}
/>
)} )}
<FloatButton <FloatButton icon={<PlusOutlined/>} type="primary" onClick={handleAddPatient} tooltip="Добавить пациента"/>
icon={<PlusOutlined/>}
type="primary"
style={{position: "fixed", bottom: 40, right: 40}}
onClick={handleAddPatient}
tooltip="Добавить пациента"
/>
<PatientFormModal <PatientFormModal
visible={isModalVisible} visible={isModalVisible}
onCancel={() => dispatch(closeModal())} onCancel={() => dispatch(closeModal())}

View File

@ -16,7 +16,7 @@ export const patientsApi = createApi({
getPatients: builder.query({ getPatients: builder.query({
query: () => '/patients/', query: () => '/patients/',
providesTags: ['Patient'], providesTags: ['Patient'],
refetchOnMountOrArgChange: 60 refetchOnMountOrArgChange: 5
}), }),
addPatient: builder.mutation({ addPatient: builder.mutation({
query: (patient) => ({ query: (patient) => ({

View File

@ -1,4 +1,5 @@
import {createSlice} from '@reduxjs/toolkit' import {createSlice} from '@reduxjs/toolkit'
import {cacheInfo} from "../../utils/cachedInfoUtils.js";
const initialState = { const initialState = {
searchText: '', searchText: '',
@ -22,7 +23,7 @@ const patientsSlice = createSlice({
}, },
setViewMode: (state, action) => { setViewMode: (state, action) => {
state.viewMode = action.payload; state.viewMode = action.payload;
localStorage.setItem('viewModePatients', action.payload); cacheInfo("viewModePatients", action.payload);
}, },
setCurrentPage: (state, action) => { setCurrentPage: (state, action) => {
state.currentPage = action.payload; state.currentPage = action.payload;