`;
}
// ============================================================
// POOLS
// ============================================================
function renderPools() {
const btns = USER.role==='admin' ? `` : '';
const cards = POOLS.map(p => {
const used = getPoolAmount(p.pid);
const pct = p.totalAmount>0?(used/p.totalAmount*100).toFixed(1):0;
const cls = pct>100?'danger':pct>=95?'warn':'ok';
const unalloc = p.totalAmount - used;
const orders = getPoolOrders(p.pid);
return `
${p.pid} ${p.name}
${p.dept} · ${p.startDate||'?'} ~ ${p.endDate||'?'} · ${p.status}
池子总额${fmtMoney(p.totalAmount)}
已分配 (${orders.length}笔)${fmtMoney(used)}
未分配${fmtMoney(Math.max(0,unalloc))}
分配率 ${pct}%${pct>100?' ⚠超分配':''} ${unalloc>0&&pct<95?'⚠建议补充':''}
${USER.role==='admin'?`
`:''}
`;
}).join('');
return `
项目池列表 ${POOLS.length}
${btns}
`;
}
// ============================================================
// ORDERS
// ============================================================
function renderOrders() {
const filterPid = new URLSearchParams(window._orderFilter||'').get('pid')||'';
const filterStatus = new URLSearchParams(window._orderFilter||'').get('status')||'';
const filterPerson = new URLSearchParams(window._orderFilter||'').get('person')||'';
const filterCross = new URLSearchParams(window._orderFilter||'').get('cross')||'';
let filtered = [...ORDERS];
if (filterPid) filtered = filtered.filter(o=>o.pid===filterPid);
if (filterStatus) filtered = filtered.filter(o=>o.status===filterStatus);
if (filterPerson) filtered = filtered.filter(o=>o.person===filterPerson);
if (filterCross==='yes') filtered = filtered.filter(o=>o.crossDept);
if (filterCross==='no') filtered = filtered.filter(o=>!o.crossDept);
const persons = [...new Set(ORDERS.map(o=>o.person))].sort();
const pids = [...new Set(ORDERS.map(o=>o.pid))].sort();
const statuses = ['待确认','已确认','执行中','已结算','已关闭'];
const btns = USER.role==='admin' ? `` : '';
const rows = filtered.map(o => `
| ${o.id} |
${o.pid} | ${o.name} | ${fmtMoney(o.amount)} |
${o.status} |
${o.person} | ${o.personDept} |
${o.settleDept}${o.crossDept?'跨部门':''} |
${o.revenueConfirmed?''+fmtMoney(o.revenueAmount)+'':'—'} |
${o.settleStatus} |
${fmtCycle(o.orderStartDate,o.orderEndDate)} |
${USER.role==='admin'?` | `:''}
`).join('');
return `
订单列表 ${filtered.length}/${ORDERS.length}
${btns}
| 订单号 | 池子 | 名称 | 金额 | 状态 | 负责人 | 归属部门 | 结算部门 | 确认收入 | 结算状态 | 订单周期 | ${USER.role==='admin'?'操作 | ':''}
${rows||'| 无匹配订单 |
'}
`;
}
function applyOrderFilter() {
const pid = document.getElementById('fPid')?.value||'';
const status = document.getElementById('fStatus')?.value||'';
const person = document.getElementById('fPerson')?.value||'';
const cross = document.getElementById('fCross')?.value||'';
window._orderFilter = `pid=${pid}&status=${status}&person=${person}&cross=${cross}`;
document.getElementById('mainContent').innerHTML = renderOrders();
}
function clearOrderFilter() {
window._orderFilter = '';
document.getElementById('mainContent').innerHTML = renderOrders();
}
// ============================================================
// PERSONS
// ============================================================
function renderPersons() {
const persons = getPersons();
const cards = persons.map(p => {
const orders = getPersonOrders(p.name);
const total = orders.reduce((s,o)=>s+o.amount,0);
const active = orders.filter(o=>o.status!=='已结算'&&o.status!=='已关闭');
// Idle check
let idleWarn = ''; let isIdle = false;
if (active.length > 0) {
let latestEnd = '1970-01-01';
active.forEach(o => { if(o.orderEndDate && o.orderEndDate>latestEnd) latestEnd = o.orderEndDate; });
const daysLeft = daysFrom(latestEnd);
if (daysLeft > 0 && daysLeft <= 60) {
const future = active.filter(o => o.orderStartDate && o.orderStartDate > latestEnd);
if (future.length === 0 && latestEnd !== '1970-01-01') {
isIdle = true;
idleWarn = `⚠ 最后订单 ${latestEnd} 结束后 (${daysLeft}天后) 无后续安排
`;
}
}
}
const orderItems = orders.slice(0,4).map(o => `${o.id} ${o.name}${fmtMoney(o.amount)}
`).join('');
const more = orders.length > 4 ? `...还有 ${orders.length-4} 笔
` : '';
return `
${p.name}
${p.dept}
订单总额${fmtMoney(total)}
订单数${orders.length} 笔
${orderItems}${more}
${idleWarn}
`;
}).join('');
return `人员负载视图 ${persons.length}人
`;
}
// ============================================================
// ALERTS
// ============================================================
function detectAlerts() {
const alerts = [];
// 1. Pool over-allocation
POOLS.forEach(p => {
const used = getPoolAmount(p.pid);
if (used > p.totalAmount) {
alerts.push({t:'超池子分配',s:'danger',d:`${p.pid} ${p.name}: 订单总额 ${fmtMoney(used)} > 池子总额 ${fmtMoney(p.totalAmount)},超出 ${fmtMoney(used-p.totalAmount)}`});
}
});
// 2. Duplicate revenue per person per pool
const personPoolRev = {};
ORDERS.filter(o=>o.revenueConfirmed).forEach(o => {
const k = o.person+'|'+o.pid;
if (!personPoolRev[k]) personPoolRev[k] = {person:o.person,pid:o.pid,total:0,orders:[]};
personPoolRev[k].total += o.revenueAmount;
personPoolRev[k].orders.push(o.id);
});
Object.values(personPoolRev).forEach(r => {
if (r.orders.length >= 2) {
const pool = POOLS.find(p=>p.pid===r.pid);
if (pool && r.total > pool.totalAmount * 0.5) {
alerts.push({t:'重复确认嫌疑',s:'danger',d:`${r.person} 在 ${r.pid} 下有 ${r.orders.length} 笔收入确认 (${r.orders.join(',')}),合计 ${fmtMoney(r.total)},超过池子50%`});
}
}
});
// 3. Pool under-allocation
POOLS.forEach(p => {
const used = getPoolAmount(p.pid);
const pct = p.totalAmount>0?(used/p.totalAmount*100):0;
const unalloc = p.totalAmount - used;
if (pct < 95 && unalloc > p.totalAmount*0.05) {
const daysLeft = daysFrom(p.endDate);
alerts.push({t:'池子未充分利用',s:daysLeft<60?'danger':'warn',d:`${p.pid} ${p.name}: 分配率仅 ${pct.toFixed(1)}%,未分配 ${fmtMoney(unalloc)}${daysLeft<60?',项目剩余不足60天':''}`});
}
});
// 4. Person idle
const persons = getPersons();
persons.forEach(p => {
const active = getPersonOrders(p.name).filter(o=>o.status!=='已结算'&&o.status!=='已关闭');
if (active.length===0) return;
let latestEnd = '1970-01-01';
active.forEach(o => { if(o.orderEndDate && o.orderEndDate>latestEnd) latestEnd=o.orderEndDate; });
const daysLeft = daysFrom(latestEnd);
if (daysLeft > 0 && daysLeft <= 60) {
const future = active.filter(o=>o.orderStartDate && o.orderStartDate>latestEnd);
if (future.length===0) {
alerts.push({t:'人员即将空闲',s:daysLeft<=30?'danger':'warn',d:`${p.name} (${p.dept}): 最后订单 ${active.map(o=>o.orderEndDate).filter(Boolean).sort().pop()},${daysLeft}天后无后续安排`});
}
}
// Gap check
const sorted = active.filter(o=>o.orderEndDate&&o.orderStartDate).sort((a,b)=>a.orderEndDate>b.orderEndDate?1:-1);
for (let i=0;i 30) {
alerts.push({t:'排期空档',s:'warn',d:`${p.name}: ${sorted[i].orderEndDate} ~ ${sorted[i+1].orderStartDate} 之间空档 ${gap} 天`});
}
}
});
// 5. Stale orders
ORDERS.forEach(o => {
if (o.status === '待确认' && o.created) {
const age = Math.floor((new Date()-new Date(o.created))/86400000);
if (age > 90) alerts.push({t:'长期待确认订单',s:'warn',d:`${o.id} ${o.name}: 创建 ${age} 天仍未确认`});
}
});
return alerts;
}
function countAlerts() { return detectAlerts().length; }
function renderAlerts() {
const alerts = detectAlerts();
return `预警中心 ${alerts.length} 项
${alerts.length===0?'✅ 当前无预警项
':''}
${alerts.map(a => `
${a.s==='danger'?'高风险':'注意'} ${a.t}
${a.d}
`).join('')}`;
}
// ============================================================
// CROSS DEPT
// ============================================================
function renderCrossDept() {
const cross = ORDERS.filter(o => o.crossDept);
const rows = cross.map(o => `
| ${o.id} | ${o.pid} | ${o.name} |
${o.personDept} |
${o.settleDept} |
${fmtMoney(o.amount)} |
${o.status} |
${o.person} |
${fmtCycle(o.orderStartDate,o.orderEndDate)} |
`).join('');
const summary = {}; cross.forEach(o => {
const k = o.personDept+'→'+o.settleDept;
if(!summary[k]) summary[k]={count:0,amount:0};
summary[k].count++; summary[k].amount+=o.amount;
});
return `跨部门结算订单 ${cross.length} 笔
${Object.entries(summary).map(([k,v])=>`
${k}
订单数${v.count} 笔
金额${fmtMoney(v.amount)}
`).join('')||'
无跨部门订单
'}
| 订单号 | 池子 | 名称 | 归属部门 | 结算部门 | 金额 | 状态 | 负责人 | 周期 |
${rows||'| 无跨部门订单 |
'}
`;
}
// ============================================================
// DAILY
// ============================================================
function renderDaily() {
const persons = getPersons();
const rows = persons.map(p => {
const orders = getPersonOrders(p.name).filter(o => o.status!=='已关闭');
const total = orders.reduce((s,o)=>s+o.amount,0);
const confirmed = orders.filter(o=>o.revenueConfirmed).reduce((s,o)=>s+o.revenueAmount,0);
const statuses = [...new Set(orders.map(o=>o.status))];
const riskCount = statuses.filter(s=>s==='待确认').length;
// Next idle date
let nextIdle = '—';
const active = orders.filter(o=>o.status!=='已结算');
if (active.length > 0) {
let latestEnd = '1970-01-01';
active.forEach(o => { if(o.orderEndDate && o.orderEndDate>latestEnd) latestEnd=o.orderEndDate; });
if (latestEnd !== '1970-01-01') nextIdle = latestEnd;
}
const isRisk = riskCount > 0;
return `
| ${p.name} | ${p.dept} | ${orders.length} |
${fmtMoney(total)} | ${fmtMoney(confirmed)} |
${statuses.map(s=>`${s}`).join(' ')} |
${nextIdle} |
${isRisk?'需关注':'正常'} |
`;
}).join('');
return `每日人员追踪 ${new Date().toISOString().split('T')[0]}
| 姓名 | 部门 | 订单数 | 订单总额 | 已确认收入 | 状态分布 | 下一空闲日 | 状态 |
${rows||'| 暂无数据 |
'}
`;
}
// ============================================================
// MODALS - Pool
// ============================================================
function showPoolModal(pid) {
const pool = pid ? POOLS.find(p=>p.pid===pid) : null;
document.getElementById('modalPoolContent').innerHTML = `
${pool?'编辑':'新建'}项目池
`;
document.getElementById('modalPool').classList.add('show');
}
async function savePool(pid) {
const data = {
pid: document.getElementById('mpPid').value.trim(),
name: document.getElementById('mpName').value.trim(),
totalAmount: Number(document.getElementById('mpAmount').value),
dept: document.getElementById('mpDept').value.trim(),
startDate: document.getElementById('mpStart').value,
endDate: document.getElementById('mpEnd').value,
status: document.getElementById('mpStatus').value,
};
if (!data.pid||!data.name) return alert('项目编号和名称不能为空');
try {
if (pid) await api('/api/pools/'+pid, {method:'PUT',body:JSON.stringify(data)});
else await api('/api/pools', {method:'POST',body:JSON.stringify(data)});
closeModal('modalPool');
await loadData();
switchTab('pools');
} catch(e) { alert(e.message); }
}
// ============================================================
// MODALS - Order
// ============================================================
function showOrderModal(id) {
const order = id ? ORDERS.find(o=>o.id===id) : null;
document.getElementById('modalOrderContent').innerHTML = `
${order?'编辑':'新建'}订单
${order?``:''}
`;
document.getElementById('modalOrder').classList.add('show');
}
async function saveOrder(id) {
const data = {
id: document.getElementById('moId').value.trim(),
pid: document.getElementById('moPid').value,
name: document.getElementById('moName').value.trim(),
amount: Number(document.getElementById('moAmount').value),
status: document.getElementById('moStatus').value,
person: document.getElementById('moPerson').value.trim(),
personDept: document.getElementById('moPersonDept').value.trim(),
settleDept: document.getElementById('moSettleDept').value.trim(),
revenueConfirmed: document.getElementById('moConfirmed').value==='1',
revenueAmount: Number(document.getElementById('moRevenue').value),
settleStatus: document.getElementById('moSettleStatus').value,
settleDate: document.getElementById('moSettleDate').value,
orderStartDate: document.getElementById('moStart').value,
orderEndDate: document.getElementById('moEnd').value,
crossDept: document.getElementById('moPersonDept').value.trim() !== document.getElementById('moSettleDept').value.trim(),
};
if (!data.id||!data.pid) return alert('订单编号和所属池子不能为空');
try {
if (id) await api('/api/orders/'+id, {method:'PUT',body:JSON.stringify(data)});
else await api('/api/orders', {method:'POST',body:JSON.stringify(data)});
closeModal('modalOrder');
await loadData();
switchTab('orders');
} catch(e) { alert(e.message); }
}
async function deleteOrder(id) {
if (!confirm('确定删除订单 '+id+'?此操作不可恢复!')) return;
try {
await api('/api/orders/'+id, {method:'DELETE'});
closeModal('modalOrder');
await loadData();
switchTab('orders');
} catch(e) { alert(e.message); }
}
// ============================================================
// MODALS - Import
// ============================================================
function showImportModal() {
document.getElementById('modalImportContent').innerHTML = `
导入 Excel / CSV
支持 .xlsx / .xls / .csv 格式。列名需包含:pid/项目编号、name/项目名称、totalAmount/池子金额 等
`;
document.getElementById('modalImport').classList.add('show');
document.getElementById('importFile').addEventListener('change', async function() {
const file = this.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async function(e) {
let pools=[], orders=[];
try {
if (file.name.endsWith('.csv')) {
const lines = e.target.result.split('\n').filter(l=>l.trim());
const header = lines[0].split(',').map(h=>h.trim().replace(/"/g,''));
lines.slice(1).forEach(l => {
const vals = l.split(',').map(v=>v.trim().replace(/"/g,''));
const row = {}; header.forEach((h,i)=>row[h]=vals[i]);
if (row.pid || row['项目编号']) {
pools.push({
pid: row.pid||row['项目编号'], name: row.name||row['项目名称'],
totalAmount: Number(row.totalAmount||row['池子金额']||0),
dept: row.dept||row['交付部门'], startDate: row.startDate||row['开始日期'],
endDate: row.endDate||row['结束日期'], status: row.status||row['项目状态']||'进行中'
});
}
});
} else {
// For xlsx, use simple parsing via readAsArrayBuffer
const XLSX = await loadXLSX();
const wb = XLSX.read(new Uint8Array(e.target.result), {type:'array'});
wb.SheetNames.forEach(name => {
const ws = wb.Sheets[name];
const data = XLSX.utils.sheet_to_json(ws, {header:1});
if (data.length < 2) return;
const headers = data[0].map(h=>String(h||'').trim());
const isPool = headers.some(h=>h.includes('池子')||h==='pid');
const isOrder = headers.some(h=>h==='id'||h==='订单编号'||h.includes('订单号'));
data.slice(1).forEach(row => {
const obj = {}; headers.forEach((h,i)=>obj[h]=row[i]);
if (isPool || (!isOrder && obj.pid)) {
pools.push({
pid: String(obj.pid||obj['项目编号']||''), name: String(obj.name||obj['项目名称']||''),
totalAmount: Number(obj.totalAmount||obj['池子金额']||0),
dept: String(obj.dept||obj['交付部门']||''), startDate: String(obj.startDate||obj['开始日期']||''),
endDate: String(obj.endDate||obj['结束日期']||''), status: String(obj.status||obj['项目状态']||'进行中')
});
} else if (isOrder || obj.id) {
orders.push({
id: String(obj.id||obj['订单编号']||obj['订单号']||''),
pid: String(obj.pid||obj['项目编号']||''), name: String(obj.name||obj['订单名称']||''),
amount: Number(obj.amount||obj['订单金额']||0),
status: String(obj.status||obj['订单状态']||'待确认'),
person: String(obj.person||obj['负责人']||''),
personDept: String(obj.personDept||obj['归属部门']||''),
settleDept: String(obj.settleDept||obj['结算部门']||''),
revenueConfirmed: obj.revenueConfirmed==='是'||obj.revenueConfirmed===1||obj['收入已确认']==='是',
revenueAmount: Number(obj.revenueAmount||obj['确认收入金额']||0),
settleStatus: String(obj.settleStatus||obj['结算状态']||'待结算'),
settleDate: obj.settleDate||obj['结算日期']||null,
crossDept: String(obj.personDept||obj['归属部门']) !== String(obj.settleDept||obj['结算部门']),
orderStartDate: String(obj.orderStartDate||obj['开始日期']||obj['订单开始日期']||''),
orderEndDate: String(obj.orderEndDate||obj['结束日期']||obj['订单结束日期']||''),
created: String(obj.created||''), updated: String(obj.updated||''),
});
}
});
});
}
} catch(err) { console.error('Parse error:', err); }
document.getElementById('importPreview').innerHTML = `解析结果:${pools.length} 个池子, ${orders.length} 笔订单`;
document.getElementById('btnImportDo').disabled = (pools.length===0&&orders.length===0);
window._importData = { pools, orders };
};
if (file.name.endsWith('.csv')) reader.readAsText(file, 'UTF-8');
else reader.readAsArrayBuffer(file);
});
}
async function loadXLSX() {
if (window.XLSX) return window.XLSX;
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.sheetjs.com/xlsx-0.20.1/package/dist/xlsx.full.min.js';
script.onload = () => resolve(window.XLSX);
script.onerror = reject;
document.head.appendChild(script);
});
}
async function doImport() {
if (!window._importData) return;
const { pools, orders } = window._importData;
const mode = document.getElementById('importMode').value;
try {
await api('/api/import', {method:'POST',body:JSON.stringify({pools,orders,mode})});
closeModal('modalImport');
await loadData();
switchTab('dashboard');
alert(`导入成功:${pools.length} 个池子, ${orders.length} 笔订单 (${mode==='replace'?'已全量替换':'已增量合并'})`);
} catch(e) { alert('导入失败: '+e.message); }
}
// ============================================================
// ADMIN PORTAL (管理员门户)
// ============================================================
function renderAdminPortal() {
if (USER.role !== 'admin') return '您没有管理员权限
';
return `
管理员门户
`;
}
async function initAdminPortal() {
await loadAdminUsers();
await loadAdminDepts();
await loadAdminOverview();
}
function switchAdminTab(key) {
document.querySelectorAll('.admin-sidebar .as-item').forEach(el => el.classList.toggle('active', el.dataset.ap === key));
document.querySelectorAll('.admin-section').forEach(el => el.classList.toggle('active', el.dataset.ap === key));
}
// ---------- 用户管理 ----------
async function loadAdminUsers() {
const container = document.getElementById('ap-users');
try {
const res = await api('/api/users');
const users = res.users;
let html = `
用户列表 ${users.length}
`;
if (users.length === 0) {
html += '暂无用户
';
} else {
html += `
| ID | 用户名 | 显示名 | 角色 | 归属部门 | 创建时间 | 操作 |
`;
users.forEach(u => {
const roleBadge = u.role === 'admin'
? '管理员'
: '部门用户';
html += `
| ${u.id} |
${u.username} |
${u.display_name || '—'} |
${roleBadge} |
${u.department || '全局'} |
${(u.created_at || '').split(' ')[0]} |
${u.username !== 'admin' ? `` : ''}
|
`;
});
html += '
';
}
html += '';
container.innerHTML = html;
} catch (e) {
container.innerHTML = `加载失败: ${e.message}
`;
}
}
function showUserEditPanel(id) {
const panel = document.getElementById('userEditPanel');
if (!panel) return;
let u = { id: 0, username: '', role: 'department', department: '', display_name: '' };
if (id) {
// Find existing user data from the table
const rows = document.querySelectorAll('#ap-users .admin-user-row');
rows.forEach(r => {
if (r.cells[0].textContent == id) {
u.id = id;
u.username = r.cells[1].textContent.trim();
u.role = r.cells[3].textContent.includes('管理员') ? 'admin' : 'department';
const deptCell = r.cells[4].textContent.trim();
u.department = deptCell === '全局' || deptCell === '—' ? '' : deptCell;
u.display_name = r.cells[2].textContent.trim();
if (u.display_name === '—') u.display_name = '';
}
});
}
panel.innerHTML = `
${id?'编辑用户': '新建用户'} ${id?`#${id}`:''}
`;
// Load departments for datalist
setTimeout(async () => {
try {
const res = await api('/api/departments');
const dl = document.getElementById('deptList');
if (dl) res.departments.forEach(d => { dl.innerHTML += `