diff --git a/.cursor/rules/fastapi.mdc b/.cursor/rules/fastapi.mdc new file mode 100644 index 0000000..7b9b8a4 --- /dev/null +++ b/.cursor/rules/fastapi.mdc @@ -0,0 +1,32 @@ +--- +description: +globs: +alwaysApply: true +--- + # FastAPI-Specific Guidelines: + - Use functional components (plain functions) and Pydantic models for input validation and response schemas. + - Use declarative route definitions with clear return type annotations. + - Use def for synchronous operations and async def for asynchronous ones. + - Minimize @app.on_event("startup") and @app.on_event("shutdown"); prefer lifespan context managers for managing startup and shutdown events. + - Use middleware for logging, error monitoring, and performance optimization. + - Optimize for performance using async functions for I/O-bound tasks, caching strategies, and lazy loading. + - Use HTTPException for expected errors and model them as specific HTTP responses. + - Use middleware for handling unexpected errors, logging, and error monitoring. + - Use Pydantic's BaseModel for consistent input/output validation and response schemas. + + Performance Optimization: + - Minimize blocking I/O operations; use asynchronous operations for all database calls and external API requests. + - Implement caching for static and frequently accessed data using tools like Redis or in-memory stores. + - Optimize data serialization and deserialization with Pydantic. + - Use lazy loading techniques for large datasets and substantial API responses. + + Key Conventions + 1. Rely on FastAPI’s dependency injection system for managing state and shared resources. + 2. Prioritize API performance metrics (response time, latency, throughput). + 3. Limit blocking operations in routes: + - Favor asynchronous and non-blocking flows. + - Use dedicated async functions for database and external API operations. + - Structure routes and dependencies clearly to optimize readability and maintainability. + + + Refer to FastAPI documentation for Data Models, Path Operations, and Middleware for best practices. \ No newline at end of file diff --git a/.cursor/rules/vue.mdc b/.cursor/rules/vue.mdc new file mode 100644 index 0000000..cdeff48 --- /dev/null +++ b/.cursor/rules/vue.mdc @@ -0,0 +1,37 @@ +--- +description: +globs: +alwaysApply: true +--- + + You have extensive expertise in Vue 3, TypeScript, Node.js, Vite, Vue Router, Pinia, VueUse, and CSS. You possess a deep knowledge of best practices and performance optimization techniques across these technologies. + + Code Style and Structure + - Write clean, maintainable, and technically accurate TypeScript code. + - Emphasize iteration and modularization and minimize code duplication. + - Prefer Composition API \ No newline at end of file diff --git a/fe/src/components/ChoreItem.vue b/fe/src/components/ChoreItem.vue new file mode 100644 index 0000000..80de7bc --- /dev/null +++ b/fe/src/components/ChoreItem.vue @@ -0,0 +1,128 @@ + + + + + + + \ No newline at end of file diff --git a/fe/src/components/CreateExpenseForm.vue b/fe/src/components/CreateExpenseForm.vue index c7c1cf0..3611cb6 100644 --- a/fe/src/components/CreateExpenseForm.vue +++ b/fe/src/components/CreateExpenseForm.vue @@ -189,7 +189,7 @@ \ No newline at end of file diff --git a/fe/src/pages/ChoresPage.vue b/fe/src/pages/ChoresPage.vue index 3c749bd..3605285 100644 --- a/fe/src/pages/ChoresPage.vue +++ b/fe/src/pages/ChoresPage.vue @@ -4,23 +4,20 @@ import { useI18n } from 'vue-i18n' import { format, startOfDay, isEqual, isToday as isTodayDate, formatDistanceToNow, parseISO } from 'date-fns' import { choreService } from '../services/choreService' import { useNotificationStore } from '../stores/notifications' -import type { Chore, ChoreCreate, ChoreUpdate, ChoreFrequency, ChoreAssignmentUpdate, ChoreAssignment, ChoreHistory } from '../types/chore' +import type { Chore, ChoreCreate, ChoreUpdate, ChoreFrequency, ChoreAssignmentUpdate, ChoreAssignment, ChoreHistory, ChoreWithCompletion } from '../types/chore' import { groupService } from '../services/groupService' import { useStorage } from '@vueuse/core' +import ChoreItem from '@/components/ChoreItem.vue'; +import { useTimeEntryStore, type TimeEntry } from '../stores/timeEntryStore'; +import { storeToRefs } from 'pinia'; +import { useAuthStore } from '@/stores/auth'; const { t } = useI18n() const props = defineProps<{ groupId?: number | string }>(); // Types -interface ChoreWithCompletion extends Chore { - current_assignment_id: number | null; - is_completed: boolean; - completed_at: string | null; - updating: boolean; - assigned_user_name?: string; - completed_by_name?: string; -} +// ChoreWithCompletion is now imported from ../types/chore interface ChoreFormData { name: string; @@ -30,6 +27,7 @@ interface ChoreFormData { next_due_date: string; type: 'personal' | 'group'; group_id: number | undefined; + parent_chore_id?: number | null; } const notificationStore = useNotificationStore() @@ -60,11 +58,26 @@ const initialChoreFormState: ChoreFormData = { next_due_date: format(new Date(), 'yyyy-MM-dd'), type: 'personal', group_id: undefined, + parent_chore_id: null, } const choreForm = ref({ ...initialChoreFormState }) const isLoading = ref(true) +const authStore = useAuthStore(); +const { isGuest } = storeToRefs(authStore); + +const timeEntryStore = useTimeEntryStore(); +const { timeEntries, loading: timeEntryLoading, error: timeEntryError } = storeToRefs(timeEntryStore); + +const activeTimer = computed(() => { + for (const assignmentId in timeEntries.value) { + const entry = timeEntries.value[assignmentId].find(te => !te.end_time); + if (entry) return entry; + } + return null; +}); + const loadChores = async () => { const now = Date.now(); if (cachedChores.value && cachedChores.value.length > 0 && (now - cachedTimestamp.value) < CACHE_DURATION) { @@ -108,8 +121,16 @@ const loadGroups = async () => { } } +const loadTimeEntries = async () => { + chores.value.forEach(chore => { + if (chore.current_assignment_id) { + timeEntryStore.fetchTimeEntriesForAssignment(chore.current_assignment_id); + } + }); +}; + onMounted(() => { - loadChores() + loadChores().then(loadTimeEntries); loadGroups() }) @@ -173,17 +194,50 @@ const filteredChores = computed(() => { return chores.value; }); -const groupedChores = computed(() => { - if (!filteredChores.value) return [] - - const choresByDate = filteredChores.value.reduce((acc, chore) => { - const dueDate = format(startOfDay(new Date(chore.next_due_date)), 'yyyy-MM-dd') - if (!acc[dueDate]) { - acc[dueDate] = [] +const availableParentChores = computed(() => { + return chores.value.filter(c => { + // A chore cannot be its own parent + if (isEditing.value && selectedChore.value && c.id === selectedChore.value.id) { + return false; } - acc[dueDate].push(chore) - return acc - }, {} as Record) + // A chore that is already a subtask cannot be a parent + if (c.parent_chore_id) { + return false; + } + // If a group is selected, only show chores from that group or personal chores + if (choreForm.value.group_id) { + return c.group_id === choreForm.value.group_id || c.type === 'personal'; + } + // If no group is selected, only show personal chores that are not in a group + return c.type === 'personal' && !c.group_id; + }); +}); + +const groupedChores = computed(() => { + if (!filteredChores.value) return []; + + const choreMap = new Map(); + filteredChores.value.forEach(chore => { + choreMap.set(chore.id, { ...chore, child_chores: [] }); + }); + + const rootChores: ChoreWithCompletion[] = []; + choreMap.forEach(chore => { + if (chore.parent_chore_id && choreMap.has(chore.parent_chore_id)) { + choreMap.get(chore.parent_chore_id)?.child_chores?.push(chore); + } else { + rootChores.push(chore); + } + }); + + const choresByDate = rootChores.reduce((acc, chore) => { + const dueDate = format(startOfDay(new Date(chore.next_due_date)), 'yyyy-MM-dd'); + if (!acc[dueDate]) { + acc[dueDate] = []; + } + acc[dueDate].push(chore); + return acc; + }, {} as Record); return Object.keys(choresByDate) .sort((a, b) => new Date(a).getTime() - new Date(b).getTime()) @@ -198,7 +252,7 @@ const groupedChores = computed(() => { ...chore, subtext: getChoreSubtext(chore) })) - } + }; }); }); @@ -238,6 +292,7 @@ const openEditChoreModal = (chore: ChoreWithCompletion) => { next_due_date: chore.next_due_date, type: chore.type, group_id: chore.group_id ?? undefined, + parent_chore_id: chore.parent_chore_id, } showChoreModal.value = true } @@ -412,10 +467,29 @@ const getDueDateStatus = (chore: ChoreWithCompletion) => { if (isEqual(dueDate, today)) return 'due-today'; return 'upcoming'; }; + +const startTimer = async (chore: ChoreWithCompletion) => { + if (chore.current_assignment_id) { + await timeEntryStore.startTimeEntry(chore.current_assignment_id); + } +}; + +const stopTimer = async (chore: ChoreWithCompletion, timeEntryId: number) => { + if (chore.current_assignment_id) { + await timeEntryStore.stopTimeEntry(chore.current_assignment_id, timeEntryId); + } +}; diff --git a/fe/src/pages/GroupDetailPage.vue b/fe/src/pages/GroupDetailPage.vue index 86ef14d..ad96527 100644 --- a/fe/src/pages/GroupDetailPage.vue +++ b/fe/src/pages/GroupDetailPage.vue @@ -219,10 +219,10 @@ @@ -242,7 +242,7 @@
Created by: {{ selectedChore.creator?.name || selectedChore.creator?.email || 'Unknown' - }} + }}
Created: @@ -383,7 +383,7 @@ diff --git a/fe/src/pages/ListsPage.vue b/fe/src/pages/ListsPage.vue index dfec99c..36cde50 100644 --- a/fe/src/pages/ListsPage.vue +++ b/fe/src/pages/ListsPage.vue @@ -1,12 +1,17 @@