feat: Добавлена страница рассылок
Добавлена страница для отправки email рассылок пациентам.
This commit is contained in:
parent
a5fccd0710
commit
44c7f031cc
@ -11,7 +11,8 @@ class PatientsRepository:
|
||||
self.db = db
|
||||
|
||||
async def get_all(self, skip: int = 0, limit: int = 100, search: str = None,
|
||||
sort_order: Literal["asc", "desc"] = "asc", all_params: bool = False) -> Tuple[Sequence[Patient], int]:
|
||||
sort_order: Literal["asc", "desc"] = "asc", all_params: bool = False) -> Tuple[
|
||||
Sequence[Patient], int]:
|
||||
stmt = select(Patient)
|
||||
|
||||
if search:
|
||||
@ -58,6 +59,11 @@ class PatientsRepository:
|
||||
|
||||
return patients, total_count
|
||||
|
||||
async def get_width_email(self) -> Sequence[Patient]:
|
||||
stmt = select(Patient).filter(Patient.email != None)
|
||||
result = await self.db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_by_id(self, patient_id: int) -> Optional[Patient]:
|
||||
stmt = select(Patient).filter_by(id=patient_id)
|
||||
result = await self.db.execute(stmt)
|
||||
|
||||
@ -47,6 +47,20 @@ async def create_patient(
|
||||
return await patients_service.create_patient(patient)
|
||||
|
||||
|
||||
@router.get(
|
||||
'/email/',
|
||||
response_model=list[PatientEntity],
|
||||
summary="Get all patients with email",
|
||||
description="Returns all patients with email",
|
||||
)
|
||||
async def get_all_patients_with_email(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
patients_service = PatientsService(db)
|
||||
return await patients_service.get_patients_with_email()
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{patient_id}/",
|
||||
response_model=PatientEntity,
|
||||
|
||||
@ -24,6 +24,13 @@ class PatientsService:
|
||||
total_count
|
||||
)
|
||||
|
||||
async def get_patients_with_email(self) -> list[PatientEntity]:
|
||||
patients = await self.patient_repository.get_width_email()
|
||||
return [
|
||||
self.model_to_entity(patient)
|
||||
for patient in patients
|
||||
]
|
||||
|
||||
async def create_patient(self, patient: PatientEntity) -> PatientEntity:
|
||||
patient_model = self.entity_to_model(patient)
|
||||
|
||||
|
||||
127
web-app/src/Components/Pages/MailingPage/MailingPage.jsx
Normal file
127
web-app/src/Components/Pages/MailingPage/MailingPage.jsx
Normal file
@ -0,0 +1,127 @@
|
||||
import { Button, Card, Input, Table, Typography, Result, Space } from "antd";
|
||||
import { MailOutlined, SearchOutlined } from "@ant-design/icons";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import useMailingPage from "./useMailingPage.js";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
const MailingPage = () => {
|
||||
const {
|
||||
patientsWithEmail,
|
||||
isLoading,
|
||||
isError,
|
||||
selectedPatientIds,
|
||||
subject,
|
||||
body,
|
||||
searchQuery,
|
||||
containerStyle,
|
||||
cardStyle,
|
||||
buttonStyle,
|
||||
handleSelectPatients,
|
||||
handleSubjectChange,
|
||||
handleBodyChange,
|
||||
handleSearchChange,
|
||||
handleSendEmail,
|
||||
isMobile,
|
||||
} = useMailingPage();
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Result
|
||||
status="error"
|
||||
title="Ошибка"
|
||||
subTitle="Произошла ошибка при загрузке данных пациентов"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "Фамилия",
|
||||
dataIndex: "last_name",
|
||||
key: "last_name",
|
||||
sorter: (a, b) => a.last_name.localeCompare(b.last_name),
|
||||
},
|
||||
{
|
||||
title: "Имя",
|
||||
dataIndex: "first_name",
|
||||
key: "first_name",
|
||||
sorter: (a, b) => a.first_name.localeCompare(b.first_name),
|
||||
},
|
||||
{
|
||||
title: "Email",
|
||||
dataIndex: "email",
|
||||
key: "email",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<Title level={1}><MailOutlined /> Рассылки</Title>
|
||||
{isLoading ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<>
|
||||
<Card style={cardStyle} title="Выбор пациентов">
|
||||
<Input
|
||||
placeholder="Поиск по имени или email"
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
style={{ marginBottom: 16, width: isMobile ? "100%" : 300 }}
|
||||
/>
|
||||
<Table
|
||||
rowSelection={{
|
||||
type: "checkbox",
|
||||
selectedRowKeys: selectedPatientIds,
|
||||
onChange: handleSelectPatients,
|
||||
}}
|
||||
columns={columns}
|
||||
dataSource={patientsWithEmail.map((patient) => ({
|
||||
...patient,
|
||||
key: patient.id,
|
||||
}))}
|
||||
pagination={{ pageSize: 10 }}
|
||||
rowKey="id"
|
||||
size={isMobile ? "small" : "middle"}
|
||||
/>
|
||||
</Card>
|
||||
<Card style={cardStyle} title="Создание письма">
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<div>
|
||||
<Text strong>Тема письма:</Text>
|
||||
<Input
|
||||
placeholder="Введите тему письма"
|
||||
value={subject}
|
||||
onChange={handleSubjectChange}
|
||||
style={{ marginTop: 8 }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Текст письма:</Text>
|
||||
<TextArea
|
||||
placeholder="Введите текст письма"
|
||||
value={body}
|
||||
onChange={handleBodyChange}
|
||||
rows={6}
|
||||
style={{ marginTop: 8 }}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<MailOutlined />}
|
||||
onClick={handleSendEmail}
|
||||
style={buttonStyle}
|
||||
>
|
||||
Отправить письмо
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MailingPage;
|
||||
109
web-app/src/Components/Pages/MailingPage/useMailingPage.js
Normal file
109
web-app/src/Components/Pages/MailingPage/useMailingPage.js
Normal file
@ -0,0 +1,109 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { notification } from "antd";
|
||||
import { Grid } from "antd";
|
||||
import { useGetPatientsWithEmailQuery } from "../../../Api/patientsApi.js";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const useMailingPage = () => {
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const {
|
||||
data: patientsWithEmail = [],
|
||||
isLoading: isLoadingPatients,
|
||||
isError: isErrorPatients,
|
||||
} = useGetPatientsWithEmailQuery(undefined, {
|
||||
pollingInterval: 10000,
|
||||
});
|
||||
|
||||
const [selectedPatientIds, setSelectedPatientIds] = useState([]);
|
||||
const [subject, setSubject] = useState("");
|
||||
const [body, setBody] = useState("");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const containerStyle = { padding: screens.xs ? 16 : 24 };
|
||||
const cardStyle = { marginBottom: 24 };
|
||||
const buttonStyle = { width: screens.xs ? "100%" : "auto", marginTop: 16 };
|
||||
|
||||
const filteredPatients = useMemo(() => {
|
||||
if (!searchQuery) return patientsWithEmail;
|
||||
const lowerQuery = searchQuery.toLowerCase();
|
||||
return patientsWithEmail.filter(
|
||||
(patient) =>
|
||||
`${patient.last_name} ${patient.first_name}`.toLowerCase().includes(lowerQuery) ||
|
||||
patient.email.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
}, [patientsWithEmail, searchQuery]);
|
||||
|
||||
const handleSelectPatients = (selectedRowKeys) => {
|
||||
setSelectedPatientIds(selectedRowKeys);
|
||||
};
|
||||
|
||||
const handleSubjectChange = (e) => {
|
||||
setSubject(e.target.value);
|
||||
};
|
||||
|
||||
const handleBodyChange = (e) => {
|
||||
setBody(e.target.value);
|
||||
};
|
||||
|
||||
const handleSearchChange = (e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
};
|
||||
|
||||
const generateMailtoLink = () => {
|
||||
if (!selectedPatientIds.length) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: "Выберите хотя бы одного пациента.",
|
||||
placement: "topRight",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
if (!subject || !body) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: "Заполните тему и текст письма.",
|
||||
placement: "topRight",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedEmails = patientsWithEmail
|
||||
.filter((patient) => selectedPatientIds.includes(patient.id))
|
||||
.map((patient) => patient.email)
|
||||
.join(",");
|
||||
|
||||
const encodedSubject = encodeURIComponent(subject);
|
||||
const encodedBody = encodeURIComponent(body);
|
||||
return `mailto:?bcc=${selectedEmails}&subject=${encodedSubject}&body=${encodedBody}`;
|
||||
};
|
||||
|
||||
const handleSendEmail = () => {
|
||||
const mailtoLink = generateMailtoLink();
|
||||
if (mailtoLink) {
|
||||
window.location.href = mailtoLink;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
patientsWithEmail: filteredPatients,
|
||||
isLoading: isLoadingPatients,
|
||||
isError: isErrorPatients,
|
||||
selectedPatientIds,
|
||||
subject,
|
||||
body,
|
||||
searchQuery,
|
||||
containerStyle,
|
||||
cardStyle,
|
||||
buttonStyle,
|
||||
handleSelectPatients,
|
||||
handleSubjectChange,
|
||||
handleBodyChange,
|
||||
handleSearchChange,
|
||||
handleSendEmail,
|
||||
isMobile: screens.xs,
|
||||
};
|
||||
};
|
||||
|
||||
export default useMailingPage;
|
||||
Loading…
x
Reference in New Issue
Block a user