переделал страницу с наборами линз с помощью redux

This commit is contained in:
Андрей Дувакин 2025-03-26 16:55:26 +05:00
parent 94ef12b336
commit 0c0fbd89c0
16 changed files with 417 additions and 191 deletions

View File

@ -191,7 +191,7 @@ LensFormModal.propTypes = {
visible: PropTypes.bool.isRequired,
onCancel: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
lens: LensPropType.isRequired,
lens: LensPropType,
}
export default LensFormModal;

View File

@ -1,7 +1,6 @@
import {useState, useEffect} from "react";
import {Modal, Button, Form, Input, Table, InputNumber, Select, Space, notification} from "antd";
import {PlusOutlined, DeleteOutlined} from "@ant-design/icons";
import axios from "axios";
import getAllLensTypes from "../../api/lens_types/getAllLensTypes.js";
import {useAuth} from "../../AuthContext.jsx";
import PropTypes from "prop-types";
@ -29,7 +28,6 @@ const SetFormModal = ({visible, onCancel, setData, onSubmit}) => {
fetchSetContents();
}, [setData, form]);
const fetchSetContents = async () => {
if (!setData) return;
@ -96,7 +94,8 @@ const SetFormModal = ({visible, onCancel, setData, onSubmit}) => {
const handleSubmit = () => {
form.validateFields().then(values => {
if (!validateContent()) return;
onSubmit({...values}, content);
const sanitizedContent = content.map(({id, ...rest}) => rest);
onSubmit({...values}, sanitizedContent);
});
};
@ -255,7 +254,7 @@ SetFormModal.propTypes = {
visible: PropTypes.bool.isRequired,
onCancel: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
setData: SetPropType.isRequired,
setData: SetPropType,
}
export default SetFormModal;

View File

@ -4,11 +4,9 @@ import {
useDeleteLensMutation,
useGetLensesQuery,
useUpdateLensMutation
} from "../redux/services/lensesApi.js";
import {
closeModal
} from "../redux/slices/lensesSlice.js";
} from "../../redux/services/lensesApi.js";
import {notification} from "antd";
import {closeModal} from "../../redux/slices/lensesSlice.js";
const useLenses = () => {
@ -44,6 +42,8 @@ const useLenses = () => {
};
const handleModalSubmit = async (lensData) => {
dispatch(closeModal());
try {
if (selectedLens) {
await updateLens({id: selectedLens.id, ...lensData}).unwrap();
@ -60,7 +60,6 @@ const useLenses = () => {
placement: "topRight",
});
}
dispatch(closeModal());
} catch (error) {
notification.error({
message: "Ошибка",

View File

@ -5,10 +5,8 @@ import {
useDeletePatientMutation,
useGetPatientsQuery,
useUpdatePatientMutation
} from "../redux/services/patientsApi";
import {
closeModal,
} from "../redux/slices/patientsSlice";
} from "../../redux/services/patientsApi.js";
import {closeModal} from "../../redux/slices/patientsSlice.js";
const usePatients = () => {
const dispatch = useDispatch();
@ -41,6 +39,8 @@ const usePatients = () => {
};
const handleModalSubmit = async (patientData) => {
dispatch(closeModal());
try {
if (selectedPatient) {
await updatePatient({ id: selectedPatient.id, ...patientData }).unwrap();
@ -57,7 +57,6 @@ const usePatients = () => {
placement: "topRight",
});
}
dispatch(closeModal());
} catch (error) {
notification.error({
message: "Ошибка",
@ -76,4 +75,4 @@ const usePatients = () => {
};
};
export default usePatients;
export default usePatients;

View File

@ -0,0 +1,118 @@
import {useDispatch, useSelector} from "react-redux";
import {notification} from "antd";
import {
useAddSetMutation, useAppendLensesFromSetMutation,
useDeleteSetMutation,
useGetSetsQuery,
useUpdateSetMutation
} from "../../redux/services/setsApi.js";
import {closeModal} from "../../redux/slices/setsSlice.js";
import {useAddSetContentMutation, useUpdateSetContentMutation} from "../../redux/services/setContentApi.js";
const useSets = () => {
const dispatch = useDispatch();
const {
selectedSet,
} = useSelector(state => state.setsUI);
const {data: sets = [], isLoading, isError} = useGetSetsQuery({
pollingInterval: 20000,
});
const [addSet] = useAddSetMutation();
const [appendLensFromSet] = useAppendLensesFromSetMutation();
const [updateSet] = useUpdateSetMutation();
const [deleteSet] = useDeleteSetMutation();
const [setContent] = useAddSetContentMutation();
const [updateContent] = useUpdateSetContentMutation();
const handleDeleteSet = async (setId) => {
try {
await deleteSet(setId).unwrap();
notification.success({
message: "Набор удален",
description: "Набор успешно удален.",
placement: "topRight",
});
} catch (error) {
notification.error({
message: "Ошибка удаления",
description: error.data?.message || "Не удалось удалить набор",
placement: "topRight",
});
}
};
const handleAppendSet = async (set) => {
try {
await appendLensFromSet(set.id).unwrap();
notification.success({
message: "Линзы добавлены",
description: "Линзы успешно добавлены.",
placement: "topRight",
});
} catch (error) {
notification.error({
message: "Ошибка добавления",
description: error.data?.message || "Не удалось добавить линзы из набора в общий список",
placement: "topRight",
});
}
};
const handleModalSetSubmit = async (set, content = []) => {
dispatch(closeModal());
try {
let refreshedSet;
if (selectedSet) {
refreshedSet = await updateSet({id: selectedSet.id, ...set}).unwrap();
notification.success({
message: "Набор обновлен",
description: "Набор успешно обновлен.",
placement: "topRight",
});
} else {
refreshedSet = await addSet(set).unwrap();
notification.success({
message: "Набор добавлен",
description: "Набор успешно добавлен.",
placement: "topRight",
});
}
if (refreshedSet && selectedSet) {
await updateContent({setId: refreshedSet.id, setContent: content}).unwrap();
} else if (refreshedSet && !selectedSet) {
await setContent({setId: refreshedSet.id, setContent: content}).unwrap();
} else {
notification.error({
message: "Ошибка",
description: "Не удалось сохранить содержимое набора",
placement: "topRight",
});
}
} catch (error) {
notification.error({
message: "Ошибка добавления",
description: error.data?.message || "Произошла ошибка при сохранении",
placement: "topRight",
});
}
};
return {
sets,
isLoading,
isError,
addSet,
updateSet,
handleDeleteSet,
handleAppendSet,
handleModalSetSubmit,
};
};
export default useSets;

View File

@ -1,5 +1,5 @@
import {useEffect} from "react";
import {getCachedInfo} from "../utils/cachedInfoUtils.js";
import {getCachedInfo} from "../../utils/cachedInfoUtils.js";
import {
closeModal,
openModal,
@ -7,7 +7,7 @@ import {
setSearchParams,
setSearchText, setShowAdvancedSearch,
setViewMode
} from "../redux/slices/lensesSlice.js";
} from "../../redux/slices/lensesSlice.js";
import {useDispatch, useSelector} from "react-redux";
@ -30,6 +30,10 @@ const useLensesUI = (lenses) => {
if (cachedViewMode) dispatch(setViewMode(cachedViewMode));
}, [dispatch]);
const containerStyle = { padding: 20 };
const filterBarStyle = { marginBottom: 20 };
const formItemStyle = { width: "100%" };
const handleSetSearchText = (value) => dispatch(setSearchText(value));
const handleCloseModal = () => dispatch(closeModal());
const handleSetCurrentPage = (page) => dispatch(setCurrentPage(page));
@ -101,6 +105,9 @@ const useLensesUI = (lenses) => {
showAdvancedSearch,
searchParams,
pagination,
containerStyle,
filterBarStyle,
formItemStyle,
filteredLenses: filteredLenses.map(lens => ({...lens, key: lens.id})),
handleSetSearchText,
handleAddLens,

View File

@ -1,6 +1,7 @@
import { useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
closeModal,
openModal,
selectPatient,
setCurrentPage,
@ -8,9 +9,8 @@ import {
setSearchText,
setSortOrder,
setViewMode
} from "../redux/slices/patientsSlice";
import { closeModal } from "../redux/slices/lensesSlice";
import { getCachedInfo } from "../utils/cachedInfoUtils";
} from "../../redux/slices/patientsSlice.js";
import { getCachedInfo } from "../../utils/cachedInfoUtils.js";
const usePatientsUI = (patients) => {
const dispatch = useDispatch();

View File

@ -0,0 +1,81 @@
import {useDispatch, useSelector} from "react-redux";
import {useEffect} from "react";
import {
closeModal,
openModal,
selectSet,
setCurrentPage,
setPageSize,
setSearchText
} from "../../redux/slices/setsSlice.js";
const useSetsUI = (sets) => {
const dispatch = useDispatch();
const {
searchText,
currentPage,
pageSize,
selectedSet,
isModalVisible,
} = useSelector(state => state.setsUI);
useEffect(() => {
document.title = "Наборы линз";
}, [dispatch]);
const containerStyle = { padding: 20 };
const filterBarStyle = { marginBottom: 20 };
const formItemStyle = { width: "100%" };
const handleSetSearchText = (value) => dispatch(setSearchText(value));
const handleCloseModal = () => dispatch(closeModal());
const handleSetCurrentPage = (page) => dispatch(setCurrentPage(page));
const handleSetPageSize = (size) => dispatch(setPageSize(size));
const handleAddSet = () => {
dispatch(selectSet(null));
dispatch(openModal());
};
const handleEditSet = (set) => {
dispatch(selectSet(set));
dispatch(openModal());
};
const handlePaginationChange = (page, pageSize) => {
handleSetCurrentPage(page);
handleSetPageSize(pageSize);
};
const pagination = {
currentPage: currentPage,
pageSize: pageSize,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => {
handlePaginationChange(page, newPageSize);
},
};
const filteredSets = sets.filter(set => set.title.toLowerCase().includes(searchText.toLowerCase()));
return {
searchText,
currentPage,
pageSize,
selectedSet,
isModalVisible,
pagination,
filteredSets,
containerStyle,
filterBarStyle,
formItemStyle,
handleSetSearchText,
handleAddSet,
handleEditSet,
handleCloseModal,
};
};
export default useSetsUI;

View File

@ -22,8 +22,8 @@ import PatientListCard from "../components/patients/PatientListCard.jsx";
import PatientFormModal from "../components/patients/PatientFormModal.jsx";
import SelectViewMode from "../components/SelectViewMode.jsx";
import LoadingIndicator from "../components/LoadingIndicator.jsx";
import usePatients from "../hooks/usePatients.js";
import usePatientsUI from "../hooks/usePatientsUI.js";
import usePatients from "../hooks/data/usePatients.js";
import usePatientsUI from "../hooks/ui/usePatientsUI.js";
const {Option} = Select;
const {Title} = Typography;

View File

@ -26,8 +26,8 @@ import LensCard from "../../components/lenses/LensListCard.jsx";
import LensFormModal from "../../components/lenses/LensFormModal.jsx";
import SelectViewMode from "../../components/SelectViewMode.jsx";
import LoadingIndicator from "../../components/LoadingIndicator.jsx";
import useLenses from "../../hooks/useLenses.js";
import useLensesUI from "../../hooks/useLensesUI.js";
import useLenses from "../../hooks/data/useLenses.js";
import useLensesUI from "../../hooks/ui/useLensesUI.js";
const {Option} = Select;
const {useBreakpoint} = Grid;

View File

@ -1,160 +1,31 @@
import {useAuth} from "../../AuthContext.jsx";
import {useEffect, useState} from "react";
import {FloatButton, Input, List, notification, Row, Typography} from "antd";
import getAllSets from "../../api/sets/getAllSets.js";
import {FloatButton, Input, List, Row, Typography} from "antd";
import {PlusOutlined, SwitcherOutlined} from "@ant-design/icons";
import SetListCard from "../../components/sets/SetListCard.jsx";
import SetFormModal from "../../components/sets/SetFormModal.jsx";
import updateSet from "../../api/sets/updateSet.js";
import addSet from "../../api/sets/addSet.js";
import deleteSet from "../../api/sets/deleteSet.js";
import addSetContent from "../../api/set_content/addSetContent.js";
import updateSetContent from "../../api/set_content/updateSetContent.js";
import appendLensesFromSet from "../../api/sets/appendLensesFromSet.js";
import LoadingIndicator from "../../components/LoadingIndicator.jsx";
import {cacheInfo, getCachedInfo, getCacheTimestamp} from "../../utils/cachedInfoUtils.js";
import useSets from "../../hooks/data/useSets.js";
import useSetsUI from "../../hooks/ui/useSetsUI.js";
const {Title} = Typography;
const SetLensesPage = () => {
const {api} = useAuth();
const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [searchText, setSearchText] = useState("");
const [sets, setSets] = useState([]);
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedSet, setSelectedSet] = useState(null);
useEffect(() => {
fetchSetsWithCache();
}, []);
useEffect(() => {
if (!isModalVisible) {
const intervalId = setInterval(fetchSets, 5000);
return () => clearInterval(intervalId);
}
}, [isModalVisible]);
const fetchSetsWithCache = async () => {
const cachedData = getCachedInfo("setsData");
const cacheTimestamp = getCacheTimestamp("setsData");
if (cachedData && cacheTimestamp && (Date.now() - cacheTimestamp) < 60 * 1000) {
setSets(cachedData);
setLoading(false);
} else {
await fetchSets();
}
};
const fetchSets = async () => {
const data = await getAllSets(api);
setSets(data);
setLoading(false);
cacheInfo("setsData", data);
};
const filteredSets = sets.filter(set => set.title.toLowerCase().includes(searchText.toLowerCase()));
const handleAddSet = () => {
setSelectedSet(null);
setIsModalVisible(true);
};
const handleEditSet = (set) => {
setSelectedSet(set);
setIsModalVisible(true);
};
const handleDeleteSet = async (set_id) => {
await deleteSet(api, set_id);
notification.success({
message: "Набор удален",
description: "Набор успешно удален.",
placement: "topRight",
});
await fetchSets();
};
const handleCancel = () => {
setIsModalVisible(false);
};
const handleAppendSet = async (set) => {
await appendLensesFromSet(api, set.id);
notification.success({
message: "Линзы добавлены",
description: "Линзы успешно добавлены.",
placement: "topRight",
});
};
const handleModalSetSubmit = async (set, content = []) => {
setIsModalVisible(false);
let refreshed_set;
if (selectedSet) {
refreshed_set = await editCurrentSet(set);
} else {
refreshed_set = await addNewSet(set);
}
if (refreshed_set && selectedSet) {
await updateContent(content, refreshed_set.id);
} else if (refreshed_set && !selectedSet) {
await setContent(content, refreshed_set.id);
}
await fetchSets();
};
const setContent = async (content, set_id) => {
await addSetContent(api, content, set_id);
};
const updateContent = async (content, set_id) => {
await updateSetContent(api, content, set_id);
};
const editCurrentSet = async (set) => {
const refreshed_set = await updateSet(api, selectedSet.id, set);
notification.success({
message: "Набор обновлен",
description: "Набор успешно обновлен.",
placement: "topRight",
});
return refreshed_set;
};
const addNewSet = async (set) => {
const refreshed_set = await addSet(api, set);
notification.success({
message: "Набор добавлен",
description: "Набор успешно добавлен.",
placement: "topRight",
});
return refreshed_set;
};
const setsData = useSets();
const setsUI = useSetsUI(setsData.sets);
return (
<div style={{padding: 20}}>
<div style={setsUI.containerStyle}>
<Title level={1}><SwitcherOutlined/> Наборы линз</Title>
<Row style={{marginBottom: 20}}>
<Row style={setsUI.filterBarStyle}>
<Input
placeholder="Поиск набора"
onChange={(e) => setSearchText(e.target.value)}
style={{width: "100%"}}
onChange={(e) => setsUI.handleSetSearchText(e.target.value)}
style={setsUI.formItemStyle}
allowClear
/>
</Row>
{loading ? (
{setsData.isLoading ? (
<LoadingIndicator/>
) : (
<List
@ -167,43 +38,33 @@ const SetLensesPage = () => {
xl: 3,
xxl: 3,
}}
dataSource={filteredSets}
dataSource={setsUI.filteredSets}
renderItem={(set) => (
<List.Item>
<SetListCard
set={set}
handleEditSet={handleEditSet}
handleDeleteSet={handleDeleteSet}
handleAppendSet={handleAppendSet}
handleEditSet={setsUI.handleEditSet}
handleDeleteSet={setsData.handleDeleteSet}
handleAppendSet={setsData.handleAppendSet}
/>
</List.Item>
)}
pagination={{
current,
pageSize,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => {
setCurrent(page);
setPageSize(newPageSize);
},
}}
pagination={setsUI.pagination}
/>
)}
<FloatButton
icon={<PlusOutlined/>}
type="primary"
style={{position: "fixed", bottom: 40, right: 40}}
tooltip="Добавить набор"
onClick={handleAddSet}
onClick={setsUI.handleAddSet}
/>
<SetFormModal
visible={isModalVisible}
onCancel={handleCancel}
onSubmit={handleModalSetSubmit}
setData={selectedSet}
visible={setsUI.isModalVisible}
onCancel={setsUI.handleCloseModal}
onSubmit={setsData.handleModalSetSubmit}
setData={setsUI.selectedSet}
/>
</div>
);

View File

@ -1,4 +1,4 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react'
import CONFIG from "../../core/сonfig.js";
export const patientsApi = createApi({
@ -27,7 +27,7 @@ export const patientsApi = createApi({
invalidatesTags: ['Patient']
}),
updatePatient: builder.mutation({
query: ({ id, ...patient }) => ({
query: ({id, ...patient}) => ({
url: `/patients/${id}/`,
method: 'PUT',
body: patient
@ -48,5 +48,5 @@ export const {
useGetPatientsQuery,
useAddPatientMutation,
useUpdatePatientMutation,
useDeletePatientMutation
} = patientsApi
useDeletePatientMutation,
} = patientsApi;

View File

@ -0,0 +1,43 @@
import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react';
import CONFIG from "../../core/сonfig.js";
export const setContentApi = createApi({
reducerPath: 'setContentApi',
baseQuery: fetchBaseQuery({
baseUrl: CONFIG.BASE_URL,
prepareHeaders: (headers) => {
const token = localStorage.getItem('access_token');
if (token) headers.set('Authorization', `Bearer ${token}`);
return headers;
}
}),
tagTypes: ['SetContent'],
endpoints: (builder) => ({
getSetContent: builder.query({
query: (setId) => `/set_content/${setId}`,
providesTags: ['SetContent'],
}),
addSetContent: builder.mutation({
query: ({setId, setContent}) => ({
url: `/set_content/${setId}/`,
method: 'POST',
body: setContent
}),
invalidatesTags: ['SetContent']
}),
updateSetContent: builder.mutation({
query: ({setId, setContent}) => ({
url: `/set_content/${setId}/`,
method: 'PUT',
body: setContent
}),
invalidatesTags: ['SetContent']
}),
}),
});
export const {
useGetSetContentQuery,
useAddSetContentMutation,
useUpdateSetContentMutation,
} = setContentApi;

View File

@ -0,0 +1,60 @@
import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react'
import CONFIG from "../../core/сonfig.js";
export const setsApi = createApi({
reducerPath: 'setsApi',
baseQuery: fetchBaseQuery({
baseUrl: CONFIG.BASE_URL,
prepareHeaders: (headers) => {
const token = localStorage.getItem('access_token');
if (token) headers.set('Authorization', `Bearer ${token}`);
return headers;
}
}),
tagTypes: ['Set'],
endpoints: (builder) => ({
getSets: builder.query({
query: () => '/sets/',
providesTags: ['Set'],
refetchOnMountOrArgChange: 5
}),
addSet: builder.mutation({
query: (set) => ({
url: '/sets/',
method: 'POST',
body: set
}),
invalidatesTags: ['Set']
}),
updateSet: builder.mutation({
query: ({id, ...set}) => ({
url: `/sets/${id}/`,
method: 'PUT',
body: set
}),
invalidatesTags: ['Set']
}),
deleteSet: builder.mutation({
query: (id) => ({
url: `/sets/${id}/`,
method: 'DELETE'
}),
invalidatesTags: ['Set']
}),
appendLensesFromSet: builder.mutation({
query: (id) => ({
url: `/sets/append_lenses/${id}/`,
method: 'POST',
}),
invalidatesTags: ['Set']
}),
}),
});
export const {
useGetSetsQuery,
useAddSetMutation,
useUpdateSetMutation,
useDeleteSetMutation,
useAppendLensesFromSetMutation,
} = setsApi;

View File

@ -0,0 +1,46 @@
import {createSlice} from '@reduxjs/toolkit'
const initialState = {
searchText: '',
currentPage: 1,
pageSize: 10,
selectedSet: null,
isModalVisible: false,
};
const setsSlice = createSlice({
name: 'setsUI',
initialState,
reducers: {
setSearchText: (state, action) => {
state.searchText = action.payload;
},
setCurrentPage: (state, action) => {
state.currentPage = action.payload;
},
setPageSize: (state, action) => {
state.pageSize = action.payload;
},
openModal: (state) => {
state.isModalVisible = true;
},
closeModal: (state) => {
state.isModalVisible = false;
state.selectedSet = null;
},
selectSet: (state, action) => {
state.selectedSet = action.payload;
}
}
});
export const {
selectSet,
setSearchText,
setCurrentPage,
setPageSize,
openModal,
closeModal
} = setsSlice.actions;
export default setsSlice.reducer;

View File

@ -2,7 +2,10 @@ import {configureStore} from '@reduxjs/toolkit';
import {patientsApi} from './services/patientsApi.js';
import patientsUIReducer from './slices/patientsSlice.js';
import {lensesApi} from './services/lensesApi.js';
import lensesReducer from './slices/lensesSlice.js';
import lensesUIReducer from './slices/lensesSlice.js';
import {setsApi} from "./services/setsApi.js";
import setsUIReducer from './slices/setsSlice.js';
import {setContentApi} from "./services/setContentApi.js";
export const store = configureStore({
reducer: {
@ -10,10 +13,20 @@ export const store = configureStore({
patientsUI: patientsUIReducer,
[lensesApi.reducerPath]: lensesApi.reducer,
lensesUI: lensesReducer,
lensesUI: lensesUIReducer,
[setsApi.reducerPath]: setsApi.reducer,
setsUI: setsUIReducer,
[setContentApi.reducerPath]: setContentApi.reducer,
},
middleware: (getDefaultMiddleware) => (
getDefaultMiddleware().concat(patientsApi.middleware, lensesApi.middleware)
getDefaultMiddleware().concat(
patientsApi.middleware,
lensesApi.middleware,
setsApi.middleware,
setContentApi.middleware,
)
)
});