diff --git a/.github/workflows/update-cache-version.yml b/.github/workflows/update-cache-version.yml
index 0d8995b..3cebab9 100644
--- a/.github/workflows/update-cache-version.yml
+++ b/.github/workflows/update-cache-version.yml
@@ -7,7 +7,7 @@ on:
paths:
- 'index.html'
- 'css/**'
- - 'js/**'
+ - 'src/**'
- 'manifest.json'
jobs:
diff --git a/.gitignore b/.gitignore
index 95c7104..16eca4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,8 @@
# Dependencies
node_modules/
-# TypeScript build output
-js/*.js
-js/*.js.map
-js/*.d.ts
+# TypeScript build output (compiled at deploy time, not committed)
+js/
# Playwright
test-results/
@@ -12,3 +10,4 @@ playwright-report/
# Misc
.DS_Store
+js/
diff --git a/e2e/app.spec.ts b/e2e/app.spec.ts
index eae98c6..9cfde54 100644
--- a/e2e/app.spec.ts
+++ b/e2e/app.spec.ts
@@ -451,4 +451,68 @@ test.describe('Task Manager App', () => {
expect(value).toBe('50');
});
});
+
+ // ========================
+ // Filter Settings Persistence
+ // ========================
+ test.describe('filter settings persistence', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.click('[data-tab="tasks"]');
+ });
+
+ test('should show the Reset Filters button', async ({ page }) => {
+ await expect(page.locator('#resetFiltersBtn')).toBeVisible();
+ });
+
+ test('should persist status filter across page reloads', async ({ page }) => {
+ await page.selectOption('#statusFilter', 'completed');
+ await page.reload();
+ await page.waitForSelector('.header');
+ await page.click('[data-tab="tasks"]');
+ const value = await page.locator('#statusFilter').inputValue();
+ expect(value).toBe('completed');
+ });
+
+ test('should persist groupBy filter across page reloads', async ({ page }) => {
+ await page.selectOption('#groupBySelect', 'priority');
+ await page.reload();
+ await page.waitForSelector('.header');
+ await page.click('[data-tab="tasks"]');
+ const value = await page.locator('#groupBySelect').inputValue();
+ expect(value).toBe('priority');
+ });
+
+ test('should persist hideCompleted state across page reloads', async ({ page }) => {
+ await page.click('#hideCompletedBtn');
+ await expect(page.locator('#hideCompletedBtn')).toContainText('Show Completed');
+ await page.reload();
+ await page.waitForSelector('.header');
+ await page.click('[data-tab="tasks"]');
+ await expect(page.locator('#hideCompletedBtn')).toContainText('Show Completed');
+ });
+
+ test('should reset all filters when Reset Filters is clicked', async ({ page }) => {
+ await page.selectOption('#statusFilter', 'pending');
+ await page.selectOption('#groupBySelect', 'category');
+ await page.click('#hideCompletedBtn');
+
+ await page.click('#resetFiltersBtn');
+
+ const statusValue = await page.locator('#statusFilter').inputValue();
+ const groupByValue = await page.locator('#groupBySelect').inputValue();
+ expect(statusValue).toBe('');
+ expect(groupByValue).toBe('');
+ await expect(page.locator('#hideCompletedBtn')).toContainText('Hide Completed');
+ });
+
+ test('should not restore filters after reset and reload', async ({ page }) => {
+ await page.selectOption('#statusFilter', 'pending');
+ await page.click('#resetFiltersBtn');
+ await page.reload();
+ await page.waitForSelector('.header');
+ await page.click('[data-tab="tasks"]');
+ const value = await page.locator('#statusFilter').inputValue();
+ expect(value).toBe('');
+ });
+ });
});
diff --git a/index.html b/index.html
index 181a891..5d6c76e 100644
--- a/index.html
+++ b/index.html
@@ -149,6 +149,7 @@
diff --git a/js/app.d.ts.map b/js/app.d.ts.map
deleted file mode 100644
index da2d55c..0000000
--- a/js/app.d.ts.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/js/app.js b/js/app.js
deleted file mode 100644
index 5fc3ec4..0000000
--- a/js/app.js
+++ /dev/null
@@ -1,2000 +0,0 @@
-// ========================
-// Main Application Logic
-// ========================
-import { storage, STORAGE_VERSION, getDaysUntilDueText } from './storage.js';
-class TaskManager {
- constructor() {
- this.currentEditingTaskId = null;
- this.currentEditingProjectId = null;
- this.currentEditingHabitId = null;
- this.currentEditingFinanceId = null;
- this.currentEditingFinanceType = null;
- this.currentEditingRewardId = null;
- this.currentEditingWishItemId = null;
- this.currentEditingNoteId = null;
- this.dragSrcWishId = null;
- this.selectedDate = new Date();
- this.tasksExpanded = false;
- this.hideCompleted = false;
- this.emojis = [
- // Activity
- '๐ช', '๐', '๐ด', '๐', '๐ง', '๐', '๐บ', 'โน๏ธ',
- // Food & Health
- '๐ฅ', '๐', '๐ฅ', '๐', '๐ฅ', '๐ง', '๐ฅค', 'โ',
- // Work & Productivity
- '๐', 'โ๏ธ', '๐ผ', '๐ฏ', '๐', '๐ป', '๐ฑ', 'โจ๏ธ',
- // Learning & Mind
- '๐ง ', '๐', '๐', '๐ก', '๐ฌ', '๐จ', '๐ต', '๐ญ',
- // Nature & Outdoors
- '๐ฟ', '๐ณ', '๐', '๐', '๐', 'โฐ๏ธ', '๐๏ธ', '๐ฆ',
- // Sleep & Rest
- '๐ด', '๐๏ธ', '๐', '๐ฏ๏ธ', '๐', '๐ค', '๐ง', '๐',
- // Social & Fun
- '๐จโ๐ฉโ๐งโ๐ฆ', '๐ค', '๐', '๐', 'โค๏ธ', '๐ค', '๐', '๐',
- // Sports & Games
- 'โฝ', '๐', '๐พ', '๐', '๐ฏ', 'โ๏ธ', '๐ฒ', '๐',
- // Habits & Goals
- 'โญ', '๐ฏ', '๐', '๐ฅ', '๐ฅ', '๐', 'โจ', '๐',
- // More Emojis
- '๐', '๐', '๐', '๐
', 'โฐ', '๐ฐ', '๐ช', '๐ข'
- ];
- this.init();
- }
- init() {
- this.setupEventListeners();
- this.initializeFinanceDateFilter();
- this.updateDateNavigator();
- this.render();
- this.processRecurringTasks();
- }
- // ========================
- // Event Listeners Setup
- // ========================
- setupEventListeners() {
- // Navigation tabs
- document.querySelectorAll('.nav-tab').forEach(tab => {
- tab.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab));
- });
- // Tasks section
- document.getElementById('addTaskBtn').addEventListener('click', () => this.openTaskModal());
- document.getElementById('toggleTaskViewBtn').addEventListener('click', () => this.toggleTaskView());
- document.getElementById('taskForm').addEventListener('submit', (e) => this.saveTask(e));
- document.getElementById('cancelTaskBtn').addEventListener('click', () => this.closeTaskModal());
- document.getElementById('deleteTaskBtn').addEventListener('click', () => this.deleteTask());
- document.getElementById('taskRepeatType').addEventListener('change', (e) => this.updateRepeatTypeUI(e.target.value));
- document.getElementById('taskCategory').addEventListener('change', (e) => this.handleCategoryChange('task', e.target.value));
- document.getElementById('taskCategorySave').addEventListener('click', () => this.handleAddCategory('task'));
- document.getElementById('taskCategoryCancel').addEventListener('click', () => this.cancelAddCategory('task'));
- document.getElementById('categoryFilter').addEventListener('change', () => this.filterTasks());
- document.getElementById('statusFilter').addEventListener('change', () => this.filterTasks());
- document.getElementById('groupBySelect').addEventListener('change', () => this.filterTasks());
- document.getElementById('searchTasks').addEventListener('input', () => this.filterTasks());
- document.getElementById('hideCompletedBtn').addEventListener('click', () => this.toggleHideCompleted());
- // Projects section
- document.getElementById('addProjectBtn').addEventListener('click', () => this.openProjectModal());
- document.getElementById('projectForm').addEventListener('submit', (e) => this.saveProject(e));
- document.getElementById('cancelProjectBtn').addEventListener('click', () => this.closeProjectModal());
- document.getElementById('deleteProjectBtn').addEventListener('click', () => this.deleteProject());
- document.getElementById('closeProjectDetailBtn').addEventListener('click', () => this.closeProjectDetailModal());
- document.getElementById('editProjectFromDetailBtn').addEventListener('click', () => this.editProjectFromDetail());
- // Habits section
- document.getElementById('addHabitBtn').addEventListener('click', () => this.openHabitModal());
- document.getElementById('habitForm').addEventListener('submit', (e) => this.saveHabit(e));
- document.getElementById('cancelHabitBtn').addEventListener('click', () => this.closeHabitModal());
- document.getElementById('deleteHabitBtn').addEventListener('click', () => this.deleteHabit());
- document.getElementById('habitEmojiBtn').addEventListener('click', () => this.openEmojiPicker());
- document.getElementById('closeEmojiBtn').addEventListener('click', () => this.closeEmojiPicker());
- document.getElementById('habitCategory').addEventListener('change', (e) => this.handleCategoryChange('habit', e.target.value));
- document.getElementById('habitCategorySave').addEventListener('click', () => this.handleAddCategory('habit'));
- document.getElementById('habitCategoryCancel').addEventListener('click', () => this.cancelAddCategory('habit'));
- // Finances section
- document.getElementById('addExpenseBtn').addEventListener('click', () => this.openFinanceModal('expense'));
- document.getElementById('addRevenueBtn').addEventListener('click', () => this.openFinanceModal('revenue'));
- document.getElementById('addChargeBtn').addEventListener('click', () => this.openFinanceModal('charge'));
- document.getElementById('financeForm').addEventListener('submit', (e) => this.saveFinance(e));
- document.getElementById('cancelFinanceBtn').addEventListener('click', () => this.closeFinanceModal());
- document.getElementById('deleteFinanceBtn').addEventListener('click', () => this.deleteFinance());
- document.getElementById('financeCategory').addEventListener('change', (e) => this.handleCategoryChange('finance', e.target.value));
- document.getElementById('financeCategorySave').addEventListener('click', () => this.handleAddCategory('finance'));
- document.getElementById('financeCategoryCancel').addEventListener('click', () => this.cancelAddCategory('finance'));
- document.querySelectorAll('.finance-tab').forEach(tab => {
- tab.addEventListener('click', (e) => this.switchFinanceTab(e.target.dataset.financeTab));
- });
- // Finance filters
- document.getElementById('filterFinancesBtn').addEventListener('click', () => this.renderFinances());
- document.getElementById('resetFinanceFilterBtn').addEventListener('click', () => this.resetFinanceFilter());
- // Shop section
- document.getElementById('addRewardBtn').addEventListener('click', () => this.openRewardModal());
- document.getElementById('rewardForm').addEventListener('submit', (e) => this.saveReward(e));
- document.getElementById('cancelRewardBtn').addEventListener('click', () => this.closeRewardModal());
- document.getElementById('deleteRewardBtn').addEventListener('click', () => this.deleteReward());
- // Wish List section
- document.getElementById('addWishItemBtn').addEventListener('click', () => this.openWishItemModal());
- document.getElementById('wishItemForm').addEventListener('submit', (e) => this.saveWishItem(e));
- document.getElementById('cancelWishItemBtn').addEventListener('click', () => this.closeWishItemModal());
- document.getElementById('deleteWishItemBtn').addEventListener('click', () => this.deleteWishItem());
- // Notes section
- document.getElementById('addNoteBtn').addEventListener('click', () => this.openNoteModal());
- document.getElementById('noteForm').addEventListener('submit', (e) => this.saveNote(e));
- document.getElementById('cancelNoteBtn').addEventListener('click', () => this.closeNoteModal());
- document.getElementById('deleteNoteBtn').addEventListener('click', () => this.deleteNote());
- // Modal close buttons
- document.querySelectorAll('.close-btn').forEach(btn => {
- btn.addEventListener('click', (e) => {
- e.target.closest('.modal').classList.remove('active');
- });
- });
- // Settings
- document.getElementById('saveTasksPerLevel').addEventListener('click', () => this.saveTasksPerLevel());
- document.getElementById('exportBtn').addEventListener('click', () => this.exportData());
- document.getElementById('importBtn').addEventListener('click', () => document.getElementById('importFile').click());
- document.getElementById('importFile').addEventListener('change', (e) => this.importData(e));
- document.getElementById('clearDataBtn').addEventListener('click', () => {
- if (storage.clearAllData()) {
- location.reload();
- }
- });
- // Modal backdrop click
- document.querySelectorAll('.modal').forEach(modal => {
- modal.addEventListener('click', (e) => {
- if (e.target === modal) {
- modal.classList.remove('active');
- }
- });
- });
- // Hamburger menu toggle
- document.getElementById('hamburgerMenu').addEventListener('click', () => {
- const navTabs = document.getElementById('navTabs');
- const hamburger = document.getElementById('hamburgerMenu');
- navTabs.classList.toggle('show');
- hamburger.classList.toggle('active');
- });
- // Close mobile menu when a tab is clicked
- document.querySelectorAll('.nav-tab').forEach(tab => {
- tab.addEventListener('click', () => {
- if (window.innerWidth <= 768) {
- const navTabs = document.getElementById('navTabs');
- const hamburger = document.getElementById('hamburgerMenu');
- navTabs.classList.remove('show');
- hamburger.classList.remove('active');
- }
- });
- });
- // Close mobile menu when clicking outside
- document.addEventListener('click', (e) => {
- if (window.innerWidth <= 768) {
- const navTabs = document.getElementById('navTabs');
- const hamburger = document.getElementById('hamburgerMenu');
- if (!navTabs.contains(e.target) && !hamburger.contains(e.target)) {
- navTabs.classList.remove('show');
- hamburger.classList.remove('active');
- }
- }
- });
- // Dashboard overview click navigation
- document.getElementById('todayTasksItem').addEventListener('click', () => this.switchTab('tasks'));
- document.getElementById('overdueTasksItem').addEventListener('click', () => {
- document.getElementById('statusFilter').value = 'overdue';
- this.switchTab('tasks');
- });
- // Date navigator
- document.getElementById('prevDayBtn').addEventListener('click', () => this.navigateDate(-1));
- document.getElementById('nextDayBtn').addEventListener('click', () => this.navigateDate(1));
- document.getElementById('goTodayBtn').addEventListener('click', () => {
- this.selectedDate = new Date();
- this.updateDateNavigator();
- const activeTabName = document.querySelector('.nav-tab.active')?.dataset.tab;
- if (activeTabName) {
- this.switchTab(activeTabName);
- }
- else {
- this.renderDashboard();
- }
- });
- }
- // ========================
- // Tab Navigation
- // ========================
- switchTab(tabName) {
- // Hide all tabs
- document.querySelectorAll('.tab-content').forEach(tab => {
- tab.classList.remove('active');
- });
- // Remove active from all nav tabs
- document.querySelectorAll('.nav-tab').forEach(tab => {
- tab.classList.remove('active');
- });
- // Show selected tab
- document.getElementById(`${tabName}-tab`).classList.add('active');
- document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
- // Re-render based on tab
- if (tabName === 'dashboard') {
- this.renderDashboard();
- }
- else if (tabName === 'tasks') {
- this.renderTasks();
- }
- else if (tabName === 'projects') {
- this.renderProjects();
- }
- else if (tabName === 'habits') {
- this.renderHabits();
- }
- else if (tabName === 'finances') {
- this.renderFinances();
- }
- else if (tabName === 'shop') {
- this.renderShop();
- }
- else if (tabName === 'wishlist') {
- this.renderWishList();
- }
- else if (tabName === 'notes') {
- this.renderNotes();
- }
- else if (tabName === 'settings') {
- this.renderSettings();
- }
- }
- switchFinanceTab(tabName) {
- document.querySelectorAll('.finance-content').forEach(tab => {
- tab.classList.remove('active');
- });
- document.querySelectorAll('.finance-tab').forEach(tab => {
- tab.classList.remove('active');
- });
- document.getElementById(`${tabName}-content`).classList.add('active');
- document.querySelector(`[data-finance-tab="${tabName}"]`).classList.add('active');
- }
- // ========================
- // Dashboard Rendering
- // ========================
- renderDashboard() {
- const today = this.getSelectedDateStr();
- const tasks = storage.getTasks();
- const habits = storage.getHabits();
- const userStats = storage.getUserStats();
- // Update header stats
- document.getElementById('totalPoints').textContent = String(userStats.totalPoints);
- document.getElementById('userLevel').textContent = String(userStats.level);
- document.getElementById('dailyStreak').textContent = String(userStats.dailyStreak);
- // Update level progress
- const settings = storage.getSettings();
- const completedTasksCount = tasks.filter(t => t.completed).length;
- const tasksInCurrentLevel = completedTasksCount % settings.tasksPerLevel;
- const tasksNeeded = settings.tasksPerLevel;
- document.getElementById('levelProgress').textContent = `${tasksInCurrentLevel}/${tasksNeeded} tasks`;
- // Selected day's overview
- const todayTasks = tasks.filter(t => t.dueDate === today && !t.completed);
- const overdueTasks = tasks.filter(t => !t.completed && !!t.dueDate && t.dueDate < today);
- const completedToday = tasks.filter(t => t.completedDate === today);
- const todayDay = this.selectedDate.getDay();
- // Find incomplete habits for selected day
- const incompleteHabits = habits.filter(habit => {
- const isValidDay = !habit.daysOfWeek || habit.daysOfWeek.includes(todayDay);
- if (!isValidDay)
- return false;
- const todaysCompletions = storage.countHabitCompletionsForDate(habit.id, today);
- const targetGoal = habit.targetGoal || 1;
- return todaysCompletions < targetGoal;
- });
- document.getElementById('todayTasksCount').textContent = String(todayTasks.length);
- document.getElementById('overdueTasksCount').textContent = String(overdueTasks.length);
- document.getElementById('completedTodayCount').textContent = String(completedToday.length);
- document.getElementById('incompleteHabitsCount').textContent = String(incompleteHabits.length);
- // Render incomplete habits list
- const incompleteHabitsList = document.getElementById('incompleteHabitsList');
- if (incompleteHabits.length === 0) {
- incompleteHabitsList.innerHTML = '
All habits completed for today! ๐
';
- }
- else {
- incompleteHabitsList.innerHTML = incompleteHabits.map(habit => {
- const todaysCompletions = storage.countHabitCompletionsForDate(habit.id, today);
- const targetGoal = habit.targetGoal || 1;
- const percentage = Math.min(100, Math.round((todaysCompletions / targetGoal) * 100));
- return `
-
-
-
- ${habit.icon}
- ${habit.name}
-
-
${todaysCompletions}/${targetGoal} (${percentage}%)
-
-
- `;
- }).join('');
- // Add click handlers to navigate to habits
- document.querySelectorAll('.habit-item').forEach(item => {
- item.addEventListener('click', () => {
- this.switchTab('habits');
- });
- });
- }
- // Recent activity
- this.renderRecentActivity();
- // Active projects
- this.renderProjectsSummary();
- }
- renderRecentActivity() {
- const data = storage.getData();
- const activities = [];
- // Collect activities from tasks
- data.tasks.filter(t => t.completedDate).forEach(t => {
- activities.push({
- type: 'task',
- message: `Completed task: ${t.title}`,
- date: t.completedDate,
- icon: 'โ'
- });
- });
- // Collect activities from habits
- if (data.dailyHabitLogs) {
- data.dailyHabitLogs.slice(-10).forEach(log => {
- const habit = data.habits.find(h => h.id === log.habitId);
- if (habit) {
- activities.push({
- type: 'habit',
- message: `Completed habit: ${habit.name}`,
- date: log.date,
- icon: 'โญ'
- });
- }
- });
- }
- // Sort by date
- activities.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
- const activityList = document.getElementById('recentActivity');
- if (activities.length === 0) {
- activityList.innerHTML = '
No activity yet. Start completing tasks!
';
- }
- else {
- activityList.innerHTML = activities.slice(0, 5).map(activity => `
-
-
${activity.icon} ${activity.message}
-
${activity.date}
-
- `).join('');
- }
- }
- renderProjectsSummary() {
- const projects = storage.getProjects();
- const tasks = storage.getTasks();
- const container = document.getElementById('projectsSummary');
- if (projects.length === 0) {
- container.innerHTML = '
No active projects. Create one to organize your tasks!
';
- return;
- }
- container.innerHTML = projects.map(project => {
- const projectTasks = tasks.filter(t => t.projectId === project.id);
- const completedTasks = projectTasks.filter(t => t.completed);
- const percentage = projectTasks.length === 0 ? 0 : Math.round((completedTasks.length / projectTasks.length) * 100);
- return `
-
-
${project.name}
-
-
${completedTasks.length}/${projectTasks.length} tasks (${percentage}%)
-
- `;
- }).join('');
- }
- // ========================
- // Tasks Management
- toggleHideCompleted() {
- this.hideCompleted = !this.hideCompleted;
- const btn = document.getElementById('hideCompletedBtn');
- btn.textContent = this.hideCompleted ? '๐ Show Completed' : '๐ Hide Completed';
- btn.classList.toggle('active', this.hideCompleted);
- this.filterTasks();
- }
- // ========================
- toggleTaskView() {
- this.tasksExpanded = !this.tasksExpanded;
- const btn = document.getElementById('toggleTaskViewBtn');
- btn.textContent = this.tasksExpanded ? 'โ Collapse Details' : 'โ Expand Details';
- document.getElementById('taskList').classList.toggle('expanded', this.tasksExpanded);
- }
- renderTasks() {
- this.updateCategoryFilter();
- this.filterTasks();
- }
- filterTasks() {
- const tasks = storage.getTasks();
- const categoryFilter = document.getElementById('categoryFilter').value;
- const statusFilter = document.getElementById('statusFilter').value;
- const searchTerm = document.getElementById('searchTasks').value.toLowerCase();
- const groupBy = document.getElementById('groupBySelect').value;
- const today = this.getSelectedDateStr();
- const filtersActive = statusFilter || searchTerm;
- let filtered = tasks.filter(task => {
- // Hide completed filter
- if (this.hideCompleted && task.completed)
- return false;
- // Category filter
- if (categoryFilter && task.category !== categoryFilter)
- return false;
- // Status filter
- if (statusFilter) {
- if (statusFilter === 'completed' && !task.completed)
- return false;
- if (statusFilter === 'pending' && task.completed)
- return false;
- if (statusFilter === 'overdue' && (!task.dueDate || task.completed || task.dueDate >= today))
- return false;
- }
- // Search filter
- if (searchTerm && !task.title.toLowerCase().includes(searchTerm) && !task.description?.toLowerCase().includes(searchTerm))
- return false;
- return true;
- });
- const taskList = document.getElementById('taskList');
- if (filtered.length === 0) {
- taskList.innerHTML = '
No tasks found.
';
- return;
- }
- let html = '';
- if (groupBy === 'priority') {
- const priorityLabels = { high: 'High Priority', medium: 'Medium Priority', low: 'Low Priority' };
- const grouped = { high: [], medium: [], low: [], ungrouped: [] };
- filtered.forEach(task => {
- if (task.priority && grouped[task.priority]) {
- grouped[task.priority].push(task);
- }
- else {
- grouped['ungrouped'].push(task);
- }
- });
- ['high', 'medium', 'low'].forEach(p => {
- if (grouped[p].length > 0) {
- html += ``;
- html += grouped[p].map(task => this.renderTaskItem(task)).join('');
- }
- });
- if (grouped['ungrouped'].length > 0) {
- html += ``;
- html += grouped['ungrouped'].map(task => this.renderTaskItem(task)).join('');
- }
- }
- else if (groupBy === 'category') {
- const withCategory = filtered.filter(task => task.category);
- const withoutCategory = filtered.filter(task => !task.category);
- const categories = [...new Set(withCategory.map(task => task.category))].sort();
- categories.forEach(cat => {
- const group = withCategory.filter(task => task.category === cat);
- if (group.length > 0) {
- html += ``;
- html += group.map(task => this.renderTaskItem(task)).join('');
- }
- });
- if (withoutCategory.length > 0) {
- html += ``;
- html += withoutCategory.map(task => this.renderTaskItem(task)).join('');
- }
- }
- else if (filtersActive) {
- html = filtered.map(task => this.renderTaskItem(task)).join('');
- }
- else {
- const priorityOrder = { high: 0, medium: 1, low: 2 };
- const overdue = filtered.filter(task => !task.completed && task.dueDate && task.dueDate < today);
- const dueToday = filtered.filter(task => task.dueDate && task.dueDate === today);
- const upcoming = filtered
- .filter(task => !task.completed &&
- task.dueDate &&
- task.dueDate > today &&
- task.repeatType !== 'daily')
- .sort((a, b) => {
- if (a.dueDate < b.dueDate)
- return -1;
- if (a.dueDate > b.dueDate)
- return 1;
- return (priorityOrder[a.priority] ?? 1) - (priorityOrder[b.priority] ?? 1);
- });
- const noDueDate = filtered.filter(task => !task.dueDate);
- if (overdue.length > 0) {
- html += ``;
- html += overdue.map(task => this.renderTaskItem(task)).join('');
- }
- if (dueToday.length > 0) {
- html += ``;
- html += dueToday.map(task => this.renderTaskItem(task)).join('');
- }
- if (upcoming.length > 0) {
- html += ``;
- html += upcoming.map(task => this.renderTaskItem(task)).join('');
- }
- if (noDueDate.length > 0) {
- html += ``;
- html += noDueDate.map(task => this.renderTaskItem(task)).join('');
- }
- if (!html) {
- html = '
No tasks found.
';
- }
- }
- taskList.innerHTML = html;
- // Add event listeners to task items
- document.querySelectorAll('.task-checkbox').forEach(checkbox => {
- checkbox.addEventListener('change', (e) => {
- const taskId = e.target.dataset.taskId;
- this.toggleTask(taskId);
- });
- });
- document.querySelectorAll('.task-item').forEach(item => {
- item.addEventListener('click', (e) => {
- if (!e.target.classList.contains('task-checkbox')) {
- this.openTaskModal(item.dataset.taskId);
- }
- });
- });
- }
- renderTaskItem(task) {
- const today = storage.formatDate(new Date());
- let status = 'pending';
- if (task.completed) {
- status = 'completed';
- }
- else if (task.dueDate && task.dueDate < today) {
- status = 'overdue';
- }
- const dueBadge = task.dueDate && !task.completed
- ? `
${getDaysUntilDueText(task.dueDate)}`
- : '';
- return `
-
-
-
-
${task.title}
- ${task.description ? `
${task.description}
` : ''}
-
- ${task.category ? `๐ ${task.category}` : ''}
- ${task.dueDate ? `๐
${task.dueDate}` : ''}
- ${task.priority}
- ${task.repeatType !== 'none' ? `๐ ${task.repeatType}` : ''}
- ${task.points ? `โญ ${task.points} pts` : ''}
-
-
-
${status}
- ${dueBadge}
-
- `;
- }
- updateCategoryFilter() {
- const tasks = storage.getTasks();
- const categories = [...new Set(tasks.map(t => t.category).filter((c) => !!c))];
- const select = document.getElementById('categoryFilter');
- const currentValue = select.value;
- select.innerHTML = '
' +
- categories.map(cat => `
`).join('');
- select.value = currentValue;
- }
- openTaskModal(taskId = null) {
- this.currentEditingTaskId = taskId;
- const modal = document.getElementById('taskModal');
- const form = document.getElementById('taskForm');
- const deleteBtn = document.getElementById('deleteTaskBtn');
- form.reset();
- deleteBtn.style.display = 'none';
- // Update project dropdown
- this.updateProjectSelect();
- // Clear all task day checkboxes
- document.querySelectorAll('input[name="taskDay"]').forEach(checkbox => {
- checkbox.checked = false;
- });
- if (taskId) {
- const task = storage.getTasks().find(t => t.id === taskId);
- if (task) {
- document.getElementById('taskTitle').value = task.title;
- document.getElementById('taskDescription').value = task.description || '';
- document.getElementById('taskDueDate').value = task.dueDate || '';
- document.getElementById('taskCategory').value = task.category || '';
- document.getElementById('taskPriority').value = task.priority || 'medium';
- document.getElementById('taskPoints').value = String(task.points || 10);
- document.getElementById('taskRepeatType').value = task.repeatType || 'none';
- document.getElementById('taskProject').value = task.projectId || '';
- document.getElementById('taskRepeatUnit').value = String(task.repeatUnit || 1);
- if (task.repeatType === 'custom') {
- document.getElementById('customRepeatDays').value = String(task.customRepeatDays || '');
- }
- if (task.repeatType === 'movable') {
- document.getElementById('movableRepeatDays').value = String(task.movableRepeatDays || '');
- }
- // Load daysOfWeek if available
- if (task.daysOfWeek && Array.isArray(task.daysOfWeek)) {
- task.daysOfWeek.forEach(day => {
- const checkbox = document.querySelector(`input[name="taskDay"][value="${day}"]`);
- if (checkbox) {
- checkbox.checked = true;
- }
- });
- }
- deleteBtn.style.display = 'block';
- this.updateRepeatTypeUI(task.repeatType);
- }
- }
- else {
- document.getElementById('taskRepeatUnit').value = '1';
- }
- // Load categories
- this.loadCategoryDropdown('task');
- modal.classList.add('active');
- }
- closeTaskModal() {
- document.getElementById('taskModal').classList.remove('active');
- this.currentEditingTaskId = null;
- // If project detail modal is open, refresh it
- if (document.getElementById('projectDetailModal').classList.contains('active')) {
- this.openProjectDetailModal(this.currentEditingProjectId);
- }
- }
- updateRepeatTypeUI(repeatType) {
- const customGroup = document.getElementById('customRepeatGroup');
- const movableGroup = document.getElementById('movableRepeatGroup');
- const repeatUnitGroup = document.getElementById('repeatUnitGroup');
- const taskDaysGroup = document.getElementById('taskDaysGroup');
- const repeatUnitLabel = document.getElementById('repeatUnitLabel');
- customGroup.style.display = repeatType === 'custom' ? 'block' : 'none';
- movableGroup.style.display = repeatType === 'movable' ? 'block' : 'none';
- // Show repeat unit for daily, weekly, monthly, yearly
- if (['daily', 'weekly', 'monthly', 'yearly'].includes(repeatType)) {
- repeatUnitGroup.style.display = 'block';
- const labels = {
- daily: 'day(s)',
- weekly: 'week(s)',
- monthly: 'month(s)',
- yearly: 'year(s)'
- };
- repeatUnitLabel.textContent = labels[repeatType] || 'unit(s)';
- }
- else {
- repeatUnitGroup.style.display = 'none';
- }
- // Show days selector for weekly and daily
- taskDaysGroup.style.display = (['weekly', 'daily'].includes(repeatType)) ? 'block' : 'none';
- }
- updateProjectSelect() {
- const projects = storage.getProjects();
- const select = document.getElementById('taskProject');
- select.innerHTML = '
' +
- projects.map(p => `
`).join('');
- }
- saveTask(e) {
- e.preventDefault();
- // Get selected days for weekly/daily tasks
- const selectedDays = Array.from(document.querySelectorAll('input[name="taskDay"]:checked'))
- .map(checkbox => parseInt(checkbox.value));
- const task = {
- title: document.getElementById('taskTitle').value,
- description: document.getElementById('taskDescription').value,
- dueDate: document.getElementById('taskDueDate').value,
- category: document.getElementById('taskCategory').value,
- priority: document.getElementById('taskPriority').value,
- points: parseInt(document.getElementById('taskPoints').value),
- repeatType: document.getElementById('taskRepeatType').value,
- projectId: document.getElementById('taskProject').value || null
- };
- // Add repeatUnit for daily, weekly, monthly, yearly tasks
- if (['daily', 'weekly', 'monthly', 'yearly'].includes(task.repeatType)) {
- task.repeatUnit = parseInt(document.getElementById('taskRepeatUnit').value) || 1;
- if (selectedDays.length > 0) {
- task.daysOfWeek = selectedDays;
- }
- }
- if (task.repeatType === 'custom') {
- task.customRepeatDays = parseInt(document.getElementById('customRepeatDays').value);
- }
- if (task.repeatType === 'movable') {
- task.movableRepeatDays = parseInt(document.getElementById('movableRepeatDays').value);
- }
- if (this.currentEditingTaskId) {
- storage.updateTask(this.currentEditingTaskId, task);
- }
- else {
- storage.addTask(task);
- }
- this.closeTaskModal();
- this.renderTasks();
- this.renderProjects();
- }
- deleteTask() {
- if (this.currentEditingTaskId) {
- if (confirm('Are you sure you want to delete this task?')) {
- storage.deleteTask(this.currentEditingTaskId);
- this.closeTaskModal();
- this.renderTasks();
- this.renderProjects();
- }
- }
- }
- toggleTask(taskId) {
- const task = storage.getTasks().find(t => t.id === taskId);
- if (task) {
- task.completed = !task.completed;
- if (task.completed) {
- task.completedDate = this.getSelectedDateStr();
- storage.addPoints(task.points, 'tasks');
- storage.updateDailyStreak(true);
- // If repeatable, immediately create next task with recalculated due date
- if (task.repeatType !== 'none') {
- this.createNextRecurringTask(task);
- }
- }
- else {
- // If uncompleting, also recalculate level
- task.completedDate = null;
- }
- storage.updateTask(taskId, task);
- storage.updateLevel();
- this.renderTasks();
- this.renderDashboard();
- this.renderProjects();
- }
- }
- nextOccurrenceOfDays(fromDate, days) {
- const next = new Date(fromDate);
- for (let i = 1; i <= 7; i++) {
- next.setDate(next.getDate() + 1);
- if (days.includes(next.getDay())) {
- return next;
- }
- }
- return next;
- }
- createNextRecurringTask(completedTask) {
- const [year, month, day] = completedTask.completedDate.split('-').map(Number);
- const completionDate = new Date(year, month - 1, day);
- const nextDueDate = new Date(completionDate);
- const hasDaysOfWeek = completedTask.daysOfWeek && completedTask.daysOfWeek.length > 0;
- switch (completedTask.repeatType) {
- case 'daily':
- if (hasDaysOfWeek) {
- const next = this.nextOccurrenceOfDays(completionDate, completedTask.daysOfWeek);
- nextDueDate.setTime(next.getTime());
- }
- else {
- nextDueDate.setDate(nextDueDate.getDate() + (completedTask.repeatUnit || 1));
- }
- break;
- case 'weekly':
- if (hasDaysOfWeek) {
- const next = this.nextOccurrenceOfDays(completionDate, completedTask.daysOfWeek);
- next.setDate(next.getDate() + 7 * ((completedTask.repeatUnit || 1) - 1));
- nextDueDate.setTime(next.getTime());
- }
- else {
- nextDueDate.setDate(nextDueDate.getDate() + 7 * (completedTask.repeatUnit || 1));
- }
- break;
- case 'monthly':
- nextDueDate.setMonth(nextDueDate.getMonth() + (completedTask.repeatUnit || 1));
- break;
- case 'yearly':
- nextDueDate.setFullYear(nextDueDate.getFullYear() + (completedTask.repeatUnit || 1));
- break;
- case 'custom':
- nextDueDate.setDate(nextDueDate.getDate() + (completedTask.customRepeatDays || 1));
- break;
- case 'movable':
- nextDueDate.setDate(nextDueDate.getDate() + (completedTask.movableRepeatDays || 1));
- break;
- default:
- return;
- }
- const newTask = {
- title: completedTask.title,
- description: completedTask.description,
- category: completedTask.category,
- priority: completedTask.priority,
- points: completedTask.points,
- projectId: completedTask.projectId,
- repeatType: completedTask.repeatType,
- repeatUnit: completedTask.repeatUnit,
- customRepeatDays: completedTask.customRepeatDays,
- movableRepeatDays: completedTask.movableRepeatDays,
- daysOfWeek: completedTask.daysOfWeek,
- dueDate: storage.formatDate(nextDueDate)
- };
- storage.addTask(newTask);
- }
- // ========================
- // Projects Management
- // ========================
- renderProjects() {
- const projects = storage.getProjects();
- const container = document.getElementById('projectsList');
- if (projects.length === 0) {
- container.innerHTML = '
No projects yet. Create one to organize your tasks!
';
- return;
- }
- container.innerHTML = projects.map(project => this.renderProjectCard(project)).join('');
- document.querySelectorAll('.project-card').forEach(card => {
- card.addEventListener('click', () => {
- this.openProjectDetailModal(card.dataset.projectId);
- });
- });
- }
- renderProjectCard(project) {
- const tasks = storage.getTasks().filter(t => t.projectId === project.id);
- const completed = tasks.filter(t => t.completed).length;
- const percentage = tasks.length === 0 ? 0 : Math.round((completed / tasks.length) * 100);
- return `
-
-
${project.name}
- ${project.description ? `
${project.description}
` : ''}
-
-
- Tasks
- ${tasks.length}
-
-
- Completed
- ${completed}
-
-
- Progress
- ${percentage}%
-
-
-
-
- `;
- }
- openProjectModal(projectId = null) {
- this.currentEditingProjectId = projectId;
- const modal = document.getElementById('projectModal');
- const form = document.getElementById('projectForm');
- const deleteBtn = document.getElementById('deleteProjectBtn');
- form.reset();
- deleteBtn.style.display = 'none';
- if (projectId) {
- const project = storage.getProjects().find(p => p.id === projectId);
- if (project) {
- document.getElementById('projectName').value = project.name;
- document.getElementById('projectDescription').value = project.description || '';
- document.getElementById('projectColor').value = project.color || 'blue';
- deleteBtn.style.display = 'block';
- }
- }
- modal.classList.add('active');
- }
- closeProjectModal() {
- document.getElementById('projectModal').classList.remove('active');
- this.currentEditingProjectId = null;
- }
- saveProject(e) {
- e.preventDefault();
- const project = {
- name: document.getElementById('projectName').value,
- description: document.getElementById('projectDescription').value,
- color: document.getElementById('projectColor').value
- };
- if (this.currentEditingProjectId) {
- storage.updateProject(this.currentEditingProjectId, project);
- }
- else {
- storage.addProject(project);
- }
- this.closeProjectModal();
- this.renderProjects();
- this.updateProjectSelect();
- }
- deleteProject() {
- if (this.currentEditingProjectId) {
- if (confirm('Are you sure you want to delete this project? This will not delete the tasks in it.')) {
- storage.deleteProject(this.currentEditingProjectId);
- this.closeProjectModal();
- this.renderProjects();
- }
- }
- }
- openProjectDetailModal(projectId) {
- const project = storage.getProjects().find(p => p.id === projectId);
- if (!project)
- return;
- this.currentEditingProjectId = projectId;
- const modal = document.getElementById('projectDetailModal');
- document.getElementById('projectDetailTitle').textContent = project.name;
- document.getElementById('projectDetailDescription').textContent = project.description || 'No description';
- const tasks = storage.getTasks().filter(t => t.projectId === projectId);
- const completed = tasks.filter(t => t.completed).length;
- const percentage = tasks.length === 0 ? 0 : Math.round((completed / tasks.length) * 100);
- document.getElementById('projectDetailTotalTasks').textContent = String(tasks.length);
- document.getElementById('projectDetailCompletedTasks').textContent = String(completed);
- document.getElementById('projectDetailProgress').textContent = percentage + '%';
- this.renderProjectDetailTasks(tasks);
- modal.classList.add('active');
- }
- closeProjectDetailModal() {
- document.getElementById('projectDetailModal').classList.remove('active');
- this.currentEditingProjectId = null;
- }
- editProjectFromDetail() {
- const projectId = this.currentEditingProjectId;
- this.closeProjectDetailModal();
- this.openProjectModal(projectId);
- }
- renderProjectDetailTasks(tasks) {
- const container = document.getElementById('projectDetailTaskList');
- if (tasks.length === 0) {
- container.innerHTML = '
No tasks in this project yet.
';
- return;
- }
- container.innerHTML = tasks.map(task => `
-
-
-
-
${task.title}
-
- ${task.dueDate ? `๐
${task.dueDate}` : ''}
- ${task.priority ? `โก ${task.priority}` : ''}
- ${task.category ? `๐ ${task.category}` : ''}
-
-
-
- `).join('');
- document.querySelectorAll('#projectDetailTaskList .task-item').forEach(item => {
- const checkbox = item.querySelector('.task-checkbox');
- checkbox.addEventListener('click', (e) => {
- e.stopPropagation();
- this.toggleTask(item.dataset.taskId);
- this.openProjectDetailModal(this.currentEditingProjectId);
- });
- item.addEventListener('click', (e) => {
- if (!e.target.classList.contains('task-checkbox')) {
- this.closeProjectDetailModal();
- this.switchTab('tasks');
- this.openTaskModal(item.dataset.taskId);
- }
- });
- });
- }
- // ========================
- // Habits Management
- // ========================
- renderHabits() {
- const habits = storage.getHabits();
- const container = document.getElementById('habitsList');
- if (habits.length === 0) {
- container.innerHTML = '
No habits yet. Create daily habits to build streaks!
';
- return;
- }
- container.innerHTML = habits.map(habit => this.renderHabitCard(habit)).join('');
- document.querySelectorAll('.habit-card').forEach(card => {
- card.addEventListener('click', (e) => {
- if (!e.target.classList.contains('habit-checkbox')) {
- this.openHabitModal(card.dataset.habitId);
- }
- });
- });
- document.querySelectorAll('.habit-checkbox').forEach(btn => {
- btn.addEventListener('click', (e) => {
- e.stopPropagation();
- const habitId = e.target.dataset.habitId;
- this.completeHabit(habitId);
- });
- });
- }
- renderHabitCard(habit) {
- const selectedDayOfWeek = this.selectedDate.getDay();
- const isValidDay = !habit.daysOfWeek || habit.daysOfWeek.includes(selectedDayOfWeek);
- const selectedDateStr = this.getSelectedDateStr();
- const isPastDay = !this.isSelectedDateToday();
- const todaysCompletions = storage.countHabitCompletionsForDate(habit.id, selectedDateStr);
- const targetGoal = habit.targetGoal || 1;
- const percentage = Math.min(100, Math.round((todaysCompletions / targetGoal) * 100));
- const isComplete = todaysCompletions >= targetGoal;
- const btnLabel = !isValidDay
- ? 'โ Not Scheduled'
- : isComplete
- ? (isPastDay ? 'โ Logged' : 'โ Done for Today')
- : (isPastDay ? '+ Log Past Day' : '+ Complete');
- return `
-
-
${habit.icon}
-
${habit.name}
- ${habit.description ? `
${habit.description}
` : ''}
-
-
- Streak
- ${habit.streak || 0}
-
-
- Points
- ${habit.points}
-
-
- Progress
- ${todaysCompletions}/${targetGoal}
-
-
-
-
-
- ${isComplete ? 'โ Complete!' : `${percentage}% Complete`}
-
-
-
-
- `;
- }
- openHabitModal(habitId = null) {
- this.currentEditingHabitId = habitId;
- const modal = document.getElementById('habitModal');
- const form = document.getElementById('habitForm');
- const deleteBtn = document.getElementById('deleteHabitBtn');
- form.reset();
- deleteBtn.style.display = 'none';
- document.getElementById('habitIcon').value = 'โญ';
- document.getElementById('habitIconDisplay').textContent = 'โญ';
- document.querySelectorAll('input[name="habitDay"]').forEach(checkbox => {
- checkbox.checked = false;
- });
- if (habitId) {
- const habit = storage.getHabits().find(h => h.id === habitId);
- if (habit) {
- document.getElementById('habitName').value = habit.name;
- document.getElementById('habitDescription').value = habit.description || '';
- document.getElementById('habitIcon').value = habit.icon || 'โญ';
- document.getElementById('habitIconDisplay').textContent = habit.icon || 'โญ';
- document.getElementById('habitCategory').value = habit.category || '';
- document.getElementById('habitPoints').value = String(habit.points || 5);
- document.getElementById('habitTargetGoal').value = String(habit.targetGoal || 1);
- deleteBtn.style.display = 'block';
- if (habit.daysOfWeek && Array.isArray(habit.daysOfWeek)) {
- habit.daysOfWeek.forEach(day => {
- const checkbox = document.querySelector(`input[name="habitDay"][value="${day}"]`);
- if (checkbox) {
- checkbox.checked = true;
- }
- });
- }
- else {
- document.querySelectorAll('input[name="habitDay"]').forEach(checkbox => {
- checkbox.checked = true;
- });
- }
- }
- }
- else {
- document.querySelectorAll('input[name="habitDay"]').forEach(checkbox => {
- checkbox.checked = true;
- });
- }
- this.loadCategoryDropdown('habit');
- modal.classList.add('active');
- }
- closeHabitModal() {
- document.getElementById('habitModal').classList.remove('active');
- this.currentEditingHabitId = null;
- }
- saveHabit(e) {
- e.preventDefault();
- const selectedDays = Array.from(document.querySelectorAll('input[name="habitDay"]:checked'))
- .map(checkbox => parseInt(checkbox.value));
- const habit = {
- name: document.getElementById('habitName').value,
- description: document.getElementById('habitDescription').value,
- icon: document.getElementById('habitIcon').value,
- category: document.getElementById('habitCategory').value || null,
- points: parseInt(document.getElementById('habitPoints').value),
- targetGoal: parseInt(document.getElementById('habitTargetGoal').value) || 1,
- daysOfWeek: selectedDays.length > 0 ? selectedDays : [0, 1, 2, 3, 4, 5, 6]
- };
- if (this.currentEditingHabitId) {
- storage.updateHabit(this.currentEditingHabitId, habit);
- }
- else {
- storage.addHabit(habit);
- }
- this.closeHabitModal();
- this.renderHabits();
- }
- deleteHabit() {
- if (this.currentEditingHabitId) {
- if (confirm('Are you sure you want to delete this habit?')) {
- storage.deleteHabit(this.currentEditingHabitId);
- this.closeHabitModal();
- this.renderHabits();
- }
- }
- }
- completeHabit(habitId) {
- const habit = storage.getHabits().find(h => h.id === habitId);
- if (habit) {
- const selectedDayOfWeek = this.selectedDate.getDay();
- const isValidDay = !habit.daysOfWeek || habit.daysOfWeek.includes(selectedDayOfWeek);
- if (isValidDay) {
- storage.logHabitCompletion(habitId, this.selectedDate);
- storage.addPoints(habit.points, 'habits');
- storage.updateDailyStreak(true);
- this.renderHabits();
- this.renderDashboard();
- }
- }
- }
- openEmojiPicker() {
- const modal = document.getElementById('emojiModal');
- const emojiGrid = document.getElementById('emojiGrid');
- emojiGrid.innerHTML = this.emojis.map(emoji => `
`).join('');
- document.querySelectorAll('.emoji-btn').forEach(btn => {
- btn.addEventListener('click', (e) => {
- e.preventDefault();
- this.selectEmoji(e.target.dataset.emoji);
- });
- });
- modal.classList.add('active');
- }
- closeEmojiPicker() {
- document.getElementById('emojiModal').classList.remove('active');
- }
- selectEmoji(emoji) {
- document.getElementById('habitIcon').value = emoji;
- document.getElementById('habitIconDisplay').textContent = emoji;
- this.closeEmojiPicker();
- }
- // ========================
- // Category Management
- // ========================
- loadCategoryDropdown(type) {
- const select = document.getElementById(`${type}Category`);
- const categories = storage.getCategories();
- const currentValue = select.value;
- const emptyOption = document.createElement('option');
- emptyOption.value = '';
- emptyOption.textContent = 'Select category...';
- const addNewOption = document.createElement('option');
- addNewOption.value = '__add_new__';
- addNewOption.textContent = '+ Add New Category';
- select.innerHTML = '';
- select.appendChild(emptyOption);
- categories.forEach(cat => {
- const option = document.createElement('option');
- option.value = cat;
- option.textContent = cat;
- select.appendChild(option);
- });
- select.appendChild(addNewOption);
- select.value = currentValue;
- }
- handleCategoryChange(type, value) {
- const inputDiv = document.getElementById(`${type}CategoryInput`);
- const textInput = document.getElementById(`${type}CategoryText`);
- if (value === '__add_new__') {
- inputDiv.style.display = 'block';
- textInput.value = '';
- textInput.focus();
- }
- else {
- inputDiv.style.display = 'none';
- }
- }
- handleAddCategory(type) {
- const textInput = document.getElementById(`${type}CategoryText`);
- const categoryName = textInput.value.trim();
- if (categoryName) {
- if (storage.addCategory(categoryName)) {
- this.loadCategoryDropdown(type);
- const select = document.getElementById(`${type}Category`);
- select.value = categoryName;
- document.getElementById(`${type}CategoryInput`).style.display = 'none';
- textInput.value = '';
- }
- else {
- alert('This category already exists!');
- }
- }
- else {
- alert('Please enter a category name');
- }
- }
- cancelAddCategory(type) {
- document.getElementById(`${type}CategoryInput`).style.display = 'none';
- document.getElementById(`${type}CategoryText`).value = '';
- document.getElementById(`${type}Category`).value = '';
- }
- // ========================
- // Settings Category Management
- // ========================
- renderCategoryManagement() {
- const list = document.getElementById('categoryList');
- if (!list)
- return;
- const categories = storage.getCategories();
- list.innerHTML = categories.length === 0
- ? '
No categories yet.'
- : categories.map(cat => `
-
- ${this.escapeHtml(cat)}
-
-
- `).join('');
- }
- escapeHtml(str) {
- return String(str).replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>');
- }
- startEditCategory(name, btn) {
- const li = btn.closest('li');
- const nameSpan = li.querySelector('.category-name');
- nameSpan.style.display = 'none';
- const input = document.createElement('input');
- input.type = 'text';
- input.className = 'category-edit-input';
- input.value = name;
- li.insertBefore(input, nameSpan);
- btn.textContent = 'Save';
- btn.onclick = () => this.saveEditCategory(name, input, btn);
- const cancelBtn = document.createElement('button');
- cancelBtn.className = 'btn btn-secondary btn-sm';
- cancelBtn.textContent = 'Cancel';
- cancelBtn.onclick = () => this.renderCategoryManagement();
- btn.after(cancelBtn);
- input.focus();
- }
- saveEditCategory(oldName, input, _btn) {
- const newName = input.value.trim();
- if (!newName) {
- alert('Please enter a category name.');
- return;
- }
- if (newName === oldName) {
- this.renderCategoryManagement();
- return;
- }
- if (!storage.updateCategory(oldName, newName)) {
- alert('A category with that name already exists.');
- return;
- }
- this.renderCategoryManagement();
- }
- deleteCategoryItem(name) {
- if (!confirm(`Delete category "${name}"? All related items will have their category cleared.`))
- return;
- storage.deleteCategory(name);
- this.renderCategoryManagement();
- }
- addCategoryFromSettings() {
- const input = document.getElementById('newCategoryText');
- const name = input.value.trim();
- if (!name) {
- alert('Please enter a category name.');
- return;
- }
- if (!storage.addCategory(name)) {
- alert('This category already exists.');
- return;
- }
- input.value = '';
- this.renderCategoryManagement();
- }
- // ========================
- // Finance Management
- // ========================
- initializeFinanceDateFilter() {
- const today = new Date();
- const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
- const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
- document.getElementById('financeStartDate').valueAsDate = firstDay;
- document.getElementById('financeEndDate').valueAsDate = lastDay;
- }
- resetFinanceFilter() {
- this.initializeFinanceDateFilter();
- this.renderFinances();
- }
- getFinanceDateRange() {
- const startDate = document.getElementById('financeStartDate').value;
- const endDate = document.getElementById('financeEndDate').value;
- return { startDate, endDate };
- }
- filterFinanceItemsByDate(items) {
- const { startDate, endDate } = this.getFinanceDateRange();
- if (!startDate && !endDate) {
- return items.map(item => ({ ...item, monthlyAmount: item.amount }));
- }
- return items.filter(item => {
- if (!item.date)
- return false;
- if (item.recurring === 'yearly' || item.recurring === 'monthly') {
- if (endDate && item.date > endDate)
- return false;
- return true;
- }
- if (startDate && item.date < startDate)
- return false;
- if (endDate && item.date > endDate)
- return false;
- return true;
- }).map(item => ({
- ...item,
- monthlyAmount: item.recurring === 'yearly' ? item.amount / 12 : item.amount
- }));
- }
- renderFinances() {
- this.updateFinanceSummary();
- this.renderExpenses();
- this.renderRevenue();
- this.renderCharges();
- }
- updateFinanceSummary() {
- const expenses = this.filterFinanceItemsByDate(storage.getExpenses());
- const revenue = this.filterFinanceItemsByDate(storage.getRevenue());
- const charges = this.filterFinanceItemsByDate(storage.getCharges());
- const totalExpenses = expenses.reduce((sum, e) => sum + (e.monthlyAmount || 0), 0);
- const totalCharges = charges.reduce((sum, c) => sum + (c.monthlyAmount || 0), 0);
- const totalRevenue = revenue.reduce((sum, r) => sum + (r.monthlyAmount || 0), 0);
- const net = totalRevenue - totalExpenses - totalCharges;
- document.getElementById('totalIncome').textContent = '$' + totalRevenue.toFixed(2);
- document.getElementById('totalExpenses').textContent = '$' + (totalExpenses + totalCharges).toFixed(2);
- document.getElementById('netBalance').textContent = '$' + net.toFixed(2);
- }
- renderExpenses() {
- const expenses = this.filterFinanceItemsByDate(storage.getExpenses());
- this.renderFinanceList(expenses, 'expensesList', 'expense');
- }
- renderRevenue() {
- const revenue = this.filterFinanceItemsByDate(storage.getRevenue());
- this.renderFinanceList(revenue, 'revenueList', 'revenue', true);
- }
- renderCharges() {
- const charges = this.filterFinanceItemsByDate(storage.getCharges());
- this.renderFinanceList(charges, 'chargesList', 'charge');
- }
- renderFinanceList(items, containerId, type, isIncome = false) {
- const container = document.getElementById(containerId);
- if (items.length === 0) {
- container.innerHTML = '
No items. Add one to get started!
';
- return;
- }
- container.innerHTML = items.map(item => {
- const displayAmount = (item.monthlyAmount !== undefined ? item.monthlyAmount : item.amount).toFixed(2);
- const monthlyLabel = item.recurring === 'yearly' ? '
/mo' : '';
- return `
-
-
-
${item.description}
-
- ${item.category ? `๐ ${item.category}` : ''}
- ๐
${item.date || 'N/A'}
- ${item.recurring ? `๐ ${item.recurring}` : ''}
-
-
-
- ${isIncome ? '+' : '-'}$${displayAmount}${monthlyLabel}
-
-
- `;
- }).join('');
- document.querySelectorAll('.finance-item').forEach(item => {
- item.addEventListener('click', () => {
- this.openFinanceModal(item.dataset.financeType, item.dataset.financeId);
- });
- });
- }
- openFinanceModal(type, financeId = null) {
- this.currentEditingFinanceType = type;
- this.currentEditingFinanceId = financeId;
- const modal = document.getElementById('financeModal');
- const form = document.getElementById('financeForm');
- const deleteBtn = document.getElementById('deleteFinanceBtn');
- const recurringGroup = document.getElementById('financeRecurringGroup');
- form.reset();
- deleteBtn.style.display = 'none';
- document.getElementById('financeDate').valueAsDate = new Date();
- this.loadCategoryDropdown('finance');
- recurringGroup.style.display = ['expense', 'revenue'].includes(type) ? 'block' : 'none';
- const titles = { expense: 'Add Expense', revenue: 'Add Revenue', charge: 'Add Other Charge' };
- document.getElementById('financeModalTitle').textContent = financeId ? `Edit ${type}` : titles[type];
- if (financeId) {
- let item;
- if (type === 'expense')
- item = storage.getExpenses().find(e => e.id === financeId);
- else if (type === 'revenue')
- item = storage.getRevenue().find(r => r.id === financeId);
- else if (type === 'charge')
- item = storage.getCharges().find(c => c.id === financeId);
- if (item) {
- document.getElementById('financeDescription').value = item.description;
- document.getElementById('financeAmount').value = String(item.amount);
- document.getElementById('financeDate').value = item.date || '';
- document.getElementById('financeCategory').value = item.category || '';
- if (item.recurring) {
- document.getElementById('financeRecurring').value = item.recurring;
- }
- deleteBtn.style.display = 'block';
- }
- }
- modal.classList.add('active');
- }
- closeFinanceModal() {
- document.getElementById('financeModal').classList.remove('active');
- this.currentEditingFinanceId = null;
- this.currentEditingFinanceType = null;
- }
- saveFinance(e) {
- e.preventDefault();
- const financeItem = {
- description: document.getElementById('financeDescription').value,
- amount: parseFloat(document.getElementById('financeAmount').value),
- date: document.getElementById('financeDate').value,
- category: document.getElementById('financeCategory').value
- };
- if (['expense', 'revenue'].includes(this.currentEditingFinanceType)) {
- financeItem.recurring = document.getElementById('financeRecurring').value;
- }
- if (this.currentEditingFinanceType === 'expense') {
- if (this.currentEditingFinanceId) {
- storage.updateExpense(this.currentEditingFinanceId, financeItem);
- }
- else {
- storage.addExpense(financeItem);
- }
- }
- else if (this.currentEditingFinanceType === 'revenue') {
- if (this.currentEditingFinanceId) {
- storage.updateRevenue(this.currentEditingFinanceId, financeItem);
- }
- else {
- storage.addRevenue(financeItem);
- }
- }
- else if (this.currentEditingFinanceType === 'charge') {
- if (this.currentEditingFinanceId) {
- storage.updateCharge(this.currentEditingFinanceId, financeItem);
- }
- else {
- storage.addCharge(financeItem);
- }
- }
- this.closeFinanceModal();
- this.renderFinances();
- }
- deleteFinance() {
- if (this.currentEditingFinanceId) {
- if (confirm('Are you sure you want to delete this item?')) {
- if (this.currentEditingFinanceType === 'expense') {
- storage.deleteExpense(this.currentEditingFinanceId);
- }
- else if (this.currentEditingFinanceType === 'revenue') {
- storage.deleteRevenue(this.currentEditingFinanceId);
- }
- else if (this.currentEditingFinanceType === 'charge') {
- storage.deleteCharge(this.currentEditingFinanceId);
- }
- this.closeFinanceModal();
- this.renderFinances();
- }
- }
- }
- // ========================
- // Shop/Rewards Management
- // ========================
- renderShop() {
- const rewards = storage.getRewards();
- const userStats = storage.getUserStats();
- const container = document.getElementById('rewardsList');
- document.getElementById('shopPointsDisplay').textContent = String(userStats.totalPoints);
- if (rewards.length === 0) {
- container.innerHTML = '
No rewards yet. Add rewards to spend your points on!
';
- return;
- }
- container.innerHTML = rewards.map(reward => {
- let alreadyPurchased = false;
- if (reward.repeatable === false) {
- const purchaseHistory = storage.getData().purchaseHistory || [];
- alreadyPurchased = purchaseHistory.some(ph => ph.rewardId === reward.id);
- }
- const disabled = userStats.totalPoints < reward.cost || alreadyPurchased;
- let purchaseLabel = 'Purchase';
- if (userStats.totalPoints < reward.cost)
- purchaseLabel = 'Not Enough Points';
- if (alreadyPurchased)
- purchaseLabel = 'Purchased';
- return `
-
-
${reward.name}
- ${reward.description ? `
${reward.description}
` : ''}
-
-
- Cost
- ${reward.cost} pts
-
-
- ${reward.repeatable === false ? 'One-time' : 'Repeatable'}
-
-
-
-
-
-
-
- `;
- }).join('');
- document.querySelectorAll('.purchase-btn').forEach(btn => {
- btn.addEventListener('click', (e) => {
- e.stopPropagation();
- const card = e.target.closest('[data-reward-id]');
- this.purchaseReward(card.dataset.rewardId);
- });
- });
- document.querySelectorAll('.edit-reward-btn').forEach(btn => {
- btn.addEventListener('click', (e) => {
- e.stopPropagation();
- const card = e.target.closest('[data-reward-id]');
- this.openRewardModal(card.dataset.rewardId);
- });
- });
- }
- openRewardModal(rewardId = null) {
- this.currentEditingRewardId = rewardId;
- const modal = document.getElementById('rewardModal');
- const form = document.getElementById('rewardForm');
- const deleteBtn = document.getElementById('deleteRewardBtn');
- form.reset();
- deleteBtn.style.display = 'none';
- document.getElementById('rewardModalTitle').textContent = rewardId ? 'Edit Reward' : 'Add Reward';
- if (rewardId) {
- const reward = storage.getRewards().find(r => r.id === rewardId);
- if (reward) {
- document.getElementById('rewardName').value = reward.name;
- document.getElementById('rewardDescription').value = reward.description || '';
- document.getElementById('rewardCost').value = String(reward.cost);
- document.getElementById('rewardRepeatable').value = String(reward.repeatable === undefined ? true : reward.repeatable);
- deleteBtn.style.display = 'block';
- }
- }
- modal.classList.add('active');
- }
- closeRewardModal() {
- document.getElementById('rewardModal').classList.remove('active');
- this.currentEditingRewardId = null;
- }
- saveReward(e) {
- e.preventDefault();
- const reward = {
- name: document.getElementById('rewardName').value,
- description: document.getElementById('rewardDescription').value,
- cost: parseInt(document.getElementById('rewardCost').value),
- repeatable: document.getElementById('rewardRepeatable').value === 'true'
- };
- if (this.currentEditingRewardId) {
- storage.updateReward(this.currentEditingRewardId, reward);
- }
- else {
- storage.addReward(reward);
- }
- this.closeRewardModal();
- this.renderShop();
- }
- deleteReward() {
- if (this.currentEditingRewardId) {
- if (confirm('Are you sure you want to delete this reward?')) {
- storage.deleteReward(this.currentEditingRewardId);
- this.closeRewardModal();
- this.renderShop();
- }
- }
- }
- purchaseReward(rewardId) {
- const reward = storage.getRewards().find(r => r.id === rewardId);
- if (!reward)
- return;
- if (confirm(`Purchase "${reward.name}" for ${reward.cost} points?`)) {
- const result = storage.purchaseReward(rewardId);
- if (result.success) {
- alert(`Congratulations! You've purchased: ${reward.name}!\n\nEnjoy your reward! ๐`);
- this.renderShop();
- this.renderDashboard();
- }
- else {
- alert(result.message);
- }
- }
- }
- // ========================
- // Wish List
- // ========================
- renderWishList() {
- const items = storage.getWishItems();
- const container = document.getElementById('wishList');
- if (items.length === 0) {
- container.innerHTML = '
No items in your wish list. Add one to get started!
';
- return;
- }
- container.innerHTML = items.map(item => this.renderWishItem(item)).join('');
- container.querySelectorAll('.wish-item').forEach(el => {
- el.addEventListener('dragstart', (e) => {
- this.dragSrcWishId = el.dataset.wishId;
- el.classList.add('dragging');
- e.dataTransfer.effectAllowed = 'move';
- });
- el.addEventListener('dragend', () => {
- this.dragSrcWishId = null;
- el.classList.remove('dragging');
- container.querySelectorAll('.wish-item').forEach(i => i.classList.remove('drag-over'));
- });
- el.addEventListener('dragover', (e) => {
- e.preventDefault();
- e.dataTransfer.dropEffect = 'move';
- container.querySelectorAll('.wish-item').forEach(i => i.classList.remove('drag-over'));
- el.classList.add('drag-over');
- });
- el.addEventListener('drop', (e) => {
- e.preventDefault();
- const targetId = el.dataset.wishId;
- if (this.dragSrcWishId && this.dragSrcWishId !== targetId) {
- const allItems = storage.getWishItems();
- const srcIdx = allItems.findIndex(i => i.id === this.dragSrcWishId);
- const tgtIdx = allItems.findIndex(i => i.id === targetId);
- if (srcIdx !== -1 && tgtIdx !== -1) {
- const reordered = [...allItems];
- const [moved] = reordered.splice(srcIdx, 1);
- reordered.splice(tgtIdx, 0, moved);
- storage.reorderWishItems(reordered.map(i => i.id));
- this.renderWishList();
- }
- }
- });
- // Touch events for mobile drag and drop support
- let touchDragOverItem = null;
- el.addEventListener('touchstart', () => {
- this.dragSrcWishId = el.dataset.wishId;
- el.classList.add('dragging');
- }, { passive: false });
- el.addEventListener('touchmove', (e) => {
- e.preventDefault();
- const touch = e.touches[0];
- // Temporarily hide the dragged element so elementFromPoint finds the element underneath
- el.style.visibility = 'hidden';
- const target = document.elementFromPoint(touch.clientX, touch.clientY);
- el.style.visibility = '';
- const targetItem = target?.closest('.wish-item') ?? null;
- if (targetItem !== touchDragOverItem) {
- touchDragOverItem?.classList.remove('drag-over');
- touchDragOverItem = targetItem !== el ? targetItem : null;
- touchDragOverItem?.classList.add('drag-over');
- }
- }, { passive: false });
- el.addEventListener('touchend', (e) => {
- el.classList.remove('dragging');
- touchDragOverItem?.classList.remove('drag-over');
- const touch = e.changedTouches[0];
- el.style.visibility = 'hidden';
- const target = document.elementFromPoint(touch.clientX, touch.clientY);
- el.style.visibility = '';
- const targetItem = target?.closest('.wish-item');
- const targetId = targetItem?.dataset.wishId;
- touchDragOverItem = null;
- if (this.dragSrcWishId && targetId && this.dragSrcWishId !== targetId) {
- const allItems = storage.getWishItems();
- const srcIdx = allItems.findIndex(i => i.id === this.dragSrcWishId);
- const tgtIdx = allItems.findIndex(i => i.id === targetId);
- if (srcIdx !== -1 && tgtIdx !== -1) {
- const reordered = [...allItems];
- const [moved] = reordered.splice(srcIdx, 1);
- reordered.splice(tgtIdx, 0, moved);
- storage.reorderWishItems(reordered.map(i => i.id));
- this.renderWishList();
- }
- }
- this.dragSrcWishId = null;
- });
- el.querySelector('.wish-item-checkbox').addEventListener('change', (e) => {
- e.stopPropagation();
- const checkbox = e.target;
- storage.updateWishItem(el.dataset.wishId, { completed: checkbox.checked });
- el.classList.toggle('completed', checkbox.checked);
- });
- el.querySelector('.edit-wish-btn').addEventListener('click', (e) => {
- e.stopPropagation();
- this.openWishItemModal(el.dataset.wishId);
- });
- });
- }
- renderWishItem(item) {
- const priceStr = item.price !== undefined && item.price !== null
- ? `
$${Number(item.price).toFixed(2)}`
- : '';
- const urlStr = item.url
- ? `
๐ View Listing`
- : '';
- const completedClass = item.completed ? ' completed' : '';
- const checkedAttr = item.completed ? ' checked' : '';
- return `
-
-
โ ฟ
-
-
-
${item.title}
-
- ${priceStr}
- ${urlStr}
-
-
-
-
- `;
- }
- openWishItemModal(itemId = null) {
- this.currentEditingWishItemId = itemId;
- const modal = document.getElementById('wishItemModal');
- const form = document.getElementById('wishItemForm');
- const deleteBtn = document.getElementById('deleteWishItemBtn');
- form.reset();
- deleteBtn.style.display = 'none';
- document.getElementById('wishItemModalTitle').textContent = itemId ? 'Edit Wish List Item' : 'Add Wish List Item';
- if (itemId) {
- const item = storage.getWishItems().find(w => w.id === itemId);
- if (item) {
- document.getElementById('wishItemTitle').value = item.title;
- document.getElementById('wishItemUrl').value = item.url || '';
- document.getElementById('wishItemPrice').value =
- item.price !== undefined && item.price !== null ? String(item.price) : '';
- deleteBtn.style.display = 'block';
- }
- }
- modal.classList.add('active');
- }
- closeWishItemModal() {
- document.getElementById('wishItemModal').classList.remove('active');
- this.currentEditingWishItemId = null;
- }
- saveWishItem(e) {
- e.preventDefault();
- const title = document.getElementById('wishItemTitle').value;
- const url = document.getElementById('wishItemUrl').value.trim() || undefined;
- const priceVal = document.getElementById('wishItemPrice').value;
- const price = priceVal !== '' ? parseFloat(priceVal) : undefined;
- const item = { title, url, price };
- if (this.currentEditingWishItemId) {
- storage.updateWishItem(this.currentEditingWishItemId, item);
- }
- else {
- storage.addWishItem(item);
- }
- this.closeWishItemModal();
- this.renderWishList();
- }
- deleteWishItem() {
- if (this.currentEditingWishItemId) {
- if (confirm('Are you sure you want to delete this wish list item?')) {
- storage.deleteWishItem(this.currentEditingWishItemId);
- this.closeWishItemModal();
- this.renderWishList();
- }
- }
- }
- // ========================
- // Notes
- // ========================
- renderNotes() {
- const notes = storage.getNotes();
- const container = document.getElementById('notesList');
- if (notes.length === 0) {
- container.innerHTML = '
No notes yet. Add one to get started!
';
- return;
- }
- container.innerHTML = notes.map(note => this.renderNoteItem(note)).join('');
- container.querySelectorAll('.note-item').forEach(el => {
- el.addEventListener('click', () => {
- this.openNoteModal(el.dataset.noteId);
- });
- });
- }
- renderNoteItem(note) {
- const rawPreview = note.content.length > 120
- ? note.content.substring(0, 120) + 'โฆ'
- : note.content;
- const title = this.escapeHtml(note.title || 'Untitled');
- const preview = rawPreview ? this.escapeHtml(rawPreview) : '
No content';
- const date = new Date(note.updatedDate ?? note.createdDate).toLocaleDateString();
- return `
-
-
${title}
-
${preview}
-
${date}
-
- `;
- }
- openNoteModal(noteId = null) {
- this.currentEditingNoteId = noteId;
- const modal = document.getElementById('noteModal');
- const form = document.getElementById('noteForm');
- const deleteBtn = document.getElementById('deleteNoteBtn');
- form.reset();
- deleteBtn.style.display = 'none';
- document.getElementById('noteModalTitle').textContent = noteId ? 'Edit Note' : 'Add Note';
- if (noteId) {
- const note = storage.getNotes().find(n => n.id === noteId);
- if (note) {
- document.getElementById('noteTitle').value = note.title;
- document.getElementById('noteContent').value = note.content;
- deleteBtn.style.display = 'block';
- }
- }
- modal.classList.add('active');
- }
- closeNoteModal() {
- document.getElementById('noteModal').classList.remove('active');
- this.currentEditingNoteId = null;
- }
- saveNote(e) {
- e.preventDefault();
- const title = document.getElementById('noteTitle').value.trim();
- const content = document.getElementById('noteContent').value.trim();
- if (this.currentEditingNoteId) {
- storage.updateNote(this.currentEditingNoteId, { title, content });
- }
- else {
- storage.addNote({ title, content });
- }
- this.closeNoteModal();
- this.renderNotes();
- }
- deleteNote() {
- if (this.currentEditingNoteId) {
- if (confirm('Are you sure you want to delete this note?')) {
- storage.deleteNote(this.currentEditingNoteId);
- this.closeNoteModal();
- this.renderNotes();
- }
- }
- }
- // ========================
- // Settings
- // ========================
- renderSettings() {
- this.loadSettings();
- this.updateSettingsStatus();
- this.renderCategoryManagement();
- }
- loadSettings() {
- const settings = storage.getSettings();
- const tasksPerLevelInput = document.getElementById('tasksPerLevel');
- if (tasksPerLevelInput) {
- tasksPerLevelInput.value = String(settings.tasksPerLevel || 30);
- }
- }
- updateSettingsStatus() {
- const settings = storage.getSettings();
- const tasks = storage.getTasks();
- const completedTasksCount = tasks.filter(t => t.completed).length;
- const userStats = storage.getUserStats();
- const tasksInCurrentLevel = completedTasksCount % settings.tasksPerLevel;
- const tasksToNext = settings.tasksPerLevel - tasksInCurrentLevel;
- const currentLevelEl = document.getElementById('settingsCurrentLevel');
- const totalCompletedEl = document.getElementById('settingsTotalCompleted');
- const tasksToNextEl = document.getElementById('settingsTasksToNext');
- if (currentLevelEl)
- currentLevelEl.textContent = String(userStats.level);
- if (totalCompletedEl)
- totalCompletedEl.textContent = String(completedTasksCount);
- if (tasksToNextEl)
- tasksToNextEl.textContent = String(tasksToNext);
- }
- saveTasksPerLevel() {
- const tasksPerLevel = parseInt(document.getElementById('tasksPerLevel').value);
- if (isNaN(tasksPerLevel) || tasksPerLevel < 1) {
- alert('Please enter a valid number (minimum 1)');
- return;
- }
- storage.updateSettings({ tasksPerLevel });
- alert('Settings saved! Level has been recalculated.');
- this.updateSettingsStatus();
- this.renderDashboard();
- }
- exportData() {
- const data = storage.exportData();
- const blob = new Blob([data], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `task-manager-backup-${new Date().toISOString().split('T')[0]}.json`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }
- importData(e) {
- const file = e.target.files?.[0];
- if (!file)
- return;
- const reader = new FileReader();
- reader.onload = (event) => {
- try {
- if (storage.importData(event.target.result)) {
- alert('Data imported successfully! Refreshing...');
- location.reload();
- }
- else {
- alert('Invalid file format. Please upload a valid Task Manager backup.');
- }
- }
- catch (error) {
- alert('Error importing file: ' + error.message);
- }
- };
- reader.readAsText(file);
- }
- // ========================
- // Recurring Tasks Processing
- // ========================
- processRecurringTasks() {
- // New recurring tasks are created immediately when a repeatable task is completed
- // via createNextRecurringTask(). The original completed task stays in history.
- }
- // ========================
- // General Rendering
- // ========================
- render() {
- document.getElementById('dataVersion').textContent = STORAGE_VERSION;
- const lastUpdated = storage.getData().lastUpdated;
- document.getElementById('lastUpdated').textContent = lastUpdated ? new Date(lastUpdated).toLocaleString() : 'Never';
- this.updateDateNavigator();
- this.renderDashboard();
- }
- // ========================
- // Date Navigation
- // ========================
- getSelectedDateStr() {
- return storage.formatDate(this.selectedDate);
- }
- isSelectedDateToday() {
- return this.getSelectedDateStr() === storage.formatDate(new Date());
- }
- navigateDate(delta) {
- const today = new Date();
- today.setHours(0, 0, 0, 0);
- const minDate = new Date(today);
- minDate.setDate(minDate.getDate() - 6);
- const newDate = new Date(this.selectedDate);
- newDate.setDate(newDate.getDate() + delta);
- newDate.setHours(0, 0, 0, 0);
- if (newDate >= minDate && newDate <= today) {
- this.selectedDate = newDate;
- this.updateDateNavigator();
- const activeTab = document.querySelector('.nav-tab.active');
- if (activeTab) {
- this.switchTab(activeTab.dataset.tab);
- }
- else {
- this.renderDashboard();
- }
- }
- }
- formatDisplayDate(date) {
- const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
- const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
- return `${days[date.getDay()]}, ${months[date.getMonth()]} ${date.getDate()}`;
- }
- updateDateNavigator() {
- const today = new Date();
- today.setHours(0, 0, 0, 0);
- const minDate = new Date(today);
- minDate.setDate(minDate.getDate() - 6);
- const sel = new Date(this.selectedDate);
- sel.setHours(0, 0, 0, 0);
- const isToday = sel.getTime() === today.getTime();
- const isPastLimit = sel.getTime() <= minDate.getTime();
- const displayStr = isToday
- ? `Today โ ${this.formatDisplayDate(this.selectedDate)}`
- : this.formatDisplayDate(this.selectedDate);
- document.getElementById('selectedDateDisplay').textContent = displayStr;
- document.getElementById('prevDayBtn').disabled = isPastLimit;
- document.getElementById('nextDayBtn').disabled = isToday;
- document.getElementById('goTodayBtn').style.display = isToday ? 'none' : 'inline-block';
- }
-}
-// Initialize the app when DOM is ready
-document.addEventListener('DOMContentLoaded', () => {
- window.app = new TaskManager();
-});
-export { TaskManager };
-//# sourceMappingURL=app.js.map
\ No newline at end of file
diff --git a/js/storage.d.ts.map b/js/storage.d.ts.map
deleted file mode 100644
index 2d3572c..0000000
--- a/js/storage.d.ts.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/js/storage.js b/js/storage.js
deleted file mode 100644
index 65bd7bb..0000000
--- a/js/storage.js
+++ /dev/null
@@ -1,710 +0,0 @@
-// ========================
-// Storage Management with Versioning
-// ========================
-const STORAGE_VERSION = '1.0.0';
-const STORAGE_KEY = 'taskManagerData';
-const DATA_SCHEMA_VERSION = 1;
-export class StorageManager {
- constructor() {
- this.initializeStorage();
- }
- initializeStorage() {
- const existingData = localStorage.getItem(STORAGE_KEY);
- if (!existingData) {
- this.createInitialData();
- }
- }
- createInitialData() {
- const initialData = {
- version: STORAGE_VERSION,
- schemaVersion: DATA_SCHEMA_VERSION,
- lastUpdated: new Date().toISOString(),
- tasks: [],
- projects: [],
- habits: [],
- dailyHabitLogs: [],
- expenses: [],
- revenue: [],
- charges: [],
- rewards: [],
- purchaseHistory: [],
- categories: ['Work', 'Personal', 'Home', 'Shopping', 'Health', 'Fitness', 'Learning', 'Productivity', 'Food', 'Transportation', 'Entertainment', 'Utilities', 'Income'],
- userStats: {
- totalPoints: 0,
- level: 1,
- dailyStreak: 0,
- lastActivityDate: null,
- pointsBreakdown: {
- tasks: 0,
- projects: 0,
- habits: 0,
- streakBonus: 0
- }
- },
- settings: {
- tasksPerLevel: 30
- },
- wishList: [],
- notes: []
- };
- localStorage.setItem(STORAGE_KEY, JSON.stringify(initialData));
- }
- getData() {
- const data = localStorage.getItem(STORAGE_KEY);
- return data ? JSON.parse(data) : this.getDefaultData();
- }
- getDefaultData() {
- this.createInitialData();
- return JSON.parse(localStorage.getItem(STORAGE_KEY));
- }
- saveData(data) {
- data.lastUpdated = new Date().toISOString();
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
- }
- // Task Management
- addTask(task) {
- const data = this.getData();
- const newTask = {
- ...task,
- id: this.generateId(),
- createdDate: new Date().toISOString(),
- completed: false,
- title: task.title || '',
- priority: task.priority || 'medium',
- points: task.points || 10,
- repeatType: task.repeatType || 'none',
- };
- data.tasks.push(newTask);
- this.saveData(data);
- return newTask;
- }
- updateTask(taskId, updates) {
- const data = this.getData();
- const task = data.tasks.find(t => t.id === taskId);
- if (task) {
- Object.assign(task, updates);
- this.saveData(data);
- }
- return task;
- }
- deleteTask(taskId) {
- const data = this.getData();
- data.tasks = data.tasks.filter(t => t.id !== taskId);
- this.saveData(data);
- }
- getTasks() {
- const data = this.getData();
- return data.tasks || [];
- }
- // Project Management
- addProject(project) {
- const data = this.getData();
- const newProject = {
- ...project,
- id: this.generateId(),
- createdDate: new Date().toISOString(),
- name: project.name || '',
- };
- data.projects.push(newProject);
- this.saveData(data);
- return newProject;
- }
- updateProject(projectId, updates) {
- const data = this.getData();
- const project = data.projects.find(p => p.id === projectId);
- if (project) {
- Object.assign(project, updates);
- this.saveData(data);
- }
- return project;
- }
- deleteProject(projectId) {
- const data = this.getData();
- data.projects = data.projects.filter(p => p.id !== projectId);
- // Remove project from all tasks
- data.tasks = data.tasks.map(t => {
- if (t.projectId === projectId) {
- t.projectId = null;
- }
- return t;
- });
- this.saveData(data);
- }
- getProjects() {
- const data = this.getData();
- return data.projects || [];
- }
- // Habit Management
- addHabit(habit) {
- const data = this.getData();
- const newHabit = {
- ...habit,
- id: this.generateId(),
- createdDate: new Date().toISOString(),
- streak: 0,
- lastCompletedDate: null,
- targetGoal: habit.targetGoal || 1,
- name: habit.name || '',
- icon: habit.icon || 'โญ',
- points: habit.points || 10,
- };
- data.habits.push(newHabit);
- this.saveData(data);
- return newHabit;
- }
- updateHabit(habitId, updates) {
- const data = this.getData();
- const habit = data.habits.find(h => h.id === habitId);
- if (habit) {
- Object.assign(habit, updates);
- if (!habit.targetGoal)
- habit.targetGoal = 1;
- this.saveData(data);
- }
- return habit;
- }
- deleteHabit(habitId) {
- const data = this.getData();
- data.habits = data.habits.filter(h => h.id !== habitId);
- this.saveData(data);
- }
- getHabits() {
- const data = this.getData();
- return data.habits || [];
- }
- logHabitCompletion(habitId, date = new Date()) {
- const data = this.getData();
- const dateStr = this.formatDate(date);
- data.dailyHabitLogs.push({
- id: this.generateId(),
- habitId,
- date: dateStr,
- timestamp: new Date().toISOString()
- });
- // Update habit streak based on fully-completed consecutive days
- const habit = data.habits.find(h => h.id === habitId);
- if (habit) {
- habit.lastCompletedDate = dateStr;
- habit.streak = this.calculateHabitStreak(habitId, habit.targetGoal, data.dailyHabitLogs);
- }
- this.saveData(data);
- }
- calculateHabitStreak(habitId, targetGoal, logs) {
- // Count completions per date for this habit
- const habitLogs = logs.filter(l => l.habitId === habitId);
- const countsByDate = {};
- for (const log of habitLogs) {
- countsByDate[log.date] = (countsByDate[log.date] || 0) + 1;
- }
- // Get dates where fully completed (>= targetGoal), sorted most recent first
- const completedDates = Object.keys(countsByDate)
- .filter(date => countsByDate[date] >= targetGoal)
- .sort()
- .reverse();
- if (completedDates.length === 0)
- return 0;
- // Count consecutive days going backward from the most recent fully-completed day
- let streak = 1;
- let currentDate = completedDates[0];
- for (let i = 1; i < completedDates.length; i++) {
- const [year, month, day] = currentDate.split('-').map(Number);
- const prevDay = new Date(year, month - 1, day);
- prevDay.setDate(prevDay.getDate() - 1);
- const expectedDate = this.formatDate(prevDay);
- if (completedDates[i] === expectedDate) {
- streak++;
- currentDate = completedDates[i];
- }
- else {
- break;
- }
- }
- return streak;
- }
- isHabitCompletedToday(habitId) {
- const data = this.getData();
- const todayStr = this.formatDate(new Date());
- return data.dailyHabitLogs.some(log => log.habitId === habitId && log.date === todayStr);
- }
- countHabitCompletionsToday(habitId) {
- const data = this.getData();
- const todayStr = this.formatDate(new Date());
- return data.dailyHabitLogs.filter(log => log.habitId === habitId && log.date === todayStr).length;
- }
- countHabitCompletionsForDate(habitId, dateStr) {
- const data = this.getData();
- return data.dailyHabitLogs.filter(log => log.habitId === habitId && log.date === dateStr).length;
- }
- // Finance Management
- addExpense(expense) {
- const data = this.getData();
- const newExpense = {
- ...expense,
- id: this.generateId(),
- createdDate: new Date().toISOString(),
- description: expense.description || '',
- amount: expense.amount || 0,
- };
- data.expenses.push(newExpense);
- this.saveData(data);
- return newExpense;
- }
- updateExpense(expenseId, updates) {
- const data = this.getData();
- const expense = data.expenses.find(e => e.id === expenseId);
- if (expense) {
- Object.assign(expense, updates);
- this.saveData(data);
- }
- return expense;
- }
- deleteExpense(expenseId) {
- const data = this.getData();
- data.expenses = data.expenses.filter(e => e.id !== expenseId);
- this.saveData(data);
- }
- getExpenses() {
- const data = this.getData();
- return data.expenses || [];
- }
- addRevenue(revenue) {
- const data = this.getData();
- const newRevenue = {
- ...revenue,
- id: this.generateId(),
- createdDate: new Date().toISOString(),
- description: revenue.description || '',
- amount: revenue.amount || 0,
- };
- data.revenue.push(newRevenue);
- this.saveData(data);
- return newRevenue;
- }
- updateRevenue(revenueId, updates) {
- const data = this.getData();
- const item = data.revenue.find(r => r.id === revenueId);
- if (item) {
- Object.assign(item, updates);
- this.saveData(data);
- }
- return item;
- }
- deleteRevenue(revenueId) {
- const data = this.getData();
- data.revenue = data.revenue.filter(r => r.id !== revenueId);
- this.saveData(data);
- }
- getRevenue() {
- const data = this.getData();
- return data.revenue || [];
- }
- // Other Charges Management
- addCharge(charge) {
- const data = this.getData();
- if (!data.charges) {
- data.charges = [];
- }
- const newCharge = {
- ...charge,
- id: this.generateId(),
- createdDate: new Date().toISOString(),
- description: charge.description || '',
- amount: charge.amount || 0,
- };
- data.charges.push(newCharge);
- this.saveData(data);
- return newCharge;
- }
- updateCharge(chargeId, updates) {
- const data = this.getData();
- if (!data.charges) {
- data.charges = [];
- }
- const charge = data.charges.find(c => c.id === chargeId);
- if (charge) {
- Object.assign(charge, updates);
- this.saveData(data);
- }
- return charge;
- }
- deleteCharge(chargeId) {
- const data = this.getData();
- if (!data.charges) {
- data.charges = [];
- }
- data.charges = data.charges.filter(c => c.id !== chargeId);
- this.saveData(data);
- }
- getCharges() {
- const data = this.getData();
- return data.charges || [];
- }
- // Rewards Shop Management
- addReward(reward) {
- const data = this.getData();
- if (!data.rewards) {
- data.rewards = [];
- }
- const newReward = {
- ...reward,
- id: this.generateId(),
- createdDate: new Date().toISOString(),
- purchased: false,
- repeatable: typeof reward.repeatable === 'undefined' ? true : reward.repeatable,
- name: reward.name || '',
- cost: reward.cost || 0,
- };
- data.rewards.push(newReward);
- this.saveData(data);
- return newReward;
- }
- updateReward(rewardId, updates) {
- const data = this.getData();
- if (!data.rewards) {
- data.rewards = [];
- }
- const reward = data.rewards.find(r => r.id === rewardId);
- if (reward) {
- Object.assign(reward, updates);
- if (typeof reward.repeatable === 'undefined')
- reward.repeatable = true;
- this.saveData(data);
- }
- return reward;
- }
- deleteReward(rewardId) {
- const data = this.getData();
- if (!data.rewards) {
- data.rewards = [];
- }
- data.rewards = data.rewards.filter(r => r.id !== rewardId);
- this.saveData(data);
- }
- getRewards() {
- const data = this.getData();
- return data.rewards || [];
- }
- purchaseReward(rewardId) {
- const data = this.getData();
- if (!data.rewards) {
- data.rewards = [];
- }
- if (!data.purchaseHistory) {
- data.purchaseHistory = [];
- }
- const reward = data.rewards.find(r => r.id === rewardId);
- if (!reward) {
- return { success: false, message: 'Reward not found' };
- }
- if (data.userStats.totalPoints < reward.cost) {
- return { success: false, message: 'Not enough points' };
- }
- // If one-time and already purchased, block
- if (reward.repeatable === false) {
- const alreadyPurchased = data.purchaseHistory.some(ph => ph.rewardId === reward.id);
- if (alreadyPurchased) {
- return { success: false, message: 'This reward can only be purchased once.' };
- }
- }
- // Deduct points
- data.userStats.totalPoints -= reward.cost;
- // Add to purchase history
- const purchase = {
- id: this.generateId(),
- rewardId: reward.id,
- rewardName: reward.name,
- rewardDescription: reward.description,
- cost: reward.cost,
- purchaseDate: new Date().toISOString()
- };
- data.purchaseHistory.push(purchase);
- this.saveData(data);
- return { success: true, purchase };
- }
- getPurchaseHistory() {
- const data = this.getData();
- return data.purchaseHistory || [];
- }
- // Wish List Management
- addWishItem(item) {
- const data = this.getData();
- if (!data.wishList)
- data.wishList = [];
- const newItem = {
- ...item,
- id: this.generateId(),
- createdDate: new Date().toISOString(),
- title: item.title || '',
- order: data.wishList.length,
- };
- data.wishList.push(newItem);
- this.saveData(data);
- return newItem;
- }
- updateWishItem(itemId, updates) {
- const data = this.getData();
- if (!data.wishList)
- data.wishList = [];
- const item = data.wishList.find(w => w.id === itemId);
- if (item) {
- Object.assign(item, updates);
- this.saveData(data);
- }
- return item;
- }
- deleteWishItem(itemId) {
- const data = this.getData();
- if (!data.wishList)
- data.wishList = [];
- data.wishList = data.wishList.filter(w => w.id !== itemId);
- // Re-index order values
- data.wishList.forEach((w, idx) => { w.order = idx; });
- this.saveData(data);
- }
- getWishItems() {
- const data = this.getData();
- if (!data.wishList)
- return [];
- return data.wishList.slice().sort((a, b) => a.order - b.order);
- }
- reorderWishItems(orderedIds) {
- const data = this.getData();
- if (!data.wishList)
- return;
- orderedIds.forEach((id, idx) => {
- const item = data.wishList.find(w => w.id === id);
- if (item)
- item.order = idx;
- });
- this.saveData(data);
- }
- // Note Management
- addNote(note) {
- const data = this.getData();
- if (!data.notes)
- data.notes = [];
- const newNote = {
- id: this.generateId(),
- title: note.title || '',
- content: note.content || '',
- createdDate: new Date().toISOString(),
- };
- data.notes.push(newNote);
- this.saveData(data);
- return newNote;
- }
- updateNote(noteId, updates) {
- const data = this.getData();
- if (!data.notes)
- data.notes = [];
- const note = data.notes.find(n => n.id === noteId);
- if (note) {
- Object.assign(note, updates);
- note.updatedDate = new Date().toISOString();
- this.saveData(data);
- }
- return note;
- }
- deleteNote(noteId) {
- const data = this.getData();
- if (!data.notes)
- data.notes = [];
- data.notes = data.notes.filter(n => n.id !== noteId);
- this.saveData(data);
- }
- getNotes() {
- const data = this.getData();
- if (!data.notes)
- return [];
- return data.notes.slice().sort((a, b) => new Date(b.updatedDate ?? b.createdDate).getTime() - new Date(a.updatedDate ?? a.createdDate).getTime());
- }
- // Points Management
- addPoints(amount, source) {
- const data = this.getData();
- data.userStats.totalPoints += amount;
- if (data.userStats.pointsBreakdown[source] !== undefined) {
- data.userStats.pointsBreakdown[source] += amount;
- }
- // Level is now calculated based on completed tasks, not points
- this.updateLevel();
- this.saveData(data);
- }
- updateLevel() {
- const data = this.getData();
- const settings = this.getSettings();
- const completedTasksCount = data.tasks.filter(t => t.completed).length;
- data.userStats.level = Math.floor(completedTasksCount / settings.tasksPerLevel) + 1;
- this.saveData(data);
- }
- updateDailyStreak(increment = true) {
- const data = this.getData();
- const today = this.formatDate(new Date());
- if (increment) {
- if (data.userStats.lastActivityDate !== today) {
- const yesterday = new Date();
- yesterday.setDate(yesterday.getDate() - 1);
- if (data.userStats.lastActivityDate !== this.formatDate(yesterday)) {
- data.userStats.dailyStreak = 1;
- }
- else {
- data.userStats.dailyStreak += 1;
- }
- }
- }
- data.userStats.lastActivityDate = today;
- this.saveData(data);
- }
- getUserStats() {
- const data = this.getData();
- return data.userStats;
- }
- // Utility Methods
- generateId() {
- return Date.now().toString(36) + Math.random().toString(36).substr(2);
- }
- formatDate(date) {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- return `${year}-${month}-${day}`;
- }
- exportData() {
- const data = this.getData();
- return JSON.stringify(data, null, 2);
- }
- importData(jsonString) {
- try {
- const data = JSON.parse(jsonString);
- // Validate that it has the required structure
- if (data.version && data.tasks !== undefined && data.projects !== undefined) {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
- return true;
- }
- return false;
- }
- catch (e) {
- console.error('Import error:', e);
- return false;
- }
- }
- clearAllData() {
- if (confirm('Are you sure you want to clear all data? This cannot be undone.')) {
- localStorage.removeItem(STORAGE_KEY);
- this.createInitialData();
- return true;
- }
- return false;
- }
- // ========================
- // Category Management
- // ========================
- getCategories() {
- const data = this.getData();
- // Migrate old per-type structure to a single shared array
- if (data.categories && !Array.isArray(data.categories)) {
- const catObj = data.categories;
- const merged = [...new Set([
- ...(catObj['tasks'] || []),
- ...(catObj['habits'] || []),
- ...(catObj['finance'] || [])
- ])];
- data.categories = merged;
- this.saveData(data);
- }
- return Array.isArray(data.categories) ? data.categories : [];
- }
- addCategory(categoryName) {
- const data = this.getData();
- if (!Array.isArray(data.categories))
- data.categories = [];
- const trimmedName = categoryName.trim();
- if (trimmedName && !data.categories.includes(trimmedName)) {
- data.categories.push(trimmedName);
- this.saveData(data);
- return true;
- }
- return false;
- }
- updateCategory(oldName, newName) {
- const data = this.getData();
- if (!Array.isArray(data.categories))
- return false;
- const trimmedNew = newName.trim();
- if (!trimmedNew || data.categories.includes(trimmedNew))
- return false;
- const idx = data.categories.indexOf(oldName);
- if (idx === -1)
- return false;
- data.categories[idx] = trimmedNew;
- // Propagate rename to all item types
- data.tasks = data.tasks.map(t => t.category === oldName ? { ...t, category: trimmedNew } : t);
- data.habits = data.habits.map(h => h.category === oldName ? { ...h, category: trimmedNew } : h);
- data.expenses = data.expenses.map(e => e.category === oldName ? { ...e, category: trimmedNew } : e);
- data.revenue = data.revenue.map(r => r.category === oldName ? { ...r, category: trimmedNew } : r);
- if (data.charges) {
- data.charges = data.charges.map(c => c.category === oldName ? { ...c, category: trimmedNew } : c);
- }
- this.saveData(data);
- return true;
- }
- deleteCategory(categoryName) {
- const data = this.getData();
- if (!Array.isArray(data.categories))
- return false;
- const idx = data.categories.indexOf(categoryName);
- if (idx === -1)
- return false;
- data.categories.splice(idx, 1);
- // Clear category from all item types
- data.tasks = data.tasks.map(t => t.category === categoryName ? { ...t, category: null } : t);
- data.habits = data.habits.map(h => h.category === categoryName ? { ...h, category: null } : h);
- data.expenses = data.expenses.map(e => e.category === categoryName ? { ...e, category: null } : e);
- data.revenue = data.revenue.map(r => r.category === categoryName ? { ...r, category: null } : r);
- if (data.charges) {
- data.charges = data.charges.map(c => c.category === categoryName ? { ...c, category: null } : c);
- }
- this.saveData(data);
- return true;
- }
- // ========================
- // Settings Management
- // ========================
- getSettings() {
- const data = this.getData();
- // Ensure settings exist with defaults
- if (!data.settings) {
- data.settings = {
- tasksPerLevel: 30
- };
- this.saveData(data);
- }
- return data.settings;
- }
- updateSettings(settings) {
- const data = this.getData();
- if (!data.settings) {
- data.settings = { tasksPerLevel: 30 };
- }
- Object.assign(data.settings, settings);
- // Recalculate level with new settings
- this.updateLevel();
- this.saveData(data);
- }
-}
-// Initialize global storage manager
-const storage = new StorageManager();
-export { storage, STORAGE_VERSION, STORAGE_KEY };
-export function getDaysUntilDueText(dueDate) {
- const todayStr = storage.formatDate(new Date());
- const todayMs = new Date(todayStr + 'T00:00:00').getTime();
- const dueMs = new Date(dueDate + 'T00:00:00').getTime();
- const days = Math.round((dueMs - todayMs) / (1000 * 60 * 60 * 24));
- if (days < 0)
- return 'Overdue';
- if (days === 0)
- return 'Today';
- if (days === 1)
- return '1 day';
- return `${days} days`;
-}
-//# sourceMappingURL=storage.js.map
\ No newline at end of file
diff --git a/service-worker.js b/service-worker.js
index 0184fef..5856b8b 100644
--- a/service-worker.js
+++ b/service-worker.js
@@ -1,4 +1,4 @@
-const CACHE_NAME = 'task-manager-va6ba90a';
+const CACHE_NAME = 'task-manager-v3347c71';
const urlsToCache = [
'/',
'/index.html',
diff --git a/src/app.ts b/src/app.ts
index ac5f565..83951e3 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -4,6 +4,8 @@
import { StorageManager, storage, STORAGE_VERSION, Task, Habit, FinanceItem, WishItem, Note, getDaysUntilDueText } from './storage.js';
+const FILTER_SETTINGS_KEY = 'taskManagerFilterSettings';
+
interface Activity {
type: string;
message: string;
@@ -59,6 +61,7 @@ class TaskManager {
this.setupEventListeners();
this.initializeFinanceDateFilter();
this.updateDateNavigator();
+ this.loadFilterSettings();
this.render();
this.processRecurringTasks();
}
@@ -82,11 +85,12 @@ class TaskManager {
document.getElementById('taskCategory')!.addEventListener('change', (e) => this.handleCategoryChange('task', (e.target as HTMLSelectElement).value));
document.getElementById('taskCategorySave')!.addEventListener('click', () => this.handleAddCategory('task'));
document.getElementById('taskCategoryCancel')!.addEventListener('click', () => this.cancelAddCategory('task'));
- document.getElementById('categoryFilter')!.addEventListener('change', () => this.filterTasks());
- document.getElementById('statusFilter')!.addEventListener('change', () => this.filterTasks());
- document.getElementById('groupBySelect')!.addEventListener('change', () => this.filterTasks());
+ document.getElementById('categoryFilter')!.addEventListener('change', () => { this.filterTasks(); this.saveFilterSettings(); });
+ document.getElementById('statusFilter')!.addEventListener('change', () => { this.filterTasks(); this.saveFilterSettings(); });
+ document.getElementById('groupBySelect')!.addEventListener('change', () => { this.filterTasks(); this.saveFilterSettings(); });
document.getElementById('searchTasks')!.addEventListener('input', () => this.filterTasks());
document.getElementById('hideCompletedBtn')!.addEventListener('click', () => this.toggleHideCompleted());
+ document.getElementById('resetFiltersBtn')!.addEventListener('click', () => this.resetFilters());
// Projects section
document.getElementById('addProjectBtn')!.addEventListener('click', () => this.openProjectModal());
@@ -125,6 +129,8 @@ class TaskManager {
// Finance filters
document.getElementById('filterFinancesBtn')!.addEventListener('click', () => this.renderFinances());
document.getElementById('resetFinanceFilterBtn')!.addEventListener('click', () => this.resetFinanceFilter());
+ document.getElementById('prevMonthBtn')!.addEventListener('click', () => this.navigateToPrevMonth());
+ document.getElementById('nextMonthBtn')!.addEventListener('click', () => this.navigateToNextMonth());
// Shop section
document.getElementById('addRewardBtn')!.addEventListener('click', () => this.openRewardModal());
@@ -161,6 +167,7 @@ class TaskManager {
location.reload();
}
});
+ document.getElementById('migrateBtn')!.addEventListener('click', () => this.migrateToLatest());
// Modal backdrop click
document.querySelectorAll('.modal').forEach(modal => {
@@ -207,6 +214,7 @@ class TaskManager {
document.getElementById('todayTasksItem')!.addEventListener('click', () => this.switchTab('tasks'));
document.getElementById('overdueTasksItem')!.addEventListener('click', () => {
(document.getElementById('statusFilter') as HTMLSelectElement).value = 'overdue';
+ this.saveFilterSettings();
this.switchTab('tasks');
});
@@ -432,9 +440,53 @@ class TaskManager {
// Tasks Management
toggleHideCompleted(): void {
this.hideCompleted = !this.hideCompleted;
+ this.updateHideCompletedBtn();
+ this.filterTasks();
+ this.saveFilterSettings();
+ }
+
+ updateHideCompletedBtn(): void {
const btn = document.getElementById('hideCompletedBtn')!;
btn.textContent = this.hideCompleted ? '๐ Show Completed' : '๐ Hide Completed';
btn.classList.toggle('active', this.hideCompleted);
+ }
+
+ saveFilterSettings(): void {
+ const categoryFilter = (document.getElementById('categoryFilter') as HTMLSelectElement).value;
+ const statusFilter = (document.getElementById('statusFilter') as HTMLSelectElement).value;
+ const groupBy = (document.getElementById('groupBySelect') as HTMLSelectElement).value;
+ const settings = { categoryFilter, statusFilter, groupBy, hideCompleted: this.hideCompleted };
+ localStorage.setItem(FILTER_SETTINGS_KEY, JSON.stringify(settings));
+ }
+
+ loadFilterSettings(): void {
+ const raw = localStorage.getItem(FILTER_SETTINGS_KEY);
+ if (!raw) return;
+ try {
+ const settings = JSON.parse(raw);
+ const categoryFilter = document.getElementById('categoryFilter') as HTMLSelectElement;
+ const statusFilter = document.getElementById('statusFilter') as HTMLSelectElement;
+ const groupBySelect = document.getElementById('groupBySelect') as HTMLSelectElement;
+ if (settings.categoryFilter !== undefined) categoryFilter.value = settings.categoryFilter;
+ if (settings.statusFilter !== undefined) statusFilter.value = settings.statusFilter;
+ if (settings.groupBy !== undefined) groupBySelect.value = settings.groupBy;
+ if (settings.hideCompleted) {
+ this.hideCompleted = true;
+ this.updateHideCompletedBtn();
+ }
+ } catch {
+ localStorage.removeItem(FILTER_SETTINGS_KEY);
+ }
+ }
+
+ resetFilters(): void {
+ (document.getElementById('categoryFilter') as HTMLSelectElement).value = '';
+ (document.getElementById('statusFilter') as HTMLSelectElement).value = '';
+ (document.getElementById('groupBySelect') as HTMLSelectElement).value = '';
+ (document.getElementById('searchTasks') as HTMLInputElement).value = '';
+ this.hideCompleted = false;
+ this.updateHideCompletedBtn();
+ localStorage.removeItem(FILTER_SETTINGS_KEY);
this.filterTasks();
}
@@ -1455,6 +1507,26 @@ class TaskManager {
this.renderFinances();
}
+ navigateToPrevMonth(): void {
+ const startInput = document.getElementById('financeStartDate') as HTMLInputElement;
+ const base = startInput.value ? new Date(startInput.value + 'T00:00:00') : new Date();
+ const firstDay = new Date(base.getFullYear(), base.getMonth() - 1, 1);
+ const lastDay = new Date(firstDay.getFullYear(), firstDay.getMonth() + 1, 0);
+ startInput.valueAsDate = firstDay;
+ (document.getElementById('financeEndDate') as HTMLInputElement).valueAsDate = lastDay;
+ this.renderFinances();
+ }
+
+ navigateToNextMonth(): void {
+ const startInput = document.getElementById('financeStartDate') as HTMLInputElement;
+ const base = startInput.value ? new Date(startInput.value + 'T00:00:00') : new Date();
+ const firstDay = new Date(base.getFullYear(), base.getMonth() + 1, 1);
+ const lastDay = new Date(firstDay.getFullYear(), firstDay.getMonth() + 1, 0);
+ startInput.valueAsDate = firstDay;
+ (document.getElementById('financeEndDate') as HTMLInputElement).valueAsDate = lastDay;
+ this.renderFinances();
+ }
+
getFinanceDateRange(): { startDate: string; endDate: string } {
const startDate = (document.getElementById('financeStartDate') as HTMLInputElement).value;
const endDate = (document.getElementById('financeEndDate') as HTMLInputElement).value;
@@ -2152,9 +2224,29 @@ class TaskManager {
const reader = new FileReader();
reader.onload = (event) => {
try {
- if (storage.importData(event.target!.result as string)) {
- alert('Data imported successfully! Refreshing...');
- location.reload();
+ const jsonString = event.target!.result as string;
+ const validation = storage.validateImportData(jsonString);
+
+ if (validation.isValid) {
+ if (storage.importData(jsonString)) {
+ alert('Data imported successfully! Refreshing...');
+ location.reload();
+ } else {
+ alert('Invalid file format. Please upload a valid Task Manager backup.');
+ }
+ } else if (validation.hasPartialData && validation.parsed) {
+ const issueList = validation.issues.join('\n - ');
+ const proceed = confirm(
+ `Warning: The imported file has invalid or missing data:\n - ${issueList}\n\nWould you like to migrate the valid data to the latest format? Missing fields will be set to defaults.`
+ );
+ if (proceed) {
+ if (storage.migrateAndImport(validation.parsed)) {
+ alert('Data migrated and imported successfully! Refreshing...');
+ location.reload();
+ } else {
+ alert('Error during migration. Import cancelled.');
+ }
+ }
} else {
alert('Invalid file format. Please upload a valid Task Manager backup.');
}
@@ -2165,6 +2257,15 @@ class TaskManager {
reader.readAsText(file);
}
+ migrateToLatest(): void {
+ if (storage.migrateToLatest()) {
+ alert('Data migrated to the latest version successfully!');
+ location.reload();
+ } else {
+ alert('An error occurred during migration. Please try again.');
+ }
+ }
+
// ========================
// Recurring Tasks Processing
// ========================
diff --git a/src/storage.ts b/src/storage.ts
index 1420142..f0c033f 100644
--- a/src/storage.ts
+++ b/src/storage.ts
@@ -151,6 +151,13 @@ export interface PurchaseResult {
purchase?: Purchase;
}
+export interface ValidationResult {
+ isValid: boolean;
+ hasPartialData: boolean;
+ issues: string[];
+ parsed: Partial
| null;
+}
+
export class StorageManager {
constructor() {
this.initializeStorage();
@@ -796,6 +803,97 @@ export class StorageManager {
}
}
+ validateImportData(jsonString: string): ValidationResult {
+ try {
+ const data = JSON.parse(jsonString) as Partial;
+ const issues: string[] = [];
+
+ if (!data.version) issues.push('Missing version field');
+ if (data.tasks === undefined) issues.push('Missing tasks field');
+ else if (!Array.isArray(data.tasks)) issues.push('Tasks is not an array');
+ if (data.projects === undefined) issues.push('Missing projects field');
+ else if (!Array.isArray(data.projects)) issues.push('Projects is not an array');
+
+ const isValid = issues.length === 0;
+ const hasPartialData = !isValid && typeof data === 'object' && data !== null &&
+ (data.tasks !== undefined || data.projects !== undefined || data.version !== undefined ||
+ data.habits !== undefined || data.userStats !== undefined || data.settings !== undefined);
+
+ return { isValid, hasPartialData, issues, parsed: data };
+ } catch (e) {
+ return { isValid: false, hasPartialData: false, issues: ['Invalid JSON format'], parsed: null };
+ }
+ }
+
+ private buildMigratedData(data: Partial): AppData {
+ const now = new Date().toISOString();
+
+ // Handle category migration (old per-type structure โ flat array)
+ let categories: string[];
+ if (data.categories && !Array.isArray(data.categories)) {
+ const catObj = data.categories as unknown as Record;
+ categories = [...new Set([
+ ...(catObj['tasks'] || []),
+ ...(catObj['habits'] || []),
+ ...(catObj['finance'] || [])
+ ])];
+ } else if (Array.isArray(data.categories) && data.categories.length > 0) {
+ categories = data.categories;
+ } else {
+ categories = ['Work', 'Personal', 'Home', 'Shopping', 'Health', 'Fitness', 'Learning',
+ 'Productivity', 'Food', 'Transportation', 'Entertainment', 'Utilities', 'Income'];
+ }
+
+ return {
+ version: STORAGE_VERSION,
+ schemaVersion: DATA_SCHEMA_VERSION,
+ lastUpdated: now,
+ tasks: Array.isArray(data.tasks) ? data.tasks : [],
+ projects: Array.isArray(data.projects) ? data.projects : [],
+ habits: Array.isArray(data.habits) ? data.habits : [],
+ dailyHabitLogs: Array.isArray(data.dailyHabitLogs) ? data.dailyHabitLogs : [],
+ expenses: Array.isArray(data.expenses) ? data.expenses : [],
+ revenue: Array.isArray(data.revenue) ? data.revenue : [],
+ charges: Array.isArray(data.charges) ? data.charges : [],
+ rewards: Array.isArray(data.rewards) ? data.rewards : [],
+ purchaseHistory: Array.isArray(data.purchaseHistory) ? data.purchaseHistory : [],
+ categories,
+ userStats: data.userStats || {
+ totalPoints: 0,
+ level: 1,
+ dailyStreak: 0,
+ lastActivityDate: null,
+ pointsBreakdown: { tasks: 0, projects: 0, habits: 0, streakBonus: 0 }
+ },
+ settings: data.settings || { tasksPerLevel: 30 },
+ wishList: Array.isArray(data.wishList) ? data.wishList : [],
+ notes: Array.isArray(data.notes) ? data.notes : [],
+ };
+ }
+
+ migrateAndImport(data: Partial): boolean {
+ try {
+ const migrated = this.buildMigratedData(data);
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(migrated));
+ return true;
+ } catch (e) {
+ console.error('Migration error:', e);
+ return false;
+ }
+ }
+
+ migrateToLatest(): boolean {
+ try {
+ const data = this.getData();
+ const migrated = this.buildMigratedData(data);
+ this.saveData(migrated);
+ return true;
+ } catch (e) {
+ console.error('Migration error:', e);
+ return false;
+ }
+ }
+
clearAllData(): boolean {
if (confirm('Are you sure you want to clear all data? This cannot be undone.')) {
localStorage.removeItem(STORAGE_KEY);
diff --git a/tests/storage.test.ts b/tests/storage.test.ts
index 398b97b..678dbcb 100644
--- a/tests/storage.test.ts
+++ b/tests/storage.test.ts
@@ -525,6 +525,146 @@ describe('StorageManager', () => {
});
});
+ // ========================
+ // validateImportData
+ // ========================
+ describe('validateImportData', () => {
+ it('should return isValid true for fully valid data', () => {
+ const data = {
+ version: '1.0.0',
+ tasks: [],
+ projects: [],
+ };
+ const result = storage.validateImportData(JSON.stringify(data));
+ expect(result.isValid).toBe(true);
+ expect(result.hasPartialData).toBe(false);
+ expect(result.issues).toHaveLength(0);
+ expect(result.parsed).not.toBeNull();
+ });
+
+ it('should flag missing version as invalid with partial data', () => {
+ const data = { tasks: [], projects: [] };
+ const result = storage.validateImportData(JSON.stringify(data));
+ expect(result.isValid).toBe(false);
+ expect(result.hasPartialData).toBe(true);
+ expect(result.issues).toContain('Missing version field');
+ });
+
+ it('should flag missing tasks as invalid with partial data', () => {
+ const data = { version: '1.0.0', projects: [] };
+ const result = storage.validateImportData(JSON.stringify(data));
+ expect(result.isValid).toBe(false);
+ expect(result.hasPartialData).toBe(true);
+ expect(result.issues).toContain('Missing tasks field');
+ });
+
+ it('should flag missing projects as invalid with partial data', () => {
+ const data = { version: '1.0.0', tasks: [] };
+ const result = storage.validateImportData(JSON.stringify(data));
+ expect(result.isValid).toBe(false);
+ expect(result.hasPartialData).toBe(true);
+ expect(result.issues).toContain('Missing projects field');
+ });
+
+ it('should return invalid and no partial data for completely unrecognized object', () => {
+ const data = { foo: 'bar' };
+ const result = storage.validateImportData(JSON.stringify(data));
+ expect(result.isValid).toBe(false);
+ expect(result.hasPartialData).toBe(false);
+ });
+
+ it('should return invalid for malformed JSON', () => {
+ const result = storage.validateImportData('not json');
+ expect(result.isValid).toBe(false);
+ expect(result.hasPartialData).toBe(false);
+ expect(result.issues).toContain('Invalid JSON format');
+ expect(result.parsed).toBeNull();
+ });
+
+ it('should flag non-array tasks as invalid', () => {
+ const data = { version: '1.0.0', tasks: 'oops', projects: [] };
+ const result = storage.validateImportData(JSON.stringify(data));
+ expect(result.isValid).toBe(false);
+ expect(result.issues).toContain('Tasks is not an array');
+ });
+ });
+
+ // ========================
+ // migrateAndImport
+ // ========================
+ describe('migrateAndImport', () => {
+ it('should import and fill in missing fields with defaults', () => {
+ const partial = { tasks: [{ id: '1', title: 'Task', completed: false }] };
+ const result = storage.migrateAndImport(partial);
+ expect(result).toBe(true);
+ const data = storage.getData();
+ expect(data.version).toBe('1.0.0');
+ expect(data.tasks).toHaveLength(1);
+ expect(Array.isArray(data.projects)).toBe(true);
+ expect(Array.isArray(data.habits)).toBe(true);
+ expect(data.settings.tasksPerLevel).toBe(30);
+ });
+
+ it('should migrate old per-type category structure to flat array', () => {
+ const partial = {
+ tasks: [],
+ projects: [],
+ categories: { tasks: ['Work', 'Personal'], habits: ['Fitness'], finance: ['Income'] }
+ };
+ const result = storage.migrateAndImport(partial as any);
+ expect(result).toBe(true);
+ const categories = storage.getCategories();
+ expect(categories).toContain('Work');
+ expect(categories).toContain('Fitness');
+ expect(categories).toContain('Income');
+ });
+
+ it('should preserve existing array categories', () => {
+ const partial = {
+ tasks: [],
+ projects: [],
+ categories: ['Custom1', 'Custom2']
+ };
+ const result = storage.migrateAndImport(partial);
+ expect(result).toBe(true);
+ const categories = storage.getCategories();
+ expect(categories).toContain('Custom1');
+ expect(categories).toContain('Custom2');
+ });
+ });
+
+ // ========================
+ // migrateToLatest
+ // ========================
+ describe('migrateToLatest', () => {
+ it('should return true and keep valid data intact', () => {
+ storage.addTask({ title: 'Keep me' });
+ const result = storage.migrateToLatest();
+ expect(result).toBe(true);
+ expect(storage.getTasks()).toHaveLength(1);
+ expect(storage.getTasks()[0].title).toBe('Keep me');
+ });
+
+ it('should update the version to latest', () => {
+ const result = storage.migrateToLatest();
+ expect(result).toBe(true);
+ expect(storage.getData().version).toBe('1.0.0');
+ });
+
+ it('should add missing fields when migrating stored data', () => {
+ // Simulate stored data missing the notes field
+ const rawData = JSON.parse(localStorage.getItem('taskManagerData')!);
+ delete rawData.notes;
+ delete rawData.wishList;
+ localStorage.setItem('taskManagerData', JSON.stringify(rawData));
+
+ const result = storage.migrateToLatest();
+ expect(result).toBe(true);
+ expect(Array.isArray(storage.getData().notes)).toBe(true);
+ expect(Array.isArray(storage.getData().wishList)).toBe(true);
+ });
+ });
+
// ========================
// Utility Methods
// ========================