o2o-negosium-original/negodata/backend/services/dashboard_service.py
Mina Choi f19c6f57ea [chore] negodata: enum 주석 영문값 보강 + 견적상태 3종 정리 + 대시보드 정비
- enum DDL 주석에 영문값 보강(status/role/delivery/usage_type/qt_type/supplier_type)
- 견적상태 3종(생성/진행중/마감)으로 정리(ON_HOLD 제거), 배송 PARTNER→SUPPLIER
- 대시보드 손질, 관련 테스트·negosium 견적유형 주석 동반

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:25:38 +09:00

102 lines
4.3 KiB
Python

import uuid
from datetime import timedelta
from fastapi import Depends
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import quotations
from common.enums import DBWRType, ErrorType
from common.utils.gtime import GTime
from crud.dashboard_crud import DashboardCRUD, IDashboardCRUD
from router.v1.dashboard.protocol import (
DashboardActionList,
DashboardEmailUnsent,
DashboardEmailUnsentItem,
DashboardQuotationRef,
DashboardScope,
Res_DashboardSummary,
)
class DashboardService:
"""대시보드 요약 집계. 회사 전체(company)와 내가 만든 견적(mine) 두 스코프를 한 응답으로 내린다.
KPI 숫자는 전수 COUNT, 액션 리스트는 정렬해서 상위 N개만. 읽기 전용(사이드이펙트 없음).
"""
LIST_LIMIT = 5 # 액션 위젯 리스트당 표시 개수
DEADLINE_HOURS = 72 # 마감 임박 기준(3일 이내)
def __init__(self, dashboard_crud: IDashboardCRUD = Depends(DashboardCRUD)):
self.dashboard_crud = dashboard_crud
async def get_summary(self, company_id: str, user_id: str) -> Res_DashboardSummary:
res = Res_DashboardSummary()
company_uuid = uuid.UUID(company_id)
user_uuid = uuid.UUID(user_id)
now = GTime.UTC()
horizon = now + timedelta(hours=self.DEADLINE_HOURS)
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
res.company = await self._scope_summary(company_uuid, None, now, horizon, month_start)
res.mine = await self._scope_summary(company_uuid, user_uuid, now, horizon, month_start)
return res
async def _scope_summary(self, company_uuid, owner_uuid, now, horizon, month_start) -> DashboardScope:
scope = DashboardScope()
scope.in_progress = await self._count(
lambda s: self.dashboard_crud.count_in_progress(s, company_uuid, owner_uuid)
)
scope.this_month = await self._count(
lambda s: self.dashboard_crud.count_created_since(s, company_uuid, owner_uuid, month_start)
)
scope.awarded_this_month = await self._count(
lambda s: self.dashboard_crud.count_awarded_since(s, company_uuid, owner_uuid, month_start)
)
scope.deadline_soon = await self._action(
lambda s: self.dashboard_crud.deadline_soon(s, company_uuid, owner_uuid, now, horizon, self.LIST_LIMIT),
with_end_time=True,
)
scope.awarded = await self._action(
lambda s: self.dashboard_crud.awarded(s, company_uuid, owner_uuid, self.LIST_LIMIT)
)
scope.equal_bid = await self._action(
lambda s: self.dashboard_crud.equal_bid(s, company_uuid, owner_uuid, self.LIST_LIMIT)
)
scope.ruptured = await self._action(
lambda s: self.dashboard_crud.ruptured(s, company_uuid, owner_uuid, self.LIST_LIMIT)
)
scope.email_unsent = await self._email_unsent(company_uuid, owner_uuid)
return scope
async def _count(self, fn) -> int:
err, n = await DB_SESSION_MNG.execute_lambda(quotations.DBType(), DBWRType.DB_READ.value, fn)
return n if err == ErrorType.SUCCESS else 0
async def _action(self, fn, with_end_time: bool = False) -> DashboardActionList:
out = DashboardActionList()
err, rows, total = await DB_SESSION_MNG.execute_lambda(quotations.DBType(), DBWRType.DB_READ.value, fn)
if err != ErrorType.SUCCESS:
return out
out.total = total
out.items = [
DashboardQuotationRef(qt_id=r[0], name=r[1] or "", end_time=(r[2] if with_end_time else None))
for r in rows
]
return out
async def _email_unsent(self, company_uuid, owner_uuid) -> DashboardEmailUnsent:
out = DashboardEmailUnsent()
err, rows, total = await DB_SESSION_MNG.execute_lambda(
quotations.DBType(),
DBWRType.DB_READ.value,
lambda s: self.dashboard_crud.email_unsent(s, company_uuid, owner_uuid, self.LIST_LIMIT),
)
if err != ErrorType.SUCCESS:
return out
out.total = total
out.quotations = [
DashboardEmailUnsentItem(qt_id=r[0], name=r[1] or "", unsent_count=int(r[2] or 0)) for r in rows
]
return out