document.addEventListener('DOMContentLoaded', () => { projectTreePromise = loadProjectTree(); populateTaskOptionSelects(); applyPermissionVisibility(); initTaskInlineEditing(); initVersionInlineEditing(); initProjectInlineEditing(); loadRouteFromUrl(); window.addEventListener('popstate', () => { loadRouteFromUrl(); }); document.querySelectorAll('[data-view="dashboard"]').forEach((button) => { button.addEventListener('click', () => { loadDashboard(); }); }); document.getElementById('adminMenuButton')?.addEventListener('click', () => { loadAdmin('workflows'); }); document.querySelectorAll('[data-admin-section]').forEach((button) => { button.addEventListener('click', () => { loadAdmin(button.dataset.adminSection); }); }); document.getElementById('workflowRefreshButton')?.addEventListener('click', () => { loadWorkflowEditor(); }); document.getElementById('workflowTaskTypeSelect')?.addEventListener('change', (event) => { selectedWorkflowTaskType = event.target.value || null; renderWorkflowEditor(); }); document.getElementById('workflowDefaultStateSelect')?.addEventListener('change', (event) => { if (!selectedWorkflowTaskType) return; saveWorkflowAction('SetDefaultState', { task_type: selectedWorkflowTaskType, state: event.target.value }); }); document.getElementById('workflowStateForm')?.addEventListener('submit', async (event) => { event.preventDefault(); const form = event.currentTarget; const data = Object.fromEntries(new FormData(form)); const saved = await saveWorkflowAction('CreateState', data); if (saved) form.reset(); }); document.getElementById('workflowTransitionForm')?.addEventListener('submit', async (event) => { event.preventDefault(); const form = event.currentTarget; const data = Object.fromEntries(new FormData(form)); const saved = await saveWorkflowAction('CreateTransition', data); if (saved) form.reset(); }); document.getElementById('workflowAssignmentForm')?.addEventListener('submit', async (event) => { event.preventDefault(); const form = event.currentTarget; const data = Object.fromEntries(new FormData(form)); data.task_type = selectedWorkflowTaskType; if (!data.task_type) { alert('Select a task type first.'); return; } const saved = await saveWorkflowAction('AssignState', data); if (saved) form.reset(); }); document.getElementById('adminWorkflowsPanel')?.addEventListener('click', (event) => { const stateButton = event.target.closest('[data-workflow-delete-state]'); const transitionButton = event.target.closest('[data-workflow-delete-transition]'); const assignmentButton = event.target.closest('[data-workflow-remove-assignment]'); if (stateButton) { if (!window.confirm('Delete this state and related transitions/assignments?')) return; saveWorkflowAction('DeleteState', {}, { state_id: stateButton.dataset.workflowDeleteState }); } if (transitionButton) { saveWorkflowAction('DeleteTransition', {}, { transition_id: transitionButton.dataset.workflowDeleteTransition }); } if (assignmentButton) { saveWorkflowAction('RemoveAssignedState', {}, { assignment_id: assignmentButton.dataset.workflowRemoveAssignment }); } }); document.querySelectorAll('[data-admin-option-form]').forEach((form) => { form.addEventListener('submit', async (event) => { event.preventDefault(); const saved = await saveAdminOptionForm(form.dataset.adminOptionForm, form); if (saved) form.reset(); }); }); document.getElementById('adminTaskTypeSelect')?.addEventListener('change', (event) => { selectedAdminTaskType = event.target.value || null; renderAdminOptions(); }); document.getElementById('adminUserSelect')?.addEventListener('change', (event) => { selectedAdminUser = event.target.value || null; renderAdminOptions(); }); document.getElementById('adminCustomFieldForm')?.addEventListener('submit', async (event) => { event.preventDefault(); const saved = await saveAdminCustomFieldForm(event.currentTarget); if (saved) event.currentTarget.reset(); }); document.getElementById('adminOptionsPanel')?.addEventListener('submit', async (event) => { const form = event.target.closest('[data-admin-option-edit]'); if (!form) return; event.preventDefault(); await saveAdminOptionForm(form.dataset.adminOptionEdit, form, form.dataset.optionId); }); document.getElementById('adminOptionsPanel')?.addEventListener('submit', async (event) => { const form = event.target.closest('[data-admin-custom-field-edit]'); if (!form) return; event.preventDefault(); await saveAdminCustomFieldForm(form, form.dataset.adminCustomFieldEdit); }); document.getElementById('adminOptionsPanel')?.addEventListener('click', (event) => { const deleteButton = event.target.closest('[data-admin-option-delete]'); if (!deleteButton) return; if (!window.confirm('Delete this option?')) return; deleteAdminOption(deleteButton.dataset.adminOptionDelete, deleteButton.dataset.optionId); }); document.getElementById('adminOptionsPanel')?.addEventListener('click', (event) => { const deleteButton = event.target.closest('[data-admin-custom-field-delete]'); if (!deleteButton) return; if (!window.confirm('Delete this custom field?')) return; deleteAdminCustomField(deleteButton.dataset.adminCustomFieldDelete); }); document.getElementById('adminUsersPanel')?.addEventListener('change', async (event) => { const checkbox = event.target.closest('[data-admin-user-right]'); if (!checkbox) return; checkbox.disabled = true; const saved = await setAdminUserRight( checkbox.dataset.userId, checkbox.dataset.rightId, checkbox.checked ); if (!saved) { checkbox.checked = !checkbox.checked; checkbox.disabled = false; } }); document.getElementById('adminUsersPanel')?.addEventListener('submit', async (event) => { const form = event.target.closest('#adminProjectAccessForm'); if (!form) return; event.preventDefault(); if (!selectedAdminUser) return; const data = Object.fromEntries(new FormData(form)); if (!data.project_id) return; await grantAdminProjectAccess(selectedAdminUser, data.project_id); }); document.getElementById('adminUsersPanel')?.addEventListener('click', async (event) => { const deleteButton = event.target.closest('[data-admin-project-access-delete]'); if (!deleteButton) return; await revokeAdminProjectAccess(deleteButton.dataset.adminProjectAccessDelete); }); document.getElementById('profileMenuButton')?.addEventListener('click', () => { loadProfile(); }); document.getElementById('taskEditButton')?.addEventListener('click', () => { if (!canEditTasks()) return; openTaskEditPopup(); }); document.getElementById('taskView')?.addEventListener('click', (event) => { const customField = event.target.closest('[data-custom-field-id]'); if (customField) { if (!canEditTasks()) return; openCustomFieldInlineEditor(customField); return; } const statusButton = event.target.closest('#taskStatusButton'); if (statusButton) { const menu = document.getElementById('taskStatusMenu'); if (menu) menu.hidden = !menu.hidden; return; } const transitionButton = event.target.closest('[data-task-transition]'); if (transitionButton) { transitionCurrentTask(transitionButton.dataset.taskTransition); } }); document.getElementById('taskView')?.addEventListener('keydown', (event) => { if (['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(event.target.tagName)) return; const customField = event.target.closest('[data-custom-field-id]'); if (!customField || !['Enter', ' '].includes(event.key)) return; event.preventDefault(); if (!canEditTasks()) return; openCustomFieldInlineEditor(customField); }); document.getElementById('taskCommentButton')?.addEventListener('click', () => { document.getElementById('taskCommentInput')?.focus(); }); document.getElementById('versionEditButton')?.addEventListener('click', () => { if (!canEditVersions()) return; openVersionEditPopup(); }); document.getElementById('projectEditButton')?.addEventListener('click', () => { if (!canEditProjects()) return; openProjectEditPopup(); }); document.querySelectorAll('[data-version-task-sort]').forEach((button) => { button.addEventListener('click', () => { const field = button.dataset.versionTaskSort; if (versionTaskSort.field === field) { versionTaskSort.direction = versionTaskSort.direction === 'asc' ? 'desc' : 'asc'; } else { versionTaskSort.field = field; versionTaskSort.direction = 'asc'; } if (currentVersion) { loadVersionTasks(currentVersion.id, 1); } }); }); document.querySelectorAll('[data-project-task-sort]').forEach((button) => { button.addEventListener('click', () => { const field = button.dataset.projectTaskSort; if (projectTaskSort.field === field) { projectTaskSort.direction = projectTaskSort.direction === 'asc' ? 'desc' : 'asc'; } else { projectTaskSort.field = field; projectTaskSort.direction = 'asc'; } if (currentProjectTaskProject) { loadProjectTasks(currentProjectTaskProject, 1); } }); }); document.addEventListener('click', (event) => { const pageButton = event.target.closest('[data-task-page]'); if (!pageButton || pageButton.disabled) return; const page = Number(pageButton.dataset.page); if (!Number.isFinite(page) || page < 1) return; if (pageButton.dataset.taskPage === 'version' && currentVersion) { loadVersionTasks(currentVersion.id, page); } if (pageButton.dataset.taskPage === 'project' && currentProjectTaskProject) { loadProjectTasks(currentProjectTaskProject, page); } }); const createProjectForm = document.getElementById('createProjectForm'); if (createProjectForm) { createProjectForm.addEventListener('submit', async (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(createProjectForm)); const mode = createProjectForm.dataset.mode ?? 'create'; const projectId = document.getElementById('projectFormId').value; const result = await apiPost('/api/project.php', { api: mode === 'edit' ? 'Edit' : 'Create', project_id: mode === 'edit' ? projectId : undefined }, data); if (result.success) { taskLookupCache.projectsById.delete(result.project_id); closePopups(); createProjectForm.reset(); projectTreePromise = loadProjectTree(); if (mode === 'edit') { await loadProject(projectId, false); } } else { alert(result.error || 'Could not create project.'); } }); } const createVersionForm = document.getElementById('createVersionForm'); if (createVersionForm) { createVersionForm.addEventListener('submit', async (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(createVersionForm)); const mode = createVersionForm.dataset.mode ?? 'create'; const versionId = data.version_id; const projectId = data.project_id; delete data.version_id; if (mode === 'edit') { delete data.project_id; } const result = await apiPost('/api/version.php', { api: mode === 'edit' ? 'Edit' : 'Create', version_id: mode === 'edit' ? versionId : undefined }, data); if (result.success) { taskLookupCache.versionsByProject.delete(projectId); closePopups(); projectTreePromise = loadProjectTree(); if (mode === 'edit') { await loadVersion(versionId, false); } } else { alert(result.error || 'Could not save version.'); } }); } const profileForm = document.getElementById('profileForm'); if (profileForm) { document.getElementById('profilePictureInput')?.addEventListener('change', (event) => { const file = event.target.files?.[0]; if (!file) { renderProfileAvatar(currentProfileUser?.picture ?? null); return; } document.getElementById('profileRemovePicture').checked = false; const reader = new FileReader(); reader.addEventListener('load', () => renderProfileAvatar(reader.result)); reader.readAsDataURL(file); }); document.getElementById('profileRemovePicture')?.addEventListener('change', (event) => { if (event.target.checked) { document.getElementById('profilePictureInput').value = ''; renderProfileAvatar(null); } else { renderProfileAvatar(currentProfileUser?.picture ?? null); } }); profileForm.addEventListener('submit', async (event) => { event.preventDefault(); const password = document.getElementById('profilePassword').value; const passwordConfirm = document.getElementById('profilePasswordConfirm').value; const status = document.getElementById('profileStatus'); const submitButton = document.getElementById('profileSubmitButton'); if (password !== passwordConfirm) { status.textContent = 'Passwords do not match.'; return; } const data = new FormData(profileForm); if (!password) { data.delete('password'); } submitButton.disabled = true; status.textContent = 'Saving...'; const result = await apiPostForm('/api/user.php', { api: 'Edit', user_id: getCurrentUserId() }, data); submitButton.disabled = false; if (!result.success) { status.textContent = result.error || 'Could not save profile.'; return; } const userResult = await apiGet('/api/user.php', { api: 'UserInfo', user_id: getCurrentUserId(), _: Date.now() }); if (userResult.success) { taskLookupCache.users = null; updateAccountProfile(userResult.user); applyTheme(userResult.user.settings?.theme ?? 'dark'); renderProfile(userResult.user); status.textContent = 'Saved.'; } else { status.textContent = 'Saved. Refresh to see all changes.'; } }); } const createTaskForm = document.getElementById('createTaskForm'); if (createTaskForm) { document.getElementById('taskFormProjectId')?.addEventListener('change', (event) => { populateTaskRelationSelects(event.target.value.trim()); }); createTaskForm.addEventListener('submit', async (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(createTaskForm)); const mode = createTaskForm.dataset.mode ?? 'create'; const taskId = data.task_id; delete data.task_id; if (mode === 'edit') { delete data.project_id; } const result = await apiPost('/api/task.php', { api: mode === 'edit' ? 'Edit' : 'Create', task_id: mode === 'edit' ? taskId : undefined }, data); if (result.success) { closePopups(); projectTreePromise = loadProjectTree(); loadTask(mode === 'edit' ? taskId : result.task_id, mode !== 'edit'); } else { alert(result.error || 'Could not save task.'); } }); } const taskCommentForm = document.getElementById('taskCommentForm'); if (taskCommentForm) { taskCommentForm.addEventListener('submit', async (event) => { event.preventDefault(); const input = document.getElementById('taskCommentInput'); const status = document.getElementById('taskCommentStatus'); const comment = input.value.trim(); if (!comment) { status.textContent = 'Comment cannot be empty.'; return; } status.textContent = 'Saving...'; try { await submitTaskComment(comment); input.value = ''; status.textContent = 'Saved.'; } catch (error) { status.textContent = error.message; } }); } document.getElementById('taskCommentList')?.addEventListener('click', (event) => { const replyButton = event.target.closest('[data-comment-reply]'); const editButton = event.target.closest('[data-comment-edit]'); const deleteButton = event.target.closest('[data-comment-delete]'); const cancelButton = event.target.closest('[data-comment-reply-cancel]'); const editCancelButton = event.target.closest('[data-comment-edit-cancel]'); if (replyButton) { const commentCard = replyButton.closest('.task-comment'); const slot = commentCard?.querySelector('.task-comment-reply-slot'); if (!slot || slot.querySelector('form')) return; slot.innerHTML = `
`; slot.querySelector('textarea')?.focus(); } if (editButton) { const commentCard = editButton.closest('.task-comment'); const slot = commentCard?.querySelector('.task-comment-edit-slot'); if (!slot || slot.querySelector('form')) return; slot.innerHTML = ` `; slot.querySelector('textarea')?.focus(); } if (deleteButton) { if (!window.confirm('Delete this comment?')) return; deleteTaskComment(deleteButton.dataset.commentDelete).catch((error) => { alert(error.message); }); } if (cancelButton) { cancelButton.closest('.task-comment-reply-slot').innerHTML = ''; } if (editCancelButton) { editCancelButton.closest('.task-comment-edit-slot').innerHTML = ''; } }); document.getElementById('taskCommentList')?.addEventListener('submit', async (event) => { const form = event.target.closest('[data-comment-reply-form], [data-comment-edit-form]'); if (!form) return; event.preventDefault(); const textarea = form.querySelector('textarea'); const button = form.querySelector('button[type="submit"]'); const comment = textarea.value.trim(); const responseTo = form.dataset.commentReplyForm; const editCommentId = form.dataset.commentEditForm; if (!comment) return; button.disabled = true; try { if (editCommentId) { await updateTaskComment(editCommentId, comment); } else { await submitTaskComment(comment, responseTo); } } catch (error) { alert(error.message); button.disabled = false; } }); });