изменил архитектуру работы с api

This commit is contained in:
Андрей Дувакин 2025-03-17 15:19:04 +05:00
parent 3fc30f8e59
commit 2372083afe
69 changed files with 1371 additions and 1806 deletions

View File

@ -20,21 +20,21 @@ def start_app():
api_app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_origins=['http://localhost:5173'],
allow_credentials=True,
allow_methods=['GET', 'POST', 'PUT', 'DELETE'],
allow_methods=['*'],
allow_headers=['*'],
)
api_app.include_router(appointments_types_router, prefix=f'{settings.APP_PREFIX}/appointment_types', tags=['appointment_types'])
api_app.include_router(appointment_router, prefix=f'{settings.APP_PREFIX}/appointments', tags=['appointments'])
api_app.include_router(auth_router, prefix=settings.APP_PREFIX, tags=['auth'])
api_app.include_router(lens_issues_router, prefix=f'{settings.APP_PREFIX}/lens_issue', tags=['lens_issue'])
api_app.include_router(lens_issues_router, prefix=f'{settings.APP_PREFIX}/lens_issues', tags=['lens_issue'])
api_app.include_router(lens_types_router, prefix=f'{settings.APP_PREFIX}/lens_types', tags=['lens_types'])
api_app.include_router(lenses_router, prefix=f'{settings.APP_PREFIX}/lenses', tags=['lenses'])
api_app.include_router(patients_router, prefix=f'{settings.APP_PREFIX}/patients', tags=['patients'])
api_app.include_router(register_router, prefix=f'{settings.APP_PREFIX}/register', tags=['register'])
api_app.include_router(scheduled_appointments_router, prefix=f'{settings.APP_PREFIX}/scheduled_appointments_router', tags=['scheduled_appointments_router'])
api_app.include_router(scheduled_appointments_router, prefix=f'{settings.APP_PREFIX}/scheduled_appointments', tags=['scheduled_appointments'])
api_app.include_router(set_content_router, prefix=f'{settings.APP_PREFIX}/set_content', tags=['set_content'])
api_app.include_router(sets_router, prefix=f'{settings.APP_PREFIX}/sets', tags=['sets'])

1446
web-app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
"@react-buddy/ide-toolbox": "^2.4.0",
"@react-buddy/palette-antd": "^5.3.0",
"antd": "^5.23.1",
"antd-dayjs-webpack-plugin": "^1.0.6",
"antd-mask-input": "^2.0.7",
"axios": "^1.7.9",
"dayjs": "^1.11.13",

View File

@ -4,11 +4,11 @@ import {AuthProvider} from "./AuthContext.jsx";
import "/src/styles/app.css";
const App = () => (
<AuthProvider>
<Router>
<Router>
<AuthProvider>
<AppRouter/>
</Router>
</AuthProvider>
</AuthProvider>
</Router>
);
export default App
export default App;

View File

@ -24,6 +24,6 @@ const AppRouter = () => (
</Route>
<Route path={"*"} element={<Navigate to={"/"}/>}/>
</Routes>
)
);
export default AppRouter;

View File

@ -1,13 +1,16 @@
import {createContext, useState, useContext, useEffect} from "react";
import PropTypes from "prop-types";
import loginUser from "./api/auth/LoginRequest.jsx";
import loginUser from "./api/auth/loginRequest.jsx";
import {Spin} from "antd";
import {useNavigate} from "react-router-dom";
import createApi from "./core/axiosConfig.jsx";
const AuthContext = createContext(undefined);
export const AuthProvider = ({children}) => {
const AuthProvider = ({children}) => {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
const token = localStorage.getItem("access_token");
@ -17,9 +20,10 @@ export const AuthProvider = ({children}) => {
setIsLoading(false);
}, []);
const login = async (loginData) => {
try {
const token = await loginUser(loginData);
const token = await loginUser(loginData, api);
localStorage.setItem("access_token", token);
setUser({token});
} catch (error) {
@ -28,6 +32,12 @@ export const AuthProvider = ({children}) => {
}
};
const logoutAndRedirect = () => {
localStorage.removeItem("access_token");
setUser(null);
navigate("/login");
};
const logout = () => {
localStorage.removeItem("access_token");
setUser(null);
@ -37,8 +47,10 @@ export const AuthProvider = ({children}) => {
return <Spin/>;
}
const api = createApi(logoutAndRedirect);
return (
<AuthContext.Provider value={{user, login, logout}}>
<AuthContext.Provider value={{user, login, logout, logoutAndRedirect, api}}>
{children}
</AuthContext.Provider>
);
@ -48,6 +60,8 @@ AuthProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export const useAuth = () => {
const useAuth = () => {
return useContext(AuthContext);
};
export {useAuth, AuthProvider};

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const getAllAppointments = async (token) => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/appointments/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь неайден или токен недействителен");
}
throw new Error(error.message);
}
};
export default getAllAppointments;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const getAllAppointments = async (api) => {
const response = await api.get(`${CONFIG.BASE_URL}/appointments/`);
return response.data;
};
export default getAllAppointments;

View File

@ -1,19 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const loginUser = async (loginData) => {
try {
const response = await axios.post(`${CONFIG.BASE_URL}/login/`, loginData, {
withCredentials: true,
});
return response.data.access_token;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Неверное имя пользователя или пароль")
}
throw new Error(error.message);
}
};
export default loginUser;

View File

@ -0,0 +1,14 @@
import CONFIG from "../../core/сonfig.jsx";
const loginUser = async (loginData, api) => {
const response = await api.post(`${CONFIG.BASE_URL}/login/`, loginData, {
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
});
return response.data.access_token;
};
export default loginUser;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const AddLensIssue = async (token, lens_issue) => {
try {
const response = await axios.post(`${CONFIG.BASE_URL}/lens_issues/`, lens_issue, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь неайден или токен недействителен");
}
throw new Error(error.message);
}
};
export default AddLensIssue;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const GetAllLensIssues = async (token) => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/lens_issues/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default GetAllLensIssues;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const AddLensIssue = async (api, lens_issue) => {
const response = await api.post(`${CONFIG.BASE_URL}/lens_issues/`, lens_issue);
return response.data;
};
export default AddLensIssue;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const GetAllLensIssues = async (api) => {
const response = await api.get(`${CONFIG.BASE_URL}/lens_issues/`);
return response.data;
};
export default GetAllLensIssues;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const getAllLensTypes = async (token) => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/lens_types/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default getAllLensTypes;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const getAllLensTypes = async (api) => {
const response = await api.get(`${CONFIG.BASE_URL}/lens_types/`);
return response.data;
};
export default getAllLensTypes;

View File

@ -1,21 +0,0 @@
import CONFIG from "../../core/Config.jsx";
import axios from "axios";
const addLens = async (token, lens) => {
try {
const response = await axios.post(`${CONFIG.BASE_URL}/lenses/`, lens, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default addLens;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const deleteLens = async (token, lens_id) => {
try {
const response = await axios.delete(`${CONFIG.BASE_URL}/lenses/${lens_id}/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default deleteLens;

View File

@ -1,20 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const getAllLenses = async (token) => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/lenses/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw Error("Ошибка авторизации: пользователь неяден или токен недействителен");
}
throw Error(error.message);
}
};
export default getAllLenses;

View File

@ -1,20 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const getNotIssuedLenses = async (token) => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/lenses/not_issued/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw Error("Ошибка авторизации: пользователь неяден или токен недействителен");
}
throw Error(error.message);
}
};
export default getNotIssuedLenses;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const updateLens = async (token, lensId, lensData) => {
try {
const response = await axios.put(`${CONFIG.BASE_URL}/lenses/${lensId}/`, lensData, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default updateLens;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const addLens = async (api, lens) => {
const response = await api.post(`${CONFIG.BASE_URL}/lenses/`, lens);
return response.data;
};
export default addLens;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const deleteLens = async (api, lens_id) => {
const response = await api.delete(`${CONFIG.BASE_URL}/lenses/${lens_id}/`);
return response.data;
};
export default deleteLens;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const getAllLenses = async (api) => {
const response = await api.get(`${CONFIG.BASE_URL}/lenses/`);
return response.data;
};
export default getAllLenses;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const getNotIssuedLenses = async (api) => {
const response = await api.get(`${CONFIG.BASE_URL}/lenses/not_issued/`);
return response.data;
};
export default getNotIssuedLenses;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const updateLens = async (api, lensId, lensData) => {
const response = await api.put(`${CONFIG.BASE_URL}/lenses/${lensId}/`, lensData);
return response.data;
};
export default updateLens;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const addPatient = async (token, patient) => {
try {
const response = await axios.post(`${CONFIG.BASE_URL}/patients/`, patient, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default addPatient;

View File

@ -1,22 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const deletePatient = async (token, patient_id) => {
try {
const response = await axios.delete(`${CONFIG.BASE_URL}/patients/${patient_id}/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
}
export default deletePatient;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const getAllPatients = async (token) => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/patients/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default getAllPatients;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const updatePatient = async (token, patientId, patientData) => {
try {
const response = await axios.put(`${CONFIG.BASE_URL}/patients/${patientId}/`, patientData, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
}
export default updatePatient;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const addPatient = async (api, patient) => {
const response = await api.post(`${CONFIG.BASE_URL}/patients/`, patient);
return response.data;
};
export default addPatient;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const deletePatient = async (api, patient_id) => {
const response = await api.delete(`${CONFIG.BASE_URL}/patients/${patient_id}/`);
return response.data;
};
export default deletePatient;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const getAllPatients = async (api) => {
const response = await api.get(`${CONFIG.BASE_URL}/patients/`);
return response.data;
};
export default getAllPatients;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const updatePatient = async (api, patientId, patientData) => {
const response = await api.put(`${CONFIG.BASE_URL}/patients/${patientId}/`, patientData);
return response.data;
};
export default updatePatient;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const getAllScheduledAppointments = async (api) => {
const response = await api.get(`${CONFIG.BASE_URL}/scheduled_appointments/`);
return response.data;
};
export default getAllScheduledAppointments;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const addSetContent = async (token, set_content, set_id) => {
try {
const response = await axios.post(`${CONFIG.BASE_URL}/set_content/${set_id}/`, set_content, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default addSetContent;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const getSetContentBySetId = async (token, set_id) => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/set_content/${set_id}/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default getSetContentBySetId;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const updateSetContent = async (token, set_content, set_id) => {
try {
const response = await axios.put(`${CONFIG.BASE_URL}/set_content/${set_id}/`, set_content, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default updateSetContent;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const addSetContent = async (api, set_content, set_id) => {
const response = await api.post(`${CONFIG.BASE_URL}/set_content/${set_id}/`, set_content);
return response.data;
};
export default addSetContent;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const getSetContentBySetId = async (api, set_id) => {
const response = await api.get(`${CONFIG.BASE_URL}/set_content/${set_id}/`);
return response.data;
};
export default getSetContentBySetId;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const updateSetContent = async (api, set_content, set_id) => {
const response = await api.put(`${CONFIG.BASE_URL}/set_content/${set_id}/`, set_content);
return response.data;
};
export default updateSetContent;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const addSet = async (token, set) => {
try {
const response = await axios.post(`${CONFIG.BASE_URL}/sets/`, set, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default addSet;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const appendLensesFromSet = async (token, set_id) => {
try {
const response = await axios.post(`${CONFIG.BASE_URL}/sets/append_lenses/${set_id}/`, {}, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default appendLensesFromSet;

View File

@ -1,20 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const deleteSet = async (token, set_id) => {
try {
const response = await axios.delete(`${CONFIG.BASE_URL}/sets/${set_id}/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default deleteSet;

View File

@ -1,20 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const getAllSets = async (token) => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/sets/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default getAllSets;

View File

@ -1,21 +0,0 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const updateSet = async (token, set_id, set) => {
try {
const response = await axios.put(`${CONFIG.BASE_URL}/sets/${set_id}/`, set, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.status === 403 || error.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default updateSet;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const addSet = async (api, set) => {
const response = await api.post(`${CONFIG.BASE_URL}/sets/`, set);
return response.data;
};
export default addSet;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const appendLensesFromSet = async (api, set_id) => {
const response = await api.post(`${CONFIG.BASE_URL}/sets/append_lenses/${set_id}/`);
return response.data;
};
export default appendLensesFromSet;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const deleteSet = async (api, set_id) => {
const response = await api.delete(`${CONFIG.BASE_URL}/sets/${set_id}/`);
return response.data;
};
export default deleteSet;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const getAllSets = async (api) => {
const response = await api.get(`${CONFIG.BASE_URL}/sets/`);
return response.data;
};
export default getAllSets;

View File

@ -0,0 +1,8 @@
import CONFIG from "../../core/сonfig.jsx";
const updateSet = async (api, set_id, set) => {
const response = await api.put(`${CONFIG.BASE_URL}/sets/${set_id}/`, set);
return response.data;
};
export default updateSet;

View File

@ -0,0 +1,18 @@
import {Spin} from "antd";
import {LoadingOutlined} from "@ant-design/icons";
const LoadingIndicator = () => {
return (
<div style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
}}>
<Spin indicator={<LoadingOutlined style={{fontSize: 64, color: "#1890ff"}} spin/>}/>
</div>
)
};
export default LoadingIndicator;

View File

@ -11,4 +11,5 @@ const PrivateRoute = () => {
return <Outlet/>;
};
export default PrivateRoute;

View File

@ -1,6 +1,7 @@
import {BuildOutlined, TableOutlined} from "@ant-design/icons";
import {Select, Tooltip} from "antd";
import PropTypes from "prop-types";
import {cacheInfo} from "../utils/cachedInfoUtils.jsx";
const {Option} = Select;
@ -13,7 +14,7 @@ const SelectViewMode = ({viewMode, setViewMode, localStorageKey, toolTipText, vi
value={viewMode}
onChange={value => {
setViewMode(value);
localStorage.setItem(localStorageKey, value);
cacheInfo(localStorageKey, value);
}}
style={{width: "100%"}}
>

View File

@ -3,15 +3,15 @@ import {
Modal, Input, Button, notification, Typography, Collapse, Steps, Row, Alert, Col, DatePicker, Spin, Grid
} from "antd";
import PropTypes from "prop-types";
import getAllPatients from "../../api/patients/GetAllPatients.jsx";
import getAllPatients from "../../api/patients/getAllPatients.jsx";
import {useAuth} from "../../AuthContext.jsx";
import dayjs from "dayjs";
import getNotIssuedLenses from "../../api/lenses/GetNotIssuedLenses.jsx";
import getNotIssuedLenses from "../../api/lenses/getNotIssuedLenses.jsx";
const {useBreakpoint} = Grid;
const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
const {user} = useAuth();
const {api} = useAuth();
const screens = useBreakpoint();
const [patients, setPatients] = useState([]);
@ -37,7 +37,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
const fetchPatients = async () => {
try {
const data = await getAllPatients(user.token);
const data = await getAllPatients(api);
setPatients(data);
} catch (error) {
console.error(error);
@ -49,7 +49,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
const fetchLenses = async () => {
try {
const data = await getNotIssuedLenses(user.token);
const data = await getNotIssuedLenses(api);
setLenses(data);
} catch (error) {
console.error(error);
@ -83,17 +83,17 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
});
const patientsItems = filteredPatients.map((patient) => ({
key: patient.id,
label: `${patient.last_name} ${patient.first_name} (${new Date(patient.birthday).toLocaleDateString("ru-RU")})`,
children: <div>
<p><b>Пациент:</b> {patient.last_name} {patient.first_name}</p>
<p><b>Дата рождения:</b> {new Date(patient.birthday).toLocaleDateString("ru-RU")}</p>
<p><b>Диагноз:</b> {patient.diagnosis}</p>
<p><b>Email:</b> {patient.email}</p>
<p><b>Телефон:</b> {patient.phone}</p>
<Button type="primary" onClick={() => setSelectedPatient(patient)}>Выбрать</Button>
</div>,
}));
key: patient.id,
label: `${patient.last_name} ${patient.first_name} (${new Date(patient.birthday).toLocaleDateString("ru-RU")})`,
children: <div>
<p><b>Пациент:</b> {patient.last_name} {patient.first_name}</p>
<p><b>Дата рождения:</b> {new Date(patient.birthday).toLocaleDateString("ru-RU")}</p>
<p><b>Диагноз:</b> {patient.diagnosis}</p>
<p><b>Email:</b> {patient.email}</p>
<p><b>Телефон:</b> {patient.phone}</p>
<Button type="primary" onClick={() => setSelectedPatient(patient)}>Выбрать</Button>
</div>,
}));
const filteredLenses = lenses.filter((lens) => {
const searchLower = searchLensString.toLowerCase();
@ -104,19 +104,19 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
})
const lensesItems = filteredLenses.map((lens) => ({
key: lens.id, label: `Линза ${lens.side} ${lens.diameter} мм`, children: <div>
<p><b>Диаметр:</b> {lens.diameter}</p>
<p><b>Тор:</b> {lens.tor}</p>
<p><b>Пресетная рефракция:</b> {lens.preset_refraction}</p>
<p><b>Диаметр:</b> {lens.diameter}</p>
<p><b>FVC:</b> {lens.fvc}</p>
<p><b>Острота зрения (Trial):</b> {lens.trial}</p>
<p><b>Периферийная торичность:</b> {lens.periphery_toricity}</p>
<p><b>Сторона:</b> {lens.side}</p>
<p><b>Esa:</b> {lens.esa}</p>
<Button type="primary" onClick={() => setSelectedLens(lens)}>Выбрать</Button>
</div>,
}));
key: lens.id, label: `Линза ${lens.side} ${lens.diameter} мм`, children: <div>
<p><b>Диаметр:</b> {lens.diameter}</p>
<p><b>Тор:</b> {lens.tor}</p>
<p><b>Пресетная рефракция:</b> {lens.preset_refraction}</p>
<p><b>Диаметр:</b> {lens.diameter}</p>
<p><b>FVC:</b> {lens.fvc}</p>
<p><b>Острота зрения (Trial):</b> {lens.trial}</p>
<p><b>Периферийная торичность:</b> {lens.periphery_toricity}</p>
<p><b>Сторона:</b> {lens.side}</p>
<p><b>Esa:</b> {lens.esa}</p>
<Button type="primary" onClick={() => setSelectedLens(lens)}>Выбрать</Button>
</div>,
}));
const SelectPatientStep = () => {
return selectedPatient ? (
@ -135,24 +135,96 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
Выбрать другого пациента
</Button>
</div>) : (<>
<Input
placeholder="Поиск пациента"
value={searchPatientString}
onChange={(e) => setSearchPatientString(e.target.value)}
style={{marginBottom: 16}}
allowClear
/>
<Input
placeholder="Поиск пациента"
value={searchPatientString}
onChange={(e) => setSearchPatientString(e.target.value)}
style={{marginBottom: 16}}
allowClear
/>
<div style={{maxHeight: 300, overflowY: "auto", border: "1px solid #d9d9d9", padding: 8}}>
<Collapse
items={patientsItems}
/>
</div>
</>)
<div style={{maxHeight: 300, overflowY: "auto", border: "1px solid #d9d9d9", padding: 8}}>
<Collapse
items={patientsItems}
/>
</div>
</>)
};
const SelectLensStep = () => {
return selectedLens ? (<div style={{padding: "10px", background: "#f5f5f5", borderRadius: 5, marginBottom: 15}}>
<Typography.Text strong>
{selectedLens.diameter} {selectedLens.tor} {selectedLens.preset_refraction}
</Typography.Text>
<p><b>Диаметр:</b> {selectedLens.diameter}</p>
<p><b>Тор:</b> {selectedLens.tor}</p>
<p><b>Пресетная рефракция:</b> {selectedLens.preset_refraction}</p>
<p><b>Диаметр:</b> {selectedLens.diameter}</p>
<p><b>FVC:</b> {selectedLens.fvc}</p>
<p><b>Острота зрения (Trial):</b> {selectedLens.trial}</p>
<p><b>Периферийная торичность:</b> {selectedLens.periphery_toricity}</p>
<p><b>Сторона:</b> {selectedLens.side}</p>
<p><b>Esa:</b> {selectedLens.esa}</p>
<Button
type="primary"
onClick={() => setSelectedLens(null)}
danger
>
Выбрать другую линзу
</Button>
</div>) : (<>
<Input
placeholder="Поиск линз"
value={searchLensString}
onChange={(e) => setSearchLensString(e.target.value)}
style={{marginBottom: 16}}
allowClear
/>
<div style={{maxHeight: 300, overflowY: "auto", border: "1px solid #d9d9d9", padding: 8}}>
<Collapse
items={lensesItems}
/>
</div>
</>)
};
const ConfirmStep = () => {
return (<>
<Alert
type="warning"
message="Внимание! После подтверждения линза будет считаться выданной, данное действие нельзя будет отменить."
style={{marginBottom: 15}}
/>
<Row style={{padding: "10px", background: "#f5f5f5", borderRadius: 5, marginBottom: 15}}
gutter={[16, 16]}>
<Col xs={24} md={16} style={{display: "flex", alignItems: "center"}}>
<Typography.Text strong>
Дата выдачи: {issueDate?.toDate().toLocaleDateString("ru-RU")}
</Typography.Text>
</Col>
<Col xs={24} md={8}>
<DatePicker
value={issueDate}
onChange={(date) => setIssueDate(date)}
format="DD.MM.YYYY"
style={{width: "100%"}}
/>
</Col>
</Row>
<div style={{padding: "10px", background: "#f5f5f5", borderRadius: 5, marginBottom: 15}}>
<Typography.Text strong>
{selectedPatient.last_name} {selectedPatient.first_name}
</Typography.Text>
<p><b>Дата рождения:</b> {new Date(selectedPatient.birthday).toLocaleDateString("ru-RU")}</p>
<p><b>Email:</b> {selectedPatient.email}</p>
<p><b>Телефон:</b> {selectedPatient.phone}</p>
<p><b>Линза:</b> {selectedLens.diameter} {selectedLens.tor} {selectedLens.preset_refraction}</p>
</div>
<div style={{padding: "10px", background: "#f5f5f5", borderRadius: 5, marginBottom: 15}}>
<Typography.Text strong>
{selectedLens.diameter} {selectedLens.tor} {selectedLens.preset_refraction}
</Typography.Text>
@ -165,80 +237,8 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
<p><b>Периферийная торичность:</b> {selectedLens.periphery_toricity}</p>
<p><b>Сторона:</b> {selectedLens.side}</p>
<p><b>Esa:</b> {selectedLens.esa}</p>
<Button
type="primary"
onClick={() => setSelectedLens(null)}
danger
>
Выбрать другую линзу
</Button>
</div>) : (<>
<Input
placeholder="Поиск линз"
value={searchLensString}
onChange={(e) => setSearchLensString(e.target.value)}
style={{marginBottom: 16}}
allowClear
/>
<div style={{maxHeight: 300, overflowY: "auto", border: "1px solid #d9d9d9", padding: 8}}>
<Collapse
items={lensesItems}
/>
</div>
</>)
};
const ConfirmStep = () => {
return (<>
<Alert
type="warning"
message="Внимание! После подтверждения линза будет считаться выданной, данное действие нельзя будет отменить."
style={{marginBottom: 15}}
/>
<Row style={{padding: "10px", background: "#f5f5f5", borderRadius: 5, marginBottom: 15}}
gutter={[16, 16]}>
<Col xs={24} md={16} style={{display: "flex", alignItems: "center"}}>
<Typography.Text strong>
Дата выдачи: {issueDate?.toDate().toLocaleDateString("ru-RU")}
</Typography.Text>
</Col>
<Col xs={24} md={8}>
<DatePicker
value={issueDate}
onChange={(date) => setIssueDate(date)}
format="DD.MM.YYYY"
style={{width: "100%"}}
/>
</Col>
</Row>
<div style={{padding: "10px", background: "#f5f5f5", borderRadius: 5, marginBottom: 15}}>
<Typography.Text strong>
{selectedPatient.last_name} {selectedPatient.first_name}
</Typography.Text>
<p><b>Дата рождения:</b> {new Date(selectedPatient.birthday).toLocaleDateString("ru-RU")}</p>
<p><b>Email:</b> {selectedPatient.email}</p>
<p><b>Телефон:</b> {selectedPatient.phone}</p>
<p><b>Линза:</b> {selectedLens.diameter} {selectedLens.tor} {selectedLens.preset_refraction}</p>
</div>
<div style={{padding: "10px", background: "#f5f5f5", borderRadius: 5, marginBottom: 15}}>
<Typography.Text strong>
{selectedLens.diameter} {selectedLens.tor} {selectedLens.preset_refraction}
</Typography.Text>
<p><b>Диаметр:</b> {selectedLens.diameter}</p>
<p><b>Тор:</b> {selectedLens.tor}</p>
<p><b>Пресетная рефракция:</b> {selectedLens.preset_refraction}</p>
<p><b>Диаметр:</b> {selectedLens.diameter}</p>
<p><b>FVC:</b> {selectedLens.fvc}</p>
<p><b>Острота зрения (Trial):</b> {selectedLens.trial}</p>
<p><b>Периферийная торичность:</b> {selectedLens.periphery_toricity}</p>
<p><b>Сторона:</b> {selectedLens.side}</p>
<p><b>Esa:</b> {selectedLens.esa}</p>
</div>
</>);
</div>
</>);
};
const steps = [{
@ -266,61 +266,61 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
};
return (<Modal
title="Выдача линзы пациенту"
open={visible}
onCancel={() => {
setSelectedPatient(null);
setSelectedLens(null);
setCurrentStep(0);
setIssueDate(dayjs(new Date()));
onCancel();
}}
footer={null}
maskClosable={false}
width={!screens.xs ? 700 : "90%"}
centered
title="Выдача линзы пациенту"
open={visible}
onCancel={() => {
setSelectedPatient(null);
setSelectedLens(null);
setCurrentStep(0);
setIssueDate(dayjs(new Date()));
onCancel();
}}
footer={null}
maskClosable={false}
width={!screens.xs ? 700 : "90%"}
centered
>
{loading ? (<div style={{display: "flex", justifyContent: "center", alignItems: "center", height: "70vh"}}>
<Spin size="large"/>
</div>) : (<div style={{maxHeight: "70vh", overflowY: "auto", padding: "10px"}}>
{steps[currentStep].content}
</div>)}
{!screens.xs && (<Steps
current={currentStep}
items={steps}
style={{marginTop: 16}}
direction={!screens.xs ? "horizontal" : "vertical"}
/>)}
<Row
justify="end"
style={{marginTop: 20}}
gutter={[8, 8]}
>
{loading ? (<div style={{display: "flex", justifyContent: "center", alignItems: "center", height: "70vh"}}>
<Spin size="large"/>
</div>) : (<div style={{maxHeight: "70vh", overflowY: "auto", padding: "10px"}}>
{steps[currentStep].content}
</div>)}
{!screens.xs && (<Steps
current={currentStep}
items={steps}
style={{marginTop: 16}}
direction={!screens.xs ? "horizontal" : "vertical"}
/>)}
<Row
justify="end"
style={{marginTop: 20}}
gutter={[8, 8]}
<Button
style={{marginRight: 8}}
onClick={() => setCurrentStep(currentStep - 1)}
disabled={!isActivePrevButton()}
>
<Button
style={{marginRight: 8}}
onClick={() => setCurrentStep(currentStep - 1)}
disabled={!isActivePrevButton()}
>
Назад
</Button>
<Button
type="primary"
onClick={async () => {
if (isActiveFinishButton()) {
await handleOk();
} else {
setCurrentStep(currentStep + 1);
}
}}
disabled={!isActiveNextButton() && !isActiveFinishButton()}
>
{isActiveFinishButton() ? "Завершить" : "Далее"}
</Button>
</Row>
</Modal>);
Назад
</Button>
<Button
type="primary"
onClick={async () => {
if (isActiveFinishButton()) {
await handleOk();
} else {
setCurrentStep(currentStep + 1);
}
}}
disabled={!isActiveNextButton() && !isActiveFinishButton()}
>
{isActiveFinishButton() ? "Завершить" : "Далее"}
</Button>
</Row>
</Modal>);
};
LensIssueFormModal.propTypes = {

View File

@ -1,12 +1,12 @@
import {Col, Form, InputNumber, Modal, notification, Row, Select} from "antd";
import {useEffect, useState} from "react";
import PropTypes from "prop-types";
import getAllLensTypes from "../../api/lens_types/GetAllLensTypes.jsx";
import getAllLensTypes from "../../api/lens_types/getAllLensTypes.jsx";
import {useAuth} from "../../AuthContext.jsx";
const LensFormModal = ({visible, onCancel, onSubmit, lens}) => {
const {user} = useAuth();
const {api} = useAuth();
const [form] = Form.useForm();
const [lensTypes, setLensTypes] = useState([]);
@ -27,7 +27,7 @@ const LensFormModal = ({visible, onCancel, onSubmit, lens}) => {
const fetchLensTypes = async () => {
try {
const data = await getAllLensTypes(user.token);
const data = await getAllLensTypes(api);
setLensTypes(data);
} catch (error) {
console.log(error);

View File

@ -2,15 +2,15 @@ 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.jsx";
import getAllLensTypes from "../../api/lens_types/getAllLensTypes.jsx";
import {useAuth} from "../../AuthContext.jsx";
import PropTypes from "prop-types";
import getSetContentBySetId from "../../api/set_content/GetSetContentBySetId.jsx";
import getSetContentBySetId from "../../api/set_content/getSetContentBySetId.jsx";
const {Option} = Select;
const SetFormModal = ({visible, onCancel, setData, onSubmit}) => {
const {user} = useAuth();
const {api} = useAuth();
const [form] = Form.useForm();
const [content, setContent] = useState([]);
@ -33,7 +33,7 @@ const SetFormModal = ({visible, onCancel, setData, onSubmit}) => {
if (!setData) return;
try {
const data = await getSetContentBySetId(user.token, setData.id);
const data = await getSetContentBySetId(api, setData.id);
setContent(data);
} catch (error) {
console.log(error);
@ -47,7 +47,7 @@ const SetFormModal = ({visible, onCancel, setData, onSubmit}) => {
const fetchLensTypes = async () => {
try {
const data = await getAllLensTypes(user.token);
const data = await getAllLensTypes(api);
setLensTypes(data);
} catch (error) {
console.log(error);
@ -278,5 +278,4 @@ SetFormModal.propTypes = {
}),
}
export default SetFormModal;

View File

@ -7,7 +7,6 @@ const SetListCard = ({set, handleEditSet, handleDeleteSet, handleAppendSet}) =>
handleDeleteSet(set.id);
};
const appendSet = () => {
handleAppendSet(set);
};

View File

@ -0,0 +1,58 @@
import axios from "axios";
import CONFIG from "./сonfig.jsx";
import {notification} from "antd";
const createApi = (logoutAndRedirect) => {
const api = axios.create({
baseURL: CONFIG.BASE_URL,
});
api.interceptors.request.use(config => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response) {
const {status} = error.response;
if (status === 401 || status === 403) {
notification.error({
message: "Ошибка авторизации",
description: "Пользователь не найден или токен недействителен",
});
logoutAndRedirect();
} else {
notification.error({
message: "Ошибка API",
description: error.response.data.message || "Что-то пошло не так",
});
}
} else if (error.request) {
notification.error({
message: "Ошибка соединения",
description: "Сервер не отвечает. Проверьте соединение с сетью.",
});
} else {
notification.error({
message: "Неизвестная ошибка",
description: "Проверьте соединение с сетью.",
});
}
return Promise.reject(error);
}
);
return api;
};
export default createApi;

View File

@ -1,33 +1,181 @@
import {useAuth} from "../AuthContext.jsx";
import {useEffect, useState} from "react";
import getAllAppointments from "../api/appointments/GetAllAppointments.jsx";
import {notification, Tabs} from "antd";
import {Button, Grid, notification, Tabs, Typography} from "antd";
import {Splitter} from "antd";
import {
CalendarOutlined, TableOutlined, MenuFoldOutlined, MenuUnfoldOutlined,
} from "@ant-design/icons";
import AppointmentsCalendarPage from "../pages/appointments_layout/AppointmentsCalendarPage.jsx";
import {CalendarOutlined, TableOutlined} from "@ant-design/icons";
import AppointmentsTablePage from "../pages/appointments_layout/AppointmentsTablePage.jsx";
import getAllAppointments from "../api/appointments/getAllAppointments.jsx";
import getAllScheduledAppointments from "../api/scheduled_appointments/getAllScheduledAppointments.jsx";
import {useAuth} from "../AuthContext.jsx";
import LoadingIndicator from "../components/LoadingIndicator.jsx";
import {cacheInfo, getCachedInfo, getCacheTimestamp} from "../utils/cachedInfoUtils.jsx";
const {useBreakpoint} = Grid;
const AppointmentsLayout = () => {
const items = [
{
key: '1',
label: 'Календарь приемов',
children: <AppointmentsCalendarPage/>,
icon: <CalendarOutlined/>,
},
{
key: '2',
label: 'Таблица приемов',
children: <AppointmentsTablePage/>,
icon: <TableOutlined/>,
const [collapsed, setCollapsed] = useState(true);
const [siderWidth, setSiderWidth] = useState(250);
const [hovered, setHovered] = useState(false);
const screens = useBreakpoint();
const {api} = useAuth();
const [loading, setLoading] = useState(true);
const [appointments, setAppointments] = useState([]);
const [scheduledAppointments, setScheduledAppointments] = useState([]);
const toggleSider = () => setCollapsed(!collapsed);
useEffect(() => {
loadDataWithCache();
}, []);
useEffect(() => {
const interval = setInterval(loadData, 5000);
return () => clearInterval(interval);
}, []);
const loadData = async () => {
await fetchAppointments();
await fetchScheduledAppointments();
setLoading(false);
};
const loadDataWithCache = async () => {
await fetchAppointmentsWithCache();
await fetchScheduledAppointmentsWithCache();
};
const fetchAppointmentsWithCache = async () => {
const cachedData = getCachedInfo("appointmentsData");
const cacheTimestamp = getCacheTimestamp("appointmentsData");
if (cachedData && cacheTimestamp && (Date.now() - cacheTimestamp) < 60 * 1000) {
setAppointments(cachedData);
return;
}
]
await fetchAppointments();
};
const fetchAppointments = async () => {
try {
const data = await getAllAppointments(api);
setAppointments(data);
cacheInfo("appointmentsData", data);
} catch (error) {
console.log(error);
notification.error({
message: "Ошибка загрузки данных", description: "Проверьте подключение к сети.", placement: "topRight",
});
}
};
const fetchScheduledAppointmentsWithCache = async () => {
const cachedData = getCachedInfo("scheduledAppointmentsData");
const cacheTimestamp = getCacheTimestamp("scheduledAppointmentsData");
if (cachedData && cacheTimestamp && (Date.now() - cacheTimestamp) < 60 * 1000) {
setScheduledAppointments(cachedData);
return;
}
await fetchScheduledAppointments();
};
const fetchScheduledAppointments = async () => {
try {
const data = await getAllScheduledAppointments(api);
setScheduledAppointments(data);
cacheInfo("scheduledAppointmentsData", data);
} catch (error) {
console.log(error);
notification.error({
message: "Ошибка загрузки данных", description: "Проверьте подключение к сети.", placement: "topRight",
});
}
};
const items = [{
key: "1",
label: "Календарь приемов",
children: <AppointmentsCalendarPage appointments={appointments} scheduledAppointments={scheduledAppointments}/>,
icon: <CalendarOutlined/>,
}, {
key: "2",
label: "Таблица приемов",
children: <AppointmentsTablePage/>,
icon: <TableOutlined/>,
},];
if (loading) {
return (<LoadingIndicator/>)
}
return (
<Tabs
defaultActiveKey="1"
items={items}
/>
<>
<Splitter
style={{flex: 1}}
min={200}
max={400}
initial={siderWidth}
onChange={setSiderWidth}
>
<Splitter.Panel style={{padding: 16}} defaultSize="80%" min="25%" max="90%">
<Tabs defaultActiveKey="1" items={items}/>
</Splitter.Panel>
{!collapsed && !screens.xs && (
<Splitter.Panel
style={{
padding: "16px", borderLeft: "1px solid #ddd", overflowY: "auto",
}}
defaultSize="20%"
min="20%"
max="75%"
>
<Typography.Title level={3} style={{marginBottom: 36}}>
Предстоящие события
</Typography.Title>
<p>Здесь будут предстоящие приемы...</p>
</Splitter.Panel>
)}
</Splitter>
<div
style={{
position: "fixed",
right: 0,
top: "50%",
transform: "translateY(-50%)",
transition: "right 0.3s ease",
zIndex: 1000,
display: screens.xs ? "none" : "block",
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Button
type="primary"
onClick={toggleSider}
icon={collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
style={{
width: hovered ? 250 : 50,
padding: hovered ? "0 20px" : "0",
overflow: "hidden",
textAlign: "left",
transition: "width 0.3s ease, padding 0.3s ease",
borderRadius: "4px 0 0 4px",
}}
>
{hovered ? (collapsed ? "Показать предстоящие события" : "Скрыть предстоящие события") : ""}
</Button>
</div>
</>
);
};

View File

@ -1,6 +1,5 @@
import {
notification,
Spin,
Table,
Input,
Row,
@ -12,21 +11,23 @@ import {
Typography,
Timeline, Grid, Pagination
} from "antd";
import getAllLensIssues from "../api/lens_issues/GetAllLensIssues.jsx";
import getAllLensIssues from "../api/lens_issues/getAllLensIssues.jsx";
import {useEffect, useState} from "react";
import {useAuth} from "../AuthContext.jsx";
import {DatabaseOutlined, LoadingOutlined, PlusOutlined, UnorderedListOutlined} from "@ant-design/icons";
import {DatabaseOutlined, PlusOutlined, UnorderedListOutlined} from "@ant-design/icons";
import LensIssueViewModal from "../components/lens_issues/LensIssueViewModal.jsx";
import dayjs from "dayjs";
import LensIssueFormModal from "../components/lens_issues/LensIssueFormModal.jsx";
import addLensIssue from "../api/lens_issues/AddLensIssue.jsx";
import addLensIssue from "../api/lens_issues/addLensIssue.jsx";
import SelectViewMode from "../components/SelectViewMode.jsx";
import LoadingIndicator from "../components/LoadingIndicator.jsx";
import {getCachedInfo, getCacheTimestamp} from "../utils/cachedInfoUtils.jsx";
const {Title} = Typography;
const {useBreakpoint} = Grid;
const IssuesPage = () => {
const {user} = useAuth();
const {api} = useAuth();
const screens = useBreakpoint();
const [loading, setLoading] = useState(true);
@ -56,13 +57,13 @@ const IssuesPage = () => {
const intervalId = setInterval(fetchLensIssues, 5000);
return () => clearInterval(intervalId);
}
}, [user, isModalVisible]);
}, [isModalVisible]);
const fetchLensIssuesWithCache = async () => {
const cachedData = localStorage.getItem("lensIssuesData");
const cacheTimestamp = localStorage.getItem("lensIssuesTimestamp");
const cachedData = getCachedInfo("lensIssuesData");
const cacheTimestamp = getCacheTimestamp("lensIssuesData");
if (cachedData && cacheTimestamp && (Date.now() - parseInt(cacheTimestamp)) < 60 * 1000) {
if (cachedData && cacheTimestamp && (Date.now() - cacheTimestamp) < 60 * 1000) {
setLensIssues(JSON.parse(cachedData));
setLoading(false);
} else {
@ -71,7 +72,7 @@ const IssuesPage = () => {
};
const fetchViewModeFromCache = () => {
const cachedViewMode = localStorage.getItem("viewModeIssues");
const cachedViewMode = getCachedInfo("viewModeIssues");
if (cachedViewMode) {
setViewMode(cachedViewMode);
}
@ -92,7 +93,7 @@ const IssuesPage = () => {
const handleSubmitFormModal = async (issue_date, patient_id, lens_id) => {
try {
await addLensIssue(user.token, {issue_date, patient_id, lens_id});
await addLensIssue(api, {issue_date, patient_id, lens_id});
setIsModalVisible(false);
notification.success({
message: "Линза выдана",
@ -112,7 +113,7 @@ const IssuesPage = () => {
const fetchLensIssues = async () => {
try {
const data = await getAllLensIssues(user.token);
const data = await getAllLensIssues(api);
setLensIssues(data);
setLoading(false);
} catch (error) {
@ -345,9 +346,7 @@ const IssuesPage = () => {
</Col>
</Row>
{loading ? (
<div style={{display: "flex", justifyContent: "center", alignItems: "center", height: "100vh"}}>
<Spin indicator={<LoadingOutlined style={{fontSize: 64, color: "#1890ff"}} spin/>}/>
</div>
<LoadingIndicator/>
) : viewMode === "table" ? (
<TableView/>
) : (

View File

@ -6,7 +6,6 @@ import {
FloatButton,
Row,
Col,
Spin,
notification,
Tooltip,
Table,
@ -16,26 +15,27 @@ import {
} from "antd";
import {
BuildOutlined,
LoadingOutlined,
PlusOutlined,
SortAscendingOutlined,
SortDescendingOutlined, TableOutlined,
TeamOutlined
} from "@ant-design/icons";
import {useAuth} from "../AuthContext.jsx";
import getAllPatients from "../api/patients/GetAllPatients.jsx";
import getAllPatients from "../api/patients/getAllPatients.jsx";
import PatientListCard from "../components/patients/PatientListCard.jsx";
import PatientFormModal from "../components/patients/PatientFormModal.jsx";
import updatePatient from "../api/patients/UpdatePatient.jsx";
import addPatient from "../api/patients/AddPatient.jsx";
import deletePatient from "../api/patients/DeletePatient.jsx";
import updatePatient from "../api/patients/updatePatient.jsx";
import addPatient from "../api/patients/addPatient.jsx";
import deletePatient from "../api/patients/deletePatient.jsx";
import SelectViewMode from "../components/SelectViewMode.jsx";
import LoadingIndicator from "../components/LoadingIndicator.jsx";
import {cacheInfo, getCachedInfo, getCacheTimestamp} from "../utils/cachedInfoUtils.jsx";
const {Option} = Select;
const {Title} = Typography
const PatientsPage = () => {
const {user} = useAuth();
const {api} = useAuth();
const [searchText, setSearchText] = useState("");
const [sortOrder, setSortOrder] = useState("asc");
const [viewMode, setViewMode] = useState("tile");
@ -59,14 +59,14 @@ const PatientsPage = () => {
const intervalId = setInterval(fetchPatients, 5000);
return () => clearInterval(intervalId);
}
}, [user, isModalVisible, selectedPatient]);
}, [isModalVisible, selectedPatient]);
const fetchPatientsWithCache = async () => {
const cachedData = localStorage.getItem("patientsData");
const cacheTimestamp = localStorage.getItem("patientsTimestamp");
const cachedData = getCachedInfo("patientsData");
const cacheTimestamp = getCacheTimestamp("patientsData");
if (cachedData && cacheTimestamp && (Date.now() - parseInt(cacheTimestamp)) < 60 * 1000) {
setPatients(JSON.parse(cachedData));
if (cachedData && cacheTimestamp && (Date.now() - cacheTimestamp) < 60 * 1000) {
setPatients(cachedData);
setLoading(false);
return;
}
@ -76,11 +76,10 @@ const PatientsPage = () => {
const fetchPatients = async () => {
try {
const data = await getAllPatients(user.token);
const data = await getAllPatients(api);
setPatients(data);
localStorage.setItem("patientsData", JSON.stringify(data));
localStorage.setItem("patientsTimestamp", Date.now().toString());
cacheInfo("patientsData", data);
} catch (error) {
console.log(error);
notification.error({
@ -90,13 +89,11 @@ const PatientsPage = () => {
})
}
if (loading) {
setLoading(false);
}
setLoading(false);
};
const fetchViewModeFromCache = () => {
const cachedViewMode = localStorage.getItem("viewModePatients");
const cachedViewMode = getCachedInfo("viewModePatients");
if (cachedViewMode) {
setViewMode(cachedViewMode);
}
@ -128,7 +125,7 @@ const PatientsPage = () => {
const handleDeletePatient = async (patient_id) => {
try {
await deletePatient(user.token, patient_id);
await deletePatient(api, patient_id);
await fetchPatients();
notification.success({
message: "Пациент удалён",
@ -171,7 +168,7 @@ const PatientsPage = () => {
};
const editPatient = async (patient) => {
await updatePatient(user.token, selectedPatient.id, patient);
await updatePatient(api, selectedPatient.id, patient);
notification.success({
message: "Пациент обновлён",
description: `Данные пациента ${patient.first_name} ${patient.last_name} успешно обновлены.`,
@ -180,7 +177,7 @@ const PatientsPage = () => {
};
const addNewPatient = async (patient) => {
await addPatient(user.token, patient);
await addPatient(api, patient);
notification.success({
message: "Пациент добавлен",
description: `Пациент ${patient.first_name} ${patient.last_name} успешно добавлен.`,
@ -370,14 +367,7 @@ const PatientsPage = () => {
</Row>
{loading ? (
<div style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
}}>
<Spin indicator={<LoadingOutlined style={{fontSize: 64, color: "#1890ff"}} spin/>}/>
</div>
<LoadingIndicator/>
) : viewMode === "tile" ? (
<TileView/>
) : (

View File

@ -1,25 +1,149 @@
import {Calendar, Grid} from "antd";
import {Calendar, Grid, ConfigProvider, Badge, Modal} from "antd";
import {useState} from "react";
import dayjs from "dayjs";
import 'dayjs/locale/ru';
import locale from 'antd/es/locale/ru_RU';
import updateLocale from 'dayjs/plugin/updateLocale';
import PropTypes from "prop-types";
const {useBreakpoint} = Grid;
const AppointmentsCalendarPage = () => {
const screens = useBreakpoint();
dayjs.extend(updateLocale);
dayjs.updateLocale('ru', {
weekStart: 1
});
const [selectedDate, setSelectedDate] = useState(dayjs(
new Date()
));
const AppointmentsCalendarPage = ({appointments, scheduledAppointments}) => {
const screens = useBreakpoint();
const [selectedDate, setSelectedDate] = useState(dayjs(new Date()));
const [modalVisible, setModalVisible] = useState(false);
const [selectedAppointments, setSelectedAppointments] = useState([]);
const dateCellRender = (value) => {
const date = value.format('YYYY-MM-DD');
const appointmentsForDate = appointments.filter(app =>
dayjs(app.appointment_datetime).format('YYYY-MM-DD') === date
);
const scheduledForDate = scheduledAppointments.filter(app =>
dayjs(app.scheduled_datetime).format('YYYY-MM-DD') === date
);
return (
<ul style={{listStyle: 'none', padding: 0}}>
{appointmentsForDate.map(app => (
<li key={app.id}>
<Badge status="success" text={`Прием ${dayjs(app.appointment_datetime).format('HH:mm')}`}/>
</li>
))}
{scheduledForDate.map(app => (
<li key={app.id}>
<Badge status="processing"
text={`Запланировано ${dayjs(app.scheduled_datetime).format('HH:mm')}`}/>
</li>
))}
</ul>
);
};
const onSelect = (date) => {
setSelectedDate(date);
const selectedDateStr = date.format('YYYY-MM-DD');
const appointmentsForDate = appointments.filter(app =>
dayjs(app.appointment_datetime).format('YYYY-MM-DD') === selectedDateStr
);
const scheduledForDate = scheduledAppointments.filter(app =>
dayjs(app.scheduled_datetime).format('YYYY-MM-DD') === selectedDateStr
);
setSelectedAppointments([...appointmentsForDate, ...scheduledForDate]);
setModalVisible(true);
};
return (
<div style={{padding: 20}}>
<Calendar
fullscreen={!screens.xs}
value={selectedDate}
onSelect={setSelectedDate}
/>
</div>
<ConfigProvider locale={locale}>
<div style={{padding: 20}}>
<Calendar
fullscreen={!screens.xs}
value={selectedDate}
onSelect={onSelect}
cellRender={dateCellRender}
/>
<Modal
title={`Приемы на ${selectedDate.format('DD.MM.YYYY')}`}
open={modalVisible}
onCancel={() => setModalVisible(false)}
footer={null}
>
{selectedAppointments.map(app => (
<div key={app.id}>
<p>{app.appointment_datetime ? 'Прием' : 'Запланировано'}: {dayjs(app.appointment_datetime || app.scheduled_datetime).format('HH:mm')}</p>
<p>Пациент: {app.patient?.name || 'Не указан'}</p>
<p>Врач: {app.doctor?.name || 'Не указан'}</p>
<p>Тип: {app.type?.name || 'Не указан'}</p>
{app.results && <p>Результаты: {app.results}</p>}
<hr/>
</div>
))}
</Modal>
</div>
</ConfigProvider>
);
};
AppointmentsCalendarPage.propTypes = {
appointments: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
results: PropTypes.string,
days_until_the_next_appointment: PropTypes.number,
appointment_datetime: PropTypes.string.isRequired,
patient: PropTypes.shape({
first_name: PropTypes.string,
last_name: PropTypes.string,
patronymic: PropTypes.string,
birthday: PropTypes.string,
address: PropTypes.string,
email: PropTypes.string,
phone: PropTypes.string,
diagnosis: PropTypes.string,
correction: PropTypes.string,
}),
doctor: PropTypes.shape({
last_name: PropTypes.string,
first_name: PropTypes.string,
login: PropTypes.string,
}),
type: PropTypes.shape({
title: PropTypes.string,
})
})
),
scheduledAppointments: PropTypes.arrayOf(
PropTypes.shape(
{
id: PropTypes.number.isRequired,
scheduled_datetime: PropTypes.string.isRequired,
patient: PropTypes.shape({
first_name: PropTypes.string,
last_name: PropTypes.string,
patronymic: PropTypes.string,
birthday: PropTypes.string,
address: PropTypes.string,
email: PropTypes.string,
phone: PropTypes.string,
diagnosis: PropTypes.string,
correction: PropTypes.string,
}),
doctor: PropTypes.shape({
last_name: PropTypes.string,
first_name: PropTypes.string,
login: PropTypes.string,
}),
type: PropTypes.shape({
title: PropTypes.string,
}),
}
)
)
};
export default AppointmentsCalendarPage;

View File

@ -1,11 +1,11 @@
import {useAuth} from "../../AuthContext.jsx";
import {useEffect, useState} from "react";
import getAllAppointments from "../../api/appointments/GetAllAppointments.jsx";
import getAllAppointments from "../../api/appointments/getAllAppointments.jsx";
import {notification} from "antd";
const AppointmentsTablePage = () => {
const {user} = useAuth();
const {api} = useAuth();
const [appointments, setAppointments] = useState([]);
@ -16,52 +16,6 @@ const AppointmentsTablePage = () => {
const [selectedAppointment, setSelectedAppointment] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchAppointmentsWithCache();
document.title = "Приемы";
}, []);
useEffect(() => {
if (!isModalVisible && !selectedAppointment) {
const intervalId = setInterval(fetchAppointments, 5000);
return () => clearInterval(intervalId);
}
}, [user, isModalVisible, selectedAppointment]);
const fetchAppointmentsWithCache = async () => {
const cachedData = localStorage.getItem("appointmentsData");
const cacheTimestamp = localStorage.getItem("appointmentsTimestamp");
const currentTime = Date.now();
if (cachedData && currentTime - cacheTimestamp < 60000) {
setAppointments(JSON.parse(cachedData));
setLoading(false);
return;
}
await fetchAppointments();
};
const fetchAppointments = async () => {
try {
const data = await getAllAppointments(user.token);
setAppointments(data);
localStorage.setItem("appointmentsData", JSON.stringify(data));
localStorage.setItem("appointmentsTimestamp", Date.now().toString());
} catch (error) {
console.log(error);
notification.error({
message: "Ошибка загрузки данных",
description: "Проверьте подключение к сети.",
placement: "topRight",
})
}
if (loading) {
setLoading(false);
}
};
return (
<div style={{padding: 20}}>

View File

@ -6,7 +6,6 @@ import {
FloatButton,
Row,
Col,
Spin,
Button,
Form,
InputNumber,
@ -18,7 +17,6 @@ import {
Typography
} from "antd";
import {
LoadingOutlined,
PlusOutlined,
DownOutlined,
UpOutlined,
@ -27,20 +25,22 @@ import {
BuildOutlined
} from "@ant-design/icons";
import LensCard from "../../components/lenses/LensListCard.jsx";
import getAllLenses from "../../api/lenses/GetAllLenses.jsx";
import addLens from "../../api/lenses/AddLens.jsx";
import updateLens from "../../api/lenses/UpdateLens.jsx";
import deleteLens from "../../api/lenses/DeleteLens.jsx";
import getAllLenses from "../../api/lenses/getAllLenses.jsx";
import addLens from "../../api/lenses/addLens.jsx";
import updateLens from "../../api/lenses/updateLens.jsx";
import deleteLens from "../../api/lenses/deleteLens.jsx";
import {useAuth} from "../../AuthContext.jsx";
import LensFormModal from "../../components/lenses/LensFormModal.jsx";
import SelectViewMode from "../../components/SelectViewMode.jsx";
import LoadingIndicator from "../../components/LoadingIndicator.jsx";
import {getCachedInfo, getCacheTimestamp} from "../../utils/cachedInfoUtils.jsx";
const {Option} = Select;
const {useBreakpoint} = Grid;
const {Title} = Typography;
const LensesPage = () => {
const {user} = useAuth();
const {api} = useAuth();
const screens = useBreakpoint();
const [current, setCurrent] = useState(1);
@ -76,11 +76,11 @@ const LensesPage = () => {
const intervalId = setInterval(fetchLenses, 5000);
return () => clearInterval(intervalId);
}
}, [user, isModalVisible, selectedLens]);
}, [isModalVisible, selectedLens]);
const fetchLensWithCache = async () => {
const cachedData = localStorage.getItem("lensData");
const cacheTimestamp = localStorage.getItem("lensTimestamp");
const cachedData = getCachedInfo("lensData");
const cacheTimestamp = getCacheTimestamp("lensData");
if (cachedData && cacheTimestamp && (Date.now() - parseInt(cacheTimestamp)) < 60 * 1000) {
setLenses(JSON.parse(cachedData));
@ -93,7 +93,7 @@ const LensesPage = () => {
const fetchLenses = async () => {
try {
const data = await getAllLenses(user.token);
const data = await getAllLenses(api);
setLenses(data);
setLoading(false);
} catch (error) {
@ -108,7 +108,7 @@ const LensesPage = () => {
};
const fetchViewModeFromCache = () => {
const cachedViewMode = localStorage.getItem("viewModeLenses");
const cachedViewMode = getCachedInfo("viewModeLenses");
if (cachedViewMode) {
setViewMode(cachedViewMode);
}
@ -148,8 +148,8 @@ const LensesPage = () => {
const handleDeleteLens = async (lensId) => {
try {
await deleteLens(user.token, lensId);
await fetchLenses(user.token);
await deleteLens(api, lensId);
await fetchLenses(api);
notification.success({
message: "Линза удалена",
description: "Линза успешно удалена.",
@ -168,14 +168,14 @@ const LensesPage = () => {
const handleModalSubmit = async (lensData) => {
try {
if (selectedLens) {
await updateLens(user.token, selectedLens.id, lensData);
await updateLens(api, selectedLens.id, lensData);
notification.success({
message: "Линза обновлена",
description: "Линза успешно обновлена.",
placement: "topRight",
});
} else {
await addLens(user.token, lensData);
await addLens(api, lensData);
notification.success({
message: "Линза добавлена",
description: "Линза успешно добавлена.",
@ -345,7 +345,6 @@ const LensesPage = () => {
/>
);
return (
<div style={{padding: 20}}>
<Title level={1}><FolderViewOutlined/> Линзы</Title>
@ -497,9 +496,7 @@ const LensesPage = () => {
)}
{loading ? (
<div style={{display: "flex", justifyContent: "center", alignItems: "center", height: "100vh"}}>
<Spin indicator={<LoadingOutlined style={{fontSize: 64, color: "#1890ff"}} spin/>}/>
</div>
<LoadingIndicator/>
) : viewMode === "tile" ? (
<TileView/>
) : (

View File

@ -1,22 +1,24 @@
import {useAuth} from "../../AuthContext.jsx";
import {useEffect, useState} from "react";
import {FloatButton, Input, List, notification, Row, Spin, Typography} from "antd";
import getAllSets from "../../api/sets/GetAllSets.jsx";
import {LoadingOutlined, PlusOutlined, SwitcherOutlined} from "@ant-design/icons";
import {FloatButton, Input, List, notification, Row, Typography} from "antd";
import getAllSets from "../../api/sets/getAllSets.jsx";
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.jsx";
import addSet from "../../api/sets/AddSet.jsx";
import deleteSet from "../../api/sets/DeleteSet.jsx";
import addSetContent from "../../api/set_content/AddSetContent.jsx";
import updateSetContent from "../../api/set_content/UpdateSetContent.jsx";
import appendLensesFromSet from "../../api/sets/AppendLensesFromSet.jsx";
import updateSet from "../../api/sets/updateSet.jsx";
import addSet from "../../api/sets/addSet.jsx";
import deleteSet from "../../api/sets/deleteSet.jsx";
import addSetContent from "../../api/set_content/addSetContent.jsx";
import updateSetContent from "../../api/set_content/updateSetContent.jsx";
import appendLensesFromSet from "../../api/sets/appendLensesFromSet.jsx";
import LoadingIndicator from "../../components/LoadingIndicator.jsx";
import {cacheInfo, getCachedInfo, getCacheTimestamp} from "../../utils/cachedInfoUtils.jsx";
const {Title} = Typography;
const SetLensesPage = () => {
const {user} = useAuth();
const {api} = useAuth();
const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(10);
@ -36,14 +38,14 @@ const SetLensesPage = () => {
const intervalId = setInterval(fetchSets, 5000);
return () => clearInterval(intervalId);
}
}, [user, isModalVisible]);
}, [isModalVisible]);
const fetchSetsWithCache = async () => {
const cachedData = localStorage.getItem("setsData");
const cacheTimestamp = localStorage.getItem("setsTimestamp");
const cachedData = getCachedInfo("setsData");
const cacheTimestamp = getCacheTimestamp("setsData");
if (cachedData && cacheTimestamp && (Date.now() - parseInt(cacheTimestamp)) < 60 * 1000) {
setSets(JSON.parse(cachedData));
if (cachedData && cacheTimestamp && (Date.now() - cacheTimestamp) < 60 * 1000) {
setSets(cachedData);
setLoading(false);
} else {
await fetchSets();
@ -51,14 +53,11 @@ const SetLensesPage = () => {
};
const fetchSets = async () => {
if (!user || !user.token) return;
try {
const data = await getAllSets(user.token);
const data = await getAllSets(api);
setSets(data);
localStorage.setItem("setsData", JSON.stringify(data));
localStorage.setItem("setsTimestamp", Date.now().toString());
cacheInfo("setsData", data);
} catch (error) {
console.log(error);
notification.error({
@ -87,7 +86,7 @@ const SetLensesPage = () => {
const handleDeleteSet = async (set_id) => {
try {
await deleteSet(user.token, set_id);
await deleteSet(api, set_id);
notification.success({
message: "Набор удален",
description: "Набор успешно удален.",
@ -110,7 +109,7 @@ const SetLensesPage = () => {
const handleAppendSet = async (set) => {
try {
await appendLensesFromSet(user.token, set.id);
await appendLensesFromSet(api, set.id);
notification.success({
message: "Линзы добавлены",
description: "Линзы успешно добавлены.",
@ -157,7 +156,7 @@ const SetLensesPage = () => {
const setContent = async (content, set_id) => {
try {
console.log(content);
await addSetContent(user.token, content, set_id);
await addSetContent(api, content, set_id);
} catch (error) {
console.error("Ошибка сохранения набора:", error);
notification.error({
@ -170,7 +169,7 @@ const SetLensesPage = () => {
const updateContent = async (content, set_id) => {
try {
await updateSetContent(user.token, content, set_id);
await updateSetContent(api, content, set_id);
} catch (error) {
console.error("Ошибка сохранения набора:", error);
notification.error({
@ -182,7 +181,7 @@ const SetLensesPage = () => {
};
const editCurrentSet = async (set) => {
const refreshed_set = await updateSet(user.token, selectedSet.id, set);
const refreshed_set = await updateSet(api, selectedSet.id, set);
notification.success({
message: "Набор обновлен",
description: "Набор успешно обновлен.",
@ -192,7 +191,7 @@ const SetLensesPage = () => {
};
const addNewSet = async (set) => {
const refreshed_set = await addSet(user.token, set);
const refreshed_set = await addSet(api, set);
notification.success({
message: "Набор добавлен",
description: "Набор успешно добавлен.",
@ -214,14 +213,7 @@ const SetLensesPage = () => {
</Row>
{loading ? (
<div style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
}}>
<Spin indicator={<LoadingOutlined style={{fontSize: 64, color: "#1890ff"}} spin/>}/>
</div>
<LoadingIndicator/>
) : (
<List
grid={{

View File

@ -4,6 +4,7 @@ body {
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
*::-webkit-scrollbar-track {

View File

@ -0,0 +1,18 @@
const cacheInfo = (key, value) => {
localStorage.setItem(key, JSON.stringify(value));
localStorage.setItem(`${key}Timestamp`, Date.now().toString());
};
const getCachedInfo = (key) => {
const data = localStorage.getItem(key);
if (!data) return null;
return JSON.parse(data);
};
const getCacheTimestamp = (key) => {
const timestamp = localStorage.getItem(`${key}Timestamp`);
if (!timestamp) return null;
return parseInt(timestamp);
};
export {cacheInfo, getCachedInfo, getCacheTimestamp};