486 lines
22 KiB
JavaScript
486 lines
22 KiB
JavaScript
async function loadStats() {
|
||||
|
|
try {
|
|||
|
|
const res = await fetch(API + '/api/stats/monthly?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
|||
|
|
if (!res.ok) return;
|
|||
|
|
const s = await res.json();
|
|||
|
|
document.getElementById('takeCount').textContent = s.take_order_count;
|
|||
|
|
document.getElementById('dispatchCount').textContent = s.dispatch_order_count;
|
|||
|
|
document.getElementById('takeIncome').textContent = '' + s.take_order_income.toFixed(2);
|
|||
|
|
document.getElementById('dispatchIncome').textContent = '' + s.dispatch_order_income.toFixed(2);
|
|||
|
|
document.getElementById('redEnvelopeCount').textContent = s.redEnvelope_count || 0;
|
|||
|
|
document.getElementById('redEnvelopeIncome').textContent = '' + (s.redEnvelope_income || 0).toFixed(2);
|
|||
|
|
document.getElementById('milkTeaCount').textContent = s.milkTea_count || 0;
|
|||
|
|
document.getElementById('milkTeaIncome').textContent = '' + (s.milkTea_income || 0).toFixed(2);
|
|||
|
|
document.getElementById('totalIncome').textContent = '' + s.total_income.toFixed(2);
|
|||
|
|
document.getElementById('totalExpense').textContent = '' + s.total_expense.toFixed(2);
|
|||
|
|
document.getElementById('netIncome').textContent = '' + (s.total_income - s.total_expense).toFixed(2);
|
|||
|
|
loadBossRankingPreview();
|
|||
|
|
loadPartnerRankingPreview();
|
|||
|
|
loadWeeklyChart();
|
|||
|
|
} catch (e) { console.error(e); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadWeeklyChart() {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API + '/api/stats/daily-income', { headers: headers() });
|
|||
|
|
if (!res.ok) return;
|
|||
|
|
const days = await res.json();
|
|||
|
|
const container = document.getElementById('weeklyChartBars');
|
|||
|
|
const max = Math.max(...days.map(d => d.income), 1);
|
|||
|
|
container.innerHTML = days.map(d => {
|
|||
|
|
const pct = Math.max((d.income / max) * 100, 3);
|
|||
|
|
const label = d.date.slice(5).replace('-', '/');
|
|||
|
|
const amount = d.income > 0 ? '' + d.income.toFixed(0) : '0';
|
|||
|
|
return `<div class="weekly-bar-col" onclick="openDailyDetail('${d.date}')">
|
|||
|
|
<span class="weekly-bar-amount">${amount}</span>
|
|||
|
|
<div class="weekly-bar" style="height:${pct}%"></div>
|
|||
|
|
<span class="weekly-bar-date">${label}</span>
|
|||
|
|
</div>`;
|
|||
|
|
}).join('');
|
|||
|
|
} catch (e) { console.error(e); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let currentDailyDate = '';
|
|||
|
|
|
|||
|
|
async function openDailyDetail(date) {
|
|||
|
|
currentDailyDate = date;
|
|||
|
|
document.getElementById('dailyDetailTitle').textContent = date + ' 收入';
|
|||
|
|
document.getElementById('dailyNavDate').textContent = date;
|
|||
|
|
document.getElementById('dailySummary').innerHTML = '<div class="ranking-empty">加载中...</div>';
|
|||
|
|
document.getElementById('dailyDetailList').innerHTML = '';
|
|||
|
|
if (!document.getElementById('dailyDetailPage').classList.contains('active')) {
|
|||
|
|
showPage('dailyDetailPage');
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API + '/api/stats/daily-detail?date=' + date, { headers: headers() });
|
|||
|
|
if (!res.ok) return;
|
|||
|
|
const data = await res.json();
|
|||
|
|
document.getElementById('dailySummary').innerHTML = `
|
|||
|
|
<div class="daily-summary-amount">${data.total_income.toFixed(2)}</div>
|
|||
|
|
<div class="daily-summary-label">当日总收入</div>
|
|||
|
|
`;
|
|||
|
|
const container = document.getElementById('dailyDetailList');
|
|||
|
|
if (data.records.length === 0) {
|
|||
|
|
container.innerHTML = '<div class="ranking-empty">当日暂无收入记录</div>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
container.innerHTML = data.records.map(r => {
|
|||
|
|
const time = formatLocalTime(r.created_at);
|
|||
|
|
const displayCategory = r.category === '红包收入' ? '红包' : (r.category || '-');
|
|||
|
|
return `<div class="order-item" onclick='openEditModal(${JSON.stringify(r).replace(/'/g, "'")})'>
|
|||
|
|
<div class="order-item-top">
|
|||
|
|
<span class="order-item-boss">${displayCategory}${r.boss ? ' · ' + r.boss : ''}</span>
|
|||
|
|
<span class="order-item-amount">${(r.amount || 0).toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="order-item-bottom">
|
|||
|
|
<span>${r.quantity ? r.quantity + '单 × ' + (r.category === '派单' ? (r.rebate || r.amount / r.quantity).toFixed(2) + '(单价' + (r.unit_price || 0).toFixed(2) + ')' : (r.unit_price || 0).toFixed(2)) : (r.source || '')}</span>
|
|||
|
|
<span>${time}${r.partner ? ' · ' + r.partner : ''}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
}).join('');
|
|||
|
|
} catch (e) { console.error(e); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function switchDailyDate(offset) {
|
|||
|
|
const d = new Date(currentDailyDate);
|
|||
|
|
d.setDate(d.getDate() + offset);
|
|||
|
|
openDailyDetail(formatLocalDate(d));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let monthlyDetailYear, monthlyDetailMonth;
|
|||
|
|
|
|||
|
|
function openMonthlyDetail(type) {
|
|||
|
|
monthlyDetailYear = currentYear;
|
|||
|
|
monthlyDetailMonth = currentMonth;
|
|||
|
|
loadMonthlyDetail(type);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function switchMonthlyDetail(type, offset) {
|
|||
|
|
monthlyDetailMonth += offset;
|
|||
|
|
if (monthlyDetailMonth > 12) { monthlyDetailMonth = 1; monthlyDetailYear++; }
|
|||
|
|
if (monthlyDetailMonth < 1) { monthlyDetailMonth = 12; monthlyDetailYear--; }
|
|||
|
|
loadMonthlyDetail(type);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openYearlyIncome() {
|
|||
|
|
loadYearlyNetIncome();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadYearlyNetIncome() {
|
|||
|
|
document.getElementById('yearlyNetIncomeSummary').innerHTML = '<div class="ranking-empty">加载中...</div>';
|
|||
|
|
document.getElementById('yearlyNetIncomeList').innerHTML = '';
|
|||
|
|
|
|||
|
|
if (!document.getElementById('yearlyNetIncomePage').classList.contains('active')) {
|
|||
|
|
showPage('yearlyNetIncomePage');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API + '/api/stats/yearly-net-income', { headers: headers() });
|
|||
|
|
if (!res.ok) return;
|
|||
|
|
const data = await res.json();
|
|||
|
|
|
|||
|
|
const grandNetIncome = (data.grandTotal.total_income || 0) - (data.grandTotal.total_expense || 0);
|
|||
|
|
const grandColor = grandNetIncome >= 0 ? '#27ae60' : '#e74c3c';
|
|||
|
|
document.getElementById('yearlyNetIncomeSummary').innerHTML = `
|
|||
|
|
<div class="daily-summary-amount" style="color:${grandColor}">${grandNetIncome.toFixed(2)}</div>
|
|||
|
|
<div class="daily-summary-label">累计净收入</div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const container = document.getElementById('yearlyNetIncomeList');
|
|||
|
|
const monthNames = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
|
|||
|
|
|
|||
|
|
if (data.yearlyStats.length === 0) {
|
|||
|
|
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
container.innerHTML = data.yearlyStats.map(y => {
|
|||
|
|
const year = y.year;
|
|||
|
|
const income = y.total_income || 0;
|
|||
|
|
const expense = y.total_expense || 0;
|
|||
|
|
const net = income - expense;
|
|||
|
|
const netColor = net >= 0 ? '#27ae60' : '#e74c3c';
|
|||
|
|
|
|||
|
|
const monthlyData = data.monthlyData[year] || [];
|
|||
|
|
const monthlyHtml = monthlyData.map(m => {
|
|||
|
|
const mIncome = m.total_income || 0;
|
|||
|
|
const mExpense = m.total_expense || 0;
|
|||
|
|
const mNet = mIncome - mExpense;
|
|||
|
|
const mColor = mNet >= 0 ? '#27ae60' : '#e74c3c';
|
|||
|
|
return `<div style="display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #f0f0f0;font-size:14px;">
|
|||
|
|
<span>${monthNames[parseInt(m.month) - 1]}</span>
|
|||
|
|
<span style="color:${mColor}">${mNet.toFixed(2)}</span>
|
|||
|
|
</div>`;
|
|||
|
|
}).join('');
|
|||
|
|
|
|||
|
|
return `<div class="order-item" style="padding:0;">
|
|||
|
|
<div class="order-item-top" style="padding:12px;">
|
|||
|
|
<span class="order-item-boss">${year}年</span>
|
|||
|
|
<span class="order-item-amount" style="color:${netColor}">${net.toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="order-item-bottom" style="padding:0 12px 12px;">
|
|||
|
|
<span>收入: ${income.toFixed(2)} | 支出: ${expense.toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div style="border-top:1px solid #eee;">${monthlyHtml}</div>
|
|||
|
|
</div>`;
|
|||
|
|
}).join('');
|
|||
|
|
} catch (e) { console.error(e); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadMonthlyDetail(type) {
|
|||
|
|
const pageId = type === 'income' ? 'monthlyIncomeDetailPage' : 'monthlyExpenseDetailPage';
|
|||
|
|
const titleId = type === 'income' ? 'monthlyIncomeTitle' : 'monthlyExpenseTitle';
|
|||
|
|
const navId = type === 'income' ? 'monthlyIncomeNav' : 'monthlyExpenseNav';
|
|||
|
|
const summaryId = type === 'income' ? 'monthlyIncomeSummary' : 'monthlyExpenseSummary';
|
|||
|
|
const listId = type === 'income' ? 'monthlyIncomeList' : 'monthlyExpenseList';
|
|||
|
|
const label = type === 'income' ? '收入' : '支出';
|
|||
|
|
|
|||
|
|
document.getElementById(titleId).textContent = `${monthlyDetailYear}年${monthlyDetailMonth}月${label}`;
|
|||
|
|
document.getElementById(navId).textContent = `${monthlyDetailYear}年${monthlyDetailMonth}月`;
|
|||
|
|
document.getElementById(summaryId).innerHTML = '<div class="ranking-empty">加载中...</div>';
|
|||
|
|
document.getElementById(listId).innerHTML = '';
|
|||
|
|
|
|||
|
|
if (!document.getElementById(pageId).classList.contains('active')) {
|
|||
|
|
showPage(pageId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API + '/api/stats/monthly-records?type=' + type + '&year=' + monthlyDetailYear + '&month=' + monthlyDetailMonth, { headers: headers() });
|
|||
|
|
if (!res.ok) return;
|
|||
|
|
const data = await res.json();
|
|||
|
|
const color = type === 'income' ? '#27ae60' : '#e74c3c';
|
|||
|
|
document.getElementById(summaryId).innerHTML = `
|
|||
|
|
<div class="daily-summary-amount" style="color:${color}">${data.total.toFixed(2)}</div>
|
|||
|
|
<div class="daily-summary-label">当月总${label}</div>
|
|||
|
|
`;
|
|||
|
|
const container = document.getElementById(listId);
|
|||
|
|
if (data.records.length === 0) {
|
|||
|
|
container.innerHTML = '<div class="ranking-empty">当月暂无' + label + '记录</div>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const grouped = {};
|
|||
|
|
data.records.forEach(r => {
|
|||
|
|
const date = formatLocalDate(r.created_at);
|
|||
|
|
if (!grouped[date]) grouped[date] = [];
|
|||
|
|
grouped[date].push(r);
|
|||
|
|
});
|
|||
|
|
let html = '';
|
|||
|
|
for (const [date, records] of Object.entries(grouped)) {
|
|||
|
|
html += `<div class="order-date-group"><div class="order-date-label">${date}</div>`;
|
|||
|
|
records.forEach(r => {
|
|||
|
|
const time = formatLocalTime(r.created_at);
|
|||
|
|
const displayCategory = r.category === '红包收入' ? '红包' : (r.category || '-');
|
|||
|
|
html += `<div class="order-item" onclick='openEditModal(${JSON.stringify(r).replace(/'/g, "'")})'>
|
|||
|
|
<div class="order-item-top">
|
|||
|
|
<span class="order-item-boss">${displayCategory}${r.boss ? ' · ' + r.boss : r.source ? ' · ' + r.source : r.destination ? ' · ' + r.destination : ''}</span>
|
|||
|
|
<span class="order-item-amount">${type === 'expense' ? '-' : ''}${(r.amount || 0).toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="order-item-bottom">
|
|||
|
|
<span>${r.quantity ? r.quantity + '单 × ' + (r.category === '派单' ? (r.rebate || r.amount / r.quantity).toFixed(2) + '(单价' + (r.unit_price || 0).toFixed(2) + ')' : (r.unit_price || 0).toFixed(2)) : (r.note || '')}</span>
|
|||
|
|
<span>${time}${r.partner ? ' · ' + r.partner : ''}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
});
|
|||
|
|
html += '</div>';
|
|||
|
|
}
|
|||
|
|
container.innerHTML = html;
|
|||
|
|
} catch (e) { console.error(e); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadBossRankingPreview() {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API + '/api/stats/boss-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
|||
|
|
if (!res.ok) return;
|
|||
|
|
const rows = await res.json();
|
|||
|
|
const container = document.getElementById('rankingPreviewList');
|
|||
|
|
if (rows.length === 0) {
|
|||
|
|
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const max = Math.max(...rows.map(r => r.total_income), 1);
|
|||
|
|
container.innerHTML = rows.map((r, i) => {
|
|||
|
|
const pct = Math.max((r.total_income / max) * 100, 2);
|
|||
|
|
return `<div class="hbar-row" onclick="openBossDetail('${r.boss.replace(/'/g, "\\'")}', 'homePage')">
|
|||
|
|
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
|||
|
|
<div class="hbar-label" title="${r.boss}">${r.boss}</div>
|
|||
|
|
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
|
|||
|
|
<div class="hbar-value">${r.total_income.toFixed(2)}</div>
|
|||
|
|
</div>`;
|
|||
|
|
}).join('');
|
|||
|
|
} catch (e) { console.error(e); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let currentBossName = '';
|
|||
|
|
let bossDetailFrom = 'homePage';
|
|||
|
|
|
|||
|
|
function openBossDetail(boss, from) {
|
|||
|
|
currentBossName = boss;
|
|||
|
|
bossDetailFrom = from || 'homePage';
|
|||
|
|
loadBossDetail();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function bossDetailGoBack() {
|
|||
|
|
showPage(bossDetailFrom);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadBossDetail() {
|
|||
|
|
document.getElementById('bossDetailTitle').textContent = currentBossName + ' 交易记录';
|
|||
|
|
document.getElementById('bossDetailSummary').innerHTML = '<div class="ranking-empty">加载中...</div>';
|
|||
|
|
document.getElementById('bossDetailList').innerHTML = '';
|
|||
|
|
showPage('bossDetailPage');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API + '/api/stats/boss-records?boss=' + encodeURIComponent(currentBossName), { headers: headers() });
|
|||
|
|
if (!res.ok) return;
|
|||
|
|
const data = await res.json();
|
|||
|
|
document.getElementById('bossDetailSummary').innerHTML = `
|
|||
|
|
<div style="display:flex;justify-content:space-around;flex-wrap:wrap;gap:8px">
|
|||
|
|
<div style="text-align:center">
|
|||
|
|
<div class="daily-summary-amount" style="font-size:24px;color:#e74c3c">${data.boss_total_expense.toFixed(2)}</div>
|
|||
|
|
<div class="daily-summary-label">老板总支出</div>
|
|||
|
|
</div>
|
|||
|
|
<div style="text-align:center">
|
|||
|
|
<div class="daily-summary-amount" style="font-size:24px;color:#27ae60">${data.my_income.toFixed(2)}</div>
|
|||
|
|
<div class="daily-summary-label">我的总收入</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
const container = document.getElementById('bossDetailList');
|
|||
|
|
if (data.records.length === 0) {
|
|||
|
|
container.innerHTML = '<div class="ranking-empty">暂无交易记录</div>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const grouped = {};
|
|||
|
|
data.records.forEach(r => {
|
|||
|
|
const d = new Date(r.created_at);
|
|||
|
|
const key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
|
|||
|
|
if (!grouped[key]) grouped[key] = { records: [], total: 0 };
|
|||
|
|
grouped[key].records.push(r);
|
|||
|
|
grouped[key].total += r.amount || 0;
|
|||
|
|
});
|
|||
|
|
let html = '';
|
|||
|
|
for (const [month, group] of Object.entries(grouped)) {
|
|||
|
|
html += `<div class="order-date-group"><div class="order-date-label">${month} 收入 ${group.total.toFixed(2)}</div>`;
|
|||
|
|
group.records.forEach(r => {
|
|||
|
|
const time = formatLocalDate(r.created_at) + ' ' + formatLocalTime(r.created_at);
|
|||
|
|
const displayCategory = r.category === '红包收入' ? '红包' : r.category;
|
|||
|
|
html += `<div class="order-item" onclick='openEditModal(${JSON.stringify(r).replace(/'/g, "'")})'>
|
|||
|
|
<div class="order-item-top">
|
|||
|
|
<span class="order-item-boss">${displayCategory}${r.partner ? ' · ' + r.partner : ''}</span>
|
|||
|
|
<span class="order-item-amount">${(r.amount || 0).toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="order-item-bottom">
|
|||
|
|
<span>${r.quantity ? r.quantity + '单 × ' + (r.category === '派单' ? (r.rebate || r.amount / r.quantity).toFixed(2) + '(单价' + (r.unit_price || 0).toFixed(2) + ')' : (r.unit_price || 0).toFixed(2)) : (r.note || '')}</span>
|
|||
|
|
<span>${time}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
});
|
|||
|
|
html += '</div>';
|
|||
|
|
}
|
|||
|
|
container.innerHTML = html;
|
|||
|
|
} catch (e) { console.error(e); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadPartnerRankingPreview() {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API + '/api/stats/partner-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
|||
|
|
if (!res.ok) return;
|
|||
|
|
const rows = await res.json();
|
|||
|
|
const container = document.getElementById('partnerRankingPreviewList');
|
|||
|
|
if (rows.length === 0) {
|
|||
|
|
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const max = Math.max(...rows.map(r => r.dispatch_count), 1);
|
|||
|
|
container.innerHTML = rows.map((r, i) => {
|
|||
|
|
const pct = Math.max((r.dispatch_count / max) * 100, 2);
|
|||
|
|
return `<div class="hbar-row" onclick="openPartnerDetail('${r.partner.replace(/'/g, "\\'")}', 'homePage')">
|
|||
|
|
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
|||
|
|
<div class="hbar-label" title="${r.partner}">${r.partner}</div>
|
|||
|
|
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
|
|||
|
|
<div class="hbar-value">${r.dispatch_count}单</div>
|
|||
|
|
</div>`;
|
|||
|
|
}).join('');
|
|||
|
|
} catch (e) { console.error(e); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openPartnerRankingPage() {
|
|||
|
|
const now = new Date();
|
|||
|
|
const y = now.getFullYear();
|
|||
|
|
const m = String(now.getMonth() + 1).padStart(2, '0');
|
|||
|
|
document.getElementById('partnerRankStartDate').value = y + '-' + m + '-01';
|
|||
|
|
document.getElementById('partnerRankEndDate').value = y + '-' + m + '-' + String(now.getDate()).padStart(2, '0');
|
|||
|
|
showPage('partnerRankingPage');
|
|||
|
|
loadPartnerRankingDetail();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadPartnerRankingDetail() {
|
|||
|
|
const startDate = document.getElementById('partnerRankStartDate').value;
|
|||
|
|
const endDate = document.getElementById('partnerRankEndDate').value;
|
|||
|
|
let url = API + '/api/stats/partner-ranking';
|
|||
|
|
const params = [];
|
|||
|
|
if (startDate) params.push('startDate=' + startDate);
|
|||
|
|
if (endDate) {
|
|||
|
|
const d = new Date(endDate);
|
|||
|
|
d.setDate(d.getDate() + 1);
|
|||
|
|
params.push('endDate=' + formatLocalDate(d));
|
|||
|
|
}
|
|||
|
|
if (params.length) url += '?' + params.join('&');
|
|||
|
|
console.log('partner-ranking url:', url);
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(url, { headers: headers() });
|
|||
|
|
console.log('partner-ranking res.ok:', res.ok);
|
|||
|
|
if (!res.ok) return;
|
|||
|
|
const data = await res.json();
|
|||
|
|
console.log('partner-ranking data:', data);
|
|||
|
|
const rows = data.rows || [];
|
|||
|
|
console.log('rows:', rows);
|
|||
|
|
const summary = data.summary || { total_partner_income: 0, total_rebate_income: 0, total_milkTea_income: 0 };
|
|||
|
|
|
|||
|
|
// 计算派单总数(从列表)
|
|||
|
|
const totalDispatch = rows.reduce((sum, r) => sum + (r.dispatch_count || 0), 0);
|
|||
|
|
console.log('totalDispatch:', totalDispatch);
|
|||
|
|
|
|||
|
|
// 更新统计卡片
|
|||
|
|
document.getElementById('partnerTotalDispatchCount').textContent = totalDispatch;
|
|||
|
|
document.getElementById('totalPartnerIncome').textContent = (summary.total_partner_income || 0).toFixed(2);
|
|||
|
|
document.getElementById('totalRebateIncome').textContent = summary.total_rebate_income.toFixed(2);
|
|||
|
|
document.getElementById('totalMilkTeaIncome').textContent = (summary.total_milkTea_income || 0).toFixed(2);
|
|||
|
|
|
|||
|
|
const container = document.getElementById('partnerRankingDetailList');
|
|||
|
|
if (rows.length === 0) {
|
|||
|
|
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const max = Math.max(...rows.map(r => r.dispatch_count), 1);
|
|||
|
|
container.innerHTML = rows.map((r, i) => {
|
|||
|
|
const pct = Math.max((r.dispatch_count / max) * 100, 2);
|
|||
|
|
return `<div class="hbar-row" onclick="openPartnerDetail('${r.partner.replace(/'/g, "\\'")}', 'partnerRankingPage')">
|
|||
|
|
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
|||
|
|
<div class="hbar-label" title="${r.partner}">${r.partner}</div>
|
|||
|
|
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
|
|||
|
|
<div class="hbar-value">${r.dispatch_count}单</div>
|
|||
|
|
</div>`;
|
|||
|
|
}).join('');
|
|||
|
|
} catch (e) { console.error(e); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let currentPartnerName = '';
|
|||
|
|
let partnerDetailFrom = 'homePage';
|
|||
|
|
|
|||
|
|
function openPartnerDetail(partner, from) {
|
|||
|
|
currentPartnerName = partner;
|
|||
|
|
partnerDetailFrom = from || 'homePage';
|
|||
|
|
loadPartnerDetail();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function partnerDetailGoBack() {
|
|||
|
|
showPage(partnerDetailFrom);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadPartnerDetail() {
|
|||
|
|
document.getElementById('partnerDetailTitle').textContent = currentPartnerName + ' 交易记录';
|
|||
|
|
document.getElementById('partnerDetailSummary').innerHTML = '<div class="ranking-empty">加载中...</div>';
|
|||
|
|
document.getElementById('partnerDetailList').innerHTML = '';
|
|||
|
|
showPage('partnerDetailPage');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(API + '/api/stats/partner-records?partner=' + encodeURIComponent(currentPartnerName), { headers: headers() });
|
|||
|
|
if (!res.ok) return;
|
|||
|
|
const data = await res.json();
|
|||
|
|
document.getElementById('partnerDetailSummary').innerHTML = `
|
|||
|
|
<div style="display:flex;justify-content:space-around;flex-wrap:wrap;gap:8px">
|
|||
|
|
<div style="text-align:center">
|
|||
|
|
<div class="daily-summary-amount" style="font-size:24px">${data.dispatch_count}</div>
|
|||
|
|
<div class="daily-summary-label">总派单数</div>
|
|||
|
|
</div>
|
|||
|
|
<div style="text-align:center">
|
|||
|
|
<div class="daily-summary-amount" style="font-size:24px;color:#e67e22">${data.spread_income.toFixed(2)}</div>
|
|||
|
|
<div class="daily-summary-label">陪玩总收入</div>
|
|||
|
|
</div>
|
|||
|
|
<div style="text-align:center">
|
|||
|
|
<div class="daily-summary-amount" style="font-size:24px;color:#27ae60">${data.rebate_income.toFixed(2)}</div>
|
|||
|
|
<div class="daily-summary-label">总返点收入</div>
|
|||
|
|
</div>
|
|||
|
|
<div style="text-align:center">
|
|||
|
|
<div class="daily-summary-amount" style="font-size:24px;color:#3498db">${data.other_income.toFixed(2)}</div>
|
|||
|
|
<div class="daily-summary-label">红包及其他</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
const container = document.getElementById('partnerDetailList');
|
|||
|
|
if (data.records.length === 0) {
|
|||
|
|
container.innerHTML = '<div class="ranking-empty">暂无交易记录</div>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const grouped = {};
|
|||
|
|
data.records.forEach(r => {
|
|||
|
|
const d = new Date(r.created_at);
|
|||
|
|
const key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
|
|||
|
|
if (!grouped[key]) grouped[key] = { records: [], total: 0 };
|
|||
|
|
grouped[key].records.push(r);
|
|||
|
|
grouped[key].total += r.amount || 0;
|
|||
|
|
});
|
|||
|
|
let html = '';
|
|||
|
|
for (const [month, group] of Object.entries(grouped)) {
|
|||
|
|
html += `<div class="order-date-group"><div class="order-date-label">${month} 收入 ${group.total.toFixed(2)}</div>`;
|
|||
|
|
group.records.forEach(r => {
|
|||
|
|
const time = formatLocalDate(r.created_at) + ' ' + formatLocalTime(r.created_at);
|
|||
|
|
const displayCategory = r.category === '红包收入' ? '红包' : r.category;
|
|||
|
|
html += `<div class="order-item" onclick='openEditModal(${JSON.stringify(r).replace(/'/g, "'")})'>
|
|||
|
|
<div class="order-item-top">
|
|||
|
|
<span class="order-item-boss">${displayCategory}${r.boss ? ' · ' + r.boss : ''}</span>
|
|||
|
|
<span class="order-item-amount">${(r.amount || 0).toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="order-item-bottom">
|
|||
|
|
<span>${r.quantity ? r.quantity + '单 × ' + (r.category === '派单' ? (r.rebate || r.amount / r.quantity).toFixed(2) + '(单价' + (r.unit_price || 0).toFixed(2) + ')' : (r.unit_price || 0).toFixed(2)) : (r.source || '')}</span>
|
|||
|
|
<span>${time}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
});
|
|||
|
|
html += '</div>';
|
|||
|
|
}
|
|||
|
|
container.innerHTML = html;
|
|||
|
|
} catch (e) { console.error(e); }
|
|||
|
|
}
|