Files
Raccoon-TimeKeeper/templates/index.html

400 lines
15 KiB
HTML
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.
{% extends "base.html" %}
{% block content %}
<!-- Time Entry Form -->
<section class="card entry-card">
<div class="card-header">
<h2>Log Time Entry</h2>
<span class="card-badge">New Entry</span>
</div>
<form id="timeEntryForm" class="entry-form">
<div class="form-grid">
<div class="form-group">
<label for="taskSelect">Task</label>
<div class="select-wrapper">
<select id="taskSelect" required>
<option value="">Select a task...</option>
</select>
<div class="select-arrow"></div>
</div>
</div>
<div class="form-group">
<label for="dateInput">Date</label>
<input type="date" id="dateInput" required>
</div>
<div class="form-group">
<label for="timeInput">Hours Worked</label>
<input type="text" id="timeInput" placeholder="e.g. 1:30 or 1.5" required>
<span class="input-hint">Formats: 1:30, 1.5, 90m, 1h 30m</span>
</div>
<div class="form-group form-group-full">
<label for="notesInput">Notes (optional)</label>
<input type="text" id="notesInput" placeholder="Brief description of work done...">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="btn-icon">+</span>
Add Entry
</button>
</div>
</form>
</section>
<!-- Add Task Modal -->
<div id="addTaskModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Add New Task</h3>
<button class="modal-close" onclick="closeAddTaskModal()">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="newTaskName">Task Name</label>
<input type="text" id="newTaskName" placeholder="Enter task name..." autofocus>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeAddTaskModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveNewTask()">Add Task</button>
</div>
</div>
</div>
<!-- Weekly Summary -->
<section class="card summary-card">
<div class="card-header">
<h2>Weekly Summary</h2>
<div class="week-selector">
<button class="btn btn-icon" onclick="changeWeek(-1)" title="Previous Week"></button>
<span id="weekLabel" class="week-label">This Week</span>
<button class="btn btn-icon" onclick="changeWeek(1)" title="Next Week"></button>
</div>
</div>
<div id="summaryContent" class="summary-content">
<div class="loading">Loading summary...</div>
</div>
<div class="summary-actions">
<button class="btn btn-secondary" onclick="printTimesheet()">
<span class="btn-icon">🖨</span>
Print Timesheet
</button>
</div>
</section>
<!-- Recent Entries -->
<section class="card entries-card">
<div class="card-header">
<h2>Recent Entries</h2>
<span class="entry-count" id="entryCount">0 entries</span>
</div>
<div id="recentEntries" class="entries-list">
<div class="loading">Loading entries...</div>
</div>
</section>
<!-- Print Template -->
<div id="printTemplate" class="print-template">
<div class="print-header">
<h1>🦝 Raccoon Timekeeper - Weekly Timesheet</h1>
<div class="print-user">{{ current_user.display_name or current_user.username }}</div>
<div class="print-period" id="printPeriod"></div>
</div>
<div id="printContent"></div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
loadTasks();
setDefaultDate();
loadWeeklySummary();
loadRecentEntries();
document.getElementById('timeEntryForm').addEventListener('submit', handleSubmit);
document.getElementById('taskSelect').addEventListener('change', handleTaskChange);
document.getElementById('newTaskName').addEventListener('keypress', function(e) {
if (e.key === 'Enter') { e.preventDefault(); saveNewTask(); }
});
});
function setDefaultDate() {
document.getElementById('dateInput').value = new Date().toISOString().split('T')[0];
}
async function loadTasks() {
try {
const response = await fetch('/api/tasks');
const tasks = await response.json();
populateTasks(tasks);
} catch (error) {
showToast('Error loading tasks', 'error');
}
}
function populateTasks(tasks) {
const select = document.getElementById('taskSelect');
select.innerHTML = '<option value="">Select a task...</option>';
tasks.forEach(task => {
const option = document.createElement('option');
option.value = task.id;
option.textContent = task.name;
select.appendChild(option);
});
const addOption = document.createElement('option');
addOption.value = '__ADD_NEW__';
addOption.textContent = ' Add new task...';
select.appendChild(addOption);
}
function handleTaskChange(e) {
if (e.target.value === '__ADD_NEW__') {
e.target.value = '';
openAddTaskModal();
}
}
function openAddTaskModal() {
document.getElementById('addTaskModal').classList.add('active');
document.getElementById('newTaskName').value = '';
document.getElementById('newTaskName').focus();
}
function closeAddTaskModal() {
document.getElementById('addTaskModal').classList.remove('active');
}
async function saveNewTask() {
const taskName = document.getElementById('newTaskName').value.trim();
if (!taskName) { showToast('Enter a task name', 'error'); return; }
try {
const response = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: taskName })
});
const result = await response.json();
if (result.success) {
showToast('Task added!', 'success');
closeAddTaskModal();
await loadTasks();
document.getElementById('taskSelect').value = result.task.id;
} else {
showToast(result.message, 'error');
}
} catch (error) {
showToast('Error adding task', 'error');
}
}
async function handleSubmit(e) {
e.preventDefault();
const data = {
task_id: document.getElementById('taskSelect').value,
date: document.getElementById('dateInput').value,
time: document.getElementById('timeInput').value,
notes: document.getElementById('notesInput').value
};
if (!data.task_id || !data.date || !data.time) {
showToast('Fill in all required fields', 'error');
return;
}
try {
const response = await fetch('/api/entries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
showToast('Time entry added!', 'success');
document.getElementById('timeInput').value = '';
document.getElementById('notesInput').value = '';
loadWeeklySummary();
loadRecentEntries();
} else {
showToast(result.message, 'error');
}
} catch (error) {
showToast('Error adding entry', 'error');
}
}
let currentWeekOffset = 0;
function changeWeek(direction) {
currentWeekOffset += direction;
loadWeeklySummary();
}
function getWeekStart(offset = 0) {
const today = new Date();
const day = today.getDay();
const diff = today.getDate() - day + (day === 0 ? -6 : 1) + (offset * 7);
const monday = new Date(today.setDate(diff));
monday.setHours(0, 0, 0, 0);
return monday;
}
async function loadWeeklySummary() {
const weekStart = getWeekStart(currentWeekOffset);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 6);
const options = { month: 'short', day: 'numeric' };
document.getElementById('weekLabel').textContent =
`${weekStart.toLocaleDateString('en-AU', options)} - ${weekEnd.toLocaleDateString('en-AU', options)}, ${weekEnd.getFullYear()}`;
try {
const response = await fetch(`/api/weekly-summary?week_start=${weekStart.toISOString()}`);
const data = await response.json();
renderWeeklySummary(data);
} catch (error) {
document.getElementById('summaryContent').innerHTML = '<div class="empty-state">⚠️ Error loading summary</div>';
}
}
function renderWeeklySummary(data) {
const container = document.getElementById('summaryContent');
if (!data.tasks || data.tasks.length === 0) {
container.innerHTML = '<div class="empty-state">📊 No entries this week</div>';
window.currentWeekData = null;
return;
}
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
let html = `<div class="summary-table-wrapper"><table class="summary-table">
<thead><tr><th class="task-col">Task</th>
${days.map((d, i) => `<th class="day-col">${d}</th>`).join('')}
<th class="total-col">Total</th></tr></thead><tbody>`;
data.tasks.forEach(task => {
html += `<tr><td class="task-name">${task.task_name}</td>
${days.map((_, i) => {
const mins = task.days[i] || 0;
return `<td class="time-cell ${mins > 0 ? 'has-time' : ''}">${mins > 0 ? formatMinutes(mins) : '-'}</td>`;
}).join('')}
<td class="total-cell">${formatMinutes(task.total_minutes)}</td></tr>`;
});
html += `<tr class="totals-row"><td><strong>Daily Total</strong></td>
${days.map((_, i) => `<td class="time-cell">${data.daily_totals[i] > 0 ? formatMinutes(data.daily_totals[i]) : '-'}</td>`).join('')}
<td class="grand-total">${formatMinutes(data.grand_total)}</td></tr>`;
html += `</tbody></table></div>
<div class="summary-stats">
<div class="stat-card"><span class="stat-value">${formatMinutes(data.grand_total)}</span><span class="stat-label">Total Hours</span></div>
<div class="stat-card"><span class="stat-value">${data.tasks.length}</span><span class="stat-label">Tasks Worked</span></div>
<div class="stat-card"><span class="stat-value">${data.entries.length}</span><span class="stat-label">Time Entries</span></div>
</div>`;
container.innerHTML = html;
window.currentWeekData = data;
}
async function loadRecentEntries() {
try {
const response = await fetch('/api/entries');
const entries = await response.json();
renderRecentEntries(entries);
} catch (error) {
document.getElementById('recentEntries').innerHTML = '<div class="empty-state">⚠️ Error loading entries</div>';
}
}
function renderRecentEntries(entries) {
const container = document.getElementById('recentEntries');
document.getElementById('entryCount').textContent = `${entries.length} entries`;
if (!entries.length) {
container.innerHTML = '<div class="empty-state">📝 No recent entries</div>';
return;
}
let html = '';
entries.slice(0, 20).forEach(entry => {
const date = new Date(entry.date);
const dateStr = date.toLocaleDateString('en-AU', { weekday: 'short', month: 'short', day: 'numeric' });
html += `<div class="entry-item" data-id="${entry.id}">
<div class="entry-main">
<span class="entry-task">${entry.task_name}</span>
<span class="entry-time">${formatMinutes(entry.total_minutes)}</span>
</div>
<div class="entry-details">
<span class="entry-date">${dateStr}</span>
${entry.notes ? `<span class="entry-notes">${entry.notes}</span>` : ''}
</div>
<button class="entry-delete" onclick="deleteEntry(${entry.id})" title="Delete">×</button>
</div>`;
});
container.innerHTML = html;
}
async function deleteEntry(entryId) {
if (!confirm('Delete this entry?')) return;
try {
const response = await fetch(`/api/entries/${entryId}`, { method: 'DELETE' });
const result = await response.json();
if (result.success) {
showToast('Entry deleted', 'success');
loadWeeklySummary();
loadRecentEntries();
} else {
showToast(result.message, 'error');
}
} catch (error) {
showToast('Error deleting entry', 'error');
}
}
function printTimesheet() {
if (!window.currentWeekData) { showToast('No data to print', 'error'); return; }
const data = window.currentWeekData;
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const weekStart = new Date(data.week_start);
const weekEnd = new Date(data.week_end);
const options = { year: 'numeric', month: 'long', day: 'numeric' };
document.getElementById('printPeriod').textContent =
`${weekStart.toLocaleDateString('en-AU', options)} - ${weekEnd.toLocaleDateString('en-AU', options)}`;
let html = `<table class="print-table"><thead><tr><th>Task</th>
${days.map(d => `<th>${d}</th>`).join('')}<th>Total</th></tr></thead><tbody>`;
data.tasks.forEach(task => {
html += `<tr><td>${task.task_name}</td>
${days.map((_, i) => `<td>${task.days[i] > 0 ? formatMinutes(task.days[i]) : '-'}</td>`).join('')}
<td><strong>${formatMinutes(task.total_minutes)}</strong></td></tr>`;
});
html += `<tr class="total-row"><td><strong>Daily Total</strong></td>
${days.map((_, i) => `<td><strong>${data.daily_totals[i] > 0 ? formatMinutes(data.daily_totals[i]) : '-'}</strong></td>`).join('')}
<td class="grand-total"><strong>${formatMinutes(data.grand_total)}</strong></td></tr></tbody></table>
<div class="print-summary"><p><strong>Total Hours:</strong> ${formatMinutes(data.grand_total)} (${(data.grand_total / 60).toFixed(2)} decimal)</p></div>`;
document.getElementById('printContent').innerHTML = html;
window.print();
}
</script>
{% endblock %}