147 lines
9.2 KiB
TypeScript
147 lines
9.2 KiB
TypeScript
import { SALARY_ROLES, type SalaryRole } from './performance-salary-roles'
|
||
import type { SalaryPosition } from './salary-position-contract'
|
||
import { percentageBasisPoints } from './salary-position-contract'
|
||
|
||
export const SALARY_TEMPLATE_BLANK_ROWS = 5
|
||
export const SALARY_PATH = '/workspaces/organization/performance-review'
|
||
|
||
export type SalaryTemplateColumn = {
|
||
key: string
|
||
label: string
|
||
kind: 'text' | 'money' | 'number' | 'percent'
|
||
width: number
|
||
}
|
||
export type SalaryTemplateGroup = { key: string; label: string; columns: SalaryTemplateColumn[] }
|
||
export type SalaryTemplate = {
|
||
role: SalaryRole
|
||
title: string
|
||
month: string
|
||
grouped: boolean
|
||
groups: SalaryTemplateGroup[]
|
||
columns: SalaryTemplateColumn[]
|
||
}
|
||
|
||
export function currentSalaryMonth(now = new Date()): string {
|
||
const parts = new Intl.DateTimeFormat('en-US', {
|
||
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit',
|
||
}).formatToParts(now)
|
||
return `${parts.find(part => part.type === 'year')!.value}-${parts.find(part => part.type === 'month')!.value}`
|
||
}
|
||
|
||
export function isSalaryMonth(value: unknown): value is string {
|
||
return typeof value === 'string' && /^(?!0000)\d{4}-(0[1-9]|1[0-2])$/.test(value)
|
||
}
|
||
|
||
export function resolveSalaryMonth(value: unknown, now = new Date()): string {
|
||
return isSalaryMonth(value) ? value : currentSalaryMonth(now)
|
||
}
|
||
|
||
export function salaryMonthLabel(month: string, offset = 0): string {
|
||
const [year, number] = month.split('-').map(Number)
|
||
const index = year * 12 + number - 1 + offset
|
||
return `${Math.floor(index / 12)}年${((index % 12) + 12) % 12 + 1}月`
|
||
}
|
||
|
||
export function salaryDetailsHref(role: SalaryRole, month: string): string {
|
||
return `${SALARY_PATH}?${new URLSearchParams({ role, month })}`
|
||
}
|
||
|
||
const column = (key: string, label: string, kind: SalaryTemplateColumn['kind'] = 'text', width = 144): SalaryTemplateColumn => ({ key, label, kind, width })
|
||
const group = (key: string, label: string, columns: SalaryTemplateColumn[]): SalaryTemplateGroup => ({ key, label, columns })
|
||
const identity = (brand = false) => [
|
||
column('name', '姓名', 'text', 104), column('nickname', '花名', 'text', 104),
|
||
column('department', '部门', 'text', 160), ...(brand ? [column('brand', '品牌', 'text', 136)] : []),
|
||
]
|
||
const baseWages = () => [column('baseSalary', '基本工资', 'money'), column('gradeSalary', '职级工资', 'money'), column('performanceSalary', '绩效工资', 'money')]
|
||
const review = (key: string, label: string, overallShare: string) => [
|
||
column(`${key}Score`, `${label} · 考核得分(综合占比${overallShare})`, 'number', 200),
|
||
]
|
||
const aiSharing = () => review('sharing', '知识分享/月度复盘分享', '0%')
|
||
|
||
export function getSalaryTemplate(role: SalaryRole, month: string): SalaryTemplate {
|
||
const current = salaryMonthLabel(month)
|
||
const previous = salaryMonthLabel(month, -1)
|
||
const beforePrevious = salaryMonthLabel(month, -2)
|
||
const salarySummary = () => group('salary', '薪资', [
|
||
column('currentTotal', `${current}合计总薪资`, 'money', 184),
|
||
column('previousTotal', `${previous}合计总薪资`, 'money', 184),
|
||
column('salaryDifference', '基础薪资差值', 'money', 160),
|
||
])
|
||
const settlement = () => group('settlement', '绩效结算', [column('performanceScore', '绩效得分', 'number')])
|
||
const remarks = () => group('remarks', '备注', [column('remarks', '备注', 'text', 224)])
|
||
const templates: Record<SalaryRole, SalaryTemplateGroup[]> = {
|
||
'ai-technical': [],
|
||
'ai-planning': [
|
||
group('identity', '基础信息', [...identity(true), ...baseWages()]),
|
||
group('points', '积分', [column('points', `${current}积分`, 'number', 160)]),
|
||
group('qualitative', '定性 · 维度权重25%', [
|
||
...review('brandReview', '品牌调性评审', '25%'), ...review('collaboration', '跨部门协同', '0%'),
|
||
]),
|
||
group('quantitative', '定量 · 维度权重70%', [
|
||
...review('marketValidation', '创意成果市场验证', '7%'),
|
||
...review('onTime', '项目按时交付率', '63%'), ...aiSharing(),
|
||
]),
|
||
group('cooperation', '配合度 · 维度权重5%', review('cooperation', '工作配合度', '5%')),
|
||
settlement(), salarySummary(), remarks(),
|
||
],
|
||
photography: [group('photography', '', [
|
||
column('nickname', '花名', 'text', 104), column('level', 'P级', 'text', 104), column('position', '岗位', 'text', 160), column('department', '部门', 'text', 160),
|
||
column('baseSalaryBasis', '基础薪资基数', 'money', 160), column('baseSalary', '基础薪资', 'money'),
|
||
column('performanceSalaryBasis', '绩效薪资基数', 'money', 160), column('performanceCoefficient', '绩效系数', 'number'),
|
||
column('performanceSalary', '绩效薪资', 'money'), column('bonus', '奖金工资', 'money'), column('fixedPay', '固定薪酬', 'number'),
|
||
column('currentTotal', `${current}合计薪资`, 'money', 184), column('previousTotal', `${previous}合计薪资`, 'money', 184),
|
||
column('beforePreviousTotal', `${beforePrevious}合计薪资`, 'money', 184), column('monthOverMonth', '环比', 'percent'), column('remarks', '备注', 'text', 224),
|
||
])],
|
||
'spatial-visual': [
|
||
group('identity', '基础信息', [...identity(), ...baseWages()]),
|
||
group('qualitative', '定性 · 维度权重20%', [
|
||
...review('aiTools', '协助搭建部门AI辅助工具', '3.2%'), ...review('collaboration', '跨部门协同', '0.8%'),
|
||
]),
|
||
group('quantitative', '定量', [
|
||
...review('onTime', '项目按时交付率', '待配置'), ...aiSharing(),
|
||
]),
|
||
group('cooperation', '配合度 · 维度权重5%', review('cooperation', '工作配合度', '0.3%')),
|
||
settlement(), salarySummary(), remarks(),
|
||
],
|
||
management: [
|
||
group('identity', '人员信息', identity()),
|
||
group('qualitative', '定性 · 维度权重70%', [
|
||
...review('visualTone', '视觉调性', '56%'), ...review('collaboration', '跨部门协同', '14%'),
|
||
]),
|
||
group('quantitative', '定量 · 维度权重20%', [
|
||
...review('marketValidation', '创意成果市场验证', '8%'), ...review('onTime', '项目按时交付率', '8%'),
|
||
...review('sharing', '知识分享/项目复盘', '4%'),
|
||
]),
|
||
group('satisfaction', '满意度 · 维度权重10%', review('cooperation', '工作配合度', '10%')),
|
||
group('settlement', '绩效得分', [column('performanceScore', `${current}绩效得分`, 'number', 184)]),
|
||
group('salary', '薪资', [
|
||
column('salaryBasis', '基础薪资', 'money'), column('baseSalary', '基本工资', 'money'), column('gradeSalary', '职级工资', 'money'),
|
||
column('performanceSalary', '绩效薪资', 'money'), column('performanceCoefficient', '绩效系数', 'number'),
|
||
column('currentTotal', `${current}新薪资`, 'money', 184), column('previousTotal', `${previous}薪资`, 'money', 184),
|
||
]),
|
||
],
|
||
}
|
||
templates['ai-technical'] = [
|
||
group('identity', '基础信息', [...identity(true), ...baseWages()]),
|
||
group('qualitative', '定性 · 维度权重25%', [...review('maintenance', '项目维护', '12.5%'), ...review('projectResult', '项目成果验证', '12.5%')]),
|
||
group('quantitative', '定量 · 维度权重70%', [...review('knowledge', 'AI知识库建设', '0%'), ...review('onTime', '项目按时交付率', '42%'), ...review('sharing', '知识分享/月度复盘分享', '28%')]),
|
||
group('cooperation', '配合度 · 维度权重5%', review('cooperation', '工作配合满意度', '5%')),
|
||
settlement(), salarySummary(), remarks(),
|
||
]
|
||
const groups = templates[role]
|
||
return { role, title: SALARY_ROLES.find(item => item.key === role)!.title, month, grouped: role !== 'photography', groups, columns: groups.flatMap(item => item.columns) }
|
||
}
|
||
|
||
export function getPositionSalaryTemplate(position: SalaryPosition, month: string): SalaryTemplate {
|
||
const base = getSalaryTemplate(position.salaryRole, month)
|
||
const prefix: SalaryTemplateGroup[] = [group('identity', '基础信息', [...identity(true), ...baseWages()])]
|
||
if (position.salaryRole === 'ai-planning') prefix.push(group('points', '积分', [column('points', `${salaryMonthLabel(month)}积分`, 'number', 160)]))
|
||
const assessment = position.dimensions.map(dimension => group(dimension.id, `${dimension.name} · 维度权重${dimension.weight}%`, dimension.metrics.flatMap(metric =>
|
||
review(metric.id, metric.name, `${Number((percentageBasisPoints(dimension.weight) * percentageBasisPoints(metric.weight) / 1e6).toFixed(6))}%`))))
|
||
const summary = position.salaryRole === 'photography'
|
||
? [group('settlement', '绩效得分', [column('performanceScore', '绩效得分', 'number')]), group('salary', '薪资', base.columns.filter(item => ['performanceCoefficient', 'bonus', 'fixedPay', 'currentTotal', 'previousTotal', 'beforePreviousTotal', 'monthOverMonth'].includes(item.key))), group('remarks', '备注', [column('remarks', '备注', 'text', 224)])]
|
||
: base.groups.filter(item => ['settlement', 'salary', 'remarks'].includes(item.key)).map(item => ({ ...item, columns: item.columns.filter(item => !['baseSalary', 'gradeSalary', 'performanceSalary'].includes(item.key)) }))
|
||
const groups = [...prefix, ...assessment, ...summary]
|
||
return { ...base, title: position.name, grouped: true, groups, columns: groups.flatMap(item => item.columns) }
|
||
}
|