gitshark

Clone repository

git clone https://git.vilip.de/git/vilip/taskflow.git
git clone git@git.vilip.de:vilip/taskflow.git

← Commits

๐ŸŽ‰ Add base app

2792f8aaad9d36bde504829021ce3aeac04f48a3 ยท vvilip ยท 2025-11-04T11:41:58Z

Changes

9 files changed, +777 -6

MODIFY package-lock.json +36 -2
diff --git a/package-lock.json b/package-lock.json
index f35fcbb..34a9d51 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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
diff --git a/package.json b/package.json
index cd36024..479e04d 100644
--- a/package.json
+++ b/package.json
@@ -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
diff --git a/services/index.ts b/services/index.ts
new file mode 100644
index 0000000..7ddd020
--- /dev/null
+++ b/services/index.ts
@@ -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
diff --git a/services/project.service.ts b/services/project.service.ts
new file mode 100644
index 0000000..3f252a8
--- /dev/null
+++ b/services/project.service.ts
@@ -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
diff --git a/services/storage.service.ts b/services/storage.service.ts
new file mode 100644
index 0000000..56127db
--- /dev/null
+++ b/services/storage.service.ts
@@ -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
diff --git a/services/tag.service.ts b/services/tag.service.ts
new file mode 100644
index 0000000..1416051
--- /dev/null
+++ b/services/tag.service.ts
@@ -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
diff --git a/services/task.service.ts b/services/task.service.ts
new file mode 100644
index 0000000..ccf0113
--- /dev/null
+++ b/services/task.service.ts
@@ -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
diff --git a/types/gtd.ts b/types/gtd.ts
new file mode 100644
index 0000000..8511af6
--- /dev/null
+++ b/types/gtd.ts
@@ -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
diff --git a/utils/id-generator.ts b/utils/id-generator.ts
new file mode 100644
index 0000000..c3fe590
--- /dev/null
+++ b/utils/id-generator.ts
@@ -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 +}

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog