gongxiangp2/lib/performance-salary-calculation.ts

74 lines
3.7 KiB
TypeScript

import type { SalaryRole } from './performance-salary-roles'
export function resolveSalarySourceMonth(displayMonth: string, submittedCount: number, environment: string | undefined, enabled: string | undefined) {
const isFallback = environment === 'development' && enabled === 'true' && displayMonth === '2026-10' && submittedCount === 0
return { displayMonth, sourceMonth: isFallback ? '2026-09' : displayMonth, isFallback }
}
export function salaryRoleForPosition(code: string): SalaryRole | null {
if (code.startsWith('ai_planning_')) return 'ai-planning'
if (code === 'director') return 'ai-planning'
if (code === 'ai_technical_designer') return 'ai-technical'
if (['camera', 'editing', 'retouching'].includes(code)) return 'photography'
if (code === 'brand_designer') return 'spatial-visual'
return null
}
export const roundSalaryScore = (value: number) => Math.round((value + Number.EPSILON) * 100) / 100
export function averageSalaryScores(values: Array<number | null | undefined>): number | null {
const scores = values.filter((value): value is number => typeof value === 'number' && Number.isFinite(value))
return scores.length ? scores.reduce((sum, score) => sum + score, 0) / scores.length : null
}
type Section = { weight: number | null; metrics: Record<string, number> }
// Keep the percentages in the incumbent salary template, without normalizing them.
export const SALARY_SCORE_WEIGHTS: Record<SalaryRole, Section[]> = {
'ai-planning': [
{ weight: .25, metrics: { brandReview: 1, collaboration: 0 } },
{ weight: .7, metrics: { marketValidation: .1, onTime: .9, sharing: 0 } },
{ weight: .05, metrics: { cooperation: 1 } },
],
photography: [], // The photography template does not yet define assessment weights.
'ai-technical': [],
'spatial-visual': [
{ weight: .2, metrics: { aiTools: .16, collaboration: .04 } },
// The source workbook leaves this section's weight blank; do not infer 75%.
{ weight: null, metrics: { onTime: .9, sharing: 0 } },
{ weight: .05, metrics: { cooperation: .06 } },
],
management: [
{ weight: .7, metrics: { visualTone: .8, collaboration: .2 } },
{ weight: .2, metrics: { marketValidation: .4, onTime: .4, sharing: .2 } },
{ weight: .1, metrics: { cooperation: 1 } },
],
}
export function calculateSalaryPerformance(role: SalaryRole, metrics: Record<string, number | null>) {
const sections = SALARY_SCORE_WEIGHTS[role]
const weighted: Record<string, number | null> = {}
let total = 0, complete = sections.length > 0
for (const section of sections) {
if (section.weight === null) complete = false
let subtotal = 0
for (const [key, share] of Object.entries(section.metrics)) {
const value = metrics[key]
weighted[key] = typeof value === 'number' && Number.isFinite(value) ? roundSalaryScore(value * share) : null
if (share === 0 || section.weight === 0) continue
if (weighted[key] === null) complete = false
else subtotal += value! * share
}
if (section.weight !== null) total += subtotal * section.weight
}
return { weighted, totalScore: complete ? roundSalaryScore(total) : null }
}
export function salaryMetricForSurvey(code: string, snapshot: unknown): string | null {
const definition = snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot) ? snapshot as Record<string, unknown> : {}
if (definition.surveyType === 'visual_case' || ['visual_brand_tone', 'visual_brand_tone_without_copy'].includes(code)) return 'visualTone'
const known: Record<string, string> = {
cross_department: 'collaboration', work_cooperation: 'cooperation', course_share: 'sharing',
project_result_validation: 'marketValidation', ai_work_review: 'aiTools',
}
return known[code] ?? null
}