Files
time-tracking-system/static/js/statistics.js
bf1942 8938ce2708 refactor(api): 重构数据库访问为SQLAlchemy绑定的session
- 统一移除手动创建的数据库session,统一使用models模块中的db.session
- 修正项目创建接口,增加开始和结束日期的格式验证与处理
- 更新导入项目接口,使用枚举类型校验项目类型并优化异常处理
- 更新统计接口,避免多次查询假期数据,优化日期字符串处理
- 删除回滚前多余的session关闭调用,改为使用db.session.rollback()
- app.py中重构数据库初始化:统一配置SQLAlchemy,动态创建数据库路径和表
- 项目模型新增开始日期和结束日期字段支持
- 添加导入批次历史记录模型支持
- 优化工具函数中日期类型提示,移除无用导入
- 更新requirements.txt依赖版本回退,确保兼容性
- 前端菜单添加导入历史导航入口,实现页面访问路由绑定
2025-09-04 18:12:24 +08:00

485 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 统计分析页面JavaScript
let cutoffPeriods = [];
let currentStats = null;
let projectHoursChart = null; // 用于存储图表实例
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
loadCutoffPeriods();
setupEventListeners();
});
// 设置事件监听器
function setupEventListeners() {
// 周期表单提交
const periodForm = document.getElementById('period-form');
if (periodForm) {
periodForm.addEventListener('submit', handlePeriodSubmit);
}
}
// 加载Cut-Off周期
async function loadCutoffPeriods() {
try {
const response = await apiGet('/api/statistics/periods');
cutoffPeriods = response.data;
populatePeriodSelect();
} catch (error) {
showError('加载周期列表失败: ' + error.message);
console.error('加载周期列表失败:', error);
}
}
// 填充周期选择框
function populatePeriodSelect() {
const select = document.getElementById('period-select');
if (!select) return;
// 清空现有选项(保留第一个默认选项)
const firstOption = select.querySelector('option');
select.innerHTML = '';
if (firstOption) {
select.appendChild(firstOption);
}
// 添加周期选项
cutoffPeriods.forEach(period => {
const option = document.createElement('option');
option.value = period.id;
option.textContent = `${period.period_name} (${formatDate(period.start_date)} - ${formatDate(period.end_date)})`;
select.appendChild(option);
});
}
// 加载周统计数据
async function loadWeeklyStats() {
const periodId = document.getElementById('period-select').value;
if (!periodId) {
hideStatsDisplay();
return;
}
try {
const response = await apiGet(`/api/statistics/weekly?period_id=${periodId}`);
currentStats = response.data;
displayStats();
} catch (error) {
showError('加载统计数据失败: ' + error.message);
console.error('加载统计数据失败:', error);
}
}
// 加载自定义日期范围统计
async function loadCustomStats() {
const startDate = document.getElementById('custom-start-date').value;
const endDate = document.getElementById('custom-end-date').value;
if (!startDate || !endDate) {
showError('请选择开始和结束日期');
return;
}
if (new Date(startDate) > new Date(endDate)) {
showError('开始日期不能晚于结束日期');
return;
}
try {
const response = await apiGet(`/api/statistics/weekly?start_date=${startDate}&end_date=${endDate}`);
currentStats = response.data;
displayStats();
// 清空周期选择
document.getElementById('period-select').value = '';
} catch (error) {
showError('加载统计数据失败: ' + error.message);
console.error('加载统计数据失败:', error);
}
}
// 显示统计数据
function displayStats() {
if (!currentStats) return;
showStatsDisplay();
updateStatsOverview();
renderDailyDetails();
renderProjectDistribution();
}
// 显示统计界面
function showStatsDisplay() {
document.getElementById('stats-overview').style.display = 'block';
document.getElementById('daily-details').style.display = 'block';
document.getElementById('project-distribution').style.display = 'block';
}
// 隐藏统计界面
function hideStatsDisplay() {
document.getElementById('stats-overview').style.display = 'none';
document.getElementById('daily-details').style.display = 'none';
document.getElementById('project-distribution').style.display = 'none';
}
// 更新统计概览
function updateStatsOverview() {
if (!currentStats) return;
// 更新周期信息
document.getElementById('current-period-name').textContent = currentStats.period.period_name;
document.getElementById('period-date-range').textContent =
`${formatDate(currentStats.period.start_date)} - ${formatDate(currentStats.period.end_date)}`;
// 更新统计数据
document.getElementById('workday-total').textContent = currentStats.workday_total;
document.getElementById('holiday-total').textContent = currentStats.holiday_total;
document.getElementById('weekly-total').textContent = currentStats.weekly_total;
document.getElementById('completion-rate').textContent = `${currentStats.completion_rate}%`;
// 更新工作天数统计
document.getElementById('working-days').textContent = currentStats.working_days;
document.getElementById('holiday-work-days').textContent = currentStats.holiday_work_days;
document.getElementById('rest-days').textContent = currentStats.rest_days;
// 更新完成度颜色
const completionElement = document.getElementById('completion-rate');
completionElement.className = 'stat-value';
if (currentStats.completion_rate >= 100) {
completionElement.style.color = '#27ae60';
} else if (currentStats.completion_rate >= 80) {
completionElement.style.color = '#f39c12';
} else {
completionElement.style.color = '#e74c3c';
}
}
// 渲染每日详情
function renderDailyDetails() {
if (!currentStats || !currentStats.daily_records) return;
const tbody = document.getElementById('daily-stats-tbody');
if (!tbody) return;
tbody.innerHTML = currentStats.daily_records.map(record => {
const rowClass = getRowClass(record);
return `
<tr class="${rowClass}">
<td>${formatDate(record.date)}</td>
<td>${record.day_of_week}</td>
<td>${escapeHtml(record.event)}</td>
<td>${escapeHtml(record.project)}</td>
<td>${record.start_time}</td>
<td>${record.end_time}</td>
<td>${escapeHtml(record.activity_num)}</td>
<td>${record.hours}</td>
</tr>
`;
}).join('');
}
// 渲染项目工时分布图表和表格
function renderProjectDistribution() {
if (!currentStats || !currentStats.project_hours) return;
const projectData = currentStats.project_hours;
const tableBody = document.getElementById('project-hours-tbody');
const ctx = document.getElementById('project-hours-chart').getContext('2d');
if (!ctx || !tableBody) return;
// 清理旧内容
tableBody.innerHTML = '';
if (projectHoursChart) {
projectHoursChart.destroy();
}
if (projectData.length === 0) {
// 在表格中显示无数据信息
tableBody.innerHTML = '<tr><td colspan="3" style="text-align: center;">暂无项目工时数据</td></tr>';
// 清理画布
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.font = "16px Arial";
ctx.fillStyle = "#888";
ctx.textAlign = "center";
ctx.fillText("暂无项目工时数据", ctx.canvas.width / 2, 50);
return;
}
// 填充表格
projectData.forEach(item => {
const row = `
<tr>
<td>${escapeHtml(item.project)}</td>
<td>${item.hours}</td>
<td>${item.percentage}%</td>
</tr>
`;
tableBody.innerHTML += row;
});
// 准备图表数据
const labels = projectData.map(item => item.project);
const data = projectData.map(item => parseFloat(item.hours.replace(':', '.')));
const backgroundColors = [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40',
'#E7E9ED', '#8DDF3C', '#FFD700', '#B22222', '#4682B4', '#D2B48C'
];
// 渲染图表
projectHoursChart = new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
label: '工时',
data: data,
backgroundColor: backgroundColors.slice(0, data.length),
borderColor: '#fff',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
},
title: {
display: false, // 标题可以省略,因为旁边有表格
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.label || '';
if (label) {
label += ': ';
}
if (context.parsed !== null) {
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((context.parsed / total) * 100).toFixed(2);
label += `${context.raw} 小时 (${percentage}%)`;
}
return label;
}
}
}
}
}
});
}
// 获取行的CSS类名根据是否为休息日
function getRowClass(record) {
if (record.is_holiday) {
if (record.is_working_on_holiday) {
return 'working-holiday-row'; // 休息日工作
} else {
return 'holiday-row'; // 休息日休息
}
}
return '';
}
// 显示创建周期模态框
function showCreatePeriodModal() {
resetForm('period-form');
showModal('create-period-modal');
}
// 计算周期信息
function calculatePeriodInfo() {
const startDate = document.getElementById('start_date').value;
const endDate = document.getElementById('end_date').value;
if (!startDate || !endDate) return;
const start = new Date(startDate);
const end = new Date(endDate);
if (start >= end) {
showError('开始日期必须早于结束日期');
return;
}
// 计算天数和周数
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
const weeks = Math.round(daysDiff / 7);
// 更新周数字段
document.getElementById('weeks').value = weeks;
// 更新目标工时(如果为空)
const targetHoursField = document.getElementById('target_hours');
if (!targetHoursField.value) {
targetHoursField.value = weeks * 40; // 默认每周40小时
}
}
// 应用周期模板
function applyTemplate(templateType) {
const today = new Date();
let startDate, endDate, weeks, targetHours, periodName;
switch (templateType) {
case 'weekly':
// 本周周期
const dayOfWeek = today.getDay();
const startOfWeek = new Date(today);
startOfWeek.setDate(today.getDate() - dayOfWeek + 1); // 周一
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 6); // 周日
startDate = startOfWeek.toISOString().split('T')[0];
endDate = endOfWeek.toISOString().split('T')[0];
weeks = 1;
targetHours = 40;
periodName = `${today.getFullYear()}年第${getWeekNumber(today)}`;
break;
case 'biweekly':
// 双周周期
startDate = getTodayString();
const biweekEnd = new Date(today);
biweekEnd.setDate(today.getDate() + 13);
endDate = biweekEnd.toISOString().split('T')[0];
weeks = 2;
targetHours = 80;
periodName = `双周周期 ${formatDate(startDate)}-${formatDate(endDate)}`;
break;
case 'four-weeks':
// 4周周期
startDate = getTodayString();
const fourWeeksEnd = new Date(today);
fourWeeksEnd.setDate(today.getDate() + 27);
endDate = fourWeeksEnd.toISOString().split('T')[0];
weeks = 4;
targetHours = 160;
periodName = `4周周期 ${formatDate(startDate)}-${formatDate(endDate)}`;
break;
case 'five-weeks':
// 5周周期
startDate = getTodayString();
const fiveWeeksEnd = new Date(today);
fiveWeeksEnd.setDate(today.getDate() + 34);
endDate = fiveWeeksEnd.toISOString().split('T')[0];
weeks = 5;
targetHours = 200;
periodName = `5周周期 ${formatDate(startDate)}-${formatDate(endDate)}`;
break;
default:
// 默认行为可以指向一个常用模板例如4周
startDate = getTodayString();
const defaultEnd = new Date(today);
defaultEnd.setDate(today.getDate() + 27);
endDate = defaultEnd.toISOString().split('T')[0];
weeks = 4;
targetHours = 160;
periodName = `4周周期 ${formatDate(startDate)}-${formatDate(endDate)}`;
break;
}
// 填充表单
document.getElementById('period_name').value = periodName;
document.getElementById('start_date').value = startDate;
document.getElementById('end_date').value = endDate;
document.getElementById('weeks').value = weeks;
document.getElementById('target_hours').value = targetHours;
}
// 获取周数
function getWeekNumber(date) {
const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
const pastDaysOfYear = (date - firstDayOfYear) / 86400000;
return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
}
// 处理周期表单提交
async function handlePeriodSubmit(e) {
e.preventDefault();
const formData = getFormData('period-form');
// 验证必填字段
if (!formData.period_name || !formData.start_date || !formData.end_date) {
showError('请填写完整的周期信息');
return;
}
try {
const response = await apiPost('/api/statistics/periods', formData);
showSuccess('Cut-Off周期创建成功');
closeModal('create-period-modal');
loadCutoffPeriods(); // 重新加载周期列表
} catch (error) {
showError(error.message);
}
}
// 管理周期
function managePeriods() {
loadPeriodsTable();
showModal('manage-periods-modal');
}
// 加载周期管理表格
function loadPeriodsTable() {
const tbody = document.getElementById('periods-tbody');
if (!tbody) return;
if (cutoffPeriods.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center">暂无周期数据</td></tr>';
return;
}
tbody.innerHTML = cutoffPeriods.map(period => `
<tr>
<td>${escapeHtml(period.period_name)}</td>
<td>${formatDate(period.start_date)}</td>
<td>${formatDate(period.end_date)}</td>
<td>${period.weeks}</td>
<td>${period.target_hours}小时</td>
<td>
<button class="btn btn-sm btn-danger" onclick="deletePeriod(${period.id})">删除</button>
</td>
</tr>
`).join('');
}
// 删除周期
async function deletePeriod(periodId) {
const period = cutoffPeriods.find(p => p.id === periodId);
if (!period) {
showError('周期不存在');
return;
}
if (!confirm(`确定要删除周期"${period.period_name}"吗?`)) {
return;
}
try {
await apiDelete(`/api/statistics/periods/${periodId}`);
showSuccess('周期删除成功');
loadCutoffPeriods(); // 重新加载周期列表
loadPeriodsTable(); // 更新管理表格
} catch (error) {
showError('删除周期失败: ' + error.message);
}
}
// HTML转义函数
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}