Compare commits

..

No commits in common. "76b72dce1c69079939a6b54fbe442744ce283678" and "914ae0528f2a6dab30a560ff6b95aeb627e638ce" have entirely different histories.

33 changed files with 640 additions and 1095 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, start_date: date | None = None, end_date: date | None = None) -> Sequence[Appointment]: async def get_all(self) -> Sequence[Appointment]:
stmt = ( stmt = (
select(Appointment) select(Appointment)
.options(joinedload(Appointment.type)) .options(joinedload(Appointment.type))
@ -19,15 +19,10 @@ 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, start_date: date | None = None, end_date: date | None = None) -> \ async def get_by_doctor_id(self, doctor_id: int) -> Sequence[Appointment]:
Sequence[Appointment]:
stmt = ( stmt = (
select(Appointment) select(Appointment)
.options(joinedload(Appointment.type)) .options(joinedload(Appointment.type))
@ -36,29 +31,10 @@ 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_upcoming_by_doctor_id(self, doctor_id: int) -> Sequence[Appointment]: async def get_by_patient_id(self, patient_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))
@ -67,10 +43,6 @@ 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,80 +1,26 @@
from datetime import date from typing import Optional, Sequence
from typing import Optional, Sequence, Tuple, Literal
from sqlalchemy import select, desc, or_, func, asc 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 app.domain.models import LensIssue, Patient, User from app.domain.models import LensIssue
class LensIssuesRepository: class LensIssuesRepository:
def __init__(self, db: AsyncSession): def __init__(self, db: AsyncSession):
self.db = db self.db = db
async def get_all( async def get_all(self) -> Sequence[LensIssue]:
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))
.join(Patient) .order_by(desc(LensIssue.issue_date))
.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)
issues = result.scalars().all() return 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 = 100, search: str = None, async def get_all(self, skip: int = 0, limit: int = 10, search: str = None,
sort_order: Literal["asc", "desc"] = "asc", all_params: bool = False) -> Tuple[Sequence[Patient], int]: sort_order: Literal["asc", "desc"] = "asc") -> Tuple[Sequence[Patient], int]:
stmt = select(Patient) stmt = select(Patient)
if search: if search:
@ -50,12 +50,6 @@ 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,8 +11,7 @@ class ScheduledAppointmentsRepository:
def __init__(self, db: AsyncSession): def __init__(self, db: AsyncSession):
self.db = db self.db = db
async def get_all(self, start_date: date | None = None, end_date: date | None = None) -> Sequence[ async def get_all(self) -> Sequence[ScheduledAppointment]:
ScheduledAppointment]:
stmt = ( stmt = (
select(ScheduledAppointment) select(ScheduledAppointment)
.options(joinedload(ScheduledAppointment.type)) .options(joinedload(ScheduledAppointment.type))
@ -21,15 +20,10 @@ 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, start_date: date | None = None, end_date: date | None = None) -> \ async def get_by_doctor_id(self, doctor_id: int) -> Sequence[ScheduledAppointment]:
Sequence[ScheduledAppointment]:
stmt = ( stmt = (
select(ScheduledAppointment) select(ScheduledAppointment)
.options(joinedload(ScheduledAppointment.type)) .options(joinedload(ScheduledAppointment.type))
@ -38,29 +32,10 @@ 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_upcoming_by_doctor_id(self, doctor_id: int) -> Sequence[ScheduledAppointment]: async def get_by_patient_id(self, patient_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))
@ -69,10 +44,6 @@ 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,6 +1,5 @@
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.appointment import AppointmentEntity from app.domain.entities.appointment import AppointmentEntity
@ -19,11 +18,9 @@ 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(start_date=start_date, end_date=end_date) return await appointments_service.get_all_appointments()
@router.get( @router.get(
@ -36,26 +33,9 @@ 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, start_date=start_date, end_date=end_date) return await appointments_service.get_appointments_by_doctor_id(doctor_id)
@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(
@ -68,12 +48,9 @@ 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, start_date=start_date, return await appointments_service.get_appointments_by_patient_id(patient_id)
end_date=end_date)
@router.post( @router.post(

View File

@ -1,12 +1,8 @@
from datetime import date from fastapi import APIRouter, Depends
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
@ -15,34 +11,17 @@ router = APIRouter()
@router.get( @router.get(
"/", "/",
response_model=PaginatedLensIssuesResponseEntity, response_model=list[LensIssueEntity],
summary="Get all lens issues", summary="Get all lens issues",
description="Returns a paginated list of lens issues with optional filtering and sorting", description="Returns a list of all lens issues",
) )
async def get_all_lens_issues( async def get_all_lens_issues(
page: int = Query(1, ge=1), db: AsyncSession = Depends(get_db),
page_size: int = Query(10, ge=1, le=100), user=Depends(get_current_user),
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)
skip = (page - 1) * page_size return await lens_issues_service.get_all_lens_issues()
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,12 +23,11 @@ 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, all_params) patients, total_count = await patients_service.get_all_patients(page, page_size, search, sort_order)
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,11 +20,9 @@ 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(start_date=start_date, end_date=end_date) return await scheduled_appointments_service.get_all_scheduled_appointments()
@router.get( @router.get(
@ -37,27 +35,9 @@ 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, start_date=start_date, return await appointments_service.get_scheduled_appointments_by_doctor_id(doctor_id)
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(
@ -70,12 +50,9 @@ 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, start_date=start_date, return await appointments_service.get_scheduled_appointments_by_patient_id(patient_id)
end_date=end_date)
@router.post( @router.post(

View File

@ -1,8 +0,0 @@
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,44 +22,45 @@ 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, start_date: date | None = None, end_date: date | None = None) -> list[ async def get_all_appointments(self) -> list[AppointmentEntity]:
AppointmentEntity]: appointments = await self.appointments_repository.get_all()
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]
async def get_appointments_by_doctor_id(self, doctor_id: int, start_date: date | None = None, return [
end_date: date | None = None) -> Optional[list[AppointmentEntity]]: self.model_to_entity(appointment)
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]
async def get_upcoming_appointments_by_doctor_id(self, doctor_id: int) -> Optional[list[AppointmentEntity]]: appointments = await self.appointments_repository.get_by_doctor_id(doctor_id)
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]
async def get_appointments_by_patient_id(self, patient_id: int, start_date: date | None = None, return [
end_date: date | None = None) -> Optional[list[AppointmentEntity]]: self.model_to_entity(appointment)
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,
end_date=end_date) appointments = await self.appointments_repository.get_by_patient_id(patient_id)
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,5 +1,4 @@
from datetime import date from typing import Optional
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
@ -23,27 +22,13 @@ 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( async def get_all_lens_issues(self) -> list[LensIssueEntity]:
self, lens_issues = await self.lens_issues_repository.get_all()
skip: int = 0,
limit: int = 10, return [
search: Optional[str] = None, self.model_to_entity(lens_issue)
sort_order: Literal["asc", "desc"] = "desc", for lens_issue in lens_issues
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,12 +13,10 @@ 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, async def get_all_patients(self, page: int = 1, page_size: int = 10, search: str = None, sort_order: Literal["asc", "desc"] = "asc") -> Tuple[
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, patients, total_count = await self.patient_repository.get_all(skip=skip, limit=page_size, search=search, sort_order=sort_order)
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,53 +19,54 @@ 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, start_date: date | None = None, end_date: date | None = None) -> \ async def get_all_scheduled_appointments(self) -> list[ScheduledAppointmentEntity]:
list[ScheduledAppointmentEntity]: scheduled_appointments = await self.scheduled_appointment_repository.get_all()
scheduled_appointments = await self.scheduled_appointment_repository.get_all(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 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]
async def get_upcoming_scheduled_appointments_by_doctor_id(self, doctor_id: int) -> Optional[ scheduled_appointments = await self.scheduled_appointment_repository.get_by_doctor_id(doctor_id)
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]
async def get_scheduled_appointments_by_patient_id(self, patient_id: int, start_date: date | None = None, return [
end_date: date | None = None) -> Optional[ self.model_to_entity(scheduled_appointment)
list[ScheduledAppointmentEntity]]: for scheduled_appointment in scheduled_appointments
]
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,
start_date=start_date, scheduled_appointments = await self.scheduled_appointment_repository.get_by_patient_id(patient_id)
end_date=end_date)
return [self.model_to_entity(scheduled_appointment) for scheduled_appointment in scheduled_appointments] return [
self.model_to_entity(scheduled_appointment)
for scheduled_appointment in scheduled_appointments
]
async def create_scheduled_appointment(self, scheduled_appointment: ScheduledAppointmentEntity, doctor_id: int) -> \ async def create_scheduled_appointment(self, scheduled_appointment: ScheduledAppointmentEntity, doctor_id: int) -> \
Optional[ScheduledAppointmentEntity]: Optional[
ScheduledAppointmentEntity
]:
patient = await self.patients_repository.get_by_id(scheduled_appointment.patient_id) patient = await self.patients_repository.get_by_id(scheduled_appointment.patient_id)
if not patient: if not patient:
@ -98,7 +99,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: int): async def cancel_scheduled_appointment(self, scheduled_appointment_id: int, doctor_id):
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:
@ -128,9 +129,11 @@ class ScheduledAppointmentsService:
return self.model_to_entity(scheduled_appointment_model) return self.model_to_entity(scheduled_appointment_model)
async def update_scheduled_appointment(self, scheduled_appointment_id: int, async def update_scheduled_appointment(
scheduled_appointment: ScheduledAppointmentEntity) -> Optional[ self,
ScheduledAppointmentEntity]: scheduled_appointment_id: int,
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,17 +7,10 @@ export const appointmentsApi = createApi({
tagTypes: ['Appointment'], tagTypes: ['Appointment'],
endpoints: (builder) => ({ endpoints: (builder) => ({
getAppointments: builder.query({ getAppointments: builder.query({
query: ({doctor_id, start_date, end_date}) => ({ query: (doctor_id) => `/appointments/doctor/${doctor_id}/`,
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'],
@ -44,7 +37,6 @@ export const appointmentsApi = createApi({
export const { export const {
useGetAppointmentsQuery, useGetAppointmentsQuery,
useGetUpcomingAppointmentsQuery,
useGetByPatientIdQuery, useGetByPatientIdQuery,
useCreateAppointmentMutation, useCreateAppointmentMutation,
useUpdateAppointmentMutation, useUpdateAppointmentMutation,

View File

@ -1,5 +1,6 @@
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',
@ -7,49 +8,22 @@ export const lensIssuesApi = createApi({
tagTypes: ['LensIssues'], tagTypes: ['LensIssues'],
endpoints: (builder) => ({ endpoints: (builder) => ({
getLensIssues: builder.query({ getLensIssues: builder.query({
query: ({ page, pageSize, search, sortOrder, startDate, endDate }) => ({ query: () => '/lens_issues/',
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'],
transformResponse: (response) => { refetchOnMountOrArgChange: 5
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,20 +18,6 @@ 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/',
@ -60,7 +46,6 @@ 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,14 +7,7 @@ export const scheduledAppointmentsApi = createApi({
tagTypes: ['ScheduledAppointment'], tagTypes: ['ScheduledAppointment'],
endpoints: (builder) => ({ endpoints: (builder) => ({
getScheduledAppointments: builder.query({ getScheduledAppointments: builder.query({
query: ({ doctor_id, start_date, end_date }) => ({ query: (doctor_id) => `/scheduled_appointments/doctor/${doctor_id}/`,
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({
@ -26,7 +19,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,
@ -45,7 +38,6 @@ export const scheduledAppointmentsApi = createApi({
export const { export const {
useGetScheduledAppointmentsQuery, useGetScheduledAppointmentsQuery,
useGetUpcomingScheduledAppointmentsQuery,
useCreateScheduledAppointmentMutation, useCreateScheduledAppointmentMutation,
useUpdateScheduledAppointmentMutation, useUpdateScheduledAppointmentMutation,
useCancelScheduledAppointmentMutation, useCancelScheduledAppointmentMutation,

View File

@ -1,4 +1,4 @@
import {useGetAllPatientsQuery, useGetPatientsQuery} from "../../../Api/patientsApi.js"; import {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,
} = useGetAllPatientsQuery(undefined); } = useGetPatientsQuery(undefined);
const { const {
data: appointmentTypes = [], data: appointmentTypes = [],
isLoading: isLoadingAppointmentTypes, isLoading: isLoadingAppointmentTypes,

View File

@ -6,6 +6,7 @@ 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;
@ -29,20 +30,9 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
const [uploadAppointmentFile, {isLoading: isUploadingFile}] = useUploadAppointmentFileMutation(); const [uploadAppointmentFile, {isLoading: isUploadingFile}] = useUploadAppointmentFileMutation();
const startDate = appointmentDate.startOf('month').utc().format('YYYY-MM-DD'); const {data: appointments = []} = useGetAppointmentsQuery(userData.id, {
const endDate = appointmentDate.endOf('month').utc().format('YYYY-MM-DD'); pollingInterval: 20000,
});
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 = [],
@ -53,16 +43,6 @@ 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"};
@ -200,7 +180,6 @@ 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({
@ -240,14 +219,9 @@ 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 = () => {
@ -356,8 +330,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",
}); });
} }
@ -431,8 +405,6 @@ const useAppointmentFormModalUI = (createAppointment, patients, cancelAppointmen
handleAddFile, handleAddFile,
handleRemoveFile, handleRemoveFile,
isUploadingFile, isUploadingFile,
isLoadingAppointments,
isErrorAppointments,
}; };
}; };

View File

@ -1,4 +1,4 @@
import {useGetAllPatientsQuery} from "../../../Api/patientsApi.js"; import {useGetPatientsQuery} 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,
} = useGetAllPatientsQuery(undefined, { } = useGetPatientsQuery(undefined, {
pollingInterval: 20000, pollingInterval: 20000,
}); });

View File

@ -1,5 +1,5 @@
import { Badge, Button, FloatButton, List, Result, Row, Space, Tag, Typography } from "antd"; import {Badge, Button, Col, FloatButton, List, Result, Row, Space, Tag, Typography} from "antd";
import { Splitter } from "antd"; import {Splitter} from "antd";
import { import {
CalendarOutlined, CalendarOutlined,
MenuFoldOutlined, MenuFoldOutlined,
@ -8,46 +8,38 @@ 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 from "../../Dummies/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx"; import ScheduledAppointmentFormModal
import ScheduledAppointmentsViewModal from "../../Widgets/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx"; from "../../Dummies/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.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 { const appointmentsData = useAppointments();
patients, // Добавляем const appointmentsPageUI = useAppointmentsUI(appointmentsData.appointments, appointmentsData.scheduledAppointments);
appointments, // Добавляем const dispatch = useDispatch();
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();
if (isError) return ( const handleEventClick = (event) => {
if (event.appointment_datetime) {
dispatch(setSelectedAppointment(event));
} else {
dispatch(setSelectedScheduledAppointment(event));
}
};
if (appointmentsData.isError) return (
<Result <Result
status="error" status="error"
title="Ошибка" title="Ошибка"
@ -57,59 +49,66 @@ const AppointmentsPage = () => {
return ( return (
<> <>
<Typography.Title level={1}><CalendarOutlined /> Приемы</Typography.Title> <Typography.Title level={1}><CalendarOutlined/> Приемы</Typography.Title>
{isLoading ? ( {appointmentsData.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={<span style={badgeTextStyle}>Запланированный прием</span>} /> text={
<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={<span style={badgeTextStyle}>Прошедший прием</span>} /> text={
<span style={appointmentsPageUI.badgeTextStyle}>
Прошедший прием
</span>
}
/>
</Tag> </Tag>
</Space> </Space>
</Row> </Row>
<Splitter <Splitter
style={splitterStyle} style={appointmentsPageUI.splitterStyle}
min={200} min={200}
max={400} max={400}
initial={siderWidth} initial={appointmentsPageUI.siderWidth}
onChange={handleSetSiderWidth} onChange={appointmentsPageUI.setSiderWidth}
> >
<Splitter.Panel <Splitter.Panel
style={splitterContentPanelStyle} style={appointmentsPageUI.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={splitterSiderPanelStyle} style={appointmentsPageUI.splitterSiderPanelStyle}
defaultSize="20%" defaultSize="20%"
min="20%" min="20%"
max="75%" max="75%"
> >
<Typography.Title level={3} style={siderTitleStyle}> <Typography.Title level={3} style={appointmentsPageUI.siderTitleStyle}>
Предстоящие события Предстоящие события
</Typography.Title> </Typography.Title>
{upcomingEvents.length ? ( {appointmentsPageUI.upcomingEvents.length ? (
<List <List
dataSource={upcomingEvents.sort((a, b) => dataSource={appointmentsPageUI.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)}
@ -127,9 +126,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')}
@ -156,43 +155,43 @@ const AppointmentsPage = () => {
)} )}
</Splitter> </Splitter>
<div <div
style={siderButtonContainerStyle} style={appointmentsPageUI.siderButtonContainerStyle}
onMouseEnter={handleHoverSider} onMouseEnter={appointmentsPageUI.handleHoverSider}
onMouseLeave={handleLeaveSider} onMouseLeave={appointmentsPageUI.handleLeaveSider}
> >
<Button <Button
type="primary" type="primary"
onClick={handleToggleSider} onClick={appointmentsPageUI.handleToggleSider}
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />} icon={appointmentsPageUI.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
style={siderButtonStyle} style={appointmentsPageUI.siderButtonStyle}
> >
{siderButtonText} {appointmentsPageUI.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={openCreateAppointmentModal} onClick={() => dispatch(openModal())}
tooltip="Прием" tooltip="Прием"
/> />
<FloatButton <FloatButton
icon={<CalendarOutlined />} icon={<CalendarOutlined/>}
onClick={openCreateScheduledAppointmentModal} onClick={appointmentsPageUI.openCreateScheduledAppointmentModal}
tooltip="Запланированный прием" tooltip="Запланированный прием"
/> />
</FloatButton.Group> </FloatButton.Group>
<AppointmentFormModal /> <AppointmentFormModal/>
<AppointmentViewModal /> <AppointmentViewModal/>
<ScheduledAppointmentFormModal /> <ScheduledAppointmentFormModal/>
<ScheduledAppointmentsViewModal /> <ScheduledAppointmentsViewModal/>
<AppointmentsListModal /> <AppointmentsListModal/>
</> </>
)} )}
</> </>

View File

@ -1,21 +1,25 @@
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 = ({currentMonth, onMonthChange, appointments, scheduledAppointments}) => { const AppointmentsCalendarTab = () => {
const appointmentsCalendarUI = useAppointmentCalendarUI(appointments, scheduledAppointments); const appointmentsData = useAppointments();
const appointmentsCalendarUI = useAppointmentCalendarUI(
appointmentsData.appointments,
appointmentsData.scheduledAppointments
);
const dateCellRender = (value) => { const dateCellRender = (value) => {
const appointmentsForDate = appointmentsCalendarUI.getAppointmentsByListAndDate( const appointmentsForDate = appointmentsCalendarUI.getAppointmentsByListAndDate(
appointments, appointmentsData.appointments,
value value
); );
const scheduledForDate = appointmentsCalendarUI.getAppointmentsByListAndDate( const scheduledForDate = appointmentsCalendarUI.getAppointmentsByListAndDate(
scheduledAppointments, appointmentsData.scheduledAppointments,
value, value,
true true
); );
@ -39,26 +43,14 @@ const AppointmentsCalendarTab = ({currentMonth, onMonthChange, appointments, sch
<div style={appointmentsCalendarUI.calendarContainerStyle}> <div style={appointmentsCalendarUI.calendarContainerStyle}>
<Calendar <Calendar
fullscreen={appointmentsCalendarUI.fullScreenCalendar} fullscreen={appointmentsCalendarUI.fullScreenCalendar}
value={currentMonth} // Используем currentMonth вместо selectedDate value={appointmentsCalendarUI.selectedDate}
onSelect={appointmentsCalendarUI.onSelect} onSelect={appointmentsCalendarUI.onSelect}
onPanelChange={(value, mode) => { onPanelChange={appointmentsCalendarUI.onPanelChange}
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

@ -0,0 +1,13 @@
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,168 +1,39 @@
import { useEffect, useMemo, useState } from "react"; import {useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js";
import { useDispatch, useSelector } from "react-redux"; import {useGetScheduledAppointmentsQuery} from "../../../Api/scheduledAppointmentsApi.js";
import { notification } from "antd"; import {useGetPatientsQuery} from "../../../Api/patientsApi.js";
import { Grid } from "antd"; import {notification} from "antd";
import { import {useEffect} from "react";
useGetAppointmentsQuery, import {useSelector} from "react-redux";
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 dispatch = useDispatch(); const {
const { userData } = useSelector(state => state.auth); userData
const { collapsed, siderWidth, hovered, selectedAppointment } = useSelector(state => state.appointmentsUI); } = useSelector(state => state.auth);
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({ doctor_id: userData.id, start_date: startDate, end_date: endDate }, { } = useGetAppointmentsQuery((userData.id), {
pollingInterval: 60000, pollingInterval: 20000,
skip: !userData.id,
}); });
const { const {
data: scheduledAppointments = [], data: scheduledAppointments = [],
isLoading: isLoadingScheduledAppointments, isLoading: isLoadingScheduledAppointments,
isError: isErrorScheduledAppointments, isError: isErrorScheduledAppointments,
} = useGetScheduledAppointmentsQuery({ doctor_id: userData.id, start_date: startDate, end_date: endDate }, { } = useGetScheduledAppointmentsQuery((userData.id), {
pollingInterval: 60000, pollingInterval: 20000,
skip: !userData.id,
}); });
const { const {
data: patients = [], data: patients = [],
isLoading: isLoadingPatients, isLoading: isLoadingPatients,
isError: isErrorPatients, isError: isErrorPatients,
} = useGetAllPatientsQuery(undefined, { } = useGetPatientsQuery(undefined, {
pollingInterval: 60000, pollingInterval: 20000,
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({
@ -185,60 +56,14 @@ const useAppointments = () => {
placement: 'topRight', placement: 'topRight',
}); });
} }
if (isErrorUpcomingAppointments) { }, [isErrorAppointments, isErrorScheduledAppointments, isErrorPatients]);
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,
isLoadingUpcomingAppointments || isLoadingUpcomingScheduledAppointments, isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients,
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

@ -0,0 +1,97 @@
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,13 +31,7 @@ ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
const HomePage = () => { const HomePage = () => {
const homePageData = useHomePage(); const homePageData = useHomePage();
const homePageUI = useHomePageUI( const homePageUI = useHomePageUI(homePageData.appointments, homePageData.scheduledAppointments, homePageData.patients);
homePageData.appointments,
homePageData.scheduledAppointments,
homePageData.patients,
homePageData.upcomingAppointments,
homePageData.upcomingScheduledAppointments
);
if (homePageData.isError) { if (homePageData.isError) {
return ( return (
@ -100,7 +94,7 @@ const HomePage = () => {
<Statistic <Statistic
title="Приемы за месяц" title="Приемы за месяц"
value={ value={
homePageData.appointments.length homePageData.appointments.filter((a) => dayjs(a.appointment_datetime).isSame(dayjs(), "month")).length
} }
/> />
</Card> </Card>
@ -113,7 +107,7 @@ const HomePage = () => {
</Row> </Row>
<Card <Card
title={`События на сегодня (${dayjs().tz("Europe/Moscow").format("DD.MM.YYYY")})`} title={`События на сегодня (${dayjs().format("DD.MM.YYYY")})`}
style={homePageUI.sectionStyle} style={homePageUI.sectionStyle}
> >
<List <List
@ -125,7 +119,7 @@ const HomePage = () => {
> >
<Space> <Space>
<Typography.Text strong> <Typography.Text strong>
{dayjs(item.appointment_datetime || item.scheduled_datetime).tz("Europe/Moscow").format("HH:mm")} {dayjs(item.appointment_datetime || item.scheduled_datetime).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,9 +1,6 @@
import {useGetAppointmentsQuery, useGetUpcomingAppointmentsQuery} from "../../../Api/appointmentsApi.js"; import {useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js";
import { import {useGetScheduledAppointmentsQuery} from "../../../Api/scheduledAppointmentsApi.js";
useGetScheduledAppointmentsQuery, import {useGetPatientsQuery} from "../../../Api/patientsApi.js";
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";
@ -17,65 +14,40 @@ 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 {useGetAppointmentTypesQuery} from "../../../Api/appointmentTypesApi.js"; // Import isBetween plugin
dayjs.extend(utc); dayjs.extend(isBetween); // Extend dayjs with isBetween
dayjs.extend(timezone);
dayjs.extend(isBetween);
const useHomePage = () => { const useHomePage = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const {userData} = useSelector((state) => state.auth);
const startDate = dayjs().startOf('month').format('YYYY-MM-DD'); const {
const endDate = dayjs().endOf('month').format('YYYY-MM-DD'); userData
} = useSelector((state) => state.auth);
const { const {
data: appointments = [], data: appointments = [],
isLoading: isLoadingAppointments, isLoading: isLoadingAppointments,
isError: isErrorAppointments, isError: isErrorAppointments,
} = useGetAppointmentsQuery({doctor_id: userData.id, start_date: startDate, end_date: endDate}, { } = useGetAppointmentsQuery((userData.id), {
pollingInterval: 60000, pollingInterval: 20000,
skip: !userData.id,
}); });
const { const {
data: scheduledAppointments = [], data: scheduledAppointments = [],
isLoading: isLoadingScheduledAppointments, isLoading: isLoadingScheduledAppointments,
isError: isErrorScheduledAppointments, isError: isErrorScheduledAppointments,
} = useGetScheduledAppointmentsQuery({doctor_id: userData.id, start_date: startDate, end_date: endDate}, { } = useGetScheduledAppointmentsQuery((userData.id), {
pollingInterval: 60000, pollingInterval: 20000,
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,
} = useGetAllPatientsQuery(undefined, { } = useGetPatientsQuery(undefined, {
pollingInterval: 60000, pollingInterval: 20000,
skip: !userData.id,
}); });
const { const {
@ -83,8 +55,7 @@ const useHomePage = () => {
isLoading: isLoadingAppointmentTypes, isLoading: isLoadingAppointmentTypes,
isError: isErrorAppointmentTypes, isError: isErrorAppointmentTypes,
} = useGetAppointmentTypesQuery(undefined, { } = useGetAppointmentTypesQuery(undefined, {
pollingInterval: 60000, pollingInterval: 20000,
skip: !userData.id,
}); });
useEffect(() => { useEffect(() => {
@ -102,20 +73,6 @@ 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: "Ошибка",
@ -130,14 +87,7 @@ 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) {
@ -163,21 +113,15 @@ 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,35 +1,32 @@
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, upcomingAppointments, upcomingScheduledAppointments) => { const useHomePageUI = (appointments, scheduledAppointments, patients) => {
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 [...upcomingAppointments, ...upcomingScheduledAppointments].filter((event) => return [...appointments, ...scheduledAppointments].filter((event) =>
dayjs(event.appointment_datetime || event.scheduled_datetime).isSame(dayjs(), "day") dayjs(event.appointment_datetime || event.scheduled_datetime).isSame(dayjs(), "day")
); );
}, [upcomingAppointments, upcomingScheduledAppointments]); }, [appointments, scheduledAppointments]);
const upcomingBirthdays = useMemo(() => { const upcomingBirthdays = useMemo(() => {
return patients.filter((p) => return patients.filter((p) =>
@ -41,12 +38,7 @@ const useHomePageUI = (appointments, scheduledAppointments, patients, upcomingAp
const data = Array(7).fill(0); const data = Array(7).fill(0);
appointments appointments
.filter((app) => .filter((app) =>
dayjs(app.appointment_datetime).isBetween( dayjs(app.appointment_datetime).isBetween(dayjs().startOf("week"), dayjs().endOf("week"), "day", "[]")
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();
@ -59,12 +51,7 @@ const useHomePageUI = (appointments, scheduledAppointments, patients, upcomingAp
const data = Array(7).fill(0); const data = Array(7).fill(0);
scheduledAppointments scheduledAppointments
.filter((app) => .filter((app) =>
dayjs(app.scheduled_datetime).isBetween( dayjs(app.scheduled_datetime).isBetween(dayjs().startOf("week"), dayjs().endOf("week"), "day", "[]")
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();
@ -77,12 +64,12 @@ const useHomePageUI = (appointments, scheduledAppointments, patients, upcomingAp
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: true, position: "top"}, legend: { display: false },
title: {display: true, text: "Приемы за неделю"}, title: { display: true, text: "Приемы за неделю" },
}, },
}; };

View File

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

View File

@ -1,183 +1,28 @@
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 { import {closeModal} from "../../../Redux/Slices/lensIssuesSlice.js";
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 [tempSearchText, setTempSearchText] = useState(""); const {data: issues = [], isLoading, isError, error} = useGetLensIssuesQuery(undefined, {
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 {
const formattedIssueDate = dayjs(issueDate).format('YYYY-MM-DD'); await addIssue({issue_date: issueDate, patient_id: patientId, lens_id: lensId});
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 || "Не удалось выдать линзу пациенту.",
@ -187,37 +32,10 @@ const useIssues = () => {
}; };
return { return {
issues: issuesData.issues, issues,
total_count: issuesData.total_count,
patients,
lenses,
isLoading, isLoading,
isError, isError,
isProcessing: isAdding, error,
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

@ -0,0 +1,147 @@
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,13 +37,11 @@ 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: "Отчество",
@ -54,7 +52,6 @@ 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: "Телефон",