Compare commits

..

No commits in common. "a5fccd0710ea56d779b8a738bbde440d20990e2b" and "b52c0d16fa41ff75083d36fc336902aeb052d733" have entirely different histories.

47 changed files with 218 additions and 843 deletions

View File

@ -2,7 +2,7 @@ from typing import Sequence, Optional
from sqlalchemy import select, desc, func from sqlalchemy import select, desc, func
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, timedelta from datetime import date
from app.domain.models import Appointment from app.domain.models import Appointment
@ -43,35 +43,6 @@ class AppointmentsRepository:
result = await self.db.execute(stmt) result = await self.db.execute(stmt)
return result.scalars().all() return result.scalars().all()
async def get_reminders(self, current_date: date) -> Sequence[Appointment]:
stmt = (
select(Appointment)
.options(joinedload(Appointment.type))
.options(joinedload(Appointment.patient))
.options(joinedload(Appointment.doctor))
.filter(Appointment.days_until_the_next_appointment.is_not(None))
)
result = await self.db.execute(stmt)
appointments = result.scalars().all()
filtered_appointments = []
for appointment in appointments:
next_appointment_date = (appointment.appointment_datetime + timedelta(
days=appointment.days_until_the_next_appointment)).date()
days_until = appointment.days_until_the_next_appointment
window_days = (
7 if days_until <= 90 else
14 if days_until <= 180 else
30 if days_until <= 365 else
60
)
window_start = next_appointment_date - timedelta(days=window_days)
window_end = next_appointment_date + timedelta(days=window_days)
if window_start <= current_date <= window_end:
filtered_appointments.append(appointment)
return filtered_appointments
async def get_upcoming_by_doctor_id(self, doctor_id: int) -> Sequence[Appointment]: async def get_upcoming_by_doctor_id(self, doctor_id: int) -> Sequence[Appointment]:
stmt = ( stmt = (
select(Appointment) select(Appointment)

View File

@ -1,55 +0,0 @@
import datetime
from typing import Optional, Sequence
from sqlalchemy import update, delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from app.domain.models.sessions import Session
class SessionsRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def create(self, session: Session) -> Session:
self.db.add(session)
await self.db.commit()
await self.db.refresh(session)
return session
async def get_by_id(self, session_id: int) -> Optional[Session]:
result = await self.db.execute(
select(Session).filter_by(id=session_id)
)
return result.scalars().first()
async def get_by_token(self, token: str) -> Optional[Session]:
result = await self.db.execute(
select(Session).filter_by(token=token, is_active=True)
)
return result.scalars().first()
async def get_by_user_id(self, user_id: int) -> Sequence[Session]:
result = await self.db.execute(
select(Session).filter_by(user_id=user_id, is_active=True)
)
return result.scalars().all()
async def deactivate_session(self, session_id: int) -> None:
await self.db.execute(
update(Session).filter_by(id=session_id).values(is_active=False)
)
await self.db.commit()
async def deactivate_all_sessions(self, user_id: int) -> None:
await self.db.execute(
update(Session).filter_by(user_id=user_id).values(is_active=False)
)
await self.db.commit()
async def cleanup_expired_sessions(self) -> None:
await self.db.execute(
delete(Session).filter(Session.expires_at < datetime.datetime.now())
)
await self.db.commit()

View File

@ -58,22 +58,6 @@ async def get_upcoming_appointments_by_doctor_id(
return await appointments_service.get_upcoming_appointments_by_doctor_id(doctor_id) return await appointments_service.get_upcoming_appointments_by_doctor_id(doctor_id)
@router.get(
"/reminders/",
response_model=list[AppointmentEntity],
summary="Get appointment reminders",
description="Returns a list of appointments with upcoming follow-up reminders based on days_until_the_next_appointment",
)
async def get_appointment_reminders(
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
current_date: date = Query(default_factory=date.today,
description="Current date for reminder calculation (YYYY-MM-DD)"),
):
appointments_service = AppointmentsService(db)
return await appointments_service.get_appointment_reminders(current_date)
@router.get( @router.get(
"/patient/{patient_id}/", "/patient/{patient_id}/",
response_model=list[AppointmentEntity], response_model=list[AppointmentEntity],

View File

@ -1,14 +1,10 @@
from fastapi import APIRouter, Depends, Response, Request from fastapi import APIRouter, Depends, Response
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from typing import List
from app.database.session import get_db from app.database.session import get_db
from app.domain.entities.auth import AuthEntity from app.domain.entities.auth import AuthEntity
from app.domain.entities.responses.session import SessionEntity
from app.domain.entities.token_entity import TokenEntity from app.domain.entities.token_entity import TokenEntity
from app.infrastructure.auth_service import AuthService from app.infrastructure.auth_service import AuthService
from app.infrastructure.dependencies import get_current_user
from app.domain.models.users import User
router = APIRouter() router = APIRouter()
@ -23,11 +19,10 @@ router = APIRouter()
async def auth_user( async def auth_user(
response: Response, response: Response,
user_data: AuthEntity, user_data: AuthEntity,
request: Request,
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
auth_service = AuthService(db) auth_service = AuthService(db)
token = await auth_service.authenticate_user(user_data.login, user_data.password, request) token = await auth_service.authenticate_user(user_data.login, user_data.password)
response.set_cookie( response.set_cookie(
key="users_access_token", key="users_access_token",
@ -37,46 +32,3 @@ async def auth_user(
) )
return token return token
@router.get(
"/sessions/",
response_model=List[SessionEntity],
summary="Get user sessions",
description="Returns a list of active sessions for the current user",
)
async def get_sessions(
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
auth_service = AuthService(db)
return await auth_service.get_user_sessions(user.id)
@router.post(
"/sessions/{session_id}/logout/",
summary="Log out from a specific session",
description="Deactivates a specific session by ID",
)
async def logout_session(
session_id: int,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
auth_service = AuthService(db)
await auth_service.deactivate_session(session_id, user.id)
return {"message": "Session deactivated"}
@router.post(
"/sessions/logout_all/",
summary="Log out from all sessions",
description="Deactivates all sessions for the current user",
)
async def logout_all_sessions(
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
auth_service = AuthService(db)
await auth_service.deactivate_all_sessions(user.id)
return {"message": "All sessions deactivated"}

View File

@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, File, UploadFile
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse from starlette.responses import FileResponse
from app.database.session import get_db, engine from app.database.session import get_db
from app.domain.entities.responses.backup import BackupResponseEntity from app.domain.entities.responses.backup import BackupResponseEntity
from app.infrastructure.backup_service import BackupService from app.infrastructure.backup_service import BackupService
from app.infrastructure.dependencies import require_admin from app.infrastructure.dependencies import require_admin
@ -26,8 +26,8 @@ async def get_all_backups(
@router.get( @router.get(
'/{backup_id}/file/', '/{backup_id}/',
response_class=FileResponse, response_model=BackupResponseEntity,
summary='Get a backup file', summary='Get a backup file',
description='Get a backup file', description='Get a backup file',
) )

View File

@ -1,123 +0,0 @@
"""0008 добавил тпблицу сессий
Revision ID: b013393cef10
Revises: 4f3877d7a2b1
Create Date: 2025-07-03 09:05:56.233095
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'b013393cef10'
down_revision: Union[str, None] = '4f3877d7a2b1'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('sessions',
sa.Column('token', sa.String(), nullable=False),
sa.Column('device_info', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('expires_at', sa.DateTime(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['public.users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('token'),
schema='public'
)
op.drop_constraint('appointment_files_appointment_id_fkey', 'appointment_files', type_='foreignkey')
op.create_foreign_key(None, 'appointment_files', 'appointments', ['appointment_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint('appointments_patient_id_fkey', 'appointments', type_='foreignkey')
op.drop_constraint('appointments_doctor_id_fkey', 'appointments', type_='foreignkey')
op.drop_constraint('appointments_type_id_fkey', 'appointments', type_='foreignkey')
op.create_foreign_key(None, 'appointments', 'patients', ['patient_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'appointments', 'users', ['doctor_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'appointments', 'appointment_types', ['type_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint('backups_user_id_fkey', 'backups', type_='foreignkey')
op.create_foreign_key(None, 'backups', 'users', ['user_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint('lens_type_id_fkey', 'lens', type_='foreignkey')
op.create_foreign_key(None, 'lens', 'lens_types', ['type_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint('lens_issues_doctor_id_fkey', 'lens_issues', type_='foreignkey')
op.drop_constraint('lens_issues_patient_id_fkey', 'lens_issues', type_='foreignkey')
op.drop_constraint('lens_issues_lens_id_fkey', 'lens_issues', type_='foreignkey')
op.create_foreign_key(None, 'lens_issues', 'patients', ['patient_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'lens_issues', 'lens', ['lens_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'lens_issues', 'users', ['doctor_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint('mailing_user_id_fkey', 'mailing', type_='foreignkey')
op.create_foreign_key(None, 'mailing', 'users', ['user_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint('mailing_options_option_id_fkey', 'mailing_options', type_='foreignkey')
op.drop_constraint('mailing_options_mailing_id_fkey', 'mailing_options', type_='foreignkey')
op.create_foreign_key(None, 'mailing_options', 'mailing', ['mailing_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'mailing_options', 'mailing_delivery_methods', ['option_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint('recipients_mailing_id_fkey', 'recipients', type_='foreignkey')
op.drop_constraint('recipients_patient_id_fkey', 'recipients', type_='foreignkey')
op.create_foreign_key(None, 'recipients', 'patients', ['patient_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'recipients', 'mailing', ['mailing_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint('scheduled_appointments_type_id_fkey', 'scheduled_appointments', type_='foreignkey')
op.drop_constraint('scheduled_appointments_doctor_id_fkey', 'scheduled_appointments', type_='foreignkey')
op.drop_constraint('scheduled_appointments_patient_id_fkey', 'scheduled_appointments', type_='foreignkey')
op.create_foreign_key(None, 'scheduled_appointments', 'appointment_types', ['type_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'scheduled_appointments', 'patients', ['patient_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'scheduled_appointments', 'users', ['doctor_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint('set_contents_type_id_fkey', 'set_contents', type_='foreignkey')
op.drop_constraint('set_contents_set_id_fkey', 'set_contents', type_='foreignkey')
op.create_foreign_key(None, 'set_contents', 'sets', ['set_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'set_contents', 'lens_types', ['type_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint('users_role_id_fkey', 'users', type_='foreignkey')
op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id'], source_schema='public', referent_schema='public')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'users', schema='public', type_='foreignkey')
op.create_foreign_key('users_role_id_fkey', 'users', 'roles', ['role_id'], ['id'])
op.drop_constraint(None, 'set_contents', schema='public', type_='foreignkey')
op.drop_constraint(None, 'set_contents', schema='public', type_='foreignkey')
op.create_foreign_key('set_contents_set_id_fkey', 'set_contents', 'sets', ['set_id'], ['id'])
op.create_foreign_key('set_contents_type_id_fkey', 'set_contents', 'lens_types', ['type_id'], ['id'])
op.drop_constraint(None, 'scheduled_appointments', schema='public', type_='foreignkey')
op.drop_constraint(None, 'scheduled_appointments', schema='public', type_='foreignkey')
op.drop_constraint(None, 'scheduled_appointments', schema='public', type_='foreignkey')
op.create_foreign_key('scheduled_appointments_patient_id_fkey', 'scheduled_appointments', 'patients', ['patient_id'], ['id'])
op.create_foreign_key('scheduled_appointments_doctor_id_fkey', 'scheduled_appointments', 'users', ['doctor_id'], ['id'])
op.create_foreign_key('scheduled_appointments_type_id_fkey', 'scheduled_appointments', 'appointment_types', ['type_id'], ['id'])
op.drop_constraint(None, 'recipients', schema='public', type_='foreignkey')
op.drop_constraint(None, 'recipients', schema='public', type_='foreignkey')
op.create_foreign_key('recipients_patient_id_fkey', 'recipients', 'patients', ['patient_id'], ['id'])
op.create_foreign_key('recipients_mailing_id_fkey', 'recipients', 'mailing', ['mailing_id'], ['id'])
op.drop_constraint(None, 'mailing_options', schema='public', type_='foreignkey')
op.drop_constraint(None, 'mailing_options', schema='public', type_='foreignkey')
op.create_foreign_key('mailing_options_mailing_id_fkey', 'mailing_options', 'mailing', ['mailing_id'], ['id'])
op.create_foreign_key('mailing_options_option_id_fkey', 'mailing_options', 'mailing_delivery_methods', ['option_id'], ['id'])
op.drop_constraint(None, 'mailing', schema='public', type_='foreignkey')
op.create_foreign_key('mailing_user_id_fkey', 'mailing', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'lens_issues', schema='public', type_='foreignkey')
op.drop_constraint(None, 'lens_issues', schema='public', type_='foreignkey')
op.drop_constraint(None, 'lens_issues', schema='public', type_='foreignkey')
op.create_foreign_key('lens_issues_lens_id_fkey', 'lens_issues', 'lens', ['lens_id'], ['id'])
op.create_foreign_key('lens_issues_patient_id_fkey', 'lens_issues', 'patients', ['patient_id'], ['id'])
op.create_foreign_key('lens_issues_doctor_id_fkey', 'lens_issues', 'users', ['doctor_id'], ['id'])
op.drop_constraint(None, 'lens', schema='public', type_='foreignkey')
op.create_foreign_key('lens_type_id_fkey', 'lens', 'lens_types', ['type_id'], ['id'])
op.drop_constraint(None, 'backups', schema='public', type_='foreignkey')
op.create_foreign_key('backups_user_id_fkey', 'backups', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'appointments', schema='public', type_='foreignkey')
op.drop_constraint(None, 'appointments', schema='public', type_='foreignkey')
op.drop_constraint(None, 'appointments', schema='public', type_='foreignkey')
op.create_foreign_key('appointments_type_id_fkey', 'appointments', 'appointment_types', ['type_id'], ['id'])
op.create_foreign_key('appointments_doctor_id_fkey', 'appointments', 'users', ['doctor_id'], ['id'])
op.create_foreign_key('appointments_patient_id_fkey', 'appointments', 'patients', ['patient_id'], ['id'])
op.drop_constraint(None, 'appointment_files', schema='public', type_='foreignkey')
op.create_foreign_key('appointment_files_appointment_id_fkey', 'appointment_files', 'appointments', ['appointment_id'], ['id'])
op.drop_table('sessions', schema='public')
# ### end Alembic commands ###

View File

@ -1,16 +0,0 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
class SessionEntity(BaseModel):
id: int
user_id: int
token: str
device_info: Optional[str]
created_at: datetime
expires_at: datetime
is_active: bool
class Config:
from_attributes = True

View File

@ -16,7 +16,6 @@ from app.domain.models.patients import Patient
from app.domain.models.recipients import Recipient from app.domain.models.recipients import Recipient
from app.domain.models.roles import Role from app.domain.models.roles import Role
from app.domain.models.scheduled_appointments import ScheduledAppointment from app.domain.models.scheduled_appointments import ScheduledAppointment
from app.domain.models.sessions import Session
from app.domain.models.set_contents import SetContent from app.domain.models.set_contents import SetContent
from app.domain.models.sets import Set from app.domain.models.sets import Set
from app.domain.models.users import User from app.domain.models.users import User

View File

@ -2,16 +2,14 @@ from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class AppointmentFile(BaseModel): class AppointmentFile(BaseModel):
__tablename__ = 'appointment_files' __tablename__ = 'appointment_files'
__table_args__ = {"schema": settings.SCHEMA}
file_path = Column(String, nullable=False) file_path = Column(String, nullable=False)
file_title = Column(String, nullable=False) file_title = Column(String, nullable=False)
appointment_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointments.id'), nullable=False) appointment_id = Column(Integer, ForeignKey('appointments.id'), nullable=False)
appointment = relationship('Appointment', back_populates='files') appointment = relationship('Appointment', back_populates='files')

View File

@ -2,12 +2,10 @@ from sqlalchemy import Column, VARCHAR
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class AppointmentType(BaseModel): class AppointmentType(BaseModel):
__tablename__ = 'appointment_types' __tablename__ = 'appointment_types'
__table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(150), nullable=False, unique=True) title = Column(VARCHAR(150), nullable=False, unique=True)

View File

@ -3,20 +3,18 @@ from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class Appointment(BaseModel): class Appointment(BaseModel):
__tablename__ = 'appointments' __tablename__ = 'appointments'
__table_args__ = {"schema": settings.SCHEMA}
results = Column(String) results = Column(String)
days_until_the_next_appointment = Column(Integer) days_until_the_next_appointment = Column(Integer)
appointment_datetime = Column(DateTime, nullable=False, server_default=func.now()) appointment_datetime = Column(DateTime, nullable=False, server_default=func.now())
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False) patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False) doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)
type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointment_types.id'), nullable=False) type_id = Column(Integer, ForeignKey('appointment_types.id'), nullable=False)
patient = relationship('Patient', back_populates='appointments') patient = relationship('Patient', back_populates='appointments')
doctor = relationship('User', back_populates='appointments') doctor = relationship('User', back_populates='appointments')

View File

@ -3,16 +3,14 @@ import datetime
from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, Boolean from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, Boolean
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class Backup(BaseModel): class Backup(BaseModel):
__tablename__ = 'backups' __tablename__ = 'backups'
__table_args__ = {"schema": settings.SCHEMA}
timestamp = Column(DateTime, nullable=False, default=datetime.datetime.now) timestamp = Column(DateTime, nullable=False, default=datetime.datetime.now)
path = Column(String, nullable=False) path = Column(String, nullable=False)
filename = Column(String, nullable=False) filename = Column(String, nullable=False)
is_by_user = Column(Boolean, nullable=False, default=False) is_by_user = Column(Boolean, nullable=False, default=False)
user_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False) user_id = Column(Integer, ForeignKey('users.id'), nullable=False)

View File

@ -4,7 +4,6 @@ from sqlalchemy import Column, Integer, ForeignKey, Float, Enum, Boolean
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class SideEnum(PyEnum): class SideEnum(PyEnum):
@ -14,7 +13,6 @@ class SideEnum(PyEnum):
class Lens(BaseModel): class Lens(BaseModel):
__tablename__ = 'lens' __tablename__ = 'lens'
__table_args__ = {"schema": settings.SCHEMA}
tor = Column(Float, nullable=False) tor = Column(Float, nullable=False)
trial = Column(Float, nullable=False) trial = Column(Float, nullable=False)
@ -26,7 +24,7 @@ class Lens(BaseModel):
side = Column(Enum(SideEnum), nullable=False) side = Column(Enum(SideEnum), nullable=False)
issued = Column(Boolean, nullable=False, default=False) issued = Column(Boolean, nullable=False, default=False)
type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens_types.id'), nullable=False) type_id = Column(Integer, ForeignKey('lens_types.id'), nullable=False)
type = relationship('LensType', back_populates='lenses') type = relationship('LensType', back_populates='lenses')

View File

@ -2,18 +2,16 @@ from sqlalchemy import Column, Integer, ForeignKey, Date
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class LensIssue(BaseModel): class LensIssue(BaseModel):
__tablename__ = 'lens_issues' __tablename__ = 'lens_issues'
__table_args__ = {"schema": settings.SCHEMA}
issue_date = Column(Date, nullable=False) issue_date = Column(Date, nullable=False)
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False) patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False) doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)
lens_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens.id'), nullable=False) lens_id = Column(Integer, ForeignKey('lens.id'), nullable=False)
patient = relationship('Patient', back_populates='lens_issues') patient = relationship('Patient', back_populates='lens_issues')
doctor = relationship('User', back_populates='lens_issues') doctor = relationship('User', back_populates='lens_issues')

View File

@ -2,12 +2,10 @@ from sqlalchemy import Column, Integer, VARCHAR
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class LensType(BaseModel): class LensType(BaseModel):
__tablename__ = 'lens_types' __tablename__ = 'lens_types'
__table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(150), nullable=False, unique=True) title = Column(VARCHAR(150), nullable=False, unique=True)

View File

@ -3,18 +3,16 @@ from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class Mailing(BaseModel): class Mailing(BaseModel):
__tablename__ = 'mailing' __tablename__ = 'mailing'
__table_args__ = {"schema": settings.SCHEMA}
text = Column(String, nullable=False) text = Column(String, nullable=False)
title = Column(String, nullable=False) title = Column(String, nullable=False)
datetime = Column(DateTime, nullable=False, default=func.utcnow) datetime = Column(DateTime, nullable=False, default=func.utcnow)
user_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False) user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
user = relationship('User', back_populates='mailing') user = relationship('User', back_populates='mailing')

View File

@ -2,12 +2,10 @@ from sqlalchemy import Column, Integer, VARCHAR
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class MailingDeliveryMethod(BaseModel): class MailingDeliveryMethod(BaseModel):
__tablename__ = 'mailing_delivery_methods' __tablename__ = 'mailing_delivery_methods'
__table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(200), nullable=False) title = Column(VARCHAR(200), nullable=False)

View File

@ -2,15 +2,13 @@ from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class MailingOption(BaseModel): class MailingOption(BaseModel):
__tablename__ = 'mailing_options' __tablename__ = 'mailing_options'
__table_args__ = {"schema": settings.SCHEMA}
option_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing_delivery_methods.id'), nullable=False) option_id = Column(Integer, ForeignKey('mailing_delivery_methods.id'), nullable=False)
mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id'), nullable=False) mailing_id = Column(Integer, ForeignKey('mailing.id'), nullable=False)
method = relationship('MailingDeliveryMethod', back_populates='mailing') method = relationship('MailingDeliveryMethod', back_populates='mailing')
mailing = relationship('Mailing', back_populates='mailing_options') mailing = relationship('Mailing', back_populates='mailing_options')

View File

@ -2,12 +2,10 @@ from sqlalchemy import Column, VARCHAR, Date, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class Patient(BaseModel): class Patient(BaseModel):
__tablename__ = 'patients' __tablename__ = 'patients'
__table_args__ = {"schema": settings.SCHEMA}
first_name = Column(VARCHAR(200), nullable=False) first_name = Column(VARCHAR(200), nullable=False)
last_name = Column(VARCHAR(200), nullable=False) last_name = Column(VARCHAR(200), nullable=False)

View File

@ -2,15 +2,13 @@ from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class Recipient(BaseModel): class Recipient(BaseModel):
__tablename__ = 'recipients' __tablename__ = 'recipients'
__table_args__ = {"schema": settings.SCHEMA}
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False) patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id'), nullable=False) mailing_id = Column(Integer, ForeignKey('mailing.id'), nullable=False)
patient = relationship('Patient', back_populates='mailing') patient = relationship('Patient', back_populates='mailing')
mailing = relationship('Mailing', back_populates='recipients') mailing = relationship('Mailing', back_populates='recipients')

View File

@ -2,12 +2,10 @@ from sqlalchemy import Column, VARCHAR
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class Role(BaseModel): class Role(BaseModel):
__tablename__ = 'roles' __tablename__ = 'roles'
__table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(150), nullable=False, unique=True) title = Column(VARCHAR(150), nullable=False, unique=True)

View File

@ -3,19 +3,17 @@ from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class ScheduledAppointment(BaseModel): class ScheduledAppointment(BaseModel):
__tablename__ = 'scheduled_appointments' __tablename__ = 'scheduled_appointments'
__table_args__ = {"schema": settings.SCHEMA}
scheduled_datetime = Column(DateTime, nullable=False, server_default=func.now()) scheduled_datetime = Column(DateTime, nullable=False, server_default=func.now())
is_canceled = Column(Boolean, nullable=False, default=False, server_default='false') is_canceled = Column(Boolean, nullable=False, default=False, server_default='false')
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False) patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False) doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)
type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointment_types.id'), nullable=False) type_id = Column(Integer, ForeignKey('appointment_types.id'), nullable=False)
patient = relationship('Patient', back_populates='scheduled_appointments') patient = relationship('Patient', back_populates='scheduled_appointments')
doctor = relationship('User', back_populates='scheduled_appointments') doctor = relationship('User', back_populates='scheduled_appointments')

View File

@ -1,20 +0,0 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from datetime import datetime
from app.domain.models.base import BaseModel
from app.settings import settings
class Session(BaseModel):
__tablename__ = 'sessions'
__table_args__ = {"schema": settings.SCHEMA}
token = Column(String, nullable=False, unique=True)
device_info = Column(String, nullable=True)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
expires_at = Column(DateTime, nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
user_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
user = relationship("User", back_populates="sessions")

View File

@ -3,12 +3,10 @@ from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.domain.models.lens import SideEnum from app.domain.models.lens import SideEnum
from app.settings import settings
class SetContent(BaseModel): class SetContent(BaseModel):
__tablename__ = 'set_contents' __tablename__ = 'set_contents'
__table_args__ = {"schema": settings.SCHEMA}
tor = Column(Float, nullable=False) tor = Column(Float, nullable=False)
trial = Column(Float, nullable=False) trial = Column(Float, nullable=False)
@ -20,8 +18,8 @@ class SetContent(BaseModel):
side = Column(Enum(SideEnum), nullable=False) side = Column(Enum(SideEnum), nullable=False)
count = Column(Integer, nullable=False) count = Column(Integer, nullable=False)
type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens_types.id'), nullable=False) type_id = Column(Integer, ForeignKey('lens_types.id'), nullable=False)
set_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.sets.id'), nullable=False) set_id = Column(Integer, ForeignKey('sets.id'), nullable=False)
type = relationship('LensType', back_populates='contents') type = relationship('LensType', back_populates='contents')
set = relationship('Set', back_populates='contents') set = relationship('Set', back_populates='contents')

View File

@ -2,12 +2,10 @@ from sqlalchemy import Column, Integer, VARCHAR
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class Set(BaseModel): class Set(BaseModel):
__tablename__ = 'sets' __tablename__ = 'sets'
__table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(150), nullable=False, unique=True) title = Column(VARCHAR(150), nullable=False, unique=True)

View File

@ -3,12 +3,10 @@ from sqlalchemy.orm import relationship
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
from app.domain.models.base import BaseModel from app.domain.models.base import BaseModel
from app.settings import settings
class User(BaseModel): class User(BaseModel):
__tablename__ = 'users' __tablename__ = 'users'
__table_args__ = {"schema": settings.SCHEMA}
first_name = Column(VARCHAR(200), nullable=False) first_name = Column(VARCHAR(200), nullable=False)
last_name = Column(VARCHAR(200), nullable=False) last_name = Column(VARCHAR(200), nullable=False)
@ -17,7 +15,7 @@ class User(BaseModel):
password = Column(String, nullable=False) password = Column(String, nullable=False)
is_blocked = Column(Boolean, nullable=False, default=False, server_default='false') is_blocked = Column(Boolean, nullable=False, default=False, server_default='false')
role_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.roles.id'), nullable=False) role_id = Column(Integer, ForeignKey('roles.id'), nullable=False)
role = relationship('Role', back_populates='users') role = relationship('Role', back_populates='users')
@ -25,7 +23,6 @@ class User(BaseModel):
appointments = relationship('Appointment', back_populates='doctor') appointments = relationship('Appointment', back_populates='doctor')
mailing = relationship('Mailing', back_populates='user') mailing = relationship('Mailing', back_populates='user')
scheduled_appointments = relationship('ScheduledAppointment', back_populates='doctor') scheduled_appointments = relationship('ScheduledAppointment', back_populates='doctor')
sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan")
def check_password(self, password): def check_password(self, password):
return check_password_hash(self.password, password) return check_password_hash(self.password, password)

View File

@ -49,10 +49,6 @@ class AppointmentsService:
appointments = await self.appointments_repository.get_upcoming_by_doctor_id(doctor_id) appointments = await self.appointments_repository.get_upcoming_by_doctor_id(doctor_id)
return [self.model_to_entity(appointment) for appointment in appointments] return [self.model_to_entity(appointment) for appointment in appointments]
async def get_appointment_reminders(self, current_date: date) -> list[AppointmentEntity]:
appointments = await self.appointments_repository.get_reminders(current_date)
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, async def get_appointments_by_patient_id(self, patient_id: int, start_date: date | None = None,
end_date: date | None = None) -> Optional[list[AppointmentEntity]]: end_date: date | None = None) -> Optional[list[AppointmentEntity]]:
patient = await self.patients_repository.get_by_id(patient_id) patient = await self.patients_repository.get_by_id(patient_id)

View File

@ -1,37 +1,23 @@
import datetime import datetime
from typing import Optional, List from typing import Optional
import jwt import jwt
from fastapi import HTTPException, Request 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 app.application.users_repository import UsersRepository from app.application.users_repository import UsersRepository
from app.application.sessions_repository import SessionsRepository
from app.domain.entities.responses.session import SessionEntity
from app.domain.models.sessions import Session
from app.settings import get_auth_data from app.settings import get_auth_data
class AuthService: class AuthService:
def __init__(self, db: AsyncSession): def __init__(self, db: AsyncSession):
self.users_repository = UsersRepository(db) self.users_repository = UsersRepository(db)
self.sessions_repository = SessionsRepository(db)
async def authenticate_user(self, login: str, password: str, request: Request) -> Optional[dict]: async def authenticate_user(self, login: str, password: str) -> Optional[dict]:
user = await self.users_repository.get_by_login(login) user = await self.users_repository.get_by_login(login)
if user and user.check_password(password): if user and user.check_password(password):
access_token = self.create_access_token({"user_id": user.id}) access_token = self.create_access_token({"user_id": user.id})
session = Session(
user_id=user.id,
token=access_token,
device_info=request.headers.get("User-Agent", "Unknown"),
created_at=datetime.datetime.now(), # Naive datetime
expires_at=datetime.datetime.now() + datetime.timedelta(days=30),
is_active=True
)
await self.sessions_repository.create(session)
return { return {
"access_token": access_token, "access_token": access_token,
"user_id": user.id "user_id": user.id
@ -39,27 +25,12 @@ class AuthService:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Неправильный логин или пароль") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Неправильный логин или пароль")
async def get_user_sessions(self, user_id: int) -> List[SessionEntity]:
sessions = await self.sessions_repository.get_by_user_id(user_id)
return [SessionEntity.from_orm(session) for session in sessions]
async def deactivate_session(self, session_id: int, user_id: int) -> None:
session = await self.sessions_repository.get_by_id(session_id)
if not session or session.user_id != user_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Сессия не найдена или доступ запрещен")
await self.sessions_repository.deactivate_session(session_id)
async def deactivate_all_sessions(self, user_id: int) -> None:
await self.sessions_repository.deactivate_all_sessions(user_id)
async def cleanup_expired_sessions(self) -> None:
await self.sessions_repository.cleanup_expired_sessions()
@staticmethod @staticmethod
def create_access_token(data: dict) -> str: def create_access_token(data: dict) -> str:
to_encode = data.copy() to_encode = data.copy()
expire = datetime.datetime.now() + datetime.timedelta(days=30) # Naive datetime expire = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=30)
to_encode.update({"exp": expire}) to_encode.update({"exp": expire})
auth_data = get_auth_data() auth_data = get_auth_data()
encode_jwt = jwt.encode(to_encode, auth_data['secret_key'], algorithm=auth_data['algorithm']) encode_jwt = jwt.encode(to_encode, auth_data['secret_key'], algorithm=auth_data['algorithm'])
return encode_jwt return encode_jwt

View File

@ -1,16 +1,14 @@
import datetime
import io import io
import os
import shutil
import subprocess import subprocess
import os
import tarfile import tarfile
from typing import Optional import datetime
from fastapi_maintenance import maintenance_mode_on from typing import Any, Coroutine, Optional
import aiofiles import aiofiles
from fastapi import HTTPException, UploadFile from fastapi import HTTPException, UploadFile
from magic import magic from magic import magic
from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse from starlette.responses import FileResponse
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename

View File

@ -8,7 +8,6 @@ from app.database.session import get_db
from app.domain.models.users import User from app.domain.models.users import User
from app.settings import get_auth_data from app.settings import get_auth_data
from app.application.users_repository import UsersRepository from app.application.users_repository import UsersRepository
from app.application.sessions_repository import SessionsRepository
security = HTTPBearer() security = HTTPBearer()
@ -30,10 +29,6 @@ async def get_current_user(
if user_id is None: if user_id is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Неправильный токен") raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Неправильный токен")
session = await SessionsRepository(db).get_by_token(credentials.credentials)
if not session or not session.is_active:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Сессия неактивна или не найдена")
user = await UsersRepository(db).get_by_id_with_role(user_id) user = await UsersRepository(db).get_by_id_with_role(user_id)
if user is None: if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Пользователь не найден") raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Пользователь не найден")

View File

@ -1,5 +1,4 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi_maintenance import MaintenanceModeMiddleware
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from app.controllers.appointment_files_router import router as appointment_files_router from app.controllers.appointment_files_router import router as appointment_files_router
@ -30,7 +29,6 @@ def start_app():
allow_methods=['*'], allow_methods=['*'],
allow_headers=['*'], allow_headers=['*'],
) )
# api_app.add_middleware(MaintenanceModeMiddleware, enable_maintenance=False)
api_app.include_router(appointment_files_router, prefix=f'{settings.APP_PREFIX}/appointment_files', api_app.include_router(appointment_files_router, prefix=f'{settings.APP_PREFIX}/appointment_files',
tags=['appointment_files']) tags=['appointment_files'])

View File

@ -12,7 +12,6 @@ class Settings(BaseSettings):
BACKUP_DIR: str = 'backups' BACKUP_DIR: str = 'backups'
BACKUP_DB_URL: str BACKUP_DB_URL: str
PG_DUMP_PATH: str PG_DUMP_PATH: str
SCHEMA: str = 'public'
class Config: class Config:
env_file = '.env' env_file = '.env'

View File

@ -12,4 +12,3 @@ pyjwt==2.10.1
python-magic==0.4.27 python-magic==0.4.27
aiofiles==24.1.0 aiofiles==24.1.0
python-multipart==0.0.20 python-multipart==0.0.20
fastapi-maintenance==0.0.4

View File

@ -31,11 +31,13 @@ export const appointmentsApi = createApi({
}), }),
invalidatesTags: ['Appointment'], invalidatesTags: ['Appointment'],
}), }),
getAppointmentReminders: builder.query({ updateAppointment: builder.mutation({
query: (current_date) => ({ query: ({id, data}) => ({
url: "/appointments/reminders/", url: `/appointments/${id}/`,
params: { current_date }, method: 'PUT',
body: data,
}), }),
invalidatesTags: ['Appointment'],
}), }),
}), }),
}); });
@ -45,5 +47,5 @@ export const {
useGetUpcomingAppointmentsQuery, useGetUpcomingAppointmentsQuery,
useGetByPatientIdQuery, useGetByPatientIdQuery,
useCreateAppointmentMutation, useCreateAppointmentMutation,
useGetAppointmentRemindersQuery useUpdateAppointmentMutation,
} = appointmentsApi; } = appointmentsApi;

View File

@ -1,6 +1,7 @@
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 authApi = createApi({ export const authApi = createApi({
reducerPath: "authApi", reducerPath: "authApi",
baseQuery: baseQueryWithAuth, baseQuery: baseQueryWithAuth,
@ -12,25 +13,7 @@ export const authApi = createApi({
body: credentials, body: credentials,
}), }),
}), }),
getSessions: builder.query({
query: () => ({
url: "/sessions/",
method: "GET",
}),
}),
logoutSession: builder.mutation({
query: (sessionId) => ({
url: `/sessions/${sessionId}/logout/`,
method: "POST",
}),
}),
logoutAllSessions: builder.mutation({
query: () => ({
url: "/sessions/logout_all/",
method: "POST",
}),
}),
}), }),
}); });
export const {useLoginMutation, useGetSessionsQuery, useLogoutSessionMutation, useLogoutAllSessionsMutation} = authApi; export const {useLoginMutation} = authApi;

View File

@ -40,6 +40,7 @@ export const backupsApi = createApi({
}, },
invalidatesTags: ['Backup'], invalidatesTags: ['Backup'],
}), }),
}), }),
}); });

View File

@ -17,7 +17,7 @@ const AdminRoute = () => {
} }
if (isUserError) { if (isUserError) {
return <Navigate to="/login"/>; return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных пользователя"/>;
} }
if (!user) { if (!user) {

View File

@ -3,13 +3,13 @@ import {useSelector} from "react-redux";
import LoadingIndicator from "../Components/Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../Components/Widgets/LoadingIndicator/LoadingIndicator.jsx";
const PrivateRoute = () => { const PrivateRoute = () => {
const {user, userData, isLoading, error} = useSelector((state) => state.auth); const {user, userData, isLoading} = useSelector((state) => state.auth);
if (isLoading) { if (isLoading) {
return <LoadingIndicator/>; return <LoadingIndicator/>;
} }
if (error || !user || !userData || userData.is_blocked) { if (!user || !userData || userData.is_blocked) {
return <Navigate to="/login"/>; return <Navigate to="/login"/>;
} }

View File

@ -3,8 +3,8 @@ import {setSelectedAppointment} from "../../../Redux/Slices/appointmentsSlice.js
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useState } from "react"; import { useState } from "react";
import { useGetAppointmentFilesQuery, useDeleteAppointmentFileMutation } from "../../../Api/appointmentFilesApi.js"; import { useGetAppointmentFilesQuery, useDeleteAppointmentFileMutation } from "../../../Api/appointmentFilesApi.js";
import { baseQueryWithAuth } from "../../../Api/baseQuery.js";
import { notification } from "antd"; import { notification } from "antd";
import CONFIG from "../../../Core/сonfig.js";
const useAppointmentView = () => { const useAppointmentView = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -81,74 +81,44 @@ const useAppointmentView = () => {
const downloadFile = async (fileId, fileName) => { const downloadFile = async (fileId, fileName) => {
try { try {
setDownloadingFiles((prev) => ({ ...prev, [fileId]: true })); setDownloadingFiles((prev) => ({ ...prev, [fileId]: true }));
const { url, ...options } = await baseQueryWithAuth(
const token = localStorage.getItem('access_token'); {
if (!token) { url: `/appointment_files/${fileId}/file/`,
notification.error({
message: "Ошибка",
description: "Токен не найден",
placement: "topRight",
});
return;
}
const response = await fetch(`${CONFIG.BASE_URL}appointment_files/${fileId}/file/`, {
method: 'GET', method: 'GET',
credentials: 'include', credentials: 'include',
headers: {
'Authorization': `Bearer ${token}`,
}, },
}); {},
{}
);
const response = await fetch(url, {
...options,
method: 'GET',
credentials: 'include',
});
if (!response.ok) { if (!response.ok) {
const errorText = await response.text();
notification.error({ notification.error({
message: "Ошибка", message: "Ошибка при скачивании файла",
description: errorText || "Не удалось скачать файл", description: "Не удалось загрузить файл.",
placement: "topRight", placement: "topRight",
}); });
return;
}
const contentType = response.headers.get('content-type');
if (!contentType || contentType.includes('text/html')) {
const errorText = await response.text();
notification.error({
message: "Ошибка",
description: errorText || "Не удалось скачать файл",
placement: "topRight",
});
return;
}
let safeFileName = fileName || "file";
if (!safeFileName.match(/\.[a-zA-Z0-9]+$/)) {
if (contentType.includes('application/pdf')) {
safeFileName += '.pdf';
} else if (contentType.includes('image/jpeg')) {
safeFileName += '.jpg';
} else if (contentType.includes('image/png')) {
safeFileName += '.png';
} else {
safeFileName += '.bin';
}
} }
const blob = await response.blob(); const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob); const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a"); const link = document.createElement("a");
link.href = downloadUrl; link.href = downloadUrl;
link.setAttribute("download", safeFileName); link.setAttribute("download", fileName || "file");
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
link.remove(); link.remove();
window.URL.revokeObjectURL(downloadUrl); window.URL.revokeObjectURL(downloadUrl);
} catch (error) { } catch (error) {
console.error("Error downloading file:", error); // Отладка console.error("Error downloading file:", error);
notification.error({ notification.error({
message: "Ошибка", message: "Ошибка при скачивании файлов",
description: error.message || "Не удалось скачать файл", description: "Не удалось загрузить файл.",
placement: "topRight", placement: "topRight",
}); });
} finally { } finally {

View File

@ -11,7 +11,7 @@ const BackupManageTab = () => {
} }
if (backupManageTabData.isErrorBackups) { if (backupManageTabData.isErrorBackups) {
return <Result status={500} title="Произошла ошибка при загрузке резервных копий"/>; return <Result status={500} title="Произошла ошибка при загрузке резервных копий"/>
} }
return ( return (
@ -22,6 +22,7 @@ const BackupManageTab = () => {
icon={<CloudDownloadOutlined/>} icon={<CloudDownloadOutlined/>}
onClick={backupManageTabData.createBackupHandler} onClick={backupManageTabData.createBackupHandler}
loading={backupManageTabData.isCreatingBackup} loading={backupManageTabData.isCreatingBackup}
> >
Создать резервную копию Создать резервную копию
</Button> </Button>
@ -45,9 +46,7 @@ const BackupManageTab = () => {
renderItem={(backup) => ( renderItem={(backup) => (
<List.Item> <List.Item>
<Typography.Text>{backup.filename}</Typography.Text> <Typography.Text>{backup.filename}</Typography.Text>
<Divider type={"vertical"}/>
<Typography.Text>{backup.is_by_user ? "Загружена" : "Создана"}: {new Date(backup.timestamp).toLocaleString()}</Typography.Text> <Typography.Text>{backup.is_by_user ? "Загружена" : "Создана"}: {new Date(backup.timestamp).toLocaleString()}</Typography.Text>
<Space>
<Tooltip title="Скачать резервную копию"> <Tooltip title="Скачать резервную копию">
<Button <Button
type="primary" type="primary"
@ -69,11 +68,11 @@ const BackupManageTab = () => {
Удалить Удалить
</Button> </Button>
</Tooltip> </Tooltip>
</Space>
</List.Item> </List.Item>
)} )}
/> />
</> </>
); );
}; };

View File

@ -1,15 +1,16 @@
import { import {
useCreateBackupMutation, useCreateBackupMutation,
useDeleteBackupMutation, useDeleteBackupMutation,
useGetBackupsQuery, useGetBackupsQuery, useUploadBackupMutation
useUploadBackupMutation,
} from "../../../../../Api/backupsApi.js"; } from "../../../../../Api/backupsApi.js";
import {useDispatch} from "react-redux";
import {notification} from "antd"; import {notification} from "antd";
import {baseQueryWithAuth} from "../../../../../Api/baseQuery.js";
import {useState} from "react"; import {useState} from "react";
import CONFIG from "../../../../../Core/сonfig.js";
const useBackupManageTab = () => { const useBackupManageTab = () => {
const dispatch = useDispatch();
const {data: backups, isLoading: isLoadingBackups, isError: isErrorBackups} = useGetBackupsQuery(undefined, { const {data: backups, isLoading: isLoadingBackups, isError: isErrorBackups} = useGetBackupsQuery(undefined, {
pollingInterval: 60000, pollingInterval: 60000,
}); });
@ -39,70 +40,47 @@ const useBackupManageTab = () => {
const downloadBackupHandler = async (backupId, fileName) => { const downloadBackupHandler = async (backupId, fileName) => {
try { try {
setDownloadingFiles(true); setDownloadingFiles(true);
const {url, ...options} = await baseQueryWithAuth(
const token = localStorage.getItem('access_token'); {
if (!token) { url: `/backups/${backupId}/`,
notification.error({
message: "Ошибка",
description: "Токен не найден",
placement: "topRight",
});
return;
}
const response = await fetch(`${CONFIG.BASE_URL}backups/${backupId}/file/`, {
method: 'GET', method: 'GET',
credentials: 'include', credentials: 'include',
headers: {
'Authorization': `Bearer ${token}`,
}, },
{},
{}
);
const response = await fetch(url, {
...options,
method: 'GET',
credentials: 'include',
}); });
if (!response.ok) { if (!response.ok) {
const errorText = await response.text();
notification.error({ notification.error({
message: "Ошибка", message: "Ошибка при скачивании файла",
description: errorText || "Не удалось скачать резервную копию", description: "Не удалось загрузить файл.",
placement: "topRight", placement: "topRight",
}); });
return;
}
const contentType = response.headers.get('content-type');
if (!contentType || (!contentType.includes('application/gzip') && !contentType.includes('application/zip'))) {
const errorText = await response.text();
notification.error({
message: "Ошибка",
description: errorText || "Не удалось скачать резервную копию",
placement: "topRight",
});
return;
} }
const blob = await response.blob(); const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob); const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a"); const link = document.createElement("a");
link.href = downloadUrl; link.href = downloadUrl;
link.setAttribute("download", fileName || "backup.tar.gz"); link.setAttribute("download", fileName || "file");
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
link.remove(); link.remove();
window.URL.revokeObjectURL(downloadUrl); window.URL.revokeObjectURL(downloadUrl);
notification.success({ setDownloadingFiles(false);
message: "Успех",
description: "Резервная копия успешно скачана",
placement: "topRight",
});
} catch (e) { } catch (e) {
console.error("Download error:", e); // Отладка console.log(e)
notification.error({ notification.error({
message: "Ошибка", message: "Ошибка",
description: e.message || "Не удалось скачать резервную копию", description: e?.data?.detail || "Не удалось загрузить резервную копию",
placement: "topRight", placement: "topRight",
}); });
} finally {
setDownloadingFiles(false);
} }
}; };
@ -140,7 +118,6 @@ const useBackupManageTab = () => {
placement: "topRight", placement: "topRight",
}); });
} catch (e) { } catch (e) {
console.error("Upload error:", e); // Отладка
notification.error({ notification.error({
message: "Ошибка", message: "Ошибка",
description: e.message || e?.data?.detail || "Не удалось загрузить резервную копию", description: e.message || e?.data?.detail || "Не удалось загрузить резервную копию",
@ -161,7 +138,7 @@ const useBackupManageTab = () => {
downloadBackupHandler, downloadBackupHandler,
deleteBackupHandler, deleteBackupHandler,
uploadBackupHandler, uploadBackupHandler,
}; }
}; };
export default useBackupManageTab; export default useBackupManageTab;

View File

@ -6,18 +6,15 @@ import {
MenuUnfoldOutlined, MenuUnfoldOutlined,
PlusOutlined, PlusOutlined,
ClockCircleOutlined, ClockCircleOutlined,
BellOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import AppointmentsCalendar from "./Components/AppointmentCalendar/AppointmentsCalendar.jsx"; import AppointmentsCalendar from "./Components/AppointmentCalendar/AppointmentsCalendar.jsx";
import useAppointmentsPage from "./useAppointmentsPage.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 AppointmentViewModal from "../../Dummies/AppointmentViewModal/AppointmentViewModal.jsx"; import AppointmentViewModal from "../../Dummies/AppointmentViewModal/AppointmentViewModal.jsx";
import ScheduledAppointmentFormModal import ScheduledAppointmentFormModal from "../../Dummies/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx";
from "../../Dummies/ScheduledAppintmentFormModal/ScheduledAppointmentFormModal.jsx"; import ScheduledAppointmentsViewModal from "../../Widgets/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx";
import ScheduledAppointmentsViewModal
from "../../Widgets/ScheduledAppointmentsViewModal/ScheduledAppointmentsViewModal.jsx";
import AppointmentsListModal from "./Components/AppointmentsListModal/AppointmentsListModal.jsx"; import AppointmentsListModal from "./Components/AppointmentsListModal/AppointmentsListModal.jsx";
const AppointmentsPage = () => { const AppointmentsPage = () => {
@ -47,7 +44,7 @@ const AppointmentsPage = () => {
handleMonthChange, handleMonthChange,
handleEventClick, handleEventClick,
openCreateAppointmentModal, openCreateAppointmentModal,
} = useAppointmentsPage(); } = useAppointments();
if (isError) return ( if (isError) return (
<Result <Result
@ -74,10 +71,6 @@ const AppointmentsPage = () => {
<Badge status={"success"} <Badge status={"success"}
text={<span style={badgeTextStyle}>Прошедший прием</span>} /> text={<span style={badgeTextStyle}>Прошедший прием</span>} />
</Tag> </Tag>
<Tag color={"yellow"} style={{width: "100%"}}>
<Badge status={"warning"}
text={<span style={badgeTextStyle}>Напоминание о приеме</span>}/>
</Tag>
</Space> </Space>
</Row> </Row>
<Splitter <Splitter
@ -96,8 +89,8 @@ const AppointmentsPage = () => {
<AppointmentsCalendar <AppointmentsCalendar
currentMonth={currentMonth} currentMonth={currentMonth}
onMonthChange={handleMonthChange} onMonthChange={handleMonthChange}
appointments={appointments} appointments={appointments} // Добавляем
scheduledAppointments={scheduledAppointments} scheduledAppointments={scheduledAppointments} // Добавляем
/> />
</Splitter.Panel> </Splitter.Panel>
{showSplitterPanel && ( {showSplitterPanel && (
@ -112,7 +105,10 @@ const AppointmentsPage = () => {
</Typography.Title> </Typography.Title>
{upcomingEvents.length ? ( {upcomingEvents.length ? (
<List <List
dataSource={upcomingEvents} dataSource={upcomingEvents.sort((a, b) =>
dayjs(a.appointment_datetime || a.scheduled_datetime).diff(
dayjs(b.appointment_datetime || b.scheduled_datetime)
))}
renderItem={(item) => ( renderItem={(item) => (
<List.Item <List.Item
onClick={() => handleEventClick(item)} onClick={() => handleEventClick(item)}
@ -120,46 +116,32 @@ const AppointmentsPage = () => {
padding: "12px", padding: "12px",
marginBottom: "8px", marginBottom: "8px",
borderRadius: "4px", borderRadius: "4px",
background: item.type === "reminder" ? "#fff7e6" : background: item.appointment_datetime ? "#f6ffed" : "#e6f7ff",
item.appointment_datetime ? "#f6ffed" : "#e6f7ff",
cursor: "pointer", cursor: "pointer",
transition: "background 0.3s", transition: "background 0.3s",
}} }}
onMouseEnter={(e) => (e.currentTarget.style.background = onMouseEnter={(e) => (e.currentTarget.style.background = item.appointment_datetime ? "#efffdb" : "#d9efff")}
item.type === "reminder" ? "#ffecd2" : onMouseLeave={(e) => (e.currentTarget.style.background = item.appointment_datetime ? "#f6ffed" : "#e6f7ff")}
item.appointment_datetime ? "#efffdb" : "#d9efff")}
onMouseLeave={(e) => (e.currentTarget.style.background =
item.type === "reminder" ? "#fff7e6" :
item.appointment_datetime ? "#f6ffed" : "#e6f7ff")}
> >
<Space direction="vertical" size={2}> <Space direction="vertical" size={2}>
<Space> <Space>
{item.type === "reminder" ? ( {item.appointment_datetime ? (
<BellOutlined style={{color: "#fa8c16"}}/>
) : 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.type === "reminder" ? item.reminder_datetime : {dayjs(item.appointment_datetime || item.scheduled_datetime).format('DD.MM.YYYY HH:mm')}
(item.appointment_datetime || item.scheduled_datetime))
.format('DD.MM.YYYY HH:mm')}
</Typography.Text> </Typography.Text>
</Space> </Space>
<Typography.Text> <Typography.Text>
{item.type === "reminder" ? "Напоминание" : {item.appointment_datetime ? 'Прием' : 'Запланировано'}
item.appointment_datetime ? "Прием" : "Запланировано"}
{item.patient ? ` - ${item.patient.last_name} ${item.patient.first_name}` : ''} {item.patient ? ` - ${item.patient.last_name} ${item.patient.first_name}` : ''}
</Typography.Text> </Typography.Text>
{item.type !== "reminder" && (
<Typography.Text type="secondary"> <Typography.Text type="secondary">
Тип: {item.type === "reminder" ? item.type.title : item.type?.title || 'Не указан'} Тип: {item.type?.title || 'Не указан'}
</Typography.Text> </Typography.Text>
)} {dayjs(item.appointment_datetime || item.scheduled_datetime).isSame(dayjs(), 'day') && (
{dayjs(item.type === "reminder" ? item.reminder_datetime :
(item.appointment_datetime || item.scheduled_datetime))
.isSame(dayjs(), 'day') && (
<Typography.Text type="warning">Сегодня</Typography.Text> <Typography.Text type="warning">Сегодня</Typography.Text>
)} )}
</Space> </Space>

View File

@ -3,7 +3,6 @@ import {useDispatch, useSelector} from "react-redux";
import {notification} from "antd"; import {notification} from "antd";
import {Grid} from "antd"; import {Grid} from "antd";
import { import {
useGetAppointmentRemindersQuery,
useGetAppointmentsQuery, useGetAppointmentsQuery,
useGetUpcomingAppointmentsQuery, useGetUpcomingAppointmentsQuery,
} from "../../../Api/appointmentsApi.js"; } from "../../../Api/appointmentsApi.js";
@ -29,7 +28,7 @@ dayjs.extend(timezone);
const {useBreakpoint} = Grid; const {useBreakpoint} = Grid;
const useAppointmentsPage = () => { const useAppointments = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const {userData} = useSelector(state => state.auth); const {userData} = useSelector(state => state.auth);
const {collapsed, siderWidth, hovered, selectedAppointment} = useSelector(state => state.appointmentsUI); const {collapsed, siderWidth, hovered, selectedAppointment} = useSelector(state => state.appointmentsUI);
@ -89,15 +88,6 @@ const useAppointmentsPage = () => {
skip: !userData.id, skip: !userData.id,
}); });
const {
data: reminders = [],
isLoading: isLoadingReminders,
isError: isErrorReminders,
} = useGetAppointmentRemindersQuery(new Date().toISOString().split("T")[0], {
skip: !userData,
pollingInterval: 60000,
});
const [localSiderWidth, setLocalSiderWidth] = useState(siderWidth); const [localSiderWidth, setLocalSiderWidth] = useState(siderWidth);
const splitterStyle = {flex: 1}; const splitterStyle = {flex: 1};
@ -143,9 +133,7 @@ const useAppointmentsPage = () => {
}; };
const handleEventClick = (event) => { const handleEventClick = (event) => {
if (event.type === "reminder") { if (event.appointment_datetime) {
dispatch(setSelectedAppointment(event));
} else if (event.appointment_datetime) {
dispatch(setSelectedAppointment(event)); dispatch(setSelectedAppointment(event));
} else { } else {
dispatch(setSelectedScheduledAppointment(event)); dispatch(setSelectedScheduledAppointment(event));
@ -162,34 +150,14 @@ const useAppointmentsPage = () => {
hovered ? (collapsed ? "Показать предстоящие события" : "Скрыть предстоящие события") : "", hovered ? (collapsed ? "Показать предстоящие события" : "Скрыть предстоящие события") : "",
[collapsed, hovered] [collapsed, hovered]
); );
const showSplitterPanel = useMemo(() => !collapsed && !screens.xs, [collapsed, screens]); const showSplitterPanel = useMemo(() => !collapsed && !screens.xs, [collapsed, screens]);
const upcomingEvents = useMemo(() => { const upcomingEvents = useMemo(() =>
const remindersWithType = reminders.map(reminder => ({ [...upcomingAppointments, ...upcomingScheduledAppointments]
...reminder, .sort((a, b) => dayjs(a.appointment_datetime || a.scheduled_datetime) - dayjs(b.appointment_datetime || b.scheduled_datetime))
type: "reminder", .slice(0, 5),
reminder_datetime: dayjs(reminder.appointment_datetime).add(reminder.days_until_the_next_appointment, 'day').toISOString(), [upcomingAppointments, upcomingScheduledAppointments]
})); );
const appointmentsWithType = upcomingAppointments.map(appointment => ({
...appointment,
type: "appointment",
}));
const scheduledAppointmentsWithType = upcomingScheduledAppointments.map(scheduled => ({
...scheduled,
type: "scheduledAppointment",
}));
return [
...appointmentsWithType,
...scheduledAppointmentsWithType,
...remindersWithType
].sort((a, b) => {
const dateA = a.type === "reminder" ? a.reminder_datetime : (a.appointment_datetime || a.scheduled_datetime);
const dateB = b.type === "reminder" ? b.reminder_datetime : (b.appointment_datetime || b.scheduled_datetime);
return dayjs(dateA).diff(dayjs(dateB));
}).slice(0, 5);
}, [upcomingAppointments, upcomingScheduledAppointments, reminders]);
useEffect(() => { useEffect(() => {
document.title = "Приемы"; document.title = "Приемы";
@ -231,27 +199,18 @@ const useAppointmentsPage = () => {
placement: 'topRight', placement: 'topRight',
}); });
} }
if (isErrorReminders) {
notification.error({
message: 'Ошибка',
description: 'Ошибка загрузки напоминаний.',
placement: 'topRight',
});
}
}, [ }, [
isErrorAppointments, isErrorAppointments,
isErrorScheduledAppointments, isErrorScheduledAppointments,
isErrorPatients, isErrorPatients,
isErrorUpcomingAppointments, isErrorUpcomingAppointments,
isErrorUpcomingScheduledAppointments, isErrorUpcomingScheduledAppointments
isErrorReminders
]); ]);
return { return {
patients, patients,
appointments, appointments,
scheduledAppointments, scheduledAppointments,
reminders,
isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients || isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients ||
isLoadingUpcomingAppointments || isLoadingUpcomingScheduledAppointments, isLoadingUpcomingAppointments || isLoadingUpcomingScheduledAppointments,
isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients || isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients ||
@ -283,4 +242,4 @@ const useAppointmentsPage = () => {
}; };
}; };
export default useAppointmentsPage; export default useAppointments;

View File

@ -74,12 +74,7 @@ const useLoginPage = () => {
const response = await loginUser(loginData).unwrap(); const response = await loginUser(loginData).unwrap();
const token = response.access_token || response.token; const token = response.access_token || response.token;
if (!token) { if (!token) {
notification.error({ throw new Error("Сервер не вернул токен авторизации");
message: "Ошибка при входе",
description: "Не удалось войти. Проверьте логин и пароль.",
placement: "topRight",
});
return;
} }
localStorage.setItem("access_token", token); localStorage.setItem("access_token", token);
dispatch(setUser({ token })); dispatch(setUser({ token }));

View File

@ -1,12 +1,11 @@
import {Button, Card, Col, Row, Typography, Result, List, Space} from "antd"; import {Button, Card, Col, Row, Typography, Result} from "antd";
import {EditOutlined, UserOutlined} from "@ant-design/icons"; import {EditOutlined, UserOutlined} from "@ant-design/icons";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import useProfilePage from "./useProfilePage.js"; import useProfilePage from "./useProfilePage.js";
import UpdateUserModalForm from "../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx"; import UpdateUserModalForm from "../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx";
const {Title, Text} = Typography;
const ProfilePage = () => { const ProfilePage = () => {
const { const {
containerStyle, containerStyle,
cardStyle, cardStyle,
@ -14,12 +13,7 @@ const ProfilePage = () => {
isLoading, isLoading,
isError, isError,
userData, userData,
sessions,
handleEditUser, handleEditUser,
handleLogoutSession,
handleLogoutAllSessions,
isLoggingOutSession,
isLoggingOutAll,
} = useProfilePage(); } = useProfilePage();
if (isError) { if (isError) {
@ -27,7 +21,7 @@ const ProfilePage = () => {
<Result <Result
status="error" status="error"
title="Ошибка" title="Ошибка"
subTitle="Произошла ошибка при загрузке данных профиля или сессий" subTitle="Произошла ошибка при загрузке данных профиля"
/> />
); );
} }
@ -38,31 +32,27 @@ const ProfilePage = () => {
<LoadingIndicator/> <LoadingIndicator/>
) : ( ) : (
<> <>
<Title level={1}> <Typography.Title level={1}>
<UserOutlined/> {userData.last_name} {userData.first_name} <UserOutlined/> {userData.last_name} {userData.first_name}
</Title> </Typography.Title>
<Title level={2}>Профиль</Title> <Typography.Title level={1}>Профиль</Typography.Title>
<Card style={cardStyle} title="Информация о пользователе"> <Card style={cardStyle}>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={24}> <Col span={24}>
<Text strong>Фамилия: </Text> <Typography.Text strong>Фамилия: </Typography.Text>
<Text>{userData.last_name || "-"}</Text> <Typography.Text>{userData.last_name || "-"}</Typography.Text>
</Col> </Col>
<Col span={24}> <Col span={24}>
<Text strong>Имя: </Text> <Typography.Text strong>Имя: </Typography.Text>
<Text>{userData.first_name || "-"}</Text> <Typography.Text>{userData.first_name || "-"}</Typography.Text>
</Col> </Col>
<Col span={24}> <Col span={24}>
<Text strong>Отчество: </Text> <Typography.Text strong>Отчество: </Typography.Text>
<Text>{userData.patronymic || "-"}</Text> <Typography.Text>{userData.patronymic || "-"}</Typography.Text>
</Col> </Col>
<Col span={24}> <Col span={24}>
<Text strong>Логин: </Text> <Typography.Text strong>Логин: </Typography.Text>
<Text>{userData.login || "-"}</Text> <Typography.Text>{userData.login || "-"}</Typography.Text>
</Col>
<Col span={24}>
<Text strong>Роль: </Text>
<Text>{userData.role?.title || "-"}</Text>
</Col> </Col>
</Row> </Row>
<Button <Button
@ -75,44 +65,6 @@ const ProfilePage = () => {
</Button> </Button>
</Card> </Card>
<Card style={cardStyle} title="Активные сессии">
<List
dataSource={sessions}
renderItem={(session) => (
<List.Item
actions={[
<Button
key={session.id}
type="primary"
danger
onClick={() => handleLogoutSession(session.id)}
loading={isLoggingOutSession}
disabled={isLoggingOutSession}
>
Завершить
</Button>,
]}
>
<List.Item.Meta
title={`Устройство: ${session.device_info || "Неизвестно"}`}
description={`Создана: ${new Date(session.created_at).toLocaleString()} | Истекает: ${new Date(session.expires_at).toLocaleString()}`}
/>
</List.Item>
)}
/>
<Space style={{marginTop: 16}}>
<Button
type="primary"
danger
onClick={handleLogoutAllSessions}
loading={isLoggingOutAll}
disabled={isLoggingOutAll}
>
Завершить все сессии
</Button>
</Space>
</Card>
<UpdateUserModalForm/> <UpdateUserModalForm/>
</> </>
)} )}

View File

@ -1,95 +1,40 @@
import {Grid} from "antd"; import {Grid} from "antd";
import {useDispatch, useSelector} from "react-redux"; import {useDispatch} from "react-redux";
import {useNavigate} from "react-router-dom";
import {setSelectedUser} from "../../../Redux/Slices/usersSlice.js"; import {setSelectedUser} from "../../../Redux/Slices/usersSlice.js";
import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js"; import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
import {useGetSessionsQuery, useLogoutSessionMutation, useLogoutAllSessionsMutation} from "../../../Api/authApi.js";
import {logout, setError} from "../../../Redux/Slices/authSlice.js";
const {useBreakpoint} = Grid; const {useBreakpoint} = Grid;
const useProfilePage = () => { const useProfilePage = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate();
const screens = useBreakpoint(); const screens = useBreakpoint();
const {user} = useSelector((state) => state.auth);
const { const {
data: userData = {}, data: userData = {},
isLoading: isLoadingUserData, isLoading: isLoadingUserData,
isError: isErrorUserData, isError: isErrorUserData,
} = useGetAuthenticatedUserDataQuery(undefined, { } = useGetAuthenticatedUserDataQuery(undefined, {
pollingInterval: 10000,
});
const {
data: sessions = [],
isLoading: isLoadingSessions,
isError: isErrorSessions,
error: sessionsError,
} = useGetSessionsQuery(undefined, {
skip: !user,
pollingInterval: 20000, pollingInterval: 20000,
}); });
const [logoutSession, {isLoading: isLoggingOutSession}] = useLogoutSessionMutation();
const [logoutAllSessions, {isLoading: isLoggingOutAll}] = useLogoutAllSessionsMutation();
const containerStyle = {padding: screens.xs ? 16 : 24}; const containerStyle = {padding: screens.xs ? 16 : 24};
const cardStyle = {marginBottom: 24}; const cardStyle = {marginBottom: 24};
const buttonStyle = {width: screens.xs ? "100%" : "auto"}; const buttonStyle = {width: screens.xs ? "100%" : "auto"};
const handleEditUser = () => { const handleEditUser = () => {
dispatch(setSelectedUser(userData)); dispatch(setSelectedUser(userData))
}; };
const handleLogoutSession = async (sessionId) => {
try {
await logoutSession(sessionId).unwrap();
} catch (error) {
const errorMessage = error?.data?.detail || "Не удалось завершить сессию";
dispatch(setError(errorMessage));
if (error?.status === 401) {
dispatch(logout());
navigate("/login");
}
}
};
const handleLogoutAllSessions = async () => {
try {
await logoutAllSessions().unwrap();
dispatch(logout());
navigate("/login");
} catch (error) {
const errorMessage = error?.data?.detail || "Не удалось завершить все сессии";
dispatch(setError(errorMessage));
if (error?.status === 401) {
dispatch(logout());
navigate("/login");
}
}
};
if (isErrorSessions && sessionsError?.status === 401) {
dispatch(logout());
navigate("/login");
}
return { return {
userData, userData,
sessions,
containerStyle, containerStyle,
cardStyle, cardStyle,
buttonStyle, buttonStyle,
isMobile: screens.xs, isMobile: screens.xs,
isLoading: isLoadingUserData || isLoadingSessions, isLoading: isLoadingUserData,
isError: isErrorUserData || isErrorSessions, isError: isErrorUserData,
handleEditUser, handleEditUser,
handleLogoutSession,
handleLogoutAllSessions,
isLoggingOutSession,
isLoggingOutAll,
}; };
}; };

View File

@ -38,9 +38,6 @@ const authSlice = createSlice({
setError(state, action) { setError(state, action) {
state.error = action.payload; state.error = action.payload;
state.isLoading = false; state.isLoading = false;
state.user = null;
state.userData = null;
localStorage.removeItem("access_token");
}, },
logout(state) { logout(state) {
state.user = null; state.user = null;