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 typing import Sequence, Optional
from sqlalchemy import select, desc, func
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from datetime import date
from app.domain.models import Appointment from app.domain.models import Appointment
@ -11,7 +11,7 @@ class AppointmentsRepository:
def __init__(self, db: AsyncSession): def __init__(self, db: AsyncSession):
self.db = db 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 = ( stmt = (
select(Appointment) select(Appointment)
.options(joinedload(Appointment.type)) .options(joinedload(Appointment.type))
@ -19,10 +19,15 @@ class AppointmentsRepository:
.options(joinedload(Appointment.doctor)) .options(joinedload(Appointment.doctor))
.order_by(desc(Appointment.appointment_datetime)) .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) result = await self.db.execute(stmt)
return result.scalars().all() 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 = ( stmt = (
select(Appointment) select(Appointment)
.options(joinedload(Appointment.type)) .options(joinedload(Appointment.type))
@ -31,10 +36,29 @@ class AppointmentsRepository:
.filter_by(doctor_id=doctor_id) .filter_by(doctor_id=doctor_id)
.order_by(desc(Appointment.appointment_datetime)) .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) result = await self.db.execute(stmt)
return result.scalars().all() 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 = ( stmt = (
select(Appointment) select(Appointment)
.options(joinedload(Appointment.type)) .options(joinedload(Appointment.type))
@ -43,6 +67,10 @@ class AppointmentsRepository:
.filter_by(patient_id=patient_id) .filter_by(patient_id=patient_id)
.order_by(desc(Appointment.appointment_datetime)) .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) result = await self.db.execute(stmt)
return result.scalars().all() 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.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from app.domain.models import LensIssue from app.domain.models import LensIssue, Patient, User
class LensIssuesRepository: class LensIssuesRepository:
def __init__(self, db: AsyncSession): def __init__(self, db: AsyncSession):
self.db = db 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 = ( stmt = (
select(LensIssue) select(LensIssue)
.options(joinedload(LensIssue.lens)) .options(joinedload(LensIssue.lens))
.options(joinedload(LensIssue.patient)) .options(joinedload(LensIssue.patient))
.options(joinedload(LensIssue.doctor)) .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) 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]: async def get_by_id(self, lens_issue_id: int) -> Optional[LensIssue]:
stmt = select(LensIssue).filter_by(id=lens_issue_id) stmt = select(LensIssue).filter_by(id=lens_issue_id)

View File

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

View File

@ -1,8 +1,8 @@
from typing import Sequence from typing import Sequence
from sqlalchemy import select, desc, func
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from datetime import date
from app.domain.models import ScheduledAppointment from app.domain.models import ScheduledAppointment
@ -11,7 +11,8 @@ class ScheduledAppointmentsRepository:
def __init__(self, db: AsyncSession): def __init__(self, db: AsyncSession):
self.db = db 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 = ( stmt = (
select(ScheduledAppointment) select(ScheduledAppointment)
.options(joinedload(ScheduledAppointment.type)) .options(joinedload(ScheduledAppointment.type))
@ -20,10 +21,15 @@ class ScheduledAppointmentsRepository:
.filter_by(is_canceled=False) .filter_by(is_canceled=False)
.order_by(desc(ScheduledAppointment.scheduled_datetime)) .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) result = await self.db.execute(stmt)
return result.scalars().all() 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 = ( stmt = (
select(ScheduledAppointment) select(ScheduledAppointment)
.options(joinedload(ScheduledAppointment.type)) .options(joinedload(ScheduledAppointment.type))
@ -32,10 +38,29 @@ class ScheduledAppointmentsRepository:
.filter_by(doctor_id=doctor_id, is_canceled=False) .filter_by(doctor_id=doctor_id, is_canceled=False)
.order_by(desc(ScheduledAppointment.scheduled_datetime)) .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) result = await self.db.execute(stmt)
return result.scalars().all() 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 = ( stmt = (
select(ScheduledAppointment) select(ScheduledAppointment)
.options(joinedload(ScheduledAppointment.type)) .options(joinedload(ScheduledAppointment.type))
@ -44,6 +69,10 @@ class ScheduledAppointmentsRepository:
.filter_by(patient_id=patient_id, is_canceled=False) .filter_by(patient_id=patient_id, is_canceled=False)
.order_by(desc(ScheduledAppointment.scheduled_datetime)) .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) result = await self.db.execute(stmt)
return result.scalars().all() 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 sqlalchemy.ext.asyncio import AsyncSession
from datetime import date
from app.database.session import get_db from app.database.session import get_db
from app.domain.entities.appointment import AppointmentEntity from app.domain.entities.appointment import AppointmentEntity
@ -18,9 +19,11 @@ router = APIRouter()
async def get_all_appointments( async def get_all_appointments(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(get_current_user), 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) 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( @router.get(
@ -33,9 +36,26 @@ async def get_all_appointments_by_doctor_id(
doctor_id: int, doctor_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(get_current_user), 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) 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( @router.get(
@ -48,9 +68,12 @@ async def get_all_appointments_by_patient_id(
patient_id: int, patient_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(get_current_user), 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) 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( @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 sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db from app.database.session import get_db
from app.domain.entities.lens_issues import LensIssueEntity 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.dependencies import get_current_user
from app.infrastructure.lens_issues_service import LensIssuesService from app.infrastructure.lens_issues_service import LensIssuesService
@ -11,17 +15,34 @@ router = APIRouter()
@router.get( @router.get(
"/", "/",
response_model=list[LensIssueEntity], response_model=PaginatedLensIssuesResponseEntity,
summary="Get all lens issues", 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( async def get_all_lens_issues(
db: AsyncSession = Depends(get_db), page: int = Query(1, ge=1),
user=Depends(get_current_user), 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) 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( @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"), 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"), 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)"), 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), db: AsyncSession = Depends(get_db),
user=Depends(get_current_user), user=Depends(get_current_user),
): ):
patients_service = PatientsService(db) 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} return {"patients": patients, "total_count": total_count}

View File

@ -1,7 +1,7 @@
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from datetime import date
from app.database.session import get_db from app.database.session import get_db
from app.domain.entities.scheduled_appointment import ScheduledAppointmentEntity from app.domain.entities.scheduled_appointment import ScheduledAppointmentEntity
@ -20,9 +20,11 @@ router = APIRouter()
async def get_all_scheduled_appointments( async def get_all_scheduled_appointments(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(get_current_user), 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) 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( @router.get(
@ -35,9 +37,27 @@ async def get_all_scheduled_appointments_by_doctor_id(
doctor_id: int, doctor_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(get_current_user), 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) 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( @router.get(
@ -50,9 +70,12 @@ async def get_all_appointments_by_patient_id(
patient_id: int, patient_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(get_current_user), 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) 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( @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 typing import Optional
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status from starlette import status
from datetime import date
from app.application.appointment_types_repository import AppointmentTypesRepository from app.application.appointment_types_repository import AppointmentTypesRepository
from app.application.appointments_repository import AppointmentsRepository from app.application.appointments_repository import AppointmentsRepository
@ -22,45 +22,44 @@ class AppointmentsService:
self.users_repository = UsersRepository(db) self.users_repository = UsersRepository(db)
self.patients_repository = PatientsRepository(db) self.patients_repository = PatientsRepository(db)
async def get_all_appointments(self) -> list[AppointmentEntity]: async def get_all_appointments(self, start_date: date | None = None, end_date: date | None = None) -> list[
appointments = await self.appointments_repository.get_all() 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 [ async def get_appointments_by_doctor_id(self, doctor_id: int, start_date: date | None = None,
self.model_to_entity(appointment) end_date: date | None = None) -> Optional[list[AppointmentEntity]]:
for appointment in appointments
]
async def get_appointments_by_doctor_id(self, doctor_id: int) -> Optional[list[AppointmentEntity]]:
doctor = await self.users_repository.get_by_id(doctor_id) doctor = await self.users_repository.get_by_id(doctor_id)
if not doctor: if not doctor:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail='Доктор с таким ID не найден', 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 [ async def get_appointments_by_patient_id(self, patient_id: int, start_date: date | None = None,
self.model_to_entity(appointment) end_date: date | None = None) -> Optional[list[AppointmentEntity]]:
for appointment in appointments
]
async def get_appointments_by_patient_id(self, patient_id: int) -> Optional[list[AppointmentEntity]]:
patient = await self.patients_repository.get_by_id(patient_id) patient = await self.patients_repository.get_by_id(patient_id)
if not patient: if not patient:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail='Пациент с таким ID не найден', detail='Пациент с таким ID не найден',
) )
appointments = await self.appointments_repository.get_by_patient_id(patient_id, start_date=start_date,
appointments = await self.appointments_repository.get_by_patient_id(patient_id) 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 create_appointment(self, appointment: AppointmentEntity, doctor_id: int) -> Optional[AppointmentEntity]: async def create_appointment(self, appointment: AppointmentEntity, doctor_id: int) -> Optional[AppointmentEntity]:
patient = await self.patients_repository.get_by_id(appointment.patient_id) 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 fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -22,13 +23,27 @@ class LensIssuesService:
self.users_repository = UsersRepository(db) self.users_repository = UsersRepository(db)
self.lenses_repository = LensesRepository(db) self.lenses_repository = LensesRepository(db)
async def get_all_lens_issues(self) -> list[LensIssueEntity]: async def get_all_lens_issues(
lens_issues = await self.lens_issues_repository.get_all() self,
skip: int = 0,
return [ limit: int = 10,
self.model_to_entity(lens_issue) search: Optional[str] = None,
for lens_issue in lens_issues 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]: 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) 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): def __init__(self, db: AsyncSession):
self.patient_repository = PatientsRepository(db) 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]: list[PatientEntity], int]:
skip = (page - 1) * page_size 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 ( return (
[self.model_to_entity(patient) for patient in patients], [self.model_to_entity(patient) for patient in patients],
total_count total_count

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import {createApi} from "@reduxjs/toolkit/query/react"; import { createApi } from "@reduxjs/toolkit/query/react";
import {baseQueryWithAuth} from "./baseQuery.js"; import { baseQueryWithAuth } from "./baseQuery.js";
export const lensIssuesApi = createApi({ export const lensIssuesApi = createApi({
reducerPath: 'lensIssuesApi', reducerPath: 'lensIssuesApi',
@ -8,22 +7,49 @@ export const lensIssuesApi = createApi({
tagTypes: ['LensIssues'], tagTypes: ['LensIssues'],
endpoints: (builder) => ({ endpoints: (builder) => ({
getLensIssues: builder.query({ 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'], 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({ addLensIssues: builder.mutation({
query: (lensIssues) => ({ query: (lensIssues) => ({
url: `/lens_issues/`, url: '/lens_issues/',
method: 'POST', method: 'POST',
body: lensIssues body: lensIssues,
}), }),
invalidatesTags: ['LensIssues'] invalidatesTags: ['LensIssues'],
}), }),
}), }),
}); });
export const { export const {
useGetLensIssuesQuery, useGetLensIssuesQuery,
useAddLensIssuesMutation useAddLensIssuesMutation,
} = lensIssuesApi; } = lensIssuesApi;

View File

@ -18,6 +18,20 @@ export const patientsApi = createApi({
}), }),
providesTags: ['Patients'], 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({ addPatient: builder.mutation({
query: (patient) => ({ query: (patient) => ({
url: '/patients/', url: '/patients/',
@ -46,6 +60,7 @@ export const patientsApi = createApi({
export const { export const {
useGetPatientsQuery, useGetPatientsQuery,
useGetAllPatientsQuery,
useAddPatientMutation, useAddPatientMutation,
useUpdatePatientMutation, useUpdatePatientMutation,
useDeletePatientMutation, useDeletePatientMutation,

View File

@ -1,5 +1,5 @@
import {createApi} from "@reduxjs/toolkit/query/react"; import { createApi } from "@reduxjs/toolkit/query/react";
import {baseQueryWithAuth} from "./baseQuery.js"; import { baseQueryWithAuth } from "./baseQuery.js";
export const scheduledAppointmentsApi = createApi({ export const scheduledAppointmentsApi = createApi({
reducerPath: 'scheduledAppointmentsApi', reducerPath: 'scheduledAppointmentsApi',
@ -7,7 +7,14 @@ export const scheduledAppointmentsApi = createApi({
tagTypes: ['ScheduledAppointment'], tagTypes: ['ScheduledAppointment'],
endpoints: (builder) => ({ endpoints: (builder) => ({
getScheduledAppointments: builder.query({ 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'], providesTags: ['ScheduledAppointment'],
}), }),
createScheduledAppointment: builder.mutation({ createScheduledAppointment: builder.mutation({
@ -19,7 +26,7 @@ export const scheduledAppointmentsApi = createApi({
invalidatesTags: ['ScheduledAppointment'], invalidatesTags: ['ScheduledAppointment'],
}), }),
updateScheduledAppointment: builder.mutation({ updateScheduledAppointment: builder.mutation({
query: ({id, data}) => ({ query: ({ id, data }) => ({
url: `/scheduled_appointments/${id}/`, url: `/scheduled_appointments/${id}/`,
method: 'PUT', method: 'PUT',
body: data, body: data,
@ -38,6 +45,7 @@ export const scheduledAppointmentsApi = createApi({
export const { export const {
useGetScheduledAppointmentsQuery, useGetScheduledAppointmentsQuery,
useGetUpcomingScheduledAppointmentsQuery,
useCreateScheduledAppointmentMutation, useCreateScheduledAppointmentMutation,
useUpdateScheduledAppointmentMutation, useUpdateScheduledAppointmentMutation,
useCancelScheduledAppointmentMutation, 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 {useGetAppointmentTypesQuery} from "../../../Api/appointmentTypesApi.js";
import { import {
useCreateAppointmentMutation, useCreateAppointmentMutation,
@ -11,7 +11,7 @@ const useAppointmentFormModal = () => {
data: patients = [], data: patients = [],
isLoading: isLoadingPatients, isLoading: isLoadingPatients,
isError: isErrorPatients, isError: isErrorPatients,
} = useGetPatientsQuery(undefined); } = useGetAllPatientsQuery(undefined);
const { const {
data: appointmentTypes = [], data: appointmentTypes = [],
isLoading: isLoadingAppointmentTypes, isLoading: isLoadingAppointmentTypes,

View File

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

View File

@ -1,5 +1,5 @@
import {Badge, Button, Col, FloatButton, List, Result, Row, Space, Tag, Typography} from "antd"; import { Badge, Button, FloatButton, List, Result, Row, Space, Tag, Typography } from "antd";
import {Splitter} from "antd"; import { Splitter } from "antd";
import { import {
CalendarOutlined, CalendarOutlined,
MenuFoldOutlined, MenuFoldOutlined,
@ -8,38 +8,46 @@ import {
ClockCircleOutlined, ClockCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/AppointmentsCalendarTab.jsx"; import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/AppointmentsCalendarTab.jsx";
import useAppointmentsUI from "./useAppointmentsUI.js";
import useAppointments from "./useAppointments.js"; import useAppointments from "./useAppointments.js";
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import AppointmentFormModal from "../../Dummies/AppointmentFormModal/AppointmentFormModal.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 AppointmentViewModal from "../../Dummies/AppointmentViewModal/AppointmentViewModal.jsx";
import ScheduledAppointmentFormModal import ScheduledAppointmentFormModal from "../../Dummies/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx";
from "../../Dummies/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx"; import ScheduledAppointmentsViewModal from "../../Widgets/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx";
import ScheduledAppointmentsViewModal
from "../../Widgets/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx";
import AppointmentsListModal from "./Components/AppointmentsListModal/AppointmentsListModal.jsx"; import AppointmentsListModal from "./Components/AppointmentsListModal/AppointmentsListModal.jsx";
const AppointmentsPage = () => { const AppointmentsPage = () => {
const appointmentsData = useAppointments(); const {
const appointmentsPageUI = useAppointmentsUI(appointmentsData.appointments, appointmentsData.scheduledAppointments); patients, // Добавляем
const dispatch = useDispatch(); 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 (isError) return (
if (event.appointment_datetime) {
dispatch(setSelectedAppointment(event));
} else {
dispatch(setSelectedScheduledAppointment(event));
}
};
if (appointmentsData.isError) return (
<Result <Result
status="error" status="error"
title="Ошибка" title="Ошибка"
@ -49,66 +57,59 @@ const AppointmentsPage = () => {
return ( return (
<> <>
<Typography.Title level={1}><CalendarOutlined/> Приемы</Typography.Title> <Typography.Title level={1}><CalendarOutlined /> Приемы</Typography.Title>
{appointmentsData.isLoading ? ( {isLoading ? (
<LoadingIndicator/> <LoadingIndicator />
) : ( ) : (
<> <>
<Row justify="end" style={{marginBottom: 10, marginRight: "2.4rem"}}> <Row justify="end" style={{ marginBottom: 10, marginRight: "2.4rem" }}>
<Space direction={"vertical"}> <Space direction={"vertical"}>
<Tag color={"blue"} style={{width: "100%"}}> <Tag color={"blue"} style={{ width: "100%" }}>
<Badge status={"processing"} <Badge status={"processing"}
text={ text={<span style={badgeTextStyle}>Запланированный прием</span>} />
<span style={appointmentsPageUI.badgeTextStyle}>
Запланированный прием
</span>
}
/>
</Tag> </Tag>
<Tag color={"green"} style={{width: "100%"}}> <Tag color={"green"} style={{ width: "100%" }}>
<Badge status={"success"} <Badge status={"success"}
text={ text={<span style={badgeTextStyle}>Прошедший прием</span>} />
<span style={appointmentsPageUI.badgeTextStyle}>
Прошедший прием
</span>
}
/>
</Tag> </Tag>
</Space> </Space>
</Row> </Row>
<Splitter <Splitter
style={appointmentsPageUI.splitterStyle} style={splitterStyle}
min={200} min={200}
max={400} max={400}
initial={appointmentsPageUI.siderWidth} initial={siderWidth}
onChange={appointmentsPageUI.setSiderWidth} onChange={handleSetSiderWidth}
> >
<Splitter.Panel <Splitter.Panel
style={appointmentsPageUI.splitterContentPanelStyle} style={splitterContentPanelStyle}
defaultSize="80%" defaultSize="80%"
min="25%" min="25%"
max="90%" max="90%"
> >
<AppointmentsCalendarTab/> <AppointmentsCalendarTab
currentMonth={currentMonth}
onMonthChange={handleMonthChange}
appointments={appointments} // Добавляем
scheduledAppointments={scheduledAppointments} // Добавляем
/>
</Splitter.Panel> </Splitter.Panel>
{showSplitterPanel && (
{appointmentsPageUI.showSplitterPanel && (
<Splitter.Panel <Splitter.Panel
style={appointmentsPageUI.splitterSiderPanelStyle} style={splitterSiderPanelStyle}
defaultSize="20%" defaultSize="20%"
min="20%" min="20%"
max="75%" max="75%"
> >
<Typography.Title level={3} style={appointmentsPageUI.siderTitleStyle}> <Typography.Title level={3} style={siderTitleStyle}>
Предстоящие события Предстоящие события
</Typography.Title> </Typography.Title>
{appointmentsPageUI.upcomingEvents.length ? ( {upcomingEvents.length ? (
<List <List
dataSource={appointmentsPageUI.upcomingEvents.sort((a, b) => dataSource={upcomingEvents.sort((a, b) =>
dayjs(a.appointment_datetime || a.scheduled_datetime).diff( dayjs(a.appointment_datetime || a.scheduled_datetime).diff(
dayjs(b.appointment_datetime || b.scheduled_datetime) dayjs(b.appointment_datetime || b.scheduled_datetime)
) ))}
)}
renderItem={(item) => ( renderItem={(item) => (
<List.Item <List.Item
onClick={() => handleEventClick(item)} onClick={() => handleEventClick(item)}
@ -126,9 +127,9 @@ const AppointmentsPage = () => {
<Space direction="vertical" size={2}> <Space direction="vertical" size={2}>
<Space> <Space>
{item.appointment_datetime ? ( {item.appointment_datetime ? (
<ClockCircleOutlined style={{color: "#52c41a"}}/> <ClockCircleOutlined style={{ color: "#52c41a" }} />
) : ( ) : (
<CalendarOutlined style={{color: "#1890ff"}}/> <CalendarOutlined style={{ color: "#1890ff" }} />
)} )}
<Typography.Text strong> <Typography.Text strong>
{dayjs(item.appointment_datetime || item.scheduled_datetime).format('DD.MM.YYYY HH:mm')} {dayjs(item.appointment_datetime || item.scheduled_datetime).format('DD.MM.YYYY HH:mm')}
@ -155,47 +156,47 @@ const AppointmentsPage = () => {
)} )}
</Splitter> </Splitter>
<div <div
style={appointmentsPageUI.siderButtonContainerStyle} style={siderButtonContainerStyle}
onMouseEnter={appointmentsPageUI.handleHoverSider} onMouseEnter={handleHoverSider}
onMouseLeave={appointmentsPageUI.handleLeaveSider} onMouseLeave={handleLeaveSider}
> >
<Button <Button
type="primary" type="primary"
onClick={appointmentsPageUI.handleToggleSider} onClick={handleToggleSider}
icon={appointmentsPageUI.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>} icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
style={appointmentsPageUI.siderButtonStyle} style={siderButtonStyle}
> >
{appointmentsPageUI.siderButtonText} {siderButtonText}
</Button> </Button>
</div> </div>
<FloatButton.Group <FloatButton.Group
placement={"left"} placement={"left"}
trigger="hover" trigger="hover"
type="primary" type="primary"
icon={<PlusOutlined/>} icon={<PlusOutlined />}
tooltip="Создать" tooltip="Создать"
> >
<FloatButton <FloatButton
icon={<PlusOutlined/>} icon={<PlusOutlined />}
onClick={() => dispatch(openModal())} onClick={openCreateAppointmentModal}
tooltip="Прием" tooltip="Прием"
/> />
<FloatButton <FloatButton
icon={<CalendarOutlined/>} icon={<CalendarOutlined />}
onClick={appointmentsPageUI.openCreateScheduledAppointmentModal} onClick={openCreateScheduledAppointmentModal}
tooltip="Запланированный прием" tooltip="Запланированный прием"
/> />
</FloatButton.Group> </FloatButton.Group>
<AppointmentFormModal/> <AppointmentFormModal />
<AppointmentViewModal/> <AppointmentViewModal />
<ScheduledAppointmentFormModal/> <ScheduledAppointmentFormModal />
<ScheduledAppointmentsViewModal/> <ScheduledAppointmentsViewModal />
<AppointmentsListModal/> <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 "dayjs/locale/ru";
import CalendarCell from "../CalendarCell/CalendarCell.jsx"; import CalendarCell from "../CalendarCell/CalendarCell.jsx";
import useAppointments from "../../useAppointments.js";
import useAppointmentCalendarUI from "./useAppointmentCalendarUI.js"; import useAppointmentCalendarUI from "./useAppointmentCalendarUI.js";
import AppointmentsListModal from "../AppointmentsListModal/AppointmentsListModal.jsx"; import AppointmentsListModal from "../AppointmentsListModal/AppointmentsListModal.jsx";
import dayjs from "dayjs"; import dayjs from "dayjs";
import PropTypes from "prop-types";
const AppointmentsCalendarTab = () => { const AppointmentsCalendarTab = ({currentMonth, onMonthChange, appointments, scheduledAppointments}) => {
const appointmentsData = useAppointments(); const appointmentsCalendarUI = useAppointmentCalendarUI(appointments, scheduledAppointments);
const appointmentsCalendarUI = useAppointmentCalendarUI(
appointmentsData.appointments,
appointmentsData.scheduledAppointments
);
const dateCellRender = (value) => { const dateCellRender = (value) => {
const appointmentsForDate = appointmentsCalendarUI.getAppointmentsByListAndDate( const appointmentsForDate = appointmentsCalendarUI.getAppointmentsByListAndDate(
appointmentsData.appointments, appointments,
value value
); );
const scheduledForDate = appointmentsCalendarUI.getAppointmentsByListAndDate( const scheduledForDate = appointmentsCalendarUI.getAppointmentsByListAndDate(
appointmentsData.scheduledAppointments, scheduledAppointments,
value, value,
true true
); );
@ -43,14 +39,26 @@ const AppointmentsCalendarTab = () => {
<div style={appointmentsCalendarUI.calendarContainerStyle}> <div style={appointmentsCalendarUI.calendarContainerStyle}>
<Calendar <Calendar
fullscreen={appointmentsCalendarUI.fullScreenCalendar} fullscreen={appointmentsCalendarUI.fullScreenCalendar}
value={appointmentsCalendarUI.selectedDate} value={currentMonth} // Используем currentMonth вместо selectedDate
onSelect={appointmentsCalendarUI.onSelect} onSelect={appointmentsCalendarUI.onSelect}
onPanelChange={appointmentsCalendarUI.onPanelChange} onPanelChange={(value, mode) => {
appointmentsCalendarUI.onPanelChange(value, mode);
if (mode === "month") {
onMonthChange(value); // Вызываем onMonthChange при смене месяца
}
}}
cellRender={dateCellRender} cellRender={dateCellRender}
/> />
<AppointmentsListModal /> <AppointmentsListModal/>
</div> </div>
); );
}; };
AppointmentsCalendarTab.propTypes = {
currentMonth: PropTypes.object.isRequired,
onMonthChange: PropTypes.func.isRequired,
appointments: PropTypes.array.isRequired,
scheduledAppointments: PropTypes.array.isRequired,
};
export default AppointmentsCalendarTab; 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 { useEffect, useMemo, useState } from "react";
import {useGetScheduledAppointmentsQuery} from "../../../Api/scheduledAppointmentsApi.js"; import { useDispatch, useSelector } from "react-redux";
import {useGetPatientsQuery} from "../../../Api/patientsApi.js"; import { notification } from "antd";
import {notification} from "antd"; import { Grid } from "antd";
import {useEffect} from "react"; import {
import {useSelector} from "react-redux"; 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 useAppointments = () => {
const { const dispatch = useDispatch();
userData const { userData } = useSelector(state => state.auth);
} = 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 { const {
data: appointments = [], data: appointments = [],
isLoading: isLoadingAppointments, isLoading: isLoadingAppointments,
isError: isErrorAppointments, isError: isErrorAppointments,
} = useGetAppointmentsQuery((userData.id), { } = useGetAppointmentsQuery({ doctor_id: userData.id, start_date: startDate, end_date: endDate }, {
pollingInterval: 20000, pollingInterval: 60000,
skip: !userData.id,
}); });
const { const {
data: scheduledAppointments = [], data: scheduledAppointments = [],
isLoading: isLoadingScheduledAppointments, isLoading: isLoadingScheduledAppointments,
isError: isErrorScheduledAppointments, isError: isErrorScheduledAppointments,
} = useGetScheduledAppointmentsQuery((userData.id), { } = useGetScheduledAppointmentsQuery({ doctor_id: userData.id, start_date: startDate, end_date: endDate }, {
pollingInterval: 20000, pollingInterval: 60000,
skip: !userData.id,
}); });
const { const {
data: patients = [], data: patients = [],
isLoading: isLoadingPatients, isLoading: isLoadingPatients,
isError: isErrorPatients, isError: isErrorPatients,
} = useGetPatientsQuery(undefined, { } = useGetAllPatientsQuery(undefined, {
pollingInterval: 20000, 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(() => { useEffect(() => {
if (isErrorAppointments) { if (isErrorAppointments) {
notification.error({ notification.error({
@ -56,14 +185,60 @@ const useAppointments = () => {
placement: 'topRight', 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 { return {
patients, patients,
appointments, appointments,
scheduledAppointments, scheduledAppointments,
isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients, isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients ||
isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients, 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 HomePage = () => {
const homePageData = useHomePage(); 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) { if (homePageData.isError) {
return ( return (
@ -94,7 +100,7 @@ const HomePage = () => {
<Statistic <Statistic
title="Приемы за месяц" title="Приемы за месяц"
value={ value={
homePageData.appointments.filter((a) => dayjs(a.appointment_datetime).isSame(dayjs(), "month")).length homePageData.appointments.length
} }
/> />
</Card> </Card>
@ -107,7 +113,7 @@ const HomePage = () => {
</Row> </Row>
<Card <Card
title={`События на сегодня (${dayjs().format("DD.MM.YYYY")})`} title={`События на сегодня (${dayjs().tz("Europe/Moscow").format("DD.MM.YYYY")})`}
style={homePageUI.sectionStyle} style={homePageUI.sectionStyle}
> >
<List <List
@ -119,7 +125,7 @@ const HomePage = () => {
> >
<Space> <Space>
<Typography.Text strong> <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>
<Typography.Text> <Typography.Text>
{item.patient ? `${item.patient.last_name} ${item.patient.first_name}` : "Без пациента"} -{" "} {item.patient ? `${item.patient.last_name} ${item.patient.first_name}` : "Без пациента"} -{" "}

View File

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

View File

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

View File

@ -10,7 +10,8 @@ import {
Typography, Typography,
Timeline, Timeline,
Grid, Grid,
Pagination, Result Pagination,
Result
} from "antd"; } from "antd";
import {DatabaseOutlined, PlusOutlined, UnorderedListOutlined} from "@ant-design/icons"; import {DatabaseOutlined, PlusOutlined, UnorderedListOutlined} from "@ant-design/icons";
import LensIssueViewModal from "./Components/LensIssueViewModal/LensIssueViewModal.jsx"; 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 SelectViewMode from "../../Widgets/SelectViewMode/SelectViewMode.jsx";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import useIssues from "./useIssues.js"; import useIssues from "./useIssues.js";
import useIssuesUI from "./useIssuesUI.js"; import {useMemo} from "react";
const {Title} = Typography; const {Title} = Typography;
const {useBreakpoint} = Grid; const {useBreakpoint} = Grid;
const IssuesPage = () => { const IssuesPage = () => {
const issuesData = useIssues(); const issuesData = useIssues();
const issuesUI = useIssuesUI(issuesData.issues);
const screens = useBreakpoint(); const screens = useBreakpoint();
const viewModes = [ const viewModes = [
{ {
value: "table", value: "table",
label: "Таблица", label: "Таблица",
icon: <DatabaseOutlined style={issuesUI.viewModIconStyle}/>, icon: <DatabaseOutlined style={issuesData.viewModIconStyle}/>,
}, },
{ {
value: "timeline", value: "timeline",
label: "Лента", label: "Лента",
icon: <UnorderedListOutlined style={issuesUI.viewModIconStyle}/>, icon: <UnorderedListOutlined style={issuesData.viewModIconStyle}/>,
}, },
]; ];
@ -73,7 +72,7 @@ const IssuesPage = () => {
title: "Действия", title: "Действия",
key: "actions", key: "actions",
render: (_, issue) => ( 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 = () => ( const TableView = () => (
<Table <Table
columns={columns} columns={columns}
dataSource={issuesUI.filteredIssues} dataSource={issuesData.issues}
rowKey="id" rowKey="id"
pagination={issuesUI.pagination} pagination={issuesData.pagination}
showSorterTooltip={false} 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 TimeLineView = () => {
const paginatedItems = timeLineItems.slice( const timeLineItems = useMemo(() => issuesData.issues.map(issue => ({
(issuesUI.currentPage - 1) * issuesUI.pageSize, label: dayjs(issue.issue_date).format("DD.MM.YYYY"),
issuesUI.currentPage * issuesUI.pageSize 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 ( return (
<> <>
<Timeline <Timeline
items={paginatedItems} items={timeLineItems}
mode={screens.xs ? "left" : "right"} mode={screens.xs ? "left" : "right"}
/> />
<Row <Row
style={{textAlign: "center", marginTop: 20}} style={{textAlign: "center", marginTop: 20}}
align={"middle"} align="middle"
justify={"end"} justify="end"
> >
<Pagination <Pagination
current={issuesUI.currentPage} {...issuesData.pagination}
pageSize={issuesUI.pageSize}
total={timeLineItems.length}
onChange={issuesUI.handlePaginationChange}
showSizeChanger={true}
pageSizeOptions={["5", "10", "20", "50"]}
/> />
</Row> </Row>
</> </>
); );
}; };
if (issuesData.isError) return ( if (issuesData.isError) return (
<Result <Result
status="error" status="error"
@ -153,46 +139,37 @@ const IssuesPage = () => {
); );
return ( return (
<div style={issuesUI.containerStyle}> <div style={issuesData.containerStyle}>
<Title level={1}><DatabaseOutlined/> Выдача линз</Title> <Title level={1}><DatabaseOutlined/> Выдача линз</Title>
<Row gutter={[16, 16]} style={issuesUI.filterBarStyle}> <Row gutter={[16, 16]} style={issuesData.filterBarStyle}>
<Col xs={24} md={24} sm={24} xl={12}> <Col xs={24} sm={24} md={12}>
<Input <Input
placeholder="Поиск по пациенту или врачу" placeholder="Поиск по пациенту или врачу"
onChange={(e) => issuesUI.handleSetSearchText(e.target.value)} value={issuesData.tempSearchText}
style={issuesUI.formItemStyle} onChange={(e) => issuesData.handleSetTempSearchText(e.target.value)}
style={issuesData.formItemStyle}
allowClear allowClear
onClear={issuesData.handleClearSearch}
/> />
</Col> </Col>
<Col xs={24} sm={24} md={issuesData.isFilterDates ? 6 : 8}>
<Col xs={24} md={ <Tooltip title="Фильтр по дате выдачи линзы">
issuesUI.isFilterDates ? 12 : 16
} sm={
16
} xl={
issuesUI.isFilterDates ? 6 : 8
}>
<Tooltip
title="Фильтр по дате выдачи линзы"
>
<DatePicker.RangePicker <DatePicker.RangePicker
allowClear={false} allowClear={false}
style={issuesUI.formItemStyle} style={issuesData.formItemStyle}
placeholder={["Дата начала", "Дата окончания"]} placeholder={["Дата начала", "Дата окончания"]}
format="DD.MM.YYYY" format="DD.MM.YYYY"
value={issuesUI.isFilterDates && issuesUI.filterDates} value={issuesData.isFilterDates && issuesData.filterDates}
onChange={issuesUI.handleFilterDateChange} onChange={issuesData.handleFilterDateChange}
/> />
</Tooltip> </Tooltip>
</Col> </Col>
{issuesUI.isFilterDates && ( {issuesData.isFilterDates && (
<Col xs={24} md={4} sm={8} xl={2}> <Col xs={24} sm={24} md={2}>
<Tooltip <Tooltip title="Cбросить фильтр">
title="Cбросить фильтр"
>
<Button <Button
onClick={issuesUI.handleResetFilterDate} onClick={issuesData.handleResetFilterDate}
type={"primary"} type="primary"
block block
> >
Сбросить Сбросить
@ -200,22 +177,19 @@ const IssuesPage = () => {
</Tooltip> </Tooltip>
</Col> </Col>
)} )}
<Col xs={24} <Col xs={24} sm={24} md={issuesData.isFilterDates ? 4 : 4}>
md={issuesUI.isFilterDates ? 8 : 8}
sm={issuesUI.isFilterDates ? 24 : 8}
xl={4}>
<SelectViewMode <SelectViewMode
viewMode={issuesUI.viewMode} viewMode={issuesData.viewMode}
setViewMode={issuesUI.handleSetViewMode} setViewMode={issuesData.handleSetViewMode}
localStorageKey={"viewModeIssues"} localStorageKey="viewModeIssues"
toolTipText={"Формат отображения выдач линз"} toolTipText="Формат отображения выдач линз"
viewModes={viewModes} viewModes={viewModes}
/> />
</Col> </Col>
</Row> </Row>
{issuesData.isLoading ? ( {issuesData.isLoading ? (
<LoadingIndicator/> <LoadingIndicator/>
) : issuesUI.viewMode === "table" ? ( ) : issuesData.viewMode === "table" ? (
<TableView/> <TableView/>
) : ( ) : (
<TimeLineView/> <TimeLineView/>
@ -223,24 +197,27 @@ const IssuesPage = () => {
<FloatButton <FloatButton
icon={<PlusOutlined/>} icon={<PlusOutlined/>}
type={"primary"} type="primary"
onClick={issuesUI.handleAddIssue} onClick={issuesData.handleAddIssue}
tooltip={"Добавить выдачу линзы"} tooltip="Добавить выдачу линзы"
/> />
<LensIssueFormModal <LensIssueFormModal
visible={issuesUI.isModalVisible} visible={issuesData.isModalVisible}
onCancel={issuesUI.handleCloseModal} onCancel={issuesData.handleCloseModal}
onSubmit={issuesData.handleSubmitFormModal} onSubmit={issuesData.handleSubmitFormModal}
isProcessing={issuesData.isProcessing}
patients={issuesData.patients}
lenses={issuesData.lenses}
/> />
<LensIssueViewModal <LensIssueViewModal
visible={issuesUI.selectedIssue !== null} visible={issuesData.selectedIssue !== null}
onCancel={issuesUI.resetSelectedIssue} onCancel={issuesData.resetSelectedIssue}
lensIssue={issuesUI.selectedIssue} lensIssue={issuesData.selectedIssue}
/> />
</div> </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 {useDispatch, useSelector} from "react-redux";
import {useAddLensIssuesMutation, useGetLensIssuesQuery} from "../../../Api/lensIssuesApi.js";
import {notification} from "antd"; 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 useIssues = () => {
const dispatch = useDispatch(); 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, 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) => { const handleSubmitFormModal = async (issueDate, patientId, lensId) => {
dispatch(closeModal()); dispatch(closeModal());
try { 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({ notification.success({
message: "Линза выдана", message: "Линза выдана",
description: "Линза успешно выдана пациенту.", description: "Линза успешно выдана пациенту.",
placement: "topRight", placement: "topRight",
}); });
refetch();
} catch (error) { } catch (error) {
console.error('Add lens issue error:', error);
notification.error({ notification.error({
message: "Ошибка выдачи линзы", message: "Ошибка выдачи линзы",
description: error?.data?.detail || "Не удалось выдать линзу пациенту.", description: error?.data?.detail || "Не удалось выдать линзу пациенту.",
@ -32,10 +187,37 @@ const useIssues = () => {
}; };
return { return {
issues, issues: issuesData.issues,
total_count: issuesData.total_count,
patients,
lenses,
isLoading, isLoading,
isError, 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, 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: "Фамилия", title: "Фамилия",
dataIndex: "last_name", dataIndex: "last_name",
key: "last_name", key: "last_name",
sorter: (a, b) => a.last_name.localeCompare(b.last_name),
}, },
{ {
title: "Имя", title: "Имя",
dataIndex: "first_name", dataIndex: "first_name",
key: "first_name", key: "first_name",
sorter: (a, b) => a.first_name.localeCompare(b.first_name),
}, },
{ {
title: "Отчество", title: "Отчество",
@ -52,6 +54,7 @@ const PatientsPage = () => {
title: "Дата рождения", title: "Дата рождения",
dataIndex: "birthday", dataIndex: "birthday",
render: patientsData.formatDate, render: patientsData.formatDate,
sorter: (a, b) => new Date(b.birthday) - new Date(a.birthday),
}, },
{ {
title: "Телефон", title: "Телефон",