From 1648f639885ef74d8e7f15648e5e7960d98d03a1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 23:27:11 +0000
Subject: [PATCH 1/3] Initial plan
From 7ba76eb6e279d580f86f24b436b5745b04775558 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 23:46:17 +0000
Subject: [PATCH 2/3] Add Migrate to Latest Version button and import
validation warning
Co-authored-by: JoeProgrammer88 <7156063+JoeProgrammer88@users.noreply.github.com>
Agent-Logs-Url: https://github.com/SpeakingInBits/TaskManagerWeb/sessions/308242b3-7d04-4e13-8fff-3c67e92587d7
---
index.html | 5 ++
js/app.d.ts.map | 2 +-
js/app.js | 36 ++++++++++-
js/storage.d.ts.map | 2 +-
js/storage.js | 92 +++++++++++++++++++++++++++
src/app.ts | 36 ++++++++++-
src/storage.ts | 98 +++++++++++++++++++++++++++++
tests/storage.test.ts | 140 ++++++++++++++++++++++++++++++++++++++++++
8 files changed, 403 insertions(+), 8 deletions(-)
diff --git a/index.html b/index.html
index 181a891..efd20c5 100644
--- a/index.html
+++ b/index.html
@@ -743,6 +743,11 @@
diff --git a/js/app.d.ts.map b/js/app.d.ts.map
index da2d55c..6615257 100644
--- a/js/app.d.ts.map
+++ b/js/app.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"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
index 5fc3ec4..d6d3250 100644
--- a/js/app.js
+++ b/js/app.js
@@ -134,6 +134,7 @@ class TaskManager {
location.reload();
}
});
+ document.getElementById('migrateBtn').addEventListener('click', () => this.migrateToLatest());
// Modal backdrop click
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
@@ -1909,9 +1910,29 @@ class TaskManager {
const reader = new FileReader();
reader.onload = (event) => {
try {
- if (storage.importData(event.target.result)) {
- alert('Data imported successfully! Refreshing...');
- location.reload();
+ const jsonString = event.target.result;
+ 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.');
@@ -1923,6 +1944,15 @@ class TaskManager {
};
reader.readAsText(file);
}
+ migrateToLatest() {
+ 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/js/storage.d.ts.map b/js/storage.d.ts.map
index 2d3572c..4435d5f 100644
--- a/js/storage.d.ts.map
+++ b/js/storage.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"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
index 65bd7bb..28241f0 100644
--- a/js/storage.js
+++ b/js/storage.js
@@ -587,6 +587,98 @@ export class StorageManager {
return false;
}
}
+ validateImportData(jsonString) {
+ try {
+ const data = JSON.parse(jsonString);
+ const issues = [];
+ 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 };
+ }
+ }
+ buildMigratedData(data) {
+ const now = new Date().toISOString();
+ // Handle category migration (old per-type structure โ flat array)
+ let categories;
+ if (data.categories && !Array.isArray(data.categories)) {
+ const catObj = data.categories;
+ 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) {
+ 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() {
+ 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() {
if (confirm('Are you sure you want to clear all data? This cannot be undone.')) {
localStorage.removeItem(STORAGE_KEY);
diff --git a/src/app.ts b/src/app.ts
index ac5f565..127a917 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -161,6 +161,7 @@ class TaskManager {
location.reload();
}
});
+ document.getElementById('migrateBtn')!.addEventListener('click', () => this.migrateToLatest());
// Modal backdrop click
document.querySelectorAll('.modal').forEach(modal => {
@@ -2152,9 +2153,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 +2186,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
// ========================
From 5200d181d96b2b4c60bbec6f3314c0eafe88faec Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 21 Mar 2026 01:32:00 +0000
Subject: [PATCH 3/3] Merge origin/main: stop tracking compiled JS output
Co-authored-by: JoeProgrammer88 <7156063+JoeProgrammer88@users.noreply.github.com>
Agent-Logs-Url: https://github.com/SpeakingInBits/TaskManagerWeb/sessions/b6ab78ca-1286-4da7-8f86-9ecede968ae8
---
.github/workflows/update-cache-version.yml | 2 +-
.gitignore | 7 +-
e2e/app.spec.ts | 64 +
index.html | 3 +
js/app.d.ts.map | 1 -
js/app.js | 2030 --------------------
js/storage.d.ts.map | 1 -
js/storage.js | 802 --------
service-worker.js | 2 +-
src/app.ts | 77 +-
10 files changed, 146 insertions(+), 2843 deletions(-)
delete mode 100644 js/app.d.ts.map
delete mode 100644 js/app.js
delete mode 100644 js/storage.d.ts.map
delete mode 100644 js/storage.js
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 efd20c5..5d6c76e 100644
--- a/index.html
+++ b/index.html
@@ -149,6 +149,7 @@ Tasks
+
@@ -476,12 +477,14 @@ No activity yet. Start completing tasks!
';
- }
- else {
- activityList.innerHTML = activities.slice(0, 5).map(activity => `
- 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 `
- 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
- ? `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 `
- No tasks in this project yet.
';
- return;
- }
- container.innerHTML = tasks.map(task => `
- 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 `
- 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' ? '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 `
- 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
- ? `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) : '