сделал отображение истории выдачи линз в виде ленты

This commit is contained in:
Андрей Дувакин 2025-03-10 17:56:43 +05:00
parent e539a9a37b
commit 744a5402a0
5 changed files with 221 additions and 125 deletions

View File

@ -4,7 +4,7 @@ import PropTypes from "prop-types";
const {Option} = Select;
const SelectViewMode = ({viewMode, setViewMode, localStorageKey, toolTipText}) => {
const SelectViewMode = ({viewMode, setViewMode, localStorageKey, toolTipText, viewModes}) => {
return (
<Tooltip
title={toolTipText}
@ -17,14 +17,12 @@ const SelectViewMode = ({viewMode, setViewMode, localStorageKey, toolTipText}) =
}}
style={{width: "100%"}}
>
<Option value={"tile"}>
<BuildOutlined style={{marginRight: 8}} />
Плитка
</Option>
<Option value={"table"}>
<TableOutlined style={{marginRight: 8}}/>
Таблица
</Option>
{viewModes.map(viewMode => (
<Option key={viewMode.value} value={viewMode.value}>
{viewMode.icon}
{viewMode.label}
</Option>
))}
</Select>
</Tooltip>
@ -35,6 +33,12 @@ SelectViewMode.propTypes = {
viewMode: PropTypes.string.isRequired,
setViewMode: PropTypes.func.isRequired,
localStorageKey: PropTypes.string.isRequired,
toolTipText: PropTypes.string.isRequired,
viewModes: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
icon: PropTypes.element,
})).isRequired,
};
export default SelectViewMode;

View File

@ -1,14 +1,19 @@
import {useEffect, useState} from "react";
import {Modal, Input, Button, notification, Typography, Collapse, Steps, Row, Alert, Col, DatePicker, Spin} from "antd";
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 {useAuth} from "../../AuthContext.jsx";
import dayjs from "dayjs";
import getNotIssuedLenses from "../../api/lenses/GetNotIssuedLenses.jsx";
const {useBreakpoint} = Grid;
const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
const {user} = useAuth();
const screens = useBreakpoint();
const [patients, setPatients] = useState([]);
const [lenses, setLenses] = useState([]);
@ -37,8 +42,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
} catch (error) {
console.error(error);
notification.error({
message: "Ошибка загрузки пациентов",
description: "Проверьте подключение к сети.",
message: "Ошибка загрузки пациентов", description: "Проверьте подключение к сети.",
});
}
};
@ -50,8 +54,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
} catch (error) {
console.error(error);
notification.error({
message: "Ошибка загрузки линз",
description: "Проверьте подключение к сети.",
message: "Ошибка загрузки линз", description: "Проверьте подключение к сети.",
});
}
};
@ -79,21 +82,18 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
.some(value => value.toLowerCase().includes(searchLower));
});
const patientsItems = filteredPatients.map((patient) => (
{
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>,
}
));
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();
@ -103,25 +103,20 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
.some(value => value.toLowerCase().includes(searchLower));
})
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>,
}
));
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>,
}));
const SelectPatientStep = () => {
return selectedPatient ? (
@ -139,9 +134,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
>
Выбрать другого пациента
</Button>
</div>
) : (
<>
</div>) : (<>
<Input
placeholder="Поиск пациента"
value={searchPatientString}
@ -155,13 +148,11 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
items={patientsItems}
/>
</div>
</>
)
</>)
};
const SelectLensStep = () => {
return selectedLens ? (
<div style={{padding: "10px", background: "#f5f5f5", borderRadius: 5, marginBottom: 15}}>
return selectedLens ? (<div style={{padding: "10px", background: "#f5f5f5", borderRadius: 5, marginBottom: 15}}>
<Typography.Text strong>
{selectedLens.diameter} {selectedLens.tor} {selectedLens.preset_refraction}
</Typography.Text>
@ -181,9 +172,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
>
Выбрать другую линзу
</Button>
</div>
) : (
<>
</div>) : (<>
<Input
placeholder="Поиск линз"
value={searchLensString}
@ -197,13 +186,11 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
items={lensesItems}
/>
</div>
</>
)
</>)
};
const ConfirmStep = () => {
return (
<>
return (<>
<Alert
type="warning"
message="Внимание! После подтверждения линза будет считаться выданной, данное действие нельзя будет отменить."
@ -251,24 +238,16 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
<p><b>Сторона:</b> {selectedLens.side}</p>
<p><b>Esa:</b> {selectedLens.esa}</p>
</div>
</>
);
</>);
};
const steps = [
{
title: 'Выбор пациента',
content: <SelectPatientStep/>,
},
{
title: 'Выбор линзы',
content: <SelectLensStep/>,
},
{
title: 'Подтверждение',
content: <ConfirmStep/>,
},
];
const steps = [{
title: 'Выбор пациента', content: <SelectPatientStep/>,
}, {
title: 'Выбор линзы', content: <SelectLensStep/>,
}, {
title: 'Подтверждение', content: <ConfirmStep/>,
},];
const isActiveNextButton = () => {
if (currentStep === 0 && !selectedPatient) {
@ -286,8 +265,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
return currentStep === steps.length - 1;
};
return (
<Modal
return (<Modal
title="Выдача линзы пациенту"
open={visible}
onCancel={() => {
@ -299,28 +277,22 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
}}
footer={null}
maskClosable={false}
width={window.innerWidth > 768 ? 700 : "90%"}
width={!screens.xs ? 700 : "90%"}
centered
>
{loading ? (
<div style={{display: "flex", justifyContent: "center", alignItems: "center", height: "70vh"}}>
{loading ? (<div style={{display: "flex", justifyContent: "center", alignItems: "center", height: "70vh"}}>
<Spin size="large"/>
</div>
) : (
<div style={{maxHeight: "70vh", overflowY: "auto", padding: "10px"}}>
</div>) : (<div style={{maxHeight: "70vh", overflowY: "auto", padding: "10px"}}>
{steps[currentStep].content}
</div>
)}
</div>)}
{window.innerWidth > 768 && (
<Steps
{!screens.xs && (<Steps
current={currentStep}
items={steps}
style={{marginTop: 16}}
direction={window.innerWidth > 768 ? "horizontal" : "vertical"}
/>
)}
direction={!screens.xs ? "horizontal" : "vertical"}
/>)}
<Row
justify="end"
@ -348,14 +320,11 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
{isActiveFinishButton() ? "Завершить" : "Далее"}
</Button>
</Row>
</Modal>
);
</Modal>);
};
LensIssueFormModal.propTypes = {
visible: PropTypes.bool.isRequired,
onCancel: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
visible: PropTypes.bool.isRequired, onCancel: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
};
export default LensIssueFormModal;

View File

@ -1,23 +1,40 @@
import {notification, Spin, Table, Input, Row, Col, DatePicker, Tooltip, Button, FloatButton, Typography} from "antd";
import {
notification,
Spin,
Table,
Input,
Row,
Col,
DatePicker,
Tooltip,
Button,
FloatButton,
Typography,
Timeline, Grid
} from "antd";
import getAllLensIssues from "../api/lens_issues/GetAllLensIssues.jsx";
import {useEffect, useState} from "react";
import {useAuth} from "../AuthContext.jsx";
import {DatabaseOutlined, LoadingOutlined, PlusOutlined} from "@ant-design/icons";
import {DatabaseOutlined, LoadingOutlined, 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 SelectViewMode from "../components/SelectViewMode.jsx";
const {Title} = Typography;
const {useBreakpoint} = Grid;
const IssuesPage = () => {
const {user} = useAuth();
const screens = useBreakpoint();
const [loading, setLoading] = useState(true);
const [lensIssues, setLensIssues] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
const [selectedIssue, setSelectedIssue] = useState(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [viewMode, setViewMode] = useState("table");
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
@ -27,6 +44,7 @@ const IssuesPage = () => {
useEffect(() => {
fetchLensIssuesWithCache();
fetchViewModeFromCache();
document.title = "Выдача линз";
}, []);
@ -49,6 +67,13 @@ const IssuesPage = () => {
}
};
const fetchViewModeFromCache = () => {
const cachedViewMode = localStorage.getItem("viewModeIssues");
if (cachedViewMode) {
setViewMode(cachedViewMode);
}
};
const handleAddIssue = () => {
setSelectedIssue(null);
setIsModalVisible(true);
@ -123,6 +148,19 @@ const IssuesPage = () => {
}
);
const viewModes = [
{
value: "table",
label: "Таблица",
icon: <DatabaseOutlined style={{marginRight: 8}}/>,
},
{
value: "timeline",
label: "Лента",
icon: <UnorderedListOutlined style={{marginRight: 8}}/>,
},
];
const columns = [
{
title: "Дата выдачи",
@ -158,25 +196,77 @@ const IssuesPage = () => {
},
];
const TableView = () => (
<Table
columns={columns}
dataSource={filteredIssues}
rowKey="id"
pagination={{
current: currentPage,
pageSize: pageSize,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => {
setCurrentPage(page);
setPageSize(newPageSize);
},
}}
showSorterTooltip={false}
/>
);
const timeLineItems = filteredIssues.map(issue => ({
label: dayjs(issue.issue_date).format("DD.MM.YYYY"),
children: (
<Row
gutter={[16, 16]}
align={"middle"}
>
<Col xs={24} md={24} sm={24} xl={13}>
<p style={{textAlign: "right"}}>Пациент: {issue.patient.last_name} {issue.patient.first_name}</p>
</Col>
<Col xs={24} md={24} sm={24} xl={5}>
<p style={{textAlign: "right"}}>Линза: {issue.lens.side} {issue.lens.diameter}</p>
</Col>
<Col xs={24} md={24} sm={24} xl={6}>
<Button
type={"dashed"}
onClick={() => setSelectedIssue(issue)}
style={{marginRight: 40}}
>
Подробнее
</Button>
</Col>
</Row>
),
}));
const TimeLineView = () => (
<Timeline
items={timeLineItems}
mode={screens.xs ? "left" : "right"}
/>
);
return (
<div style={{padding: 20}}>
<Title level={1}><DatabaseOutlined/> Выдача линз</Title>
<Row gutter={[16, 16]} style={{marginBottom: 20}}>
<Col xs={24} md={12} sm={24} xl={14}>
<Col xs={24} md={24} sm={24} xl={12}>
<Input
placeholder="Поиск по пациенту или дате"
onChange={handleSearch}
style={{marginBottom: 20, width: "100%"}}
style={{width: "100%"}}
allowClear
/>
</Col>
<Col xs={24} md={
startFilterDate && endFilterDate ? 8 : 12
startFilterDate && endFilterDate ? 12 : 16
} sm={
startFilterDate && endFilterDate ? 16 : 24
16
} xl={
startFilterDate && endFilterDate ? 8 : 10
startFilterDate && endFilterDate ? 6 : 8
}>
<Tooltip
title="Фильтр по дате выдачи линзы"
@ -203,7 +293,6 @@ const IssuesPage = () => {
setStartFilterDate(null);
setEndFilterDate(null);
}}
style={{marginLeft: 10}}
type={"primary"}
block
>
@ -212,29 +301,27 @@ const IssuesPage = () => {
</Tooltip>
</Col>
)}
<Col xs={24}
md={startFilterDate && endFilterDate ? 8 : 8}
sm={startFilterDate && endFilterDate ? 24 : 8}
xl={4}>
<SelectViewMode
viewMode={viewMode}
setViewMode={setViewMode}
localStorageKey={"viewModeIssues"}
toolTipText={"Формат отображения выдач линз"}
viewModes={viewModes}
/>
</Col>
</Row>
{loading ? (
<div style={{display: "flex", justifyContent: "center", alignItems: "center", height: "100vh"}}>
<Spin indicator={<LoadingOutlined style={{fontSize: 64, color: "#1890ff"}} spin/>}/>
</div>
) : viewMode === "table" ? (
<TableView/>
) : (
<Table
columns={columns}
dataSource={filteredIssues}
rowKey="id"
pagination={{
current: currentPage,
pageSize: pageSize,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => {
setCurrentPage(page);
setPageSize(newPageSize);
},
}}
showSorterTooltip={false}
/>
<TimeLineView/>
)}
<FloatButton

View File

@ -10,7 +10,12 @@ import {
Button,
Form,
InputNumber,
Card, Grid, notification, Table, Popconfirm, Tooltip, Typography
Card,
Grid,
notification,
Table,
Popconfirm,
Typography
} from "antd";
import {
LoadingOutlined,
@ -18,7 +23,8 @@ import {
DownOutlined,
UpOutlined,
FolderViewOutlined,
BorderlessTableOutlined, TableOutlined
TableOutlined,
BuildOutlined
} from "@ant-design/icons";
import LensCard from "../components/lenses/LensListCard.jsx";
import getAllLenses from "../api/lenses/GetAllLenses.jsx";
@ -101,6 +107,7 @@ const LensesPage = () => {
}
};
const fetchViewModeFromCache = () => {
const cachedViewMode = localStorage.getItem("viewModeLenses");
if (cachedViewMode) {
@ -226,6 +233,19 @@ const LensesPage = () => {
/>
);
const viewModes = [
{
value: "tile",
label: "Плитка",
icon: <BuildOutlined style={{marginRight: 8}}/>
},
{
value: "table",
label: "Таблица",
icon: <TableOutlined style={{marginRight: 8}}/>
}
];
const columns = [
{
title: "Тор",
@ -354,6 +374,7 @@ const LensesPage = () => {
setViewMode={setViewMode}
localStorageKey={"viewModeLenses"}
toolTipText={"Формат отображения линз"}
viewModes={viewModes}
/>
</Col>
</Row>

View File

@ -15,10 +15,11 @@ import {
Typography
} from "antd";
import {
BuildOutlined,
LoadingOutlined,
PlusOutlined,
SortAscendingOutlined,
SortDescendingOutlined,
SortDescendingOutlined, TableOutlined,
TeamOutlined
} from "@ant-design/icons";
import {useAuth} from "../AuthContext.jsx";
@ -223,6 +224,19 @@ const PatientsPage = () => {
/>
);
const viewModes = [
{
value: "tile",
label: "Плитка",
icon: <BuildOutlined style={{marginRight: 8}}/>
},
{
value: "table",
label: "Таблица",
icon: <TableOutlined style={{marginRight: 8}}/>
}
];
const columns = [
{
title: "Фамилия",
@ -352,11 +366,12 @@ const PatientsPage = () => {
setViewMode={setViewMode}
localStorageKey={"viewModePatients"}
toolTipText={"Формат отображения пациентов"}
viewModes={viewModes}
/>
</Col>
</Row>
{loading ? (
{loading ? (
<div style={{
display: "flex",
justifyContent: "center",