mirror of
https://github.com/koloideal/DutyLog.git
synced 2026-06-10 10:25:29 +03:00
update
This commit is contained in:
@@ -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 ###
|
||||||
@@ -35,11 +35,17 @@ from dutylog.application.bot.user_dialogs.admin_dialogs.rooms_management import
|
|||||||
create_room_input_window,
|
create_room_input_window,
|
||||||
create_room_confirm_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 (
|
from dutylog.application.bot.user_dialogs.admin_dialogs.hours_management import (
|
||||||
add_hours_select_window,
|
add_hours_select_window,
|
||||||
remove_hours_select_window,
|
remove_hours_select_window,
|
||||||
add_hours_custom_window,
|
add_hours_custom_window,
|
||||||
remove_hours_custom_window,
|
remove_hours_custom_window,
|
||||||
|
add_hours_remark_window,
|
||||||
|
remove_hours_remark_window,
|
||||||
add_hours_confirm_window,
|
add_hours_confirm_window,
|
||||||
remove_hours_confirm_window,
|
remove_hours_confirm_window,
|
||||||
)
|
)
|
||||||
@@ -59,6 +65,8 @@ admin_menu_dialog = Dialog(
|
|||||||
remove_hours_select_window,
|
remove_hours_select_window,
|
||||||
add_hours_custom_window,
|
add_hours_custom_window,
|
||||||
remove_hours_custom_window,
|
remove_hours_custom_window,
|
||||||
|
add_hours_remark_window,
|
||||||
|
remove_hours_remark_window,
|
||||||
add_hours_confirm_window,
|
add_hours_confirm_window,
|
||||||
remove_hours_confirm_window,
|
remove_hours_confirm_window,
|
||||||
create_resident_name_window,
|
create_resident_name_window,
|
||||||
@@ -75,6 +83,8 @@ admin_menu_dialog = Dialog(
|
|||||||
create_room_select_floor_window,
|
create_room_select_floor_window,
|
||||||
create_room_input_window,
|
create_room_input_window,
|
||||||
create_room_confirm_window,
|
create_room_confirm_window,
|
||||||
|
reporting_period_window,
|
||||||
|
next_period_confirm_window,
|
||||||
statistics_window,
|
statistics_window,
|
||||||
broadcast_window,
|
broadcast_window,
|
||||||
broadcast_confirm_window,
|
broadcast_confirm_window,
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ async def on_hours_selected(
|
|||||||
dialog_manager.dialog_data["selected_hours"] = int(item_id)
|
dialog_manager.dialog_data["selected_hours"] = int(item_id)
|
||||||
|
|
||||||
if dialog_manager.current_context().state == AdminMenuSG.add_hours_select:
|
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:
|
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(
|
async def on_custom_hours_click(
|
||||||
@@ -84,19 +84,59 @@ async def on_custom_hours_input(
|
|||||||
dialog_manager.dialog_data["selected_hours"] = hours
|
dialog_manager.dialog_data["selected_hours"] = hours
|
||||||
|
|
||||||
if dialog_manager.current_context().state == AdminMenuSG.add_hours_custom:
|
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:
|
else:
|
||||||
await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm)
|
await dialog_manager.switch_to(AdminMenuSG.remove_hours_remark)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await message.answer("⚠️ Пожалуйста, введите корректное число")
|
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(
|
async def get_hours_confirm_data(
|
||||||
dialog_manager: DialogManager,
|
dialog_manager: DialogManager,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
hours = dialog_manager.dialog_data.get("selected_hours", 0)
|
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
|
@inject
|
||||||
@@ -108,6 +148,7 @@ async def on_add_hours_confirm(
|
|||||||
):
|
):
|
||||||
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
|
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
|
||||||
hours = dialog_manager.dialog_data.get("selected_hours")
|
hours = dialog_manager.dialog_data.get("selected_hours")
|
||||||
|
remark = dialog_manager.dialog_data.get("hours_remark")
|
||||||
admin_id = callback.from_user.id
|
admin_id = callback.from_user.id
|
||||||
|
|
||||||
if resident_id and hours:
|
if resident_id and hours:
|
||||||
@@ -116,6 +157,7 @@ async def on_add_hours_confirm(
|
|||||||
amount=hours,
|
amount=hours,
|
||||||
admin_id=admin_id,
|
admin_id=admin_id,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
|
remark=remark,
|
||||||
)
|
)
|
||||||
|
|
||||||
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
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")
|
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
|
||||||
hours = dialog_manager.dialog_data.get("selected_hours")
|
hours = dialog_manager.dialog_data.get("selected_hours")
|
||||||
|
remark = dialog_manager.dialog_data.get("hours_remark")
|
||||||
admin_id = callback.from_user.id
|
admin_id = callback.from_user.id
|
||||||
|
|
||||||
if resident_id and hours:
|
if resident_id and hours:
|
||||||
@@ -147,6 +190,7 @@ async def on_remove_hours_confirm(
|
|||||||
resident_id=resident_id,
|
resident_id=resident_id,
|
||||||
amount=hours,
|
amount=hours,
|
||||||
admin_id=admin_id,
|
admin_id=admin_id,
|
||||||
|
remark=remark,
|
||||||
)
|
)
|
||||||
|
|
||||||
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
||||||
@@ -234,8 +278,35 @@ remove_hours_custom_window = Window(
|
|||||||
state=AdminMenuSG.remove_hours_custom,
|
state=AdminMenuSG.remove_hours_custom,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_hours_remark_window = Window(
|
||||||
|
Const("<blockquote>📝 <b>Причина добавления часов</b></blockquote>\n\n<blockquote>Укажите причину добавления часов (обязательно).</blockquote>"),
|
||||||
|
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("<blockquote>📝 <b>Причина снятия часов</b></blockquote>\n\n<blockquote>Укажите причину снятия часов (необязательно).</blockquote>"),
|
||||||
|
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(
|
add_hours_confirm_window = Window(
|
||||||
Format("<blockquote>➕ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите добавить <code>{hours}</code> часов?"),
|
Format("<blockquote>➕ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите добавить <code>{hours}</code> часов?\n\n<b>Причина:</b> {remark}"),
|
||||||
Row(
|
Row(
|
||||||
Button(
|
Button(
|
||||||
Const("✅ Да"),
|
Const("✅ Да"),
|
||||||
@@ -253,7 +324,7 @@ add_hours_confirm_window = Window(
|
|||||||
)
|
)
|
||||||
|
|
||||||
remove_hours_confirm_window = Window(
|
remove_hours_confirm_window = Window(
|
||||||
Format("<blockquote>➖ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите отнять <code>{hours}</code> часов?"),
|
Format("<blockquote>➖ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите отнять <code>{hours}</code> часов?\n\n<b>Причина:</b> {remark}"),
|
||||||
Row(
|
Row(
|
||||||
Button(
|
Button(
|
||||||
Const("✅ Да"),
|
Const("✅ Да"),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from aiogram_dialog.widgets.text import Format, Const
|
|||||||
from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button
|
from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button
|
||||||
from dishka import FromDishka
|
from dishka import FromDishka
|
||||||
from dishka.integrations.aiogram_dialog import inject
|
from dishka.integrations.aiogram_dialog import inject
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from dutylog.application.bot.user_dialogs.states import AdminMenuSG
|
from dutylog.application.bot.user_dialogs.states import AdminMenuSG
|
||||||
from dutylog.infrastructure.database.repositories.users_repository import (
|
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 (
|
from dutylog.infrastructure.database.repositories.residents_repository import (
|
||||||
ResidentsRepository,
|
ResidentsRepository,
|
||||||
)
|
)
|
||||||
|
from dutylog.infrastructure.database.repositories.reporting_periods_repository import (
|
||||||
|
ReportingPeriodsRepository,
|
||||||
|
)
|
||||||
from dutylog.infrastructure.utils.config import Config
|
from dutylog.infrastructure.utils.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
MONTH_NAMES = {
|
||||||
|
1: "Январь",
|
||||||
|
2: "Февраль",
|
||||||
|
3: "Март",
|
||||||
|
4: "Апрель",
|
||||||
|
5: "Май",
|
||||||
|
6: "Июнь",
|
||||||
|
7: "Июль",
|
||||||
|
8: "Август",
|
||||||
|
9: "Сентябрь",
|
||||||
|
10: "Октябрь",
|
||||||
|
11: "Ноябрь",
|
||||||
|
12: "Декабрь",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@inject
|
@inject
|
||||||
async def get_admin_menu_data(
|
async def get_admin_menu_data(
|
||||||
event_from_user: User,
|
event_from_user: User,
|
||||||
users_repository: FromDishka[UsersRepository],
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
reporting_periods_repository: FromDishka[ReportingPeriodsRepository],
|
||||||
config: FromDishka[Config],
|
config: FromDishka[Config],
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -36,11 +57,32 @@ async def get_admin_menu_data(
|
|||||||
else:
|
else:
|
||||||
greeting = "👨💼 <b>Администратор</b>"
|
greeting = "👨💼 <b>Администратор</b>"
|
||||||
|
|
||||||
|
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"""
|
||||||
|
━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
📅 <b>Активный отчётный период</b>
|
||||||
|
<blockquote>Месяц: <b>{reporting_month} {reporting_year}</b>
|
||||||
|
Начало: <code>{start_date.strftime('%d.%m.%Y')}</code>
|
||||||
|
Прошло дней: <code>{days_passed}</code></blockquote>
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
period_info = ""
|
||||||
|
|
||||||
content = f"""
|
content = f"""
|
||||||
{greeting}
|
{greeting}
|
||||||
|
|
||||||
<blockquote>📋 <b>Панель управления</b></blockquote>
|
<blockquote>📋 <b>Панель управления</b></blockquote>
|
||||||
|
{period_info}
|
||||||
Выберите действие:
|
Выберите действие:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -91,6 +133,10 @@ async def on_floors_click(callback, button, dialog_manager):
|
|||||||
await dialog_manager.switch_to(AdminMenuSG.floors)
|
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(
|
main_menu_window = Window(
|
||||||
Format("{content}"),
|
Format("{content}"),
|
||||||
SwitchTo(
|
SwitchTo(
|
||||||
@@ -110,6 +156,11 @@ main_menu_window = Window(
|
|||||||
on_click=on_floors_click,
|
on_click=on_floors_click,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Button(
|
||||||
|
Const("📅 Отчётный период"),
|
||||||
|
id="reporting_period_btn",
|
||||||
|
on_click=on_reporting_period_click,
|
||||||
|
),
|
||||||
SwitchTo(
|
SwitchTo(
|
||||||
Const("📊 Статистика"),
|
Const("📊 Статистика"),
|
||||||
id="stats_btn",
|
id="stats_btn",
|
||||||
|
|||||||
+203
@@ -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"""
|
||||||
|
<blockquote>📅 <b>Отчётный период</b></blockquote>
|
||||||
|
|
||||||
|
<b>Статус:</b> 🟢 Активен
|
||||||
|
|
||||||
|
<b>Отчётный месяц:</b> <code>{reporting_month} {reporting_year}</code>
|
||||||
|
<b>Дата начала:</b> <code>{start_date.strftime('%d.%m.%Y')}</code>
|
||||||
|
<b>Прошло дней:</b> <code>{days_passed}</code>
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
Используйте кнопки ниже для управления периодом.
|
||||||
|
"""
|
||||||
|
has_active = True
|
||||||
|
else:
|
||||||
|
content = """
|
||||||
|
<blockquote>📅 <b>Отчётный период</b></blockquote>
|
||||||
|
|
||||||
|
<b>Статус:</b> ⚪️ Нет активного периода
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
Создайте новый отчётный период, чтобы начать учёт дежурств.
|
||||||
|
"""
|
||||||
|
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"""
|
||||||
|
<blockquote>⚠️ <b>Подтверждение</b></blockquote>
|
||||||
|
|
||||||
|
Вы уверены, что хотите начать следующий отчётный период?
|
||||||
|
|
||||||
|
<b>Текущий период:</b>
|
||||||
|
• Месяц: <code>{reporting_month} {reporting_year}</code>
|
||||||
|
• Начало: <code>{start_date.strftime('%d.%m.%Y')}</code>
|
||||||
|
|
||||||
|
Текущий период будет закрыт с датой окончания <code>{datetime.now().date().strftime('%d.%m.%Y')}</code>, и будет создан новый период.
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
content = f"""
|
||||||
|
<blockquote>⚠️ <b>Подтверждение</b></blockquote>
|
||||||
|
|
||||||
|
Вы уверены, что хотите создать новый отчётный период?
|
||||||
|
|
||||||
|
Период начнётся с <code>{datetime.now().date().strftime('%d.%m.%Y')}</code>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
@@ -22,7 +22,9 @@ class AdminMenuSG(StatesGroup):
|
|||||||
remove_hours_select = State()
|
remove_hours_select = State()
|
||||||
add_hours_custom = State()
|
add_hours_custom = State()
|
||||||
remove_hours_custom = State()
|
remove_hours_custom = State()
|
||||||
|
add_hours_remark = State()
|
||||||
add_hours_confirm = State()
|
add_hours_confirm = State()
|
||||||
|
remove_hours_remark = State()
|
||||||
remove_hours_confirm = State()
|
remove_hours_confirm = State()
|
||||||
create_resident_name = State()
|
create_resident_name = State()
|
||||||
create_resident_floor = State()
|
create_resident_floor = State()
|
||||||
@@ -38,6 +40,8 @@ class AdminMenuSG(StatesGroup):
|
|||||||
create_room_select_floor = State()
|
create_room_select_floor = State()
|
||||||
create_room_input = State()
|
create_room_input = State()
|
||||||
create_room_confirm = State()
|
create_room_confirm = State()
|
||||||
|
reporting_period = State()
|
||||||
|
next_period_confirm = State()
|
||||||
statistics = State()
|
statistics = State()
|
||||||
broadcast = State()
|
broadcast = State()
|
||||||
broadcast_confirm = State()
|
broadcast_confirm = State()
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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.room import Room
|
||||||
from dutylog.infrastructure.database.models.resident import Resident
|
from dutylog.infrastructure.database.models.resident import Resident
|
||||||
from dutylog.infrastructure.database.models.floor import Floor
|
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"]
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class HoursTransaction(Base):
|
|||||||
admin_id: Mapped[int] = mapped_column(
|
admin_id: Mapped[int] = mapped_column(
|
||||||
BigInteger, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
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(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True), default=msk_now
|
DateTime(timezone=True), default=msk_now
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -24,12 +24,14 @@ class HoursTransactionsRepository:
|
|||||||
amount: int,
|
amount: int,
|
||||||
admin_id: int | None = None,
|
admin_id: int | None = None,
|
||||||
is_active: bool = True,
|
is_active: bool = True,
|
||||||
|
remark: str | None = None,
|
||||||
) -> tuple[HoursTransaction, Resident | None]:
|
) -> tuple[HoursTransaction, Resident | None]:
|
||||||
transaction = HoursTransaction(
|
transaction = HoursTransaction(
|
||||||
resident_id=resident_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,
|
||||||
|
remark=remark,
|
||||||
)
|
)
|
||||||
transaction = await self.transactions_dao.create(transaction)
|
transaction = await self.transactions_dao.create(transaction)
|
||||||
|
|
||||||
@@ -54,12 +56,14 @@ class HoursTransactionsRepository:
|
|||||||
amount: int,
|
amount: int,
|
||||||
admin_id: int | None = None,
|
admin_id: int | None = None,
|
||||||
is_active: bool = True,
|
is_active: bool = True,
|
||||||
|
remark: str | None = None,
|
||||||
) -> tuple[HoursTransaction, Resident | None]:
|
) -> tuple[HoursTransaction, Resident | None]:
|
||||||
transaction = HoursTransaction(
|
transaction = HoursTransaction(
|
||||||
resident_id=resident_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,
|
||||||
|
remark=remark,
|
||||||
)
|
)
|
||||||
transaction = await self.transactions_dao.create(transaction)
|
transaction = await self.transactions_dao.create(transaction)
|
||||||
|
|
||||||
@@ -83,6 +87,7 @@ class HoursTransactionsRepository:
|
|||||||
resident_id: int,
|
resident_id: int,
|
||||||
amount: int,
|
amount: int,
|
||||||
admin_id: int | None = None,
|
admin_id: int | None = None,
|
||||||
|
remark: str | None = None,
|
||||||
) -> tuple[HoursTransaction, Resident | None]:
|
) -> tuple[HoursTransaction, Resident | None]:
|
||||||
"""Перемещает часы из неотработанных в отработанные"""
|
"""Перемещает часы из неотработанных в отработанные"""
|
||||||
transaction = HoursTransaction(
|
transaction = HoursTransaction(
|
||||||
@@ -90,6 +95,7 @@ class HoursTransactionsRepository:
|
|||||||
transaction_type=TransactionType.DECREASE.value,
|
transaction_type=TransactionType.DECREASE.value,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
admin_id=admin_id,
|
admin_id=admin_id,
|
||||||
|
remark=remark,
|
||||||
)
|
)
|
||||||
transaction = await self.transactions_dao.create(transaction)
|
transaction = await self.transactions_dao.create(transaction)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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.rooms_dao import RoomsDAO
|
||||||
from dutylog.infrastructure.database.dao.residents_dao import ResidentsDAO
|
from dutylog.infrastructure.database.dao.residents_dao import ResidentsDAO
|
||||||
from dutylog.infrastructure.database.dao.floors_dao import FloorsDAO
|
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 (
|
from dutylog.infrastructure.database.repositories.users_repository import (
|
||||||
UsersRepository,
|
UsersRepository,
|
||||||
)
|
)
|
||||||
@@ -26,6 +29,9 @@ from dutylog.infrastructure.database.repositories.residents_repository import (
|
|||||||
from dutylog.infrastructure.database.repositories.floors_repository import (
|
from dutylog.infrastructure.database.repositories.floors_repository import (
|
||||||
FloorsRepository,
|
FloorsRepository,
|
||||||
)
|
)
|
||||||
|
from dutylog.infrastructure.database.repositories.reporting_periods_repository import (
|
||||||
|
ReportingPeriodsRepository,
|
||||||
|
)
|
||||||
from dutylog.infrastructure.utils.config import Config, load_config
|
from dutylog.infrastructure.utils.config import Config, load_config
|
||||||
|
|
||||||
|
|
||||||
@@ -75,6 +81,10 @@ class DAOProvider(Provider):
|
|||||||
def get_floors_dao(self, session: AsyncSession) -> FloorsDAO:
|
def get_floors_dao(self, session: AsyncSession) -> FloorsDAO:
|
||||||
return FloorsDAO(session)
|
return FloorsDAO(session)
|
||||||
|
|
||||||
|
@provide(scope=Scope.REQUEST)
|
||||||
|
def get_reporting_periods_dao(self, session: AsyncSession) -> ReportingPeriodsDAO:
|
||||||
|
return ReportingPeriodsDAO(session)
|
||||||
|
|
||||||
|
|
||||||
class RepositoryProvider(Provider):
|
class RepositoryProvider(Provider):
|
||||||
@provide(scope=Scope.REQUEST)
|
@provide(scope=Scope.REQUEST)
|
||||||
@@ -102,3 +112,9 @@ class RepositoryProvider(Provider):
|
|||||||
@provide(scope=Scope.REQUEST)
|
@provide(scope=Scope.REQUEST)
|
||||||
def get_floors_repository(self, floors_dao: FloorsDAO) -> FloorsRepository:
|
def get_floors_repository(self, floors_dao: FloorsDAO) -> FloorsRepository:
|
||||||
return FloorsRepository(floors_dao)
|
return FloorsRepository(floors_dao)
|
||||||
|
|
||||||
|
@provide(scope=Scope.REQUEST)
|
||||||
|
def get_reporting_periods_repository(
|
||||||
|
self, reporting_periods_dao: ReportingPeriodsDAO
|
||||||
|
) -> ReportingPeriodsRepository:
|
||||||
|
return ReportingPeriodsRepository(reporting_periods_dao)
|
||||||
|
|||||||
Reference in New Issue
Block a user