FP 订单管理跟踪系统

Fixed Price 项目池子 & 订单追踪平台

FP 订单管理跟踪系统

`; } // ============================================================ // 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}
${cards||'
暂无池子数据
'}
`; } // ============================================================ // 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}人
${cards||'
暂无人员数据
'}
`; } // ============================================================ // 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 += `
`; users.forEach(u => { const roleBadge = u.role === 'admin' ? '管理员' : '部门用户'; html += ``; }); html += '
ID用户名显示名角色归属部门创建时间操作
${u.id} ${u.username} ${u.display_name || '—'} ${roleBadge} ${u.department || '全局'} ${(u.created_at || '').split(' ')[0]} ${u.username !== 'admin' ? `` : ''}
'; } 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 += `