diff --git a/alembic/versions/8861cafdae4f_add_remark_to_hours_transaction_and_.py b/alembic/versions/8861cafdae4f_add_remark_to_hours_transaction_and_.py new file mode 100644 index 0000000..3a1227f --- /dev/null +++ b/alembic/versions/8861cafdae4f_add_remark_to_hours_transaction_and_.py @@ -0,0 +1,39 @@ +"""add_remark_to_hours_transaction_and_reporting_period_model + +Revision ID: 8861cafdae4f +Revises: 0f74944ba3bb +Create Date: 2026-03-01 13:51:37.323937 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '8861cafdae4f' +down_revision: Union[str, Sequence[str], None] = '0f74944ba3bb' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('reporting_periods', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('start_date', sa.Date(), nullable=False), + sa.Column('end_date', sa.Date(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.add_column('hours_transactions', sa.Column('remark', sa.String(length=500), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('hours_transactions', 'remark') + op.drop_table('reporting_periods') + # ### end Alembic commands ### diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py index 111035f..9939650 100644 --- a/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py @@ -35,11 +35,17 @@ from dutylog.application.bot.user_dialogs.admin_dialogs.rooms_management import create_room_input_window, create_room_confirm_window, ) +from dutylog.application.bot.user_dialogs.admin_dialogs.reporting_period_management import ( + reporting_period_window, + next_period_confirm_window, +) from dutylog.application.bot.user_dialogs.admin_dialogs.hours_management import ( add_hours_select_window, remove_hours_select_window, add_hours_custom_window, remove_hours_custom_window, + add_hours_remark_window, + remove_hours_remark_window, add_hours_confirm_window, remove_hours_confirm_window, ) @@ -59,6 +65,8 @@ admin_menu_dialog = Dialog( remove_hours_select_window, add_hours_custom_window, remove_hours_custom_window, + add_hours_remark_window, + remove_hours_remark_window, add_hours_confirm_window, remove_hours_confirm_window, create_resident_name_window, @@ -75,6 +83,8 @@ admin_menu_dialog = Dialog( create_room_select_floor_window, create_room_input_window, create_room_confirm_window, + reporting_period_window, + next_period_confirm_window, statistics_window, broadcast_window, broadcast_confirm_window, diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/hours_management.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/hours_management.py index 8145ab3..cd8d744 100644 --- a/src/dutylog/application/bot/user_dialogs/admin_dialogs/hours_management.py +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/hours_management.py @@ -50,9 +50,9 @@ async def on_hours_selected( dialog_manager.dialog_data["selected_hours"] = int(item_id) if dialog_manager.current_context().state == AdminMenuSG.add_hours_select: - await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm) + await dialog_manager.switch_to(AdminMenuSG.add_hours_remark) else: - await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm) + await dialog_manager.switch_to(AdminMenuSG.remove_hours_remark) async def on_custom_hours_click( @@ -84,19 +84,59 @@ async def on_custom_hours_input( dialog_manager.dialog_data["selected_hours"] = hours if dialog_manager.current_context().state == AdminMenuSG.add_hours_custom: - await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm) + await dialog_manager.switch_to(AdminMenuSG.add_hours_remark) else: - await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm) + await dialog_manager.switch_to(AdminMenuSG.remove_hours_remark) except ValueError: await message.answer("⚠️ Пожалуйста, введите корректное число") +async def on_add_hours_remark_input( + message: Message, + widget: MessageInput, + dialog_manager: DialogManager, +): + if not message.text or len(message.text.strip()) < 1: + await message.answer("⚠️ Пожалуйста, введите причину добавления часов") + return + + dialog_manager.dialog_data["hours_remark"] = message.text.strip() + await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm) + + +async def on_remove_hours_remark_input( + message: Message, + widget: MessageInput, + dialog_manager: DialogManager, +): + if message.text and len(message.text.strip()) > 0: + dialog_manager.dialog_data["hours_remark"] = message.text.strip() + else: + dialog_manager.dialog_data["hours_remark"] = None + + await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm) + + +async def on_skip_remark( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + dialog_manager.dialog_data["hours_remark"] = None + await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm) + + async def get_hours_confirm_data( dialog_manager: DialogManager, **kwargs, ): hours = dialog_manager.dialog_data.get("selected_hours", 0) - return {"hours": hours} + remark = dialog_manager.dialog_data.get("hours_remark") + + return { + "hours": hours, + "remark": remark if remark else "Не указана", + } @inject @@ -108,6 +148,7 @@ async def on_add_hours_confirm( ): resident_id = dialog_manager.dialog_data.get("selected_resident_id") hours = dialog_manager.dialog_data.get("selected_hours") + remark = dialog_manager.dialog_data.get("hours_remark") admin_id = callback.from_user.id if resident_id and hours: @@ -116,6 +157,7 @@ async def on_add_hours_confirm( amount=hours, admin_id=admin_id, is_active=True, + remark=remark, ) await dialog_manager.switch_to(AdminMenuSG.resident_info) @@ -131,6 +173,7 @@ async def on_remove_hours_confirm( ): resident_id = dialog_manager.dialog_data.get("selected_resident_id") hours = dialog_manager.dialog_data.get("selected_hours") + remark = dialog_manager.dialog_data.get("hours_remark") admin_id = callback.from_user.id if resident_id and hours: @@ -147,6 +190,7 @@ async def on_remove_hours_confirm( resident_id=resident_id, amount=hours, admin_id=admin_id, + remark=remark, ) await dialog_manager.switch_to(AdminMenuSG.resident_info) @@ -234,8 +278,35 @@ remove_hours_custom_window = Window( state=AdminMenuSG.remove_hours_custom, ) +add_hours_remark_window = Window( + Const("
📝 Причина добавления часов\n\n
Укажите причину добавления часов (обязательно)."), + MessageInput(on_add_hours_remark_input), + SwitchTo( + Const("◀️ Назад"), + id="back_to_add_hours_select", + state=AdminMenuSG.add_hours_select, + ), + state=AdminMenuSG.add_hours_remark, +) + +remove_hours_remark_window = Window( + Const("
📝 Причина снятия часов\n\n
Укажите причину снятия часов (необязательно)."), + MessageInput(on_remove_hours_remark_input), + Button( + Const("⏭ Пропустить"), + id="skip_remark_btn", + on_click=on_skip_remark, + ), + SwitchTo( + Const("◀️ Назад"), + id="back_to_remove_hours_select", + state=AdminMenuSG.remove_hours_select, + ), + state=AdminMenuSG.remove_hours_remark, +) + add_hours_confirm_window = Window( - Format("
➕ Подтверждение\n\nВы уверены, что хотите добавить
{hours} часов?"),
+ Format("➕ Подтверждение\n\nВы уверены, что хотите добавить
{hours} часов?\n\nПричина: {remark}"),
Row(
Button(
Const("✅ Да"),
@@ -253,7 +324,7 @@ add_hours_confirm_window = Window(
)
remove_hours_confirm_window = Window(
- Format("➖ Подтверждение\n\nВы уверены, что хотите отнять
{hours} часов?"),
+ Format("➖ Подтверждение\n\nВы уверены, что хотите отнять
{hours} часов?\n\nПричина: {remark}"),
Row(
Button(
Const("✅ Да"),
diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/main_menu.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/main_menu.py
index 5de0001..4c75de1 100644
--- a/src/dutylog/application/bot/user_dialogs/admin_dialogs/main_menu.py
+++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/main_menu.py
@@ -4,6 +4,7 @@ from aiogram_dialog.widgets.text import Format, Const
from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button
from dishka import FromDishka
from dishka.integrations.aiogram_dialog import inject
+from datetime import datetime, timedelta
from dutylog.application.bot.user_dialogs.states import AdminMenuSG
from dutylog.infrastructure.database.repositories.users_repository import (
@@ -12,13 +13,33 @@ from dutylog.infrastructure.database.repositories.users_repository import (
from dutylog.infrastructure.database.repositories.residents_repository import (
ResidentsRepository,
)
+from dutylog.infrastructure.database.repositories.reporting_periods_repository import (
+ ReportingPeriodsRepository,
+)
from dutylog.infrastructure.utils.config import Config
+MONTH_NAMES = {
+ 1: "Январь",
+ 2: "Февраль",
+ 3: "Март",
+ 4: "Апрель",
+ 5: "Май",
+ 6: "Июнь",
+ 7: "Июль",
+ 8: "Август",
+ 9: "Сентябрь",
+ 10: "Октябрь",
+ 11: "Ноябрь",
+ 12: "Декабрь",
+}
+
+
@inject
async def get_admin_menu_data(
event_from_user: User,
users_repository: FromDishka[UsersRepository],
+ reporting_periods_repository: FromDishka[ReportingPeriodsRepository],
config: FromDishka[Config],
**kwargs,
):
@@ -36,11 +57,32 @@ async def get_admin_menu_data(
else:
greeting = "👨💼 Администратор"
+ active_period = await reporting_periods_repository.get_active_period()
+
+ if active_period:
+ start_date = active_period.start_date
+ next_day = start_date + timedelta(days=1)
+ reporting_month = MONTH_NAMES[next_day.month]
+ reporting_year = next_day.year
+
+ days_passed = (datetime.now().date() - start_date).days
+
+ period_info = f"""
+━━━━━━━━━━━━━━━━━━━━
+
+📅 Активный отчётный период
+Месяц: {reporting_month} {reporting_year} +Начало:+""" + else: + period_info = "" + content = f""" {greeting}{start_date.strftime('%d.%m.%Y')}+Прошло дней:{days_passed}
📋 Панель управления- +{period_info} Выберите действие: """ @@ -91,6 +133,10 @@ async def on_floors_click(callback, button, dialog_manager): await dialog_manager.switch_to(AdminMenuSG.floors) +async def on_reporting_period_click(callback, button, dialog_manager): + await dialog_manager.switch_to(AdminMenuSG.reporting_period) + + main_menu_window = Window( Format("{content}"), SwitchTo( @@ -110,6 +156,11 @@ main_menu_window = Window( on_click=on_floors_click, ), ), + Button( + Const("📅 Отчётный период"), + id="reporting_period_btn", + on_click=on_reporting_period_click, + ), SwitchTo( Const("📊 Статистика"), id="stats_btn", diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/reporting_period_management.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/reporting_period_management.py new file mode 100644 index 0000000..354767f --- /dev/null +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/reporting_period_management.py @@ -0,0 +1,203 @@ +from aiogram.types import CallbackQuery +from aiogram_dialog import Window, DialogManager +from aiogram_dialog.widgets.text import Format, Const +from aiogram_dialog.widgets.kbd import SwitchTo, Button, Row +from dishka import FromDishka +from dishka.integrations.aiogram_dialog import inject +from datetime import datetime, timedelta + +from dutylog.application.bot.user_dialogs.states import AdminMenuSG +from dutylog.infrastructure.database.repositories.reporting_periods_repository import ( + ReportingPeriodsRepository, +) + + +MONTH_NAMES = { + 1: "Январь", + 2: "Февраль", + 3: "Март", + 4: "Апрель", + 5: "Май", + 6: "Июнь", + 7: "Июль", + 8: "Август", + 9: "Сентябрь", + 10: "Октябрь", + 11: "Ноябрь", + 12: "Декабрь", +} + + +async def on_reporting_period_click( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + await dialog_manager.switch_to(AdminMenuSG.reporting_period) + + +@inject +async def get_reporting_period_data( + reporting_periods_repository: FromDishka[ReportingPeriodsRepository], + **kwargs, +): + active_period = await reporting_periods_repository.get_active_period() + + if active_period: + start_date = active_period.start_date + next_day = start_date + timedelta(days=1) + reporting_month = MONTH_NAMES[next_day.month] + reporting_year = next_day.year + + days_passed = (datetime.now().date() - start_date).days + + content = f""" +
📅 Отчётный период+ +Статус: 🟢 Активен + +Отчётный месяц:
{reporting_month} {reporting_year}
+Дата начала: {start_date.strftime('%d.%m.%Y')}
+Прошло дней: {days_passed}
+
+━━━━━━━━━━━━━━━━━━━━
+
+Используйте кнопки ниже для управления периодом.
+"""
+ has_active = True
+ else:
+ content = """
+📅 Отчётный период+ +Статус: ⚪️ Нет активного периода + +━━━━━━━━━━━━━━━━━━━━ + +Создайте новый отчётный период, чтобы начать учёт дежурств. +""" + has_active = False + + return { + "content": content, + "has_active": has_active, + } + + +async def on_next_period_click( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + await dialog_manager.switch_to(AdminMenuSG.next_period_confirm) + + +async def on_make_report_click( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + await callback.answer("⚠️ Функционал в разработке", show_alert=True) + + +@inject +async def get_next_period_confirm_data( + reporting_periods_repository: FromDishka[ReportingPeriodsRepository], + **kwargs, +): + active_period = await reporting_periods_repository.get_active_period() + + if active_period: + start_date = active_period.start_date + next_day = start_date + timedelta(days=1) + reporting_month = MONTH_NAMES[next_day.month] + reporting_year = next_day.year + + content = f""" +
⚠️ Подтверждение+ +Вы уверены, что хотите начать следующий отчётный период? + +Текущий период: +• Месяц:
{reporting_month} {reporting_year}
+• Начало: {start_date.strftime('%d.%m.%Y')}
+
+Текущий период будет закрыт с датой окончания {datetime.now().date().strftime('%d.%m.%Y')}, и будет создан новый период.
+"""
+ else:
+ content = f"""
+⚠️ Подтверждение+ +Вы уверены, что хотите создать новый отчётный период? + +Период начнётся с
{datetime.now().date().strftime('%d.%m.%Y')}.
+"""
+
+ return {"content": content}
+
+
+@inject
+async def on_next_period_confirm(
+ callback: CallbackQuery,
+ button: Button,
+ dialog_manager: DialogManager,
+ reporting_periods_repository: FromDishka[ReportingPeriodsRepository],
+):
+ active_period = await reporting_periods_repository.get_active_period()
+ current_date = datetime.now().date()
+
+ if active_period:
+ await reporting_periods_repository.close_period(active_period.id, current_date)
+
+ await reporting_periods_repository.create_period(current_date)
+
+ await callback.answer("✅ Новый отчётный период создан!")
+ await dialog_manager.switch_to(AdminMenuSG.reporting_period)
+
+
+async def on_next_period_cancel(
+ callback: CallbackQuery,
+ button: Button,
+ dialog_manager: DialogManager,
+):
+ await dialog_manager.switch_to(AdminMenuSG.reporting_period)
+
+
+reporting_period_window = Window(
+ Format("{content}"),
+ Button(
+ Const("➡️ Следующий период"),
+ id="next_period_btn",
+ on_click=on_next_period_click,
+ ),
+ Button(
+ Const("📊 Сделать отчёт"),
+ id="make_report_btn",
+ on_click=on_make_report_click,
+ when="has_active",
+ ),
+ SwitchTo(
+ Const("◀️ Назад"),
+ id="back_to_admin_menu_from_period",
+ state=AdminMenuSG.main,
+ ),
+ state=AdminMenuSG.reporting_period,
+ getter=get_reporting_period_data,
+)
+
+next_period_confirm_window = Window(
+ Format("{content}"),
+ Row(
+ Button(
+ Const("✅ Да"),
+ id="confirm_next_period",
+ on_click=on_next_period_confirm,
+ ),
+ Button(
+ Const("❌ Нет"),
+ id="cancel_next_period",
+ on_click=on_next_period_cancel,
+ ),
+ ),
+ state=AdminMenuSG.next_period_confirm,
+ getter=get_next_period_confirm_data,
+)
diff --git a/src/dutylog/application/bot/user_dialogs/states.py b/src/dutylog/application/bot/user_dialogs/states.py
index 9bbce5f..8147281 100644
--- a/src/dutylog/application/bot/user_dialogs/states.py
+++ b/src/dutylog/application/bot/user_dialogs/states.py
@@ -22,7 +22,9 @@ class AdminMenuSG(StatesGroup):
remove_hours_select = State()
add_hours_custom = State()
remove_hours_custom = State()
+ add_hours_remark = State()
add_hours_confirm = State()
+ remove_hours_remark = State()
remove_hours_confirm = State()
create_resident_name = State()
create_resident_floor = State()
@@ -38,6 +40,8 @@ class AdminMenuSG(StatesGroup):
create_room_select_floor = State()
create_room_input = State()
create_room_confirm = State()
+ reporting_period = State()
+ next_period_confirm = State()
statistics = State()
broadcast = State()
broadcast_confirm = State()
diff --git a/src/dutylog/infrastructure/database/dao/reporting_periods_dao.py b/src/dutylog/infrastructure/database/dao/reporting_periods_dao.py
new file mode 100644
index 0000000..626242e
--- /dev/null
+++ b/src/dutylog/infrastructure/database/dao/reporting_periods_dao.py
@@ -0,0 +1,42 @@
+from sqlalchemy import select, update, delete
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from dutylog.infrastructure.database.models.reporting_period import ReportingPeriod
+
+
+class ReportingPeriodsDAO:
+ def __init__(self, session: AsyncSession):
+ self.session = session
+
+ async def get_by_id(self, period_id: int) -> ReportingPeriod | None:
+ result = await self.session.execute(
+ select(ReportingPeriod).where(ReportingPeriod.id == period_id)
+ )
+ return result.scalar_one_or_none()
+
+ async def get_all(self) -> list[ReportingPeriod]:
+ result = await self.session.execute(select(ReportingPeriod))
+ return list(result.scalars().all())
+
+ async def get_active_period(self) -> ReportingPeriod | None:
+ result = await self.session.execute(
+ select(ReportingPeriod).where(ReportingPeriod.end_date.is_(None))
+ )
+ return result.scalar_one_or_none()
+
+ async def create(self, period: ReportingPeriod) -> ReportingPeriod:
+ self.session.add(period)
+ await self.session.commit()
+ await self.session.refresh(period)
+ return period
+
+ async def update(self, period_id: int, **kwargs) -> ReportingPeriod | None:
+ await self.session.execute(
+ update(ReportingPeriod).where(ReportingPeriod.id == period_id).values(**kwargs)
+ )
+ await self.session.commit()
+ return await self.get_by_id(period_id)
+
+ async def delete(self, period_id: int) -> None:
+ await self.session.execute(delete(ReportingPeriod).where(ReportingPeriod.id == period_id))
+ await self.session.commit()
diff --git a/src/dutylog/infrastructure/database/models/__init__.py b/src/dutylog/infrastructure/database/models/__init__.py
index a049653..b130e72 100644
--- a/src/dutylog/infrastructure/database/models/__init__.py
+++ b/src/dutylog/infrastructure/database/models/__init__.py
@@ -4,5 +4,6 @@ from dutylog.infrastructure.database.models.hours_transaction import HoursTransa
from dutylog.infrastructure.database.models.room import Room
from dutylog.infrastructure.database.models.resident import Resident
from dutylog.infrastructure.database.models.floor import Floor
+from dutylog.infrastructure.database.models.reporting_period import ReportingPeriod
-__all__ = ["Base", "User", "HoursTransaction", "Room", "Resident", "Floor"]
+__all__ = ["Base", "User", "HoursTransaction", "Room", "Resident", "Floor", "ReportingPeriod"]
diff --git a/src/dutylog/infrastructure/database/models/hours_transaction.py b/src/dutylog/infrastructure/database/models/hours_transaction.py
index 3cf7eb4..f9a3df7 100644
--- a/src/dutylog/infrastructure/database/models/hours_transaction.py
+++ b/src/dutylog/infrastructure/database/models/hours_transaction.py
@@ -25,6 +25,7 @@ class HoursTransaction(Base):
admin_id: Mapped[int] = mapped_column(
BigInteger, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
+ remark: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=msk_now
)
diff --git a/src/dutylog/infrastructure/database/models/reporting_period.py b/src/dutylog/infrastructure/database/models/reporting_period.py
new file mode 100644
index 0000000..623e8c6
--- /dev/null
+++ b/src/dutylog/infrastructure/database/models/reporting_period.py
@@ -0,0 +1,14 @@
+from datetime import date
+
+from sqlalchemy import Integer, Date
+from sqlalchemy.orm import Mapped, mapped_column
+
+from dutylog.infrastructure.database.models.base import Base
+
+
+class ReportingPeriod(Base):
+ __tablename__ = "reporting_periods"
+
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
+ start_date: Mapped[date] = mapped_column(Date, nullable=False)
+ end_date: Mapped[date | None] = mapped_column(Date, nullable=True)
diff --git a/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py b/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py
index d9a2abb..d0be656 100644
--- a/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py
+++ b/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py
@@ -24,12 +24,14 @@ class HoursTransactionsRepository:
amount: int,
admin_id: int | None = None,
is_active: bool = True,
+ remark: str | None = None,
) -> tuple[HoursTransaction, Resident | None]:
transaction = HoursTransaction(
resident_id=resident_id,
transaction_type=TransactionType.INCREASE.value,
amount=amount,
admin_id=admin_id,
+ remark=remark,
)
transaction = await self.transactions_dao.create(transaction)
@@ -54,12 +56,14 @@ class HoursTransactionsRepository:
amount: int,
admin_id: int | None = None,
is_active: bool = True,
+ remark: str | None = None,
) -> tuple[HoursTransaction, Resident | None]:
transaction = HoursTransaction(
resident_id=resident_id,
transaction_type=TransactionType.DECREASE.value,
amount=amount,
admin_id=admin_id,
+ remark=remark,
)
transaction = await self.transactions_dao.create(transaction)
@@ -83,6 +87,7 @@ class HoursTransactionsRepository:
resident_id: int,
amount: int,
admin_id: int | None = None,
+ remark: str | None = None,
) -> tuple[HoursTransaction, Resident | None]:
"""Перемещает часы из неотработанных в отработанные"""
transaction = HoursTransaction(
@@ -90,6 +95,7 @@ class HoursTransactionsRepository:
transaction_type=TransactionType.DECREASE.value,
amount=amount,
admin_id=admin_id,
+ remark=remark,
)
transaction = await self.transactions_dao.create(transaction)
diff --git a/src/dutylog/infrastructure/database/repositories/reporting_periods_repository.py b/src/dutylog/infrastructure/database/repositories/reporting_periods_repository.py
new file mode 100644
index 0000000..7c5bb49
--- /dev/null
+++ b/src/dutylog/infrastructure/database/repositories/reporting_periods_repository.py
@@ -0,0 +1,28 @@
+from datetime import date
+
+from dutylog.infrastructure.database.dao.reporting_periods_dao import ReportingPeriodsDAO
+from dutylog.infrastructure.database.models.reporting_period import ReportingPeriod
+
+
+class ReportingPeriodsRepository:
+ def __init__(self, reporting_periods_dao: ReportingPeriodsDAO):
+ self.reporting_periods_dao = reporting_periods_dao
+
+ async def create_period(self, start_date: date) -> ReportingPeriod:
+ period = ReportingPeriod(start_date=start_date)
+ return await self.reporting_periods_dao.create(period)
+
+ async def close_period(self, period_id: int, end_date: date) -> ReportingPeriod | None:
+ return await self.reporting_periods_dao.update(period_id, end_date=end_date)
+
+ async def get_period_by_id(self, period_id: int) -> ReportingPeriod | None:
+ return await self.reporting_periods_dao.get_by_id(period_id)
+
+ async def get_all_periods(self) -> list[ReportingPeriod]:
+ return await self.reporting_periods_dao.get_all()
+
+ async def get_active_period(self) -> ReportingPeriod | None:
+ return await self.reporting_periods_dao.get_active_period()
+
+ async def delete_period(self, period_id: int) -> None:
+ await self.reporting_periods_dao.delete(period_id)
diff --git a/src/dutylog/infrastructure/ioc.py b/src/dutylog/infrastructure/ioc.py
index 868ce65..e001005 100644
--- a/src/dutylog/infrastructure/ioc.py
+++ b/src/dutylog/infrastructure/ioc.py
@@ -11,6 +11,9 @@ from dutylog.infrastructure.database.dao.hours_transactions_dao import (
from dutylog.infrastructure.database.dao.rooms_dao import RoomsDAO
from dutylog.infrastructure.database.dao.residents_dao import ResidentsDAO
from dutylog.infrastructure.database.dao.floors_dao import FloorsDAO
+from dutylog.infrastructure.database.dao.reporting_periods_dao import (
+ ReportingPeriodsDAO,
+)
from dutylog.infrastructure.database.repositories.users_repository import (
UsersRepository,
)
@@ -26,6 +29,9 @@ from dutylog.infrastructure.database.repositories.residents_repository import (
from dutylog.infrastructure.database.repositories.floors_repository import (
FloorsRepository,
)
+from dutylog.infrastructure.database.repositories.reporting_periods_repository import (
+ ReportingPeriodsRepository,
+)
from dutylog.infrastructure.utils.config import Config, load_config
@@ -75,6 +81,10 @@ class DAOProvider(Provider):
def get_floors_dao(self, session: AsyncSession) -> FloorsDAO:
return FloorsDAO(session)
+ @provide(scope=Scope.REQUEST)
+ def get_reporting_periods_dao(self, session: AsyncSession) -> ReportingPeriodsDAO:
+ return ReportingPeriodsDAO(session)
+
class RepositoryProvider(Provider):
@provide(scope=Scope.REQUEST)
@@ -102,3 +112,9 @@ class RepositoryProvider(Provider):
@provide(scope=Scope.REQUEST)
def get_floors_repository(self, floors_dao: FloorsDAO) -> FloorsRepository:
return FloorsRepository(floors_dao)
+
+ @provide(scope=Scope.REQUEST)
+ def get_reporting_periods_repository(
+ self, reporting_periods_dao: ReportingPeriodsDAO
+ ) -> ReportingPeriodsRepository:
+ return ReportingPeriodsRepository(reporting_periods_dao)