Update index template with user context
This commit is contained in:
@@ -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">
|
||||
<div class="card-header">
|
||||
<h2>Log Time</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="timeEntryForm" class="time-entry-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group flex-2">
|
||||
<label for="taskSelect">Task</label>
|
||||
<select id="taskSelect" required>
|
||||
<option value="">Select a task...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group flex-1">
|
||||
<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>
|
||||
<!-- 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 class="form-group">
|
||||
<label for="notesInput">Notes (optional)</label>
|
||||
<input type="text" id="notesInput" placeholder="Brief description of work done...">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span>+</span> Add Entry
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Weekly Summary -->
|
||||
<section class="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="form-group">
|
||||
<label for="dateInput">Date</label>
|
||||
<input type="date" id="dateInput" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="summaryContent">
|
||||
<div class="loading">Loading summary...</div>
|
||||
</div>
|
||||
<div class="summary-actions">
|
||||
<button class="btn btn-secondary" id="printTimesheetBtn">
|
||||
🖨️ Print Timesheet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Recent Entries -->
|
||||
<section class="card">
|
||||
<div class="card-header">
|
||||
<h2>Recent Entries</h2>
|
||||
<span class="badge" id="entryCount">0 entries</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="recentEntries">
|
||||
<div class="loading">Loading entries...</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>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- New Task Modal -->
|
||||
<div id="newTaskModal" class="modal">
|
||||
<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="closeModal('newTaskModal')">×</button>
|
||||
<button class="modal-close" onclick="closeAddTaskModal()">×</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 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="closeModal('newTaskModal')">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="submitNewTask()">Add Task</button>
|
||||
<button class="btn btn-secondary" onclick="closeAddTaskModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveNewTask()">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')">×</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>
|
||||
<!-- 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>
|
||||
|
||||
<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-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 %}
|
||||
|
||||
Reference in New Issue
Block a user