This commit is contained in:
2026-02-27 22:44:02 +03:00
parent 55c3629868
commit c57a40b09a
14 changed files with 331 additions and 69 deletions
@@ -7,15 +7,16 @@ from dishka.integrations.aiogram_dialog import inject
from dutylog.application.bot.user_dialogs.states import MainMenuSG from dutylog.application.bot.user_dialogs.states import MainMenuSG
from dutylog.infrastructure.database.repositories.users_repository import UsersRepository from dutylog.infrastructure.database.repositories.users_repository import UsersRepository
from dutylog.infrastructure.database.repositories.residents_repository import ResidentsRepository
from dutylog.infrastructure.database.repositories.hours_transactions_repository import HoursTransactionsRepository from dutylog.infrastructure.database.repositories.hours_transactions_repository import HoursTransactionsRepository
from dutylog.infrastructure.utils.config import Config from dutylog.infrastructure.utils.config import Config
@inject @inject
async def get_main_menu_data( async def get_main_menu_data(
dialog_manager: DialogManager,
event_from_user: User, event_from_user: User,
users_repository: FromDishka[UsersRepository], users_repository: FromDishka[UsersRepository],
residents_repository: FromDishka[ResidentsRepository],
config: FromDishka[Config], config: FromDishka[Config],
**kwargs, **kwargs,
): ):
@@ -37,14 +38,26 @@ async def get_main_menu_data(
greeting = f"👋 <b>Привет, {event_from_user.first_name}!</b>" greeting = f"👋 <b>Привет, {event_from_user.first_name}!</b>"
if not is_admin and not is_creator: if not is_admin and not is_creator:
resident = await residents_repository.get_resident_by_user_id(event_from_user.id)
if not resident:
content = f"""
{greeting}
<blockquote>⚠️ <b>Профиль не найден</b></blockquote>
Вы еще не привязаны к резиденту.
Обратитесь к администратору для регистрации.
"""
else:
content = f""" content = f"""
{greeting} {greeting}
⏰ <b>Ваши часы дежурств</b> ⏰ <b>Ваши часы дежурств</b>
<blockquote>🟢 <b>Отработанные часы:</b> <code>{user.active_hours}</code> ч <blockquote>🟢 <b>Отработанные часы:</b> <code>{resident.active_hours}</code> ч
━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━
🔴 Неотработанные часы: <code>{user.inactive_hours}</code> ч</blockquote> 🔴 Неотработанные часы: <code>{resident.inactive_hours}</code> ч</blockquote>
""" """
else: else:
content = f""" content = f"""
@@ -58,17 +71,27 @@ async def get_main_menu_data(
return { return {
"content": content, "content": content,
"is_regular_user": not is_admin and not is_creator, "is_regular_user": not is_admin and not is_creator,
"has_resident": resident is not None if not is_admin and not is_creator else False,
} }
@inject @inject
async def get_history_data( async def get_history_data(
dialog_manager: DialogManager,
event_from_user: User, event_from_user: User,
residents_repository: FromDishka[ResidentsRepository],
transactions_repository: FromDishka[HoursTransactionsRepository], transactions_repository: FromDishka[HoursTransactionsRepository],
**kwargs, **kwargs,
): ):
transactions = await transactions_repository.get_user_history(event_from_user.id) resident = await residents_repository.get_resident_by_user_id(event_from_user.id)
if not resident:
history_text = """
<blockquote>📜 <b>История операций</b></blockquote>
<i>Профиль не найден</i>
"""
else:
transactions = await transactions_repository.get_resident_history(resident.id)
last_10 = transactions[:10] last_10 = transactions[:10]
if not last_10: if not last_10:
@@ -103,7 +126,7 @@ main_menu_dialog = Dialog(
Const("📜 История"), Const("📜 История"),
id="history_btn", id="history_btn",
state=MainMenuSG.history, state=MainMenuSG.history,
when="is_regular_user", when="has_resident",
), ),
state=MainMenuSG.main, state=MainMenuSG.main,
getter=get_main_menu_data, getter=get_main_menu_data,
@@ -20,10 +20,10 @@ class HoursTransactionsDAO:
) )
return list(result.scalars().all()) return list(result.scalars().all())
async def get_by_user_id(self, user_id: int) -> list[HoursTransaction]: async def get_by_resident_id(self, resident_id: int) -> list[HoursTransaction]:
result = await self.session.execute( result = await self.session.execute(
select(HoursTransaction) select(HoursTransaction)
.where(HoursTransaction.user_id == user_id) .where(HoursTransaction.resident_id == resident_id)
.order_by(HoursTransaction.created_at.desc()) .order_by(HoursTransaction.created_at.desc())
) )
return list(result.scalars().all()) return list(result.scalars().all())
@@ -0,0 +1,56 @@
from sqlalchemy import select, update, delete
from sqlalchemy.ext.asyncio import AsyncSession
from dutylog.infrastructure.database.models.resident import Resident
class ResidentsDAO:
def __init__(self, session: AsyncSession):
self.session = session
async def get_by_id(self, resident_id: int) -> Resident | None:
result = await self.session.execute(
select(Resident).where(Resident.id == resident_id)
)
return result.scalar_one_or_none()
async def get_by_user_id(self, user_id: int) -> Resident | None:
result = await self.session.execute(
select(Resident).where(Resident.user_entity == user_id)
)
return result.scalar_one_or_none()
async def get_by_room(self, room_id: int) -> list[Resident]:
result = await self.session.execute(
select(Resident).where(Resident.room == room_id)
)
return list(result.scalars().all())
async def get_available(self) -> list[Resident]:
result = await self.session.execute(
select(Resident).where(Resident.is_busy == False)
)
return list(result.scalars().all())
async def get_all(self) -> list[Resident]:
result = await self.session.execute(select(Resident))
return list(result.scalars().all())
async def create(self, resident: Resident) -> Resident:
self.session.add(resident)
await self.session.commit()
await self.session.refresh(resident)
return resident
async def update(self, resident_id: int, **kwargs) -> Resident | None:
await self.session.execute(
update(Resident).where(Resident.id == resident_id).values(**kwargs)
)
await self.session.commit()
return await self.get_by_id(resident_id)
async def delete(self, resident_id: int) -> None:
await self.session.execute(
delete(Resident).where(Resident.id == resident_id)
)
await self.session.commit()
@@ -0,0 +1,44 @@
from sqlalchemy import select, update, delete
from sqlalchemy.ext.asyncio import AsyncSession
from dutylog.infrastructure.database.models.room import Room
class RoomsDAO:
def __init__(self, session: AsyncSession):
self.session = session
async def get_by_id(self, room_id: int) -> Room | None:
result = await self.session.execute(
select(Room).where(Room.id == room_id)
)
return result.scalar_one_or_none()
async def get_by_number(self, number: int) -> Room | None:
result = await self.session.execute(
select(Room).where(Room.number == number)
)
return result.scalar_one_or_none()
async def get_all(self) -> list[Room]:
result = await self.session.execute(select(Room))
return list(result.scalars().all())
async def create(self, room: Room) -> Room:
self.session.add(room)
await self.session.commit()
await self.session.refresh(room)
return room
async def update(self, room_id: int, **kwargs) -> Room | None:
await self.session.execute(
update(Room).where(Room.id == room_id).values(**kwargs)
)
await self.session.commit()
return await self.get_by_id(room_id)
async def delete(self, room_id: int) -> None:
await self.session.execute(
delete(Room).where(Room.id == room_id)
)
await self.session.commit()
@@ -1,5 +1,7 @@
from dutylog.infrastructure.database.models.base import Base from dutylog.infrastructure.database.models.base import Base
from dutylog.infrastructure.database.models.user import User from dutylog.infrastructure.database.models.user import User
from dutylog.infrastructure.database.models.hours_transaction import HoursTransaction from dutylog.infrastructure.database.models.hours_transaction import HoursTransaction
from dutylog.infrastructure.database.models.room import Room
from dutylog.infrastructure.database.models.resident import Resident
__all__ = ["Base", "User", "HoursTransaction"] __all__ = ["Base", "User", "HoursTransaction", "Room", "Resident"]
@@ -1,4 +1,4 @@
from datetime import datetime, timezone, timedelta from datetime import datetime
from enum import Enum from enum import Enum
from sqlalchemy import BigInteger, Integer, String, DateTime, ForeignKey from sqlalchemy import BigInteger, Integer, String, DateTime, ForeignKey
@@ -17,7 +17,7 @@ class HoursTransaction(Base):
__tablename__ = "hours_transactions" __tablename__ = "hours_transactions"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) resident_id: Mapped[int] = mapped_column(Integer, ForeignKey("residents.id", ondelete="CASCADE"), nullable=False)
transaction_type: Mapped[str] = mapped_column(String(50), nullable=False) transaction_type: Mapped[str] = mapped_column(String(50), nullable=False)
amount: Mapped[int] = mapped_column(Integer, nullable=False) amount: Mapped[int] = mapped_column(Integer, nullable=False)
admin_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.id", ondelete="SET NULL"), nullable=True) admin_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
@@ -0,0 +1,21 @@
from datetime import datetime
from sqlalchemy import BigInteger, Boolean, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from dutylog.infrastructure.database.models.base import Base
from dutylog.infrastructure.utils.datetime import msk_now
class Resident(Base):
__tablename__ = "residents"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
real_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
room: Mapped[int] = mapped_column(Integer, ForeignKey("rooms.id", ondelete="CASCADE"), nullable=False)
user_entity: Mapped[int | None] = mapped_column(BigInteger, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, unique=True)
is_busy: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
active_hours: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
inactive_hours: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=msk_now)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=msk_now, onupdate=msk_now)
@@ -0,0 +1,11 @@
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped, mapped_column
from dutylog.infrastructure.database.models.base import Base
class Room(Base):
__tablename__ = "rooms"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
number: Mapped[int] = mapped_column(Integer, nullable=False, unique=True)
@@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from sqlalchemy import BigInteger, Boolean, Integer, String, DateTime from sqlalchemy import BigInteger, Boolean, String, DateTime
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from dutylog.infrastructure.database.models.base import Base from dutylog.infrastructure.database.models.base import Base
@@ -15,7 +15,5 @@ class User(Base):
first_name: Mapped[str | None] = mapped_column(String(255), nullable=True) first_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
last_name: Mapped[str | None] = mapped_column(String(255), nullable=True) last_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") is_admin: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
active_hours: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
inactive_hours: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=msk_now) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=msk_now)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=msk_now, onupdate=msk_now) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=msk_now, onupdate=msk_now)
@@ -1,72 +1,72 @@
from dutylog.infrastructure.database.dao.hours_transactions_dao import HoursTransactionsDAO from dutylog.infrastructure.database.dao.hours_transactions_dao import HoursTransactionsDAO
from dutylog.infrastructure.database.dao.users_dao import UsersDAO from dutylog.infrastructure.database.dao.residents_dao import ResidentsDAO
from dutylog.infrastructure.database.models.hours_transaction import HoursTransaction, TransactionType from dutylog.infrastructure.database.models.hours_transaction import HoursTransaction, TransactionType
from dutylog.infrastructure.database.models.user import User from dutylog.infrastructure.database.models.resident import Resident
class HoursTransactionsRepository: class HoursTransactionsRepository:
def __init__( def __init__(
self, self,
transactions_dao: HoursTransactionsDAO, transactions_dao: HoursTransactionsDAO,
users_dao: UsersDAO, residents_dao: ResidentsDAO,
): ):
self.transactions_dao = transactions_dao self.transactions_dao = transactions_dao
self.users_dao = users_dao self.residents_dao = residents_dao
async def add_hours( async def add_hours(
self, self,
user_id: int, resident_id: int,
amount: int, amount: int,
admin_id: int | None = None, admin_id: int | None = None,
is_active: bool = True, is_active: bool = True,
) -> tuple[HoursTransaction, User | None]: ) -> tuple[HoursTransaction, Resident | None]:
transaction = HoursTransaction( transaction = HoursTransaction(
user_id=user_id, resident_id=resident_id,
transaction_type=TransactionType.INCREASE.value, transaction_type=TransactionType.INCREASE.value,
amount=amount, amount=amount,
admin_id=admin_id, admin_id=admin_id,
) )
transaction = await self.transactions_dao.create(transaction) transaction = await self.transactions_dao.create(transaction)
user = await self.users_dao.get_by_id(user_id) resident = await self.residents_dao.get_by_id(resident_id)
if user: if resident:
if is_active: if is_active:
new_hours = user.active_hours + amount new_hours = resident.active_hours + amount
user = await self.users_dao.update(user_id, active_hours=new_hours) resident = await self.residents_dao.update(resident_id, active_hours=new_hours)
else: else:
new_hours = user.inactive_hours + amount new_hours = resident.inactive_hours + amount
user = await self.users_dao.update(user_id, inactive_hours=new_hours) resident = await self.residents_dao.update(resident_id, inactive_hours=new_hours)
return transaction, user return transaction, resident
async def remove_hours( async def remove_hours(
self, self,
user_id: int, resident_id: int,
amount: int, amount: int,
admin_id: int | None = None, admin_id: int | None = None,
is_active: bool = True, is_active: bool = True,
) -> tuple[HoursTransaction, User | None]: ) -> tuple[HoursTransaction, Resident | None]:
transaction = HoursTransaction( transaction = HoursTransaction(
user_id=user_id, resident_id=resident_id,
transaction_type=TransactionType.DECREASE.value, transaction_type=TransactionType.DECREASE.value,
amount=amount, amount=amount,
admin_id=admin_id, admin_id=admin_id,
) )
transaction = await self.transactions_dao.create(transaction) transaction = await self.transactions_dao.create(transaction)
user = await self.users_dao.get_by_id(user_id) resident = await self.residents_dao.get_by_id(resident_id)
if user: if resident:
if is_active: if is_active:
new_hours = max(0, user.active_hours - amount) new_hours = max(0, resident.active_hours - amount)
user = await self.users_dao.update(user_id, active_hours=new_hours) resident = await self.residents_dao.update(resident_id, active_hours=new_hours)
else: else:
new_hours = max(0, user.inactive_hours - amount) new_hours = max(0, resident.inactive_hours - amount)
user = await self.users_dao.update(user_id, inactive_hours=new_hours) resident = await self.residents_dao.update(resident_id, inactive_hours=new_hours)
return transaction, user return transaction, resident
async def get_user_history(self, user_id: int) -> list[HoursTransaction]: async def get_resident_history(self, resident_id: int) -> list[HoursTransaction]:
return await self.transactions_dao.get_by_user_id(user_id) return await self.transactions_dao.get_by_resident_id(resident_id)
async def get_all_transactions(self) -> list[HoursTransaction]: async def get_all_transactions(self) -> list[HoursTransaction]:
return await self.transactions_dao.get_all() return await self.transactions_dao.get_all()
@@ -0,0 +1,74 @@
from dutylog.infrastructure.database.dao.residents_dao import ResidentsDAO
from dutylog.infrastructure.database.models.resident import Resident
class ResidentsRepository:
def __init__(self, residents_dao: ResidentsDAO):
self.residents_dao = residents_dao
async def create_resident(
self,
room_id: int,
real_name: str | None = None,
) -> Resident:
resident = Resident(
room=room_id,
real_name=real_name,
)
return await self.residents_dao.create(resident)
async def bind_user_to_resident(self, resident_id: int, user_id: int) -> Resident | None:
return await self.residents_dao.update(
resident_id,
user_entity=user_id,
is_busy=True,
)
async def unbind_user_from_resident(self, resident_id: int) -> Resident | None:
return await self.residents_dao.update(
resident_id,
user_entity=None,
is_busy=False,
)
async def update_resident_info(
self,
resident_id: int,
real_name: str | None = None,
) -> Resident | None:
return await self.residents_dao.update(
resident_id,
real_name=real_name,
)
async def add_active_hours(self, resident_id: int, hours: int) -> Resident | None:
resident = await self.residents_dao.get_by_id(resident_id)
if resident:
new_hours = resident.active_hours + hours
return await self.residents_dao.update(resident_id, active_hours=new_hours)
return None
async def add_inactive_hours(self, resident_id: int, hours: int) -> Resident | None:
resident = await self.residents_dao.get_by_id(resident_id)
if resident:
new_hours = resident.inactive_hours + hours
return await self.residents_dao.update(resident_id, inactive_hours=new_hours)
return None
async def get_resident_by_id(self, resident_id: int) -> Resident | None:
return await self.residents_dao.get_by_id(resident_id)
async def get_resident_by_user_id(self, user_id: int) -> Resident | None:
return await self.residents_dao.get_by_user_id(user_id)
async def get_residents_by_room(self, room_id: int) -> list[Resident]:
return await self.residents_dao.get_by_room(room_id)
async def get_available_residents(self) -> list[Resident]:
return await self.residents_dao.get_available()
async def get_all_residents(self) -> list[Resident]:
return await self.residents_dao.get_all()
async def delete_resident(self, resident_id: int) -> None:
await self.residents_dao.delete(resident_id)
@@ -0,0 +1,26 @@
from dutylog.infrastructure.database.dao.rooms_dao import RoomsDAO
from dutylog.infrastructure.database.models.room import Room
class RoomsRepository:
def __init__(self, rooms_dao: RoomsDAO):
self.rooms_dao = rooms_dao
async def get_or_create_room(self, number: int) -> Room:
room = await self.rooms_dao.get_by_number(number)
if not room:
room = Room(number=number)
room = await self.rooms_dao.create(room)
return room
async def get_room_by_id(self, room_id: int) -> Room | None:
return await self.rooms_dao.get_by_id(room_id)
async def get_room_by_number(self, number: int) -> Room | None:
return await self.rooms_dao.get_by_number(number)
async def get_all_rooms(self) -> list[Room]:
return await self.rooms_dao.get_all()
async def delete_room(self, room_id: int) -> None:
await self.rooms_dao.delete(room_id)
@@ -41,20 +41,6 @@ class UsersRepository:
async def set_admin_status(self, user_id: int, is_admin: bool) -> User | None: async def set_admin_status(self, user_id: int, is_admin: bool) -> User | None:
return await self.users_dao.update(user_id, is_admin=is_admin) return await self.users_dao.update(user_id, is_admin=is_admin)
async def add_active_hours(self, user_id: int, hours: int) -> User | None:
user = await self.users_dao.get_by_id(user_id)
if user:
new_hours = user.active_hours + hours
return await self.users_dao.update(user_id, active_hours=new_hours)
return None
async def add_inactive_hours(self, user_id: int, hours: int) -> User | None:
user = await self.users_dao.get_by_id(user_id)
if user:
new_hours = user.inactive_hours + hours
return await self.users_dao.update(user_id, inactive_hours=new_hours)
return None
async def get_all_admins(self) -> list[User]: async def get_all_admins(self) -> list[User]:
all_users = await self.users_dao.get_all() all_users = await self.users_dao.get_all()
return [user for user in all_users if user.is_admin] return [user for user in all_users if user.is_admin]
+23 -2
View File
@@ -6,8 +6,12 @@ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
from dutylog.infrastructure.database.config import create_engine, create_session_maker from dutylog.infrastructure.database.config import create_engine, create_session_maker
from dutylog.infrastructure.database.dao.users_dao import UsersDAO from dutylog.infrastructure.database.dao.users_dao import UsersDAO
from dutylog.infrastructure.database.dao.hours_transactions_dao import HoursTransactionsDAO from dutylog.infrastructure.database.dao.hours_transactions_dao import HoursTransactionsDAO
from dutylog.infrastructure.database.dao.rooms_dao import RoomsDAO
from dutylog.infrastructure.database.dao.residents_dao import ResidentsDAO
from dutylog.infrastructure.database.repositories.users_repository import UsersRepository from dutylog.infrastructure.database.repositories.users_repository import UsersRepository
from dutylog.infrastructure.database.repositories.hours_transactions_repository import HoursTransactionsRepository from dutylog.infrastructure.database.repositories.hours_transactions_repository import HoursTransactionsRepository
from dutylog.infrastructure.database.repositories.rooms_repository import RoomsRepository
from dutylog.infrastructure.database.repositories.residents_repository import ResidentsRepository
from dutylog.infrastructure.utils.config import Config, load_config from dutylog.infrastructure.utils.config import Config, load_config
@@ -43,6 +47,14 @@ class DAOProvider(Provider):
def get_hours_transactions_dao(self, session: AsyncSession) -> HoursTransactionsDAO: def get_hours_transactions_dao(self, session: AsyncSession) -> HoursTransactionsDAO:
return HoursTransactionsDAO(session) return HoursTransactionsDAO(session)
@provide(scope=Scope.REQUEST)
def get_rooms_dao(self, session: AsyncSession) -> RoomsDAO:
return RoomsDAO(session)
@provide(scope=Scope.REQUEST)
def get_residents_dao(self, session: AsyncSession) -> ResidentsDAO:
return ResidentsDAO(session)
class RepositoryProvider(Provider): class RepositoryProvider(Provider):
@provide(scope=Scope.REQUEST) @provide(scope=Scope.REQUEST)
@@ -53,9 +65,18 @@ class RepositoryProvider(Provider):
def get_hours_transactions_repository( def get_hours_transactions_repository(
self, self,
transactions_dao: HoursTransactionsDAO, transactions_dao: HoursTransactionsDAO,
users_dao: UsersDAO, residents_dao: ResidentsDAO,
) -> HoursTransactionsRepository: ) -> HoursTransactionsRepository:
return HoursTransactionsRepository(transactions_dao, users_dao) return HoursTransactionsRepository(transactions_dao, residents_dao)
@provide(scope=Scope.REQUEST)
def get_rooms_repository(self, rooms_dao: RoomsDAO) -> RoomsRepository:
return RoomsRepository(rooms_dao)
@provide(scope=Scope.REQUEST)
def get_residents_repository(self, residents_dao: ResidentsDAO) -> ResidentsRepository:
return ResidentsRepository(residents_dao)