74 lines
3.7 KiB
TypeScript
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
|
|
}
|