feat: Обновление UI и логики форм и страниц

Обновлены компоненты форм, страницы авторизации и другие. Добавлены индикаторы загрузки и улучшены обработка ошибок.
This commit is contained in:
Андрей Дувакин 2025-06-06 19:24:47 +05:00
parent 333a471f9d
commit 6cfa2dcca2
12 changed files with 85 additions and 42 deletions

View File

@ -331,7 +331,7 @@ const AppointmentFormModal = () => {
<Button <Button
style={appointmentFormModalUI.footerButtonStyle} style={appointmentFormModalUI.footerButtonStyle}
onClick={appointmentFormModalUI.handleClickBackButton} onClick={appointmentFormModalUI.handleClickBackButton}
disabled={appointmentFormModalUI.disableBackButton || appointmentFormModalUI.isUploadingFile || appointmentFormModalData.isCreating} disabled={appointmentFormModalUI.disableBackButton || appointmentFormModalUI.isUploadingFile || appointmentFormModalData.isProcessed}
> >
Назад Назад
</Button> </Button>
@ -339,7 +339,7 @@ const AppointmentFormModal = () => {
type="primary" type="primary"
onClick={appointmentFormModalUI.handleClickNextButton} onClick={appointmentFormModalUI.handleClickNextButton}
disabled={appointmentFormModalUI.disableNextButton} disabled={appointmentFormModalUI.disableNextButton}
loading={appointmentFormModalData.isCreating || appointmentFormModalUI.isUploadingFile} loading={appointmentFormModalData.isProcessed || appointmentFormModalUI.isUploadingFile}
> >
{appointmentFormModalUI.nextButtonText} {appointmentFormModalUI.nextButtonText}
</Button> </Button>

View File

@ -1,10 +1,10 @@
import { useGetPatientsQuery } from "../../../Api/patientsApi.js"; import {useGetPatientsQuery} from "../../../Api/patientsApi.js";
import { useGetAppointmentTypesQuery } from "../../../Api/appointmentTypesApi.js"; import {useGetAppointmentTypesQuery} from "../../../Api/appointmentTypesApi.js";
import { import {
useCreateAppointmentMutation, useCreateAppointmentMutation,
useGetByPatientIdQuery, useGetByPatientIdQuery,
} from "../../../Api/appointmentsApi.js"; } from "../../../Api/appointmentsApi.js";
import { useCancelScheduledAppointmentMutation } from "../../../Api/scheduledAppointmentsApi.js"; import {useCancelScheduledAppointmentMutation} from "../../../Api/scheduledAppointmentsApi.js";
const useAppointmentFormModal = () => { const useAppointmentFormModal = () => {
const { const {
@ -18,8 +18,11 @@ const useAppointmentFormModal = () => {
isError: isErrorAppointmentTypes, isError: isErrorAppointmentTypes,
} = useGetAppointmentTypesQuery(undefined); } = useGetAppointmentTypesQuery(undefined);
const [createAppointment, { isLoading: isCreating, isError: isErrorCreating }] = useCreateAppointmentMutation(); const [createAppointment, {isLoading: isCreating, isError: isErrorCreating}] = useCreateAppointmentMutation();
const [cancelAppointment] = useCancelScheduledAppointmentMutation(); const [cancelAppointment, {
isLoading: isCanceling,
isError: isErrorCanceling
}] = useCancelScheduledAppointmentMutation();
return { return {
patients, patients,
@ -27,9 +30,9 @@ const useAppointmentFormModal = () => {
createAppointment, createAppointment,
cancelAppointment, cancelAppointment,
useGetByPatientIdQuery, useGetByPatientIdQuery,
isLoading: isLoadingPatients || isLoadingAppointmentTypes || isCreating, isLoading: isLoadingPatients || isLoadingAppointmentTypes || isCreating || isCanceling,
isError: isErrorPatients || isErrorAppointmentTypes || isErrorCreating, isError: isErrorPatients || isErrorAppointmentTypes || isErrorCreating || isErrorCanceling,
isCreating, isProcessed: isCreating || isCanceling,
}; };
}; };

View File

@ -17,6 +17,7 @@ const PatientFormModal = () => {
open={patientFormModalUI.isModalVisible} open={patientFormModalUI.isModalVisible}
onCancel={patientFormModalUI.handleCancel} onCancel={patientFormModalUI.handleCancel}
onOk={patientFormModalUI.handleOk} onOk={patientFormModalUI.handleOk}
confirmLoading={patientFormModalData.isOperationLoading}
okText={"Сохранить"} okText={"Сохранить"}
cancelText={"Отмена"} cancelText={"Отмена"}
maskClosable={false} maskClosable={false}

View File

@ -7,8 +7,8 @@ import {useAddPatientMutation, useUpdatePatientMutation} from "../../../Api/pati
const usePatientFormModal = () => { const usePatientFormModal = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [addPatient] = useAddPatientMutation(); const [addPatient, { isLoading: isAdding }] = useAddPatientMutation();
const [updatePatient] = useUpdatePatientMutation(); const [updatePatient, { isLoading: isUpdating }] = useUpdatePatientMutation();
const { const {
selectedPatient, selectedPatient,
@ -44,6 +44,7 @@ const usePatientFormModal = () => {
return { return {
handleModalSubmit, handleModalSubmit,
isOperationLoading: isAdding || isUpdating
}; };
}; };

View File

@ -96,6 +96,7 @@ const ScheduledAppointmentFormModal = () => {
disabled={scheduledAppointmentFormModalUI.selectedPatient === null || scheduledAppointmentFormModalUI.selectedDateTime === null || scheduledAppointmentFormModalUI.selectedAppointmentType === null} disabled={scheduledAppointmentFormModalUI.selectedPatient === null || scheduledAppointmentFormModalUI.selectedDateTime === null || scheduledAppointmentFormModalUI.selectedAppointmentType === null}
onClick={scheduledAppointmentFormModalUI.handleCreateScheduledAppointment} onClick={scheduledAppointmentFormModalUI.handleCreateScheduledAppointment}
style={scheduledAppointmentFormModalUI.buttonStyle} style={scheduledAppointmentFormModalUI.buttonStyle}
loading={scheduledAppointmentFormModalData.isProcessing}
> >
Запланировать Запланировать
</Button> </Button>

View File

@ -28,6 +28,7 @@ const useScheduledAppointmentFormModal = () => {
createScheduledAppointment, createScheduledAppointment,
isLoading: isLoadingPatients || isLoadingAppointmentTypes || isCreating, isLoading: isLoadingPatients || isLoadingAppointmentTypes || isCreating,
isError: isErrorPatients || isErrorAppointmentTypes || isErrorCreating, isError: isErrorPatients || isErrorAppointmentTypes || isErrorCreating,
isProcessing: isCreating
}; };
}; };

View File

@ -5,7 +5,7 @@ import useLensForm from "./useLensForm.js";
import useLensFormUI from "./useLensFormUI.js"; import useLensFormUI from "./useLensFormUI.js";
const LensFormModal = ({visible, onCancel, onSubmit, lens}) => { const LensFormModal = ({visible, onCancel, onSubmit, lens, isProcessing}) => {
const lensFormData = useLensForm(); const lensFormData = useLensForm();
const lensFormUI = useLensFormUI(lensFormData.lensTypes, visible, onCancel, onSubmit, lens); const lensFormUI = useLensFormUI(lensFormData.lensTypes, visible, onCancel, onSubmit, lens);
@ -15,6 +15,7 @@ const LensFormModal = ({visible, onCancel, onSubmit, lens}) => {
open={visible} open={visible}
onCancel={lensFormUI.handleCancel} onCancel={lensFormUI.handleCancel}
onOk={lensFormUI.handleOk} onOk={lensFormUI.handleOk}
confirmLoading={isProcessing}
okText={"Сохранить"} okText={"Сохранить"}
cancelText={"Отмена"} cancelText={"Отмена"}
maskClosable={false} maskClosable={false}
@ -156,6 +157,7 @@ LensFormModal.propTypes = {
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
lens: LensPropType, lens: LensPropType,
isProcessing: PropTypes.bool,
} }
export default LensFormModal; export default LensFormModal;

View File

@ -124,7 +124,12 @@ const LensesTab = () => {
okText="Да, удалить" okText="Да, удалить"
cancelText="Отмена" cancelText="Отмена"
> >
<Button danger>Удалить</Button> <Button
loading={lensesData.isProcessing}
danger
>
Удалить
</Button>
</Popconfirm> </Popconfirm>
</div> </div>
), ),
@ -327,6 +332,7 @@ const LensesTab = () => {
onCancel={lensesUI.handleCloseModal} onCancel={lensesUI.handleCloseModal}
onSubmit={lensesData.handleModalSubmit} onSubmit={lensesData.handleModalSubmit}
lens={lensesUI.selectedLens} lens={lensesUI.selectedLens}
isProcessing={lensesData.isProcessing}
/> />
</div> </div>
); );

View File

@ -20,9 +20,9 @@ const useLenses = () => {
pollingInterval: 20000, pollingInterval: 20000,
}); });
const [addLens] = useAddLensMutation(); const [addLens, {isLoading: isAdding}] = useAddLensMutation();
const [updateLens] = useUpdateLensMutation(); const [updateLens, {isLoading: isUpdating}] = useUpdateLensMutation();
const [deleteLens] = useDeleteLensMutation(); const [deleteLens, {isLoading: isDeleting}] = useDeleteLensMutation();
const handleDeleteLens = async (lensId) => { const handleDeleteLens = async (lensId) => {
try { try {
@ -73,6 +73,7 @@ const useLenses = () => {
lenses, lenses,
isLoading, isLoading,
isError, isError,
isProcessing: isAdding || isUpdating || isDeleting,
handleDeleteLens, handleDeleteLens,
handleModalSubmit, handleModalSubmit,
} }

View File

@ -1,31 +1,40 @@
import {Form, Input, Button, Row, Col, Typography} from "antd"; import {Form, Input, Button, Row, Col, Typography, Image, Space} from "antd";
import {useSelector} from "react-redux";
import useLoginPage from "./useLoginPage.js"; import useLoginPage from "./useLoginPage.js";
import useLoginPageUI from "./useLoginPageUI.js"; import useLoginPageUI from "./useLoginPageUI.js";
const {Title} = Typography; const {Title} = Typography;
const LoginPage = () => { const LoginPage = () => {
const {error} = useSelector((state) => state.auth);
const {onFinish, isLoading} = useLoginPage(); const {onFinish, isLoading} = useLoginPage();
const {containerStyle, formContainerStyle, titleStyle, errorStyle, labels} = useLoginPageUI(); const {
containerStyle,
formContainerStyle,
titleStyle,
logoBlockStyle,
logoStyle,
appNameStyle,
labels
} = useLoginPageUI();
return ( return (
<Row justify="center" align="middle" style={containerStyle}> <Row justify="center" align="middle" style={containerStyle}>
<Col xs={24} sm={18} md={12} lg={8} xl={6}> <Col xs={24} sm={18} md={12} lg={8} xl={6}>
<Space direction="horizontal" style={logoBlockStyle} size="large">
<Image
src="/logo_rounded.png"
style={logoStyle}
preview={false}
alt="Visus API Logo"
/>
<Title level={1} style={appNameStyle}>
Visus+
</Title>
</Space>
<div style={formContainerStyle}> <div style={formContainerStyle}>
<Title level={2} style={titleStyle}> <Title level={2} style={titleStyle}>
{labels.title} {labels.title}
</Title> </Title>
{error && (
<div style={errorStyle}>
{labels.errorPrefix}
{error}
</div>
)}
<Form name="login" initialValues={{remember: true}} onFinish={onFinish}> <Form name="login" initialValues={{remember: true}} onFinish={onFinish}>
<Form.Item <Form.Item
name="login" name="login"

View File

@ -13,14 +13,14 @@ const useLoginPage = () => {
const response = await loginUser(loginData).unwrap(); const response = await loginUser(loginData).unwrap();
const token = response.access_token || response.token; const token = response.access_token || response.token;
if (!token) { if (!token) {
throw new Error("Токен не получен от сервера"); throw new Error("Сервер не вернул токен авторизации");
} }
localStorage.setItem("access_token", token); localStorage.setItem("access_token", token);
dispatch(setUser({ token })); dispatch(setUser({ token }));
await dispatch(checkAuth()).unwrap(); await dispatch(checkAuth()).unwrap();
} catch (error) { } catch (error) {
const errorMessage = error?.data?.detail || "Не удалось войти"; const errorMessage = error?.data?.detail || "Не удалось войти. Проверьте логин и пароль.";
console.error(error); console.error(error);
dispatch(setError(errorMessage)); dispatch(setError(errorMessage));
notification.error({ notification.error({

View File

@ -1,13 +1,13 @@
import {useEffect, useRef} from "react"; import { useEffect, useRef } from "react";
import {useNavigate} from "react-router-dom"; import { useNavigate } from "react-router-dom";
import {useSelector} from "react-redux"; import { useSelector } from "react-redux";
import {Grid} from "antd"; import { Grid } from "antd";
const {useBreakpoint} = Grid; const { useBreakpoint } = Grid;
const useLoginPageUI = () => { const useLoginPageUI = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const {user, userData, isLoading} = useSelector((state) => state.auth); const { user, userData, isLoading } = useSelector((state) => state.auth);
const screens = useBreakpoint(); const screens = useBreakpoint();
const hasRedirected = useRef(false); const hasRedirected = useRef(false);
@ -19,15 +19,32 @@ const useLoginPageUI = () => {
padding: screens.xs ? 10 : 20, padding: screens.xs ? 10 : 20,
border: "1px solid #ddd", border: "1px solid #ddd",
borderRadius: 8, borderRadius: 8,
textAlign: "center",
}; };
const titleStyle = { const titleStyle = {
textAlign: "center", textAlign: "center",
marginBottom: 20,
}; };
const errorStyle = { const logoStyle = {
color: "red", width: 80,
marginBottom: 15, marginBottom: 10,
borderRadius: 20,
border: "1px solid #ddd",
};
const appNameStyle = {
textAlign: "center",
color: "#1890ff",
marginBottom: 40,
};
const logoBlockStyle = {
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
}; };
const labels = { const labels = {
@ -37,7 +54,6 @@ const useLoginPageUI = () => {
submitButton: "Войти", submitButton: "Войти",
loginRequired: "Пожалуйста, введите логин", loginRequired: "Пожалуйста, введите логин",
passwordRequired: "Пожалуйста, введите пароль", passwordRequired: "Пожалуйста, введите пароль",
errorPrefix: "Ошибка при входе: ",
}; };
useEffect(() => { useEffect(() => {
@ -52,8 +68,10 @@ const useLoginPageUI = () => {
containerStyle, containerStyle,
formContainerStyle, formContainerStyle,
titleStyle, titleStyle,
errorStyle, logoStyle,
appNameStyle,
labels, labels,
logoBlockStyle,
}; };
}; };