Update index template with user context

This commit is contained in:
2025-12-10 10:43:07 +11:00
parent b648eef93e
commit 689e45a166

View File

@@ -1,146 +1,109 @@
{% extends "base.html" %}
{% block title %}Raccoon Timekeeper - Log Time{% endblock %}
{% block content %}
<div class="content-grid">
<!-- Time Entry Form -->
<section class="card">
<section class="card entry-card">
<div class="card-header">
<h2>Log Time</h2>
<h2>Log Time Entry</h2>
<span class="card-badge">New Entry</span>
</div>
<div class="card-body">
<form id="timeEntryForm" class="time-entry-form">
<div class="form-row">
<div class="form-group flex-2">
<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 class="form-group flex-1">
</div>
<div class="form-group">
<label for="dateInput">Date</label>
<input type="date" id="dateInput" required>
</div>
<div class="form-group flex-1">
<label for="timeInput">Time</label>
<input type="text" id="timeInput" placeholder="1:30, 1.5, 90m" required>
<span class="hint">Formats: 1:30, 1.5, 90m, 1h 30m</span>
</div>
</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>+</span> Add Entry
<span class="btn-icon">+</span>
Add Entry
</button>
</div>
</form>
</div>
</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">
<section class="card summary-card">
<div class="card-header">
<h2>Weekly Summary</h2>
<div class="week-navigator">
<button class="btn btn-icon" id="prevWeek" title="Previous Week"></button>
<span id="weekLabel">Loading...</span>
<button class="btn btn-icon" id="nextWeek" title="Next Week"></button>
<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 class="card-body">
<div id="summaryContent">
<div id="summaryContent" class="summary-content">
<div class="loading">Loading summary...</div>
</div>
<div class="summary-actions">
<button class="btn btn-secondary" id="printTimesheetBtn">
🖨️ Print Timesheet
<button class="btn btn-secondary" onclick="printTimesheet()">
<span class="btn-icon">🖨</span>
Print Timesheet
</button>
</div>
</div>
</section>
<!-- Recent Entries -->
<section class="card">
<section class="card entries-card">
<div class="card-header">
<h2>Recent Entries</h2>
<span class="badge" id="entryCount">0 entries</span>
<span class="entry-count" id="entryCount">0 entries</span>
</div>
<div class="card-body">
<div id="recentEntries">
<div id="recentEntries" class="entries-list">
<div class="loading">Loading entries...</div>
</div>
</div>
</section>
</div>
<!-- New Task Modal -->
<div id="newTaskModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Add New Task</h3>
<button class="modal-close" onclick="closeModal('newTaskModal')">&times;</button>
</div>
<div class="modal-body">
<form id="newTaskForm">
<div class="form-group">
<label for="newTaskName">Task Name</label>
<input type="text" id="newTaskName" required placeholder="Enter task name...">
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('newTaskModal')">Cancel</button>
<button class="btn btn-primary" onclick="submitNewTask()">Add Task</button>
</div>
</div>
</div>
<!-- Edit Entry Modal -->
<div id="editEntryModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Edit Entry</h3>
<button class="modal-close" onclick="closeModal('editEntryModal')">&times;</button>
</div>
<div class="modal-body">
<form id="editEntryForm">
<input type="hidden" id="editEntryId">
<div class="form-group">
<label for="editTaskSelect">Task</label>
<select id="editTaskSelect" required></select>
</div>
<div class="form-row">
<div class="form-group flex-1">
<label for="editDateInput">Date</label>
<input type="date" id="editDateInput" required>
</div>
<div class="form-group flex-1">
<label for="editTimeInput">Time</label>
<input type="text" id="editTimeInput" required>
</div>
</div>
<div class="form-group">
<label for="editNotesInput">Notes</label>
<input type="text" id="editNotesInput">
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-danger" onclick="deleteCurrentEntry()">Delete</button>
<button class="btn btn-secondary" onclick="closeModal('editEntryModal')">Cancel</button>
<button class="btn btn-primary" onclick="submitEditEntry()">Save Changes</button>
</div>
</div>
</div>
<!-- Print Template -->
<div id="printTemplate" class="print-only">
<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>
@@ -148,5 +111,289 @@
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
<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 %}