console
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>门店薪酬与利润核算系统 · 完整版</title>
<style>
*{box-sizing:border-box;}
body{font-family:"Microsoft YaHei",sans-serif;margin:0;padding:10px;background:#f0f2f5;color:#333;font-size:13px;}
.wrap{max-width:1800px;margin:auto;background:#fff;padding:14px;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.08);}
h1{font-size:19px;text-align:center;background:linear-gradient(135deg,#1e3a5f,#0d47a1);color:#fff;padding:14px;margin:-14px -14px 16px -14px;border-radius:10px 10px 0 0;letter-spacing:1px;}
h2{font-size:15px;margin:22px 0 10px 0;padding-left:10px;border-left:5px solid #ff9800;color:#1e3a5f;}
h3{font-size:14px;color:#1e3a5f;margin:14px 0 8px;}
.scroll{overflow-x:auto;width:100%;}
.hero{background:linear-gradient(135deg,#1e3a5f 0%,#1565c0 100%);border-radius:12px;padding:20px;color:#fff;margin-bottom:18px;box-shadow:0 6px 18px rgba(0,0,0,0.18);}
.hero-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:14px;}
.hero-item{text-align:center;padding:10px;background:rgba(255,255,255,0.08);border-radius:8px;}
.hero-item .label{font-size:12px;opacity:0.85;margin-bottom:4px;}
.hero-item .value{font-size:24px;font-weight:bold;color:#ffeb3b;text-shadow:1px 1px 3px rgba(0,0,0,0.3);}
.hero-item .sub{font-size:11px;opacity:0.8;margin-top:3px;}
.hero-tier{background:rgba(255,235,59,0.15);border:2px dashed #ffeb3b;border-radius:8px;padding:10px;text-align:center;font-size:13px;}
.hero-tier b{color:#ffeb3b;font-size:16px;}
table{width:100%;border-collapse:collapse;font-size:12px;background:#fff;}
th,td{border:1px solid #d6dde5;padding:6px 4px;text-align:center;vertical-align:middle;}
th{background:#e8eef5;color:#1e3a5f;font-weight:bold;white-space:nowrap;}
tbody tr:nth-child(even){background:#f8fafc;}
tbody tr:hover{background:#fff8e1;}
input[type=number],input[type=text]{width:100%;padding:5px 3px;border:1px solid #90caf9;background:#e3f2fd;border-radius:4px;font-size:12px;text-align:center;color:#0d47a1;font-weight:bold;}
input:focus{outline:none;border-color:#1e3a5f;background:#fff;box-shadow:0 0 5px rgba(30,58,95,0.2);}
input:disabled{background:#f0f0f0!important;color:#b0b0b0!important;border-color:#ddd!important;cursor:not-allowed;}
select{padding:5px 3px;border:1px solid #90caf9;background:#e3f2fd;border-radius:4px;font-size:12px;color:#0d47a1;font-weight:bold;text-align:center;}
.hl-yellow{background:#fff9c4!important;color:#bf360c!important;font-weight:bold;}
.hl-green{background:#e8f5e9!important;color:#2e7d32!important;font-weight:bold;}
.hl-blue{background:#e3f2fd!important;color:#1565c0!important;font-weight:bold;}
.hl-orange{background:#ffe0b2!important;color:#e65100!important;font-weight:bold;}
.hl-red{background:#ffebee!important;color:#c62828!important;font-weight:bold;}
.param-card{background:#f8fafc;border:1px solid #cfd8dc;border-radius:8px;padding:12px;margin-bottom:12px;}
.param-card table td{border:none;padding:5px 4px;text-align:left;}
.param-card input{width:110px;}
.param-card .p-label{font-size:12px;color:#455a64;}
.toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;background:#fff3e0;border:2px solid #ff9800;border-radius:8px;padding:10px 12px;margin-bottom:12px;font-size:13px;}
.toolbar .item{display:flex;align-items:center;gap:6px;color:#bf360c;font-weight:bold;}
.toolbar .item input{width:120px;background:#fff;}
.btn{display:block;width:100%;padding:12px;background:#ff9800;color:#fff;text-align:center;font-size:15px;font-weight:bold;border:none;border-radius:8px;margin-bottom:14px;cursor:pointer;box-shadow:0 3px 8px rgba(0,0,0,0.15);}
.btn:hover{background:#fb8c00;}
.btn-sm{display:inline-block;padding:6px 12px;background:#ff9800;color:#fff;border:none;border-radius:5px;font-size:12px;font-weight:bold;cursor:pointer;margin-right:4px;}
.btn-sm:hover{background:#fb8c00;}
.btn-green{background:#43a047;} .btn-green:hover{background:#2e7d32;}
.btn-red{background:#e53935;} .btn-red:hover{background:#c62828;}
.btn-blue{background:#1e88e5;} .btn-blue:hover{background:#1565c0;}
.tip{background:#e3f2fd;border-left:4px solid #2196f3;padding:10px 14px;border-radius:4px;font-size:12px;line-height:1.7;color:#1565c0;margin-bottom:12px;}
.tip b{color:#0d47a1;}
.ok-box{background:#e8f5e9;border:2px solid #43a047;border-radius:6px;padding:8px 12px;font-size:12px;color:#2e7d32;font-weight:bold;margin-bottom:10px;}
.err-box{background:#ffebee;border:2px solid #e53935;border-radius:6px;padding:8px 12px;font-size:12px;color:#c62828;font-weight:bold;margin-bottom:10px;}
.fin-table td:first-child{text-align:left;padding-left:16px;}
.fin-table .amt{text-align:right;font-weight:bold;color:#c62828;font-size:14px;}
.fin-table .total td{background:#e0f2f1;font-size:16px;font-weight:bold;color:#004d40;}
.fin-table .sub td{background:#fafafa;padding-left:36px;color:#555;font-weight:normal;}
.tier-row-active{background:#c8e6c9!important;box-shadow:inset 0 0 0 2px #2e7d32;}
.tier-row-active td{font-weight:bold;}
.speech{background:#fff8e1;border:2px solid #ffb300;border-radius:10px;padding:16px;margin-top:12px;font-size:13px;line-height:2;}
.speech-quote{background:#fff;border-left:5px solid #ff9800;padding:12px 16px;margin:8px 0;color:#1e3a5f;font-size:14px;line-height:2;border-radius:4px;}
.data-table{min-width:2600px;}
.data-table th{font-size:11px;padding:5px 3px;}
.data-table td{font-size:11px;}
.data-table input{font-size:11px;padding:4px 2px;}
.ratio-bar{display:flex;justify-content:space-between;align-items:center;background:#fff3e0;border:2px solid #ff9800;border-radius:6px;padding:8px 12px;margin-top:8px;font-size:12px;font-weight:bold;}
.ratio-bar .lbl{color:#bf360c;}
.ratio-bar .val{font-size:14px;}
.rank-1{background:#ffd700!important;color:#b71c1c!important;font-weight:bold;}
.rank-2{background:#e0e0e0!important;color:#333!important;font-weight:bold;}
.rank-3{background:#ffb74d!important;color:#4e342e!important;font-weight:bold;}
.rule-table td{text-align:left;font-size:12px;line-height:1.6;}
.rule-table td:first-child{text-align:center;font-weight:bold;background:#f8fafc;}
.calc-detail{font-size:10px;color:#666;font-weight:normal;display:block;margin-top:2px;}
.derive-box{background:#f8fafc;border:1px solid #cfd8dc;border-radius:8px;padding:14px;line-height:2.2;font-size:13px;}
.derive-box .row{padding:2px 0;}
.derive-box .plus{color:#2e7d32;font-weight:bold;}
.derive-box .minus{color:#c62828;font-weight:bold;}
.derive-box .sep{border-top:2px dashed #999;margin:8px 0;padding-top:8px;}
.derive-box .result{background:#fff3e0;padding:10px;border-radius:6px;border:2px solid #ff9800;margin-top:8px;}
@media(max-width:768px){
.hero-item .value{font-size:18px;}
h1{font-size:15px;}
}
</style>
</head>
<body>
<div class="wrap">
<button class="btn" onclick="downloadPage()">⬇️ 保存为本地文件</button>
<h1>🏆 门店薪酬与利润核算系统 · 完整版</h1>
<div class="hero">
<div class="hero-grid">
<div class="hero-item"><div class="label">老板最终到手纯利</div><div class="value" id="hero_profit">¥0</div><div class="sub" id="hero_margin">利润率 0%</div></div>
<div class="hero-item"><div class="label">当月核销总业绩</div><div class="value" id="hero_revenue">¥0</div><div class="sub">充卡不计营收</div></div>
<div class="hero-item"><div class="label">全店总提成率</div><div class="value" id="hero_comm_rate" style="color:#ffb74d;">0%</div><div class="sub" id="hero_comm_amt">总提成 ¥0</div></div>
<div class="hero-item"><div class="label">员工实发工资合计</div><div class="value" id="hero_salary" style="color:#81c784;">¥0</div><div class="sub" id="hero_avg">人均 ¥0</div></div>
</div>
<div class="hero-tier" id="hero_tier">当前档位:<b>未达任何档位</b> | 提点加成:<b>+0%</b></div>
</div>
<h2>一、门店基础参数</h2>
<div class="param-card">
<table>
<tr>
<td class="p-label">当月总业绩(不含充卡)</td><td><input type="number" id="p_revenue" value="80000" oninput="calc()"></td>
<td class="p-label">变动成本率</td><td><input type="number" id="p_var_rate" value="15" oninput="calc()"> %</td>
<td class="p-label">房租</td><td><input type="number" id="p_rent" value="6000" oninput="calc()"></td>
</tr>
<tr>
<td class="p-label">其他固定成本</td><td><input type="number" id="p_other" value="0" oninput="calc()"></td>
<td class="p-label">每人固定补贴</td><td><input type="number" id="p_subsidy" value="500" oninput="calc()"></td>
<td class="p-label">利润分红提取比例</td><td><input type="number" id="p_pool_rate" value="30" oninput="calc()"> %</td>
</tr>
<tr>
<td class="p-label">发型组分红池占比</td><td><input type="number" id="p_hair_share" value="60" oninput="calc()"> %</td>
<td class="p-label">洗护组分红池占比</td><td><input type="number" id="p_wash_share" value="40" oninput="calc()"> %</td>
<td class="p-label">指定客目标占比</td><td><input type="number" id="p_zd_target" value="30" oninput="calc()"> %</td>
</tr>
</table>
</div>
<h2>二、项目单价、提点与项目占比</h2>
<div class="tip">
<b>项目占比的作用:</b>① 一键均分业绩时按占比分配到每位员工 ② 校验面板按"总业绩 × 占比"核算门店该项目营收。<br>
<b>💡 双人合作项目说明:</b>如洗剪吹,客人消费 50 元,发型师和洗护师都各记 50 元业绩,各自按自己提点算提成,所以两边加总 > 门店营收是正常的。
</div>
<div class="scroll">
<table>
<thead><tr><th>项目</th><th>单价</th><th>项目占比</th><th>发型师提点</th><th>洗护师提点</th><th>助理提点</th></tr></thead>
<tbody>
<tr><td>洗剪吹</td><td><input type="number" id="pr_sc" value="50" oninput="calc()"></td><td class="hl-yellow"><input type="number" id="r_sc" value="40" oninput="calc()"> %</td><td><input type="number" id="h_sc" value="35" oninput="calc()">%</td><td><input type="number" id="w_sc" value="25" oninput="calc()">%</td><td><input type="number" id="a_sc" value="0" oninput="calc()">%</td></tr>
<tr><td>洗吹</td><td><input type="number" id="pr_cw" value="30" oninput="calc()"></td><td class="hl-yellow"><input type="number" id="r_cw" value="10" oninput="calc()"> %</td><td><input type="number" id="h_cw" value="33" oninput="calc()">%</td><td><input type="number" id="w_cw" value="25" oninput="calc()">%</td><td><input type="number" id="a_cw" value="0" oninput="calc()">%</td></tr>
<tr><td>吹风洗头疗</td><td><input type="number" id="pr_cf" value="40" oninput="calc()"></td><td class="hl-yellow"><input type="number" id="r_cf" value="5" oninput="calc()"> %</td><td><input type="number" id="h_cf" value="15" oninput="calc()">%</td><td><input type="number" id="w_cf" value="20" oninput="calc()">%</td><td><input type="number" id="a_cf" value="0" oninput="calc()">%</td></tr>
<tr><td>头疗加剪发</td><td><input type="number" id="pr_tljf" value="150" oninput="calc()"></td><td class="hl-yellow"><input type="number" id="r_tljf" value="5" oninput="calc()"> %</td><td><input type="number" id="h_tljf" value="15" oninput="calc()">%</td><td><input type="number" id="w_tljf" value="30" oninput="calc()">%</td><td><input type="number" id="a_tljf" value="0" oninput="calc()">%</td></tr>
<tr><td>烫染</td><td><input type="number" id="pr_tr" value="200" oninput="calc()"></td><td class="hl-yellow"><input type="number" id="r_tr" value="30" oninput="calc()"> %</td><td><input type="number" id="h_tr" value="30" oninput="calc()">%</td><td><input type="number" id="w_tr" value="0" oninput="calc()">%</td><td><input type="number" id="a_tr" value="18" oninput="calc()">%</td></tr>
<tr><td>单人头疗</td><td><input type="number" id="pr_tl" value="100" oninput="calc()"></td><td class="hl-yellow"><input type="number" id="r_tl" value="10" oninput="calc()"> %</td><td><input type="number" id="h_tl" value="0" oninput="calc()">%</td><td><input type="number" id="w_tl" value="30" oninput="calc()">%</td><td><input type="number" id="a_tl" value="0" oninput="calc()">%</td></tr>
<tr><td>造型单人</td><td><input type="number" id="pr_zx" value="80" oninput="calc()"></td><td class="hl-yellow"><input type="number" id="r_zx" value="0" oninput="calc()"> %</td><td><input type="number" id="h_zx" value="25" oninput="calc()">%</td><td><input type="number" id="w_zx" value="0" oninput="calc()">%</td><td><input type="number" id="a_zx" value="10" oninput="calc()">%</td></tr>
<tr><td>外卖单人</td><td><input type="number" id="pr_wm" value="50" oninput="calc()"></td><td class="hl-yellow"><input type="number" id="r_wm" value="0" oninput="calc()"> %</td><td><input type="number" id="h_wm" value="10" oninput="calc()">%</td><td><input type="number" id="w_wm" value="10" oninput="calc()">%</td><td><input type="number" id="a_wm" value="5" oninput="calc()">%</td></tr>
</tbody>
</table>
</div>
<div class="ratio-bar"><span class="lbl">📊 项目占比合计:</span><span class="val" id="ratio_sum">-</span></div>
<h2>三、充卡与指定客参数</h2>
<div class="scroll">
<table>
<thead><tr><th>参数</th><th>填写值</th><th>含义</th><th>实时举例</th></tr></thead>
<tbody>
<tr><td class="hl-orange">充卡业绩占比</td><td><input type="number" id="r_card" value="15" oninput="calc()"> %</td><td style="text-align:left;font-size:11px;">全店充卡业绩 = 总业绩 × 占比</td><td id="ex_card_ratio" style="text-align:left;font-size:11px;color:#0d47a1;">-</td></tr>
<tr><td class="hl-orange">指定客业绩占比</td><td><input type="number" id="r_zd" value="20" oninput="calc()"> %</td><td style="text-align:left;font-size:11px;">全店指定客业绩 = 总业绩 × 占比</td><td id="ex_zd_ratio" style="text-align:left;font-size:11px;color:#0d47a1;">-</td></tr>
<tr><td class="hl-orange">充卡折算比例</td><td><input type="number" id="p_card_perf_rate" value="100" oninput="calc()"> %</td><td style="text-align:left;font-size:11px;">100% = 充多少算多少;80% = 打8折算</td><td id="ex_card_perf" style="text-align:left;font-size:11px;color:#0d47a1;">-</td></tr>
<tr><td class="hl-orange">充卡提点</td><td><input type="number" id="p_card_comm" value="5" oninput="calc()"> %</td><td style="text-align:left;font-size:11px;">5% = 卖 1000 元卡拿 50 元</td><td id="ex_card_comm" style="text-align:left;font-size:11px;color:#0d47a1;">-</td></tr>
<tr><td class="hl-orange">指定客提点</td><td><input type="number" id="p_zd_comm" value="3" oninput="calc()"> %</td><td style="text-align:left;font-size:11px;">3% = 做 1000 元拿 30 元</td><td id="ex_zd_comm" style="text-align:left;font-size:11px;color:#0d47a1;">-</td></tr>
</tbody>
</table>
</div>
<h2>四、业绩阶梯提点</h2>
<div class="toolbar">
<div class="item">🎯 本月实际完成业绩 <input type="number" id="actual_revenue" value="80000" oninput="calc()"> 元</div>
<div class="item" style="background:#e8f5e9;color:#2e7d32;padding:6px 12px;border-radius:5px;border:2px solid #43a047;" id="tier_match">当前匹配:未达档位</div>
<button class="btn-sm btn-green" onclick="addTier()">+ 增加档位</button>
</div>
<div class="scroll">
<table>
<thead><tr><th>档位名称</th><th>业绩门槛</th><th>提点加成</th><th>效果预览</th><th>操作</th></tr></thead>
<tbody id="tier_body"></tbody>
</table>
</div>
<h2>五、员工明细</h2>
<div class="toolbar" style="background:#e8f5e9;border-color:#43a047;">
<div class="item" style="color:#1b5e20;">
👥 人数:发型师 <input type="number" id="n_hair" value="3" min="0" style="width:55px;" onchange="syncHeadcount()"> 人
洗护师 <input type="number" id="n_wash" value="3" min="0" style="width:55px;" onchange="syncHeadcount()"> 人
助理 <input type="number" id="n_assist" value="1" min="0" style="width:55px;" onchange="syncHeadcount()"> 人
</div>
<button class="btn-sm btn-green" onclick="addStaff('hair')">+ 发型师</button>
<button class="btn-sm btn-green" onclick="addStaff('wash')">+ 洗护师</button>
<button class="btn-sm btn-green" onclick="addStaff('assist')">+ 助理</button>
<button class="btn-sm btn-blue" onclick="autoAllocate()">🎲 一键均分业绩</button>
</div>
<div class="scroll">
<table class="data-table">
<thead>
<tr>
<th rowspan="2">姓名</th><th rowspan="2">岗位</th>
<th colspan="2">洗剪吹</th><th colspan="2">洗吹</th><th colspan="2">吹风洗</th>
<th colspan="2">头疗剪发</th><th colspan="2">烫染</th><th colspan="2">头疗</th>
<th colspan="2">造型</th><th colspan="2">外卖</th>
<th rowspan="2">计件<br>基数</th><th rowspan="2">项目<br>提成</th>
<th rowspan="2">充卡<br>现金</th><th rowspan="2">充卡<br>提成</th>
<th rowspan="2">指定客<br>业绩</th><th rowspan="2">指定客<br>提成</th>
<th rowspan="2">总提成</th>
<th rowspan="2">散客<br>现金</th>
<th colspan="5">考核分</th>
<th rowspan="2">总分</th><th rowspan="2">排名</th><th rowspan="2">分红工资</th><th rowspan="2">实发工资</th>
<th rowspan="2">操作</th>
</tr>
<tr>
<th>业绩</th><th>提成</th><th>业绩</th><th>提成</th><th>业绩</th><th>提成</th>
<th>业绩</th><th>提成</th><th>业绩</th><th>提成</th><th>业绩</th><th>提成</th>
<th>业绩</th><th>提成</th><th>业绩</th><th>提成</th>
<th>劳动<br>(30)</th><th>充卡<br>(20)</th><th>指定<br>(20)</th><th>口碑<br>(10)</th><th>管理<br>(20)</th>
</tr>
</thead>
<tbody id="staff_body"></tbody>
</table>
</div>
<!-- ★★★ 五-B:分红考核机制详解(新增) ★★★ -->
<h2>五-B、分红考核机制详解(员工一看就懂)</h2>
<div class="tip" style="font-size:13px;">
<b>一句话总结:</b>门店利润扣完成本后,按比例提取"分红池",再按每位员工的"考核分占组内总分的比例"分给每个人。<br>
<b style="color:#bf360c;">分数越高、排名越前 → 分红越多。排名只是给人看的,真正分钱是按"分数占比"算的。</b>
</div>
<div style="background:#e8f5e9;border:2px solid #43a047;border-radius:8px;padding:10px 14px;margin-bottom:12px;">
<label style="font-size:13px;font-weight:bold;color:#1b5e20;cursor:pointer;">
<input type="checkbox" id="auto_score_mode" onchange="toggleAutoMode()" style="width:auto;display:inline-block;margin-right:6px;transform:scale(1.2);">
⚡ 启用自动算分(劳动/充卡/指定分由系统按规则自动算,只留口碑/管理分手动填)
</label>
<div style="font-size:12px;color:#555;margin-top:6px;">
关闭时,5 项分全部手动填(原逻辑)。开启后,员工表里劳动分/充卡分/指定分的输入框会变灰自动算,你只需填口碑分和管理分。
</div>
</div>
<h3>📊 第一步:分红池是怎么算出来的(按当前真实数字)</h3>
<div class="derive-box" id="derive_pool"></div>
<h3>📋 第二步:考核分 5 项规则(怎么算的、拿满分的条件)</h3>
<table class="rule-table">
<thead>
<tr><th width="12%">项目</th><th width="8%">满分</th><th width="40%">计算规则</th><th width="40%">举例说明(数字演示)</th></tr>
</thead>
<tbody>
<tr>
<td>1. 劳动业绩分</td>
<td>30 分</td>
<td>组内第一名拿满 30 分,其余:<br><b>(个人业绩 ÷ 组内第一名业绩) × 30</b></td>
<td>发型组 A 做 5万(第一名),B 做 2.5万,C 做 1万:<br>A = (5万÷5万)×30 = <b>30 分</b><br>B = (2.5万÷5万)×30 = <b>15 分</b><br>C = (1万÷5万)×30 = <b>6 分</b></td>
</tr>
<tr>
<td>2. 充卡业绩分</td>
<td>20 分</td>
<td>组内第一名拿满 20 分,其余:<br><b>(个人充卡 ÷ 组内第一名充卡) × 20</b></td>
<td>A 充 1万(第一名),B 充 5千,C 充 0:<br>A = (1万÷1万)×20 = <b>20 分</b><br>B = (5千÷1万)×20 = <b>10 分</b><br>C = <b>0 分</b></td>
</tr>
<tr>
<td>3. 指定客占比分</td>
<td>20 分</td>
<td>达标拿满分 20,未达按比例:<br><b>(实际占比 ÷ 目标占比) × 20</b><br>实际占比 = 个人指定客业绩 ÷ 个人项目业绩</td>
<td>目标占比 30%:<br>A 实际 30% → 达标 = <b>20 分</b><br>B 实际 15% → (15%÷30%)×20 = <b>10 分</b><br>C 实际 5% → (5%÷30%)×20 = <b>3.33 分</b></td>
</tr>
<tr>
<td>4. 线上口碑分</td>
<td>10 分</td>
<td>底分 5 分,好评 +0.5/个,差评 -5/个。老板手动填。</td>
<td>A 有 4 个好评:5 + 4×0.5 = <b>7 分</b><br>B 有 1 个差评:5 - 5 = <b>0 分</b></td>
</tr>
<tr>
<td>5. 管理综合分</td>
<td>20 分</td>
<td>出勤、卫生、团队配合等,老板直接打分。</td>
<td>老板给 A 打 18 分,给 B 打 20 分(全勤)</td>
</tr>
</tbody>
</table>
<h3>🧮 第三步:实时算分演示(按当前员工真实数据)</h3>
<div class="tip">
💡 下面按规则自动算出每位员工的 5 项分和总分。<b>如果启用了自动算分,员工表里也是这个结果;如果没启用,下面是按规则算出的参考值,员工表里以你手动填的为准。</b>
</div>
<div class="scroll">
<table id="score_demo_table">
<thead>
<tr>
<th>组别</th><th>姓名</th>
<th>个人业绩<br>(计件基数)</th><th>劳动分<br>(满分30)</th>
<th>个人充卡</th><th>充卡分<br>(满分20)</th>
<th>指定客业绩</th><th>指定客占比</th><th>指定分<br>(满分20)</th>
<th>口碑分<br>(满分10)</th><th>管理分<br>(满分20)</th>
<th>总分</th>
</tr>
</thead>
<tbody id="score_demo_body"></tbody>
</table>
</div>
<h3>🏆 第四步:排名与分红(发型组、洗护组分别排名)</h3>
<div class="scroll">
<table id="rank_demo_table">
<thead>
<tr>
<th>组别</th><th>排名</th><th>姓名</th>
<th>总分</th><th>组内总分占比</th>
<th>组分红池</th><th>个人分红金额</th>
</tr>
</thead>
<tbody id="rank_demo_body"></tbody>
</table>
</div>
<h3>💡 第五步:第一名是怎么来的(举例详细说明)</h3>
<div id="first_place_explain" style="background:#fff8e1;border:2px solid #ffb300;border-radius:8px;padding:14px;line-height:2;font-size:13px;"></div>
<div class="tip" style="margin-top:12px;">
<b>📌 排名规则(123 名怎么分):</b><br>
• 按总分从高到低排,第 1 名、第 2 名、第 3 名...<br>
• <b>同分并列:</b>如果两人都是 85 分,两人都是第 1 名,下一名是第 3 名(如 85、85、70 → 第1、第1、第3)。<br>
• <b>排名只是给人看的激励,真正分钱是按"分数占比"算的。</b>即使排第 3 名,只要分数占比高,分红照样多。<br>
• <b>分红公式:</b>个人分红 = 组分红池 × (个人总分 ÷ 组内总分合计)。<br>
• <b>助理已纳入洗护组:</b>助理和洗护师一起排名、一起分洗护池。
</div>
<!-- 原有模块继续 -->
<h2>六、门店财务剥洋葱核算</h2>
<table class="fin-table">
<thead><tr><th width="45%">项目</th><th width="20%">金额 / 比例</th><th width="35%">说明</th></tr></thead>
<tbody>
<tr><td><b>1. 门店营业收入</b></td><td class="amt" id="f_revenue">0</td><td>门店划卡(充卡不算营收)</td></tr>
<tr><td>减去:变动成本</td><td class="amt" id="f_var">0</td><td>营收 × 变动成本率</td></tr>
<tr><td>减去:固定成本</td><td class="amt" id="f_fix">0</td><td>房租 + 其他固定</td></tr>
<tr><td><b>减去:全店员工总提成</b></td><td class="amt" id="f_total_comm" style="color:#bf360c;">0</td><td>项目提成 + 充卡提成 + 指定客提成</td></tr>
<tr class="sub"><td>其中:项目提成</td><td class="amt" id="f_item_comm">0</td><td>项目业绩 × 提点(含阶梯加成)</td></tr>
<tr class="sub"><td>其中:充卡提成</td><td class="amt" id="f_card_comm">0</td><td>充卡现金 × 折算 × 提点</td></tr>
<tr class="sub"><td>其中:指定客提成</td><td class="amt" id="f_zd_comm">0</td><td>指定客业绩 × 提点</td></tr>
<tr><td>减去:员工固定补贴</td><td class="amt" id="f_subsidy">0</td><td>人数 × 每人补贴</td></tr>
<tr style="background:#fff3e0;"><td><b>2. 门店基础纯利(分红前)</b></td><td class="amt" id="f_base_profit" style="color:#e65100;">0</td><td>营收 - 变动 - 固定 - 总提成 - 补贴</td></tr>
<tr><td>减去:实发分红</td><td class="amt" id="f_pool">0</td><td>基础纯利 × 分红比例</td></tr>
<tr class="total"><td><b>3. 老板最终到手纯利</b></td><td class="amt" id="f_final_profit">0</td><td>基础纯利 - 实发分红</td></tr>
<tr style="background:#e3f2fd;"><td><b>核心指标</b></td><td></td><td></td></tr>
<tr><td>门店利润率</td><td class="amt" id="f_margin" style="color:#2e7d32;">0%</td><td>老板纯利 ÷ 总业绩</td></tr>
<tr><td><b>全店总提成率</b></td><td class="amt" id="f_comm_rate" style="color:#c62828;">0%</td><td>总提成 ÷ 总业绩</td></tr>
<tr class="sub"><td>其中:项目提成率</td><td class="amt" id="f_item_rate">0%</td><td>项目提成 ÷ 总业绩</td></tr>
<tr class="sub"><td>其中:充卡提成率</td><td class="amt" id="f_card_rate">0%</td><td>充卡提成 ÷ 总业绩</td></tr>
<tr class="sub"><td>其中:指定客提成率</td><td class="amt" id="f_zd_rate">0%</td><td>指定客提成 ÷ 总业绩</td></tr>
<tr><td>全店新充现金总额</td><td class="amt" id="f_card_total" style="color:#1565c0;">0</td><td>所有员工充卡合计</td></tr>
</tbody>
</table>
<h2>七、员工工资对账</h2>
<div id="reconcile_box"></div>
<h2>八、个人实际提成点测算</h2>
<div class="toolbar" style="background:#f3e5f5;border-color:#8e24aa;">
<div class="item" style="color:#4a148c;">
测算门店业绩 <input type="number" id="sim_revenue" value="30000" oninput="renderPersonal()" style="width:120px;"> 元
| 分红池比例 <input type="number" id="sim_pool_rate" value="80" oninput="renderPersonal()" style="width:70px;"> %
| <label style="font-size:12px;"><input type="checkbox" id="sim_include_assist" checked onchange="renderPersonal()" style="width:auto;display:inline-block;margin-right:4px;"> 助理纳入洗护组分红</label>
</div>
</div>
<div class="scroll">
<table>
<thead>
<tr><th>姓名</th><th>岗位</th><th>计件基数</th><th>项目提成</th><th>充卡提成</th><th>指定客提成</th><th>总提成</th>
<th>总分</th><th>分红池</th><th>分红</th><th>补贴</th><th>实发工资</th><th>个人实际提成点</th><th>占门店业绩比</th></tr>
</thead>
<tbody id="personal_body"></tbody>
</table>
</div>
<div id="personal_summary"></div>
<h2>九、提成比例健康区间 + 一键缩放</h2>
<div class="toolbar" style="background:#e8f5e9;border-color:#43a047;">
<div class="item" style="color:#1b5e20;">
目标利润率 <input type="number" id="p_target_margin" value="20" oninput="calc()" style="width:60px;"> %
| 警戒利润率 <input type="number" id="p_warn_margin" value="10" oninput="calc()" style="width:60px;"> %
| 最低竞争力提成率 <input type="number" id="p_min_comm_rate" value="20" oninput="calc()" style="width:60px;"> %
</div>
</div>
<div class="scroll">
<table>
<thead>
<tr><th>业绩档位</th><th>固定成本率</th><th>盈亏平衡提成率</th><th>健康上限</th><th>警戒线</th>
<th>推荐区间</th><th>最推荐值</th><th>当前实际提成率</th><th>老板纯利</th><th>老板利润率</th><th>评级</th></tr>
</thead>
<tbody id="advice_body"></tbody>
</table>
</div>
<div class="toolbar" style="background:#fff3e0;border-color:#ff9800;margin-top:12px;">
<div class="item" style="color:#bf360c;">
目标总提成率 <input type="number" id="scale_target" placeholder="例如 32.5" style="width:100px;"> %
<button class="btn-sm btn-green" onclick="fillRecommend()">📋 填入当前推荐值</button>
<button class="btn-sm btn-red" onclick="scaleCommissions()">⚖️ 一键缩放项目提成</button>
</div>
</div>
<h2>十、面对应聘者直接照着说(自动生成)</h2>
<div id="speech_box" class="speech"></div>
<h2>十一、数据一致性校验</h2>
<div id="validate_box"></div>
</div>
<script id="tiers-data" type="application/json">[{"name":"第1档","threshold":0,"bonus":0},{"name":"第2档","threshold":30000,"bonus":1},{"name":"第3档","threshold":50000,"bonus":2},{"name":"第4档","threshold":80000,"bonus":3},{"name":"第5档","threshold":120000,"bonus":5}]</script>
<script>
window.onerror = function(msg, src, line){ console.error('错误:' + msg + ' 行 ' + line); return false; };
function v(id){ var el = document.getElementById(id); if(!el) return 0; var n = parseFloat(el.value); return isNaN(n) ? 0 : n; }
function fmt(n){ var num = Number(n); if(isNaN(num)) return '0.00'; return num.toLocaleString('zh-CN',{minimumFractionDigits:2,maximumFractionDigits:2}); }
function set(id, txt){ var el = document.getElementById(id); if(el) el.innerText = txt; }
var ITEMS = ['sc','cw','cf','tljf','tr','tl','zx','wm'];
var ROLE_ALLOW = {hair:['sc','cw','cf','tljf','tr','zx','wm'],wash:['sc','cw','cf','tljf','tl','wm'],assist:['tr','zx','wm']};
var ROLE_NAME = {hair:'发型师',wash:'洗护师',assist:'助理'};
var TIERS = [];
try { var td = document.getElementById('tiers-data'); if(td && td.textContent.trim()) TIERS = JSON.parse(td.textContent); } catch(e){}
if(!TIERS || TIERS.length === 0) TIERS = [
{name:'第1档',threshold:0,bonus:0},{name:'第2档',threshold:30000,bonus:1},
{name:'第3档',threshold:50000,bonus:2},{name:'第4档',threshold:80000,bonus:3},
{name:'第5档',threshold:120000,bonus:5}
];
function getBonusAt(r){ for(var i=TIERS.length-1;i>=0;i--) if(r>=TIERS[i].threshold) return TIERS[i].bonus; return 0; }
function getTierAt(r){ for(var i=TIERS.length-1;i>=0;i--) if(r>=TIERS[i].threshold) return TIERS[i]; return null; }
function renderTiers(){
var body = document.getElementById('tier_body'); if(!body) return;
var match = getTierAt(v('actual_revenue'));
var matchIdx = match ? TIERS.indexOf(match) : -1;
var hSc = v('h_sc');
var html = '';
for(var i=0;i<TIERS.length;i++){
var t = TIERS[i];
var cls = (i === matchIdx) ? 'tier-row-active' : '';
html += '<tr class="'+cls+'">' +
'<td><input type="text" value="'+t.name+'" onchange="updateTier('+i+',\'name\',this.value)" style="width:80px;"></td>' +
'<td><input type="number" value="'+t.threshold+'" onchange="updateTier('+i+',\'threshold\',this.value)"> 元起</td>' +
'<td><input type="number" value="'+t.bonus+'" onchange="updateTier('+i+',\'bonus\',this.value)"> %</td>' +
'<td style="font-size:11px;color:#555;">洗剪吹提点 '+hSc+'% → '+(hSc+t.bonus)+'%</td>' +
'<td><button class="btn-sm btn-red" onclick="removeTier('+i+')">删</button></td></tr>';
}
body.innerHTML = html;
var mBox = document.getElementById('tier_match');
if(mBox) mBox.innerHTML = match ? ('✅ 当前匹配:<b>'+match.name+'</b> | 加成 <b>+'+match.bonus+'%</b>') : '⚠️ 未达任何档位';
}
function updateTier(idx,key,val){
if(key === 'threshold' || key === 'bonus') TIERS[idx][key] = parseFloat(val) || 0;
else TIERS[idx][key] = val;
calc();
}
function addTier(){ var last = TIERS[TIERS.length-1]; TIERS.push({name:'第'+(TIERS.length+1)+'档', threshold:last?last.threshold+30000:0, bonus:0}); calc(); }
function removeTier(idx){ if(TIERS.length <= 1){ alert('至少保留一个档位'); return; } if(!confirm('确定删除?')) return; TIERS.splice(idx,1); calc(); }
function createRow(role, name){
var tr = document.createElement('tr');
var roleName = ROLE_NAME[role] || '发型师';
var cells = '';
for(var i=0;i<ITEMS.length;i++){
var k = ITEMS[i];
cells += '<td><input type="number" class="v_'+k+'" value="0" oninput="calc()"></td>' +
'<td class="c_'+k+'" style="background:#fff5f5;color:#c62828;font-weight:bold;">0.00</td>';
}
tr.innerHTML =
'<td><input type="text" class="v_name" value="'+(name||roleName)+'" oninput="calc()"></td>' +
'<td><select class="v_role" onchange="onRoleChange(this)">' +
'<option value="hair"'+(role==='hair'?' selected':'')+'>发型师</option>' +
'<option value="wash"'+(role==='wash'?' selected':'')+'>洗护师</option>' +
'<option value="assist"'+(role==='assist'?' selected':'')+'>助理</option></select></td>' +
cells +
'<td class="out_base" style="background:#fff3e0;font-weight:bold;">0.00</td>' +
'<td class="out_item_comm" style="background:#ffe0b2;font-weight:bold;color:#bf360c;">0.00</td>' +
'<td><input type="number" class="v_card" value="0" oninput="calc()"></td>' +
'<td class="out_card_comm" style="background:#e8f5e9;color:#2e7d32;font-weight:bold;">0.00</td>' +
'<td><input type="number" class="v_zd_perf" value="0" oninput="calc()"></td>' +
'<td class="out_zd_comm" style="background:#e8f5e9;color:#2e7d32;font-weight:bold;">0.00</td>' +
'<td class="out_total_comm" style="background:#ffe0b2;color:#bf360c;font-weight:bold;">0.00</td>' +
'<td><input type="number" class="v_cash" value="0" oninput="calc()"></td>' +
'<td><input type="number" class="v_s_labor" value="0" oninput="calc()"></td>' +
'<td><input type="number" class="v_s_card" value="0" oninput="calc()"></td>' +
'<td><input type="number" class="v_s_zd" value="0" oninput="calc()"></td>' +
'<td><input type="number" class="v_s_rep" value="0" oninput="calc()"></td>' +
'<td><input type="number" class="v_s_manage" value="0" oninput="calc()"></td>' +
'<td class="out_score" style="background:#e0f7fa;color:#006064;font-weight:bold;">0</td>' +
'<td class="out_rank" style="font-weight:bold;">-</td>' +
'<td class="out_bonus" style="background:#e8f5e9;color:#2e7d32;font-weight:bold;">0.00</td>' +
'<td class="out_salary" style="background:#1e3a5f;color:#fff;font-weight:bold;">0.00</td>' +
'<td><button class="btn-sm btn-red" onclick="removeStaff(this)">删</button></td>';
return tr;
}
function onRoleChange(sel){
var tr = sel.parentNode.parentNode;
var role = sel.value;
var allow = ROLE_ALLOW[role] || [];
for(var i=0;i<ITEMS.length;i++){
var k = ITEMS[i];
var inp = tr.querySelector('.v_'+k);
if(!inp) continue;
var ok = allow.indexOf(k) !== -1;
inp.disabled = !ok;
if(!ok) inp.value = 0;
var c = tr.querySelector('.c_'+k);
if(c) c.style.opacity = ok ? 1 : 0.3;
}
calc();
}
function addStaff(role){ var body = document.getElementById('staff_body'); if(!body) return; body.appendChild(createRow(role)); calc(); }
function removeStaff(btn){
var tr = btn.parentNode.parentNode; var body = tr.parentNode;
if(body.children.length <= 1){ alert('至少保留一位员工'); return; }
if(!confirm('确定删除?')) return;
body.removeChild(tr); calc();
}
function syncHeadcount(){
var body = document.getElementById('staff_body'); if(!body) return;
var target = {hair:Math.max(0,parseInt(document.getElementById('n_hair').value)||0),wash:Math.max(0,parseInt(document.getElementById('n_wash').value)||0),assist:Math.max(0,parseInt(document.getElementById('n_assist').value)||0)};
var current = {hair:0,wash:0,assist:0};
for(var i=0;i<body.children.length;i++){ var r = body.children[i].querySelector('.v_role').value; if(current[r] !== undefined) current[r]++; }
['hair','wash','assist'].forEach(function(role){ while(current[role] < target[role]){ body.appendChild(createRow(role)); current[role]++; } });
['hair','wash','assist'].forEach(function(role){ while(current[role] > target[role]){ var all = body.children, last = null; for(var j=all.length-1;j>=0;j--){ if(all[j].querySelector('.v_role').value === role){ last = all[j]; break; } } if(!last) break; body.removeChild(last); current[role]--; } });
if(body.children.length === 0) body.appendChild(createRow('hair'));
calc();
}
function autoAllocate(){
var revenue = v('p_revenue');
if(revenue <= 0){ alert('请先填写门店总业绩'); return; }
var body = document.getElementById('staff_body'); if(!body) return;
var hair=[],wash=[],assist=[];
for(var i=0;i<body.children.length;i++){
var r = body.children[i].querySelector('.v_role').value;
if(r==='hair') hair.push(body.children[i]);
else if(r==='wash') wash.push(body.children[i]);
else if(r==='assist') assist.push(body.children[i]);
}
var ratios = {};
ITEMS.forEach(function(k){ ratios[k] = v('r_'+k)/100; });
var nH = Math.max(hair.length,1), nW = Math.max(wash.length,1), nA = Math.max(assist.length,1);
var nAll = Math.max(body.children.length,1);
function alloc(rows, count, keys){
rows.forEach(function(tr){
keys.forEach(function(k){
var inp = tr.querySelector('.v_'+k);
if(inp) inp.value = (revenue * ratios[k] / count).toFixed(2);
});
});
}
alloc(hair, nH, ['sc','cw','cf','tljf','tr','zx','wm']);
alloc(wash, nW, ['sc','cw','cf','tljf','tl','wm']);
alloc(assist, nA, ['tr','zx','wm']);
var cardPerfRate = v('p_card_perf_rate')/100;
var cardPerfTotal = revenue * v('r_card')/100;
var cardCashTotal = cardPerfRate > 0 ? cardPerfTotal / cardPerfRate : 0;
var zdTotal = revenue * v('r_zd')/100;
for(var i=0;i<body.children.length;i++){
var cardInp = body.children[i].querySelector('.v_card');
if(cardInp) cardInp.value = (cardCashTotal / nAll).toFixed(2);
var zdInp = body.children[i].querySelector('.v_zd_perf');
if(zdInp) zdInp.value = (zdTotal / nAll).toFixed(2);
}
calc();
}
// ============ 自动算分 ============
function getStaffStat(tr){
var base = 0;
for(var i=0;i<ITEMS.length;i++){
var inp = tr.querySelector('.v_'+ITEMS[i]);
if(inp && !inp.disabled) base += parseFloat(inp.value) || 0;
}
var cardInp = tr.querySelector('.v_card');
var card = cardInp ? parseFloat(cardInp.value) || 0 : 0;
var zdInp = tr.querySelector('.v_zd_perf');
var zd = zdInp ? parseFloat(zdInp.value) || 0 : 0;
return {base:base, card:card, zd:zd};
}
function calcAutoScores(){
var body = document.getElementById('staff_body'); if(!body) return;
var hairRows = [], washRows = [];
for(var i=0;i<body.children.length;i++){
var tr = body.children[i];
var role = tr.querySelector('.v_role').value;
if(role === 'hair') hairRows.push(tr);
else if(role === 'wash' || role === 'assist') washRows.push(tr);
}
var targetRate = v('p_zd_target') / 100;
function processGroup(rows){
var maxRev = 0, maxCard = 0;
rows.forEach(function(tr){ var s = getStaffStat(tr); if(s.base > maxRev) maxRev = s.base; if(s.card > maxCard) maxCard = s.card; });
rows.forEach(function(tr){
var s = getStaffStat(tr);
var labor = maxRev > 0 ? (s.base / maxRev) * 30 : 0;
var cardScore = maxCard > 0 ? (s.card / maxCard) * 20 : 0;
var zdRate = s.base > 0 ? s.zd / s.base : 0;
var zdScore;
if(targetRate <= 0 || zdRate >= targetRate) zdScore = targetRate <= 0 ? 20 : 20;
else zdScore = (zdRate / targetRate) * 20;
var lInp = tr.querySelector('.v_s_labor'); if(lInp) lInp.value = labor.toFixed(1);
var cInp = tr.querySelector('.v_s_card'); if(cInp) cInp.value = cardScore.toFixed(1);
var zInp = tr.querySelector('.v_s_zd'); if(zInp) zInp.value = zdScore.toFixed(1);
});
}
processGroup(hairRows);
processGroup(washRows);
}
function toggleAutoMode(){
var isAuto = document.getElementById('auto_score_mode').checked;
var body = document.getElementById('staff_body'); if(!body) return;
for(var i=0;i<body.children.length;i++){
var tr = body.children[i];
['labor','card','zd'].forEach(function(k){
var inp = tr.querySelector('.v_s_' + k);
if(inp) inp.disabled = isAuto;
});
}
calc();
}
// ============ 核心计算 ============
function calc(){
// 自动算分(如果开启)
var autoMode = document.getElementById('auto_score_mode');
if(autoMode && autoMode.checked) calcAutoScores();
var revenue = v('p_revenue');
var varRate = v('p_var_rate')/100;
var rent = v('p_rent'), other = v('p_other'), subsidy = v('p_subsidy');
var poolRate = v('p_pool_rate')/100;
var hairShare = v('p_hair_share')/100;
var washShare = v('p_wash_share')/100;
var cardPerfRate = v('p_card_perf_rate')/100;
var cardCommRate = v('p_card_comm')/100;
var zdCommRate = v('p_zd_comm')/100;
var ratioSum = 0;
ITEMS.forEach(function(k){ ratioSum += v('r_'+k); });
var rSumEl = document.getElementById('ratio_sum');
if(rSumEl){
var ok = Math.abs(ratioSum - 100) < 0.01;
rSumEl.innerHTML = '<b style="color:'+(ok?'#2e7d32':'#c62828')+'">'+ratioSum.toFixed(1)+'%</b>' + (ok?' ✓':' ⚠️ 应等于100%');
}
var bonusPct = getBonusAt(revenue);
var H={},W={},A={};
for(var i=0;i<ITEMS.length;i++){
var k = ITEMS[i];
var h = v('h_'+k), w = v('w_'+k), a = v('a_'+k);
H[k] = h > 0 ? (h + bonusPct)/100 : 0;
W[k] = w > 0 ? (w + bonusPct)/100 : 0;
A[k] = a > 0 ? (a + bonusPct)/100 : 0;
}
var body = document.getElementById('staff_body'); if(!body) return;
var rows = Array.prototype.slice.call(body.children);
var storeItemComm=0, storeCardComm=0, storeZdComm=0, storeCardCash=0, storeCashCash=0;
var totalSubsidy=0;
var hairScore=0, washScore=0;
var cnt={hair:0,wash:0,assist:0};
var staffData = [];
rows.forEach(function(tr){
var role = tr.querySelector('.v_role').value;
if(cnt[role] !== undefined) cnt[role]++;
var rateMap = role === 'hair' ? H : role === 'wash' ? W : A;
var itemComm=0, base=0;
for(var kk=0;kk<ITEMS.length;kk++){
var key = ITEMS[kk];
var inp = tr.querySelector('.v_'+key);
var val = inp ? (parseFloat(inp.value)||0) : 0;
var comm = val * (rateMap[key] || 0);
var cell = tr.querySelector('.c_'+key);
if(cell) cell.innerText = comm.toFixed(2);
itemComm += comm;
base += val;
}
var cardEl = tr.querySelector('.v_card');
var cardCash = cardEl ? (parseFloat(cardEl.value)||0) : 0;
var cardComm = cardCash * cardPerfRate * cardCommRate;
var zdEl = tr.querySelector('.v_zd_perf');
var zdPerf = zdEl ? (parseFloat(zdEl.value)||0) : 0;
var zdComm = zdPerf * zdCommRate;
var cashEl = tr.querySelector('.v_cash');
var cashCash = cashEl ? (parseFloat(cashEl.value)||0) : 0;
var totalComm = itemComm + cardComm + zdComm;
var score=0;
['labor','card','zd','rep','manage'].forEach(function(sk){
var s = tr.querySelector('.v_s_'+sk);
if(s) score += parseFloat(s.value)||0;
});
tr.querySelector('.out_base').innerText = base.toFixed(2);
tr.querySelector('.out_item_comm').innerText = itemComm.toFixed(2);
tr.querySelector('.out_card_comm').innerText = cardComm.toFixed(2);
tr.querySelector('.out_zd_comm').innerText = zdComm.toFixed(2);
tr.querySelector('.out_total_comm').innerText = totalComm.toFixed(2);
tr.querySelector('.out_score').innerText = score.toFixed(1);
storeItemComm += itemComm;
storeCardComm += cardComm;
storeZdComm += zdComm;
storeCardCash += cardCash;
storeCashCash += cashCash;
totalSubsidy += subsidy;
if(role==='hair') hairScore += score;
else if(role==='wash' || role==='assist') washScore += score;
var name = tr.querySelector('.v_name').value;
staffData.push({tr:tr, role:role, name:name, base:base, itemComm:itemComm, cardComm:cardComm, zdComm:zdComm, totalComm:totalComm, score:score, cardCash:cardCash, zdPerf:zdPerf,
laborScore:parseFloat((tr.querySelector('.v_s_labor')||{}).value)||0,
cardScore:parseFloat((tr.querySelector('.v_s_card')||{}).value)||0,
zdScore:parseFloat((tr.querySelector('.v_s_zd')||{}).value)||0,
repScore:parseFloat((tr.querySelector('.v_s_rep')||{}).value)||0,
manageScore:parseFloat((tr.querySelector('.v_s_manage')||{}).value)||0
});
});
var storeTotalComm = storeItemComm + storeCardComm + storeZdComm;
var varCost = revenue * varRate;
var fixCost = rent + other;
var baseProfit = revenue - varCost - fixCost - storeTotalComm - totalSubsidy;
var pool = baseProfit > 0 ? baseProfit * poolRate : 0;
var hairPool = pool * hairShare;
var washPool = pool * washShare;
var finalProfit = baseProfit - pool;
// 排名(分组,标准竞赛排名:同分并列)
function getRankMap(rows, groupScoreFn){
// 返回 {tr: {rank, totalScoreInGroup}}
var arr = rows.map(function(tr){ return {tr:tr, score: groupScoreFn(tr)}; });
var result = {};
arr.forEach(function(item){
var rank = 1;
arr.forEach(function(other){ if(other.score > item.score + 1e-9) rank++; });
result[item.tr] = {rank: rank, score: item.score};
});
return result;
}
var hairRows = staffData.filter(function(s){ return s.role === 'hair'; }).map(function(s){ return s.tr; });
var washRows = staffData.filter(function(s){ return s.role === 'wash' || s.role === 'assist'; }).map(function(s){ return s.tr; });
var hairRankMap = getRankMap(hairRows, function(tr){ return parseFloat(tr.querySelector('.out_score').innerText) || 0; });
var washRankMap = getRankMap(washRows, function(tr){ return parseFloat(tr.querySelector('.out_score').innerText) || 0; });
// 更新排名显示
staffData.forEach(function(sd){
var rankEl = sd.tr.querySelector('.out_rank');
var rankInfo = sd.role === 'hair' ? hairRankMap[sd.tr] : washRankMap[sd.tr];
if(rankEl && rankInfo){
rankEl.innerText = '第' + rankInfo.rank + '名';
rankEl.classList.remove('rank-1','rank-2','rank-3');
if(rankInfo.rank <= 3) rankEl.classList.add('rank-' + rankInfo.rank);
}
});
// 分红下发
var totalSalary = 0;
staffData.forEach(function(sd){
var ratio=0, bonus=0;
if(sd.role === 'hair' && hairScore > 0){ ratio = sd.score / hairScore; bonus = hairPool * ratio; }
else if((sd.role === 'wash' || sd.role === 'assist') && washScore > 0){ ratio = sd.score / washScore; bonus = washPool * ratio; }
var salary = sd.totalComm + subsidy + bonus;
sd.bonus = bonus;
sd.salary = salary;
sd.ratio = ratio;
sd.tr.querySelector('.out_bonus').innerText = bonus.toFixed(2);
sd.tr.querySelector('.out_salary').innerText = salary.toFixed(2);
totalSalary += salary;
});
var margin = revenue > 0 ? finalProfit/revenue*100 : 0;
var commRate = revenue > 0 ? storeTotalComm/revenue*100 : 0;
var avg = staffData.length > 0 ? totalSalary/staffData.length : 0;
set('hero_profit', '¥' + fmt(finalProfit));
set('hero_margin', '利润率 ' + margin.toFixed(1) + '%');
set('hero_revenue', '¥' + fmt(revenue));
set('hero_comm_rate', commRate.toFixed(1) + '%');
set('hero_comm_amt', '总提成 ¥' + fmt(storeTotalComm));
set('hero_salary', '¥' + fmt(totalSalary));
set('hero_avg', '人均 ¥' + fmt(avg));
var tierNow = getTierAt(v('actual_revenue'));
var heroT = document.getElementById('hero_tier');
if(heroT) heroT.innerHTML = tierNow ? ('当前档位:<b>'+tierNow.name+'</b> | 提点加成:<b>+'+tierNow.bonus+'%</b>') : '当前档位:<b>未达任何档位</b> | 提点加成:<b>+0%</b>';
set('f_revenue', '¥' + fmt(revenue));
set('f_var', '¥' + fmt(varCost));
set('f_fix', '¥' + fmt(fixCost));
set('f_total_comm', '¥' + fmt(storeTotalComm));
set('f_item_comm', '¥' + fmt(storeItemComm));
set('f_card_comm', '¥' + fmt(storeCardComm));
set('f_zd_comm', '¥' + fmt(storeZdComm));
set('f_subsidy', '¥' + fmt(totalSubsidy));
set('f_base_profit', '¥' + fmt(baseProfit));
set('f_pool', '¥' + fmt(pool));
set('f_final_profit', '¥' + fmt(finalProfit));
set('f_margin', margin.toFixed(2) + '%');
set('f_comm_rate', commRate.toFixed(2) + '%');
set('f_item_rate', (revenue>0?storeItemComm/revenue*100:0).toFixed(2) + '%');
set('f_card_rate', (revenue>0?storeCardComm/revenue*100:0).toFixed(2) + '%');
set('f_zd_rate', (revenue>0?storeZdComm/revenue*100:0).toFixed(2) + '%');
set('f_card_total', '¥' + fmt(storeCardCash));
// 举例
var demoCard = 10000;
var cardPerf = demoCard * cardPerfRate;
set('ex_card_perf', '卖' + fmt(demoCard) + '元卡 → ' + fmt(demoCard) + '×' + (cardPerfRate*100).toFixed(0) + '% = ' + fmt(cardPerf) + ' 元');
set('ex_card_comm', fmt(cardPerf) + '×' + (cardCommRate*100).toFixed(0) + '% = ' + fmt(cardPerf*cardCommRate) + ' 元充卡提成');
set('ex_zd_comm', '指定客业绩10000元×' + (zdCommRate*100).toFixed(0) + '% = ' + fmt(10000*zdCommRate) + ' 元提成');
set('ex_card_ratio', '总业绩 ' + fmt(revenue) + ' × ' + v('r_card').toFixed(0) + '% = 全店充卡业绩 ' + fmt(revenue*v('r_card')/100) + ' 元');
set('ex_zd_ratio', '总业绩 ' + fmt(revenue) + ' × ' + v('r_zd').toFixed(0) + '% = 全店指定客业绩 ' + fmt(revenue*v('r_zd')/100) + ' 元');
window._cr = {
revenue:revenue, varCost:varCost, fixCost:fixCost, storeItemComm:storeItemComm,
storeCardComm:storeCardComm, storeZdComm:storeZdComm, storeTotalComm:storeTotalComm,
totalSubsidy:totalSubsidy, baseProfit:baseProfit, pool:pool, finalProfit:finalProfit,
totalSalary:totalSalary, staffData:staffData, hairScore:hairScore, washScore:washScore,
hairPool:hairPool, washPool:washPool, cnt:cnt, margin:margin, commRate:commRate,
storeCardCash:storeCardCash, storeCashCash:storeCashCash, bonusPct:bonusPct,
hairRankMap:hairRankMap, washRankMap:washRankMap
};
renderTiers();
renderDividendDetail();
updateAdvice();
updateReconcile();
renderPersonal();
renderSpeech();
validate();
}
// ============ 五-B:分红考核机制详解 ============
function renderDividendDetail(){
var cr = window._cr; if(!cr) return;
// ---------- 1. 分红池推导 ----------
var deriveBox = document.getElementById('derive_pool');
if(deriveBox){
var html = '';
html += '<div class="row">① 门店营业收入:<b>¥' + fmt(cr.revenue) + '</b></div>';
html += '<div class="row">② 减去变动成本(' + v('p_var_rate') + '%):<span class="minus">-¥' + fmt(cr.varCost) + '</span></div>';
html += '<div class="row">③ 减去固定成本(房租+其他):<span class="minus">-¥' + fmt(cr.fixCost) + '</span></div>';
html += '<div class="row">④ 减去全店员工总提成:<span class="minus">-¥' + fmt(cr.storeTotalComm) + '</span></div>';
html += '<div class="row" style="padding-left:20px;font-size:11px;color:#666;">其中:项目提成 ¥' + fmt(cr.storeItemComm) + ' | 充卡提成 ¥' + fmt(cr.storeCardComm) + ' | 指定客提成 ¥' + fmt(cr.storeZdComm) + '</div>';
html += '<div class="row">⑤ 减去员工补贴(' + cr.staffData.length + '人 × ¥' + v('p_subsidy') + '):<span class="minus">-¥' + fmt(cr.totalSubsidy) + '</span></div>';
html += '<div class="sep"></div>';
html += '<div class="row"><b>= 门店基础纯利:<span class="plus">¥' + fmt(cr.baseProfit) + '</span></b></div>';
html += '<div class="sep"></div>';
html += '<div class="row">⑥ 乘以分红提取比例(' + v('p_pool_rate') + '%):<b>¥' + fmt(cr.baseProfit) + ' × ' + v('p_pool_rate') + '% = ¥' + fmt(cr.pool) + '</b></div>';
html += '<div class="result">';
html += '<b>💰 分红池总额 = ¥' + fmt(cr.pool) + '</b><br>';
html += '<b style="color:#bf360c;">→ 发型组分红池 = ¥' + fmt(cr.pool) + ' × ' + v('p_hair_share') + '% = ¥' + fmt(cr.hairPool) + '</b><br>';
html += '<b style="color:#1565c0;">→ 洗护组分红池(含助理)= ¥' + fmt(cr.pool) + ' × ' + v('p_wash_share') + '% = ¥' + fmt(cr.washPool) + '</b>';
if(cr.baseProfit <= 0) html += '<br><span style="color:#c62828;">⚠️ 基础纯利为负,分红池为 0,本月无分红</span>';
html += '</div>';
deriveBox.innerHTML = html;
}
// ---------- 2. 实时算分演示 ----------
var scoreBody = document.getElementById('score_demo_body');
if(scoreBody){
// 分组算最大值
function calcGroup(rows){
var maxRev = 0, maxCard = 0;
rows.forEach(function(sd){ if(sd.base > maxRev) maxRev = sd.base; if(sd.cardCash > maxCard) maxCard = sd.cardCash; });
return {maxRev: maxRev, maxCard: maxCard};
}
var hairArr = cr.staffData.filter(function(s){ return s.role === 'hair'; });
var washArr = cr.staffData.filter(function(s){ return s.role === 'wash' || s.role === 'assist'; });
var hairStats = calcGroup(hairArr);
var washStats = calcGroup(washArr);
var targetRate = v('p_zd_target');
function renderScoreRow(sd, groupName, stats){
var isHair = sd.role === 'hair';
var stat = isHair ? hairStats : washStats;
var labor = stat.maxRev > 0 ? (sd.base / stat.maxRev) * 30 : 0;
var cardS = stat.maxCard > 0 ? (sd.cardCash / stat.maxCard) * 20 : 0;
var zdRate = sd.base > 0 ? sd.zdPerf / sd.base : 0;
var zdS = targetRate > 0 ? Math.min(20, (zdRate * 100 / targetRate) * 20) : 20;
var total = labor + cardS + zdS + sd.repScore + sd.manageScore;
var roleCls = isHair ? 'hl-yellow' : (sd.role === 'wash' ? 'hl-blue' : '');
return '<tr>' +
'<td class="' + roleCls + '">' + groupName + '</td>' +
'<td><b>' + sd.name + '</b><br><small style="color:#666;">' + ROLE_NAME[sd.role] + '</small></td>' +
'<td>¥' + fmt(sd.base) + '</td>' +
'<td style="background:#e8f5e9;">' + labor.toFixed(1) + '分<span class="calc-detail">(' + fmt(sd.base) + '÷' + fmt(stat.maxRev) + '×30)</span></td>' +
'<td>¥' + fmt(sd.cardCash) + '</td>' +
'<td style="background:#e8f5e9;">' + cardS.toFixed(1) + '分<span class="calc-detail">(' + fmt(sd.cardCash) + '÷' + fmt(stat.maxCard) + '×20)</span></td>' +
'<td>¥' + fmt(sd.zdPerf) + '</td>' +
'<td>' + (zdRate*100).toFixed(1) + '%</td>' +
'<td style="background:#e8f5e9;">' + zdS.toFixed(1) + '分<span class="calc-detail">(' + (zdRate*100).toFixed(1) + '%÷' + targetRate + '%×20)</span></td>' +
'<td>' + sd.repScore.toFixed(1) + '</td>' +
'<td>' + sd.manageScore.toFixed(1) + '</td>' +
'<td style="background:#ffe0b2;font-weight:bold;color:#bf360c;">' + total.toFixed(1) + '</td>' +
'</tr>';
}
var html = '';
hairArr.forEach(function(sd){ html += renderScoreRow(sd, '发型组', hairStats); });
washArr.forEach(function(sd){ html += renderScoreRow(sd, '洗护组', washStats); });
scoreBody.innerHTML = html;
}
// ---------- 3. 排名与分红 ----------
var rankBody = document.getElementById('rank_demo_body');
if(rankBody){
function renderRankRows(arr, groupName, rankMap, groupScore, groupPool){
// 按排名排序
var sorted = arr.slice().sort(function(a,b){
var ra = rankMap[a.tr] ? rankMap[a.tr].rank : 999;
var rb = rankMap[b.tr] ? rankMap[b.tr].rank : 999;
return ra - rb;
});
var html = '';
sorted.forEach(function(sd){
var rankInfo = rankMap[sd.tr];
var rank = rankInfo ? rankInfo.rank : '-';
var ratio = groupScore > 0 ? sd.score / groupScore : 0;
var bonus = groupPool * ratio;
var rankCls = rank === 1 ? 'rank-1' : rank === 2 ? 'rank-2' : rank === 3 ? 'rank-3' : '';
// 检查并列
var isTie = false;
sorted.forEach(function(o){ if(o !== sd && rankMap[o.tr] && rankMap[o.tr].rank === rank) isTie = true; });
html += '<tr>' +
'<td>' + groupName + '</td>' +
'<td class="' + rankCls + '" style="font-size:14px;">第' + rank + '名' + (isTie ? '(并列)' : '') + '</td>' +
'<td><b>' + sd.name + '</b></td>' +
'<td>' + sd.score.toFixed(1) + '分</td>' +
'<td>' + (ratio*100).toFixed(1) + '%</td>' +
'<td>¥' + fmt(groupPool) + '</td>' +
'<td style="color:#2e7d32;font-weight:bold;font-size:14px;">¥' + fmt(bonus) + '</td>' +
'</tr>';
});
return html;
}
var html = '';
html += renderRankRows(hairArr, '发型组', cr.hairRankMap, cr.hairScore, cr.hairPool);
html += renderRankRows(washArr, '洗护组', cr.washRankMap, cr.washScore, cr.washPool);
rankBody.innerHTML = html;
}
// ---------- 4. 第一名详细说明 ----------
var explainBox = document.getElementById('first_place_explain');
if(explainBox){
var html = '';
var hairArr = cr.staffData.filter(function(s){ return s.role === 'hair'; });
var washArr = cr.staffData.filter(function(s){ return s.role === 'wash' || s.role === 'assist'; });
var hairFirst = null, washFirst = null;
hairArr.forEach(function(sd){ if(!hairFirst || (cr.hairRankMap[sd.tr] && cr.hairRankMap[sd.tr].rank < cr.hairRankMap[hairFirst.tr].rank)) hairFirst = sd; });
washArr.forEach(function(sd){ if(!washFirst || (cr.washRankMap[sd.tr] && cr.washRankMap[sd.tr].rank < cr.washRankMap[washFirst.tr].rank)) washFirst = sd; });
if(hairFirst){
var maxRev = 0, maxCard = 0;
hairArr.forEach(function(sd){ if(sd.base > maxRev) maxRev = sd.base; if(sd.cardCash > maxCard) maxCard = sd.cardCash; });
var targetRate = v('p_zd_target');
var labor = maxRev > 0 ? (hairFirst.base / maxRev) * 30 : 0;
var cardS = maxCard > 0 ? (hairFirst.cardCash / maxCard) * 20 : 0;
var zdRate = hairFirst.base > 0 ? hairFirst.zdPerf / hairFirst.base : 0;
var zdS = targetRate > 0 ? Math.min(20, (zdRate * 100 / targetRate) * 20) : 20;
var total = labor + cardS + zdS + hairFirst.repScore + hairFirst.manageScore;
var rankInfo = cr.hairRankMap[hairFirst.tr];
html += '<b style="color:#bf360c;font-size:14px;">🏆 发型组第一名:' + hairFirst.name + '</b>(总分 ' + total.toFixed(1) + ' 分,第 ' + (rankInfo?rankInfo.rank:'?') + ' 名)<br><br>';
html += '📌 <b>他/她是怎么拿到第一名的?</b><br>';
html += '① <b>劳动业绩分</b>:个人业绩 ¥' + fmt(hairFirst.base) + ' ÷ 组内最高业绩 ¥' + fmt(maxRev) + ' × 30 = <b>' + labor.toFixed(1) + ' 分</b><br>';
html += '② <b>充卡业绩分</b>:个人充卡 ¥' + fmt(hairFirst.cardCash) + ' ÷ 组内最高充卡 ¥' + fmt(maxCard) + ' × 20 = <b>' + cardS.toFixed(1) + ' 分</b><br>';
html += '③ <b>指定客占比分</b>:实际占比 ' + (zdRate*100).toFixed(1) + '% ÷ 目标 ' + targetRate + '% × 20 = <b>' + zdS.toFixed(1) + ' 分</b><br>';
html += '④ <b>口碑分</b>:<b>' + hairFirst.repScore.toFixed(1) + ' 分</b>(手动填)<br>';
html += '⑤ <b>管理分</b>:<b>' + hairFirst.manageScore.toFixed(1) + ' 分</b>(老板打分)<br>';
html += '<b style="color:#bf360c;">总分 = ' + labor.toFixed(1) + ' + ' + cardS.toFixed(1) + ' + ' + zdS.toFixed(1) + ' + ' + hairFirst.repScore.toFixed(1) + ' + ' + hairFirst.manageScore.toFixed(1) + ' = ' + total.toFixed(1) + ' 分</b><br><br>';
html += '💰 <b>他/她的分红怎么算?</b><br>';
html += '发型组分红池 = <b>¥' + fmt(cr.hairPool) + '</b><br>';
html += '组内总分合计 = <b>' + cr.hairScore.toFixed(1) + ' 分</b><br>';
html += '个人分红 = ' + fmt(cr.hairPool) + ' × (' + total.toFixed(1) + ' ÷ ' + cr.hairScore.toFixed(1) + ') = <b style="color:#2e7d32;font-size:15px;">¥' + fmt(hairFirst.bonus) + '</b><br>';
}
if(washFirst){
html += '<br><hr style="border:none;border-top:2px dashed #ccc;margin:15px 0;"><br>';
var maxRev = 0, maxCard = 0;
washArr.forEach(function(sd){ if(sd.base > maxRev) maxRev = sd.base; if(sd.cardCash > maxCard) maxCard = sd.cardCash; });
var targetRate = v('p_zd_target');
var labor = maxRev > 0 ? (washFirst.base / maxRev) * 30 : 0;
var cardS = maxCard > 0 ? (washFirst.cardCash / maxCard) * 20 : 0;
var zdRate = washFirst.base > 0 ? washFirst.zdPerf / washFirst.base : 0;
var zdS = targetRate > 0 ? Math.min(20, (zdRate * 100 / targetRate) * 20) : 20;
var total = labor + cardS + zdS + washFirst.repScore + washFirst.manageScore;
var rankInfo = cr.washRankMap[washFirst.tr];
html += '<b style="color:#1565c0;font-size:14px;">🏆 洗护组第一名(含助理):' + washFirst.name + '</b>(总分 ' + total.toFixed(1) + ' 分,第 ' + (rankInfo?rankInfo.rank:'?') + ' 名)<br><br>';
html += '📌 <b>他/她是怎么拿到第一名的?</b><br>';
html += '① <b>劳动业绩分</b>:个人业绩 ¥' + fmt(washFirst.base) + ' ÷ 组内最高业绩 ¥' + fmt(maxRev) + ' × 30 = <b>' + labor.toFixed(1) + ' 分</b><br>';
html += '② <b>充卡业绩分</b>:个人充卡 ¥' + fmt(washFirst.cardCash) + ' ÷ 组内最高充卡 ¥' + fmt(maxCard) + ' × 20 = <b>' + cardS.toFixed(1) + ' 分</b><br>';
html += '③ <b>指定客占比分</b>:实际占比 ' + (zdRate*100).toFixed(1) + '% ÷ 目标 ' + targetRate + '% × 20 = <b>' + zdS.toFixed(1) + ' 分</b><br>';
html += '④ <b>口碑分</b>:<b>' + washFirst.repScore.toFixed(1) + ' 分</b><br>';
html += '⑤ <b>管理分</b>:<b>' + washFirst.manageScore.toFixed(1) + ' 分</b><br>';
html += '<b style="color:#1565c0;">总分 = ' + labor.toFixed(1) + ' + ' + cardS.toFixed(1) + ' + ' + zdS.toFixed(1) + ' + ' + washFirst.repScore.toFixed(1) + ' + ' + washFirst.manageScore.toFixed(1) + ' = ' + total.toFixed(1) + ' 分</b><br><br>';
html += '💰 <b>他/她的分红怎么算?</b><br>';
html += '洗护组分红池 = <b>¥' + fmt(cr.washPool) + '</b><br>';
html += '组内总分合计 = <b>' + cr.washScore.toFixed(1) + ' 分</b><br>';
html += '个人分红 = ' + fmt(cr.washPool) + ' × (' + total.toFixed(1) + ' ÷ ' + cr.washScore.toFixed(1) + ') = <b style="color:#2e7d32;font-size:15px;">¥' + fmt(washFirst.bonus) + '</b><br>';
}
if(!hairFirst && !washFirst) html = '暂无员工数据';
explainBox.innerHTML = html;
}
}
// ============ 健康区间 ============
function updateAdvice(){
var cr = window._cr; if(!cr) return;
var revenue = cr.revenue;
var varRate = v('p_var_rate')/100;
var rent = v('p_rent'), other = v('p_other'), subsidy = v('p_subsidy');
var poolRate = v('p_pool_rate')/100;
var targetMargin = v('p_target_margin')/100;
var warnMargin = v('p_warn_margin')/100;
var minComm = v('p_min_comm_rate')/100;
var staffCount = cr.staffData.length;
var fixedCost = rent + other + subsidy * staffCount;
var denomB = poolRate < 0.999 ? 1 - poolRate : 1;
var curCommRate = revenue > 0 ? cr.storeTotalComm/revenue : 0;
var tiers = [30000,50000,80000,100000,150000,200000];
if(revenue > 0 && tiers.indexOf(revenue) === -1){ tiers.push(revenue); tiers.sort(function(a,b){return a-b;}); }
var html = '';
tiers.forEach(function(R){
if(R <= 0) return;
var fixedRate = fixedCost/R;
var beRate = 1 - varRate - fixedRate;
var healthRate = beRate - targetMargin/denomB;
var warnRate = beRate - warnMargin/denomB;
var recLow = Math.max(healthRate*0.7, minComm);
var recHigh = healthRate;
var recMid = (recLow + recHigh)/2;
var recStr = '', recCls = '';
if(healthRate <= 0){ recStr = '无法达成'; recCls = 'hl-red'; recLow = 0; recMid = 0; }
else if(recLow >= recHigh){ recStr = '≤' + (healthRate*100).toFixed(1) + '%'; recCls = 'hl-orange'; recLow = 0; }
else { recStr = (recLow*100).toFixed(1) + '% ~ ' + (recHigh*100).toFixed(1) + '%'; recCls = 'hl-green'; }
var profit = (R - R*varRate - fixedCost - R*curCommRate) * denomB;
var marginAt = profit/R;
var grade = '', gradeCls = '';
if(healthRate <= 0){ grade = '🔴 该档位无解'; gradeCls = 'hl-red'; }
else if(curCommRate > warnRate){ grade = '🔴 过高'; gradeCls = 'hl-red'; }
else if(curCommRate > healthRate){ grade = '🟠 偏高'; gradeCls = 'hl-orange'; }
else if(recLow < recHigh && curCommRate < recLow){ grade = '🟡 偏低'; gradeCls = 'hl-orange'; }
else { grade = '🟢 理想'; gradeCls = 'hl-green'; }
var isCur = Math.abs(R - revenue) < 1;
var rowCls = isCur ? ' style="background:#fff9c4;"' : '';
html += '<tr' + rowCls + '>' +
'<td>'+fmt(R)+(isCur?' 👈当前':'')+'</td>' +
'<td>'+(fixedRate*100).toFixed(1)+'%</td>' +
'<td>'+(beRate>0?(beRate*100).toFixed(1)+'%':'已亏损')+'</td>' +
'<td>'+(healthRate>0?(healthRate*100).toFixed(1)+'%':'无法达成')+'</td>' +
'<td>'+(warnRate>0?(warnRate*100).toFixed(1)+'%':'无法达成')+'</td>' +
'<td class="'+recCls+'">'+recStr+'</td>' +
'<td class="'+recCls+'">'+(recMid>0?(recMid*100).toFixed(1)+'%':'-')+'</td>' +
'<td>'+(curCommRate*100).toFixed(1)+'%</td>' +
'<td>¥'+fmt(profit)+'</td>' +
'<td>'+(marginAt*100).toFixed(1)+'%</td>' +
'<td class="'+gradeCls+'">'+grade+'</td></tr>';
});
var b = document.getElementById('advice_body'); if(b) b.innerHTML = html;
}
function fillRecommend(){
var cr = window._cr; if(!cr) return;
var varRate = v('p_var_rate')/100;
var rent = v('p_rent'), other = v('p_other'), subsidy = v('p_subsidy');
var poolRate = v('p_pool_rate')/100;
var targetMargin = v('p_target_margin')/100;
var minComm = v('p_min_comm_rate')/100;
var staffCount = cr.staffData.length;
var fixedCost = rent + other + subsidy * staffCount;
var R = cr.revenue;
if(R <= 0){ alert('请先填写门店总业绩'); return; }
var denomB = poolRate < 0.999 ? 1 - poolRate : 1;
var healthRate = 1 - varRate - fixedCost/R - targetMargin/denomB;
var recLow = Math.max(healthRate*0.7, minComm);
var recMid = (recLow+healthRate)/2;
var inp = document.getElementById('scale_target');
if(inp){
if(recMid > 0) inp.value = (recMid*100).toFixed(1);
else if(healthRate > 0) inp.value = (healthRate*100).toFixed(1);
else alert('当前业绩无法达成目标利润率');
}
}
function scaleCommissions(){
var cr = window._cr; if(!cr) return;
var inp = document.getElementById('scale_target');
if(!inp) return;
var target = parseFloat(inp.value)/100;
if(isNaN(target) || target <= 0){ alert('请填写目标总提成率'); return; }
var revenue = cr.revenue;
if(revenue <= 0){ alert('门店业绩为0'); return; }
var nonItem = (cr.storeCardComm + cr.storeZdComm)/revenue;
var targetItem = target - nonItem;
if(targetItem <= 0){ alert('目标总提成率低于充卡+指定客提成率,无法仅靠缩放项目提成实现'); return; }
var curItem = cr.storeItemComm/revenue;
if(curItem <= 0){ alert('当前项目提成为0'); return; }
var k = targetItem / curItem;
if(k > 3 || k < 0.2){ if(!confirm('缩放系数 '+k.toFixed(2)+',幅度较大,确认吗?')) return; }
var ids = [];
ITEMS.forEach(function(kk){ ids.push('h_'+kk, 'w_'+kk, 'a_'+kk); });
var changed = 0;
ids.forEach(function(id){
var el = document.getElementById(id);
if(el){ var val = parseFloat(el.value)||0; if(val > 0){ el.value = (val*k).toFixed(2); changed++; } }
});
calc();
alert('已调整 '+changed+' 个提成比例。目标总提成率:'+(target*100).toFixed(1)+'%');
}
// ============ 对账 ============
function updateReconcile(){
var box = document.getElementById('reconcile_box'); if(!box) return;
var cr = window._cr; if(!cr) return;
var empItem=0, empCard=0, empZd=0, empTotal=0, empBonus=0, empSalary=0;
cr.staffData.forEach(function(sd){ empItem += sd.itemComm; empCard += sd.cardComm; empZd += sd.zdComm; empTotal += sd.totalComm; empBonus += sd.bonus; empSalary += sd.salary; });
var empSub = cr.totalSubsidy;
var empCommRate = cr.revenue > 0 ? empTotal/cr.revenue*100 : 0;
var finCommRate = cr.revenue > 0 ? cr.storeTotalComm/cr.revenue*100 : 0;
var theorySalary = empTotal + empSub + empBonus;
var tol = 0.01 * cr.staffData.length + 0.02;
function row(name, emp, fin, isPct, note){
var diff = emp - fin; var ok = Math.abs(diff) <= tol; var cls = ok ? 'hl-green' : 'hl-red';
var e = isPct ? emp.toFixed(2)+'%' : '¥'+fmt(emp);
var f = isPct ? fin.toFixed(2)+'%' : '¥'+fmt(fin);
var d = (diff>=0?'+':'') + (isPct ? diff.toFixed(4)+'%' : '¥'+fmt(diff));
return '<tr><td>'+name+'</td><td>'+e+'</td><td>'+f+'</td><td class="'+cls+'">'+d+'</td><td class="'+cls+'">'+(ok?'✓':'⚠️')+'</td><td style="font-size:11px;color:#666;text-align:left;">'+(note||'')+'</td></tr>';
}
var html = '<div class="tip"><b>对账口径:</b>员工表「总提成」不含补贴和分红;「实发工资」= 总提成+补贴+分红。</div>';
html += '<table><thead><tr><th>对账项</th><th>员工表合计</th><th>财务表数值</th><th>差额</th><th>状态</th><th>说明</th></tr></thead><tbody>';
html += row('项目提成', empItem, cr.storeItemComm, false, '各行项目提成加总');
html += row('充卡提成', empCard, cr.storeCardComm, false, '各行充卡提成加总');
html += row('指定客提成', empZd, cr.storeZdComm, false, '各行指定客提成加总');
html += row('总提成', empTotal, cr.storeTotalComm, false, '= 三项之和');
html += row('补贴合计', empSub, cr.totalSubsidy, false, '员工数 × 补贴');
html += row('分红合计', empBonus, cr.pool, false, '各行分红加总(含助理)');
html += row('实发工资合计', empSalary, theorySalary, false, '= 总提成+补贴+分红');
html += row('全店提成率', empCommRate, finCommRate, true, '应与财务表一致');
html += '</tbody></table>';
var allOk = Math.abs(empTotal - cr.storeTotalComm) <= tol && Math.abs(empSalary - theorySalary) <= tol && Math.abs(empCommRate - finCommRate) <= 0.02 && Math.abs(empBonus - cr.pool) <= tol;
html += '<div class="'+(allOk?'ok-box':'err-box')+'">' + (allOk ? '✅ 员工表与财务表完全对得上。' : '⚠️ 存在对不上项。') + '</div>';
html += '<div class="tip"><b>📌 实发工资 vs 全店提成:</b>实发工资合计 = <b>¥'+fmt(empSalary)+'</b>,全店总提成 = <b>¥'+fmt(empTotal)+'</b>,差额 = <b>¥'+fmt(empSalary-empTotal)+'</b>(= 补贴 ¥'+fmt(empSub)+' + 分红 ¥'+fmt(empBonus)+')。<b>两者不等是正常的。</b></div>';
box.innerHTML = html;
}
// ============ 个人提成点 ============
function renderPersonal(){
var cr = window._cr; var body = document.getElementById('personal_body'); var sumBox = document.getElementById('personal_summary');
if(!cr || !body) return;
var simRevenue = v('sim_revenue'); var poolRate = v('sim_pool_rate')/100;
var includeAssist = document.getElementById('sim_include_assist').checked;
if(simRevenue <= 0){ body.innerHTML = '<tr><td colspan="14" style="color:#999;">请输入测算业绩</td></tr>'; if(sumBox) sumBox.innerHTML = ''; return; }
var curRevenue = cr.revenue; var scale = curRevenue > 0 ? simRevenue/curRevenue : 0;
var simBonus = getBonusAt(simRevenue);
var H={},W={},A={};
for(var i=0;i<ITEMS.length;i++){ var k = ITEMS[i]; var h = v('h_'+k), w = v('w_'+k), a = v('a_'+k); H[k] = h > 0 ? (h+simBonus)/100 : 0; W[k] = w > 0 ? (w+simBonus)/100 : 0; A[k] = a > 0 ? (a+simBonus)/100 : 0; }
var cardPerfRate = v('p_card_perf_rate')/100; var cardCommRate = v('p_card_comm')/100; var zdCommRate = v('p_zd_comm')/100;
var h_share = v('p_hair_share')/100; var w_share = v('p_wash_share')/100;
var varRate = v('p_var_rate')/100; var rent = v('p_rent'), other = v('p_other'), subsidy = v('p_subsidy');
var staff = [];
cr.staffData.forEach(function(sd){
var tr = sd.tr; var role = sd.role; var rateMap = role === 'hair' ? H : role === 'wash' ? W : A;
var itemComm=0, base=0;
for(var kk=0;kk<ITEMS.length;kk++){ var key = ITEMS[kk]; var inp = tr.querySelector('.v_'+key); var val = (inp ? parseFloat(inp.value)||0 : 0) * scale; itemComm += val * (rateMap[key] || 0); base += val; }
var cardCash = (sd.cardCash || 0) * scale; var cardComm = cardCash * cardPerfRate * cardCommRate;
var zdPerf = sd.zdPerf * scale; var zdComm = zdPerf * zdCommRate;
staff.push({name:sd.name, role:role, base:base, itemComm:itemComm, cardComm:cardComm, zdComm:zdComm, totalComm:itemComm+cardComm+zdComm, score:sd.score});
});
var totalScoreH=0, totalScoreW=0; var storeComm = 0;
staff.forEach(function(s){ storeComm += s.totalComm; if(s.role==='hair') totalScoreH += s.score; else if(s.role==='wash' || (s.role==='assist' && includeAssist)) totalScoreW += s.score; });
var fixedCost = rent + other + subsidy * staff.length;
var baseProfit = simRevenue - simRevenue*varRate - fixedCost - storeComm;
var pool = baseProfit > 0 ? baseProfit*poolRate : 0;
var hPool = pool*h_share, wPool = pool*w_share;
var html = ''; var sumSalary=0, sumBase=0;
staff.forEach(function(s){
var ratio=0, bonus=0, poolName='';
if(s.role==='hair'){ poolName='发型组'; if(totalScoreH>0){ ratio=s.score/totalScoreH; bonus=hPool*ratio; } }
else if(s.role==='wash'){ poolName='洗护组'; if(totalScoreW>0){ ratio=s.score/totalScoreW; bonus=wPool*ratio; } }
else { poolName = includeAssist?'洗护组(含助理)':'不分红'; if(includeAssist && totalScoreW>0){ ratio=s.score/totalScoreW; bonus=wPool*ratio; } }
var salary = s.totalComm + subsidy + bonus;
var rateOnBase = s.base > 0 ? salary/s.base*100 : 0; var rateOnRev = simRevenue > 0 ? salary/simRevenue*100 : 0;
sumSalary += salary; sumBase += s.base;
var rowCls = s.role==='hair' ? ' style="background:#fff8e1;"' : s.role==='wash' ? ' style="background:#e3f2fd;"' : ' style="background:#fce4ec;"';
html += '<tr'+rowCls+'><td>'+s.name+'</td><td>'+ROLE_NAME[s.role]+'</td><td>¥'+fmt(s.base)+'</td><td>¥'+fmt(s.itemComm)+'</td><td>¥'+fmt(s.cardComm)+'</td><td>¥'+fmt(s.zdComm)+'</td><td>¥'+fmt(s.totalComm)+'</td><td>'+s.score.toFixed(1)+'</td><td>'+poolName+'</td><td>¥'+fmt(bonus)+'</td><td>¥'+fmt(subsidy)+'</td><td style="font-weight:bold;color:#004d40;">¥'+fmt(salary)+'</td><td class="hl-orange">'+rateOnBase.toFixed(2)+'%</td><td>'+rateOnRev.toFixed(2)+'%</td></tr>';
});
body.innerHTML = html;
var sHtml = '<div class="tip"><b>📊 测算汇总(门店业绩 ¥'+fmt(simRevenue)+',分红池 '+(poolRate*100).toFixed(0)+'%):</b><br>门店总提成 ¥'+fmt(storeComm)+'(提成率 '+(storeComm/simRevenue*100).toFixed(2)+'%)| 基础纯利 ¥'+fmt(baseProfit)+' | 分红池 ¥'+fmt(pool)+'<br>员工实发工资合计 ¥'+fmt(sumSalary)+' | 人均 ¥'+fmt(sumSalary/staff.length)+'</div>';
if(sumBox) sumBox.innerHTML = sHtml;
}
// ============ 宣讲话术 ============
function renderSpeech(){
var cr = window._cr; if(!cr) return;
var box = document.getElementById('speech_box'); if(!box) return;
var revenue = v('actual_revenue'); var tier = getTierAt(revenue); var bonusPct = tier ? tier.bonus : 0;
var avgSalary = cr.staffData.length > 0 ? cr.totalSalary/cr.staffData.length : 0;
var sortedTiers = TIERS.slice().sort(function(a,b){return a.threshold-b.threshold;});
function simAvg(R){
var scale = cr.revenue > 0 ? R/cr.revenue : 0; var b = getBonusAt(R);
var H={},W={},A={};
for(var i=0;i<ITEMS.length;i++){ var k = ITEMS[i]; H[k] = v('h_'+k) > 0 ? (v('h_'+k)+b)/100 : 0; W[k] = v('w_'+k) > 0 ? (v('w_'+k)+b)/100 : 0; A[k] = v('a_'+k) > 0 ? (v('a_'+k)+b)/100 : 0; }
var totalComm=0;
cr.staffData.forEach(function(sd){
var tr = sd.tr; var role = sd.role; var rateMap = role==='hair'?H:role==='wash'?W:A;
for(var kk=0;kk<ITEMS.length;kk++){ var key=ITEMS[kk]; var inp = tr.querySelector('.v_'+key); var val = (inp ? parseFloat(inp.value)||0 : 0) * scale; totalComm += val * (rateMap[key]||0); }
var cardCash = (sd.cardCash||0)*scale; totalComm += cardCash * v('p_card_perf_rate')/100 * v('p_card_comm')/100;
totalComm += sd.zdPerf * scale * v('p_zd_comm')/100;
});
var fixedCost = v('p_rent')+v('p_other')+v('p_subsidy')*cr.staffData.length;
var baseProfit = R - R*v('p_var_rate')/100 - fixedCost - totalComm;
var pool = baseProfit>0 ? baseProfit*v('p_pool_rate')/100 : 0;
var totalSalary = totalComm + v('p_subsidy')*cr.staffData.length + pool;
return {avg: cr.staffData.length>0 ? totalSalary/cr.staffData.length : 0, profit: baseProfit-pool};
}
var r3 = simAvg(30000); var r8 = simAvg(80000); var r12 = simAvg(120000);
var html = '<div class="speech-quote">"我们的薪酬逻辑:<b>你的工资不是老板给的,是客人给的。</b><br><br>门店做到 <b>3万</b>,员工平均工资约 <b>¥'+fmt(r3.avg)+'</b>;<br>做到 <b>8万</b>,提点加成 <b>+'+getBonusAt(80000)+'%</b>,平均工资能到 <b>¥'+fmt(r8.avg)+'</b>'+(r3.avg>0?'(+'+( (r8.avg-r3.avg)/r3.avg*100 ).toFixed(0)+'%)':'')+';<br>做到 <b>12万</b>,加成 <b>+'+getBonusAt(120000)+'%</b>,平均工资 <b>¥'+fmt(r12.avg)+'</b>'+(r3.avg>0?'(+'+( (r12.avg-r3.avg)/r3.avg*100 ).toFixed(0)+'%)':'')+'。<br><br>门店纯利也从 <b>¥'+fmt(r3.profit)+'</b> 涨到 <b>¥'+fmt(r12.profit)+'</b>。<br><br>你想拿高工资,不是跟我谈底薪,是跟我一起把业绩做起来。<b>蛋糕做大,你分得更多,我也赚得更多。</b>"</div>';
html += '<div class="tip"><b>📌 当前状态:</b>业绩 ¥'+fmt(revenue)+',档位 '+(tier?tier.name:'未达')+',加成 +'+bonusPct+'%,平均工资 ¥'+fmt(avgSalary)+'。<br><b>🎯 你的档位设置:</b>'+sortedTiers.map(function(t){ return t.name+'('+fmt(t.threshold)+'元起)'+'+'+t.bonus+'%'; }).join(' | ')+'</div>';
box.innerHTML = html;
}
// ============ 校验 ============
function validate(){
var box = document.getElementById('validate_box'); if(!box) return;
var cr = window._cr; if(!cr) return;
var issues = [], passes = [];
var ratioSum = 0; ITEMS.forEach(function(k){ ratioSum += v('r_'+k); });
if(Math.abs(ratioSum - 100) > 0.01) issues.push('项目占比合计 = '+ratioSum.toFixed(1)+'%,应等于 100%'); else passes.push('项目占比合计 = 100%');
var shareSum = v('p_hair_share') + v('p_wash_share');
if(Math.abs(shareSum - 100) > 0.01) issues.push('发型+洗护分配比 = '+shareSum.toFixed(1)+'%,应等于100%'); else passes.push('发型+洗护分配比 = 100%');
var scoreLimits = {labor:30, card:20, zd:20, rep:10, manage:20};
var scoreNames = {labor:'劳动分', card:'充卡分', zd:'指定分', rep:'口碑分', manage:'管理分'};
cr.staffData.forEach(function(sd){
var tr = sd.tr; var name = sd.name;
for(var sk in scoreLimits){ var el = tr.querySelector('.v_s_'+sk); var sv = el ? parseFloat(el.value)||0 : 0; if(sv > scoreLimits[sk]) issues.push(name+' 的 '+scoreNames[sk]+' = '+sv+'(上限'+scoreLimits[sk]+')'); }
});
var bonusPaid = cr.staffData.reduce(function(a,sd){return a+sd.bonus;},0);
if(Math.abs(cr.pool - bonusPaid) > 0.5) issues.push('理论分红池 ¥'+fmt(cr.pool)+' 与实发分红 ¥'+fmt(bonusPaid)+' 不一致');
if(cr.storeCashCash > 0.01) issues.push('散客现金 ¥'+fmt(cr.storeCashCash)+',目前未计入营业收入');
if(cr.commRate > 60) issues.push('总店提成率 '+cr.commRate.toFixed(2)+'% 超过60%,请检查');
var rCard = v('r_card'), rZd = v('r_zd');
if(rCard > 100) issues.push('充卡业绩占比 '+rCard+'% 超过100%');
if(rZd > 100) issues.push('指定客业绩占比 '+rZd+'% 超过100%');
if(rCard + rZd > 100) issues.push('充卡占比('+rCard+'%) + 指定客占比('+rZd+'%) 超过100%');
var html = '';
if(issues.length === 0) html = '<div class="ok-box">✅ 所有校验通过,当前数据口径一致,计算结果可信。</div>';
else html = '<div class="err-box">⚠️ 发现 '+issues.length+' 个问题:<br>' + issues.map(function(i){return '• '+i;}).join('<br>') + '</div>';
if(passes.length > 0) html += '<div class="tip" style="background:#e8f5e9;border-left-color:#43a047;color:#2e7d32;"><b>已通过项:</b>' + passes.join(' | ') + '</div>';
html += '<div class="tip"><b>📌 校验项:</b>项目占比=100% | 分配比=100% | 考核分是否超限 | 分红池 vs 实发分红 | 散客现金 | 总提成率≤60% | 充卡/指定客占比合理。<br><b>💡 前提提醒:</b>员工各项目业绩加总应 ≈ 门店总业绩。</div>';
box.innerHTML = html;
}
// ============ 初始化 ============
function init(){
var body = document.getElementById('staff_body'); if(!body) return;
if(body.children.length === 0){
var defaults = [
['hair','发型师A',[30,20,20,10,20]], ['hair','发型师B',[25,10,20,8,18]], ['hair','发型师C',[20,0,15,5,20]],
['wash','洗护师A',[30,20,20,10,20]], ['wash','洗护师B',[25,10,20,8,18]], ['wash','洗护师C',[20,0,15,5,20]],
['assist','助理1',[10,0,0,5,10]]
];
defaults.forEach(function(d){
var tr = createRow(d[0], d[1]); body.appendChild(tr);
var scores = d[2];
['labor','card','zd','rep','manage'].forEach(function(k, i){ var inp = tr.querySelector('.v_s_'+k); if(inp) inp.value = scores[i]; });
});
}
autoAllocate();
}
init();
function downloadPage(){
document.querySelectorAll('input').forEach(function(inp){ inp.setAttribute('value', inp.value); });
var td = document.getElementById('tiers-data'); if(td) td.textContent = JSON.stringify(TIERS);
var html = "<!DOCTYPE html>\n" + document.documentElement.outerHTML;
var blob = new Blob([html], {type:'text/html;charset=utf-8'});
var d = new Date();
var stamp = d.getFullYear()+String(d.getMonth()+1).padStart(2,'0')+String(d.getDate()).padStart(2,'0');
var a = document.createElement('a'); a.href = URL.createObjectURL(blob);
a.download = '门店薪酬核算_'+stamp+'.html';
document.body.appendChild(a); a.click(); document.body.removeChild(a);
setTimeout(function(){ URL.revokeObjectURL(a.href); }, 1000);
}
</script>
</body>
</html>