- Fix missing import statements for CSV/Excel export functionality - Refactor export code into modular helper functions for better maintainability - Add comprehensive Team Hours export feature with CSV and Excel support - Enhance export UI styling with modern gradients and hover effects - Add role-based access control for team export functionality - Include date range filtering and team leader inclusion options - Add proper error handling and user feedback for export operations - Update dependencies to include pandas and xlsxwriter - Fix JavaScript scope issues for export button functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
238 lines
9.9 KiB
HTML
238 lines
9.9 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block content %}
|
|
<div class="timetrack-container">
|
|
<h2>Team Hours</h2>
|
|
|
|
<div class="date-filter">
|
|
<form id="date-range-form" method="GET" action="{{ url_for('team_hours') }}">
|
|
<div class="form-group">
|
|
<label for="start-date">Start Date:</label>
|
|
<input type="date" id="start-date" name="start_date" value="{{ start_date.strftime('%Y-%m-%d') }}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="end-date">End Date:</label>
|
|
<input type="date" id="end-date" name="end_date" value="{{ end_date.strftime('%Y-%m-%d') }}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="include-self">
|
|
<input type="checkbox" id="include-self" name="include_self" {% if request.args.get('include_self') %}checked{% endif %}> Include my hours
|
|
</label>
|
|
</div>
|
|
<button type="submit" class="btn">Apply Filter</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Export Buttons -->
|
|
<div class="export-button-container" id="export-buttons" style="display: none;">
|
|
<h4>Export Team Hours</h4>
|
|
<div class="quick-export-buttons">
|
|
<button class="btn" onclick="exportTeamHours('csv')">Export as CSV</button>
|
|
<button class="btn" onclick="exportTeamHours('excel')">Export as Excel</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="team-hours-container">
|
|
<div id="loading">Loading team data...</div>
|
|
<div id="team-info" style="display: none;">
|
|
<h3>Team: <span id="team-name"></span></h3>
|
|
<p id="team-description"></p>
|
|
</div>
|
|
|
|
<div id="team-hours-table" style="display: none;">
|
|
<table class="time-history">
|
|
<thead id="table-header">
|
|
<tr>
|
|
<th>Team Member</th>
|
|
{% for date in date_range %}
|
|
<th>{{ date.strftime('%a, %b %d') }}</th>
|
|
{% endfor %}
|
|
<th>Total Hours</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="table-body">
|
|
<!-- Team member data will be added dynamically -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div id="no-data" style="display: none;">
|
|
<p>No time entries found for the selected date range.</p>
|
|
</div>
|
|
|
|
<div id="error-message" style="display: none;" class="error-message">
|
|
<!-- Error messages will be displayed here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="team-hours-details" id="member-details" style="display: none;">
|
|
<h3>Detailed Entries for <span id="selected-member"></span></h3>
|
|
<table class="time-history">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Arrival</th>
|
|
<th>Departure</th>
|
|
<th>Work Duration</th>
|
|
<th>Break Duration</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="details-body">
|
|
<!-- Entry details will be added dynamically -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Load team hours data when the page loads
|
|
loadTeamHoursData();
|
|
|
|
// Handle date filter form submission
|
|
document.getElementById('date-range-form').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
loadTeamHoursData();
|
|
});
|
|
|
|
function loadTeamHoursData() {
|
|
// Show loading indicator
|
|
document.getElementById('loading').style.display = 'block';
|
|
document.getElementById('team-hours-table').style.display = 'none';
|
|
document.getElementById('team-info').style.display = 'none';
|
|
document.getElementById('no-data').style.display = 'none';
|
|
document.getElementById('error-message').style.display = 'none';
|
|
document.getElementById('member-details').style.display = 'none';
|
|
|
|
// Get filter values
|
|
const startDate = document.getElementById('start-date').value;
|
|
const endDate = document.getElementById('end-date').value;
|
|
const includeSelf = document.getElementById('include-self').checked;
|
|
|
|
// Build API URL with query parameters
|
|
const apiUrl = `/api/team/hours_data?start_date=${startDate}&end_date=${endDate}&include_self=${includeSelf}`;
|
|
|
|
// Fetch data from API
|
|
fetch(apiUrl)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
return response.json().then(data => {
|
|
throw new Error(data.message || 'Failed to load team hours data');
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.success) {
|
|
displayTeamData(data);
|
|
} else {
|
|
showError(data.message || 'Failed to load team hours data.');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching team hours data:', error);
|
|
showError(error.message || 'An error occurred while loading the team hours data.');
|
|
});
|
|
}
|
|
|
|
function displayTeamData(data) {
|
|
// Populate team info
|
|
document.getElementById('team-name').textContent = data.team.name;
|
|
document.getElementById('team-description').textContent = data.team.description || '';
|
|
document.getElementById('team-info').style.display = 'block';
|
|
|
|
// Populate team hours table
|
|
const tableHeader = document.getElementById('table-header').querySelector('tr');
|
|
tableHeader.innerHTML = '<th>Team Member</th>';
|
|
data.date_range.forEach(dateStr => {
|
|
const th = document.createElement('th');
|
|
th.textContent = new Date(dateStr).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
tableHeader.appendChild(th);
|
|
});
|
|
const totalHoursTh = document.createElement('th');
|
|
totalHoursTh.textContent = 'Total Hours';
|
|
tableHeader.appendChild(totalHoursTh);
|
|
|
|
const tableBody = document.getElementById('table-body');
|
|
tableBody.innerHTML = '';
|
|
data.team_data.forEach(memberData => {
|
|
const row = document.createElement('tr');
|
|
|
|
// Add username cell
|
|
const usernameCell = document.createElement('td');
|
|
usernameCell.textContent = memberData.user.username;
|
|
row.appendChild(usernameCell);
|
|
|
|
// Add daily hours cells
|
|
data.date_range.forEach(dateStr => {
|
|
const cell = document.createElement('td');
|
|
cell.textContent = `${memberData.daily_hours[dateStr] || 0}h`;
|
|
row.appendChild(cell);
|
|
});
|
|
|
|
// Add total hours cell
|
|
const totalCell = document.createElement('td');
|
|
totalCell.innerHTML = `<strong>${memberData.total_hours}h</strong>`;
|
|
row.appendChild(totalCell);
|
|
|
|
tableBody.appendChild(row);
|
|
});
|
|
|
|
// Populate detailed entries
|
|
document.getElementById('team-hours-table').style.display = 'block';
|
|
document.getElementById('export-buttons').style.display = 'block';
|
|
document.getElementById('loading').style.display = 'none';
|
|
}
|
|
|
|
function showError(message) {
|
|
document.getElementById('error-message').textContent = message;
|
|
document.getElementById('error-message').style.display = 'block';
|
|
document.getElementById('loading').style.display = 'none';
|
|
}
|
|
|
|
});
|
|
|
|
// Export function (global scope)
|
|
function exportTeamHours(format) {
|
|
console.log('Export function called with format:', format);
|
|
|
|
try {
|
|
// Get current filter values
|
|
const startDate = document.getElementById('start-date').value;
|
|
const endDate = document.getElementById('end-date').value;
|
|
const includeSelf = document.getElementById('include-self').checked;
|
|
|
|
console.log('Filter values:', { startDate, endDate, includeSelf });
|
|
|
|
// Validate required fields
|
|
if (!startDate || !endDate) {
|
|
alert('Please select both start and end dates before exporting.');
|
|
return;
|
|
}
|
|
|
|
// Build export URL with query parameters
|
|
const exportUrl = `/download_team_hours_export?format=${format}&start_date=${startDate}&end_date=${endDate}&include_self=${includeSelf}`;
|
|
|
|
console.log('Export URL:', exportUrl);
|
|
|
|
// Show loading indicator
|
|
const exportButtons = document.getElementById('export-buttons');
|
|
const originalHTML = exportButtons.innerHTML;
|
|
exportButtons.innerHTML = '<h4>Generating export...</h4><p>Please wait...</p>';
|
|
|
|
// Trigger download
|
|
window.location.href = exportUrl;
|
|
|
|
// Restore buttons after a short delay
|
|
setTimeout(() => {
|
|
exportButtons.innerHTML = originalHTML;
|
|
}, 2000);
|
|
|
|
} catch (error) {
|
|
console.error('Error in exportTeamHours:', error);
|
|
alert('An error occurred while trying to export. Please try again.');
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|