HTML Task Management App
생성일: 2024년 10월 7일
답변에 사용된 모델 OpenAI 01 Mini by OpenAI01
생성일: 2024년 10월 7일
답변에 사용된 모델 OpenAI 01 Mini by OpenAI01
Entwickeln eines HTML-Setups
Ich arbeite an einem HTML-Setup mit Hierarchien, Erweiterungen und benutzerdefinierten Feldern.
Felder und Aufgaben
Ich denke darüber nach, wie benutzerdefinierte Felder für jede Aufgabe ausgefüllt und als Spalten in der Tabellenansicht angezeigt werden. Aufgaben können gelöscht oder abgeschlossen und in Pop-ups bearbeitet werden.
Certainly! Below is a comprehensive single-file HTML application that fulfills all the specified requirements. This application allows users to create hierarchical tasks with custom fields, view them in a table, filter tasks, and manage tasks using popups. The data is stored locally using IndexedDB.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Task Manager App</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } #taskTable { width: 100%; border-collapse: collapse; } #taskTable th, #taskTable td { border: 1px solid #ccc; padding: 8px; text-align: left; } #taskTable th { background-color: #f4f4f4; } .expand-btn { cursor: pointer; margin-right: 5px; } .hidden { display: none; } /* Modal Styles */ .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4); } .modal-content { background-color: #fff; margin: 10% auto; padding: 20px; border: 1px solid #888; width: 50%; border-radius: 5px; } .close { color: #aaa; float: right; font-size: 24px; font-weight: bold; cursor: pointer; } .form-group { margin-bottom: 10px; } label { display: block; margin-bottom: 5px; } input[type="text"], textarea, select, input[type="number"], input[type="date"] { width: 100%; padding: 8px; box-sizing: border-box; } button { padding: 8px 12px; margin-right: 5px; } .filter-section { margin-bottom: 20px; } .filter-section input, .filter-section select { margin-right: 10px; } </style> </head> <body> <h1>Task Manager</h1> <!-- Control Buttons --> <div> <button id="addTaskBtn">Add Task</button> <button id="addCustomFieldBtn">Add Custom Field</button> </div> <!-- Filter Section --> <div class="filter-section"> <input type="text" id="textFilter" placeholder="Filter by text"> <span id="customFilters"></span> <button id="applyFilterBtn">Apply Filters</button> <button id="clearFilterBtn">Clear Filters</button> </div> <!-- Task Table --> <table id="taskTable"> <thead> <tr id="tableHeader"> <th>Actions</th> <th>Title</th> <th>Description</th> <th>Status</th> <!-- Custom fields headers will be appended here --> </tr> </thead> <tbody id="taskTableBody"> <!-- Tasks will be populated here --> </tbody> </table> <!-- Add/Edit Task Modal --> <div id="taskModal" class="modal"> <div class="modal-content"> <span class="close" id="closeTaskModal">×</span> <h2 id="taskModalTitle">Add Task</h2> <form id="taskForm"> <input type="hidden" id="taskId"> <input type="hidden" id="parentId"> <div class="form-group"> <label for="taskTitle">Title:</label> <input type="text" id="taskTitle" required> </div> <div class="form-group"> <label for="taskDescription">Description:</label> <textarea id="taskDescription" rows="4" required></textarea> </div> <div id="customFieldsContainer"> <!-- Custom fields will be generated here --> </div> <button type="submit">Save Task</button> </form> </div> </div> <!-- Add Custom Field Modal --> <div id="customFieldModal" class="modal"> <div class="modal-content"> <span class="close" id="closeCustomFieldModal">×</span> <h2>Add Custom Field</h2> <form id="customFieldForm"> <div class="form-group"> <label for="customFieldName">Field Name:</label> <input type="text" id="customFieldName" required> </div> <div class="form-group"> <label for="customFieldType">Field Type:</label> <select id="customFieldType" required> <option value="">Select Type</option> <option value="string">String</option> <option value="number">Number</option> <option value="date">Date</option> <option value="singleSelect">Single Select</option> </select> </div> <div class="form-group" id="singleSelectOptions" style="display: none;"> <label for="customFieldOptions">Options (comma separated):</label> <input type="text" id="customFieldOptions"> </div> <button type="submit">Add Custom Field</button> </form> </div> </div> <script> // Initialize IndexedDB let db; const request = indexedDB.open('TaskManagerDB', 1); request.onupgradeneeded = function(event) { db = event.target.result; const tasksStore = db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true }); tasksStore.createIndex('parentId', 'parentId', { unique: false }); const customFieldsStore = db.createObjectStore('customFields', { keyPath: 'id', autoIncrement: true }); }; request.onsuccess = function(event) { db = event.target.result; loadCustomFields(); loadTasks(); }; request.onerror = function(event) { console.error('IndexedDB error:', event.target.errorCode); }; // Utility Functions function $(id) { return document.getElementById(id); } // Modal Controls const taskModal = $('taskModal'); const customFieldModal = $('customFieldModal'); const closeTaskModal = $('closeTaskModal'); const closeCustomFieldModal = $('closeCustomFieldModal'); closeTaskModal.onclick = () => { taskModal.style.display = 'none'; }; closeCustomFieldModal.onclick = () => { customFieldModal.style.display = 'none'; }; window.onclick = function(event) { if (event.target == taskModal) { taskModal.style.display = 'none'; } if (event.target == customFieldModal) { customFieldModal.style.display = 'none'; } }; // Add Task Button $('addTaskBtn').addEventListener('click', () => { openTaskModal(); }); // Add Custom Field Button $('addCustomFieldBtn').addEventListener('click', () => { openCustomFieldModal(); }); // Handle Custom Field Type Selection $('customFieldType').addEventListener('change', function() { if (this.value === 'singleSelect') { $('singleSelectOptions').style.display = 'block'; } else { $('singleSelectOptions').style.display = 'none'; } }); // Add Custom Field Form Submission $('customFieldForm').addEventListener('submit', function(e) { e.preventDefault(); const name = $('customFieldName').value.trim(); const type = $('customFieldType').value; let options = []; if (type === 'singleSelect') { const opts = $('customFieldOptions').value.trim(); if (opts === '') { alert('Please provide options for single select.'); return; } options = opts.split(',').map(opt => opt.trim()).filter(opt => opt); if (options.length === 0) { alert('Please provide valid options.'); return; } } const transaction = db.transaction(['customFields'], 'readwrite'); const store = transaction.objectStore('customFields'); store.add({ name, type, options }); transaction.oncomplete = () => { customFieldModal.style.display = 'none'; $('customFieldForm').reset(); $('singleSelectOptions').style.display = 'none'; loadCustomFields(); loadTasks(); }; transaction.onerror = (event) => { console.error('Error adding custom field:', event.target.error); }; }); // Open Custom Field Modal function openCustomFieldModal() { $('customFieldForm').reset(); $('singleSelectOptions').style.display = 'none'; customFieldModal.style.display = 'block'; } // Open Task Modal function openTaskModal(task = null, parentId = null) { $('taskForm').reset(); $('customFieldsContainer').innerHTML = ''; if (task) { $('taskModalTitle').innerText = 'Edit Task'; $('taskId').value = task.id; $('parentId').value = task.parentId || ''; $('taskTitle').value = task.title; $('taskDescription').value = task.description; } else { $('taskModalTitle').innerText = parentId ? 'Add Sub-Task' : 'Add Task'; $('taskId').value = ''; $('parentId').value = parentId || ''; $('taskTitle').value = ''; $('taskDescription').value = ''; } // Load custom fields const transaction = db.transaction(['customFields'], 'readonly'); const store = transaction.objectStore('customFields'); const getAll = store.getAll(); getAll.onsuccess = () => { const customFields = getAll.result; customFields.forEach(field => { const div = document.createElement('div'); div.className = 'form-group'; const label = document.createElement('label'); label.innerText = field.name + ':'; div.appendChild(label); let input; switch(field.type) { case 'string': input = document.createElement('input'); input.type = 'text'; input.id = 'custom_' + field.id; input.value = task ? (task.customFields ? task.customFields[field.id] || '' : '') : ''; break; case 'number': input = document.createElement('input'); input.type = 'number'; input.id = 'custom_' + field.id; input.value = task ? (task.customFields ? task.customFields[field.id] || '' : '') : ''; break; case 'date': input = document.createElement('input'); input.type = 'date'; input.id = 'custom_' + field.id; input.value = task ? (task.customFields ? task.customFields[field.id] || '' : '') : ''; break; case 'singleSelect': input = document.createElement('select'); input.id = 'custom_' + field.id; const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.innerText = 'Select'; input.appendChild(defaultOption); field.options.forEach(opt => { const option = document.createElement('option'); option.value = opt; option.innerText = opt; input.appendChild(option); }); input.value = task ? (task.customFields ? task.customFields[field.id] || '' : '') : ''; break; default: break; } if (input) { input.required = false; div.appendChild(input); $('customFieldsContainer').appendChild(div); } }); taskModal.style.display = 'block'; }; getAll.onerror = (event) => { console.error('Error fetching custom fields:', event.target.error); }; } // Task Form Submission $('taskForm').addEventListener('submit', function(e) { e.preventDefault(); const id = $('taskId').value; const parentId = $('parentId').value || null; const title = $('taskTitle').value.trim(); const description = $('taskDescription').value.trim(); // Gather custom fields const customFields = {}; const transaction = db.transaction(['customFields'], 'readonly'); const store = transaction.objectStore('customFields'); const getAll = store.getAll(); getAll.onsuccess = () => { const fields = getAll.result; fields.forEach(field => { const value = $('custom_' + field.id).value; if (value !== undefined) { customFields[field.id] = value; } }); const task = { title, description, parentId, customFields, status: 'Open' }; const dbTransaction = db.transaction(['tasks'], 'readwrite'); const tasksStore = dbTransaction.objectStore('tasks'); if (id) { task.id = Number(id); tasksStore.put(task); } else { tasksStore.add(task); } dbTransaction.oncomplete = () => { taskModal.style.display = 'none'; loadTasks(); }; dbTransaction.onerror = (event) => { console.error('Error saving task:', event.target.error); }; }; getAll.onerror = (event) => { console.error('Error fetching custom fields:', event.target.error); }; }); // Load Custom Fields and Update Table Header and Filters let customFieldsList = []; function loadCustomFields() { const transaction = db.transaction(['customFields'], 'readonly'); const store = transaction.objectStore('customFields'); const getAll = store.getAll(); getAll.onsuccess = () => { customFieldsList = getAll.result; updateTableHeader(); updateFilterSection(); }; getAll.onerror = (event) => { console.error('Error loading custom fields:', event.target.error); }; } // Update Table Header with Custom Fields function updateTableHeader() { const headerRow = $('tableHeader'); // Remove existing custom field headers while (headerRow.children.length > 4) { headerRow.removeChild(headerRow.lastChild); } customFieldsList.forEach(field => { const th = document.createElement('th'); th.innerText = field.name; headerRow.appendChild(th); }); // Add Actions column at the end if needed } // Update Filter Section with Custom Fields function updateFilterSection() { const customFilters = $('customFilters'); customFilters.innerHTML = ''; customFieldsList.forEach(field => { const label = document.createElement('label'); label.innerText = field.name + ': '; let input; switch(field.type) { case 'string': input = document.createElement('input'); input.type = 'text'; break; case 'number': input = document.createElement('input'); input.type = 'number'; break; case 'date': input = document.createElement('input'); input.type = 'date'; break; case 'singleSelect': input = document.createElement('select'); const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.innerText = 'All'; input.appendChild(defaultOption); field.options.forEach(opt => { const option = document.createElement('option'); option.value = opt; option.innerText = opt; input.appendChild(option); }); break; default: break; } if (input) { input.id = 'filter_custom_' + field.id; label.appendChild(input); customFilters.appendChild(label); } }); } // Load and Display Tasks function loadTasks() { const transaction = db.transaction(['tasks'], 'readonly'); const store = transaction.objectStore('tasks'); const getAll = store.getAll(); getAll.onsuccess = () => { const tasks = getAll.result; displayTasks(tasks); }; getAll.onerror = (event) => { console.error('Error loading tasks:', event.target.error); }; } // Display Tasks in Table with Hierarchy function displayTasks(tasks) { const tbody = $('taskTableBody'); tbody.innerHTML = ''; // Apply Filters const filteredTasks = applyFilters(tasks); // Build a map for parent-child relationships const taskMap = {}; filteredTasks.forEach(task => { taskMap[task.id] = { ...task, children: [] }; }); const rootTasks = []; filteredTasks.forEach(task => { if (task.parentId && taskMap[task.parentId]) { taskMap[task.parentId].children.push(taskMap[task.id]); } else { rootTasks.push(taskMap[task.id]); } }); // Recursive function to add tasks to table function addTaskRow(task, level = 0) { const tr = document.createElement('tr'); tr.dataset.id = task.id; tr.dataset.parentId = task.parentId || ''; tr.dataset.level = level; tr.classList.add('task-row'); // Actions Cell const actionsTd = document.createElement('td'); if (task.children.length > 0) { const expandBtn = document.createElement('span'); expandBtn.innerText = '+'; expandBtn.className = 'expand-btn'; expandBtn.onclick = () => toggleChildren(task.id, expandBtn); actionsTd.appendChild(expandBtn); } // Add Edit and Delete Buttons const editBtn = document.createElement('button'); editBtn.innerText = 'Edit'; editBtn.onclick = () => openTaskModal(task); actionsTd.appendChild(editBtn); const deleteBtn = document.createElement('button'); deleteBtn.innerText = task.status === 'Closed' ? 'Reopen' : 'Close'; deleteBtn.onclick = () => toggleTaskStatus(task); actionsTd.appendChild(deleteBtn); const removeBtn = document.createElement('button'); removeBtn.innerText = 'Delete'; removeBtn.onclick = () => deleteTask(task.id); actionsTd.appendChild(removeBtn); tr.appendChild(actionsTd); // Title Cell with Indentation for Hierarchy const titleTd = document.createElement('td'); titleTd.style.paddingLeft = (20 * level) + 'px'; titleTd.innerText = task.title; tr.appendChild(titleTd); // Description Cell const descTd = document.createElement('td'); descTd.innerText = task.description; tr.appendChild(descTd); // Status Cell const statusTd = document.createElement('td'); statusTd.innerText = task.status; tr.appendChild(statusTd); // Custom Fields customFieldsList.forEach(field => { const fieldTd = document.createElement('td'); const value = task.customFields ? task.customFields[field.id] || '' : ''; fieldTd.innerText = value; tr.appendChild(fieldTd); }); tbody.appendChild(tr); // Hide child rows initially task.children.forEach(child => addTaskRow(child, level + 1)); } rootTasks.forEach(task => addTaskRow(task)); } // Toggle Expand/Collapse of Child Tasks function toggleChildren(parentId, btn) { const visible = btn.innerText === '+'; btn.innerText = visible ? '-' : '+'; const rows = document.querySelectorAll(`tr[data-parent-id='${parentId}']`); rows.forEach(row => { row.style.display = visible ? '' : 'none'; // If collapsing, also collapse all nested children if (!visible) { const childBtn = row.querySelector('.expand-btn'); if (childBtn && childBtn.innerText === '-') { childBtn.innerText = '+'; toggleChildren(row.dataset.id, childBtn); } } }); } // Toggle Task Status (Close/Reopen) function toggleTaskStatus(task) { const transaction = db.transaction(['tasks'], 'readwrite'); const store = transaction.objectStore('tasks'); const get = store.get(task.id); get.onsuccess = () => { const updatedTask = get.result; updatedTask.status = updatedTask.status === 'Closed' ? 'Open' : 'Closed'; store.put(updatedTask); }; transaction.oncomplete = () => { loadTasks(); }; transaction.onerror = (event) => { console.error('Error updating task status:', event.target.error); }; } // Delete Task and its Sub-Tasks function deleteTask(taskId) { if (!confirm('Are you sure you want to delete this task and all its sub-tasks?')) return; const transaction = db.transaction(['tasks'], 'readwrite'); const store = transaction.objectStore('tasks'); // Recursive delete function function deleteRecursively(id) { const get = store.get(id); get.onsuccess = () => { const task = get.result; // Find children const index = store.index('parentId'); const range = IDBKeyRange.only(id); const request = index.openCursor(range); request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { deleteRecursively(cursor.primaryKey); cursor.continue(); } }; // Delete the task store.delete(id); }; } deleteRecursively(taskId); transaction.oncomplete = () => { loadTasks(); }; transaction.onerror = (event) => { console.error('Error deleting task:', event.target.error); }; } // Apply Filters to Tasks function applyFilters(tasks) { const textFilter = $('textFilter').value.trim().toLowerCase(); const customFilters = {}; customFieldsList.forEach(field => { const value = $(`filter_custom_${field.id}`).value.trim(); if (value) { customFilters[field.id] = value.toLowerCase(); } }); return tasks.filter(task => { // Text filter const matchesText = task.title.toLowerCase().includes(textFilter) || task.description.toLowerCase().includes(textFilter); if (!matchesText) return false; // Custom filters for (let fieldId in customFilters) { const taskValue = task.customFields ? (task.customFields[fieldId] || '').toString().toLowerCase() : ''; if (!taskValue.includes(customFilters[fieldId])) { return false; } } return true; }); } // Apply Filters Button $('applyFilterBtn').addEventListener('click', () => { loadTasks(); }); // Clear Filters Button $('clearFilterBtn').addEventListener('click', () => { $('textFilter').value = ''; customFieldsList.forEach(field => { $(`filter_custom_${field.id}`).value = ''; }); loadTasks(); }); </script> </body> </html>
HTML Structure:
Styling (CSS):
JavaScript Functionality:
tasks
for storing task data and customFields
for storing custom field definitions.$
: Simplifies document.getElementById
calls.parentId
.Data Persistence:
Hierarchical Task Display:
Dynamic Table Headers and Filters:
Adding Custom Fields:
Adding Tasks:
Editing Tasks:
Closing/Reopening Tasks:
Deleting Tasks:
Filtering Tasks:
Managing Task Hierarchy:
Feel free to customize and enhance this application further based on your specific needs!