Files

624 lines
22 KiB
JavaScript

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 = `
<form class="task-comment-reply-form" data-comment-reply-form="${replyButton.dataset.commentReply}">
<textarea class="form-control" name="comment" rows="2" placeholder="Write a reply..."></textarea>
<div class="task-comment-reply-actions">
<button class="btn btn-sm btn-primary" type="submit">Reply</button>
<button class="btn btn-sm btn-outline-secondary" type="button" data-comment-reply-cancel>Cancel</button>
</div>
</form>
`;
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 = `
<form class="task-comment-edit-form" data-comment-edit-form="${editButton.dataset.commentEdit}">
<textarea class="form-control" name="comment" rows="3">${escapeHtml(commentCard.dataset.commentText ?? '')}</textarea>
<div class="task-comment-reply-actions">
<button class="btn btn-sm btn-primary" type="submit">Save</button>
<button class="btn btn-sm btn-outline-secondary" type="button" data-comment-edit-cancel>Cancel</button>
</div>
</form>
`;
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;
}
});
});