๐ Add base app
Changes
9 files changed, +777 -6
MODIFY
package-lock.json
+36 -2
@@ -9,11 +9,13 @@
9
9
"version": "1.0.0",
10
10
"dependencies": {
11
11
"@expo/vector-icons": "^15.0.3",
12
+ "@react-native-async-storage/async-storage": "^2.2.0",
12
13
"@react-navigation/bottom-tabs": "^7.4.0",
13
14
"@react-navigation/elements": "^2.6.3",
14
15
"@react-navigation/native": "^7.1.8",
15
16
"expo": "~54.0.22",
16
17
"expo-constants": "~18.0.10",
18
+ "expo-file-system": "^19.0.17",
17
19
"expo-font": "~14.0.9",
18
20
"expo-haptics": "~15.0.7",
19
21
"expo-image": "~3.0.10",
@@ -1465,7 +1467,6 @@
1465
1467
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
1466
1468
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
1467
1469
"license": "MIT",
1468
- "peer": true,
1469
1470
"engines": {
1470
1471
"node": ">=6.9.0"
1471
1472
}
@@ -2873,6 +2874,18 @@
2873
2874
}
2874
2875
}
2875
2876
},
2877
+ "node_modules/@react-native-async-storage/async-storage": {
2878
+ "version": "2.2.0",
2879
+ "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
2880
+ "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
2881
+ "license": "MIT",
2882
+ "dependencies": {
2883
+ "merge-options": "^3.0.4"
2884
+ },
2885
+ "peerDependencies": {
2886
+ "react-native": "^0.0.0-0 || >=0.65 <1.0"
2887
+ }
2888
+ },
2876
2889
"node_modules/@react-native/assets-registry": {
2877
2890
"version": "0.81.5",
2878
2891
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
@@ -5923,6 +5936,7 @@
5923
5936
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
5924
5937
"dev": true,
5925
5938
"license": "MIT",
5939
+ "peer": true,
5926
5940
"dependencies": {
5927
5941
"@rtsao/scc": "^1.1.0",
5928
5942
"array-includes": "^3.1.9",
@@ -8093,6 +8107,15 @@
8093
8107
"url": "https://github.com/sponsors/ljharb"
8094
8108
}
8095
8109
},
8110
+ "node_modules/is-plain-obj": {
8111
+ "version": "2.1.0",
8112
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
8113
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
8114
+ "license": "MIT",
8115
+ "engines": {
8116
+ "node": ">=8"
8117
+ }
8118
+ },
8096
8119
"node_modules/is-regex": {
8097
8120
"version": "1.2.1",
8098
8121
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -9085,6 +9108,18 @@
9085
9108
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
9086
9109
"license": "MIT"
9087
9110
},
9111
+ "node_modules/merge-options": {
9112
+ "version": "3.0.4",
9113
+ "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
9114
+ "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
9115
+ "license": "MIT",
9116
+ "dependencies": {
9117
+ "is-plain-obj": "^2.1.0"
9118
+ },
9119
+ "engines": {
9120
+ "node": ">=10"
9121
+ }
9122
+ },
9088
9123
"node_modules/merge-stream": {
9089
9124
"version": "2.0.0",
9090
9125
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -10587,7 +10622,6 @@
10587
10622
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz",
10588
10623
"integrity": "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg==",
10589
10624
"license": "MIT",
10590
- "peer": true,
10591
10625
"dependencies": {
10592
10626
"react-native-is-edge-to-edge": "^1.2.1",
10593
10627
"semver": "7.7.2"
MODIFY
package.json
+6 -4
@@ -12,11 +12,13 @@
12
12
},
13
13
"dependencies": {
14
14
"@expo/vector-icons": "^15.0.3",
15
+ "@react-native-async-storage/async-storage": "^2.2.0",
15
16
"@react-navigation/bottom-tabs": "^7.4.0",
16
17
"@react-navigation/elements": "^2.6.3",
17
18
"@react-navigation/native": "^7.1.8",
18
19
"expo": "~54.0.22",
19
20
"expo-constants": "~18.0.10",
21
+ "expo-file-system": "^19.0.17",
20
22
"expo-font": "~14.0.9",
21
23
"expo-haptics": "~15.0.7",
22
24
"expo-image": "~3.0.10",
@@ -31,17 +33,17 @@
31
33
"react-dom": "19.1.0",
32
34
"react-native": "0.81.5",
33
35
"react-native-gesture-handler": "~2.28.0",
34
- "react-native-worklets": "0.5.1",
35
36
"react-native-reanimated": "~4.1.1",
36
37
"react-native-safe-area-context": "~5.6.0",
37
38
"react-native-screens": "~4.16.0",
38
- "react-native-web": "~0.21.0"
39
+ "react-native-web": "~0.21.0",
40
+ "react-native-worklets": "0.5.1"
39
41
},
40
42
"devDependencies": {
41
43
"@types/react": "~19.1.0",
42
- "typescript": "~5.9.2",
43
44
"eslint": "^9.25.0",
44
- "eslint-config-expo": "~10.0.0"
45
+ "eslint-config-expo": "~10.0.0",
46
+ "typescript": "~5.9.2"
45
47
},
46
48
"private": true
47
49
}
ADD
services/index.ts
+8 -0
@@ -0,0 +1,8 @@
1
+/**
2
+ * Service exports
3
+ */
4
+
5
+export { default as storageService } from './storage.service';
6
+export { default as taskService } from './task.service';
7
+export { default as projectService } from './project.service';
8
+export { default as tagService } from './tag.service';
ADD
services/project.service.ts
+125 -0
@@ -0,0 +1,125 @@
1
+/**
2
+ * Project Service
3
+ * Business logic for project management
4
+ */
5
+
6
+import { Project } from '../types/gtd';
7
+import storageService from './storage.service';
8
+import { generateId } from '../utils/id-generator';
9
+
10
+class ProjectService {
11
+ /**
12
+ * Get all projects
13
+ */
14
+ async getAllProjects(): Promise<Project[]> {
15
+ const data = await storageService.getData();
16
+ return data.projects.filter(p => !p.archived);
17
+ }
18
+
19
+ /**
20
+ * Get archived projects
21
+ */
22
+ async getArchivedProjects(): Promise<Project[]> {
23
+ const data = await storageService.getData();
24
+ return data.projects.filter(p => p.archived);
25
+ }
26
+
27
+ /**
28
+ * Get project by ID
29
+ */
30
+ async getProjectById(projectId: string): Promise<Project | undefined> {
31
+ const data = await storageService.getData();
32
+ return data.projects.find(p => p.id === projectId);
33
+ }
34
+
35
+ /**
36
+ * Create a new project
37
+ */
38
+ async createProject(projectData: Partial<Project>): Promise<Project> {
39
+ const data = await storageService.getData();
40
+
41
+ const newProject: Project = {
42
+ id: generateId(),
43
+ name: projectData.name || '',
44
+ description: projectData.description,
45
+ goal: projectData.goal,
46
+ color: projectData.color,
47
+ archived: false,
48
+ createdAt: Date.now(),
49
+ updatedAt: Date.now(),
50
+ };
51
+
52
+ data.projects.push(newProject);
53
+ await storageService.saveData(data);
54
+
55
+ return newProject;
56
+ }
57
+
58
+ /**
59
+ * Update a project
60
+ */
61
+ async updateProject(projectId: string, updates: Partial<Project>): Promise<Project> {
62
+ const data = await storageService.getData();
63
+ const projectIndex = data.projects.findIndex(p => p.id === projectId);
64
+
65
+ if (projectIndex === -1) {
66
+ throw new Error('Project not found');
67
+ }
68
+
69
+ const updatedProject = {
70
+ ...data.projects[projectIndex],
71
+ ...updates,
72
+ id: projectId,
73
+ updatedAt: Date.now(),
74
+ };
75
+
76
+ data.projects[projectIndex] = updatedProject;
77
+ await storageService.saveData(data);
78
+
79
+ return updatedProject;
80
+ }
81
+
82
+ /**
83
+ * Archive a project
84
+ */
85
+ async archiveProject(projectId: string): Promise<Project> {
86
+ return this.updateProject(projectId, { archived: true });
87
+ }
88
+
89
+ /**
90
+ * Unarchive a project
91
+ */
92
+ async unarchiveProject(projectId: string): Promise<Project> {
93
+ return this.updateProject(projectId, { archived: false });
94
+ }
95
+
96
+ /**
97
+ * Delete a project
98
+ */
99
+ async deleteProject(projectId: string): Promise<void> {
100
+ const data = await storageService.getData();
101
+
102
+ // Remove project
103
+ data.projects = data.projects.filter(p => p.id !== projectId);
104
+
105
+ // Remove project reference from tasks
106
+ data.tasks = data.tasks.map(task => {
107
+ if (task.projectId === projectId) {
108
+ return { ...task, projectId: undefined, updatedAt: Date.now() };
109
+ }
110
+ return task;
111
+ });
112
+
113
+ await storageService.saveData(data);
114
+ }
115
+
116
+ /**
117
+ * Get task count for a project
118
+ */
119
+ async getProjectTaskCount(projectId: string): Promise<number> {
120
+ const data = await storageService.getData();
121
+ return data.tasks.filter(t => t.projectId === projectId && !t.completed).length;
122
+ }
123
+}
124
+
125
+export default new ProjectService();
ADD
services/storage.service.ts
+190 -0
@@ -0,0 +1,190 @@
1
+/**
2
+ * Storage Service
3
+ * Handles local data persistence using AsyncStorage
4
+ * Data is stored as JSON for human readability and portability
5
+ */
6
+
7
+import AsyncStorage from '@react-native-async-storage/async-storage';
8
+import * as FileSystem from 'expo-file-system';
9
+import { GTDData, Task, Project, Tag, SyncMetadata } from '../types/gtd';
10
+
11
+const STORAGE_KEY = '@taskflow_gtd_data';
12
+const STORAGE_VERSION = '1.0.0';
13
+
14
+class StorageService {
15
+ private cache: GTDData | null = null;
16
+
17
+ /**
18
+ * Initialize storage with empty data structure
19
+ */
20
+ private getEmptyData(): GTDData {
21
+ return {
22
+ tasks: [],
23
+ projects: [],
24
+ tags: [],
25
+ version: STORAGE_VERSION,
26
+ lastSync: undefined,
27
+ syncHash: undefined,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Load all data from storage
33
+ */
34
+ async loadData(): Promise<GTDData> {
35
+ try {
36
+ const jsonData = await AsyncStorage.getItem(STORAGE_KEY);
37
+
38
+ if (!jsonData) {
39
+ const emptyData = this.getEmptyData();
40
+ await this.saveData(emptyData);
41
+ this.cache = emptyData;
42
+ return emptyData;
43
+ }
44
+
45
+ const data: GTDData = JSON.parse(jsonData);
46
+ this.cache = data;
47
+ return data;
48
+ } catch (error) {
49
+ console.error('Error loading data:', error);
50
+ throw new Error('Failed to load data from storage');
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Save all data to storage
56
+ */
57
+ async saveData(data: GTDData): Promise<void> {
58
+ try {
59
+ data.version = STORAGE_VERSION;
60
+ const jsonData = JSON.stringify(data, null, 2);
61
+ await AsyncStorage.setItem(STORAGE_KEY, jsonData);
62
+ this.cache = data;
63
+ } catch (error) {
64
+ console.error('Error saving data:', error);
65
+ throw new Error('Failed to save data to storage');
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Get current data (from cache if available)
71
+ */
72
+ async getData(): Promise<GTDData> {
73
+ if (this.cache) {
74
+ return this.cache;
75
+ }
76
+ return this.loadData();
77
+ }
78
+
79
+ /**
80
+ * Export data as JSON file to device storage
81
+ */
82
+ async exportToFile(): Promise<string> {
83
+ try {
84
+ const data = await this.getData();
85
+ const jsonData = JSON.stringify(data, null, 2);
86
+
87
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
88
+ const filename = `taskflow-export-${timestamp}.json`;
89
+ const fileUri = `${FileSystem.documentDirectory}${filename}`;
90
+
91
+ await FileSystem.writeAsStringAsync(fileUri, jsonData, {
92
+ encoding: FileSystem.EncodingType.UTF8,
93
+ });
94
+
95
+ return fileUri;
96
+ } catch (error) {
97
+ console.error('Error exporting data:', error);
98
+ throw new Error('Failed to export data');
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Import data from JSON string
104
+ */
105
+ async importFromJson(jsonString: string): Promise<void> {
106
+ try {
107
+ const data: GTDData = JSON.parse(jsonString);
108
+
109
+ // Validate basic structure
110
+ if (!data.tasks || !data.projects || !data.tags) {
111
+ throw new Error('Invalid data format');
112
+ }
113
+
114
+ await this.saveData(data);
115
+ } catch (error) {
116
+ console.error('Error importing data:', error);
117
+ throw new Error('Failed to import data');
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Calculate hash for sync conflict detection
123
+ */
124
+ private calculateHash(data: GTDData): string {
125
+ const sortedData = {
126
+ tasks: [...data.tasks].sort((a, b) => a.id.localeCompare(b.id)),
127
+ projects: [...data.projects].sort((a, b) => a.id.localeCompare(b.id)),
128
+ tags: [...data.tags].sort((a, b) => a.id.localeCompare(b.id)),
129
+ };
130
+
131
+ const str = JSON.stringify(sortedData);
132
+ let hash = 0;
133
+ for (let i = 0; i < str.length; i++) {
134
+ const char = str.charCodeAt(i);
135
+ hash = ((hash << 5) - hash) + char;
136
+ hash = hash & hash;
137
+ }
138
+ return hash.toString(36);
139
+ }
140
+
141
+ /**
142
+ * Prepare sync metadata
143
+ */
144
+ async getSyncMetadata(): Promise<SyncMetadata> {
145
+ const data = await this.getData();
146
+ const hash = this.calculateHash(data);
147
+
148
+ return {
149
+ lastSyncTimestamp: data.lastSync || 0,
150
+ syncHash: hash,
151
+ conflictDetected: false,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Update sync metadata after successful sync
157
+ */
158
+ async updateSyncMetadata(): Promise<void> {
159
+ const data = await this.getData();
160
+ const hash = this.calculateHash(data);
161
+
162
+ data.lastSync = Date.now();
163
+ data.syncHash = hash;
164
+
165
+ await this.saveData(data);
166
+ }
167
+
168
+ /**
169
+ * Clear all data (for testing/reset)
170
+ */
171
+ async clearAll(): Promise<void> {
172
+ try {
173
+ await AsyncStorage.removeItem(STORAGE_KEY);
174
+ this.cache = null;
175
+ } catch (error) {
176
+ console.error('Error clearing data:', error);
177
+ throw new Error('Failed to clear data');
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Check if conflict exists with remote data
183
+ */
184
+ async detectConflict(remoteHash: string): Promise<boolean> {
185
+ const localMetadata = await this.getSyncMetadata();
186
+ return localMetadata.syncHash !== remoteHash;
187
+ }
188
+}
189
+
190
+export default new StorageService();
ADD
services/tag.service.ts
+128 -0
@@ -0,0 +1,128 @@
1
+/**
2
+ * Tag Service
3
+ * Business logic for tag management
4
+ */
5
+
6
+import { Tag } from '../types/gtd';
7
+import storageService from './storage.service';
8
+import { generateId } from '../utils/id-generator';
9
+
10
+class TagService {
11
+ /**
12
+ * Get all tags
13
+ */
14
+ async getAllTags(): Promise<Tag[]> {
15
+ const data = await storageService.getData();
16
+ return data.tags;
17
+ }
18
+
19
+ /**
20
+ * Get tag by ID
21
+ */
22
+ async getTagById(tagId: string): Promise<Tag | undefined> {
23
+ const data = await storageService.getData();
24
+ return data.tags.find(t => t.id === tagId);
25
+ }
26
+
27
+ /**
28
+ * Create a new tag
29
+ */
30
+ async createTag(tagData: Partial<Tag>): Promise<Tag> {
31
+ const data = await storageService.getData();
32
+
33
+ // Check if tag with same name already exists
34
+ const existingTag = data.tags.find(
35
+ t => t.name.toLowerCase() === tagData.name?.toLowerCase()
36
+ );
37
+
38
+ if (existingTag) {
39
+ return existingTag;
40
+ }
41
+
42
+ const newTag: Tag = {
43
+ id: generateId(),
44
+ name: tagData.name || '',
45
+ color: tagData.color,
46
+ createdAt: Date.now(),
47
+ };
48
+
49
+ data.tags.push(newTag);
50
+ await storageService.saveData(data);
51
+
52
+ return newTag;
53
+ }
54
+
55
+ /**
56
+ * Update a tag
57
+ */
58
+ async updateTag(tagId: string, updates: Partial<Tag>): Promise<Tag> {
59
+ const data = await storageService.getData();
60
+ const tagIndex = data.tags.findIndex(t => t.id === tagId);
61
+
62
+ if (tagIndex === -1) {
63
+ throw new Error('Tag not found');
64
+ }
65
+
66
+ const updatedTag = {
67
+ ...data.tags[tagIndex],
68
+ ...updates,
69
+ id: tagId,
70
+ };
71
+
72
+ data.tags[tagIndex] = updatedTag;
73
+ await storageService.saveData(data);
74
+
75
+ return updatedTag;
76
+ }
77
+
78
+ /**
79
+ * Delete a tag
80
+ */
81
+ async deleteTag(tagId: string): Promise<void> {
82
+ const data = await storageService.getData();
83
+
84
+ // Remove tag
85
+ data.tags = data.tags.filter(t => t.id !== tagId);
86
+
87
+ // Remove tag reference from tasks
88
+ data.tasks = data.tasks.map(task => {
89
+ if (task.tagIds.includes(tagId)) {
90
+ return {
91
+ ...task,
92
+ tagIds: task.tagIds.filter(id => id !== tagId),
93
+ updatedAt: Date.now(),
94
+ };
95
+ }
96
+ return task;
97
+ });
98
+
99
+ await storageService.saveData(data);
100
+ }
101
+
102
+ /**
103
+ * Get task count for a tag
104
+ */
105
+ async getTagTaskCount(tagId: string): Promise<number> {
106
+ const data = await storageService.getData();
107
+ return data.tasks.filter(t => t.tagIds.includes(tagId) && !t.completed).length;
108
+ }
109
+
110
+ /**
111
+ * Get or create tag by name
112
+ */
113
+ async getOrCreateTag(tagName: string, color?: string): Promise<Tag> {
114
+ const data = await storageService.getData();
115
+
116
+ const existingTag = data.tags.find(
117
+ t => t.name.toLowerCase() === tagName.toLowerCase()
118
+ );
119
+
120
+ if (existingTag) {
121
+ return existingTag;
122
+ }
123
+
124
+ return this.createTag({ name: tagName, color });
125
+ }
126
+}
127
+
128
+export default new TagService();
ADD
services/task.service.ts
+218 -0
@@ -0,0 +1,218 @@
1
+/**
2
+ * Task Service
3
+ * Business logic for task management
4
+ */
5
+
6
+import { Task, TaskStatus, Priority } from '../types/gtd';
7
+import storageService from './storage.service';
8
+import { generateId } from '../utils/id-generator';
9
+
10
+class TaskService {
11
+ /**
12
+ * Get all tasks
13
+ */
14
+ async getAllTasks(): Promise<Task[]> {
15
+ const data = await storageService.getData();
16
+ return data.tasks;
17
+ }
18
+
19
+ /**
20
+ * Get tasks by status
21
+ */
22
+ async getTasksByStatus(status: TaskStatus): Promise<Task[]> {
23
+ const tasks = await this.getAllTasks();
24
+ return tasks.filter(task => task.status === status && !task.completed);
25
+ }
26
+
27
+ /**
28
+ * Get inbox tasks
29
+ */
30
+ async getInboxTasks(): Promise<Task[]> {
31
+ return this.getTasksByStatus('inbox');
32
+ }
33
+
34
+ /**
35
+ * Get today's tasks
36
+ */
37
+ async getTodayTasks(): Promise<Task[]> {
38
+ const tasks = await this.getAllTasks();
39
+ const today = new Date();
40
+ today.setHours(0, 0, 0, 0);
41
+ const tomorrow = new Date(today);
42
+ tomorrow.setDate(tomorrow.getDate() + 1);
43
+
44
+ return tasks.filter(task => {
45
+ if (task.completed) return false;
46
+ if (!task.dueDate) return false;
47
+ const dueDate = new Date(task.dueDate);
48
+ return dueDate >= today && dueDate < tomorrow;
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Get tomorrow's tasks
54
+ */
55
+ async getTomorrowTasks(): Promise<Task[]> {
56
+ const tasks = await this.getAllTasks();
57
+ const tomorrow = new Date();
58
+ tomorrow.setDate(tomorrow.getDate() + 1);
59
+ tomorrow.setHours(0, 0, 0, 0);
60
+ const dayAfter = new Date(tomorrow);
61
+ dayAfter.setDate(dayAfter.getDate() + 1);
62
+
63
+ return tasks.filter(task => {
64
+ if (task.completed) return false;
65
+ if (!task.dueDate) return false;
66
+ const dueDate = new Date(task.dueDate);
67
+ return dueDate >= tomorrow && dueDate < dayAfter;
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Get overdue tasks
73
+ */
74
+ async getOverdueTasks(): Promise<Task[]> {
75
+ const tasks = await this.getAllTasks();
76
+ const today = new Date();
77
+ today.setHours(0, 0, 0, 0);
78
+
79
+ return tasks.filter(task => {
80
+ if (task.completed) return false;
81
+ if (!task.dueDate) return false;
82
+ const dueDate = new Date(task.dueDate);
83
+ return dueDate < today;
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Get tasks by project
89
+ */
90
+ async getTasksByProject(projectId: string): Promise<Task[]> {
91
+ const tasks = await this.getAllTasks();
92
+ return tasks.filter(task => task.projectId === projectId && !task.completed);
93
+ }
94
+
95
+ /**
96
+ * Get tasks by tag
97
+ */
98
+ async getTasksByTag(tagId: string): Promise<Task[]> {
99
+ const tasks = await this.getAllTasks();
100
+ return tasks.filter(task => task.tagIds.includes(tagId) && !task.completed);
101
+ }
102
+
103
+ /**
104
+ * Get completed tasks
105
+ */
106
+ async getCompletedTasks(): Promise<Task[]> {
107
+ const tasks = await this.getAllTasks();
108
+ return tasks.filter(task => task.completed).sort((a, b) =>
109
+ (b.completedAt || 0) - (a.completedAt || 0)
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Create a new task
115
+ */
116
+ async createTask(taskData: Partial<Task>): Promise<Task> {
117
+ const data = await storageService.getData();
118
+
119
+ const newTask: Task = {
120
+ id: generateId(),
121
+ title: taskData.title || '',
122
+ description: taskData.description,
123
+ status: taskData.status || 'inbox',
124
+ priority: taskData.priority,
125
+ dueDate: taskData.dueDate,
126
+ projectId: taskData.projectId,
127
+ tagIds: taskData.tagIds || [],
128
+ completed: false,
129
+ createdAt: Date.now(),
130
+ updatedAt: Date.now(),
131
+ };
132
+
133
+ data.tasks.push(newTask);
134
+ await storageService.saveData(data);
135
+
136
+ return newTask;
137
+ }
138
+
139
+ /**
140
+ * Update a task
141
+ */
142
+ async updateTask(taskId: string, updates: Partial<Task>): Promise<Task> {
143
+ const data = await storageService.getData();
144
+ const taskIndex = data.tasks.findIndex(t => t.id === taskId);
145
+
146
+ if (taskIndex === -1) {
147
+ throw new Error('Task not found');
148
+ }
149
+
150
+ const updatedTask = {
151
+ ...data.tasks[taskIndex],
152
+ ...updates,
153
+ id: taskId,
154
+ updatedAt: Date.now(),
155
+ };
156
+
157
+ data.tasks[taskIndex] = updatedTask;
158
+ await storageService.saveData(data);
159
+
160
+ return updatedTask;
161
+ }
162
+
163
+ /**
164
+ * Mark task as complete
165
+ */
166
+ async completeTask(taskId: string): Promise<Task> {
167
+ return this.updateTask(taskId, {
168
+ completed: true,
169
+ completedAt: Date.now(),
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Mark task as incomplete
175
+ */
176
+ async uncompleteTask(taskId: string): Promise<Task> {
177
+ return this.updateTask(taskId, {
178
+ completed: false,
179
+ completedAt: undefined,
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Move task to different status
185
+ */
186
+ async moveTask(taskId: string, status: TaskStatus): Promise<Task> {
187
+ return this.updateTask(taskId, { status });
188
+ }
189
+
190
+ /**
191
+ * Delete a task
192
+ */
193
+ async deleteTask(taskId: string): Promise<void> {
194
+ const data = await storageService.getData();
195
+ data.tasks = data.tasks.filter(t => t.id !== taskId);
196
+ await storageService.saveData(data);
197
+ }
198
+
199
+ /**
200
+ * Archive completed tasks older than specified days
201
+ */
202
+ async archiveOldCompletedTasks(daysOld: number = 30): Promise<number> {
203
+ const data = await storageService.getData();
204
+ const cutoffDate = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
205
+
206
+ const beforeCount = data.tasks.length;
207
+ data.tasks = data.tasks.filter(task => {
208
+ if (!task.completed) return true;
209
+ if (!task.completedAt) return true;
210
+ return task.completedAt > cutoffDate;
211
+ });
212
+
213
+ await storageService.saveData(data);
214
+ return beforeCount - data.tasks.length;
215
+ }
216
+}
217
+
218
+export default new TaskService();
ADD
types/gtd.ts
+56 -0
@@ -0,0 +1,56 @@
1
+/**
2
+ * GTD Data Models
3
+ * Based on David Allen's Getting Things Done methodology
4
+ */
5
+
6
+export type Priority = 'low' | 'medium' | 'high';
7
+
8
+export type TaskStatus = 'inbox' | 'next' | 'waiting' | 'someday' | 'done';
9
+
10
+export interface Tag {
11
+ id: string;
12
+ name: string;
13
+ color?: string;
14
+ createdAt: number;
15
+}
16
+
17
+export interface Project {
18
+ id: string;
19
+ name: string;
20
+ description?: string;
21
+ goal?: string;
22
+ color?: string;
23
+ archived: boolean;
24
+ createdAt: number;
25
+ updatedAt: number;
26
+}
27
+
28
+export interface Task {
29
+ id: string;
30
+ title: string;
31
+ description?: string;
32
+ status: TaskStatus;
33
+ priority?: Priority;
34
+ dueDate?: number; // timestamp
35
+ projectId?: string;
36
+ tagIds: string[];
37
+ completed: boolean;
38
+ completedAt?: number;
39
+ createdAt: number;
40
+ updatedAt: number;
41
+}
42
+
43
+export interface GTDData {
44
+ tasks: Task[];
45
+ projects: Project[];
46
+ tags: Tag[];
47
+ version: string;
48
+ lastSync?: number;
49
+ syncHash?: string;
50
+}
51
+
52
+export interface SyncMetadata {
53
+ lastSyncTimestamp: number;
54
+ syncHash: string;
55
+ conflictDetected: boolean;
56
+}
ADD
utils/id-generator.ts
+10 -0
@@ -0,0 +1,10 @@
1
+/**
2
+ * ID Generator
3
+ * Generates unique IDs for tasks, projects, and tags
4
+ */
5
+
6
+export function generateId(): string {
7
+ const timestamp = Date.now().toString(36);
8
+ const randomPart = Math.random().toString(36).substring(2, 9);
9
+ return `${timestamp}-${randomPart}`;
10
+}