From f49e15c05c8725754829f6d699f9c706523a5a40 Mon Sep 17 00:00:00 2001 From: mohamad Date: Mon, 9 Jun 2025 21:02:51 +0200 Subject: [PATCH] feat: Introduce FastAPI and Vue.js guidelines, enhance API structure, and add caching support This commit adds new guidelines for FastAPI and Vue.js development, emphasizing best practices for component structure, API performance, and data handling. It also introduces caching mechanisms using Redis for improved performance and updates the API structure to streamline authentication and user management. Additionally, new endpoints for categories and time entries are implemented, enhancing the overall functionality of the application. --- .cursor/rules/fastapi.mdc | 32 + .cursor/rules/vue.mdc | 37 + .../bdf7427ccfa3_feature_updates_phase1.py | 91 +++ ...1c_add_updated_at_and_version_to_groups.py | 51 ++ be/app/api/auth/guest.py | 55 ++ be/app/api/auth/jwt.py | 26 + be/app/api/v1/api.py | 10 +- be/app/api/v1/endpoints/categories.py | 75 ++ be/app/api/v1/endpoints/chores.py | 126 ++- be/app/api/v1/endpoints/history.py | 46 ++ be/app/api/v1/endpoints/lists.py | 62 +- be/app/api/v1/endpoints/users.py | 11 + be/app/auth.py | 2 +- be/app/core/cache.py | 78 ++ be/app/core/redis.py | 7 + be/app/core/security.py | 41 +- be/app/crud/audit.py | 77 ++ be/app/crud/category.py | 38 + be/app/crud/chore.py | 138 ++-- be/app/crud/expense.py | 110 ++- be/app/crud/group.py | 39 +- be/app/crud/item.py | 4 + be/app/crud/list.py | 59 +- be/app/crud/settlement.py | 43 +- be/app/crud/settlement_activity.py | 8 + be/app/crud/user.py | 5 +- be/app/main.py | 26 - be/app/models.py | 53 +- be/app/schemas/audit.py | 20 + be/app/schemas/category.py | 19 + be/app/schemas/chore.py | 4 + be/app/schemas/item.py | 2 + be/app/schemas/time_entry.py | 22 + be/app/schemas/token.py | 8 + be/app/schemas/user.py | 4 + be/requirements.txt | 3 +- fe/src/components/CategoryForm.vue | 47 ++ fe/src/components/ChoreItem.vue | 128 +++ fe/src/components/CreateExpenseForm.vue | 2 +- fe/src/components/CreateGroupModal.vue | 2 +- fe/src/components/CreateListModal.vue | 3 +- fe/src/components/valerie/VButton.vue | 33 +- fe/src/config/api-config.ts | 21 +- fe/src/config/api.ts | 17 - fe/src/layouts/MainLayout.vue | 4 + fe/src/pages/AccountPage.vue | 28 +- fe/src/pages/CategoriesPage.vue | 65 ++ fe/src/pages/ChoresPage.vue | 201 +++-- fe/src/pages/ExpensePage.vue | 738 ++++++++++++++++++ fe/src/pages/GroupDetailPage.vue | 8 +- fe/src/pages/GroupsPage.vue | 2 +- fe/src/pages/ListDetailPage.vue | 299 ++++--- fe/src/pages/ListsPage.vue | 106 ++- fe/src/pages/LoginPage.vue | 48 ++ fe/src/pages/__tests__/AccountPage.spec.ts | 166 ++-- fe/src/router/routes.ts | 6 + fe/src/services/api.ts | 4 +- fe/src/services/choreService.ts | 4 +- fe/src/services/groupService.ts | 4 +- fe/src/stores/auth.ts | 12 + fe/src/stores/categoryStore.ts | 86 ++ fe/src/stores/listDetailStore.ts | 2 +- fe/src/stores/offline.ts | 3 +- fe/src/stores/timeEntryStore.ts | 79 ++ fe/src/types/chore.ts | 14 +- fe/src/types/expense.ts | 32 +- fe/src/types/item.ts | 1 + fe/src/utils/formatters.ts | 22 + 68 files changed, 3110 insertions(+), 509 deletions(-) create mode 100644 .cursor/rules/fastapi.mdc create mode 100644 .cursor/rules/vue.mdc create mode 100644 be/alembic/versions/bdf7427ccfa3_feature_updates_phase1.py create mode 100644 be/alembic/versions/c693ade3601c_add_updated_at_and_version_to_groups.py create mode 100644 be/app/api/auth/guest.py create mode 100644 be/app/api/auth/jwt.py create mode 100644 be/app/api/v1/endpoints/categories.py create mode 100644 be/app/api/v1/endpoints/history.py create mode 100644 be/app/api/v1/endpoints/users.py create mode 100644 be/app/core/cache.py create mode 100644 be/app/core/redis.py create mode 100644 be/app/crud/audit.py create mode 100644 be/app/crud/category.py create mode 100644 be/app/schemas/audit.py create mode 100644 be/app/schemas/category.py create mode 100644 be/app/schemas/time_entry.py create mode 100644 be/app/schemas/token.py create mode 100644 fe/src/components/CategoryForm.vue create mode 100644 fe/src/components/ChoreItem.vue delete mode 100644 fe/src/config/api.ts create mode 100644 fe/src/pages/CategoriesPage.vue create mode 100644 fe/src/pages/ExpensePage.vue create mode 100644 fe/src/stores/categoryStore.ts create mode 100644 fe/src/stores/timeEntryStore.ts create mode 100644 fe/src/utils/formatters.ts 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 @@