feat: Добавлена страница рассылок

Добавлена страница для отправки email рассылок пациентам.
This commit is contained in:
Андрей Дувакин 2025-07-03 14:42:54 +05:00
parent a5fccd0710
commit 44c7f031cc
5 changed files with 264 additions and 1 deletions

View File

@ -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)

View File

@ -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,

View File

@ -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)

View 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;

View 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;