diff --git a/TaskTracker.Api/Pages/Analytics.cshtml.cs b/TaskTracker.Api/Pages/Analytics.cshtml.cs index 9e2a01e..8410130 100644 --- a/TaskTracker.Api/Pages/Analytics.cshtml.cs +++ b/TaskTracker.Api/Pages/Analytics.cshtml.cs @@ -7,6 +7,7 @@ using TaskTracker.Core.Interfaces; namespace TaskTracker.Api.Pages; +[IgnoreAntiforgeryToken] public class AnalyticsModel : PageModel { private readonly ITaskRepository _taskRepo; diff --git a/TaskTracker.Api/Pages/Index.cshtml b/TaskTracker.Api/Pages/Index.cshtml new file mode 100644 index 0000000..2bab136 --- /dev/null +++ b/TaskTracker.Api/Pages/Index.cshtml @@ -0,0 +1,2 @@ +@page +@model TaskTracker.Api.Pages.IndexModel diff --git a/TaskTracker.Api/Pages/Index.cshtml.cs b/TaskTracker.Api/Pages/Index.cshtml.cs new file mode 100644 index 0000000..3249a30 --- /dev/null +++ b/TaskTracker.Api/Pages/Index.cshtml.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace TaskTracker.Api.Pages; + +public class IndexModel : PageModel +{ + public IActionResult OnGet() => RedirectToPage("/Board"); +} diff --git a/TaskTracker.Api/Pages/Partials/_MappingRow.cshtml b/TaskTracker.Api/Pages/Partials/_MappingRow.cshtml index 461ef58..14e4ea3 100644 --- a/TaskTracker.Api/Pages/Partials/_MappingRow.cshtml +++ b/TaskTracker.Api/Pages/Partials/_MappingRow.cshtml @@ -28,7 +28,7 @@ - - - - `; - container.querySelectorAll('[data-action]').forEach(btn => { - btn.addEventListener('click', async () => { - const action = btn.dataset.action; - const id = btn.dataset.id; - try { - if (action === 'pause') await api.tasks.pause(id); - else if (action === 'complete') await api.tasks.complete(id); - refreshDashboard(); - } catch (e) { alert(e.message); } - }); - }); -} - -function renderStats(allTasks) { - const counts = { Pending: 0, Active: 0, Paused: 0, Completed: 0, Abandoned: 0 }; - allTasks.forEach(t => counts[t.status] = (counts[t.status] || 0) + 1); - const container = document.getElementById('dash-stats'); - container.innerHTML = Object.entries(counts).map(([status, count]) => ` -
-
${count}
-
${status}
-
`).join(''); -} - -function renderContextSummary(summary) { - const container = document.getElementById('dash-context'); - if (!summary || summary.length === 0) { - container.innerHTML = `
No recent activity
`; - return; - } - container.innerHTML = summary.slice(0, 10).map(item => ` -
-
-
${esc(item.appName)}
-
${esc(item.category)}
-
-
${item.eventCount}
-
`).join(''); -} - -function formatElapsed(since) { - const diff = Math.floor((Date.now() - since.getTime()) / 1000); - if (diff < 60) return `${diff}s`; - if (diff < 3600) return `${Math.floor(diff / 60)}m`; - const h = Math.floor(diff / 3600); - const m = Math.floor((diff % 3600) / 60); - return `${h}h ${m}m`; -} - -function esc(str) { - if (!str) return ''; - const d = document.createElement('div'); - d.textContent = str; - return d.innerHTML; -} diff --git a/TaskTracker.Api/wwwroot/js/pages/mappings.js b/TaskTracker.Api/wwwroot/js/pages/mappings.js deleted file mode 100644 index 86e15eb..0000000 --- a/TaskTracker.Api/wwwroot/js/pages/mappings.js +++ /dev/null @@ -1,119 +0,0 @@ -import * as api from '../api.js'; -import { showModal } from '../components/modal.js'; - -const el = () => document.getElementById('page-mappings'); - -export async function initMappings() { - el().innerHTML = ` -

App Mappings

-
-
Map process names, window titles, or URLs to categories
- -
-
`; - - document.getElementById('btn-new-mapping').addEventListener('click', () => showMappingForm()); - await loadMappings(); -} - -async function loadMappings() { - try { - const mappings = await api.mappings.list(); - const container = document.getElementById('mapping-list'); - if (!mappings || !mappings.length) { - container.innerHTML = `
No mappings configured
`; - return; - } - container.innerHTML = ` - - - - ${mappings.map(m => ` - - - - - - - `).join('')} - -
PatternMatch TypeCategoryFriendly NameActions
${esc(m.pattern)}${m.matchType}${esc(m.category)}${esc(m.friendlyName) || '-'} -
- - -
-
`; - container.querySelectorAll('[data-edit]').forEach(btn => { - btn.addEventListener('click', () => { - const m = mappings.find(x => x.id === parseInt(btn.dataset.edit)); - if (m) showMappingForm(m); - }); - }); - container.querySelectorAll('[data-delete]').forEach(btn => { - btn.addEventListener('click', () => confirmDelete(parseInt(btn.dataset.delete))); - }); - } catch (e) { - document.getElementById('mapping-list').innerHTML = `
Failed to load mappings
`; - } -} - -function showMappingForm(existing = null) { - const title = existing ? 'Edit Mapping' : 'New Mapping'; - showModal(title, ` -
- - -
-
- - -
-
- - -
-
- - -
`, - [ - { label: 'Cancel', onClick: () => {} }, - { - label: existing ? 'Save' : 'Create', cls: 'btn-primary', onClick: async (modal) => { - const pattern = modal.querySelector('#map-pattern').value.trim(); - const matchType = modal.querySelector('#map-match-type').value; - const category = modal.querySelector('#map-category').value.trim(); - const friendlyName = modal.querySelector('#map-friendly').value.trim() || null; - if (!pattern || !category) { alert('Pattern and Category are required'); throw new Error('cancel'); } - const body = { pattern, matchType, category, friendlyName }; - if (existing) await api.mappings.update(existing.id, body); - else await api.mappings.create(body); - loadMappings(); - }, - }, - ]); - setTimeout(() => document.getElementById('map-pattern')?.focus(), 100); -} - -function confirmDelete(id) { - showModal('Delete Mapping', `

Are you sure you want to delete this mapping?

`, [ - { label: 'Cancel', onClick: () => {} }, - { - label: 'Delete', cls: 'btn-danger', onClick: async () => { - await api.mappings.remove(id); - loadMappings(); - }, - }, - ]); -} - -function esc(str) { - if (!str) return ''; - const d = document.createElement('div'); - d.textContent = str; - return d.innerHTML; -} diff --git a/TaskTracker.Api/wwwroot/js/pages/tasks.js b/TaskTracker.Api/wwwroot/js/pages/tasks.js deleted file mode 100644 index c015247..0000000 --- a/TaskTracker.Api/wwwroot/js/pages/tasks.js +++ /dev/null @@ -1,357 +0,0 @@ -import * as api from '../api.js'; -import { showModal, closeModal } from '../components/modal.js'; - -const el = () => document.getElementById('page-tasks'); -let currentFilter = null; -let selectedTaskId = null; - -export function initTasks() { - el().innerHTML = ` -

Tasks

-
-
- -
-
- `; - - renderFilters(); - document.getElementById('btn-new-task').addEventListener('click', () => showNewTaskModal()); - loadTasks(); -} - -function renderFilters() { - const statuses = [null, 'Pending', 'Active', 'Paused', 'Completed', 'Abandoned']; - const labels = ['All', 'Pending', 'Active', 'Paused', 'Completed', 'Abandoned']; - const container = document.getElementById('task-filters'); - container.innerHTML = statuses.map((s, i) => ` - `).join(''); - container.querySelectorAll('.filter-btn').forEach(btn => { - btn.addEventListener('click', () => { - currentFilter = btn.dataset.status || null; - renderFilters(); - loadTasks(); - }); - }); -} - -async function loadTasks() { - try { - const tasks = await api.tasks.list(currentFilter); - renderTaskList(tasks); - } catch (e) { - document.getElementById('task-list').innerHTML = `
Failed to load tasks
`; - } -} - -function renderTaskList(tasks) { - const container = document.getElementById('task-list'); - document.getElementById('task-detail').classList.add('hidden'); - container.classList.remove('hidden'); - if (!tasks.length) { - container.innerHTML = `
No tasks found
`; - return; - } - container.innerHTML = tasks.map(t => { - const subCount = t.subTasks ? t.subTasks.length : 0; - return ` -
-
- ${t.status} - ${esc(t.title)} - ${subCount > 0 ? `${subCount} subtask${subCount !== 1 ? 's' : ''}` : ''} -
-
${t.category ? esc(t.category) + ' · ' : ''}${formatDate(t.createdAt)}
-
`; - }).join(''); - container.querySelectorAll('.task-item').forEach(item => { - item.addEventListener('click', () => showTaskDetail(parseInt(item.dataset.id))); - }); -} - -async function buildBreadcrumbs(task) { - const trail = [{ id: task.id, title: task.title }]; - let current = task; - while (current.parentTaskId) { - try { - current = await api.tasks.get(current.parentTaskId); - trail.unshift({ id: current.id, title: current.title }); - } catch { - break; - } - } - return trail; -} - -async function showTaskDetail(id) { - selectedTaskId = id; - try { - const task = await api.tasks.get(id); - const container = document.getElementById('task-detail'); - document.getElementById('task-list').classList.add('hidden'); - container.classList.remove('hidden'); - - // Build breadcrumb trail - const breadcrumbs = await buildBreadcrumbs(task); - const breadcrumbHtml = breadcrumbs.length > 1 - ? `` - : ''; - - container.innerHTML = ` - - ${breadcrumbHtml} -
-
-
-
${esc(task.title)}
- ${task.status} -
-
-
- ${task.description ? `

${esc(task.description)}

` : ''} -
-
Category
${esc(task.category) || 'None'}
-
Created
${formatDateTime(task.createdAt)}
-
Started
${task.startedAt ? formatDateTime(task.startedAt) : 'Not started'}
-
Completed
${task.completedAt ? formatDateTime(task.completedAt) : '-'}
-
-
Subtasks
-
- ${task.status !== 'Completed' && task.status !== 'Abandoned' ? `` : ''} -
Notes
-
-
-
- -
- -
- ${task.contextEvents && task.contextEvents.length ? ` -
Linked Context Events
-
- - - - ${task.contextEvents.slice(0, 50).map(e => ` - - - - - `).join('')} - -
AppTitleTime
${esc(e.appName)}${esc(e.windowTitle)}${formatTime(e.timestamp)}
-
` : ''} -
`; - - // Render action buttons based on status - renderActions(task); - - // Render subtasks - renderSubTasks(task.subTasks || []); - - // Render notes - renderNotes(task.notes || []); - - // Breadcrumb navigation - container.querySelectorAll('.breadcrumb-link').forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - showTaskDetail(parseInt(link.dataset.id)); - }); - }); - - // Back button - document.getElementById('btn-back-tasks').addEventListener('click', () => { - if (task.parentTaskId) { - showTaskDetail(task.parentTaskId); - } else { - container.classList.add('hidden'); - document.getElementById('task-list').classList.remove('hidden'); - loadTasks(); - } - }); - - // Add subtask button - const addSubBtn = document.getElementById('btn-add-subtask'); - if (addSubBtn) { - addSubBtn.addEventListener('click', () => showNewTaskModal(task.id)); - } - - // Add note - const noteInput = document.getElementById('note-input'); - document.getElementById('btn-add-note').addEventListener('click', () => addNote(task.id, noteInput)); - noteInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter') addNote(task.id, noteInput); - }); - } catch (e) { - alert('Failed to load task: ' + e.message); - } -} - -function renderSubTasks(subTasks) { - const container = document.getElementById('task-subtasks'); - if (!subTasks.length) { - container.innerHTML = `
No subtasks
`; - return; - } - container.innerHTML = subTasks.map(st => { - const subCount = st.subTasks ? st.subTasks.length : 0; - const canStart = st.status === 'Pending' || st.status === 'Paused'; - const canComplete = st.status === 'Active' || st.status === 'Paused'; - return ` -
-
- ${st.status} - ${esc(st.title)} - ${subCount > 0 ? `${subCount}` : ''} -
-
- ${canStart ? `` : ''} - ${canComplete ? `` : ''} -
-
`; - }).join(''); - - // Navigate to subtask detail - container.querySelectorAll('.subtask-item-title').forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - showTaskDetail(parseInt(link.dataset.id)); - }); - }); - - // Inline subtask actions - container.querySelectorAll('.subtask-action').forEach(btn => { - btn.addEventListener('click', async (e) => { - e.stopPropagation(); - const action = btn.dataset.action; - const stId = parseInt(btn.dataset.id); - try { - if (action === 'start') await api.tasks.start(stId); - else if (action === 'complete') await api.tasks.complete(stId); - showTaskDetail(selectedTaskId); - } catch (err) { alert(err.message); } - }); - }); -} - -function renderActions(task) { - const container = document.getElementById('task-actions'); - const actions = []; - switch (task.status) { - case 'Pending': - actions.push({ label: 'Start', cls: 'btn-success', action: () => api.tasks.start(task.id) }); - actions.push({ label: 'Abandon', cls: 'btn-danger', action: () => api.tasks.abandon(task.id) }); - break; - case 'Active': - actions.push({ label: 'Pause', cls: 'btn-warning', action: () => api.tasks.pause(task.id) }); - actions.push({ label: 'Complete', cls: 'btn-success', action: () => api.tasks.complete(task.id) }); - actions.push({ label: 'Abandon', cls: 'btn-danger', action: () => api.tasks.abandon(task.id) }); - break; - case 'Paused': - actions.push({ label: 'Resume', cls: 'btn-success', action: () => api.tasks.resume(task.id) }); - actions.push({ label: 'Complete', cls: 'btn-success', action: () => api.tasks.complete(task.id) }); - actions.push({ label: 'Abandon', cls: 'btn-danger', action: () => api.tasks.abandon(task.id) }); - break; - } - container.innerHTML = ''; - actions.forEach(({ label, cls, action }) => { - const btn = document.createElement('button'); - btn.className = `btn btn-sm ${cls}`; - btn.textContent = label; - btn.addEventListener('click', async () => { - try { - await action(); - showTaskDetail(task.id); - } catch (e) { alert(e.message); } - }); - container.appendChild(btn); - }); -} - -function renderNotes(notes) { - const container = document.getElementById('task-notes'); - if (!notes.length) { - container.innerHTML = `
No notes yet
`; - return; - } - container.innerHTML = notes.map(n => ` -
-
- ${n.type} - ${formatDateTime(n.createdAt)} -
-
${esc(n.content)}
-
`).join(''); -} - -async function addNote(taskId, input) { - const content = input.value.trim(); - if (!content) return; - try { - await api.notes.create(taskId, { content, type: 'General' }); - input.value = ''; - showTaskDetail(taskId); - } catch (e) { alert(e.message); } -} - -function showNewTaskModal(parentTaskId = null) { - const title = parentTaskId ? 'New Subtask' : 'New Task'; - showModal(title, ` -
- - -
-
- - -
-
- - -
`, - [ - { label: 'Cancel', onClick: () => {} }, - { - label: 'Create', cls: 'btn-primary', onClick: async (modal) => { - const taskTitle = modal.querySelector('#new-task-title').value.trim(); - if (!taskTitle) { alert('Title is required'); throw new Error('cancel'); } - const description = modal.querySelector('#new-task-desc').value.trim() || null; - const category = modal.querySelector('#new-task-cat').value.trim() || null; - const body = { title: taskTitle, description, category }; - if (parentTaskId) body.parentTaskId = parentTaskId; - await api.tasks.create(body); - if (parentTaskId) { - showTaskDetail(parentTaskId); - } else { - loadTasks(); - } - }, - }, - ]); - setTimeout(() => document.getElementById('new-task-title')?.focus(), 100); -} - -function formatDate(iso) { - return new Date(iso).toLocaleDateString(); -} - -function formatDateTime(iso) { - const d = new Date(iso); - return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); -} - -function formatTime(iso) { - return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); -} - -function esc(str) { - if (!str) return ''; - const d = document.createElement('div'); - d.textContent = str; - return d.innerHTML; -}