Compare commits

...

10 Commits

Author SHA1 Message Date
76b72dce1c refactor(useIssues): Улучшен поиск и фильтрация Issues 2025-06-08 14:37:31 +05:00
0a7fd16a29 feat: Исправление фильтрации и UI для выдачи линз 2025-06-08 14:35:03 +05:00
b8bc7023a0 feat: lensIssuesApi
fix(IssuesPage): Исправлены фильтры и пагинация.
2025-06-08 14:18:05 +05:00
f0a712eb9d feat: Добавлена загрузка и отображение данных на главной странице
Добавлены запросы для получения предстоящих и запланированных приемов.
Оптимизирована логика получения данных о приемах и расписании.
2025-06-08 13:26:48 +05:00
c609c6471d feat: Добавлена поддержка предстоящих приемов 2025-06-08 13:21:46 +05:00
3a22fd05be feat: Добавлена фильтрация и новые функции для приемов.
Добавлены фильтрация по дате для получения приемов и запланированных приемов. Добавлены функции открытия модалок.
2025-06-08 13:07:34 +05:00
3f3762c066 feat: Добавлена фильтрация по дате для приемов
Добавлена возможность фильтрации приемов и назначений по дате.
2025-06-08 12:50:13 +05:00
118ae84930 refactor: AppointmentsPage: Refactoring UI logic
Перенос UI-логики из AppointmentsPage в useAppointments.
Удален useAppointmentsUI.js.
2025-06-08 10:27:31 +05:00
7a2733cda6 refactor: Замена useGetPatientsQuery на useGetAllPatientsQuery 2025-06-08 10:23:44 +05:00
7a2ef98fd5 feat: IssuesPage
Добавлены фильтрация, пагинация и поиск.
Улучшен UI.
Удален useIssuesUI.js.
Добавлен PaginatedLensIssuesResponseEntity.
2025-06-08 10:14:41 +05:00
33 changed files with 1095 additions and 640 deletions

View File

@ -1,8 +1,8 @@
from typing import Sequence, Optional
from sqlalchemy import select, desc
from sqlalchemy import select, desc, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from datetime import date
from app.domain.models import Appointment
@ -11,7 +11,7 @@ class AppointmentsRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_all(self) -> Sequence[Appointment]:
async def get_all(self, start_date: date | None = None, end_date: date | None = None) -> Sequence[Appointment]:
stmt = (
select(Appointment)
.options(joinedload(Appointment.type))
@ -19,10 +19,15 @@ class AppointmentsRepository:
.options(joinedload(Appointment.doctor))
.order_by(desc(Appointment.appointment_datetime))
)
if start_date:
stmt = stmt.filter(Appointment.appointment_datetime >= start_date)
if end_date:
stmt = stmt.filter(Appointment.appointment_datetime <= end_date)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_by_doctor_id(self, doctor_id: int) -> Sequence[Appointment]:
async def get_by_doctor_id(self, doctor_id: int, start_date: date | None = None, end_date: date | None = None) -> \
Sequence[Appointment]:
stmt = (
select(Appointment)
.options(joinedload(Appointment.type))
@ -31,10 +36,29 @@ class AppointmentsRepository:
.filter_by(doctor_id=doctor_id)
.order_by(desc(Appointment.appointment_datetime))
)
if start_date:
stmt = stmt.filter(Appointment.appointment_datetime >= start_date)
if end_date:
stmt = stmt.filter(Appointment.appointment_datetime <= end_date)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_by_patient_id(self, patient_id: int) -> Sequence[Appointment]:
async def get_upcoming_by_doctor_id(self, doctor_id: int) -> Sequence[Appointment]:
stmt = (
select(Appointment)
.options(joinedload(Appointment.type))
.options(joinedload(Appointment.patient))
.options(joinedload(Appointment.doctor))
.filter_by(doctor_id=doctor_id)
.filter(Appointment.appointment_datetime >= func.now())
.order_by(Appointment.appointment_datetime)
.limit(5)
)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_by_patient_id(self, patient_id: int, start_date: date | None = None, end_date: date | None = None) -> \
Sequence[Appointment]:
stmt = (
select(Appointment)
.options(joinedload(Appointment.type))
@ -43,6 +67,10 @@ class AppointmentsRepository:
.filter_by(patient_id=patient_id)
.order_by(desc(Appointment.appointment_datetime))
)
if start_date:
stmt = stmt.filter(Appointment.appointment_datetime >= start_date)
if end_date:
stmt = stmt.filter(Appointment.appointment_datetime <= end_date)
result = await self.db.execute(stmt)
return result.scalars().all()

View File

@ -1,26 +1,80 @@
from typing import Optional, Sequence
from datetime import date
from typing import Optional, Sequence, Tuple, Literal
from sqlalchemy import select, desc
from sqlalchemy import select, desc, or_, func, asc
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from app.domain.models import LensIssue
from app.domain.models import LensIssue, Patient, User
class LensIssuesRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_all(self) -> Sequence[LensIssue]:
async def get_all(
self,
skip: int = 0,
limit: int = 10,
search: Optional[str] = None,
sort_order: Literal["asc", "desc"] = "desc",
start_date: Optional[date] = None,
end_date: Optional[date] = None
) -> Tuple[Sequence[LensIssue], int]:
stmt = (
select(LensIssue)
.options(joinedload(LensIssue.lens))
.options(joinedload(LensIssue.patient))
.options(joinedload(LensIssue.doctor))
.order_by(desc(LensIssue.issue_date))
.join(Patient)
.join(User)
)
if search:
search = f"%{search}%"
stmt = stmt.filter(
or_(
Patient.last_name.ilike(search),
Patient.first_name.ilike(search),
User.last_name.ilike(search),
User.first_name.ilike(search)
)
)
if start_date:
stmt = stmt.filter(LensIssue.issue_date >= start_date)
if end_date:
stmt = stmt.filter(LensIssue.issue_date <= end_date)
stmt = stmt.order_by(
desc(LensIssue.issue_date) if sort_order == "desc" else asc(LensIssue.issue_date)
)
count_stmt = select(func.count()).select_from(LensIssue).join(Patient).join(User)
if search:
search = f"%{search}%"
count_stmt = count_stmt.filter(
or_(
Patient.last_name.ilike(search),
Patient.first_name.ilike(search),
User.last_name.ilike(search),
User.first_name.ilike(search)
)
)
if start_date:
count_stmt = count_stmt.filter(LensIssue.issue_date >= start_date)
if end_date:
count_stmt = count_stmt.filter(LensIssue.issue_date <= end_date)
stmt = stmt.offset(skip).limit(limit)
result = await self.db.execute(stmt)
return result.scalars().all()
issues = result.scalars().all()
count_result = await self.db.execute(count_stmt)
total_count = count_result.scalar()
return issues, total_count
async def get_by_id(self, lens_issue_id: int) -> Optional[LensIssue]:
stmt = select(LensIssue).filter_by(id=lens_issue_id)

View File

@ -10,8 +10,8 @@ class PatientsRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_all(self, skip: int = 0, limit: int = 10, search: str = None,
sort_order: Literal["asc", "desc"] = "asc") -> Tuple[Sequence[Patient], int]:
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]:
stmt = select(Patient)
if search:
@ -50,6 +50,12 @@ class PatientsRepository:
count_result = await self.db.execute(count_stmt)
total_count = count_result.scalar()
if not all_params:
stmt = stmt.offset(skip).limit(limit)
result = await self.db.execute(stmt)
patients = result.scalars().all()
return patients, total_count
async def get_by_id(self, patient_id: int) -> Optional[Patient]:

View File

@ -1,8 +1,8 @@
from typing import Sequence
from sqlalchemy import select, desc
from sqlalchemy import select, desc, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from datetime import date
from app.domain.models import ScheduledAppointment
@ -11,7 +11,8 @@ class ScheduledAppointmentsRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_all(self) -> Sequence[ScheduledAppointment]:
async def get_all(self, start_date: date | None = None, end_date: date | None = None) -> Sequence[
ScheduledAppointment]:
stmt = (
select(ScheduledAppointment)
.options(joinedload(ScheduledAppointment.type))
@ -20,10 +21,15 @@ class ScheduledAppointmentsRepository:
.filter_by(is_canceled=False)
.order_by(desc(ScheduledAppointment.scheduled_datetime))
)
if start_date:
stmt = stmt.filter(ScheduledAppointment.scheduled_datetime >= start_date)
if end_date:
stmt = stmt.filter(ScheduledAppointment.scheduled_datetime <= end_date)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_by_doctor_id(self, doctor_id: int) -> Sequence[ScheduledAppointment]:
async def get_by_doctor_id(self, doctor_id: int, start_date: date | None = None, end_date: date | None = None) -> \
Sequence[ScheduledAppointment]:
stmt = (
select(ScheduledAppointment)
.options(joinedload(ScheduledAppointment.type))
@ -32,10 +38,29 @@ class ScheduledAppointmentsRepository:
.filter_by(doctor_id=doctor_id, is_canceled=False)
.order_by(desc(ScheduledAppointment.scheduled_datetime))
)
if start_date:
stmt = stmt.filter(ScheduledAppointment.scheduled_datetime >= start_date)
if end_date:
stmt = stmt.filter(ScheduledAppointment.scheduled_datetime <= end_date)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_by_patient_id(self, patient_id: int) -> Sequence[ScheduledAppointment]:
async def get_upcoming_by_doctor_id(self, doctor_id: int) -> Sequence[ScheduledAppointment]:
stmt = (
select(ScheduledAppointment)
.options(joinedload(ScheduledAppointment.type))
.options(joinedload(ScheduledAppointment.patient))
.options(joinedload(ScheduledAppointment.doctor))
.filter_by(doctor_id=doctor_id, is_canceled=False)
.filter(ScheduledAppointment.scheduled_datetime >= func.now())
.order_by(ScheduledAppointment.scheduled_datetime)
.limit(5)
)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_by_patient_id(self, patient_id: int, start_date: date | None = None, end_date: date | None = None) -> \
Sequence[ScheduledAppointment]:
stmt = (
select(ScheduledAppointment)
.options(joinedload(ScheduledAppointment.type))
@ -44,6 +69,10 @@ class ScheduledAppointmentsRepository:
.filter_by(patient_id=patient_id, is_canceled=False)
.order_by(desc(ScheduledAppointment.scheduled_datetime))
)
if start_date:
stmt = stmt.filter(ScheduledAppointment.scheduled_datetime >= start_date)
if end_date:
stmt = stmt.filter(ScheduledAppointment.scheduled_datetime <= end_date)
result = await self.db.execute(stmt)
return result.scalars().all()

View File

@ -1,5 +1,6 @@
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import date
from app.database.session import get_db
from app.domain.entities.appointment import AppointmentEntity
@ -18,9 +19,11 @@ router = APIRouter()
async def get_all_appointments(
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
start_date: date | None = Query(None, description="Start date for filtering (YYYY-MM-DD)"),
end_date: date | None = Query(None, description="End date for filtering (YYYY-MM-DD)"),
):
appointments_service = AppointmentsService(db)
return await appointments_service.get_all_appointments()
return await appointments_service.get_all_appointments(start_date=start_date, end_date=end_date)
@router.get(
@ -33,9 +36,26 @@ async def get_all_appointments_by_doctor_id(
doctor_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
start_date: date | None = Query(None, description="Start date for filtering (YYYY-MM-DD)"),
end_date: date | None = Query(None, description="End date for filtering (YYYY-MM-DD)"),
):
appointments_service = AppointmentsService(db)
return await appointments_service.get_appointments_by_doctor_id(doctor_id)
return await appointments_service.get_appointments_by_doctor_id(doctor_id, start_date=start_date, end_date=end_date)
@router.get(
"/doctor/{doctor_id}/upcoming/",
response_model=list[AppointmentEntity],
summary="Get upcoming appointments for doctor",
description="Returns the next 5 upcoming appointments for doctor",
)
async def get_upcoming_appointments_by_doctor_id(
doctor_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
appointments_service = AppointmentsService(db)
return await appointments_service.get_upcoming_appointments_by_doctor_id(doctor_id)
@router.get(
@ -48,9 +68,12 @@ async def get_all_appointments_by_patient_id(
patient_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
start_date: date | None = Query(None, description="Start date for filtering (YYYY-MM-DD)"),
end_date: date | None = Query(None, description="End date for filtering (YYYY-MM-DD)"),
):
appointments_service = AppointmentsService(db)
return await appointments_service.get_appointments_by_patient_id(patient_id)
return await appointments_service.get_appointments_by_patient_id(patient_id, start_date=start_date,
end_date=end_date)
@router.post(

View File

@ -1,8 +1,12 @@
from fastapi import APIRouter, Depends
from datetime import date
from typing import Optional, Literal
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.lens_issues import LensIssueEntity
from app.domain.entities.responses.paginated_issue import PaginatedLensIssuesResponseEntity
from app.infrastructure.dependencies import get_current_user
from app.infrastructure.lens_issues_service import LensIssuesService
@ -11,17 +15,34 @@ router = APIRouter()
@router.get(
"/",
response_model=list[LensIssueEntity],
response_model=PaginatedLensIssuesResponseEntity,
summary="Get all lens issues",
description="Returns a list of all lens issues",
description="Returns a paginated list of lens issues with optional filtering and sorting",
)
async def get_all_lens_issues(
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
search: Optional[str] = Query(None),
sort_order: Literal["asc", "desc"] = Query("desc"),
start_date: Optional[date] = Query(None),
end_date: Optional[date] = Query(None),
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
lens_issues_service = LensIssuesService(db)
return await lens_issues_service.get_all_lens_issues()
skip = (page - 1) * page_size
issues, total_count = await lens_issues_service.get_all_lens_issues(
skip=skip,
limit=page_size,
search=search,
sort_order=sort_order,
start_date=start_date,
end_date=end_date
)
return PaginatedLensIssuesResponseEntity(
issues=issues,
total_count=total_count
)
@router.post(
"/",

View File

@ -23,11 +23,12 @@ async def get_all_patients(
page_size: int = Query(10, ge=1, le=100, description="Number of patients per page"),
search: str = Query(None, description="Search term for filtering patients"),
sort_order: Literal["asc", "desc"] = Query("asc", description="Sort order by first_name (asc or desc)"),
all_params: bool = Query(False, description="Get all patients"),
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
patients_service = PatientsService(db)
patients, total_count = await patients_service.get_all_patients(page, page_size, search, sort_order)
patients, total_count = await patients_service.get_all_patients(page, page_size, search, sort_order, all_params)
return {"patients": patients, "total_count": total_count}

View File

@ -1,7 +1,7 @@
from typing import Optional
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import date
from app.database.session import get_db
from app.domain.entities.scheduled_appointment import ScheduledAppointmentEntity
@ -20,9 +20,11 @@ router = APIRouter()
async def get_all_scheduled_appointments(
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
start_date: date | None = Query(None, description="Start date for filtering (YYYY-MM-DD)"),
end_date: date | None = Query(None, description="End date for filtering (YYYY-MM-DD)"),
):
scheduled_appointments_service = ScheduledAppointmentsService(db)
return await scheduled_appointments_service.get_all_scheduled_appointments()
return await scheduled_appointments_service.get_all_scheduled_appointments(start_date=start_date, end_date=end_date)
@router.get(
@ -35,9 +37,27 @@ async def get_all_scheduled_appointments_by_doctor_id(
doctor_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
start_date: date | None = Query(None, description="Start date for filtering (YYYY-MM-DD)"),
end_date: date | None = Query(None, description="End date for filtering (YYYY-MM-DD)"),
):
appointments_service = ScheduledAppointmentsService(db)
return await appointments_service.get_scheduled_appointments_by_doctor_id(doctor_id)
return await appointments_service.get_scheduled_appointments_by_doctor_id(doctor_id, start_date=start_date,
end_date=end_date)
@router.get(
"/doctor/{doctor_id}/upcoming/",
response_model=list[ScheduledAppointmentEntity],
summary="Get upcoming scheduled appointments for doctor",
description="Returns the next 5 upcoming scheduled appointments for doctor",
)
async def get_upcoming_scheduled_appointments_by_doctor_id(
doctor_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
appointments_service = ScheduledAppointmentsService(db)
return await appointments_service.get_upcoming_scheduled_appointments_by_doctor_id(doctor_id)
@router.get(
@ -50,9 +70,12 @@ async def get_all_appointments_by_patient_id(
patient_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
start_date: date | None = Query(None, description="Start date for filtering (YYYY-MM-DD)"),
end_date: date | None = Query(None, description="End date for filtering (YYYY-MM-DD)"),
):
appointments_service = ScheduledAppointmentsService(db)
return await appointments_service.get_scheduled_appointments_by_patient_id(patient_id)
return await appointments_service.get_scheduled_appointments_by_patient_id(patient_id, start_date=start_date,
end_date=end_date)
@router.post(

View File

@ -0,0 +1,8 @@
from pydantic import BaseModel
from app.domain.entities.lens_issues import LensIssueEntity
class PaginatedLensIssuesResponseEntity(BaseModel):
issues: list[LensIssueEntity]
total_count: int

View File

@ -1,8 +1,8 @@
from typing import Optional
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status
from datetime import date
from app.application.appointment_types_repository import AppointmentTypesRepository
from app.application.appointments_repository import AppointmentsRepository
@ -22,45 +22,44 @@ class AppointmentsService:
self.users_repository = UsersRepository(db)
self.patients_repository = PatientsRepository(db)
async def get_all_appointments(self) -> list[AppointmentEntity]:
appointments = await self.appointments_repository.get_all()
async def get_all_appointments(self, start_date: date | None = None, end_date: date | None = None) -> list[
AppointmentEntity]:
appointments = await self.appointments_repository.get_all(start_date=start_date, end_date=end_date)
return [self.model_to_entity(appointment) for appointment in appointments]
return [
self.model_to_entity(appointment)
for appointment in appointments
]
async def get_appointments_by_doctor_id(self, doctor_id: int) -> Optional[list[AppointmentEntity]]:
async def get_appointments_by_doctor_id(self, doctor_id: int, start_date: date | None = None,
end_date: date | None = None) -> Optional[list[AppointmentEntity]]:
doctor = await self.users_repository.get_by_id(doctor_id)
if not doctor:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Доктор с таким ID не найден',
)
appointments = await self.appointments_repository.get_by_doctor_id(doctor_id, start_date=start_date,
end_date=end_date)
return [self.model_to_entity(appointment) for appointment in appointments]
appointments = await self.appointments_repository.get_by_doctor_id(doctor_id)
async def get_upcoming_appointments_by_doctor_id(self, doctor_id: int) -> Optional[list[AppointmentEntity]]:
doctor = await self.users_repository.get_by_id(doctor_id)
if not doctor:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Доктор с таким ID не найден',
)
appointments = await self.appointments_repository.get_upcoming_by_doctor_id(doctor_id)
return [self.model_to_entity(appointment) for appointment in appointments]
return [
self.model_to_entity(appointment)
for appointment in appointments
]
async def get_appointments_by_patient_id(self, patient_id: int) -> Optional[list[AppointmentEntity]]:
async def get_appointments_by_patient_id(self, patient_id: int, start_date: date | None = None,
end_date: date | None = None) -> Optional[list[AppointmentEntity]]:
patient = await self.patients_repository.get_by_id(patient_id)
if not patient:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Пациент с таким ID не найден',
)
appointments = await self.appointments_repository.get_by_patient_id(patient_id)
return [
self.model_to_entity(appointment)
for appointment in appointments
]
appointments = await self.appointments_repository.get_by_patient_id(patient_id, start_date=start_date,
end_date=end_date)
return [self.model_to_entity(appointment) for appointment in appointments]
async def create_appointment(self, appointment: AppointmentEntity, doctor_id: int) -> Optional[AppointmentEntity]:
patient = await self.patients_repository.get_by_id(appointment.patient_id)

View File

@ -1,4 +1,5 @@
from typing import Optional
from datetime import date
from typing import Optional, Literal, Tuple
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
@ -22,13 +23,27 @@ class LensIssuesService:
self.users_repository = UsersRepository(db)
self.lenses_repository = LensesRepository(db)
async def get_all_lens_issues(self) -> list[LensIssueEntity]:
lens_issues = await self.lens_issues_repository.get_all()
return [
self.model_to_entity(lens_issue)
for lens_issue in lens_issues
]
async def get_all_lens_issues(
self,
skip: int = 0,
limit: int = 10,
search: Optional[str] = None,
sort_order: Literal["asc", "desc"] = "desc",
start_date: Optional[date] = None,
end_date: Optional[date] = None
) -> Tuple[list[LensIssueEntity], int]:
lens_issues, total_count = await self.lens_issues_repository.get_all(
skip=skip,
limit=limit,
search=search,
sort_order=sort_order,
start_date=start_date,
end_date=end_date
)
return (
[self.model_to_entity(lens_issue) for lens_issue in lens_issues],
total_count
)
async def create_lens_issue(self, lens_issue: LensIssueEntity, user_id: int) -> Optional[LensIssueEntity]:
patient = await self.patient_repository.get_by_id(lens_issue.patient_id)

View File

@ -13,10 +13,12 @@ class PatientsService:
def __init__(self, db: AsyncSession):
self.patient_repository = PatientsRepository(db)
async def get_all_patients(self, page: int = 1, page_size: int = 10, search: str = None, sort_order: Literal["asc", "desc"] = "asc") -> Tuple[
async def get_all_patients(self, page: int = 1, page_size: int = 10, search: str = None,
sort_order: Literal["asc", "desc"] = "asc", all_params: bool = False) -> Tuple[
list[PatientEntity], int]:
skip = (page - 1) * page_size
patients, total_count = await self.patient_repository.get_all(skip=skip, limit=page_size, search=search, sort_order=sort_order)
patients, total_count = await self.patient_repository.get_all(skip=skip, limit=page_size, search=search,
sort_order=sort_order, all_params=all_params)
return (
[self.model_to_entity(patient) for patient in patients],
total_count

View File

@ -1,8 +1,8 @@
from typing import Optional
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status
from datetime import date
from app.application.appointment_types_repository import AppointmentTypesRepository
from app.application.patients_repository import PatientsRepository
@ -19,54 +19,53 @@ class ScheduledAppointmentsService:
self.users_repository = UsersRepository(db)
self.patients_repository = PatientsRepository(db)
async def get_all_scheduled_appointments(self) -> list[ScheduledAppointmentEntity]:
scheduled_appointments = await self.scheduled_appointment_repository.get_all()
return [
self.model_to_entity(scheduled_appointment)
for scheduled_appointment in scheduled_appointments
]
async def get_scheduled_appointments_by_doctor_id(self, doctor_id: int) -> Optional[
list[ScheduledAppointmentEntity]
]:
doctor = self.users_repository.get_by_id(doctor_id)
async def get_all_scheduled_appointments(self, start_date: date | None = None, end_date: date | None = None) -> \
list[ScheduledAppointmentEntity]:
scheduled_appointments = await self.scheduled_appointment_repository.get_all(start_date=start_date,
end_date=end_date)
return [self.model_to_entity(scheduled_appointment) for scheduled_appointment in scheduled_appointments]
async def get_scheduled_appointments_by_doctor_id(self, doctor_id: int, start_date: date | None = None,
end_date: date | None = None) -> Optional[
list[ScheduledAppointmentEntity]]:
doctor = await self.users_repository.get_by_id(doctor_id)
if not doctor:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Доктор с таким ID не найден',
)
scheduled_appointments = await self.scheduled_appointment_repository.get_by_doctor_id(doctor_id,
start_date=start_date,
end_date=end_date)
return [self.model_to_entity(scheduled_appointment) for scheduled_appointment in scheduled_appointments]
scheduled_appointments = await self.scheduled_appointment_repository.get_by_doctor_id(doctor_id)
async def get_upcoming_scheduled_appointments_by_doctor_id(self, doctor_id: int) -> Optional[
list[ScheduledAppointmentEntity]]:
doctor = await self.users_repository.get_by_id(doctor_id)
if not doctor:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Доктор с таким ID не найден',
)
scheduled_appointments = await self.scheduled_appointment_repository.get_upcoming_by_doctor_id(doctor_id)
return [self.model_to_entity(scheduled_appointment) for scheduled_appointment in scheduled_appointments]
return [
self.model_to_entity(scheduled_appointment)
for scheduled_appointment in scheduled_appointments
]
async def get_scheduled_appointments_by_patient_id(self, patient_id: int) -> Optional[
list[ScheduledAppointmentEntity]
]:
async def get_scheduled_appointments_by_patient_id(self, patient_id: int, start_date: date | None = None,
end_date: date | None = None) -> Optional[
list[ScheduledAppointmentEntity]]:
patient = await self.patients_repository.get_by_id(patient_id)
if not patient:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Пациент с таким ID не найден',
)
scheduled_appointments = await self.scheduled_appointment_repository.get_by_patient_id(patient_id)
return [
self.model_to_entity(scheduled_appointment)
for scheduled_appointment in scheduled_appointments
]
scheduled_appointments = await self.scheduled_appointment_repository.get_by_patient_id(patient_id,
start_date=start_date,
end_date=end_date)
return [self.model_to_entity(scheduled_appointment) for scheduled_appointment in scheduled_appointments]
async def create_scheduled_appointment(self, scheduled_appointment: ScheduledAppointmentEntity, doctor_id: int) -> \
Optional[
ScheduledAppointmentEntity
]:
Optional[ScheduledAppointmentEntity]:
patient = await self.patients_repository.get_by_id(scheduled_appointment.patient_id)
if not patient:
@ -99,7 +98,7 @@ class ScheduledAppointmentsService:
return self.model_to_entity(scheduled_appointment_model)
async def cancel_scheduled_appointment(self, scheduled_appointment_id: int, doctor_id):
async def cancel_scheduled_appointment(self, scheduled_appointment_id: int, doctor_id: int):
scheduled_appointment_model = await self.scheduled_appointment_repository.get_by_id(scheduled_appointment_id)
if not scheduled_appointment_model:
@ -129,11 +128,9 @@ class ScheduledAppointmentsService:
return self.model_to_entity(scheduled_appointment_model)
async def update_scheduled_appointment(
self,
scheduled_appointment_id: int,
scheduled_appointment: ScheduledAppointmentEntity
) -> Optional[ScheduledAppointmentEntity]:
async def update_scheduled_appointment(self, scheduled_appointment_id: int,
scheduled_appointment: ScheduledAppointmentEntity) -> Optional[
ScheduledAppointmentEntity]:
scheduled_appointment_model = await self.scheduled_appointment_repository.get_by_id(scheduled_appointment_id)
if not scheduled_appointment_model:

View File

@ -7,10 +7,17 @@ export const appointmentsApi = createApi({
tagTypes: ['Appointment'],
endpoints: (builder) => ({
getAppointments: builder.query({
query: (doctor_id) => `/appointments/doctor/${doctor_id}/`,
query: ({doctor_id, start_date, end_date}) => ({
url: `/appointments/doctor/${doctor_id}/`,
params: {start_date, end_date},
}),
providesTags: ['Appointment'],
refetchOnMountOrArgChange: 5,
}),
getUpcomingAppointments: builder.query({
query: (doctor_id) => `/appointments/doctor/${doctor_id}/upcoming/`,
providesTags: ['Appointment'],
}),
getByPatientId: builder.query({
query: (id) => `/appointments/patient/${id}/`,
providesTags: ['Appointment'],
@ -37,6 +44,7 @@ export const appointmentsApi = createApi({
export const {
useGetAppointmentsQuery,
useGetUpcomingAppointmentsQuery,
useGetByPatientIdQuery,
useCreateAppointmentMutation,
useUpdateAppointmentMutation,

View File

@ -1,6 +1,5 @@
import {createApi} from "@reduxjs/toolkit/query/react";
import {baseQueryWithAuth} from "./baseQuery.js";
import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQueryWithAuth } from "./baseQuery.js";
export const lensIssuesApi = createApi({
reducerPath: 'lensIssuesApi',
@ -8,22 +7,49 @@ export const lensIssuesApi = createApi({
tagTypes: ['LensIssues'],
endpoints: (builder) => ({
getLensIssues: builder.query({
query: () => '/lens_issues/',
query: ({ page, pageSize, search, sortOrder, startDate, endDate }) => ({
url: '/lens_issues/',
params: {
page,
page_size: pageSize,
search: search || undefined,
sort_order: sortOrder || undefined,
start_date: startDate || undefined,
end_date: endDate || undefined,
},
}),
providesTags: ['LensIssues'],
refetchOnMountOrArgChange: 5
transformResponse: (response) => {
if (!response) {
console.warn('Empty lens issues API response:', response);
return { issues: [], total_count: 0 };
}
if (Array.isArray(response.results) && typeof response.count === 'number') {
return { issues: response.results, total_count: response.count };
}
if (Array.isArray(response.issues) && typeof response.total_count === 'number') {
return response;
}
console.warn('Unexpected lens issues API response:', response);
return { issues: [], total_count: 0 };
},
transformErrorResponse: (response) => {
console.error('Lens issues API error:', response);
return response.data?.detail || 'Unknown error';
},
}),
addLensIssues: builder.mutation({
query: (lensIssues) => ({
url: `/lens_issues/`,
url: '/lens_issues/',
method: 'POST',
body: lensIssues
body: lensIssues,
}),
invalidatesTags: ['LensIssues']
invalidatesTags: ['LensIssues'],
}),
}),
});
export const {
useGetLensIssuesQuery,
useAddLensIssuesMutation
useAddLensIssuesMutation,
} = lensIssuesApi;

View File

@ -18,6 +18,20 @@ export const patientsApi = createApi({
}),
providesTags: ['Patients'],
}),
getAllPatients: builder.query({
query: () => ({
url: '/patients/',
params: { all_params: true },
}),
providesTags: ['Patient'],
transformResponse: (response) => {
if (!response || !Array.isArray(response.patients)) {
console.warn('Unexpected patients API response:', response);
return [];
}
return response.patients;
},
}),
addPatient: builder.mutation({
query: (patient) => ({
url: '/patients/',
@ -46,6 +60,7 @@ export const patientsApi = createApi({
export const {
useGetPatientsQuery,
useGetAllPatientsQuery,
useAddPatientMutation,
useUpdatePatientMutation,
useDeletePatientMutation,

View File

@ -1,5 +1,5 @@
import {createApi} from "@reduxjs/toolkit/query/react";
import {baseQueryWithAuth} from "./baseQuery.js";
import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQueryWithAuth } from "./baseQuery.js";
export const scheduledAppointmentsApi = createApi({
reducerPath: 'scheduledAppointmentsApi',
@ -7,7 +7,14 @@ export const scheduledAppointmentsApi = createApi({
tagTypes: ['ScheduledAppointment'],
endpoints: (builder) => ({
getScheduledAppointments: builder.query({
query: (doctor_id) => `/scheduled_appointments/doctor/${doctor_id}/`,
query: ({ doctor_id, start_date, end_date }) => ({
url: `/scheduled_appointments/doctor/${doctor_id}/`,
params: { start_date, end_date },
}),
providesTags: ['ScheduledAppointment'],
}),
getUpcomingScheduledAppointments: builder.query({
query: (doctor_id) => `/scheduled_appointments/doctor/${doctor_id}/upcoming/`,
providesTags: ['ScheduledAppointment'],
}),
createScheduledAppointment: builder.mutation({
@ -19,7 +26,7 @@ export const scheduledAppointmentsApi = createApi({
invalidatesTags: ['ScheduledAppointment'],
}),
updateScheduledAppointment: builder.mutation({
query: ({id, data}) => ({
query: ({ id, data }) => ({
url: `/scheduled_appointments/${id}/`,
method: 'PUT',
body: data,
@ -38,6 +45,7 @@ export const scheduledAppointmentsApi = createApi({
export const {
useGetScheduledAppointmentsQuery,
useGetUpcomingScheduledAppointmentsQuery,
useCreateScheduledAppointmentMutation,
useUpdateScheduledAppointmentMutation,
useCancelScheduledAppointmentMutation,

View File

@ -1,4 +1,4 @@
import {useGetPatientsQuery} from "../../../Api/patientsApi.js";
import {useGetAllPatientsQuery, useGetPatientsQuery} from "../../../Api/patientsApi.js";
import {useGetAppointmentTypesQuery} from "../../../Api/appointmentTypesApi.js";
import {
useCreateAppointmentMutation,
@ -11,7 +11,7 @@ const useAppointmentFormModal = () => {
data: patients = [],
isLoading: isLoadingPatients,
isError: isErrorPatients,
} = useGetPatientsQuery(undefined);
} = useGetAllPatientsQuery(undefined);
const {
data: appointmentTypes = [],
isLoading: isLoadingAppointmentTypes,

View File

@ -6,7 +6,6 @@ import dayjs from "dayjs";
import {useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js";
import {Grid} from "antd";
import {useUploadAppointmentFileMutation} from "../../../Api/appointmentFilesApi.js";
import Compressor from 'compressorjs';
const {useBreakpoint} = Grid;
@ -30,9 +29,20 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
const [uploadAppointmentFile, {isLoading: isUploadingFile}] = useUploadAppointmentFileMutation();
const {data: appointments = []} = useGetAppointmentsQuery(userData.id, {
pollingInterval: 20000,
});
const startDate = appointmentDate.startOf('month').utc().format('YYYY-MM-DD');
const endDate = appointmentDate.endOf('month').utc().format('YYYY-MM-DD');
const {
data: appointments = [],
isLoading: isLoadingAppointments,
isError: isErrorAppointments,
} = useGetAppointmentsQuery(
{doctor_id: userData.id, start_date: startDate, end_date: endDate},
{
pollingInterval: 60000,
skip: !userData.id,
}
);
const {
data: previousAppointments = [],
@ -43,6 +53,16 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
skip: !selectedPatient,
});
useEffect(() => {
if (isErrorAppointments) {
notification.error({
message: 'Ошибка',
description: 'Ошибка загрузки приемов.',
placement: 'topRight',
});
}
}, [isErrorAppointments]);
const blockStepStyle = {marginBottom: 16};
const searchInputStyle = {marginBottom: 16};
const chooseContainerStyle = {maxHeight: 400, overflowY: "auto"};
@ -180,6 +200,7 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
appointment_datetime: dayjs(scheduledData.appointment_datetime),
results: scheduledData.results || "",
});
setAppointmentDate(dayjs(scheduledData.appointment_datetime));
}
} else {
form.setFieldsValue({
@ -219,9 +240,14 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
setFormValues({});
setIsDrawerVisible(false);
setDraftFiles([]);
setAppointmentDate(dayjs(new Date())); // Сбрасываем дату
};
const handleSetAppointmentDate = (date) => {
setAppointmentDate(date);
form.setFieldsValue({appointment_datetime: date}); // Синхронизируем форму
};
const handleSetAppointmentDate = (date) => setAppointmentDate(date);
const modalWidth = useMemo(() => (screenXS ? 700 : "90%"), [screenXS]);
const showDrawer = () => {
@ -330,8 +356,8 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
} catch (error) {
console.error("Error creating appointment:", error);
notification.error({
message: "Ошибка загрузки файла",
description: `Не удалось загрузить файл ${JSON.stringify(error.data?.detail || error.data || error.message || "Неизвестная ошибка", null, 2)}`,
message: "Ошибка создания приема",
description: `Не удалось создать прием: ${JSON.stringify(error.data?.detail || error.data || error.message || "Неизвестная ошибка", null, 2)}`,
placement: "topRight",
});
}
@ -405,6 +431,8 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
handleAddFile,
handleRemoveFile,
isUploadingFile,
isLoadingAppointments,
isErrorAppointments,
};
};

View File

@ -1,4 +1,4 @@
import {useGetPatientsQuery} from "../../../Api/patientsApi.js";
import {useGetAllPatientsQuery} from "../../../Api/patientsApi.js";
import {useGetAppointmentTypesQuery} from "../../../Api/appointmentTypesApi.js";
import {useCreateScheduledAppointmentMutation} from "../../../Api/scheduledAppointmentsApi.js";
@ -8,7 +8,7 @@ const useScheduledAppointmentFormModal = () => {
data: patients = [],
isLoading: isLoadingPatients,
isError: isErrorPatients,
} = useGetPatientsQuery(undefined, {
} = useGetAllPatientsQuery(undefined, {
pollingInterval: 20000,
});

View File

@ -1,5 +1,5 @@
import {Badge, Button, Col, FloatButton, List, Result, Row, Space, Tag, Typography} from "antd";
import {Splitter} from "antd";
import { Badge, Button, FloatButton, List, Result, Row, Space, Tag, Typography } from "antd";
import { Splitter } from "antd";
import {
CalendarOutlined,
MenuFoldOutlined,
@ -8,38 +8,46 @@ import {
ClockCircleOutlined,
} from "@ant-design/icons";
import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/AppointmentsCalendarTab.jsx";
import useAppointmentsUI from "./useAppointmentsUI.js";
import useAppointments from "./useAppointments.js";
import dayjs from 'dayjs';
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import AppointmentFormModal from "../../Dummies/AppointmentFormModal/AppointmentFormModal.jsx";
import {useDispatch} from "react-redux";
import {
openModal,
setSelectedAppointment,
setSelectedScheduledAppointment
} from "../../../Redux/Slices/appointmentsSlice.js";
import AppointmentViewModal from "../../Dummies/AppointmentViewModal/AppointmentViewModal.jsx";
import ScheduledAppointmentFormModal
from "../../Dummies/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx";
import ScheduledAppointmentsViewModal
from "../../Widgets/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx";
import ScheduledAppointmentFormModal from "../../Dummies/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx";
import ScheduledAppointmentsViewModal from "../../Widgets/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx";
import AppointmentsListModal from "./Components/AppointmentsListModal/AppointmentsListModal.jsx";
const AppointmentsPage = () => {
const appointmentsData = useAppointments();
const appointmentsPageUI = useAppointmentsUI(appointmentsData.appointments, appointmentsData.scheduledAppointments);
const dispatch = useDispatch();
const {
patients, // Добавляем
appointments, // Добавляем
scheduledAppointments, // Добавляем
isLoading,
isError,
collapsed,
siderWidth,
showSplitterPanel,
siderButtonText,
splitterStyle,
splitterContentPanelStyle,
splitterSiderPanelStyle,
siderTitleStyle,
siderButtonContainerStyle,
siderButtonStyle,
badgeTextStyle,
upcomingEvents,
handleToggleSider,
handleHoverSider,
handleLeaveSider,
handleSetSiderWidth,
openCreateScheduledAppointmentModal,
currentMonth,
handleMonthChange,
handleEventClick,
openCreateAppointmentModal,
} = useAppointments();
const handleEventClick = (event) => {
if (event.appointment_datetime) {
dispatch(setSelectedAppointment(event));
} else {
dispatch(setSelectedScheduledAppointment(event));
}
};
if (appointmentsData.isError) return (
if (isError) return (
<Result
status="error"
title="Ошибка"
@ -49,66 +57,59 @@ const AppointmentsPage = () => {
return (
<>
<Typography.Title level={1}><CalendarOutlined/> Приемы</Typography.Title>
{appointmentsData.isLoading ? (
<LoadingIndicator/>
<Typography.Title level={1}><CalendarOutlined /> Приемы</Typography.Title>
{isLoading ? (
<LoadingIndicator />
) : (
<>
<Row justify="end" style={{marginBottom: 10, marginRight: "2.4rem"}}>
<Row justify="end" style={{ marginBottom: 10, marginRight: "2.4rem" }}>
<Space direction={"vertical"}>
<Tag color={"blue"} style={{width: "100%"}}>
<Tag color={"blue"} style={{ width: "100%" }}>
<Badge status={"processing"}
text={
<span style={appointmentsPageUI.badgeTextStyle}>
Запланированный прием
</span>
}
/>
text={<span style={badgeTextStyle}>Запланированный прием</span>} />
</Tag>
<Tag color={"green"} style={{width: "100%"}}>
<Tag color={"green"} style={{ width: "100%" }}>
<Badge status={"success"}
text={
<span style={appointmentsPageUI.badgeTextStyle}>
Прошедший прием
</span>
}
/>
text={<span style={badgeTextStyle}>Прошедший прием</span>} />
</Tag>
</Space>
</Row>
<Splitter
style={appointmentsPageUI.splitterStyle}
style={splitterStyle}
min={200}
max={400}
initial={appointmentsPageUI.siderWidth}
onChange={appointmentsPageUI.setSiderWidth}
initial={siderWidth}
onChange={handleSetSiderWidth}
>
<Splitter.Panel
style={appointmentsPageUI.splitterContentPanelStyle}
style={splitterContentPanelStyle}
defaultSize="80%"
min="25%"
max="90%"
>
<AppointmentsCalendarTab/>
<AppointmentsCalendarTab
currentMonth={currentMonth}
onMonthChange={handleMonthChange}
appointments={appointments} // Добавляем
scheduledAppointments={scheduledAppointments} // Добавляем
/>
</Splitter.Panel>
{appointmentsPageUI.showSplitterPanel && (
{showSplitterPanel && (
<Splitter.Panel
style={appointmentsPageUI.splitterSiderPanelStyle}
style={splitterSiderPanelStyle}
defaultSize="20%"
min="20%"
max="75%"
>
<Typography.Title level={3} style={appointmentsPageUI.siderTitleStyle}>
<Typography.Title level={3} style={siderTitleStyle}>
Предстоящие события
</Typography.Title>
{appointmentsPageUI.upcomingEvents.length ? (
{upcomingEvents.length ? (
<List
dataSource={appointmentsPageUI.upcomingEvents.sort((a, b) =>
dataSource={upcomingEvents.sort((a, b) =>
dayjs(a.appointment_datetime || a.scheduled_datetime).diff(
dayjs(b.appointment_datetime || b.scheduled_datetime)
)
)}
))}
renderItem={(item) => (
<List.Item
onClick={() => handleEventClick(item)}
@ -126,9 +127,9 @@ const AppointmentsPage = () => {
<Space direction="vertical" size={2}>
<Space>
{item.appointment_datetime ? (
<ClockCircleOutlined style={{color: "#52c41a"}}/>
<ClockCircleOutlined style={{ color: "#52c41a" }} />
) : (
<CalendarOutlined style={{color: "#1890ff"}}/>
<CalendarOutlined style={{ color: "#1890ff" }} />
)}
<Typography.Text strong>
{dayjs(item.appointment_datetime || item.scheduled_datetime).format('DD.MM.YYYY HH:mm')}
@ -155,47 +156,47 @@ const AppointmentsPage = () => {
)}
</Splitter>
<div
style={appointmentsPageUI.siderButtonContainerStyle}
onMouseEnter={appointmentsPageUI.handleHoverSider}
onMouseLeave={appointmentsPageUI.handleLeaveSider}
style={siderButtonContainerStyle}
onMouseEnter={handleHoverSider}
onMouseLeave={handleLeaveSider}
>
<Button
type="primary"
onClick={appointmentsPageUI.handleToggleSider}
icon={appointmentsPageUI.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
style={appointmentsPageUI.siderButtonStyle}
onClick={handleToggleSider}
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
style={siderButtonStyle}
>
{appointmentsPageUI.siderButtonText}
{siderButtonText}
</Button>
</div>
<FloatButton.Group
placement={"left"}
trigger="hover"
type="primary"
icon={<PlusOutlined/>}
icon={<PlusOutlined />}
tooltip="Создать"
>
<FloatButton
icon={<PlusOutlined/>}
onClick={() => dispatch(openModal())}
icon={<PlusOutlined />}
onClick={openCreateAppointmentModal}
tooltip="Прием"
/>
<FloatButton
icon={<CalendarOutlined/>}
onClick={appointmentsPageUI.openCreateScheduledAppointmentModal}
icon={<CalendarOutlined />}
onClick={openCreateScheduledAppointmentModal}
tooltip="Запланированный прием"
/>
</FloatButton.Group>
<AppointmentFormModal/>
<AppointmentViewModal/>
<ScheduledAppointmentFormModal/>
<ScheduledAppointmentsViewModal/>
<AppointmentsListModal/>
<AppointmentFormModal />
<AppointmentViewModal />
<ScheduledAppointmentFormModal />
<ScheduledAppointmentsViewModal />
<AppointmentsListModal />
</>
)}
</>
);
};
export default AppointmentsPage;
export default AppointmentsPage;

View File

@ -1,25 +1,21 @@
import { Calendar } from "antd";
import {Calendar} from "antd";
import "dayjs/locale/ru";
import CalendarCell from "../CalendarCell/CalendarCell.jsx";
import useAppointments from "../../useAppointments.js";
import useAppointmentCalendarUI from "./useAppointmentCalendarUI.js";
import AppointmentsListModal from "../AppointmentsListModal/AppointmentsListModal.jsx";
import dayjs from "dayjs";
import PropTypes from "prop-types";
const AppointmentsCalendarTab = () => {
const appointmentsData = useAppointments();
const appointmentsCalendarUI = useAppointmentCalendarUI(
appointmentsData.appointments,
appointmentsData.scheduledAppointments
);
const AppointmentsCalendarTab = ({currentMonth, onMonthChange, appointments, scheduledAppointments}) => {
const appointmentsCalendarUI = useAppointmentCalendarUI(appointments, scheduledAppointments);
const dateCellRender = (value) => {
const appointmentsForDate = appointmentsCalendarUI.getAppointmentsByListAndDate(
appointmentsData.appointments,
appointments,
value
);
const scheduledForDate = appointmentsCalendarUI.getAppointmentsByListAndDate(
appointmentsData.scheduledAppointments,
scheduledAppointments,
value,
true
);
@ -43,14 +39,26 @@ const AppointmentsCalendarTab = () => {
<div style={appointmentsCalendarUI.calendarContainerStyle}>
<Calendar
fullscreen={appointmentsCalendarUI.fullScreenCalendar}
value={appointmentsCalendarUI.selectedDate}
value={currentMonth} // Используем currentMonth вместо selectedDate
onSelect={appointmentsCalendarUI.onSelect}
onPanelChange={appointmentsCalendarUI.onPanelChange}
onPanelChange={(value, mode) => {
appointmentsCalendarUI.onPanelChange(value, mode);
if (mode === "month") {
onMonthChange(value); // Вызываем onMonthChange при смене месяца
}
}}
cellRender={dateCellRender}
/>
<AppointmentsListModal />
<AppointmentsListModal/>
</div>
);
};
AppointmentsCalendarTab.propTypes = {
currentMonth: PropTypes.object.isRequired,
onMonthChange: PropTypes.func.isRequired,
appointments: PropTypes.array.isRequired,
scheduledAppointments: PropTypes.array.isRequired,
};
export default AppointmentsCalendarTab;

View File

@ -1,13 +0,0 @@
import {useCreateAppointmentMutation, useUpdateAppointmentMutation} from "../../../../../Api/appointmentsApi.js";
import {
useCreateScheduledAppointmentMutation,
useUpdateScheduledAppointmentMutation
} from "../../../../../Api/scheduledAppointmentsApi.js";
const useAppointmentCalendar = () => {
const [createAppointment] = useCreateAppointmentMutation();
const [updateAppointment] = useUpdateAppointmentMutation();
const [createScheduledAppointment] = useCreateScheduledAppointmentMutation();
const [updateScheduledAppointment] = useUpdateScheduledAppointmentMutation();
};

View File

@ -1,39 +1,168 @@
import {useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js";
import {useGetScheduledAppointmentsQuery} from "../../../Api/scheduledAppointmentsApi.js";
import {useGetPatientsQuery} from "../../../Api/patientsApi.js";
import {notification} from "antd";
import {useEffect} from "react";
import {useSelector} from "react-redux";
import { useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { notification } from "antd";
import { Grid } from "antd";
import {
useGetAppointmentsQuery,
useGetUpcomingAppointmentsQuery,
} from "../../../Api/appointmentsApi.js";
import { useGetAllPatientsQuery } from "../../../Api/patientsApi.js";
import {
openModal,
openScheduledModal,
setHovered,
setSelectedAppointment,
setSelectedScheduledAppointment,
toggleSider
} from "../../../Redux/Slices/appointmentsSlice.js";
import {
useGetScheduledAppointmentsQuery,
useGetUpcomingScheduledAppointmentsQuery,
} from "../../../Api/scheduledAppointmentsApi.js";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
dayjs.extend(utc);
dayjs.extend(timezone);
const { useBreakpoint } = Grid;
const useAppointments = () => {
const {
userData
} = useSelector(state => state.auth);
const dispatch = useDispatch();
const { userData } = useSelector(state => state.auth);
const { collapsed, siderWidth, hovered, selectedAppointment } = useSelector(state => state.appointmentsUI);
const screens = useBreakpoint();
const [currentMonth, setCurrentMonth] = useState(dayjs().startOf('month'));
const startDate = currentMonth.startOf('month').format('YYYY-MM-DD');
const endDate = currentMonth.endOf('month').format('YYYY-MM-DD');
const handleMonthChange = (newMonth) => {
setCurrentMonth(dayjs(newMonth).startOf('month'));
};
const {
data: appointments = [],
isLoading: isLoadingAppointments,
isError: isErrorAppointments,
} = useGetAppointmentsQuery((userData.id), {
pollingInterval: 20000,
} = useGetAppointmentsQuery({ doctor_id: userData.id, start_date: startDate, end_date: endDate }, {
pollingInterval: 60000,
skip: !userData.id,
});
const {
data: scheduledAppointments = [],
isLoading: isLoadingScheduledAppointments,
isError: isErrorScheduledAppointments,
} = useGetScheduledAppointmentsQuery((userData.id), {
pollingInterval: 20000,
} = useGetScheduledAppointmentsQuery({ doctor_id: userData.id, start_date: startDate, end_date: endDate }, {
pollingInterval: 60000,
skip: !userData.id,
});
const {
data: patients = [],
isLoading: isLoadingPatients,
isError: isErrorPatients,
} = useGetPatientsQuery(undefined, {
pollingInterval: 20000,
} = useGetAllPatientsQuery(undefined, {
pollingInterval: 60000,
skip: !userData.id,
});
const {
data: upcomingAppointments = [],
isLoading: isLoadingUpcomingAppointments,
isError: isErrorUpcomingAppointments,
} = useGetUpcomingAppointmentsQuery(userData.id, {
pollingInterval: 60000,
skip: !userData.id,
});
const {
data: upcomingScheduledAppointments = [],
isLoading: isLoadingUpcomingScheduledAppointments,
isError: isErrorUpcomingScheduledAppointments,
} = useGetUpcomingScheduledAppointmentsQuery(userData.id, {
pollingInterval: 60000,
skip: !userData.id,
});
const [localSiderWidth, setLocalSiderWidth] = useState(siderWidth);
const splitterStyle = { flex: 1 };
const splitterContentPanelStyle = { padding: 16 };
const splitterSiderPanelStyle = { padding: "16px", borderLeft: "1px solid #ddd", overflowY: "auto" };
const siderTitleStyle = { marginBottom: 36 };
const siderButtonContainerStyle = {
position: "fixed",
right: 0,
top: "50%",
transform: "translateY(-50%)",
transition: "right 0.3s ease",
zIndex: 1000,
display: screens.xs ? "none" : "block",
};
const siderButtonStyle = {
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",
};
const badgeTextStyle = {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
display: "inline-block",
width: "100%",
};
const handleToggleSider = () => dispatch(toggleSider());
const handleHoverSider = () => dispatch(setHovered(true));
const handleLeaveSider = () => dispatch(setHovered(false));
const handleSetSiderWidth = (width) => setLocalSiderWidth(width);
const handleCancelViewModal = () => {
if (selectedAppointment) {
dispatch(setSelectedAppointment(null));
} else {
dispatch(setSelectedScheduledAppointment(null));
}
};
const handleEventClick = (event) => {
if (event.appointment_datetime) {
dispatch(setSelectedAppointment(event));
} else {
dispatch(setSelectedScheduledAppointment(event));
}
};
const openCreateScheduledAppointmentModal = () => {
dispatch(openScheduledModal());
};
const openCreateAppointmentModal = () => dispatch(openModal());
const siderButtonText = useMemo(() =>
hovered ? (collapsed ? "Показать предстоящие события" : "Скрыть предстоящие события") : "",
[collapsed, hovered]
);
const showSplitterPanel = useMemo(() => !collapsed && !screens.xs, [collapsed, screens]);
const upcomingEvents = useMemo(() =>
[...upcomingAppointments, ...upcomingScheduledAppointments]
.sort((a, b) => dayjs(a.appointment_datetime || a.scheduled_datetime) - dayjs(b.appointment_datetime || b.scheduled_datetime))
.slice(0, 5),
[upcomingAppointments, upcomingScheduledAppointments]
);
useEffect(() => {
document.title = "Приемы";
}, []);
useEffect(() => {
if (isErrorAppointments) {
notification.error({
@ -56,14 +185,60 @@ const useAppointments = () => {
placement: 'topRight',
});
}
}, [isErrorAppointments, isErrorScheduledAppointments, isErrorPatients]);
if (isErrorUpcomingAppointments) {
notification.error({
message: 'Ошибка',
description: 'Ошибка загрузки предстоящих приемов.',
placement: 'topRight',
});
}
if (isErrorUpcomingScheduledAppointments) {
notification.error({
message: 'Ошибка',
description: 'Ошибка загрузки предстоящих запланированных приемов.',
placement: 'topRight',
});
}
}, [
isErrorAppointments,
isErrorScheduledAppointments,
isErrorPatients,
isErrorUpcomingAppointments,
isErrorUpcomingScheduledAppointments
]);
return {
patients,
appointments,
scheduledAppointments,
isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients,
isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients,
isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients ||
isLoadingUpcomingAppointments || isLoadingUpcomingScheduledAppointments,
isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients ||
isErrorUpcomingAppointments || isErrorUpcomingScheduledAppointments,
collapsed,
siderWidth: localSiderWidth,
hovered,
showSplitterPanel,
siderButtonText,
splitterStyle,
splitterContentPanelStyle,
splitterSiderPanelStyle,
siderTitleStyle,
siderButtonContainerStyle,
siderButtonStyle,
badgeTextStyle,
upcomingEvents,
selectedAppointment,
handleCancelViewModal,
handleToggleSider,
handleHoverSider,
handleLeaveSider,
handleSetSiderWidth,
openCreateScheduledAppointmentModal,
currentMonth,
handleMonthChange,
handleEventClick,
openCreateAppointmentModal,
};
};

View File

@ -1,97 +0,0 @@
import { useDispatch, useSelector } from "react-redux";
import { Grid } from "antd";
import {
setHovered,
setSelectedAppointment,
setSelectedScheduledAppointment,
toggleSider,
openScheduledModal,
} from "../../../Redux/Slices/appointmentsSlice.js";
import { useEffect, useMemo } from "react";
import dayjs from "dayjs";
const { useBreakpoint } = Grid;
const useAppointmentsUI = (appointments, scheduledAppointments) => {
const dispatch = useDispatch();
const {
collapsed,
siderWidth,
hovered,
selectedAppointment,
} = useSelector(state => state.appointmentsUI);
const screens = useBreakpoint();
useEffect(() => {
document.title = "Приемы";
}, []);
const splitterStyle = { flex: 1 };
const splitterContentPanelStyle = { padding: 16 };
const splitterSiderPanelStyle = { padding: "16px", borderLeft: "1px solid #ddd", overflowY: "auto" };
const siderTitleStyle = { marginBottom: 36 };
const siderButtonContainerStyle = {
position: "fixed",
right: 0,
top: "50%",
transform: "translateY(-50%)",
transition: "right 0.3s ease",
zIndex: 1000,
display: screens.xs ? "none" : "block",
};
const siderButtonStyle = {
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",
};
const handleToggleSider = () => dispatch(toggleSider());
const handleHoverSider = () => dispatch(setHovered(true));
const handleLeaveSider = () => dispatch(setHovered(false));
const handleCancelViewModal = () => {
if (selectedAppointment) {
dispatch(setSelectedAppointment(null));
} else {
dispatch(setSelectedScheduledAppointment(null));
}
};
const openCreateScheduledAppointmentModal = () => {
dispatch(openScheduledModal());
};
const siderButtonText = useMemo(() => hovered ? (collapsed ? "Показать предстоящие события" : "Скрыть предстоящие события") : "", [collapsed, hovered]);
const showSplitterPanel = useMemo(() => !collapsed && !screens.xs, [collapsed, screens]);
const upcomingEvents = [...appointments, ...scheduledAppointments]
.filter(app => dayjs(app.appointment_datetime || app.scheduled_datetime).isAfter(dayjs()))
.sort((a, b) => dayjs(a.appointment_datetime || a.scheduled_datetime) - dayjs(b.appointment_datetime || b.scheduled_datetime))
.slice(0, 5);
return {
collapsed,
siderWidth,
hovered,
showSplitterPanel,
siderButtonText,
splitterStyle,
splitterContentPanelStyle,
splitterSiderPanelStyle,
siderTitleStyle,
siderButtonContainerStyle,
siderButtonStyle,
upcomingEvents,
selectedAppointment,
handleCancelViewModal,
handleToggleSider,
handleHoverSider,
handleLeaveSider,
openCreateScheduledAppointmentModal,
};
};
export default useAppointmentsUI;

View File

@ -31,7 +31,13 @@ ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
const HomePage = () => {
const homePageData = useHomePage();
const homePageUI = useHomePageUI(homePageData.appointments, homePageData.scheduledAppointments, homePageData.patients);
const homePageUI = useHomePageUI(
homePageData.appointments,
homePageData.scheduledAppointments,
homePageData.patients,
homePageData.upcomingAppointments,
homePageData.upcomingScheduledAppointments
);
if (homePageData.isError) {
return (
@ -94,7 +100,7 @@ const HomePage = () => {
<Statistic
title="Приемы за месяц"
value={
homePageData.appointments.filter((a) => dayjs(a.appointment_datetime).isSame(dayjs(), "month")).length
homePageData.appointments.length
}
/>
</Card>
@ -107,7 +113,7 @@ const HomePage = () => {
</Row>
<Card
title={`События на сегодня (${dayjs().format("DD.MM.YYYY")})`}
title={`События на сегодня (${dayjs().tz("Europe/Moscow").format("DD.MM.YYYY")})`}
style={homePageUI.sectionStyle}
>
<List
@ -119,7 +125,7 @@ const HomePage = () => {
>
<Space>
<Typography.Text strong>
{dayjs(item.appointment_datetime || item.scheduled_datetime).format("HH:mm")}
{dayjs(item.appointment_datetime || item.scheduled_datetime).tz("Europe/Moscow").format("HH:mm")}
</Typography.Text>
<Typography.Text>
{item.patient ? `${item.patient.last_name} ${item.patient.first_name}` : "Без пациента"} -{" "}

View File

@ -1,6 +1,9 @@
import {useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js";
import {useGetScheduledAppointmentsQuery} from "../../../Api/scheduledAppointmentsApi.js";
import {useGetPatientsQuery} from "../../../Api/patientsApi.js";
import {useGetAppointmentsQuery, useGetUpcomingAppointmentsQuery} from "../../../Api/appointmentsApi.js";
import {
useGetScheduledAppointmentsQuery,
useGetUpcomingScheduledAppointmentsQuery
} from "../../../Api/scheduledAppointmentsApi.js";
import {useGetAllPatientsQuery} from "../../../Api/patientsApi.js";
import {notification} from "antd";
import {useEffect} from "react";
import {useDispatch, useSelector} from "react-redux";
@ -14,40 +17,65 @@ import {
openModal as openPatientModal,
} from "../../../Redux/Slices/patientsSlice.js";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import isBetween from "dayjs/plugin/isBetween";
import {useGetAppointmentTypesQuery} from "../../../Api/appointmentTypesApi.js"; // Import isBetween plugin
import {useGetAppointmentTypesQuery} from "../../../Api/appointmentTypesApi.js";
dayjs.extend(isBetween); // Extend dayjs with isBetween
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(isBetween);
const useHomePage = () => {
const dispatch = useDispatch();
const {userData} = useSelector((state) => state.auth);
const {
userData
} = useSelector((state) => state.auth);
const startDate = dayjs().startOf('month').format('YYYY-MM-DD');
const endDate = dayjs().endOf('month').format('YYYY-MM-DD');
const {
data: appointments = [],
isLoading: isLoadingAppointments,
isError: isErrorAppointments,
} = useGetAppointmentsQuery((userData.id), {
pollingInterval: 20000,
} = useGetAppointmentsQuery({doctor_id: userData.id, start_date: startDate, end_date: endDate}, {
pollingInterval: 60000,
skip: !userData.id,
});
const {
data: scheduledAppointments = [],
isLoading: isLoadingScheduledAppointments,
isError: isErrorScheduledAppointments,
} = useGetScheduledAppointmentsQuery((userData.id), {
pollingInterval: 20000,
} = useGetScheduledAppointmentsQuery({doctor_id: userData.id, start_date: startDate, end_date: endDate}, {
pollingInterval: 60000,
skip: !userData.id,
});
const {
data: upcomingAppointments = [],
isLoading: isLoadingUpcomingAppointments,
isError: isErrorUpcomingAppointments,
} = useGetUpcomingAppointmentsQuery(userData.id, {
pollingInterval: 60000,
skip: !userData.id,
});
const {
data: upcomingScheduledAppointments = [],
isLoading: isLoadingUpcomingScheduledAppointments,
isError: isErrorUpcomingScheduledAppointments,
} = useGetUpcomingScheduledAppointmentsQuery(userData.id, {
pollingInterval: 60000,
skip: !userData.id,
});
const {
data: patients = [],
isLoading: isLoadingPatients,
isError: isErrorPatients,
} = useGetPatientsQuery(undefined, {
pollingInterval: 20000,
} = useGetAllPatientsQuery(undefined, {
pollingInterval: 60000,
skip: !userData.id,
});
const {
@ -55,7 +83,8 @@ const useHomePage = () => {
isLoading: isLoadingAppointmentTypes,
isError: isErrorAppointmentTypes,
} = useGetAppointmentTypesQuery(undefined, {
pollingInterval: 20000,
pollingInterval: 60000,
skip: !userData.id,
});
useEffect(() => {
@ -73,6 +102,20 @@ const useHomePage = () => {
placement: "topRight",
});
}
if (isErrorUpcomingAppointments) {
notification.error({
message: "Ошибка",
description: "Ошибка загрузки предстоящих приемов.",
placement: "topRight",
});
}
if (isErrorUpcomingScheduledAppointments) {
notification.error({
message: "Ошибка",
description: "Ошибка загрузки предстоящих запланированных приемов.",
placement: "topRight",
});
}
if (isErrorPatients) {
notification.error({
message: "Ошибка",
@ -87,7 +130,14 @@ const useHomePage = () => {
placement: "topRight",
});
}
}, [isErrorAppointments, isErrorScheduledAppointments, isErrorPatients, isErrorAppointmentTypes]);
}, [
isErrorAppointments,
isErrorScheduledAppointments,
isErrorUpcomingAppointments,
isErrorUpcomingScheduledAppointments,
isErrorPatients,
isErrorAppointmentTypes
]);
const handleEventClick = (event) => {
if (event.appointment_datetime) {
@ -113,15 +163,21 @@ const useHomePage = () => {
patients,
appointments,
scheduledAppointments,
upcomingAppointments,
upcomingScheduledAppointments,
appointmentTypes,
isLoading:
isLoadingAppointments ||
isLoadingScheduledAppointments ||
isLoadingUpcomingAppointments ||
isLoadingUpcomingScheduledAppointments ||
isLoadingPatients ||
isLoadingAppointmentTypes,
isError:
isErrorAppointments ||
isErrorScheduledAppointments ||
isErrorUpcomingAppointments ||
isErrorUpcomingScheduledAppointments ||
isErrorPatients ||
isErrorAppointmentTypes,
handleEventClick,

View File

@ -1,32 +1,35 @@
import { Grid } from "antd";
import {Grid} from "antd";
import {useEffect, useMemo} from "react";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import isBetween from "dayjs/plugin/isBetween";
import {useSelector} from "react-redux"; // Import isBetween plugin
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(isBetween);
const { useBreakpoint } = Grid;
const {useBreakpoint} = Grid;
const useHomePageUI = (appointments, scheduledAppointments, patients) => {
const useHomePageUI = (appointments, scheduledAppointments, patients, upcomingAppointments, upcomingScheduledAppointments) => {
const screens = useBreakpoint();
const containerStyle = { padding: screens.xs ? 16 : 24 };
const sectionStyle = { marginBottom: 24 };
const cardStyle = { height: "100%" };
const listItemStyle = { cursor: "pointer", padding: "12px", borderRadius: 4 };
const buttonStyle = { width: screens.xs ? "100%" : "auto" };
const chartContainerStyle = { padding: 16, background: "#fff", borderRadius: 4 };
const containerStyle = {padding: screens.xs ? 16 : 24};
const sectionStyle = {marginBottom: 24};
const cardStyle = {height: "100%"};
const listItemStyle = {cursor: "pointer", padding: "12px", borderRadius: 4};
const buttonStyle = {width: screens.xs ? "100%" : "auto"};
const chartContainerStyle = {padding: 16, background: "#fff", borderRadius: 4};
useEffect(() => {
document.title = "Главная страница";
}, []);
const todayEvents = useMemo(() => {
return [...appointments, ...scheduledAppointments].filter((event) =>
return [...upcomingAppointments, ...upcomingScheduledAppointments].filter((event) =>
dayjs(event.appointment_datetime || event.scheduled_datetime).isSame(dayjs(), "day")
);
}, [appointments, scheduledAppointments]);
}, [upcomingAppointments, upcomingScheduledAppointments]);
const upcomingBirthdays = useMemo(() => {
return patients.filter((p) =>
@ -38,7 +41,12 @@ const useHomePageUI = (appointments, scheduledAppointments, patients) => {
const data = Array(7).fill(0);
appointments
.filter((app) =>
dayjs(app.appointment_datetime).isBetween(dayjs().startOf("week"), dayjs().endOf("week"), "day", "[]")
dayjs(app.appointment_datetime).isBetween(
dayjs().startOf("week"),
dayjs().endOf("week"),
"day",
"[]"
)
)
.forEach((app) => {
const dayIndex = dayjs(app.appointment_datetime).day();
@ -51,7 +59,12 @@ const useHomePageUI = (appointments, scheduledAppointments, patients) => {
const data = Array(7).fill(0);
scheduledAppointments
.filter((app) =>
dayjs(app.scheduled_datetime).isBetween(dayjs().startOf("week"), dayjs().endOf("week"), "day", "[]")
dayjs(app.scheduled_datetime).isBetween(
dayjs().startOf("week"),
dayjs().endOf("week"),
"day",
"[]"
)
)
.forEach((app) => {
const dayIndex = dayjs(app.scheduled_datetime).day();
@ -64,12 +77,12 @@ const useHomePageUI = (appointments, scheduledAppointments, patients) => {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { beginAtZero: true, title: { display: true, text: "Количество приемов" } },
x: { title: { display: true, text: "День недели" } },
y: {beginAtZero: true, title: {display: true, text: "Количество приемов"}},
x: {title: {display: true, text: "День недели"}},
},
plugins: {
legend: { display: false },
title: { display: true, text: "Приемы за неделю" },
legend: {display: true, position: "top"},
title: {display: true, text: "Приемы за неделю"},
},
};

View File

@ -1,9 +1,9 @@
import {useGetPatientsQuery} from "../../../../../Api/patientsApi.js";
import {useGetAllPatientsQuery, useGetPatientsQuery} from "../../../../../Api/patientsApi.js";
import {useGetNotIssuedLensesQuery} from "../../../../../Api/lensesApi.js";
const useLensIssueForm = () => {
const {data: patients = [], isLoading: isLoadingPatients, isError: isErrorPatients} = useGetPatientsQuery(undefined, {
const {data: patients = [], isLoading: isLoadingPatients, isError: isErrorPatients} = useGetAllPatientsQuery(undefined, {
pollingInterval: 10000,
});
const {data: lenses = [], isLoading: isLoadingLenses, isError: isErrorLenses} = useGetNotIssuedLensesQuery(undefined, {

View File

@ -10,7 +10,8 @@ import {
Typography,
Timeline,
Grid,
Pagination, Result
Pagination,
Result
} from "antd";
import {DatabaseOutlined, PlusOutlined, UnorderedListOutlined} from "@ant-design/icons";
import LensIssueViewModal from "./Components/LensIssueViewModal/LensIssueViewModal.jsx";
@ -19,27 +20,25 @@ import LensIssueFormModal from "./Components/LensIssueFormModal/LensIssueFormMod
import SelectViewMode from "../../Widgets/SelectViewMode/SelectViewMode.jsx";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import useIssues from "./useIssues.js";
import useIssuesUI from "./useIssuesUI.js";
import {useMemo} from "react";
const {Title} = Typography;
const {useBreakpoint} = Grid;
const IssuesPage = () => {
const issuesData = useIssues();
const issuesUI = useIssuesUI(issuesData.issues);
const screens = useBreakpoint();
const viewModes = [
{
value: "table",
label: "Таблица",
icon: <DatabaseOutlined style={issuesUI.viewModIconStyle}/>,
icon: <DatabaseOutlined style={issuesData.viewModIconStyle}/>,
},
{
value: "timeline",
label: "Лента",
icon: <UnorderedListOutlined style={issuesUI.viewModIconStyle}/>,
icon: <UnorderedListOutlined style={issuesData.viewModIconStyle}/>,
},
];
@ -73,7 +72,7 @@ const IssuesPage = () => {
title: "Действия",
key: "actions",
render: (_, issue) => (
<Button type={"link"} onClick={() => issuesUI.handleSelectIssue(issue)}>Подробнее</Button>
<Button type="link" onClick={() => issuesData.handleSelectIssue(issue)}>Подробнее</Button>
),
},
];
@ -81,69 +80,56 @@ const IssuesPage = () => {
const TableView = () => (
<Table
columns={columns}
dataSource={issuesUI.filteredIssues}
dataSource={issuesData.issues}
rowKey="id"
pagination={issuesUI.pagination}
pagination={issuesData.pagination}
showSorterTooltip={false}
/>
);
const timeLineItems = issuesUI.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={() => issuesUI.handleSelectIssue(issue)}
style={{marginRight: 40}}
>
Подробнее
</Button>
</Col>
</Row>
),
}));
const TimeLineView = () => {
const paginatedItems = timeLineItems.slice(
(issuesUI.currentPage - 1) * issuesUI.pageSize,
issuesUI.currentPage * issuesUI.pageSize
);
const timeLineItems = useMemo(() => issuesData.issues.map(issue => ({
label: dayjs(issue.issue_date).format("DD.MM.YYYY"),
children: (
<Row gutter={[16, 16]} align="middle">
<Col xs={24} sm={24} md={13}>
<p style={{textAlign: "right"}}>Пациент: {issue.patient.last_name} {issue.patient.first_name}</p>
</Col>
<Col xs={24} sm={24} md={5}>
<p style={{textAlign: "right"}}>Линза: {issue.lens.side} {issue.lens.diameter}</p>
</Col>
<Col xs={24} sm={24} md={6}>
<Button
type="dashed"
onClick={() => issuesData.handleSelectIssue(issue)}
style={{marginRight: 40}}
>
Подробнее
</Button>
</Col>
</Row>
),
})), []);
return (
<>
<Timeline
items={paginatedItems}
items={timeLineItems}
mode={screens.xs ? "left" : "right"}
/>
<Row
style={{textAlign: "center", marginTop: 20}}
align={"middle"}
justify={"end"}
align="middle"
justify="end"
>
<Pagination
current={issuesUI.currentPage}
pageSize={issuesUI.pageSize}
total={timeLineItems.length}
onChange={issuesUI.handlePaginationChange}
showSizeChanger={true}
pageSizeOptions={["5", "10", "20", "50"]}
{...issuesData.pagination}
/>
</Row>
</>
);
};
if (issuesData.isError) return (
<Result
status="error"
@ -153,46 +139,37 @@ const IssuesPage = () => {
);
return (
<div style={issuesUI.containerStyle}>
<div style={issuesData.containerStyle}>
<Title level={1}><DatabaseOutlined/> Выдача линз</Title>
<Row gutter={[16, 16]} style={issuesUI.filterBarStyle}>
<Col xs={24} md={24} sm={24} xl={12}>
<Row gutter={[16, 16]} style={issuesData.filterBarStyle}>
<Col xs={24} sm={24} md={12}>
<Input
placeholder="Поиск по пациенту или врачу"
onChange={(e) => issuesUI.handleSetSearchText(e.target.value)}
style={issuesUI.formItemStyle}
value={issuesData.tempSearchText}
onChange={(e) => issuesData.handleSetTempSearchText(e.target.value)}
style={issuesData.formItemStyle}
allowClear
onClear={issuesData.handleClearSearch}
/>
</Col>
<Col xs={24} md={
issuesUI.isFilterDates ? 12 : 16
} sm={
16
} xl={
issuesUI.isFilterDates ? 6 : 8
}>
<Tooltip
title="Фильтр по дате выдачи линзы"
>
<Col xs={24} sm={24} md={issuesData.isFilterDates ? 6 : 8}>
<Tooltip title="Фильтр по дате выдачи линзы">
<DatePicker.RangePicker
allowClear={false}
style={issuesUI.formItemStyle}
style={issuesData.formItemStyle}
placeholder={["Дата начала", "Дата окончания"]}
format="DD.MM.YYYY"
value={issuesUI.isFilterDates && issuesUI.filterDates}
onChange={issuesUI.handleFilterDateChange}
value={issuesData.isFilterDates && issuesData.filterDates}
onChange={issuesData.handleFilterDateChange}
/>
</Tooltip>
</Col>
{issuesUI.isFilterDates && (
<Col xs={24} md={4} sm={8} xl={2}>
<Tooltip
title="Cбросить фильтр"
>
{issuesData.isFilterDates && (
<Col xs={24} sm={24} md={2}>
<Tooltip title="Cбросить фильтр">
<Button
onClick={issuesUI.handleResetFilterDate}
type={"primary"}
onClick={issuesData.handleResetFilterDate}
type="primary"
block
>
Сбросить
@ -200,22 +177,19 @@ const IssuesPage = () => {
</Tooltip>
</Col>
)}
<Col xs={24}
md={issuesUI.isFilterDates ? 8 : 8}
sm={issuesUI.isFilterDates ? 24 : 8}
xl={4}>
<Col xs={24} sm={24} md={issuesData.isFilterDates ? 4 : 4}>
<SelectViewMode
viewMode={issuesUI.viewMode}
setViewMode={issuesUI.handleSetViewMode}
localStorageKey={"viewModeIssues"}
toolTipText={"Формат отображения выдач линз"}
viewMode={issuesData.viewMode}
setViewMode={issuesData.handleSetViewMode}
localStorageKey="viewModeIssues"
toolTipText="Формат отображения выдач линз"
viewModes={viewModes}
/>
</Col>
</Row>
{issuesData.isLoading ? (
<LoadingIndicator/>
) : issuesUI.viewMode === "table" ? (
) : issuesData.viewMode === "table" ? (
<TableView/>
) : (
<TimeLineView/>
@ -223,24 +197,27 @@ const IssuesPage = () => {
<FloatButton
icon={<PlusOutlined/>}
type={"primary"}
onClick={issuesUI.handleAddIssue}
tooltip={"Добавить выдачу линзы"}
type="primary"
onClick={issuesData.handleAddIssue}
tooltip="Добавить выдачу линзы"
/>
<LensIssueFormModal
visible={issuesUI.isModalVisible}
onCancel={issuesUI.handleCloseModal}
visible={issuesData.isModalVisible}
onCancel={issuesData.handleCloseModal}
onSubmit={issuesData.handleSubmitFormModal}
isProcessing={issuesData.isProcessing}
patients={issuesData.patients}
lenses={issuesData.lenses}
/>
<LensIssueViewModal
visible={issuesUI.selectedIssue !== null}
onCancel={issuesUI.resetSelectedIssue}
lensIssue={issuesUI.selectedIssue}
visible={issuesData.selectedIssue !== null}
onCancel={issuesData.resetSelectedIssue}
lensIssue={issuesData.selectedIssue}
/>
</div>
);
};
export default IssuesPage;
export default IssuesPage;

View File

@ -1,28 +1,183 @@
import {useEffect, useState} from "react";
import {useDispatch, useSelector} from "react-redux";
import {useAddLensIssuesMutation, useGetLensIssuesQuery} from "../../../Api/lensIssuesApi.js";
import {notification} from "antd";
import {closeModal} from "../../../Redux/Slices/lensIssuesSlice.js";
import {
useAddLensIssuesMutation,
useGetLensIssuesQuery,
} from "../../../Api/lensIssuesApi.js";
import {useGetAllPatientsQuery} from "../../../Api/patientsApi.js";
import {
closeModal,
openModal,
selectIssue,
setCurrentPage,
setEndFilterDate,
setPageSize,
setViewMode,
setStartFilterDate,
} from "../../../Redux/Slices/lensIssuesSlice.js";
import {getCachedInfo} from "../../../Utils/cachedInfoUtils.js";
import dayjs from "dayjs";
import {useGetNotIssuedLensesQuery} from "../../../Api/lensesApi.js";
const useIssues = () => {
const dispatch = useDispatch();
const {
currentPage,
pageSize,
selectedIssue,
isModalVisible,
viewMode,
startFilterDate,
endFilterDate,
} = useSelector(state => state.lensIssuesUI);
const {data: issues = [], isLoading, isError, error} = useGetLensIssuesQuery(undefined, {
const [tempSearchText, setTempSearchText] = useState("");
const {
data: issuesData = {issues: [], total_count: 0},
isLoading: isIssuesLoading,
isError: isIssuesError,
error: issuesError,
refetch
} = useGetLensIssuesQuery({
page: currentPage,
pageSize,
search: tempSearchText || undefined, // Используем tempSearchText напрямую
sortOrder: 'desc',
startDate: startFilterDate ? dayjs(startFilterDate).format('YYYY-MM-DD') : undefined,
endDate: endFilterDate ? dayjs(endFilterDate).format('YYYY-MM-DD') : undefined,
}, {
pollingInterval: 20000,
});
const [addIssue] = useAddLensIssuesMutation();
const {data: patients = [], isLoading: isPatientsLoading, isError: isPatientsError} = useGetAllPatientsQuery();
const {data: lenses = [], isLoading: isLensesLoading, isError: isLensesError} = useGetNotIssuedLensesQuery();
const [addIssue, {isLoading: isAdding}] = useAddLensIssuesMutation();
const isLoading = isIssuesLoading || isPatientsLoading || isLensesLoading;
const isError = isIssuesError || isPatientsError || isLensesError;
useEffect(() => {
document.title = "Выдача линз";
const cachedViewMode = getCachedInfo("viewModeIssues");
if (cachedViewMode) dispatch(setViewMode(cachedViewMode));
}, [dispatch]);
useEffect(() => {
if (isIssuesError) {
notification.error({
message: "Ошибка загрузки выдач",
description: issuesError?.data?.detail || "Не удалось загрузить выдачи линз",
placement: "topRight",
});
}
if (isPatientsError) {
notification.error({
message: "Ошибка загрузки пациентов",
description: "Не удалось загрузить список пациентов",
placement: "topRight",
});
}
if (isLensesError) {
notification.error({
message: "Ошибка загрузки линз",
description: "Не удалось загрузить список линз",
placement: "topRight",
});
}
}, [isIssuesError, isPatientsError, isLensesError, issuesError]);
const startFilterDateConverted = startFilterDate ? dayjs(startFilterDate) : null;
const endFilterDateConverted = endFilterDate ? dayjs(endFilterDate) : null;
const filterDates = [startFilterDateConverted, endFilterDateConverted];
const isFilterDates = startFilterDate && endFilterDate;
const containerStyle = {padding: 20};
const filterBarStyle = {marginBottom: 20};
const formItemStyle = {width: "100%"};
const viewModIconStyle = {marginRight: 8};
const advancedSearchCardStyle = {
marginBottom: 20,
boxShadow: "0 1px 6px rgba(0, 0, 0, 0.15)",
borderRadius: 8
};
const handleSetTempSearchText = (value) => {
setTempSearchText(value);
dispatch(setCurrentPage(1)); // Сбрасываем на первую страницу
refetch(); // Обновляем данные при изменении поиска
};
const handleClearSearch = () => {
setTempSearchText('');
dispatch(setCurrentPage(1));
refetch();
};
const handleSetViewMode = (mode) => dispatch(setViewMode(mode));
const handleSetCurrentPage = (page) => {
dispatch(setCurrentPage(page));
};
const handleSetPageSize = (size) => {
dispatch(setPageSize(size));
};
const handleCloseModal = () => dispatch(closeModal());
const handleSelectIssue = (issue) => dispatch(selectIssue(issue));
const resetSelectedIssue = () => dispatch(selectIssue(null));
const handleAddIssue = () => {
dispatch(selectIssue(null));
dispatch(openModal());
};
const handlePaginationChange = (page, pageSize) => {
handleSetCurrentPage(page);
handleSetPageSize(pageSize);
refetch();
};
const handleFilterDateChange = (dates) => {
if (dates) {
const [start, end] = dates;
dispatch(setStartFilterDate(start.toISOString()));
dispatch(setEndFilterDate(end.toISOString()));
dispatch(setCurrentPage(1));
refetch();
}
};
const handleResetFilterDate = () => {
dispatch(setStartFilterDate(null));
dispatch(setEndFilterDate(null));
dispatch(setCurrentPage(1));
refetch();
};
const pagination = {
current: currentPage,
pageSize,
total: issuesData.total_count,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: handlePaginationChange,
onShowSizeChange: handlePaginationChange,
};
const handleSubmitFormModal = async (issueDate, patientId, lensId) => {
dispatch(closeModal());
try {
await addIssue({issue_date: issueDate, patient_id: patientId, lens_id: lensId});
const formattedIssueDate = dayjs(issueDate).format('YYYY-MM-DD');
await addIssue({issue_date: formattedIssueDate, patient_id: patientId, lens_id: lensId}).unwrap();
notification.success({
message: "Линза выдана",
description: "Линза успешно выдана пациенту.",
placement: "topRight",
});
refetch();
} catch (error) {
console.error('Add lens issue error:', error);
notification.error({
message: "Ошибка выдачи линзы",
description: error?.data?.detail || "Не удалось выдать линзу пациенту.",
@ -32,10 +187,37 @@ const useIssues = () => {
};
return {
issues,
issues: issuesData.issues,
total_count: issuesData.total_count,
patients,
lenses,
isLoading,
isError,
error,
isProcessing: isAdding,
tempSearchText, // Возвращаем только tempSearchText
selectedIssue,
isModalVisible,
viewMode,
currentPage,
pageSize,
filterDates,
isFilterDates,
containerStyle,
filterBarStyle,
formItemStyle,
viewModIconStyle,
advancedSearchCardStyle,
pagination,
handleAddIssue,
handlePaginationChange,
handleSetTempSearchText,
handleClearSearch,
handleSetViewMode,
handleCloseModal,
handleFilterDateChange,
resetSelectedIssue,
handleSelectIssue,
handleResetFilterDate,
handleSubmitFormModal,
};
};

View File

@ -1,147 +0,0 @@
import {useDispatch, useSelector} from "react-redux";
import {getCachedInfo} from "../../../Utils/cachedInfoUtils.js";
import {
closeModal,
openModal,
selectIssue,
setCurrentPage,
setEndFilterDate,
setPageSize,
setSearchText,
setStartFilterDate,
setViewMode
} from "../../../Redux/Slices/lensIssuesSlice.js";
import {useEffect, useMemo} from "react";
import dayjs from "dayjs";
const useIssuesUI = (issues) => {
const dispatch = useDispatch();
const {
searchText,
currentPage,
pageSize,
selectedIssue,
isModalVisible,
viewMode,
startFilterDate,
endFilterDate,
} = useSelector(state => state.lensIssuesUI);
useEffect(() => {
document.title = "Выдача линз";
const cachedViewMode = getCachedInfo("viewModeIssues");
if (cachedViewMode) dispatch(setViewMode(cachedViewMode));
}, [dispatch])
const startFilterDateConverted = startFilterDate ? dayjs(startFilterDate) : null;
const endFilterDateConverted = endFilterDate ? dayjs(endFilterDate) : null;
const filterDates = [startFilterDateConverted, endFilterDateConverted];
const isFilterDates = startFilterDate && endFilterDate;
const containerStyle = { padding: 20 };
const filterBarStyle = { marginBottom: 20 };
const formItemStyle = { width: "100%" };
const viewModIconStyle = { marginRight: 8 };
const advancedSearchCardStyle = {
marginBottom: 20,
boxShadow: "0 1px 6px rgba(0, 0, 0, 0.15)",
borderRadius: 8
};
const handleSetSearchText = (value) => dispatch(setSearchText(value));
const handleSetViewMode = (mode) => dispatch(setViewMode(mode));
const handleSetCurrentPage = (page) => dispatch(setCurrentPage(page));
const handleSetPageSize = (size) => dispatch(setPageSize(size));
const handleCloseModal = () => dispatch(closeModal());
const handleSelectIssue = (issue) => dispatch(selectIssue(issue));
const resetSelectedIssue = () => dispatch(selectIssue(null));
const handleAddIssue = () => {
dispatch(selectIssue(null));
dispatch(openModal());
};
const handlePaginationChange = (page, pageSize) => {
handleSetCurrentPage(page);
handleSetPageSize(pageSize);
};
const handleFilterDateChange = (dates) => {
if (dates) {
const [start, end] = dates;
dispatch(setStartFilterDate(start.toISOString()));
dispatch(setEndFilterDate(end.toISOString()));
}
};
const handleResetFilterDate = () => {
dispatch(setStartFilterDate(null));
dispatch(setEndFilterDate(null));
};
const filteredIssues = useMemo(() => {
return issues.filter(issue => {
let dateFilter = true;
if (startFilterDateConverted && endFilterDateConverted) {
const issueDate = dayjs(issue.issue_date);
dateFilter = issueDate.isAfter(startFilterDateConverted) && issueDate.isBefore(endFilterDateConverted);
}
return (
(
issue.patient.last_name.toLowerCase().includes(searchText) ||
issue.patient.first_name.toLowerCase().includes(searchText) ||
issue.doctor.last_name.toLowerCase().includes(searchText) ||
issue.doctor.first_name.toLowerCase().includes(searchText)
) &&
dateFilter
)
});
}, [issues, searchText, startFilterDateConverted, endFilterDateConverted]);
const pagination = {
current: currentPage,
pageSize: pageSize,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => {
setCurrentPage(page);
setPageSize(newPageSize);
},
};
return {
filteredIssues,
pagination,
searchText,
selectedIssue,
isModalVisible,
viewMode,
currentPage,
pageSize,
filterDates,
isFilterDates,
containerStyle,
filterBarStyle,
formItemStyle,
viewModIconStyle,
advancedSearchCardStyle,
handleAddIssue,
handlePaginationChange,
handleSetSearchText,
handleSetViewMode,
handleSetCurrentPage,
handleSetPageSize,
handleCloseModal,
handleFilterDateChange,
resetSelectedIssue,
handleSelectIssue,
handleResetFilterDate,
};
};
export default useIssuesUI;

View File

@ -37,11 +37,13 @@ const PatientsPage = () => {
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: "Отчество",
@ -52,6 +54,7 @@ const PatientsPage = () => {
title: "Дата рождения",
dataIndex: "birthday",
render: patientsData.formatDate,
sorter: (a, b) => new Date(b.birthday) - new Date(a.birthday),
},
{
title: "Телефон",