feat: 本月人力成本包含管理层 + 今日提交分组 + 筛选bug修复
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 2m7s
Build and Deploy Web / build-and-deploy (push) Successful in 2m27s

- 本月人力成本 = 提交人成本 + 管理层成本(豁免角色)
- 今日提交情况分三组:已提交合格(≥8h)、时间不足(<8h)、未提交
- 修复内容提交页同时筛选人和项目时筛选失效的bug

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-03-03 19:46:14 +08:00
parent f9016ab2af
commit b62da90c8b
3 changed files with 51 additions and 15 deletions

View File

@ -34,7 +34,8 @@ def get_dashboard(
today = date.today() today = date.today()
month_start = today.replace(day=1) month_start = today.replace(day=1)
# 本月人力成本(简化:统计本月所有有提交的人的日成本) # 本月人力成本 = 提交人成本 + 管理层成本
# 1. 提交人成本(统计本月所有有提交的人的日成本)
monthly_submitters = db.query(Submission.user_id, Submission.submit_date).filter( monthly_submitters = db.query(Submission.user_id, Submission.submit_date).filter(
Submission.submit_date >= month_start, Submission.submit_date >= month_start,
Submission.submit_date <= today, Submission.submit_date <= today,
@ -50,6 +51,26 @@ def get_dashboard(
if user: if user:
monthly_labor += user.daily_cost monthly_labor += user.daily_cost
# 2. 管理层成本(豁免提交角色的人)
from models import Role
exempt_role_ids = set(
r.id for r in db.query(Role).filter(Role.exempt_submission == 1).all()
)
if exempt_role_ids:
exempt_users = db.query(User).filter(
User.is_active == 1,
User.monthly_salary > 0,
User.role_id.in_(exempt_role_ids),
).all()
# 本月有提交记录的工作日数
monthly_working_days = db.query(Submission.submit_date).filter(
Submission.submit_date >= month_start,
Submission.submit_date <= today,
).distinct().count()
# 管理层成本 = 每人日薪 × 本月工作日数
monthly_management = sum(u.daily_cost * monthly_working_days for u in exempt_users)
monthly_labor += monthly_management
# 本月 AI 工具成本 # 本月 AI 工具成本
monthly_ai = db.query(sa_func.sum(AIToolCost.amount)).filter( monthly_ai = db.query(sa_func.sum(AIToolCost.amount)).filter(
AIToolCost.record_date >= month_start, AIToolCost.record_date >= month_start,
@ -237,7 +258,8 @@ def get_dashboard(
Submission.submit_date == today, Submission.submit_date == today,
).distinct().all() ).distinct().all()
) )
submitted_users = [] submitted_sufficient = [] # 提交且满8小时
submitted_insufficient = [] # 提交但不足8小时
not_submitted_users = [] not_submitted_users = []
for u in all_active_users: for u in all_active_users:
info = {"id": u.id, "name": u.name} info = {"id": u.id, "name": u.name}
@ -247,14 +269,19 @@ def get_dashboard(
Submission.submit_date == today, Submission.submit_date == today,
).scalar() or 0 ).scalar() or 0
info["hours"] = round(hours, 1) info["hours"] = round(hours, 1)
submitted_users.append(info) if hours >= 8:
submitted_sufficient.append(info)
else:
submitted_insufficient.append(info)
else: else:
not_submitted_users.append(info) not_submitted_users.append(info)
daily_attendance = { daily_attendance = {
"total": len(all_active_users), "total": len(all_active_users),
"submitted_count": len(submitted_users), "submitted_sufficient_count": len(submitted_sufficient),
"submitted_insufficient_count": len(submitted_insufficient),
"not_submitted_count": len(not_submitted_users), "not_submitted_count": len(not_submitted_users),
"submitted": submitted_users, "submitted_sufficient": submitted_sufficient,
"submitted_insufficient": submitted_insufficient,
"not_submitted": not_submitted_users, "not_submitted": not_submitted_users,
} }

View File

@ -48,14 +48,13 @@ def list_submissions(
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
): ):
q = db.query(Submission) q = db.query(Submission)
# 查看项目内提交时,所有人都可见(方便横向对比) # 权限检查:没有 user:view 权限只能看自己的
# 全局提交列表时,没有 user:view 权限只能看自己的 if not current_user.has_permission("user:view"):
if project_id:
pass # 项目内提交不做用户过滤
elif not current_user.has_permission("user:view"):
q = q.filter(Submission.user_id == current_user.id) q = q.filter(Submission.user_id == current_user.id)
elif user_id: elif user_id:
# 有权限且指定了用户,过滤该用户
q = q.filter(Submission.user_id == user_id) q = q.filter(Submission.user_id == user_id)
# 项目筛选
if project_id: if project_id:
q = q.filter(Submission.project_id == project_id) q = q.filter(Submission.project_id == project_id)
if start_date: if start_date:

View File

@ -74,22 +74,29 @@
今日提交情况 今日提交情况
</span> </span>
<div class="attendance-summary"> <div class="attendance-summary">
<span class="att-tag submitted">已提交 {{ data.daily_attendance.submitted_count }}</span> <span class="att-tag submitted">已提交合格 {{ data.daily_attendance.submitted_sufficient_count }}</span>
<span class="att-tag insufficient" v-if="data.daily_attendance.submitted_insufficient_count > 0">时间不足 {{ data.daily_attendance.submitted_insufficient_count }}</span>
<span class="att-tag not-submitted" v-if="data.daily_attendance.not_submitted_count > 0">未提交 {{ data.daily_attendance.not_submitted_count }}</span> <span class="att-tag not-submitted" v-if="data.daily_attendance.not_submitted_count > 0">未提交 {{ data.daily_attendance.not_submitted_count }}</span>
<el-icon :size="14" style="color:var(--text-secondary);margin-left:4px;transition:transform 0.2s" :style="{transform: attendanceExpanded ? 'rotate(180deg)' : ''}"><ArrowDown /></el-icon> <el-icon :size="14" style="color:var(--text-secondary);margin-left:4px;transition:transform 0.2s" :style="{transform: attendanceExpanded ? 'rotate(180deg)' : ''}"><ArrowDown /></el-icon>
</div> </div>
</div> </div>
<div class="card-body attendance-body" v-show="attendanceExpanded"> <div class="card-body attendance-body" v-show="attendanceExpanded">
<div v-if="data.daily_attendance.not_submitted?.length" class="att-section"> <div v-if="data.daily_attendance.not_submitted?.length" class="att-section">
<div class="att-section-title not-submitted-title">未提交{{ data.daily_attendance.not_submitted.length }}</div> <div class="att-section-title not-submitted-title"> 未提交{{ data.daily_attendance.not_submitted.length }}</div>
<div class="att-user-list"> <div class="att-user-list">
<span v-for="u in data.daily_attendance.not_submitted" :key="u.id" class="att-user not-submitted">{{ u.name }}</span> <span v-for="u in data.daily_attendance.not_submitted" :key="u.id" class="att-user not-submitted">{{ u.name }}</span>
</div> </div>
</div> </div>
<div v-if="data.daily_attendance.submitted?.length" class="att-section"> <div v-if="data.daily_attendance.submitted_insufficient?.length" class="att-section">
<div class="att-section-title">已提交{{ data.daily_attendance.submitted.length }}</div> <div class="att-section-title insufficient-title"> 时间不足{{ data.daily_attendance.submitted_insufficient.length }}</div>
<div class="att-user-list"> <div class="att-user-list">
<span v-for="u in data.daily_attendance.submitted" :key="u.id" class="att-user submitted">{{ u.name }} <small>{{ u.hours }}h</small></span> <span v-for="u in data.daily_attendance.submitted_insufficient" :key="u.id" class="att-user insufficient">{{ u.name }} <small>{{ u.hours }}h</small></span>
</div>
</div>
<div v-if="data.daily_attendance.submitted_sufficient?.length" class="att-section">
<div class="att-section-title"> 已提交合格{{ data.daily_attendance.submitted_sufficient.length }}</div>
<div class="att-user-list">
<span v-for="u in data.daily_attendance.submitted_sufficient" :key="u.id" class="att-user submitted">{{ u.name }} <small>{{ u.hours }}h</small></span>
</div> </div>
</div> </div>
</div> </div>
@ -491,17 +498,20 @@ onUnmounted(() => {
.attendance-summary { display: flex; align-items: center; gap: 8px; } .attendance-summary { display: flex; align-items: center; gap: 8px; }
.att-tag { font-size: 13px; font-weight: 600; padding: 2px 10px; border-radius: 10px; } .att-tag { font-size: 13px; font-weight: 600; padding: 2px 10px; border-radius: 10px; }
.att-tag.submitted { color: #34C759; background: #E8F8EE; } .att-tag.submitted { color: #34C759; background: #E8F8EE; }
.att-tag.insufficient { color: #FF9500; background: #FFF3E0; }
.att-tag.not-submitted { color: #FF3B30; background: #FFE8E7; } .att-tag.not-submitted { color: #FF3B30; background: #FFE8E7; }
.attendance-body { padding-top: 8px !important; padding-bottom: 12px !important; } .attendance-body { padding-top: 8px !important; padding-bottom: 12px !important; }
.att-section { margin-bottom: 12px; } .att-section { margin-bottom: 12px; }
.att-section:last-child { margin-bottom: 0; } .att-section:last-child { margin-bottom: 0; }
.att-section-title { font-size: 12px; color: var(--text-secondary); margin-bottom: 8px; font-weight: 500; } .att-section-title { font-size: 12px; color: var(--text-secondary); margin-bottom: 8px; font-weight: 500; }
.att-section-title.not-submitted-title { color: #FF3B30; } .att-section-title.not-submitted-title { color: #FF3B30; }
.att-section-title.insufficient-title { color: #FF9500; }
.att-user-list { display: flex; flex-wrap: wrap; gap: 6px; } .att-user-list { display: flex; flex-wrap: wrap; gap: 6px; }
.att-user { .att-user {
font-size: 13px; padding: 4px 12px; border-radius: 6px; white-space: nowrap; font-size: 13px; padding: 4px 12px; border-radius: 6px; white-space: nowrap;
} }
.att-user.not-submitted { background: #FFF5F5; color: #CC2D25; font-weight: 500; } .att-user.not-submitted { background: #FFF5F5; color: #CC2D25; font-weight: 500; }
.att-user.insufficient { background: #FFF3E0; color: #D97706; font-weight: 500; }
.att-user.submitted { background: #F7F8FA; color: var(--text-regular); } .att-user.submitted { background: #F7F8FA; color: var(--text-regular); }
.att-user.submitted small { color: var(--text-secondary); margin-left: 2px; } .att-user.submitted small { color: var(--text-secondary); margin-left: 2px; }