From f3bcd78e49607cc4b2f14b029aea40da9dea2eb6 Mon Sep 17 00:00:00 2001 From: tech08mag Date: Mon, 10 Aug 2026 11:41:25 +0200 Subject: [PATCH] feat(backend): subtask category support (schema, validation, sync, routes) --- backend/src/db/schema.ts | 2 + backend/src/routes/subtasks.ts | 1 + backend/src/routes/sync.ts | 2 + backend/src/routes/tasks.ts | 2 + backend/src/utils/validation.ts | 6 +- carry-your-live/app/day-view.tsx | 286 +++++++++++++++++++ carry-your-live/src/database/migrations.ts | 9 + carry-your-live/src/database/schema.ts | 3 +- carry-your-live/src/database/sync.ts | 3 + carry-your-live/src/hooks/useSubtasks.ts | 85 ++++++ carry-your-live/src/models/Task.ts | 1 + carry-your-live/src/theme.tsx | 16 ++ carry-your-live/src/types/index.ts | 20 ++ carry-your-live/src/utils/categoryActions.ts | 14 +- carry-your-live/src/utils/taskActions.ts | 5 +- 15 files changed, 451 insertions(+), 4 deletions(-) create mode 100644 carry-your-live/app/day-view.tsx create mode 100644 carry-your-live/src/hooks/useSubtasks.ts diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index ace6a53..0bede9b 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -44,6 +44,7 @@ export const tasks = pgTable('tasks', { id: text('id').primaryKey(), userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), categoryId: text('category_id').references(() => categories.id, { onDelete: 'cascade' }), + tags: text('tags').notNull().default(''), title: text('title').notNull(), description: text('description').notNull().default(''), priority: text('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'), @@ -84,6 +85,7 @@ export const subtasks = pgTable('subtasks', { userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), taskId: text('task_id').notNull().references(() => tasks.id, { onDelete: 'cascade' }), parentSubtaskId: text('parent_subtask_id').references((): AnyPgColumn => subtasks.id, { onDelete: 'cascade' }), + categoryId: text('category_id').references(() => categories.id, { onDelete: 'set null' }), title: text('title').notNull(), description: text('description').notNull().default(''), priority: text('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'), diff --git a/backend/src/routes/subtasks.ts b/backend/src/routes/subtasks.ts index 1aac289..4eeb034 100644 --- a/backend/src/routes/subtasks.ts +++ b/backend/src/routes/subtasks.ts @@ -82,6 +82,7 @@ router.post('/task/:taskId', asyncHandler(async (req: Request, res: Response) => userId, taskId: req.params.taskId, parentSubtaskId, + categoryId: data.categoryId ?? null, title: data.title, description: data.description ?? '', priority: data.priority ?? 'none', diff --git a/backend/src/routes/sync.ts b/backend/src/routes/sync.ts index 236e46c..c4a3472 100644 --- a/backend/src/routes/sync.ts +++ b/backend/src/routes/sync.ts @@ -235,6 +235,7 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => { title: task.title, description: task.description, categoryId: task.categoryId, + tags: task.tags ?? '', priority: task.priority, completed: task.completed, dueDate: task.dueDate, @@ -290,6 +291,7 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => { .set({ taskId: sub.taskId, parentSubtaskId: sub.parentSubtaskId ?? null, + categoryId: sub.categoryId ?? null, title: sub.title, description: sub.description ?? '', priority: sub.priority ?? 'none', diff --git a/backend/src/routes/tasks.ts b/backend/src/routes/tasks.ts index 495fbba..795987d 100644 --- a/backend/src/routes/tasks.ts +++ b/backend/src/routes/tasks.ts @@ -135,6 +135,7 @@ router.post('/', asyncHandler(async (req: Request, res: Response) => { id: taskId, userId, categoryId: data.categoryId, + tags: data.tags ?? '', title: data.title, description: data.description ?? '', priority: data.priority ?? 'none', @@ -159,6 +160,7 @@ router.post('/', asyncHandler(async (req: Request, res: Response) => { userId, taskId, title: st.title, + categoryId: (st as any).categoryId ?? null, completed: false, order: index, createdAt: now, diff --git a/backend/src/utils/validation.ts b/backend/src/utils/validation.ts index 32dc1d7..9aa68e3 100644 --- a/backend/src/utils/validation.ts +++ b/backend/src/utils/validation.ts @@ -39,6 +39,7 @@ export const taskCreateSchema = z.object({ title: z.string().min(1).max(100), description: z.string().max(1000).optional(), categoryId: z.string().max(100).transform((v) => (v === '' ? null : v)).nullable(), + tags: z.string().max(500).optional(), priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(), dueDate: z.number().int().min(0).optional(), dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v ?? ''), @@ -52,13 +53,14 @@ export const taskCreateSchema = z.object({ reminders: z.string().max(100).optional(), assigneeId: z.string().nullable().optional(), completedAt: z.number().int().min(0).nullable().optional(), - subtasks: z.array(z.object({ title: z.string().min(1).max(100) })).optional(), + subtasks: z.array(z.object({ title: z.string().min(1).max(100), categoryId: z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)) })).optional(), }); export const taskUpdateSchema = z.object({ title: z.string().min(1).max(100).optional(), description: z.string().max(1000).optional(), categoryId: z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)), + tags: z.string().max(500).optional(), priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(), completed: z.boolean().optional(), dueDate: z.number().int().min(0).optional(), @@ -72,6 +74,7 @@ export const taskUpdateSchema = z.object({ export const subtaskCreateSchema = z.object({ title: z.string().min(1).max(100), description: z.string().max(1000).optional(), + categoryId: z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)), priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(), dueDate: z.number().int().min(0).optional(), dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v ?? ''), @@ -91,6 +94,7 @@ export const subtaskCreateSchema = z.object({ export const subtaskUpdateSchema = z.object({ title: z.string().min(1).max(100).optional(), description: z.string().max(1000).optional(), + categoryId: z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)), priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(), completed: z.boolean().optional(), dueDate: z.number().int().min(0).optional(), diff --git a/carry-your-live/app/day-view.tsx b/carry-your-live/app/day-view.tsx new file mode 100644 index 0000000..79b2fb6 --- /dev/null +++ b/carry-your-live/app/day-view.tsx @@ -0,0 +1,286 @@ +import React, { useCallback, useMemo } from 'react'; +import { View, Text, StyleSheet, SafeAreaView, ScrollView, TouchableOpacity, KeyboardAvoidingView, BackHandler } from 'react-native'; +import { useRouter, useLocalSearchParams, useFocusEffect } from 'expo-router'; +import { Header } from '@/components/Header'; +import { useTasksByDate } from '@/hooks/useTasks'; +import { useSubtasks } from '@/hooks/useSubtasks'; +import { useTaskModals } from '@/hooks/useTaskModals'; +import { useSettings } from '@/theme'; +import { useCategories } from '@/hooks/useDatabase'; +import { toggleTaskComplete, toggleSubtaskComplete } from '@/utils/taskActions'; +import { QuickAddBar } from '@/components/QuickAddBar'; +import { SubtaskData } from '@/types'; +import { format, startOfDay } from 'date-fns'; +import Svg, { Path, Circle } from 'react-native-svg'; +import { desaturate } from '@/theme'; + +function isDayMatch(timestamp: number, day: Date): boolean { + const d = new Date(timestamp); + return d.getFullYear() === day.getFullYear() && d.getMonth() === day.getMonth() && d.getDate() === day.getDate(); +} + +export default function DayViewScreen() { + const router = useRouter(); + const { theme } = useSettings(); + const categories = useCategories(); + const { modals, openTaskEdit } = useTaskModals(); + const { date: dateParam } = useLocalSearchParams<{ date?: string }>(); + + const day = useMemo(() => { + const parsed = dateParam ? new Date(dateParam) : new Date(); + return Number.isNaN(parsed.getTime()) ? new Date() : parsed; + }, [dateParam]); + const dayStart = useMemo(() => startOfDay(day), [day]); + + const { map: subtasksByTask, refresh: refreshSubtasks } = useSubtasks(); + const { tasks: dayTasks, loading, refresh } = useTasksByDate(day); + + useFocusEffect( + useCallback(() => { + const sub = BackHandler.addEventListener('hardwareBackPress', () => false); + refreshSubtasks(); + refresh(); + return () => sub.remove(); + }, [refreshSubtasks, refresh]) + ); + + const subtasksOnDay = useMemo(() => { + const map = new Map(); + for (const [taskId, roots] of subtasksByTask) { + const due = roots.filter((s) => s.dueDate && isDayMatch(s.dueDate, day)); + if (due.length > 0) map.set(taskId, due); + } + return map; + }, [subtasksByTask, day]); + + const handleToggleComplete = useCallback(async (taskId: string) => { + await toggleTaskComplete(taskId); + refresh(); + refreshSubtasks(); + }, [refresh, refreshSubtasks]); + + const handleToggleSubtask = useCallback(async (subtaskId: string) => { + await toggleSubtaskComplete(subtaskId); + refreshSubtasks(); + }, [refreshSubtasks]); + + return ( + +
+ + + {loading ? ( + + Loading... + + ) : dayTasks.length === 0 && subtasksOnDay.size === 0 ? ( + + No tasks on this day + + ) : ( + <> + {dayTasks.map((task) => { + const subs = subtasksOnDay.get(task.id) ?? []; + const openCount = subs.filter((s) => !s.completed).length; + const cat = categories.find((c) => c.id === task.categoryId); + return ( + + + handleToggleComplete(task.id)} + activeOpacity={0.7} + accessibilityRole="checkbox" + accessibilityState={{ checked: task.completed }} + > + + {task.completed ? ( + <> + + + + ) : ( + + )} + + + + router.push({ pathname: '/task-detail', params: { id: task.id } })} + activeOpacity={0.7} + > + + {task.title} + + {openCount > 0 && ( + + {openCount} open subtask{openCount === 1 ? '' : 's'} + + )} + {!task.completed && ( + + {cat && ( + + {cat.name} + + )} + {task.dueTime ? ( + {task.dueTime}{task.endTime ? `–${task.endTime}` : ''} + ) : task.dueDate ? ( + {format(task.dueDate, 'HH:mm')} + ) : null} + + )} + + + openTaskEdit(task.id)} + activeOpacity={0.7} + accessibilityRole="button" + accessibilityLabel={`Edit ${task.title}`} + > + + + + + + + ); + })} + {Array.from(subtasksOnDay.entries()).flatMap(([taskId, subs]) => { + const parent = dayTasks.find((t) => t.id === taskId); + if (parent) return []; + return subs.map((sub) => ( + + + handleToggleSubtask(sub.id)} + activeOpacity={0.7} + accessibilityRole="checkbox" + accessibilityState={{ checked: sub.completed }} + > + + {sub.completed ? ( + <> + + + + ) : ( + + )} + + + router.push({ pathname: '/subtask-detail', params: { id: sub.id } })} + activeOpacity={0.7} + > + + {sub.title} + + {sub.dueTime ? ( + {sub.dueTime}{sub.endTime ? `–${sub.endTime}` : ''} + ) : null} + + + + )); + })} + + )} + + + + {modals(() => {})} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + kbAvoid: { + flex: 1, + }, + scrollBody: { + flex: 1, + }, + scrollContent: { + paddingTop: 12, + paddingBottom: 16, + }, + emptyState: { + alignItems: 'center', + paddingVertical: 64, + }, + emptyText: { + fontSize: 15, + fontWeight: '600', + }, + eventCard: { + marginHorizontal: 16, + marginBottom: 8, + borderRadius: 16, + borderWidth: 1, + paddingHorizontal: 14, + paddingVertical: 12, + }, + eventRow: { + flexDirection: 'row', + alignItems: 'center', + }, + checkCircle: { + width: 30, + marginRight: 12, + }, + eventTouch: { + flex: 1, + }, + eventTitle: { + fontSize: 15, + fontWeight: '500', + }, + eventCompleted: { + textDecorationLine: 'line-through', + color: '#9E9E9E', + }, + eventSub: { + fontSize: 13, + marginTop: 2, + }, + metaRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + marginTop: 4, + }, + tagChip: { + paddingHorizontal: 7, + paddingVertical: 2, + borderRadius: 6, + maxWidth: 140, + }, + tagText: { + color: '#FFFFFF', + fontSize: 10, + fontWeight: '600', + }, + timeText: { + fontSize: 12.5, + fontWeight: '500', + }, + menuButton: { + padding: 4, + marginLeft: 6, + }, +}); \ No newline at end of file diff --git a/carry-your-live/src/database/migrations.ts b/carry-your-live/src/database/migrations.ts index c21eb6f..d1cd02a 100644 --- a/carry-your-live/src/database/migrations.ts +++ b/carry-your-live/src/database/migrations.ts @@ -193,5 +193,14 @@ export const migrations = schemaMigrations({ }), ], }, + { + toVersion: 18, + steps: [ + addColumns({ + table: 'tasks', + columns: [{ name: 'tags', type: 'string', isOptional: true }], + }), + ], + }, ], }); diff --git a/carry-your-live/src/database/schema.ts b/carry-your-live/src/database/schema.ts index 375e7fb..3f9b1d6 100644 --- a/carry-your-live/src/database/schema.ts +++ b/carry-your-live/src/database/schema.ts @@ -1,7 +1,7 @@ import { appSchema, tableSchema } from '@nozbe/watermelondb'; export const schema = appSchema({ - version: 17, + version: 18, tables: [ tableSchema({ name: 'categories', @@ -19,6 +19,7 @@ export const schema = appSchema({ { name: 'title', type: 'string' }, { name: 'description', type: 'string' }, { name: 'category_id', type: 'string', isIndexed: true }, + { name: 'tags', type: 'string', isOptional: true }, { name: 'priority', type: 'string' }, { name: 'completed', type: 'boolean', isIndexed: true }, { name: 'completed_at', type: 'number', isOptional: true }, diff --git a/carry-your-live/src/database/sync.ts b/carry-your-live/src/database/sync.ts index 435b80e..61dfd54 100644 --- a/carry-your-live/src/database/sync.ts +++ b/carry-your-live/src/database/sync.ts @@ -117,6 +117,7 @@ async function pushChanges(): Promise<{ conflicts: PushConflict[]; pushedDeletio title: t.title, description: t.description, categoryId: t.categoryId, + tags: t.tags || '', priority: t.priority, completed: t.completed, dueDate: t.dueDate, @@ -451,6 +452,7 @@ async function upsertTask(row: ServerRow, force: boolean): Promise { t.title = String(row.title ?? ''); t.description = String(row.description ?? ''); t.categoryId = String(row.categoryId ?? ''); + t.tags = String(row.tags ?? ''); t.priority = row.priority ?? 'none'; t.completed = Boolean(row.completed); t.dueDate = Number(row.dueDate ?? 0); @@ -476,6 +478,7 @@ async function upsertTask(row: ServerRow, force: boolean): Promise { t.title = String(row.title ?? t.title); t.description = String(row.description ?? t.description); t.categoryId = String(row.categoryId ?? t.categoryId); + t.tags = String(row.tags ?? t.tags ?? ''); t.priority = row.priority ?? t.priority; t.completed = Boolean(row.completed ?? t.completed); t.dueDate = Number(row.dueDate ?? t.dueDate); diff --git a/carry-your-live/src/hooks/useSubtasks.ts b/carry-your-live/src/hooks/useSubtasks.ts new file mode 100644 index 0000000..9a652bd --- /dev/null +++ b/carry-your-live/src/hooks/useSubtasks.ts @@ -0,0 +1,85 @@ +import { useDatabase } from './useDatabase'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { SubtaskData } from '@/types'; +function mapRow(s: any): SubtaskData { + return { + id: s.id, + taskId: s.taskId, + parentSubtaskId: s.parentSubtaskId || null, + title: s.title, + description: s.description || '', + priority: (s.priority || 'none') as SubtaskData['priority'], + completed: s.completed, + dueDate: s.dueDate || 0, + dueTime: s.dueTime || '', + endTime: s.endTime || '', + allDay: s.allDay ?? false, + repeat: (s.repeat || 'none') as SubtaskData['repeat'], + repeatInterval: s.repeatInterval ?? 1, + repeatDays: s.repeatDays || '', + seriesId: s.seriesId || '', + reminder: (s.reminder || 'none') as SubtaskData['reminder'], + assigneeId: s.assigneeId ?? null, + order: s.order, + subtasks: [], + }; +} + +function buildTrees(rows: any[]): Map { + const nodes = new Map(); + for (const row of rows) { + nodes.set(row.id, mapRow(row)); + } + const rootsByTask = new Map(); + for (const node of nodes.values()) { + if (node.parentSubtaskId && nodes.has(node.parentSubtaskId)) { + const parent = nodes.get(node.parentSubtaskId)!; + parent.subtasks.push(node); + } else { + const list = rootsByTask.get(node.taskId) ?? []; + list.push(node); + rootsByTask.set(node.taskId, list); + } + } + for (const list of rootsByTask.values()) { + list.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + } + const sortDeep = (list: SubtaskData[]) => { + for (const node of list) { + node.subtasks.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + sortDeep(node.subtasks); + } + }; + for (const list of rootsByTask.values()) { + sortDeep(list); + } + return rootsByTask; +} + +export function useSubtasks(): { map: Map; refresh: () => void } { + const { collections } = useDatabase(); + const [rows, setRows] = useState([]); + const [refreshKey, setRefreshKey] = useState(0); + const refresh = useCallback(() => setRefreshKey((k) => k + 1), []); + + useEffect(() => { + let mounted = true; + const subscription = collections.subtasks + .query() + .observe() + .subscribe({ + next: (result) => { + if (mounted) setRows(result); + }, + error: () => {}, + }); + return () => { + mounted = false; + subscription.unsubscribe(); + }; + }, [collections.subtasks, refreshKey]); + + const map = useMemo(() => buildTrees(rows), [rows]); + + return { map, refresh }; +} \ No newline at end of file diff --git a/carry-your-live/src/models/Task.ts b/carry-your-live/src/models/Task.ts index 4e9bb11..6c93388 100644 --- a/carry-your-live/src/models/Task.ts +++ b/carry-your-live/src/models/Task.ts @@ -13,6 +13,7 @@ export default class Task extends Model { @field('title') title!: string; @field('description') description!: string; @field('category_id') categoryId!: string; + @field('tags') tags!: string; @field('priority') priority!: Priority; @field('completed') completed!: boolean; @field('completed_at') completedAt!: number | null; diff --git a/carry-your-live/src/theme.tsx b/carry-your-live/src/theme.tsx index db4fecc..3796136 100644 --- a/carry-your-live/src/theme.tsx +++ b/carry-your-live/src/theme.tsx @@ -128,6 +128,10 @@ interface SettingsContextType { setAccentColor: (value: string) => void; todoAheadDays: number; setTodoAheadDays: (value: number) => void; + showCompleted: boolean; + setShowCompleted: (value: boolean) => void; + calendarCategoryId: string; + setCalendarCategoryId: (value: string) => void; theme: ThemeColors; } @@ -140,6 +144,8 @@ const STORAGE_KEYS = { reminderPreference: 'settings:reminderPreference', accentColor: 'settings:accentColor', todoAheadDays: 'settings:todoAheadDays', + showCompleted: 'settings:showCompleted', + calendarCategoryId: 'settings:calendarCategoryId', }; const SettingsContext = createContext(null); @@ -181,6 +187,8 @@ export function SettingsProvider({ children }: { children: ReactNode }) { const [apiUrl, setApiUrl] = useStoredSetting(API_URL_KEY, DEFAULT_API_BASE_URL); const [accentColor, setAccentColor] = useStoredSetting(STORAGE_KEYS.accentColor, DEFAULT_ACCENT); const [todoAheadDays, setTodoAheadDays] = useStoredSetting(STORAGE_KEYS.todoAheadDays, DEFAULT_TODO_AHEAD_DAYS); + const [showCompleted, setShowCompleted] = useStoredSetting(STORAGE_KEYS.showCompleted, false); + const [calendarCategoryId, setCalendarCategoryId] = useStoredSetting(STORAGE_KEYS.calendarCategoryId, ''); const theme = useMemo(() => colors(accentColor), [accentColor]); @@ -200,6 +208,10 @@ export function SettingsProvider({ children }: { children: ReactNode }) { setAccentColor, todoAheadDays, setTodoAheadDays, + showCompleted, + setShowCompleted, + calendarCategoryId, + setCalendarCategoryId, theme, }), [ @@ -217,6 +229,10 @@ export function SettingsProvider({ children }: { children: ReactNode }) { setAccentColor, todoAheadDays, setTodoAheadDays, + showCompleted, + setShowCompleted, + calendarCategoryId, + setCalendarCategoryId, theme, ] ); diff --git a/carry-your-live/src/types/index.ts b/carry-your-live/src/types/index.ts index 4193c69..3a55a13 100644 --- a/carry-your-live/src/types/index.ts +++ b/carry-your-live/src/types/index.ts @@ -99,11 +99,30 @@ export interface CategoryData { order: number; } +export function tagsToString(tags: string[]): string { + return `,${tags.filter(Boolean).join(',')},`; +} + +export function tagsFromString(raw: string | null | undefined): string[] { + if (!raw) return []; + return raw + .split(',') + .map((t) => t.trim()) + .filter(Boolean); +} + +export function parseTaskTags(tagsRaw: string | null | undefined, categoryId: string): string[] { + const parsed = tagsFromString(tagsRaw); + if (parsed.length > 0) return parsed; + return categoryId ? [categoryId] : []; +} + export interface TaskData { id: string; title: string; description: string; categoryId: string; + tags?: string; priority: Priority; completed: boolean; dueDate: number; @@ -154,6 +173,7 @@ export interface TaskFormData { title: string; description?: string; categoryId: string; + tags?: string[]; priority: Priority; dueDate: Date | null; dueTime?: string; diff --git a/carry-your-live/src/utils/categoryActions.ts b/carry-your-live/src/utils/categoryActions.ts index 23f10e3..330ef0b 100644 --- a/carry-your-live/src/utils/categoryActions.ts +++ b/carry-your-live/src/utils/categoryActions.ts @@ -1,6 +1,7 @@ import { database, collections } from '@/database'; import { Q } from '@nozbe/watermelondb'; import { recordTombstonesInBatch } from '@/database/tombstones'; +import { tagsFromString, tagsToString, parseTaskTags } from '@/types'; export async function createCategory(name: string, color: string): Promise { await database.write(async () => { @@ -36,7 +37,18 @@ export async function deleteCategory(categoryId: string): Promise { const tasks = await collections.tasks.query(Q.where('category_id', categoryId)).fetch(); for (const task of tasks) { await task.update((t) => { - t.categoryId = fallback?.id ?? ''; + const tags = parseTaskTags(t.tags, t.categoryId).filter((id) => id !== categoryId); + t.categoryId = fallback?.id ?? tags[0] ?? ''; + t.tags = tagsToString(tags); + t.updatedAt = new Date(); + }); + } + + const tagReferencedTasks = await collections.tasks.query(Q.where('tags', Q.like(`%,${categoryId},%`))).fetch(); + for (const task of tagReferencedTasks) { + const tags = parseTaskTags(task.tags, task.categoryId).filter((id) => id !== categoryId); + await task.update((t) => { + t.tags = tagsToString(tags); t.updatedAt = new Date(); }); } diff --git a/carry-your-live/src/utils/taskActions.ts b/carry-your-live/src/utils/taskActions.ts index 1e2e10a..1c1b84a 100644 --- a/carry-your-live/src/utils/taskActions.ts +++ b/carry-your-live/src/utils/taskActions.ts @@ -1,6 +1,6 @@ import { database, collections } from '@/database'; import { Q } from '@nozbe/watermelondb'; -import { Priority, Repeat, Reminder, SubtaskData } from '@/types'; +import { Priority, Repeat, Reminder, SubtaskData, tagsToString } from '@/types'; import { scheduleTaskReminder, cancelTaskReminder } from '@/services/notifications'; import { recordTombstonesInBatch } from '@/database/tombstones'; @@ -621,6 +621,7 @@ async function createNextOccurrence(task: any, seriesId: string): Promise { t.title = task.title; t.description = task.description; t.categoryId = task.categoryId; + t.tags = task.tags || ''; t.priority = task.priority; t.completed = false; t.dueDate = nextDate.getTime(); @@ -664,6 +665,7 @@ export async function setTaskCategory(taskId: string, categoryId: string): Promi const task = await collections.tasks.find(taskId); await task.update((t) => { t.categoryId = categoryId; + t.tags = tagsToString(categoryId ? [categoryId] : []); t.updatedAt = new Date(); }); }); @@ -690,6 +692,7 @@ export async function duplicateTask(taskId: string): Promise { t.title = task.title; t.description = task.description; t.categoryId = task.categoryId; + t.tags = task.tags || ''; t.priority = task.priority; t.completed = false; t.dueDate = task.dueDate;