647 lines
22 KiB
JavaScript
647 lines
22 KiB
JavaScript
// --- Basic Todo Model ---
|
|
const STORAGE_KEY = 'todos-v1';
|
|
const CATEGORY_KEY = 'todo-categories';
|
|
let todos = [];
|
|
let categories = [];
|
|
|
|
function loadTodos() {
|
|
todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
|
|
categories = JSON.parse(localStorage.getItem(CATEGORY_KEY) || '[]');
|
|
}
|
|
|
|
function saveTodos() {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
|
|
localStorage.setItem(CATEGORY_KEY, JSON.stringify(categories));
|
|
}
|
|
|
|
function getNextId() {
|
|
return todos.length ? Math.max(...todos.map(t => t.id)) + 1 : 1;
|
|
}
|
|
|
|
function renderForm() {
|
|
const form = document.getElementById('todo-form');
|
|
form.innerHTML = `
|
|
<div class="form-section">
|
|
<label for="task">Task Description</label>
|
|
<input type="text" id="task" placeholder="Task Description" required>
|
|
</div>
|
|
|
|
<div class="form-section grid-3">
|
|
<div>
|
|
<label for="creation-date">Creation Date</label>
|
|
<input type="date" id="creation-date" title="Creation Date">
|
|
</div>
|
|
<div>
|
|
<label for="next-date">Next Planned Work Date</label>
|
|
<input type="date" id="next-date" title="Next Planned Work Date">
|
|
</div>
|
|
<div>
|
|
<label for="due-date">Due Date</label>
|
|
<input type="date" id="due-date" title="Due Date">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section grid-3">
|
|
<div>
|
|
<label for="status">Status</label>
|
|
<select id="status">
|
|
<option value="">(Blank)</option>
|
|
<option value="Busy">Busy</option>
|
|
<option value="Done">Done</option>
|
|
<option value="W4A">W4A</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="urgent">Urgent (1-7)</label>
|
|
<input type="number" id="urgent" min="1" max="7" placeholder="Urgent (1-7)">
|
|
</div>
|
|
<div>
|
|
<label for="importance">Importance (1-7)</label>
|
|
<input type="number" id="importance" min="1" max="7" placeholder="Importance (1-7)">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section grid-2">
|
|
<div>
|
|
<label for="time-estimation">Time Estimation (min)</label>
|
|
<input type="number" id="time-estimation" min="0" placeholder="Time Est. (min)">
|
|
</div>
|
|
<div>
|
|
<label for="actual-time">Actual Time Spent (min)</label>
|
|
<input type="number" id="actual-time" min="0" placeholder="Actual Time (min)">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section grid-2">
|
|
<div>
|
|
<label for="category">Category</label>
|
|
<input type="text" id="category" placeholder="Category" list="category-list">
|
|
<datalist id="category-list"></datalist>
|
|
</div>
|
|
<div>
|
|
<label for="linked-tasks">Link with other tasks (IDs, comma separated)</label>
|
|
<input type="text" id="linked-tasks" placeholder="Link with other tasks (IDs, comma separated)">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section grid-2">
|
|
<div>
|
|
<label for="coms">Coms URL</label>
|
|
<input type="url" id="coms" placeholder="Coms URL">
|
|
</div>
|
|
<div>
|
|
<label for="link">Link URL</label>
|
|
<input type="url" id="link" placeholder="Link URL">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<label for="comments">Comments</label>
|
|
<textarea id="comments" placeholder="Comments"></textarea>
|
|
</div>
|
|
|
|
<button type="submit" class="add-todo-btn">Add Todo</button>
|
|
`;
|
|
// Autocomplete for categories
|
|
const datalist = document.getElementById('category-list');
|
|
datalist.innerHTML = categories.map(cat => `<option value="${cat}">`).join('');
|
|
// Ensure addTodo is always bound after form render
|
|
form.removeEventListener('submit', addTodo); // prevent duplicates
|
|
form.addEventListener('submit', addTodo);
|
|
}
|
|
|
|
let sortState = { column: null, asc: true };
|
|
|
|
const sortableColumns = [
|
|
{ key: 'id', label: 'ID' },
|
|
{ key: 'task', label: 'Description' },
|
|
{ key: 'creationDate', label: 'Creation Date' },
|
|
{ key: 'nextDate', label: 'Next Date' },
|
|
{ key: 'dueDate', label: 'Due Date' },
|
|
{ key: 'urgent', label: 'Urgent' },
|
|
{ key: 'importance', label: 'Importance' },
|
|
{ key: 'prio', label: 'Prio' }
|
|
];
|
|
|
|
function sortTodos(column) {
|
|
if (sortState.column === column) {
|
|
sortState.asc = !sortState.asc;
|
|
} else {
|
|
sortState.column = column;
|
|
sortState.asc = true;
|
|
}
|
|
todos.sort((a, b) => {
|
|
let valA = a[column];
|
|
let valB = b[column];
|
|
// For date columns, compare as dates
|
|
if (["creationDate","nextDate","dueDate"].includes(column)) {
|
|
valA = valA || '';
|
|
valB = valB || '';
|
|
return sortState.asc ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
} else if (typeof valA === 'string') {
|
|
return sortState.asc ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
} else {
|
|
return sortState.asc ? valA - valB : valB - valA;
|
|
}
|
|
});
|
|
renderTodos();
|
|
}
|
|
|
|
function formatDisplayDate(dateStr) {
|
|
if (!dateStr) return '';
|
|
// Accepts YYYY-MM-DD or ISO string
|
|
let d = dateStr.length > 10 ? new Date(dateStr) : null;
|
|
let year, month, day;
|
|
if (d && !isNaN(d)) {
|
|
year = d.getFullYear().toString().slice(-2);
|
|
month = String(d.getMonth() + 1).padStart(2, '0');
|
|
day = String(d.getDate()).padStart(2, '0');
|
|
} else if (/^\d{4}-\d{2}-\d{2}/.test(dateStr)) {
|
|
year = dateStr.slice(2,4);
|
|
month = dateStr.slice(5,7);
|
|
day = dateStr.slice(8,10);
|
|
} else {
|
|
return dateStr;
|
|
}
|
|
return `${day}-${month}-${year}`;
|
|
}
|
|
|
|
function renderTodos() {
|
|
const list = document.getElementById('todos-list');
|
|
if (!todos.length) {
|
|
list.innerHTML = '<em>No todos yet.</em>';
|
|
return;
|
|
}
|
|
const columns = [
|
|
{ key: 'id', label: 'ID', sortable: true },
|
|
{ key: 'task', label: 'Description', sortable: true },
|
|
{ key: 'creationDate', label: 'Creation Date', sortable: true },
|
|
{ key: 'nextDate', label: 'Next Date', sortable: true },
|
|
{ key: 'dueDate', label: 'Due Date', sortable: true },
|
|
{ key: 'status', label: 'Status' },
|
|
{ key: 'urgent', label: 'Urgent', sortable: true },
|
|
{ key: 'importance', label: 'Importance', sortable: true },
|
|
{ key: 'prio', label: 'Prio', sortable: true },
|
|
{ key: 'timeEstimation', label: 'Time Est. (min)' },
|
|
{ key: 'actualTime', label: 'Actual Time (min)' },
|
|
{ key: 'category', label: 'Category' },
|
|
{ key: 'coms', label: 'Coms' },
|
|
{ key: 'link', label: 'Link' },
|
|
{ key: 'comments', label: 'Comments' },
|
|
{ key: 'linkedTasks', label: 'Linked Tasks' },
|
|
];
|
|
list.innerHTML = `
|
|
<div class="table-responsive">
|
|
<table class="todos-table">
|
|
<thead>
|
|
<tr>
|
|
${columns.map(col =>
|
|
col.sortable ?
|
|
`<th class="sortable" data-col="${col.key}">${col.label} ${sortState.column === col.key ? (sortState.asc ? '▲' : '▼') : ''}</th>` :
|
|
`<th>${col.label}</th>`
|
|
).join('')}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${todos.map((todo, rowIdx) => `
|
|
<tr>
|
|
${columns.map(col => renderEditableCell(todo, col.key, rowIdx)).join('')}
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
// Add sorting event listeners
|
|
document.querySelectorAll('.todos-table th.sortable').forEach(th => {
|
|
th.addEventListener('click', () => sortTodos(th.dataset.col));
|
|
});
|
|
// Add modal editing listeners
|
|
document.querySelectorAll('.todos-table td[data-editable="true"]').forEach(td => {
|
|
td.addEventListener('click', function(e) {
|
|
const rowIdx = +td.dataset.row;
|
|
const key = td.dataset.key;
|
|
if (window.showEditTodoModal) {
|
|
window.showEditTodoModal(rowIdx, key);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderEditableCell(todo, key, rowIdx) {
|
|
// Non-editable fields: id, prio
|
|
const nonEditable = ['id', 'prio'];
|
|
// Editable fields
|
|
const editable = [
|
|
'task', 'creationDate', 'nextDate', 'dueDate', 'status', 'urgent', 'importance',
|
|
'timeEstimation', 'actualTime', 'category', 'coms', 'link', 'comments', 'linkedTasks'
|
|
];
|
|
let value = todo[key] || '';
|
|
if (["creationDate","nextDate","dueDate"].includes(key)) {
|
|
value = formatDisplayDate(todo[key]);
|
|
}
|
|
if (key === 'coms' && value) value = `<a href="${value}" target="_blank">Link</a>`;
|
|
if (key === 'link' && value) value = `<a href="${value}" target="_blank">Link</a>`;
|
|
if (key === 'status') return `<td class="status-${todo.status || 'blank'}" data-editable="true" data-row="${rowIdx}" data-key="${key}">${todo.status || ''}</td>`;
|
|
if (nonEditable.includes(key)) return `<td>${todo[key]}</td>`;
|
|
return `<td data-editable="true" data-row="${rowIdx}" data-key="${key}">${value}</td>`;
|
|
}
|
|
|
|
function handleCellEdit(e) {
|
|
const td = e.currentTarget;
|
|
const rowIdx = +td.dataset.row;
|
|
const key = td.dataset.key;
|
|
const todo = todos[rowIdx];
|
|
let input;
|
|
let oldValue = todo[key] || '';
|
|
|
|
// Helper to format date as YYYY-MM-DD
|
|
function formatDate(val) {
|
|
if (!val) return '';
|
|
if (val.length === 10 && val.match(/^\d{4}-\d{2}-\d{2}$/)) return val;
|
|
const d = new Date(val);
|
|
if (isNaN(d)) return '';
|
|
return d.toISOString().slice(0, 10);
|
|
}
|
|
|
|
if (["creationDate","nextDate","dueDate"].includes(key)) {
|
|
input = document.createElement('input');
|
|
input.type = 'date';
|
|
input.value = formatDate(oldValue);
|
|
let changed = false;
|
|
input.addEventListener('change', () => {
|
|
changed = true;
|
|
saveCellEdit(td, rowIdx, key, input.value);
|
|
});
|
|
input.addEventListener('blur', () => {
|
|
// If the user clicked the date picker, allow change to fire first
|
|
setTimeout(() => {
|
|
if (!changed) saveCellEdit(td, rowIdx, key, input.value);
|
|
}, 150);
|
|
});
|
|
} else if (["urgent","importance","timeEstimation","actualTime"].includes(key)) {
|
|
input = document.createElement('input');
|
|
input.type = 'number';
|
|
input.value = oldValue;
|
|
if (["urgent","importance"].includes(key)) {
|
|
input.min = 1; input.max = 7;
|
|
}
|
|
if (["timeEstimation","actualTime"].includes(key)) {
|
|
input.min = 0;
|
|
}
|
|
// Save on both change (for arrows) and blur (for manual input)
|
|
input.addEventListener('change', () => saveCellEdit(td, rowIdx, key, input.value));
|
|
input.addEventListener('blur', () => saveCellEdit(td, rowIdx, key, input.value));
|
|
input.addEventListener('keydown', (evt) => {
|
|
if (evt.key === 'Enter') {
|
|
evt.preventDefault();
|
|
input.blur();
|
|
}
|
|
});
|
|
} else if (key === 'status') {
|
|
input = document.createElement('select');
|
|
['','Busy','Done','W4A'].forEach(opt => {
|
|
const option = document.createElement('option');
|
|
option.value = opt;
|
|
option.text = opt || '(Blank)';
|
|
if (opt === oldValue) option.selected = true;
|
|
input.appendChild(option);
|
|
});
|
|
input.addEventListener('change', () => saveCellEdit(td, rowIdx, key, input.value));
|
|
} else if (key === 'category') {
|
|
input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.value = oldValue;
|
|
input.setAttribute('list','category-list');
|
|
input.addEventListener('keydown', (evt) => {
|
|
if (evt.key === 'Enter') {
|
|
evt.preventDefault();
|
|
input.blur();
|
|
}
|
|
});
|
|
} else if (key === 'comments' || key === 'linkedTasks') {
|
|
input = document.createElement('textarea');
|
|
input.value = oldValue;
|
|
input.addEventListener('keydown', (evt) => {
|
|
if (evt.key === 'Enter' && !evt.shiftKey) {
|
|
evt.preventDefault();
|
|
input.blur();
|
|
}
|
|
});
|
|
} else {
|
|
input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.value = oldValue;
|
|
input.addEventListener('keydown', (evt) => {
|
|
if (evt.key === 'Enter') {
|
|
evt.preventDefault();
|
|
input.blur();
|
|
}
|
|
});
|
|
}
|
|
input.className = 'table-edit-input';
|
|
td.innerHTML = '';
|
|
td.appendChild(input);
|
|
// For date input, ensure the picker opens
|
|
if (["creationDate","nextDate","dueDate"].includes(key)) {
|
|
setTimeout(() => {
|
|
input.focus();
|
|
if (typeof input.showPicker === 'function') {
|
|
input.showPicker();
|
|
}
|
|
}, 0);
|
|
// For date fields, do NOT save or close on blur at all—only on change
|
|
} else {
|
|
input.focus();
|
|
input.addEventListener('blur', () => saveCellEdit(td, rowIdx, key, input.value));
|
|
}
|
|
}
|
|
|
|
|
|
function saveCellEdit(td, rowIdx, key, value) {
|
|
// Type conversions
|
|
if (["urgent","importance","timeEstimation","actualTime"].includes(key)) {
|
|
value = parseInt(value) || 0;
|
|
}
|
|
if (["creationDate","nextDate","dueDate"].includes(key)) {
|
|
value = value || '';
|
|
}
|
|
if (key === 'category') {
|
|
if (value && !categories.includes(value)) {
|
|
categories.push(value);
|
|
}
|
|
}
|
|
todos[rowIdx][key] = value;
|
|
// Recalculate prio if urgency or importance changed
|
|
if (key === 'urgent' || key === 'importance') {
|
|
todos[rowIdx].prio = (parseInt(todos[rowIdx].urgent) || 1) * (parseInt(todos[rowIdx].importance) || 1);
|
|
}
|
|
saveTodos();
|
|
renderTodos();
|
|
}
|
|
|
|
|
|
function addTodo(e) {
|
|
e.preventDefault();
|
|
const task = document.getElementById('task').value.trim();
|
|
const creationDate = document.getElementById('creation-date').value || new Date().toISOString().slice(0,10);
|
|
const nextDate = document.getElementById('next-date').value;
|
|
const dueDate = document.getElementById('due-date').value;
|
|
const status = document.getElementById('status').value;
|
|
const urgent = parseInt(document.getElementById('urgent').value) || 1;
|
|
const importance = parseInt(document.getElementById('importance').value) || 1;
|
|
const prio = urgent * importance;
|
|
const timeEstimation = parseInt(document.getElementById('time-estimation').value) || 0;
|
|
const actualTime = parseInt(document.getElementById('actual-time').value) || 0;
|
|
let category = document.getElementById('category').value.trim();
|
|
if (category && !categories.includes(category)) {
|
|
categories.push(category);
|
|
}
|
|
const coms = document.getElementById('coms').value.trim();
|
|
const link = document.getElementById('link').value.trim();
|
|
const comments = document.getElementById('comments').value.trim();
|
|
const linkedTasks = document.getElementById('linked-tasks').value.trim();
|
|
const todo = {
|
|
id: getNextId(),
|
|
creationDate,
|
|
nextDate,
|
|
dueDate,
|
|
status,
|
|
urgent,
|
|
importance,
|
|
prio,
|
|
timeEstimation,
|
|
actualTime,
|
|
category,
|
|
task,
|
|
coms,
|
|
link,
|
|
comments,
|
|
linkedTasks
|
|
};
|
|
todos.push(todo);
|
|
saveTodos();
|
|
renderForm();
|
|
renderTodos();
|
|
document.getElementById('todo-form').reset();
|
|
}
|
|
|
|
function switchTab(tab) {
|
|
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
|
|
if (tab === 'list') {
|
|
document.getElementById('tab-list-btn').classList.add('active');
|
|
document.getElementById('tab-content-list').classList.add('active');
|
|
} else if (tab === 'form') {
|
|
document.getElementById('tab-form-btn').classList.add('active');
|
|
document.getElementById('tab-content-form').classList.add('active');
|
|
} else if (tab === 'admin') {
|
|
document.getElementById('tab-admin-btn').classList.add('active');
|
|
document.getElementById('tab-content-admin').classList.add('active');
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
loadTodos();
|
|
renderForm();
|
|
renderTodos();
|
|
document.getElementById('tab-list-btn').addEventListener('click', () => switchTab('list'));
|
|
document.getElementById('tab-form-btn').addEventListener('click', () => switchTab('form'));
|
|
const adminBtn = document.getElementById('tab-admin-btn');
|
|
if (adminBtn) adminBtn.addEventListener('click', () => switchTab('admin'));
|
|
|
|
// --- Password Modal Helper ---
|
|
function showPasswordModal({title, confirm=false}) {
|
|
return new Promise((resolve, reject) => {
|
|
const modal = document.getElementById('password-modal');
|
|
const titleEl = document.getElementById('password-modal-title');
|
|
const input = document.getElementById('password-modal-input');
|
|
const confirmInput = document.getElementById('password-modal-confirm');
|
|
const okBtn = document.getElementById('password-modal-ok');
|
|
const cancelBtn = document.getElementById('password-modal-cancel');
|
|
const errorEl = document.getElementById('password-modal-error');
|
|
titleEl.textContent = title;
|
|
input.value = '';
|
|
confirmInput.value = '';
|
|
errorEl.style.display = 'none';
|
|
confirmInput.style.display = confirm ? '' : 'none';
|
|
modal.style.display = 'flex';
|
|
input.focus();
|
|
function cleanup() {
|
|
modal.style.display = 'none';
|
|
okBtn.onclick = null;
|
|
cancelBtn.onclick = null;
|
|
input.onkeydown = null;
|
|
confirmInput.onkeydown = null;
|
|
}
|
|
okBtn.onclick = () => {
|
|
const pw = input.value;
|
|
if (confirm) {
|
|
const pw2 = confirmInput.value;
|
|
if (!pw || !pw2) {
|
|
errorEl.textContent = 'Please enter and confirm your password.';
|
|
errorEl.style.display = 'block';
|
|
return;
|
|
}
|
|
if (pw !== pw2) {
|
|
errorEl.textContent = 'Passwords do not match.';
|
|
errorEl.style.display = 'block';
|
|
return;
|
|
}
|
|
} else {
|
|
if (!pw) {
|
|
errorEl.textContent = 'Please enter your password.';
|
|
errorEl.style.display = 'block';
|
|
return;
|
|
}
|
|
}
|
|
cleanup();
|
|
resolve(pw);
|
|
};
|
|
cancelBtn.onclick = () => { cleanup(); reject('cancel'); };
|
|
input.onkeydown = e => { if (e.key === 'Enter') okBtn.onclick(); };
|
|
confirmInput.onkeydown = e => { if (e.key === 'Enter') okBtn.onclick(); };
|
|
});
|
|
}
|
|
|
|
// --- Encryption helpers ---
|
|
async function getKeyFromPassword(password, salt) {
|
|
const enc = new TextEncoder();
|
|
const keyMaterial = await window.crypto.subtle.importKey(
|
|
'raw', enc.encode(password), {name: 'PBKDF2'}, false, ['deriveKey']);
|
|
return window.crypto.subtle.deriveKey(
|
|
{name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256'},
|
|
keyMaterial,
|
|
{name: 'AES-GCM', length: 256},
|
|
false,
|
|
['encrypt', 'decrypt']
|
|
);
|
|
}
|
|
async function encryptData(data, password) {
|
|
const enc = new TextEncoder();
|
|
const salt = window.crypto.getRandomValues(new Uint8Array(16));
|
|
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
const key = await getKeyFromPassword(password, salt);
|
|
const ciphertext = await window.crypto.subtle.encrypt(
|
|
{name: 'AES-GCM', iv},
|
|
key,
|
|
enc.encode(data)
|
|
);
|
|
// Output as base64: salt + iv + ciphertext
|
|
function toB64(buf) { return btoa(String.fromCharCode(...new Uint8Array(buf))); }
|
|
return toB64(salt) + ':' + toB64(iv) + ':' + toB64(ciphertext);
|
|
}
|
|
async function decryptData(b64, password) {
|
|
function fromB64(b64) { return Uint8Array.from(atob(b64), c => c.charCodeAt(0)); }
|
|
const [saltB64, ivB64, ctB64] = b64.split(':');
|
|
const salt = fromB64(saltB64);
|
|
const iv = fromB64(ivB64);
|
|
const ciphertext = fromB64(ctB64);
|
|
const key = await getKeyFromPassword(password, salt);
|
|
const dec = new TextDecoder();
|
|
const plaintext = await window.crypto.subtle.decrypt(
|
|
{name: 'AES-GCM', iv},
|
|
key,
|
|
ciphertext
|
|
);
|
|
return dec.decode(plaintext);
|
|
}
|
|
|
|
// Export JSON (password-protected)
|
|
const exportBtn = document.getElementById('export-json-btn');
|
|
if (exportBtn) exportBtn.addEventListener('click', async () => {
|
|
let password;
|
|
try {
|
|
password = await showPasswordModal({title: 'Set Export Password', confirm: true});
|
|
} catch { return; }
|
|
if (!password) return;
|
|
const data = {
|
|
todos,
|
|
categories
|
|
};
|
|
const json = JSON.stringify(data, null, 2);
|
|
try {
|
|
const encrypted = await encryptData(json, password);
|
|
const blob = new Blob([encrypted], {type: 'application/json'});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'todos-export.json';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
setTimeout(() => {
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}, 100);
|
|
// Optionally show in <pre>
|
|
const pre = document.getElementById('export-json-output');
|
|
if (pre) {
|
|
pre.style.display = 'block';
|
|
pre.textContent = encrypted;
|
|
}
|
|
} catch (err) {
|
|
alert('Encryption failed: ' + err.message);
|
|
}
|
|
});
|
|
|
|
// Import JSON (password-protected)
|
|
const importBtn = document.getElementById('import-json-btn');
|
|
const importInput = document.getElementById('import-json-input');
|
|
if (importBtn && importInput) {
|
|
importBtn.addEventListener('click', () => importInput.click());
|
|
importInput.addEventListener('change', async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
let password;
|
|
try {
|
|
password = await showPasswordModal({title: 'Enter Password to Import', confirm: false});
|
|
} catch { return; }
|
|
if (!password) return;
|
|
const reader = new FileReader();
|
|
reader.onload = async function(evt) {
|
|
try {
|
|
const encrypted = evt.target.result;
|
|
let json;
|
|
try {
|
|
json = await decryptData(encrypted, password);
|
|
} catch (err) {
|
|
throw new Error('Incorrect password or corrupt file.');
|
|
}
|
|
const data = JSON.parse(json);
|
|
if (!data.todos || !Array.isArray(data.todos)) throw new Error('Missing or invalid todos array.');
|
|
if (!data.categories || !Array.isArray(data.categories)) throw new Error('Missing or invalid categories array.');
|
|
todos = data.todos;
|
|
categories = data.categories;
|
|
saveTodos();
|
|
renderTodos();
|
|
renderForm();
|
|
alert('Import successful!');
|
|
switchTab('list');
|
|
} catch (err) {
|
|
alert('Import failed: ' + err.message);
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
// Reset input so same file can be imported again
|
|
importInput.value = '';
|
|
});
|
|
}
|
|
|
|
|
|
// Delete all data
|
|
const delBtn = document.getElementById('delete-all-btn');
|
|
if (delBtn) delBtn.addEventListener('click', () => {
|
|
if (!confirm('Are you sure you want to delete ALL todos and categories? This cannot be undone.')) return;
|
|
todos = [];
|
|
categories = [];
|
|
saveTodos();
|
|
renderTodos();
|
|
renderForm();
|
|
alert('All data deleted.');
|
|
switchTab('list');
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
// If you want to enable password, set a flag in localStorage and show/hide #password-section accordingly.
|