772 lines
27 KiB
HTML
772 lines
27 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block content %}
|
|
<div class="page-container timetrack-container">
|
|
<div class="page-header analytics-header">
|
|
<h2><i class="ti ti-chart-bar"></i> Time Analytics</h2>
|
|
<div class="mode-switcher">
|
|
<button class="mode-btn {% if mode == 'personal' %}active{% endif %}"
|
|
onclick="switchMode('personal')">Personal</button>
|
|
{% if g.user.team_id and g.user.role in [Role.TEAM_LEADER, Role.SUPERVISOR, Role.ADMIN] %}
|
|
<button class="mode-btn {% if mode == 'team' %}active{% endif %}"
|
|
onclick="switchMode('team')">Team</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unified Filter Panel -->
|
|
<div class="filter-panel">
|
|
<div class="filter-row">
|
|
<div class="filter-group">
|
|
<label for="start-date">Start Date:</label>
|
|
<input type="date" id="start-date" value="{{ default_start_date }}">
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="end-date">End Date:</label>
|
|
<input type="date" id="end-date" value="{{ default_end_date }}">
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="project-filter">Project:</label>
|
|
<select id="project-filter">
|
|
<option value="">All Projects</option>
|
|
<option value="none">No Project Assigned</option>
|
|
{% for project in available_projects %}
|
|
<option value="{{ project.id }}">{{ project.code }} - {{ project.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<button id="apply-filters" class="btn btn-primary">Apply Filters</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- View Tabs -->
|
|
<div class="view-tabs">
|
|
<button class="tab-btn active" data-view="table"><i class="ti ti-clipboard-list"></i> Table View</button>
|
|
<button class="tab-btn" data-view="graph"><i class="ti ti-trending-up"></i> Graph View</button>
|
|
{% if mode == 'team' %}
|
|
<button class="tab-btn" data-view="team"><i class="ti ti-users"></i> Team Summary</button>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Loading Indicator -->
|
|
<div id="loading-indicator" class="loading" style="display: none;">
|
|
<div class="spinner"></div>
|
|
Loading analytics data...
|
|
</div>
|
|
|
|
<!-- Error Display -->
|
|
<div id="error-display" class="error-message" style="display: none;"></div>
|
|
|
|
<!-- Table View -->
|
|
<div id="table-view" class="view-content active">
|
|
<div class="view-header">
|
|
<h3>Detailed Time Entries</h3>
|
|
<div class="export-buttons">
|
|
<button class="btn btn-secondary" onclick="exportData('csv', 'table')">Export CSV</button>
|
|
<button class="btn btn-secondary" onclick="exportData('excel', 'table')">Export Excel</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-container">
|
|
<table id="entries-table" class="time-history">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
{% if mode == 'team' %}
|
|
<th>User</th>
|
|
{% endif %}
|
|
<th>Project</th>
|
|
<th>Arrival</th>
|
|
<th>Departure</th>
|
|
<th>Duration</th>
|
|
<th>Break</th>
|
|
<th>Notes</th>
|
|
{% if mode == 'personal' %}
|
|
<th>Actions</th>
|
|
{% endif %}
|
|
</tr>
|
|
</thead>
|
|
<tbody id="entries-tbody">
|
|
<tr>
|
|
<td colspan="{% if mode == 'team' %}8{% else %}9{% endif %}" class="text-center">
|
|
Click "Apply Filters" to load data
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Graph View -->
|
|
<div id="graph-view" class="view-content">
|
|
<div class="view-header">
|
|
<h3>Visual Analytics</h3>
|
|
<div class="chart-controls">
|
|
<select id="chart-type">
|
|
<option value="timeSeries">Time Series</option>
|
|
<option value="projectDistribution">Project Distribution</option>
|
|
<option value="burndown">Burndown Chart</option>
|
|
</select>
|
|
<div class="export-buttons">
|
|
<button class="btn btn-secondary" onclick="exportChart('png')">Export PNG</button>
|
|
<button class="btn btn-secondary" onclick="exportChart('pdf')">Export PDF</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="charts-container">
|
|
<div class="chart-wrapper">
|
|
<canvas id="main-chart"></canvas>
|
|
</div>
|
|
<div class="chart-stats">
|
|
<div class="stat-card">
|
|
<span id="total-hours">0</span>
|
|
<h4 id="stat-label-1">Total Hours</h4>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span id="total-days">0</span>
|
|
<h4 id="stat-label-2">Total Days</h4>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span id="avg-hours">0</span>
|
|
<h4 id="stat-label-3">Average Hours/Day</h4>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Team Summary View -->
|
|
{% if mode == 'team' %}
|
|
<div id="team-view" class="view-content">
|
|
<div class="view-header">
|
|
<h3>Team Hours Summary</h3>
|
|
<div class="export-buttons">
|
|
<button class="btn btn-secondary" onclick="exportData('csv', 'team')">Export CSV</button>
|
|
<button class="btn btn-secondary" onclick="exportData('excel', 'team')">Export Excel</button>
|
|
</div>
|
|
</div>
|
|
<div class="team-summary-container">
|
|
<table id="team-table" class="time-history">
|
|
<thead id="team-table-head">
|
|
<tr>
|
|
<th>Team Member</th>
|
|
<th>Total Hours</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="team-tbody">
|
|
<tr>
|
|
<td colspan="2" class="text-center">
|
|
Click "Apply Filters" to load team data
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Edit Modal (for personal mode) -->
|
|
{% if mode == 'personal' %}
|
|
<div id="editModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<span class="close">×</span>
|
|
<h2>Edit Time Entry</h2>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="editForm">
|
|
<input type="hidden" id="editEntryId">
|
|
<div class="form-group">
|
|
<label for="editArrivalTime">Arrival Time:</label>
|
|
<input type="datetime-local" id="editArrivalTime" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="editDepartureTime">Departure Time:</label>
|
|
<input type="datetime-local" id="editDepartureTime">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="editNotes">Notes:</label>
|
|
<textarea id="editNotes" rows="3"></textarea>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick="closeEditModal()">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="saveEntry()">Save Changes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div id="deleteModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<span class="close">×</span>
|
|
<h2>Confirm Deletion</h2>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Are you sure you want to delete this time entry? This action cannot be undone.</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick="closeDeleteModal()">Cancel</button>
|
|
<button type="button" class="btn btn-danger" onclick="confirmDelete()">Delete</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Chart.js CDN -->
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<!-- jsPDF for PDF export -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
|
|
|
<script>
|
|
// Global analytics state and controller
|
|
let analyticsController;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
analyticsController = new TimeAnalyticsController();
|
|
analyticsController.init();
|
|
});
|
|
|
|
class TimeAnalyticsController {
|
|
constructor() {
|
|
this.state = {
|
|
mode: '{{ mode }}',
|
|
dateRange: {
|
|
start: '{{ default_start_date }}',
|
|
end: '{{ default_end_date }}'
|
|
},
|
|
selectedProject: '',
|
|
activeView: 'table',
|
|
data: null
|
|
};
|
|
this.charts = {};
|
|
}
|
|
|
|
init() {
|
|
this.setupEventListeners();
|
|
// Auto-load data on initialization
|
|
this.loadData();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Filter controls
|
|
document.getElementById('apply-filters').addEventListener('click', () => {
|
|
this.updateFilters();
|
|
this.loadData();
|
|
});
|
|
|
|
// Tab switching
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
this.switchView(e.target.dataset.view);
|
|
});
|
|
});
|
|
|
|
// Chart type switching
|
|
const chartTypeSelect = document.getElementById('chart-type');
|
|
if (chartTypeSelect) {
|
|
chartTypeSelect.addEventListener('change', () => {
|
|
// For burndown chart, we need to reload data from the server
|
|
if (chartTypeSelect.value === 'burndown') {
|
|
this.loadData();
|
|
} else {
|
|
this.updateChart();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Modal close handlers
|
|
document.querySelectorAll('.close').forEach(closeBtn => {
|
|
closeBtn.addEventListener('click', (e) => {
|
|
e.target.closest('.modal').style.display = 'none';
|
|
});
|
|
});
|
|
|
|
// Click outside modal to close
|
|
window.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('modal')) {
|
|
e.target.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
updateFilters() {
|
|
this.state.dateRange.start = document.getElementById('start-date').value;
|
|
this.state.dateRange.end = document.getElementById('end-date').value;
|
|
this.state.selectedProject = document.getElementById('project-filter').value;
|
|
}
|
|
|
|
async loadData() {
|
|
this.showLoading(true);
|
|
this.hideError();
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
mode: this.state.mode,
|
|
view: this.state.activeView,
|
|
start_date: this.state.dateRange.start,
|
|
end_date: this.state.dateRange.end
|
|
});
|
|
|
|
if (this.state.selectedProject) {
|
|
params.append('project_id', this.state.selectedProject);
|
|
}
|
|
|
|
// Add chart_type parameter for graph view
|
|
if (this.state.activeView === 'graph') {
|
|
const chartType = document.getElementById('chart-type')?.value || 'timeSeries';
|
|
params.append('chart_type', chartType);
|
|
}
|
|
|
|
const response = await fetch(`/api/analytics/data?${params}`);
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Failed to load data');
|
|
}
|
|
|
|
this.state.data = data;
|
|
this.refreshCurrentView();
|
|
|
|
} catch (error) {
|
|
this.showError(error.message);
|
|
} finally {
|
|
this.showLoading(false);
|
|
}
|
|
}
|
|
|
|
refreshCurrentView() {
|
|
switch (this.state.activeView) {
|
|
case 'table':
|
|
this.updateTableView();
|
|
break;
|
|
case 'graph':
|
|
this.updateGraphView();
|
|
break;
|
|
case 'team':
|
|
this.updateTeamView();
|
|
break;
|
|
}
|
|
}
|
|
|
|
switchView(viewType) {
|
|
// Update tab appearance
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
document.querySelector(`[data-view="${viewType}"]`).classList.add('active');
|
|
|
|
// Show/hide view content
|
|
document.querySelectorAll('.view-content').forEach(content => {
|
|
content.classList.remove('active');
|
|
});
|
|
document.getElementById(`${viewType}-view`).classList.add('active');
|
|
|
|
this.state.activeView = viewType;
|
|
|
|
// Always load new data since different views need different data structures
|
|
this.loadData();
|
|
}
|
|
|
|
updateTableView() {
|
|
const tbody = document.getElementById('entries-tbody');
|
|
const entries = this.state.data.entries || [];
|
|
|
|
if (entries.length === 0) {
|
|
tbody.innerHTML = `<tr><td colspan="9" class="text-center">No entries found for the selected criteria</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = entries.map(entry => `
|
|
<tr>
|
|
<td>${entry.date}</td>
|
|
${this.state.mode === 'team' ? `<td>${entry.user_name}</td>` : ''}
|
|
<td>
|
|
${entry.project_code ? `<span class="project-tag">${entry.project_code}</span>` : ''}
|
|
${entry.project_name}
|
|
</td>
|
|
<td>${entry.arrival_time}</td>
|
|
<td>${entry.departure_time}</td>
|
|
<td>${entry.duration}</td>
|
|
<td>${entry.break_duration}</td>
|
|
<td class="notes-preview" title="${entry.notes}">
|
|
${entry.notes.length > 50 ? entry.notes.substring(0, 50) + '...' : entry.notes}
|
|
</td>
|
|
${this.state.mode === 'personal' ? `
|
|
<td>
|
|
<button class="btn btn-sm btn-primary" onclick="editEntry(${entry.id})">Edit</button>
|
|
<button class="btn btn-sm btn-danger" onclick="deleteEntry(${entry.id})">Delete</button>
|
|
</td>` : ''}
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
updateGraphView() {
|
|
const data = this.state.data;
|
|
if (!data) return;
|
|
|
|
const chartType = document.getElementById('chart-type').value;
|
|
|
|
// Update stats based on chart type
|
|
if (chartType === 'burndown' && data.burndown) {
|
|
document.getElementById('total-hours').textContent = data.burndown.total_tasks || '0';
|
|
document.getElementById('total-days').textContent = data.burndown.dates?.length || '0';
|
|
document.getElementById('avg-hours').textContent = data.burndown.tasks_completed || '0';
|
|
|
|
// Update stat labels for burndown
|
|
document.getElementById('stat-label-1').textContent = 'Total Tasks';
|
|
document.getElementById('stat-label-2').textContent = 'Timeline Days';
|
|
document.getElementById('stat-label-3').textContent = 'Completed Tasks';
|
|
} else {
|
|
document.getElementById('total-hours').textContent = data.totalHours?.toFixed(1) || '0';
|
|
document.getElementById('total-days').textContent = data.totalDays || '0';
|
|
document.getElementById('avg-hours').textContent =
|
|
data.totalDays > 0 ? (data.totalHours / data.totalDays).toFixed(1) : '0';
|
|
|
|
// Restore original stat labels
|
|
document.getElementById('stat-label-1').textContent = 'Total Hours';
|
|
document.getElementById('stat-label-2').textContent = 'Total Days';
|
|
document.getElementById('stat-label-3').textContent = 'Average Hours/Day';
|
|
}
|
|
|
|
this.updateChart();
|
|
}
|
|
|
|
updateChart() {
|
|
const chartType = document.getElementById('chart-type').value;
|
|
const canvas = document.getElementById('main-chart');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Destroy existing chart
|
|
if (this.charts.main) {
|
|
this.charts.main.destroy();
|
|
}
|
|
|
|
const data = this.state.data;
|
|
if (!data) return;
|
|
|
|
if (chartType === 'timeSeries') {
|
|
this.charts.main = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.timeSeries?.map(d => d.date) || [],
|
|
datasets: [{
|
|
label: 'Hours Worked',
|
|
data: data.timeSeries?.map(d => d.hours) || [],
|
|
borderColor: '#4CAF50',
|
|
backgroundColor: 'rgba(76, 175, 80, 0.1)',
|
|
fill: true,
|
|
tension: 0.1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'Daily Hours Worked'
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Hours'
|
|
}
|
|
},
|
|
x: {
|
|
title: {
|
|
display: true,
|
|
text: 'Date'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} else if (chartType === 'projectDistribution') {
|
|
this.charts.main = new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: data.projectDistribution?.map(d => d.project) || [],
|
|
datasets: [{
|
|
data: data.projectDistribution?.map(d => d.hours) || [],
|
|
backgroundColor: [
|
|
'#4CAF50', '#2196F3', '#FF9800', '#E91E63',
|
|
'#9C27B0', '#00BCD4', '#8BC34A', '#FFC107'
|
|
]
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'Time Distribution by Project'
|
|
},
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} else if (chartType === 'burndown') {
|
|
this.charts.main = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.burndown?.dates || [],
|
|
datasets: [{
|
|
label: 'Remaining Tasks',
|
|
data: data.burndown?.remaining || [],
|
|
borderColor: '#FF5722',
|
|
backgroundColor: 'rgba(255, 87, 34, 0.1)',
|
|
fill: true,
|
|
tension: 0.1,
|
|
pointBackgroundColor: '#FF5722',
|
|
pointBorderColor: '#FF5722',
|
|
pointRadius: 4
|
|
}, {
|
|
label: 'Ideal Burndown',
|
|
data: data.burndown?.ideal || [],
|
|
borderColor: '#4CAF50',
|
|
backgroundColor: 'transparent',
|
|
borderDash: [5, 5],
|
|
fill: false,
|
|
tension: 0,
|
|
pointRadius: 0
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'Project Burndown Chart'
|
|
},
|
|
legend: {
|
|
display: true,
|
|
position: 'top'
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Remaining Tasks'
|
|
},
|
|
ticks: {
|
|
stepSize: 1
|
|
}
|
|
},
|
|
x: {
|
|
title: {
|
|
display: true,
|
|
text: 'Date'
|
|
}
|
|
}
|
|
},
|
|
interaction: {
|
|
intersect: false,
|
|
mode: 'index'
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
updateTeamView() {
|
|
const tbody = document.getElementById('team-tbody');
|
|
const teamData = this.state.data.team_data || [];
|
|
|
|
if (teamData.length === 0) {
|
|
tbody.innerHTML = `<tr><td colspan="2" class="text-center">No team data found</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = teamData.map(member => `
|
|
<tr>
|
|
<td>${member.username}</td>
|
|
<td>${member.total_hours}h</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
showLoading(show) {
|
|
const indicator = document.getElementById('loading-indicator');
|
|
indicator.style.display = show ? 'block' : 'none';
|
|
}
|
|
|
|
showError(message) {
|
|
const errorDiv = document.getElementById('error-display');
|
|
errorDiv.textContent = message;
|
|
errorDiv.style.display = 'block';
|
|
}
|
|
|
|
hideError() {
|
|
document.getElementById('error-display').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Global functions for mode switching and exports
|
|
function switchMode(mode) {
|
|
window.location.href = `/analytics/${mode}`;
|
|
}
|
|
|
|
function exportData(format, viewType) {
|
|
const params = new URLSearchParams({
|
|
format: format,
|
|
view: viewType,
|
|
mode: analyticsController.state.mode,
|
|
start_date: analyticsController.state.dateRange.start,
|
|
end_date: analyticsController.state.dateRange.end
|
|
});
|
|
|
|
if (analyticsController.state.selectedProject) {
|
|
params.append('project_id', analyticsController.state.selectedProject);
|
|
}
|
|
|
|
window.location.href = `/api/analytics/export?${params}`;
|
|
}
|
|
|
|
function exportChart(format) {
|
|
const chart = analyticsController.charts.main;
|
|
if (!chart) return;
|
|
|
|
const canvas = chart.canvas;
|
|
|
|
if (format === 'png') {
|
|
const link = document.createElement('a');
|
|
link.download = 'analytics-chart.png';
|
|
link.href = canvas.toDataURL('image/png');
|
|
link.click();
|
|
} else if (format === 'pdf') {
|
|
// Get chart title for PDF
|
|
const chartType = document.getElementById('chart-type').value;
|
|
const title = chartType === 'timeSeries' ? 'Daily Hours Worked' :
|
|
chartType === 'projectDistribution' ? 'Time Distribution by Project' :
|
|
'Project Burndown Chart';
|
|
|
|
// Create PDF using jsPDF
|
|
const { jsPDF } = window.jspdf;
|
|
const pdf = new jsPDF('landscape', 'mm', 'a4');
|
|
|
|
// Add title
|
|
pdf.setFontSize(16);
|
|
pdf.text(title, 20, 20);
|
|
|
|
// Add chart image to PDF
|
|
const imgData = canvas.toDataURL('image/png');
|
|
const imgWidth = 250;
|
|
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
|
|
|
pdf.addImage(imgData, 'PNG', 20, 35, imgWidth, imgHeight);
|
|
|
|
// Add export info
|
|
pdf.setFontSize(10);
|
|
const dateRange = `${analyticsController.state.dateRange.start} to ${analyticsController.state.dateRange.end}`;
|
|
pdf.text(`Date Range: ${dateRange}`, 20, imgHeight + 50);
|
|
pdf.text(`Generated: ${new Date().toLocaleString()}`, 20, imgHeight + 60);
|
|
|
|
// Save PDF
|
|
pdf.save('analytics-chart.pdf');
|
|
}
|
|
}
|
|
|
|
// Entry management functions (personal mode only)
|
|
{% if mode == 'personal' %}
|
|
function editEntry(entryId) {
|
|
// Find entry data
|
|
const entry = analyticsController.state.data.entries.find(e => e.id === entryId);
|
|
if (!entry) return;
|
|
|
|
// Populate modal
|
|
document.getElementById('editEntryId').value = entryId;
|
|
document.getElementById('editArrivalTime').value =
|
|
`${entry.date}T${entry.arrival_time}`;
|
|
|
|
if (entry.departure_time !== 'Active') {
|
|
document.getElementById('editDepartureTime').value =
|
|
`${entry.date}T${entry.departure_time}`;
|
|
}
|
|
|
|
document.getElementById('editNotes').value = entry.notes;
|
|
|
|
// Show modal
|
|
document.getElementById('editModal').style.display = 'block';
|
|
}
|
|
|
|
function closeEditModal() {
|
|
document.getElementById('editModal').style.display = 'none';
|
|
}
|
|
|
|
async function saveEntry() {
|
|
const entryId = document.getElementById('editEntryId').value;
|
|
const arrivalTime = document.getElementById('editArrivalTime').value;
|
|
const departureTime = document.getElementById('editDepartureTime').value;
|
|
const notes = document.getElementById('editNotes').value;
|
|
|
|
try {
|
|
const response = await fetch(`/api/update/${entryId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
arrival_time: arrivalTime,
|
|
departure_time: departureTime || null,
|
|
notes: notes
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
closeEditModal();
|
|
analyticsController.loadData(); // Refresh data
|
|
} else {
|
|
alert('Error updating entry: ' + result.message);
|
|
}
|
|
} catch (error) {
|
|
alert('Error updating entry: ' + error.message);
|
|
}
|
|
}
|
|
|
|
let entryToDelete = null;
|
|
|
|
function deleteEntry(entryId) {
|
|
entryToDelete = entryId;
|
|
document.getElementById('deleteModal').style.display = 'block';
|
|
}
|
|
|
|
function closeDeleteModal() {
|
|
document.getElementById('deleteModal').style.display = 'none';
|
|
entryToDelete = null;
|
|
}
|
|
|
|
async function confirmDelete() {
|
|
if (!entryToDelete) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/delete/${entryToDelete}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
closeDeleteModal();
|
|
analyticsController.loadData(); // Refresh data
|
|
} else {
|
|
alert('Error deleting entry: ' + result.message);
|
|
}
|
|
} catch (error) {
|
|
alert('Error deleting entry: ' + error.message);
|
|
}
|
|
}
|
|
{% endif %}
|
|
</script>
|
|
{% endblock %} |