diff --git a/.cursor/rules/roadmap.mdc b/.cursor/rules/roadmap.mdc new file mode 100644 index 0000000..8c78231 --- /dev/null +++ b/.cursor/rules/roadmap.mdc @@ -0,0 +1,267 @@ +--- +description: +globs: +alwaysApply: false +--- +Of course. Based on a thorough review of your project's structure and code, here is a detailed, LLM-friendly task list to implement the requested features. + +This plan is designed to be sequential and modular, focusing on backend database changes first, then backend logic and APIs, and finally the corresponding frontend implementation for each feature. + +--- + +### **High-Level Strategy & Recommendations** + +1. **Iterative Implementation:** Tackle one major feature at a time (e.g., complete Audit Logging, then Archiving, etc.). This keeps pull requests manageable and easier to review. +2. **Traceability:** The request for traceability is key. We will use timestamp-based flags (`archived_at`, `deleted_at`) instead of booleans and create dedicated history/log tables for critical actions. + +--- + +### **Phase 1: Database Schema Redesign** + +This is the most critical first step. All subsequent tasks depend on these changes. You will need to create a new Alembic migration to apply these. + +**File to Modify:** `be/app/models.py` +**Action:** Create a new Alembic migration file (`alembic revision -m "feature_updates_phase1"`) and implement the following changes in `upgrade()`. + +**1. Financial Audit Logging** +* Create a new table to log every financial transaction and change. This ensures complete traceability. + + ```python + # In be/app/models.py + class FinancialAuditLog(Base): + __tablename__ = 'financial_audit_log' + id = Column(Integer, primary_key=True, index=True) + timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + user_id = Column(Integer, ForeignKey('users.id'), nullable=True) # User who performed the action. Nullable for system actions. + action_type = Column(String, nullable=False, index=True) # e.g., 'EXPENSE_CREATED', 'SPLIT_PAID', 'SETTLEMENT_DELETED' + entity_type = Column(String, nullable=False) # e.g., 'Expense', 'ExpenseSplit', 'Settlement' + entity_id = Column(Integer, nullable=False) + details = Column(JSONB, nullable=True) # To store 'before' and 'after' states or other relevant data. + + user = relationship("User") + ``` + +**2. Archiving Lists and History** +* Modify the `lists` table to support soft deletion/archiving. + + ```python + # In be/app/models.py, class List(Base): + # REMOVE: is_deleted = Column(Boolean, default=False, nullable=False) # If it exists + archived_at = Column(DateTime(timezone=True), nullable=True, index=True) + ``` + +**3. Chore Subtasks** +* Add a self-referencing foreign key to the `chores` table. + + ```python + # In be/app/models.py, class Chore(Base): + parent_chore_id = Column(Integer, ForeignKey('chores.id'), nullable=True, index=True) + + # Add relationships + parent_chore = relationship("Chore", remote_side=[id], back_populates="child_chores") + child_chores = relationship("Chore", back_populates="parent_chore", cascade="all, delete-orphan") + ``` + +**4. List Categories** +* Create a new `categories` table and link it to the `items` table. This allows items to be categorized. + + ```python + # In be/app/models.py + class Category(Base): + __tablename__ = 'categories' + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False, index=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=True) # Nullable for global categories + group_id = Column(Integer, ForeignKey('groups.id'), nullable=True) # Nullable for user-specific or global + # Add constraints to ensure either user_id or group_id is set, or both are null for global categories + __table_args__ = (UniqueConstraint('name', 'user_id', 'group_id', name='uq_category_scope'),) + + # In be/app/models.py, class Item(Base): + category_id = Column(Integer, ForeignKey('categories.id'), nullable=True) + category = relationship("Category") + ``` + +**5. Time Tracking for Chores** +* Create a new `time_entries` table to log time spent on chore assignments. + + ```python + # In be/app/models.py + class TimeEntry(Base): + __tablename__ = 'time_entries' + id = Column(Integer, primary_key=True, index=True) + chore_assignment_id = Column(Integer, ForeignKey('chore_assignments.id', ondelete="CASCADE"), nullable=False) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False) + start_time = Column(DateTime(timezone=True), nullable=False) + end_time = Column(DateTime(timezone=True), nullable=True) + duration_seconds = Column(Integer, nullable=True) # Calculated on end_time set + + assignment = relationship("ChoreAssignment") + user = relationship("User") + ``` + +--- + +### **Phase 2: Backend Implementation** + +For each feature, implement the necessary backend logic. + +#### **Task 2.1: Implement Financial Audit Logging** + +* **Goal:** Automatically log all changes to expenses, splits, and settlements. +* **Tasks:** + 1. **CRUD (`be/app/crud/audit.py`):** + * Create a new file `audit.py`. + * Implement `create_financial_audit_log(db: AsyncSession, user_id: int, action_type: str, entity: Base, details: dict)`. This function will create a new log entry. + 2. **Integrate Logging:** + * Modify `be/app/crud/expense.py`: In `create_expense`, `update_expense`, `delete_expense`, call `create_financial_audit_log`. For updates, the `details` JSONB should contain `{"before": {...}, "after": {...}}`. + * Modify `be/app/crud/settlement.py`: Do the same for `create_settlement`, `update_settlement`, `delete_settlement`. + * Modify `be/app/crud/settlement_activity.py`: Do the same for `create_settlement_activity`. + 3. **API (`be/app/api/v1/endpoints/history.py` - new file):** + * Create a new endpoint `GET /history/financial/group/{group_id}` to view the audit log for a group. + * Create a new endpoint `GET /history/financial/user/me` for a user's personal financial history. + +#### **Task 2.2: Implement Archiving** + +* **Goal:** Allow users to archive lists instead of permanently deleting them. +* **Tasks:** + 1. **CRUD (`be/app/crud/list.py`):** + * Rename `delete_list` to `archive_list`. Instead of `db.delete(list_db)`, it should set `list_db.archived_at = datetime.now(timezone.utc)`. + * Modify `get_lists_for_user` to filter out archived lists by default: `.where(ListModel.archived_at.is_(None))`. + 2. **API (`be/app/api/v1/endpoints/lists.py`):** + * Update the `DELETE /{list_id}` endpoint to call `archive_list`. + * Create a new endpoint `GET /archived` to fetch archived lists for the user. + * Create a new endpoint `POST /{list_id}/unarchive` to set `archived_at` back to `NULL`. + +#### **Task 2.3: Implement Chore Subtasks & Unmarking Completion** + +* **Goal:** Allow chores to have a hierarchy and for completion to be reversible. +* **Tasks:** + 1. **Schemas (`be/app/schemas/chore.py`):** + * Update `ChorePublic` and `ChoreCreate` schemas to include `parent_chore_id: Optional[int]` and `child_chores: List[ChorePublic] = []`. + 2. **CRUD (`be/app/crud/chore.py`):** + * Modify `create_chore` and `update_chore` to handle the `parent_chore_id`. + * In `update_chore_assignment`, enhance the `is_complete=False` logic. When a chore is re-opened, log it to the history. Decide on the policy for the parent chore's `next_due_date` (recommendation: do not automatically roll it back; let the user adjust it manually if needed). + 3. **API (`be/app/api/v1/endpoints/chores.py`):** + * Update the `POST` and `PUT` endpoints for chores to accept `parent_chore_id`. + * The `PUT /assignments/{assignment_id}` endpoint already supports setting `is_complete`. Ensure it correctly calls the updated CRUD logic. + +#### **Task 2.4: Implement List Categories** + +* **Goal:** Allow items to be categorized for better organization. +* **Tasks:** + 1. **Schemas (`be/app/schemas/category.py` - new file):** + * Create `CategoryCreate`, `CategoryUpdate`, `CategoryPublic`. + 2. **CRUD (`be/app/crud/category.py` - new file):** + * Implement full CRUD functions for categories (`create_category`, `get_user_categories`, `update_category`, `delete_category`). + 3. **API (`be/app/api/v1/endpoints/categories.py` - new file):** + * Create endpoints for `GET /`, `POST /`, `PUT /{id}`, `DELETE /{id}` for categories. + 4. **Item Integration:** + * Update `ItemCreate` and `ItemUpdate` schemas in `be/app/schemas/item.py` to include `category_id: Optional[int]`. + * Update `crud_item.create_item` and `crud_item.update_item` to handle setting the `category_id`. + +#### **Task 2.5: Enhance OCR for Receipts** + +* **skipped** + +#### **Task 2.6: Implement "Continue as Guest"** + +* **Goal:** Allow users to use the app without creating a full account. +* **Tasks:** + 1. **DB Model (`be/app/models.py`):** + * Add `is_guest = Column(Boolean, default=False, nullable=False)` to the `User` model. + 2. **Auth (`be/app/api/auth/guest.py` - new file):** + * Create a new router for guest functionality. + * Implement a `POST /auth/guest` endpoint. This endpoint will: + * Create a new user with a unique but temporary-looking email (e.g., `guest_{uuid}@guest.mitlist.app`). + * Set `is_guest=True`. + * Generate and return JWT tokens for this guest user, just like a normal login. + 3. **Claim Account (`be/app/api/auth/guest.py`):** + * Implement a `POST /auth/guest/claim` endpoint (requires auth). This endpoint will take a new email and password, update the `is_guest=False`, set the new credentials, and mark the email for verification. + +#### **Task 2.7: Implement Redis** + +* **Goal:** Integrate Redis for caching to improve performance. +* **Tasks:** + 1. **Dependencies (`be/requirements.txt`):** Add `redis`. + 2. **Configuration (`be/app/config.py`):** Add `REDIS_URL` to settings. + 3. **Connection (`be/app/core/redis.py` - new file):** Create a Redis connection pool. + 4. **Caching (`be/app/core/cache.py` - new file):** Implement a simple caching decorator. + ```python + # Example decorator + def cache(expire_time: int = 3600): + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + # ... logic to check cache, return if hit ... + # ... if miss, call func, store result in cache ... + return result + return wrapper + return decorator + ``` + 5. **Apply Caching:** Apply the `@cache` decorator to read-heavy, non-volatile CRUD functions like `crud_group.get_group_by_id`. + +--- + +### **Phase 3: Frontend Implementation** + +Implement the UI for the new features, using your Valerie UI components. + +#### **Task 3.1: Implement Archiving UI** + +* **Goal:** Allow users to archive and view archived lists. +* **Files to Modify:** `fe/src/pages/ListsPage.vue`, `fe/src/stores/listStore.ts` (if you create one). +* **Tasks:** + 1. Change the "Delete" action on lists to "Archive". + 2. Add a toggle/filter to show archived lists. + 3. When viewing archived lists, show an "Unarchive" button. + +#### **Task 3.2: Implement Subtasks and Unmarking UI** + +* **Goal:** Update the chore interface for subtasks and undoing completion. +* **Files to Modify:** `fe/src/pages/ChoresPage.vue`, `fe/src/components/ChoreItem.vue` (if it exists). +* **Tasks:** + 1. Modify the chore list to be a nested/tree view to display parent-child relationships. + 2. Update the chore creation/edit modal to include a "Parent Chore" dropdown. + 3. On completed chores, change the "Completed" checkmark to an "Undo" button. Clicking it should call the API to set `is_complete` to `false`. + +#### **Task 3.3: Implement Category Management and Supermarkt Mode** + +* **Goal:** Add category features and the special "Supermarkt Mode". +* **Files to Modify:** `fe/src/pages/ListDetailPage.vue`, `fe/src/components/Item.vue`. +* **Tasks:** + 1. Create a new page/modal for managing categories (CRUD). + 2. In the `ListDetailPage`, add a "Category" dropdown when adding/editing an item. + 3. Display items grouped by category. + 4. **Supermarkt Mode:** + * Add a toggle button on the `ListDetailPage` to enter "Supermarkt Mode". + * When an item is checked, apply a temporary CSS class to other items in the same category. + * Ensure the price input field appears next to checked items. + * Add a `VProgressBar` at the top, with `value` bound to `completedItems.length` and `max` bound to `totalItems.length`. + +#### **Task 3.4: Implement Time Tracking UI** + +* **Goal:** Allow users to track time on chores. +* **Files to Modify:** `fe/src/pages/ChoresPage.vue`. +* **Tasks:** + 1. Add a "Start/Stop" timer button on each chore assignment. + 2. Clicking "Start" sends a `POST /time_entries` request. + 3. Clicking "Stop" sends a `PUT /time_entries/{id}` request. + 4. Display the total time spent on the chore. + + +#### **Task 3.5: Implement Guest Flow** + +* **Goal:** Provide a seamless entry point for new users. +* **Files to Modify:** `fe/src/pages/LoginPage.vue`, `fe/src/stores/auth.ts`, `fe/src/router/index.ts`. +* **Tasks:** + 1. On the `LoginPage`, add a "Continue as Guest" button. + 2. This button calls a new `authStore.loginAsGuest()` action. + 3. The action hits the `POST /auth/guest` endpoint, receives tokens, and stores them. + 4. The router logic needs adjustment to handle guest users. You might want to protect certain pages (like "Account Settings") even from guests. + 5. Add a persistent banner in the UI for guest users: "You are using a guest account. **Sign up** to save your data." + + +``` + + +**Final Note:** This is a comprehensive roadmap. Each major task can be broken down further into smaller sub-tasks. Good luck with the implementation \ No newline at end of file diff --git a/be/app/api/auth/guest.py b/be/app/api/auth/guest.py index e0958c6..23b9186 100644 --- a/be/app/api/auth/guest.py +++ b/be/app/api/auth/guest.py @@ -4,10 +4,10 @@ import uuid from app import models from app.schemas.user import UserCreate, UserClaim, UserPublic -from app.schemas.token import Token +from app.schemas.auth import Token from app.database import get_session -from app.auth import current_active_user -from app.core.security import create_access_token, get_password_hash +from app.auth import current_active_user, get_jwt_strategy, get_refresh_jwt_strategy +from app.core.security import get_password_hash from app.crud import user as crud_user router = APIRouter() @@ -23,8 +23,18 @@ async def create_guest_user(db: AsyncSession = Depends(get_session)): user_in = UserCreate(email=guest_email, password=guest_password) user = await crud_user.create_user(db, user_in=user_in, is_guest=True) - access_token = create_access_token(data={"sub": user.email}) - return {"access_token": access_token, "token_type": "bearer"} + # Use the same JWT strategy as regular login to generate both access and refresh tokens + access_strategy = get_jwt_strategy() + refresh_strategy = get_refresh_jwt_strategy() + + access_token = await access_strategy.write_token(user) + refresh_token = await refresh_strategy.write_token(user) + + return { + "access_token": access_token, + "refresh_token": refresh_token, + "token_type": "bearer" + } @router.post("/guest/claim", response_model=UserPublic) async def claim_guest_account( diff --git a/be/app/schemas/item.py b/be/app/schemas/item.py index 2148b90..3cee512 100644 --- a/be/app/schemas/item.py +++ b/be/app/schemas/item.py @@ -3,6 +3,11 @@ from datetime import datetime from typing import Optional from decimal import Decimal +class UserReference(BaseModel): + id: int + name: Optional[str] = None + model_config = ConfigDict(from_attributes=True) + class ItemPublic(BaseModel): id: int list_id: int @@ -10,8 +15,11 @@ class ItemPublic(BaseModel): quantity: Optional[str] = None is_complete: bool price: Optional[Decimal] = None + category_id: Optional[int] = None added_by_id: int completed_by_id: Optional[int] = None + added_by_user: Optional[UserReference] = None + completed_by_user: Optional[UserReference] = None created_at: datetime updated_at: datetime version: int diff --git a/fe/src/components/ChoreItem.vue b/fe/src/components/ChoreItem.vue index 80de7bc..a00a3d9 100644 --- a/fe/src/components/ChoreItem.vue +++ b/fe/src/components/ChoreItem.vue @@ -113,11 +113,231 @@ export default { \ No newline at end of file diff --git a/fe/src/components/list-detail/CostSummaryDialog.vue b/fe/src/components/list-detail/CostSummaryDialog.vue new file mode 100644 index 0000000..1dfc313 --- /dev/null +++ b/fe/src/components/list-detail/CostSummaryDialog.vue @@ -0,0 +1,142 @@ + + + + + \ No newline at end of file diff --git a/fe/src/components/list-detail/ExpenseSection.vue b/fe/src/components/list-detail/ExpenseSection.vue new file mode 100644 index 0000000..5430ef2 --- /dev/null +++ b/fe/src/components/list-detail/ExpenseSection.vue @@ -0,0 +1,384 @@ + + + + + \ No newline at end of file diff --git a/fe/src/components/list-detail/ItemsList.vue b/fe/src/components/list-detail/ItemsList.vue new file mode 100644 index 0000000..344b8f5 --- /dev/null +++ b/fe/src/components/list-detail/ItemsList.vue @@ -0,0 +1,255 @@ + + + + + \ No newline at end of file diff --git a/fe/src/components/list-detail/ListItem.vue b/fe/src/components/list-detail/ListItem.vue new file mode 100644 index 0000000..f31121b --- /dev/null +++ b/fe/src/components/list-detail/ListItem.vue @@ -0,0 +1,497 @@ + + + + + \ No newline at end of file diff --git a/fe/src/components/list-detail/OcrDialog.vue b/fe/src/components/list-detail/OcrDialog.vue new file mode 100644 index 0000000..6631c90 --- /dev/null +++ b/fe/src/components/list-detail/OcrDialog.vue @@ -0,0 +1,114 @@ + + + \ No newline at end of file diff --git a/fe/src/components/list-detail/SettleShareModal.vue b/fe/src/components/list-detail/SettleShareModal.vue new file mode 100644 index 0000000..312023b --- /dev/null +++ b/fe/src/components/list-detail/SettleShareModal.vue @@ -0,0 +1,73 @@ + + + \ No newline at end of file diff --git a/fe/src/i18n/en.json b/fe/src/i18n/en.json index 8d7a972..4a6bca4 100644 --- a/fe/src/i18n/en.json +++ b/fe/src/i18n/en.json @@ -375,7 +375,8 @@ "confirm": "Confirm", "saveChanges": "Save Changes", "close": "Close", - "costSummary": "Cost Summary" + "costSummary": "Cost Summary", + "exitSupermarketMode": "Exit Supermarket Mode" }, "badges": { "groupList": "Group List", @@ -394,7 +395,12 @@ }, "pricePlaceholder": "Price", "editItemAriaLabel": "Edit item", - "deleteItemAriaLabel": "Delete item" + "deleteItemAriaLabel": "Delete item", + "addedBy": "Added by", + "completedBy": "Completed by", + "addedByTooltip": "This item was added by {name}", + "completedByTooltip": "This item was completed by {name}", + "noCategory": "No Category" }, "modals": { "ocr": { diff --git a/fe/src/layouts/MainLayout.vue b/fe/src/layouts/MainLayout.vue index 998a05a..dc9ce37 100644 --- a/fe/src/layouts/MainLayout.vue +++ b/fe/src/layouts/MainLayout.vue @@ -207,7 +207,10 @@ const navigateToGroups = () => { onMounted(async () => { if (authStore.isAuthenticated) { try { - await authStore.fetchCurrentUser(); + // Only fetch user if we don't have user data yet + if (!authStore.user) { + await authStore.fetchCurrentUser(); + } await groupStore.fetchGroups(); } catch (error) { console.error('Failed to initialize app data:', error); diff --git a/fe/src/pages/AuthCallbackPage.vue b/fe/src/pages/AuthCallbackPage.vue index d50a510..7f55750 100644 --- a/fe/src/pages/AuthCallbackPage.vue +++ b/fe/src/pages/AuthCallbackPage.vue @@ -41,7 +41,11 @@ onMounted(async () => { throw new Error(t('authCallbackPage.errors.noTokenProvided')); } - await authStore.setTokens({ access_token: tokenToUse, refresh_token: refreshToken }); + authStore.setTokens({ access_token: tokenToUse, refresh_token: refreshToken }); + + // Fetch user data after setting tokens + await authStore.fetchCurrentUser(); + notificationStore.addNotification({ message: t('loginPage.notifications.loginSuccess'), type: 'success' }); router.push('/'); } catch (err) { diff --git a/fe/src/pages/ExpensePage.vue b/fe/src/pages/ExpensePage.vue index 8db1bb5..67a17d8 100644 --- a/fe/src/pages/ExpensePage.vue +++ b/fe/src/pages/ExpensePage.vue @@ -1,13 +1,13 @@