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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,8 @@
from datetime import date
from typing import Optional, Literal
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.lens_issues import LensIssueEntity
from app.domain.entities.responses.paginated_issue import PaginatedLensIssuesResponseEntity
from app.infrastructure.dependencies import get_current_user
from app.infrastructure.lens_issues_service import LensIssuesService
@ -15,34 +11,17 @@ router = APIRouter()
@router.get(
"/",
response_model=PaginatedLensIssuesResponseEntity,
response_model=list[LensIssueEntity],
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(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
search: Optional[str] = Query(None),
sort_order: Literal["asc", "desc"] = Query("desc"),
start_date: Optional[date] = Query(None),
end_date: Optional[date] = Query(None),
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
lens_issues_service = LensIssuesService(db)
skip = (page - 1) * page_size
issues, total_count = await lens_issues_service.get_all_lens_issues(
skip=skip,
limit=page_size,
search=search,
sort_order=sort_order,
start_date=start_date,
end_date=end_date
)
return PaginatedLensIssuesResponseEntity(
issues=issues,
total_count=total_count
)
return await lens_issues_service.get_all_lens_issues()
@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"),
search: str = Query(None, description="Search term for filtering patients"),
sort_order: Literal["asc", "desc"] = Query("asc", description="Sort order by first_name (asc or desc)"),
all_params: bool = Query(False, description="Get all patients"),
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
patients_service = PatientsService(db)
patients, total_count = await patients_service.get_all_patients(page, page_size, search, sort_order, all_params)
patients, total_count = await patients_service.get_all_patients(page, page_size, search, sort_order)
return {"patients": patients, "total_count": total_count}

View File

@ -1,7 +1,7 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import date
from app.database.session import get_db
from app.domain.entities.scheduled_appointment import ScheduledAppointmentEntity
@ -20,11 +20,9 @@ router = APIRouter()
async def get_all_scheduled_appointments(
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
start_date: date | None = Query(None, description="Start date for filtering (YYYY-MM-DD)"),
end_date: date | None = Query(None, description="End date for filtering (YYYY-MM-DD)"),
):
scheduled_appointments_service = ScheduledAppointmentsService(db)
return await scheduled_appointments_service.get_all_scheduled_appointments(start_date=start_date, end_date=end_date)
return await scheduled_appointments_service.get_all_scheduled_appointments()
@router.get(
@ -37,27 +35,9 @@ async def get_all_scheduled_appointments_by_doctor_id(
doctor_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
start_date: date | None = Query(None, description="Start date for filtering (YYYY-MM-DD)"),
end_date: date | None = Query(None, description="End date for filtering (YYYY-MM-DD)"),
):
appointments_service = ScheduledAppointmentsService(db)
return await appointments_service.get_scheduled_appointments_by_doctor_id(doctor_id, start_date=start_date,
end_date=end_date)
@router.get(
"/doctor/{doctor_id}/upcoming/",
response_model=list[ScheduledAppointmentEntity],
summary="Get upcoming scheduled appointments for doctor",
description="Returns the next 5 upcoming scheduled appointments for doctor",
)
async def get_upcoming_scheduled_appointments_by_doctor_id(
doctor_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
appointments_service = ScheduledAppointmentsService(db)
return await appointments_service.get_upcoming_scheduled_appointments_by_doctor_id(doctor_id)
return await appointments_service.get_scheduled_appointments_by_doctor_id(doctor_id)
@router.get(
@ -70,12 +50,9 @@ async def get_all_appointments_by_patient_id(
patient_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
start_date: date | None = Query(None, description="Start date for filtering (YYYY-MM-DD)"),
end_date: date | None = Query(None, description="End date for filtering (YYYY-MM-DD)"),
):
appointments_service = ScheduledAppointmentsService(db)
return await appointments_service.get_scheduled_appointments_by_patient_id(patient_id, start_date=start_date,
end_date=end_date)
return await appointments_service.get_scheduled_appointments_by_patient_id(patient_id)
@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 fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status
from datetime import date
from app.application.appointment_types_repository import AppointmentTypesRepository
from app.application.appointments_repository import AppointmentsRepository
@ -22,44 +22,45 @@ class AppointmentsService:
self.users_repository = UsersRepository(db)
self.patients_repository = PatientsRepository(db)
async def get_all_appointments(self, start_date: date | None = None, end_date: date | None = None) -> list[
AppointmentEntity]:
appointments = await self.appointments_repository.get_all(start_date=start_date, end_date=end_date)
return [self.model_to_entity(appointment) for appointment in appointments]
async def get_all_appointments(self) -> list[AppointmentEntity]:
appointments = await self.appointments_repository.get_all()
async def get_appointments_by_doctor_id(self, doctor_id: int, start_date: date | None = None,
end_date: date | None = None) -> Optional[list[AppointmentEntity]]:
return [
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)
if not doctor:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Доктор с таким ID не найден',
)
appointments = await self.appointments_repository.get_by_doctor_id(doctor_id, start_date=start_date,
end_date=end_date)
return [self.model_to_entity(appointment) for appointment in appointments]
async def get_upcoming_appointments_by_doctor_id(self, doctor_id: int) -> Optional[list[AppointmentEntity]]:
doctor = await self.users_repository.get_by_id(doctor_id)
if not doctor:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Доктор с таким ID не найден',
)
appointments = await self.appointments_repository.get_upcoming_by_doctor_id(doctor_id)
return [self.model_to_entity(appointment) for appointment in appointments]
appointments = await self.appointments_repository.get_by_doctor_id(doctor_id)
async def get_appointments_by_patient_id(self, patient_id: int, start_date: date | None = None,
end_date: date | None = None) -> Optional[list[AppointmentEntity]]:
return [
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)
if not patient:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Пациент с таким ID не найден',
)
appointments = await self.appointments_repository.get_by_patient_id(patient_id, start_date=start_date,
end_date=end_date)
return [self.model_to_entity(appointment) for appointment in appointments]
appointments = await self.appointments_repository.get_by_patient_id(patient_id)
return [
self.model_to_entity(appointment)
for appointment in appointments
]
async def create_appointment(self, appointment: AppointmentEntity, doctor_id: int) -> Optional[AppointmentEntity]:
patient = await self.patients_repository.get_by_id(appointment.patient_id)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -1,183 +1,28 @@
import {useEffect, useState} from "react";
import {useDispatch, useSelector} from "react-redux";
import {useAddLensIssuesMutation, useGetLensIssuesQuery} from "../../../Api/lensIssuesApi.js";
import {notification} from "antd";
import {
useAddLensIssuesMutation,
useGetLensIssuesQuery,
} from "../../../Api/lensIssuesApi.js";
import {useGetAllPatientsQuery} from "../../../Api/patientsApi.js";
import {
closeModal,
openModal,
selectIssue,
setCurrentPage,
setEndFilterDate,
setPageSize,
setViewMode,
setStartFilterDate,
} from "../../../Redux/Slices/lensIssuesSlice.js";
import {getCachedInfo} from "../../../Utils/cachedInfoUtils.js";
import dayjs from "dayjs";
import {useGetNotIssuedLensesQuery} from "../../../Api/lensesApi.js";
import {closeModal} from "../../../Redux/Slices/lensIssuesSlice.js";
const useIssues = () => {
const dispatch = useDispatch();
const {
currentPage,
pageSize,
selectedIssue,
isModalVisible,
viewMode,
startFilterDate,
endFilterDate,
} = useSelector(state => state.lensIssuesUI);
const [tempSearchText, setTempSearchText] = useState("");
const {
data: issuesData = {issues: [], total_count: 0},
isLoading: isIssuesLoading,
isError: isIssuesError,
error: issuesError,
refetch
} = useGetLensIssuesQuery({
page: currentPage,
pageSize,
search: tempSearchText || undefined, // Используем tempSearchText напрямую
sortOrder: 'desc',
startDate: startFilterDate ? dayjs(startFilterDate).format('YYYY-MM-DD') : undefined,
endDate: endFilterDate ? dayjs(endFilterDate).format('YYYY-MM-DD') : undefined,
}, {
const {data: issues = [], isLoading, isError, error} = useGetLensIssuesQuery(undefined, {
pollingInterval: 20000,
});
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 [addIssue] = useAddLensIssuesMutation();
const handleSubmitFormModal = async (issueDate, patientId, lensId) => {
dispatch(closeModal());
try {
const formattedIssueDate = dayjs(issueDate).format('YYYY-MM-DD');
await addIssue({issue_date: formattedIssueDate, patient_id: patientId, lens_id: lensId}).unwrap();
await addIssue({issue_date: issueDate, patient_id: patientId, lens_id: lensId});
notification.success({
message: "Линза выдана",
description: "Линза успешно выдана пациенту.",
placement: "topRight",
});
refetch();
} catch (error) {
console.error('Add lens issue error:', error);
notification.error({
message: "Ошибка выдачи линзы",
description: error?.data?.detail || "Не удалось выдать линзу пациенту.",
@ -187,37 +32,10 @@ const useIssues = () => {
};
return {
issues: issuesData.issues,
total_count: issuesData.total_count,
patients,
lenses,
issues,
isLoading,
isError,
isProcessing: isAdding,
tempSearchText, // Возвращаем только tempSearchText
selectedIssue,
isModalVisible,
viewMode,
currentPage,
pageSize,
filterDates,
isFilterDates,
containerStyle,
filterBarStyle,
formItemStyle,
viewModIconStyle,
advancedSearchCardStyle,
pagination,
handleAddIssue,
handlePaginationChange,
handleSetTempSearchText,
handleClearSearch,
handleSetViewMode,
handleCloseModal,
handleFilterDateChange,
resetSelectedIssue,
handleSelectIssue,
handleResetFilterDate,
error,
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: "Фамилия",
dataIndex: "last_name",
key: "last_name",
sorter: (a, b) => a.last_name.localeCompare(b.last_name),
},
{
title: "Имя",
dataIndex: "first_name",
key: "first_name",
sorter: (a, b) => a.first_name.localeCompare(b.first_name),
},
{
title: "Отчество",
@ -54,7 +52,6 @@ const PatientsPage = () => {
title: "Дата рождения",
dataIndex: "birthday",
render: patientsData.formatDate,
sorter: (a, b) => new Date(b.birthday) - new Date(a.birthday),
},
{
title: "Телефон",