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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ emit('stop-timer', chore, timeEntryId)" />
+
+
+
+
+
+
+
+
+
\ 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);
+ }
+};
+
+
+ You are using a guest account.
+ Sign up
+ to save your data permanently.
+