- Added cache: 'no-store' to prevent cached 500 errors - Enhanced error messages with status codes - Display detailed error messages to user - Log API response text for debugging - Helps diagnose mobile loading issues
716 lines
27 KiB
JavaScript
716 lines
27 KiB
JavaScript
/**
|
|
* Admin Calendar - Task Management
|
|
*
|
|
* Manages scheduled tasks with filtering, CRUD operations, and session-init integration
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// State
|
|
let currentView = 'list';
|
|
let currentFilters = {
|
|
status: '',
|
|
category: '',
|
|
priority: '',
|
|
projectId: '',
|
|
includeCompleted: false
|
|
};
|
|
let currentPage = 0;
|
|
const pageSize = 50;
|
|
let allTasks = [];
|
|
|
|
// DOM Elements
|
|
const tasksContainer = document.getElementById('tasks-container');
|
|
const addTaskBtn = document.getElementById('add-task-btn');
|
|
const taskModal = document.getElementById('task-modal');
|
|
const taskDetailModal = document.getElementById('task-detail-modal');
|
|
const taskForm = document.getElementById('task-form');
|
|
const paginationContainer = document.getElementById('pagination-container');
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
function init() {
|
|
setupEventListeners();
|
|
loadStats();
|
|
loadTasks();
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Add task button
|
|
addTaskBtn.addEventListener('click', () => openTaskModal());
|
|
|
|
// Modal controls
|
|
document.getElementById('modal-close').addEventListener('click', closeTaskModal);
|
|
document.getElementById('modal-cancel').addEventListener('click', closeTaskModal);
|
|
document.getElementById('detail-modal-close').addEventListener('click', closeDetailModal);
|
|
|
|
// Task form submission
|
|
taskForm.addEventListener('submit', handleTaskSubmit);
|
|
|
|
// Filters
|
|
document.getElementById('filter-status').addEventListener('change', handleFilterChange);
|
|
document.getElementById('filter-category').addEventListener('change', handleFilterChange);
|
|
document.getElementById('filter-priority').addEventListener('change', handleFilterChange);
|
|
document.getElementById('filter-project').addEventListener('change', handleFilterChange);
|
|
document.getElementById('include-completed').addEventListener('change', handleFilterChange);
|
|
document.getElementById('clear-filters-btn').addEventListener('click', clearFilters);
|
|
|
|
// View toggle
|
|
document.getElementById('view-list').addEventListener('click', () => setView('list'));
|
|
document.getElementById('view-timeline').addEventListener('click', () => setView('timeline'));
|
|
|
|
// Pagination
|
|
document.getElementById('pagination-prev').addEventListener('click', () => changePage(-1));
|
|
document.getElementById('pagination-next').addEventListener('click', () => changePage(1));
|
|
|
|
// Event delegation for task actions
|
|
tasksContainer.addEventListener('click', handleTaskAction);
|
|
document.getElementById('task-detail-content').addEventListener('click', handleDetailAction);
|
|
}
|
|
|
|
function handleTaskAction(e) {
|
|
const button = e.target.closest('button[data-action]');
|
|
if (!button) return;
|
|
|
|
const action = button.dataset.action;
|
|
const taskId = button.dataset.taskId;
|
|
|
|
if (action === 'view') viewTask(taskId);
|
|
else if (action === 'complete') completeTask(taskId);
|
|
else if (action === 'edit') editTask(taskId);
|
|
else if (action === 'dismiss') dismissTask(taskId);
|
|
}
|
|
|
|
function handleDetailAction(e) {
|
|
const button = e.target.closest('button[data-action]');
|
|
if (!button) return;
|
|
|
|
const action = button.dataset.action;
|
|
const taskId = button.dataset.taskId;
|
|
|
|
if (action === 'complete') completeTask(taskId);
|
|
else if (action === 'edit') editTask(taskId);
|
|
else if (action === 'dismiss') dismissTask(taskId);
|
|
else if (action === 'delete') deleteTask(taskId);
|
|
}
|
|
|
|
async function loadStats() {
|
|
try {
|
|
const token = localStorage.getItem('admin_token');
|
|
const response = await fetch('/api/calendar/stats', {
|
|
cache: 'no-store',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('Stats API error:', response.status, errorText);
|
|
throw new Error(`Failed to load stats: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const stats = data.stats;
|
|
|
|
document.getElementById('stat-total').textContent = stats.total || 0;
|
|
document.getElementById('stat-pending').textContent = stats.pending || 0;
|
|
document.getElementById('stat-overdue').textContent = stats.overdue || 0;
|
|
document.getElementById('stat-due-soon').textContent = stats.dueSoon || 0;
|
|
document.getElementById('stat-completed').textContent = stats.completed || 0;
|
|
|
|
} catch (error) {
|
|
console.error('Error loading stats:', error);
|
|
showError('Failed to load statistics: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function loadTasks() {
|
|
try {
|
|
const token = localStorage.getItem('admin_token');
|
|
|
|
// Build query parameters
|
|
const params = new URLSearchParams({
|
|
limit: pageSize,
|
|
skip: currentPage * pageSize
|
|
});
|
|
|
|
if (currentFilters.status) params.append('status', currentFilters.status);
|
|
if (currentFilters.category) params.append('category', currentFilters.category);
|
|
if (currentFilters.priority) params.append('priority', currentFilters.priority);
|
|
if (currentFilters.projectId) params.append('projectId', currentFilters.projectId);
|
|
if (currentFilters.includeCompleted) params.append('includeCompleted', 'true');
|
|
|
|
const response = await fetch(`/api/calendar/tasks?${params}`, {
|
|
cache: 'no-store',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('Tasks API error:', response.status, errorText);
|
|
throw new Error(`Failed to load tasks: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
allTasks = data.tasks;
|
|
|
|
renderTasks(allTasks);
|
|
updatePagination(data.pagination);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading tasks:', error);
|
|
tasksContainer.innerHTML = `
|
|
<div class="text-center py-12 text-red-600">
|
|
<p>Failed to load tasks: ${error.message}</p>
|
|
<p class="text-sm mt-2">Check console for details</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function renderTasks(tasks) {
|
|
if (!tasks || tasks.length === 0) {
|
|
tasksContainer.innerHTML = `
|
|
<div class="text-center py-12 text-gray-500">
|
|
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
|
</svg>
|
|
<p class="mt-2">No tasks found</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
if (currentView === 'list') {
|
|
renderListView(tasks);
|
|
} else {
|
|
renderTimelineView(tasks);
|
|
}
|
|
}
|
|
|
|
function renderListView(tasks) {
|
|
const html = tasks.map(task => {
|
|
const statusColor = getStatusColor(task.status);
|
|
const priorityColor = getPriorityColor(task.priority);
|
|
const dueDate = new Date(task.dueDate);
|
|
const isOverdue = task.status === 'overdue' || (task.status === 'pending' && dueDate < new Date());
|
|
|
|
return `
|
|
<div class="bg-white rounded-lg shadow p-4 mb-3 hover:shadow-md transition-shadow">
|
|
<div class="flex justify-between items-start">
|
|
<div class="flex-1">
|
|
<div class="flex items-center space-x-2 mb-2">
|
|
<span class="px-2 py-1 text-xs rounded-full ${statusColor}">${task.status}</span>
|
|
<span class="px-2 py-1 text-xs rounded-full ${priorityColor}">${task.priority}</span>
|
|
<span class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-700">${task.category}</span>
|
|
${task.recurrence !== 'once' ? `<span class="px-2 py-1 text-xs rounded-full bg-purple-100 text-purple-700">↻ ${task.recurrence}</span>` : ''}
|
|
</div>
|
|
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-1">${escapeHtml(task.title)}</h3>
|
|
<p class="text-sm text-gray-600 mb-2">${escapeHtml(task.description.substring(0, 150))}${task.description.length > 150 ? '...' : ''}</p>
|
|
|
|
<div class="flex items-center space-x-4 text-xs text-gray-500">
|
|
<div class="flex items-center">
|
|
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
</svg>
|
|
<span class="${isOverdue ? 'text-red-600 font-semibold' : ''}">${formatDate(dueDate)}</span>
|
|
</div>
|
|
${task.assignedTo ? `
|
|
<div class="flex items-center">
|
|
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
|
</svg>
|
|
${escapeHtml(task.assignedTo)}
|
|
</div>
|
|
` : ''}
|
|
${task.documentRef ? `
|
|
<div class="flex items-center">
|
|
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
</svg>
|
|
<span class="text-blue-600">Document linked</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ml-4 flex flex-col space-y-2">
|
|
<button data-action="view" data-task-id="${task.id}" class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200">
|
|
View
|
|
</button>
|
|
${task.status === 'pending' || task.status === 'overdue' ? `
|
|
<button data-action="complete" data-task-id="${task.id}" class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded hover:bg-green-200">
|
|
Complete
|
|
</button>
|
|
` : ''}
|
|
<button data-action="edit" data-task-id="${task.id}" class="px-3 py-1 text-xs bg-gray-100 text-gray-700 rounded hover:bg-gray-200">
|
|
Edit
|
|
</button>
|
|
${task.status === 'pending' ? `
|
|
<button data-action="dismiss" data-task-id="${task.id}" class="px-3 py-1 text-xs bg-yellow-100 text-yellow-700 rounded hover:bg-yellow-200">
|
|
Dismiss
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
tasksContainer.innerHTML = html;
|
|
}
|
|
|
|
function renderTimelineView(tasks) {
|
|
// Group tasks by due date
|
|
const grouped = {};
|
|
tasks.forEach(task => {
|
|
const date = new Date(task.dueDate).toDateString();
|
|
if (!grouped[date]) grouped[date] = [];
|
|
grouped[date].push(task);
|
|
});
|
|
|
|
const html = Object.keys(grouped).sort((a, b) => new Date(a) - new Date(b)).map(date => {
|
|
const dateTasks = grouped[date];
|
|
return `
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-3 border-b border-gray-200 pb-2">
|
|
${formatDateHeader(new Date(date))}
|
|
</h3>
|
|
<div class="space-y-2">
|
|
${dateTasks.map(task => `
|
|
<div class="bg-white rounded-lg shadow p-3 hover:shadow-md transition-shadow flex justify-between items-center">
|
|
<div class="flex-1">
|
|
<div class="flex items-center space-x-2 mb-1">
|
|
<span class="px-2 py-1 text-xs rounded-full ${getStatusColor(task.status)}">${task.status}</span>
|
|
<span class="px-2 py-1 text-xs rounded-full ${getPriorityColor(task.priority)}">${task.priority}</span>
|
|
</div>
|
|
<h4 class="font-medium text-gray-900">${escapeHtml(task.title)}</h4>
|
|
<p class="text-sm text-gray-600">${escapeHtml(task.category)}</p>
|
|
</div>
|
|
<div class="flex space-x-2">
|
|
<button data-action="view" data-task-id="${task.id}" class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200">
|
|
View
|
|
</button>
|
|
${task.status === 'pending' || task.status === 'overdue' ? `
|
|
<button data-action="complete" data-task-id="${task.id}" class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded hover:bg-green-200">
|
|
Complete
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
tasksContainer.innerHTML = html || '<div class="text-center py-12 text-gray-500">No tasks to display</div>';
|
|
}
|
|
|
|
function updatePagination(pagination) {
|
|
if (!pagination) return;
|
|
|
|
const showing = Math.min(pagination.skip + pagination.limit, pagination.total);
|
|
document.getElementById('pagination-showing').textContent = showing;
|
|
document.getElementById('pagination-total').textContent = pagination.total;
|
|
|
|
document.getElementById('pagination-prev').disabled = pagination.skip === 0;
|
|
document.getElementById('pagination-next').disabled = !pagination.hasMore;
|
|
|
|
if (pagination.total > 0) {
|
|
paginationContainer.classList.remove('hidden');
|
|
} else {
|
|
paginationContainer.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function changePage(delta) {
|
|
currentPage += delta;
|
|
if (currentPage < 0) currentPage = 0;
|
|
loadTasks();
|
|
}
|
|
|
|
function setView(view) {
|
|
currentView = view;
|
|
|
|
// Update button styles
|
|
document.getElementById('view-list').className = view === 'list'
|
|
? 'px-3 py-2 text-sm font-medium rounded-md bg-blue-100 text-blue-700'
|
|
: 'px-3 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-100';
|
|
|
|
document.getElementById('view-timeline').className = view === 'timeline'
|
|
? 'px-3 py-2 text-sm font-medium rounded-md bg-blue-100 text-blue-700'
|
|
: 'px-3 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-100';
|
|
|
|
renderTasks(allTasks);
|
|
}
|
|
|
|
function handleFilterChange(e) {
|
|
const id = e.target.id;
|
|
|
|
if (id === 'filter-status') currentFilters.status = e.target.value;
|
|
else if (id === 'filter-category') currentFilters.category = e.target.value;
|
|
else if (id === 'filter-priority') currentFilters.priority = e.target.value;
|
|
else if (id === 'filter-project') currentFilters.projectId = e.target.value;
|
|
else if (id === 'include-completed') currentFilters.includeCompleted = e.target.checked;
|
|
|
|
currentPage = 0;
|
|
loadTasks();
|
|
}
|
|
|
|
function clearFilters() {
|
|
currentFilters = {
|
|
status: '',
|
|
category: '',
|
|
priority: '',
|
|
projectId: '',
|
|
includeCompleted: false
|
|
};
|
|
|
|
document.getElementById('filter-status').value = '';
|
|
document.getElementById('filter-category').value = '';
|
|
document.getElementById('filter-priority').value = '';
|
|
document.getElementById('filter-project').value = '';
|
|
document.getElementById('include-completed').checked = false;
|
|
|
|
currentPage = 0;
|
|
loadTasks();
|
|
}
|
|
|
|
function openTaskModal(taskId = null) {
|
|
document.getElementById('modal-title').textContent = taskId ? 'Edit Task' : 'Add Task';
|
|
|
|
if (taskId) {
|
|
loadTaskForEdit(taskId);
|
|
} else {
|
|
taskForm.reset();
|
|
document.getElementById('task-id').value = '';
|
|
document.getElementById('task-show-in-session-init').checked = true;
|
|
document.getElementById('task-reminder-days').value = '7';
|
|
document.getElementById('task-assigned-to').value = 'PM';
|
|
}
|
|
|
|
taskModal.classList.remove('hidden');
|
|
}
|
|
|
|
function closeTaskModal() {
|
|
taskModal.classList.add('hidden');
|
|
taskForm.reset();
|
|
}
|
|
|
|
function closeDetailModal() {
|
|
taskDetailModal.classList.add('hidden');
|
|
}
|
|
|
|
async function loadTaskForEdit(taskId) {
|
|
try {
|
|
const token = localStorage.getItem('admin_token');
|
|
const response = await fetch(`/api/calendar/tasks/${taskId}`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to load task');
|
|
|
|
const data = await response.json();
|
|
const task = data.task;
|
|
|
|
document.getElementById('task-id').value = task.id;
|
|
document.getElementById('task-title').value = task.title;
|
|
document.getElementById('task-description').value = task.description;
|
|
document.getElementById('task-due-date').value = formatDateTimeLocal(new Date(task.dueDate));
|
|
document.getElementById('task-priority').value = task.priority;
|
|
document.getElementById('task-category').value = task.category;
|
|
document.getElementById('task-recurrence').value = task.recurrence;
|
|
document.getElementById('task-document-ref').value = task.documentRef || '';
|
|
document.getElementById('task-assigned-to').value = task.assignedTo || 'PM';
|
|
document.getElementById('task-show-in-session-init').checked = task.showInSessionInit;
|
|
document.getElementById('task-reminder-days').value = task.reminderDaysBefore;
|
|
|
|
} catch (error) {
|
|
console.error('Error loading task:', error);
|
|
showError('Failed to load task details');
|
|
closeTaskModal();
|
|
}
|
|
}
|
|
|
|
async function handleTaskSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const taskId = document.getElementById('task-id').value;
|
|
const taskData = {
|
|
title: document.getElementById('task-title').value,
|
|
description: document.getElementById('task-description').value,
|
|
dueDate: document.getElementById('task-due-date').value,
|
|
priority: document.getElementById('task-priority').value,
|
|
category: document.getElementById('task-category').value,
|
|
recurrence: document.getElementById('task-recurrence').value,
|
|
documentRef: document.getElementById('task-document-ref').value || null,
|
|
assignedTo: document.getElementById('task-assigned-to').value,
|
|
showInSessionInit: document.getElementById('task-show-in-session-init').checked,
|
|
reminderDaysBefore: parseInt(document.getElementById('task-reminder-days').value)
|
|
};
|
|
|
|
try {
|
|
const token = localStorage.getItem('admin_token');
|
|
const url = taskId ? `/api/calendar/tasks/${taskId}` : '/api/calendar/tasks';
|
|
const method = taskId ? 'PUT' : 'POST';
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(taskData)
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to save task');
|
|
|
|
closeTaskModal();
|
|
loadTasks();
|
|
loadStats();
|
|
showSuccess(taskId ? 'Task updated successfully' : 'Task created successfully');
|
|
|
|
} catch (error) {
|
|
console.error('Error saving task:', error);
|
|
showError('Failed to save task. Please try again.');
|
|
}
|
|
}
|
|
|
|
async function viewTask(taskId) {
|
|
try {
|
|
const token = localStorage.getItem('admin_token');
|
|
const response = await fetch(`/api/calendar/tasks/${taskId}`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to load task');
|
|
|
|
const data = await response.json();
|
|
const task = data.task;
|
|
|
|
const html = `
|
|
<div class="space-y-4">
|
|
<div class="flex items-center space-x-2">
|
|
<span class="px-3 py-1 text-sm rounded-full ${getStatusColor(task.status)}">${task.status}</span>
|
|
<span class="px-3 py-1 text-sm rounded-full ${getPriorityColor(task.priority)}">${task.priority}</span>
|
|
<span class="px-3 py-1 text-sm rounded-full bg-gray-100 text-gray-700">${task.category}</span>
|
|
</div>
|
|
|
|
<h2 class="text-2xl font-bold text-gray-900">${escapeHtml(task.title)}</h2>
|
|
|
|
<div class="bg-gray-50 rounded-lg p-4">
|
|
<p class="text-gray-700">${escapeHtml(task.description)}</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span class="font-medium text-gray-700">Due Date:</span>
|
|
<span class="text-gray-900 ml-2">${formatDateTime(new Date(task.dueDate))}</span>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-gray-700">Recurrence:</span>
|
|
<span class="text-gray-900 ml-2">${task.recurrence}</span>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-gray-700">Assigned To:</span>
|
|
<span class="text-gray-900 ml-2">${escapeHtml(task.assignedTo || 'Unassigned')}</span>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-gray-700">Session-Init:</span>
|
|
<span class="text-gray-900 ml-2">${task.showInSessionInit ? 'Yes' : 'No'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
${task.documentRef ? `
|
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<div class="flex items-start">
|
|
<svg class="h-5 w-5 text-blue-600 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
</svg>
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium text-blue-900">Linked Document</p>
|
|
<p class="text-sm text-blue-700 mt-1">${escapeHtml(task.documentRef)}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="flex justify-end space-x-3 pt-4 border-t">
|
|
${task.status === 'pending' || task.status === 'overdue' ? `
|
|
<button data-action="complete" data-task-id="${task.id}" class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700">
|
|
Mark Complete
|
|
</button>
|
|
` : ''}
|
|
<button data-action="edit" data-task-id="${task.id}" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
|
|
Edit Task
|
|
</button>
|
|
${task.status === 'pending' ? `
|
|
<button data-action="dismiss" data-task-id="${task.id}" class="px-4 py-2 bg-yellow-600 text-white rounded-md hover:bg-yellow-700">
|
|
Dismiss
|
|
</button>
|
|
` : ''}
|
|
<button data-action="delete" data-task-id="${task.id}" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('task-detail-content').innerHTML = html;
|
|
taskDetailModal.classList.remove('hidden');
|
|
|
|
} catch (error) {
|
|
console.error('Error viewing task:', error);
|
|
showError('Failed to load task details');
|
|
}
|
|
}
|
|
|
|
async function completeTask(taskId) {
|
|
if (!confirm('Mark this task as completed?')) return;
|
|
|
|
try {
|
|
const token = localStorage.getItem('admin_token');
|
|
const response = await fetch(`/api/calendar/tasks/${taskId}/complete`, {
|
|
method: 'POST',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to complete task');
|
|
|
|
closeDetailModal();
|
|
loadTasks();
|
|
loadStats();
|
|
showSuccess('Task marked as completed');
|
|
|
|
} catch (error) {
|
|
console.error('Error completing task:', error);
|
|
showError('Failed to complete task');
|
|
}
|
|
}
|
|
|
|
async function dismissTask(taskId) {
|
|
const reason = prompt('Reason for dismissing this task?');
|
|
if (!reason) return;
|
|
|
|
try {
|
|
const token = localStorage.getItem('admin_token');
|
|
const response = await fetch(`/api/calendar/tasks/${taskId}/dismiss`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ reason })
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to dismiss task');
|
|
|
|
closeDetailModal();
|
|
loadTasks();
|
|
loadStats();
|
|
showSuccess('Task dismissed');
|
|
|
|
} catch (error) {
|
|
console.error('Error dismissing task:', error);
|
|
showError('Failed to dismiss task');
|
|
}
|
|
}
|
|
|
|
async function deleteTask(taskId) {
|
|
if (!confirm('Are you sure you want to delete this task? This action cannot be undone.')) return;
|
|
|
|
try {
|
|
const token = localStorage.getItem('admin_token');
|
|
const response = await fetch(`/api/calendar/tasks/${taskId}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to delete task');
|
|
|
|
closeDetailModal();
|
|
loadTasks();
|
|
loadStats();
|
|
showSuccess('Task deleted successfully');
|
|
|
|
} catch (error) {
|
|
console.error('Error deleting task:', error);
|
|
showError('Failed to delete task');
|
|
}
|
|
}
|
|
|
|
function editTask(taskId) {
|
|
closeDetailModal();
|
|
openTaskModal(taskId);
|
|
}
|
|
|
|
// Utility functions
|
|
function getStatusColor(status) {
|
|
const colors = {
|
|
pending: 'bg-yellow-100 text-yellow-800',
|
|
in_progress: 'bg-blue-100 text-blue-800',
|
|
completed: 'bg-green-100 text-green-800',
|
|
dismissed: 'bg-gray-100 text-gray-800',
|
|
overdue: 'bg-red-100 text-red-800'
|
|
};
|
|
return colors[status] || 'bg-gray-100 text-gray-800';
|
|
}
|
|
|
|
function getPriorityColor(priority) {
|
|
const colors = {
|
|
LOW: 'bg-gray-100 text-gray-700',
|
|
MEDIUM: 'bg-blue-100 text-blue-700',
|
|
HIGH: 'bg-orange-100 text-orange-700',
|
|
CRITICAL: 'bg-red-100 text-red-700'
|
|
};
|
|
return colors[priority] || 'bg-gray-100 text-gray-700';
|
|
}
|
|
|
|
function formatDate(date) {
|
|
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
|
return date.toLocaleDateString('en-US', options);
|
|
}
|
|
|
|
function formatDateTime(date) {
|
|
const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
|
|
return date.toLocaleDateString('en-US', options);
|
|
}
|
|
|
|
function formatDateHeader(date) {
|
|
const today = new Date();
|
|
const tomorrow = new Date(today);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
|
|
if (date.toDateString() === today.toDateString()) return 'Today';
|
|
if (date.toDateString() === tomorrow.toDateString()) return 'Tomorrow';
|
|
|
|
return formatDate(date);
|
|
}
|
|
|
|
function formatDateTimeLocal(date) {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
alert(message);
|
|
}
|
|
|
|
function showError(message) {
|
|
alert('Error: ' + message);
|
|
}
|
|
|
|
})();
|