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

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

View File

@ -1,14 +1,19 @@
import {useEffect, useState} from "react"; 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 PropTypes from "prop-types";
import getAllPatients from "../../api/patients/GetAllPatients.jsx"; import getAllPatients from "../../api/patients/GetAllPatients.jsx";
import {useAuth} from "../../AuthContext.jsx"; import {useAuth} from "../../AuthContext.jsx";
import dayjs from "dayjs"; 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 LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
const {user} = useAuth(); const {user} = useAuth();
const screens = useBreakpoint();
const [patients, setPatients] = useState([]); const [patients, setPatients] = useState([]);
const [lenses, setLenses] = useState([]); const [lenses, setLenses] = useState([]);
@ -37,8 +42,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
notification.error({ notification.error({
message: "Ошибка загрузки пациентов", message: "Ошибка загрузки пациентов", description: "Проверьте подключение к сети.",
description: "Проверьте подключение к сети.",
}); });
} }
}; };
@ -50,8 +54,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
notification.error({ notification.error({
message: "Ошибка загрузки линз", message: "Ошибка загрузки линз", description: "Проверьте подключение к сети.",
description: "Проверьте подключение к сети.",
}); });
} }
}; };
@ -79,12 +82,10 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
.some(value => value.toLowerCase().includes(searchLower)); .some(value => value.toLowerCase().includes(searchLower));
}); });
const patientsItems = filteredPatients.map((patient) => ( const patientsItems = filteredPatients.map((patient) => ({
{
key: patient.id, key: patient.id,
label: `${patient.last_name} ${patient.first_name} (${new Date(patient.birthday).toLocaleDateString("ru-RU")})`, label: `${patient.last_name} ${patient.first_name} (${new Date(patient.birthday).toLocaleDateString("ru-RU")})`,
children: children: <div>
<div>
<p><b>Пациент:</b> {patient.last_name} {patient.first_name}</p> <p><b>Пациент:</b> {patient.last_name} {patient.first_name}</p>
<p><b>Дата рождения:</b> {new Date(patient.birthday).toLocaleDateString("ru-RU")}</p> <p><b>Дата рождения:</b> {new Date(patient.birthday).toLocaleDateString("ru-RU")}</p>
<p><b>Диагноз:</b> {patient.diagnosis}</p> <p><b>Диагноз:</b> {patient.diagnosis}</p>
@ -92,8 +93,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
<p><b>Телефон:</b> {patient.phone}</p> <p><b>Телефон:</b> {patient.phone}</p>
<Button type="primary" onClick={() => setSelectedPatient(patient)}>Выбрать</Button> <Button type="primary" onClick={() => setSelectedPatient(patient)}>Выбрать</Button>
</div>, </div>,
} }));
));
const filteredLenses = lenses.filter((lens) => { const filteredLenses = lenses.filter((lens) => {
const searchLower = searchLensString.toLowerCase(); const searchLower = searchLensString.toLowerCase();
@ -103,12 +103,8 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
.some(value => value.toLowerCase().includes(searchLower)); .some(value => value.toLowerCase().includes(searchLower));
}) })
const lensesItems = filteredLenses.map((lens) => ( const lensesItems = filteredLenses.map((lens) => ({
{ key: lens.id, label: `Линза ${lens.side} ${lens.diameter} мм`, children: <div>
key: lens.id,
label: `Линза ${lens.side} ${lens.diameter} мм`,
children:
<div>
<p><b>Диаметр:</b> {lens.diameter}</p> <p><b>Диаметр:</b> {lens.diameter}</p>
<p><b>Тор:</b> {lens.tor}</p> <p><b>Тор:</b> {lens.tor}</p>
<p><b>Пресетная рефракция:</b> {lens.preset_refraction}</p> <p><b>Пресетная рефракция:</b> {lens.preset_refraction}</p>
@ -120,8 +116,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
<p><b>Esa:</b> {lens.esa}</p> <p><b>Esa:</b> {lens.esa}</p>
<Button type="primary" onClick={() => setSelectedLens(lens)}>Выбрать</Button> <Button type="primary" onClick={() => setSelectedLens(lens)}>Выбрать</Button>
</div>, </div>,
} }));
));
const SelectPatientStep = () => { const SelectPatientStep = () => {
return selectedPatient ? ( return selectedPatient ? (
@ -139,9 +134,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
> >
Выбрать другого пациента Выбрать другого пациента
</Button> </Button>
</div> </div>) : (<>
) : (
<>
<Input <Input
placeholder="Поиск пациента" placeholder="Поиск пациента"
value={searchPatientString} value={searchPatientString}
@ -155,13 +148,11 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
items={patientsItems} items={patientsItems}
/> />
</div> </div>
</> </>)
)
}; };
const SelectLensStep = () => { const SelectLensStep = () => {
return selectedLens ? ( return selectedLens ? (<div style={{padding: "10px", background: "#f5f5f5", borderRadius: 5, marginBottom: 15}}>
<div style={{padding: "10px", background: "#f5f5f5", borderRadius: 5, marginBottom: 15}}>
<Typography.Text strong> <Typography.Text strong>
{selectedLens.diameter} {selectedLens.tor} {selectedLens.preset_refraction} {selectedLens.diameter} {selectedLens.tor} {selectedLens.preset_refraction}
</Typography.Text> </Typography.Text>
@ -181,9 +172,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
> >
Выбрать другую линзу Выбрать другую линзу
</Button> </Button>
</div> </div>) : (<>
) : (
<>
<Input <Input
placeholder="Поиск линз" placeholder="Поиск линз"
value={searchLensString} value={searchLensString}
@ -197,13 +186,11 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
items={lensesItems} items={lensesItems}
/> />
</div> </div>
</> </>)
)
}; };
const ConfirmStep = () => { const ConfirmStep = () => {
return ( return (<>
<>
<Alert <Alert
type="warning" type="warning"
message="Внимание! После подтверждения линза будет считаться выданной, данное действие нельзя будет отменить." message="Внимание! После подтверждения линза будет считаться выданной, данное действие нельзя будет отменить."
@ -251,24 +238,16 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
<p><b>Сторона:</b> {selectedLens.side}</p> <p><b>Сторона:</b> {selectedLens.side}</p>
<p><b>Esa:</b> {selectedLens.esa}</p> <p><b>Esa:</b> {selectedLens.esa}</p>
</div> </div>
</> </>);
);
}; };
const steps = [ const steps = [{
{ title: 'Выбор пациента', content: <SelectPatientStep/>,
title: 'Выбор пациента', }, {
content: <SelectPatientStep/>, title: 'Выбор линзы', content: <SelectLensStep/>,
}, }, {
{ title: 'Подтверждение', content: <ConfirmStep/>,
title: 'Выбор линзы', },];
content: <SelectLensStep/>,
},
{
title: 'Подтверждение',
content: <ConfirmStep/>,
},
];
const isActiveNextButton = () => { const isActiveNextButton = () => {
if (currentStep === 0 && !selectedPatient) { if (currentStep === 0 && !selectedPatient) {
@ -286,8 +265,7 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
return currentStep === steps.length - 1; return currentStep === steps.length - 1;
}; };
return ( return (<Modal
<Modal
title="Выдача линзы пациенту" title="Выдача линзы пациенту"
open={visible} open={visible}
onCancel={() => { onCancel={() => {
@ -299,28 +277,22 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
}} }}
footer={null} footer={null}
maskClosable={false} maskClosable={false}
width={window.innerWidth > 768 ? 700 : "90%"} width={!screens.xs ? 700 : "90%"}
centered centered
> >
{loading ? ( {loading ? (<div style={{display: "flex", justifyContent: "center", alignItems: "center", height: "70vh"}}>
<div style={{display: "flex", justifyContent: "center", alignItems: "center", height: "70vh"}}>
<Spin size="large"/> <Spin size="large"/>
</div> </div>) : (<div style={{maxHeight: "70vh", overflowY: "auto", padding: "10px"}}>
) : (
<div style={{maxHeight: "70vh", overflowY: "auto", padding: "10px"}}>
{steps[currentStep].content} {steps[currentStep].content}
</div> </div>)}
)}
{window.innerWidth > 768 && ( {!screens.xs && (<Steps
<Steps
current={currentStep} current={currentStep}
items={steps} items={steps}
style={{marginTop: 16}} style={{marginTop: 16}}
direction={window.innerWidth > 768 ? "horizontal" : "vertical"} direction={!screens.xs ? "horizontal" : "vertical"}
/> />)}
)}
<Row <Row
justify="end" justify="end"
@ -348,14 +320,11 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
{isActiveFinishButton() ? "Завершить" : "Далее"} {isActiveFinishButton() ? "Завершить" : "Далее"}
</Button> </Button>
</Row> </Row>
</Modal> </Modal>);
);
}; };
LensIssueFormModal.propTypes = { LensIssueFormModal.propTypes = {
visible: PropTypes.bool.isRequired, visible: PropTypes.bool.isRequired, onCancel: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
}; };
export default LensIssueFormModal; 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 getAllLensIssues from "../api/lens_issues/GetAllLensIssues.jsx";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {useAuth} from "../AuthContext.jsx"; 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 LensIssueViewModal from "../components/lens_issues/LensIssueViewModal.jsx";
import dayjs from "dayjs"; import dayjs from "dayjs";
import LensIssueFormModal from "../components/lens_issues/LensIssueFormModal.jsx"; 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";
const {Title} = Typography; const {Title} = Typography;
const {useBreakpoint} = Grid;
const IssuesPage = () => { const IssuesPage = () => {
const {user} = useAuth(); const {user} = useAuth();
const screens = useBreakpoint();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [lensIssues, setLensIssues] = useState([]); const [lensIssues, setLensIssues] = useState([]);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [selectedIssue, setSelectedIssue] = useState(null); const [selectedIssue, setSelectedIssue] = useState(null);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [viewMode, setViewMode] = useState("table");
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
@ -27,6 +44,7 @@ const IssuesPage = () => {
useEffect(() => { useEffect(() => {
fetchLensIssuesWithCache(); fetchLensIssuesWithCache();
fetchViewModeFromCache();
document.title = "Выдача линз"; document.title = "Выдача линз";
}, []); }, []);
@ -49,6 +67,13 @@ const IssuesPage = () => {
} }
}; };
const fetchViewModeFromCache = () => {
const cachedViewMode = localStorage.getItem("viewModeIssues");
if (cachedViewMode) {
setViewMode(cachedViewMode);
}
};
const handleAddIssue = () => { const handleAddIssue = () => {
setSelectedIssue(null); setSelectedIssue(null);
setIsModalVisible(true); 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 = [ const columns = [
{ {
title: "Дата выдачи", 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 ( return (
<div style={{padding: 20}}> <div style={{padding: 20}}>
<Title level={1}><DatabaseOutlined/> Выдача линз</Title> <Title level={1}><DatabaseOutlined/> Выдача линз</Title>
<Row gutter={[16, 16]} style={{marginBottom: 20}}> <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 <Input
placeholder="Поиск по пациенту или дате" placeholder="Поиск по пациенту или дате"
onChange={handleSearch} onChange={handleSearch}
style={{marginBottom: 20, width: "100%"}} style={{width: "100%"}}
allowClear allowClear
/> />
</Col> </Col>
<Col xs={24} md={ <Col xs={24} md={
startFilterDate && endFilterDate ? 8 : 12 startFilterDate && endFilterDate ? 12 : 16
} sm={ } sm={
startFilterDate && endFilterDate ? 16 : 24 16
} xl={ } xl={
startFilterDate && endFilterDate ? 8 : 10 startFilterDate && endFilterDate ? 6 : 8
}> }>
<Tooltip <Tooltip
title="Фильтр по дате выдачи линзы" title="Фильтр по дате выдачи линзы"
@ -203,7 +293,6 @@ const IssuesPage = () => {
setStartFilterDate(null); setStartFilterDate(null);
setEndFilterDate(null); setEndFilterDate(null);
}} }}
style={{marginLeft: 10}}
type={"primary"} type={"primary"}
block block
> >
@ -212,29 +301,27 @@ const IssuesPage = () => {
</Tooltip> </Tooltip>
</Col> </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> </Row>
{loading ? ( {loading ? (
<div style={{display: "flex", justifyContent: "center", alignItems: "center", height: "100vh"}}> <div style={{display: "flex", justifyContent: "center", alignItems: "center", height: "100vh"}}>
<Spin indicator={<LoadingOutlined style={{fontSize: 64, color: "#1890ff"}} spin/>}/> <Spin indicator={<LoadingOutlined style={{fontSize: 64, color: "#1890ff"}} spin/>}/>
</div> </div>
) : viewMode === "table" ? (
<TableView/>
) : ( ) : (
<Table <TimeLineView/>
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}
/>
)} )}
<FloatButton <FloatButton

View File

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

View File

@ -15,10 +15,11 @@ import {
Typography Typography
} from "antd"; } from "antd";
import { import {
BuildOutlined,
LoadingOutlined, LoadingOutlined,
PlusOutlined, PlusOutlined,
SortAscendingOutlined, SortAscendingOutlined,
SortDescendingOutlined, SortDescendingOutlined, TableOutlined,
TeamOutlined TeamOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import {useAuth} from "../AuthContext.jsx"; 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 = [ const columns = [
{ {
title: "Фамилия", title: "Фамилия",
@ -352,6 +366,7 @@ const PatientsPage = () => {
setViewMode={setViewMode} setViewMode={setViewMode}
localStorageKey={"viewModePatients"} localStorageKey={"viewModePatients"}
toolTipText={"Формат отображения пациентов"} toolTipText={"Формат отображения пациентов"}
viewModes={viewModes}
/> />
</Col> </Col>
</Row> </Row>