feat: work shifts planning
This commit is contained in:
		
							
								
								
									
										1
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								main.py
									
									
									
									
									
								
							@@ -47,6 +47,7 @@ routers_list = [
 | 
				
			|||||||
    routers.task_router,
 | 
					    routers.task_router,
 | 
				
			||||||
    routers.statistics_router,
 | 
					    routers.statistics_router,
 | 
				
			||||||
    routers.work_shifts_router,
 | 
					    routers.work_shifts_router,
 | 
				
			||||||
 | 
					    routers.work_shifts_planning_router,
 | 
				
			||||||
    routers.transaction_router,
 | 
					    routers.transaction_router,
 | 
				
			||||||
    routers.shipping_router,
 | 
					    routers.shipping_router,
 | 
				
			||||||
    routers.department_router,
 | 
					    routers.department_router,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,14 @@
 | 
				
			|||||||
from datetime import datetime
 | 
					from datetime import datetime, date
 | 
				
			||||||
from typing import TYPE_CHECKING
 | 
					from typing import TYPE_CHECKING
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from sqlalchemy import ForeignKey
 | 
					from sqlalchemy import ForeignKey, Table, Column
 | 
				
			||||||
from sqlalchemy.sql import expression
 | 
					from sqlalchemy.sql import expression
 | 
				
			||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
 | 
					from sqlalchemy.orm import Mapped, mapped_column, relationship
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from models.base import BaseModel
 | 
					from models.base import BaseModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
    from models import User
 | 
					    from models import User, Position
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WorkShift(BaseModel):
 | 
					class WorkShift(BaseModel):
 | 
				
			||||||
@@ -63,3 +63,29 @@ class WorkShiftPause(BaseModel):
 | 
				
			|||||||
        back_populates="pauses",
 | 
					        back_populates="pauses",
 | 
				
			||||||
        lazy="selectin",
 | 
					        lazy="selectin",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					work_shifts_positions = Table(
 | 
				
			||||||
 | 
					    'work_shifts_positions',
 | 
				
			||||||
 | 
					    BaseModel.metadata,
 | 
				
			||||||
 | 
					    Column('position_key', ForeignKey('positions.key'), primary_key=True),
 | 
				
			||||||
 | 
					    Column('work_shift_id', ForeignKey('planned_work_shifts.id'), primary_key=True),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PlannedWorkShift(BaseModel):
 | 
				
			||||||
 | 
					    __tablename__ = "planned_work_shifts"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    id: Mapped[int] = mapped_column(primary_key=True)
 | 
				
			||||||
 | 
					    shift_date: Mapped[date] = mapped_column(nullable=False, index=True)
 | 
				
			||||||
 | 
					    created_at: Mapped[datetime] = mapped_column(nullable=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False, index=True)
 | 
				
			||||||
 | 
					    user: Mapped["User"] = relationship(lazy="selectin", backref="planned_work_shifts")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    positions: Mapped[list["Position"]] = relationship(
 | 
				
			||||||
 | 
					        "Position",
 | 
				
			||||||
 | 
					        uselist=True,
 | 
				
			||||||
 | 
					        secondary=work_shifts_positions,
 | 
				
			||||||
 | 
					        lazy="selectin",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,7 @@ from .time_tracking import time_tracking_router
 | 
				
			|||||||
from .billing import billing_router
 | 
					from .billing import billing_router
 | 
				
			||||||
from .task import task_router
 | 
					from .task import task_router
 | 
				
			||||||
from .work_shifts import work_shifts_router
 | 
					from .work_shifts import work_shifts_router
 | 
				
			||||||
 | 
					from .work_shifts_planning import work_shifts_planning_router
 | 
				
			||||||
from .statistics import statistics_router
 | 
					from .statistics import statistics_router
 | 
				
			||||||
from .transaction import transaction_router
 | 
					from .transaction import transaction_router
 | 
				
			||||||
from .shipping import shipping_router
 | 
					from .shipping import shipping_router
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										34
									
								
								routers/work_shifts_planning.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								routers/work_shifts_planning.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					from fastapi import APIRouter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from backend.dependecies import SessionDependency
 | 
				
			||||||
 | 
					from schemas.work_shifts_planning import *
 | 
				
			||||||
 | 
					from services.work_shifts_planning import WorkShiftsPlanningService
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					work_shifts_planning_router = APIRouter(
 | 
				
			||||||
 | 
					    prefix="/work-shifts-planning",
 | 
				
			||||||
 | 
					    tags=["work-shifts-planning"],
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@work_shifts_planning_router.post(
 | 
				
			||||||
 | 
					    "/",
 | 
				
			||||||
 | 
					    response_model=GetPlannedWorkShiftsResponse,
 | 
				
			||||||
 | 
					    operation_id="get_work_shifts",
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					async def get_work_shifts(
 | 
				
			||||||
 | 
					        session: SessionDependency,
 | 
				
			||||||
 | 
					        request: GetWorkShiftsPlanningDataRequest,
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    return await WorkShiftsPlanningService(session).get_work_shifts(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@work_shifts_planning_router.post(
 | 
				
			||||||
 | 
					    "/update",
 | 
				
			||||||
 | 
					    response_model=UpdatePlanningWorkShiftResponse,
 | 
				
			||||||
 | 
					    operation_id="update_work_shift",
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					async def update_work_shift(
 | 
				
			||||||
 | 
					        session: SessionDependency,
 | 
				
			||||||
 | 
					        request: UpdatePlanningWorkShiftRequest,
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    return await WorkShiftsPlanningService(session).update_work_shift(request)
 | 
				
			||||||
							
								
								
									
										47
									
								
								schemas/work_shifts_planning.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								schemas/work_shifts_planning.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					from datetime import datetime, date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from schemas.base import BaseSchema, OkMessageSchema
 | 
				
			||||||
 | 
					from schemas.position import PositionSchema
 | 
				
			||||||
 | 
					from schemas.user import UserSchema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# region Entities
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PlannedWorkShiftSchema(BaseSchema):
 | 
				
			||||||
 | 
					    id: int
 | 
				
			||||||
 | 
					    shift_date: datetime
 | 
				
			||||||
 | 
					    positions: list[PositionSchema]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PlanningTableRow(BaseSchema):
 | 
				
			||||||
 | 
					    user: UserSchema
 | 
				
			||||||
 | 
					    shifts: list[PlannedWorkShiftSchema]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# region Requests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GetWorkShiftsPlanningDataRequest(BaseSchema):
 | 
				
			||||||
 | 
					    date_from: date
 | 
				
			||||||
 | 
					    date_to: date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UpdatePlanningWorkShiftRequest(BaseSchema):
 | 
				
			||||||
 | 
					    shift_date: date
 | 
				
			||||||
 | 
					    position_keys: list[str]
 | 
				
			||||||
 | 
					    user_id: int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# region Responses
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GetPlannedWorkShiftsResponse(BaseSchema):
 | 
				
			||||||
 | 
					    shifts: list[PlanningTableRow]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UpdatePlanningWorkShiftResponse(OkMessageSchema):
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# endregion
 | 
				
			||||||
							
								
								
									
										81
									
								
								services/work_shifts_planning.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								services/work_shifts_planning.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					from collections import defaultdict
 | 
				
			||||||
 | 
					from typing import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from sqlalchemy import select, and_
 | 
				
			||||||
 | 
					from sqlalchemy.orm import joinedload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from models import Position
 | 
				
			||||||
 | 
					from models.work_shifts import PlannedWorkShift
 | 
				
			||||||
 | 
					from schemas.work_shifts_planning import *
 | 
				
			||||||
 | 
					from services.base import BaseService
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WorkShiftsPlanningService(BaseService):
 | 
				
			||||||
 | 
					    async def get_work_shifts(self, request: GetWorkShiftsPlanningDataRequest) -> GetPlannedWorkShiftsResponse:
 | 
				
			||||||
 | 
					        users_shifts_ids = (
 | 
				
			||||||
 | 
					            select(PlannedWorkShift)
 | 
				
			||||||
 | 
					            .options(
 | 
				
			||||||
 | 
					                joinedload(PlannedWorkShift.user),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .where(PlannedWorkShift.shift_date.between(request.date_from, request.date_to))
 | 
				
			||||||
 | 
					            .order_by(PlannedWorkShift.shift_date)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        shifts = (await self.session.scalars(users_shifts_ids)).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        user_shifts_dict = defaultdict(list)
 | 
				
			||||||
 | 
					        for shift in shifts:
 | 
				
			||||||
 | 
					            user_shifts_dict[shift.user].append(shift)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        result_shifts = []
 | 
				
			||||||
 | 
					        for user, shifts in user_shifts_dict.items():
 | 
				
			||||||
 | 
					            user_schema = UserSchema.model_validate(user)
 | 
				
			||||||
 | 
					            shifts_schema = [PlannedWorkShiftSchema.model_validate(shift) for shift in shifts]
 | 
				
			||||||
 | 
					            result_shifts.append(
 | 
				
			||||||
 | 
					                PlanningTableRow(
 | 
				
			||||||
 | 
					                    user=user_schema,
 | 
				
			||||||
 | 
					                    shifts=shifts_schema,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return GetPlannedWorkShiftsResponse(shifts=result_shifts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def _get_work_shift(self, user_id: int, shift_date: date) -> Optional[PlannedWorkShift]:
 | 
				
			||||||
 | 
					        stmt = (
 | 
				
			||||||
 | 
					            select(PlannedWorkShift)
 | 
				
			||||||
 | 
					            .where(
 | 
				
			||||||
 | 
					                and_(
 | 
				
			||||||
 | 
					                    PlannedWorkShift.user_id == user_id,
 | 
				
			||||||
 | 
					                    PlannedWorkShift.shift_date == shift_date,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        work_shift = await self.session.execute(stmt)
 | 
				
			||||||
 | 
					        work_shift = work_shift.one_or_none()
 | 
				
			||||||
 | 
					        return work_shift[0] if work_shift else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def update_work_shift(self, request: UpdatePlanningWorkShiftRequest) -> UpdatePlanningWorkShiftResponse:
 | 
				
			||||||
 | 
					        work_shift = await self._get_work_shift(request.user_id, request.shift_date)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        positions: list[Position] = []
 | 
				
			||||||
 | 
					        for position_key in request.position_keys:
 | 
				
			||||||
 | 
					            position: Optional[Position] = await self.session.get(Position, position_key)
 | 
				
			||||||
 | 
					            if position:
 | 
				
			||||||
 | 
					                positions.append(position)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                return UpdatePlanningWorkShiftResponse(ok=False, message=f"Должность с ID: {position_key} не найдена")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not work_shift:
 | 
				
			||||||
 | 
					            work_shift = PlannedWorkShift(
 | 
				
			||||||
 | 
					                user_id=request.user_id,
 | 
				
			||||||
 | 
					                shift_date=request.shift_date,
 | 
				
			||||||
 | 
					                positions=positions,
 | 
				
			||||||
 | 
					                created_at=datetime.now(),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            self.session.add(work_shift)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            work_shift.positions = positions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await self.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return UpdatePlanningWorkShiftResponse(ok=True, message="Данные о смене сохранены")
 | 
				
			||||||
		Reference in New Issue
	
	Block a user