diff --git a/be/app/api/v1/endpoints/chores.py b/be/app/api/v1/endpoints/chores.py
index 01a15d7..be38736 100644
--- a/be/app/api/v1/endpoints/chores.py
+++ b/be/app/api/v1/endpoints/chores.py
@@ -231,7 +231,7 @@ async def update_group_chore(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Chore's group_id if provided must match path group_id ({group_id}).")
# Ensure chore_in has the correct type for the CRUD operation
- chore_payload = chore_in.model_copy(update={"type": ChoreTypeEnum.group, "group_id": group_id} if chore_in.type is None else chore_in)
+ chore_payload = chore_in.model_copy(update={"type": ChoreTypeEnum.group, "group_id": group_id} if chore_in.type is None else {"group_id": group_id})
try:
updated_chore = await crud_chore.update_chore(db=db, chore_id=chore_id, chore_in=chore_payload, user_id=current_user.id, group_id=group_id)
diff --git a/be/app/api/v1/endpoints/invites.py b/be/app/api/v1/endpoints/invites.py
index 51d0bcf..b1be222 100644
--- a/be/app/api/v1/endpoints/invites.py
+++ b/be/app/api/v1/endpoints/invites.py
@@ -8,6 +8,7 @@ from app.auth import current_active_user
from app.models import User as UserModel, UserRoleEnum
from app.schemas.invite import InviteAccept
from app.schemas.message import Message
+from app.schemas.group import GroupPublic
from app.crud import invite as crud_invite
from app.crud import group as crud_group
from app.core.exceptions import (
@@ -16,7 +17,8 @@ from app.core.exceptions import (
InviteAlreadyUsedError,
InviteCreationError,
GroupNotFoundError,
- GroupMembershipError
+ GroupMembershipError,
+ GroupOperationError
)
logger = logging.getLogger(__name__)
@@ -24,7 +26,7 @@ router = APIRouter()
@router.post(
"/accept", # Route relative to prefix "/invites"
- response_model=Message,
+ response_model=GroupPublic,
summary="Accept Group Invite",
tags=["Invites"]
)
@@ -34,28 +36,19 @@ async def accept_invite(
current_user: UserModel = Depends(current_active_user),
):
"""Accepts a group invite using the provided invite code."""
- logger.info(f"User {current_user.email} attempting to accept invite code: {invite_in.invite_code}")
+ logger.info(f"User {current_user.email} attempting to accept invite code: {invite_in.code}")
- # Get the invite
- invite = await crud_invite.get_invite_by_code(db, invite_code=invite_in.invite_code)
+ # Get the invite - this function should only return valid, active invites
+ invite = await crud_invite.get_active_invite_by_code(db, code=invite_in.code)
if not invite:
- logger.warning(f"Invalid invite code attempted by user {current_user.email}: {invite_in.invite_code}")
- raise InviteNotFoundError(invite_in.invite_code)
-
- # Check if invite is expired
- if invite.is_expired():
- logger.warning(f"Expired invite code attempted by user {current_user.email}: {invite_in.invite_code}")
- raise InviteExpiredError(invite_in.invite_code)
-
- # Check if invite has already been used
- if invite.used_at:
- logger.warning(f"Already used invite code attempted by user {current_user.email}: {invite_in.invite_code}")
- raise InviteAlreadyUsedError(invite_in.invite_code)
+ logger.warning(f"Invalid or inactive invite code attempted by user {current_user.email}: {invite_in.code}")
+ # We can use a more generic error or a specific one. InviteNotFound is reasonable.
+ raise InviteNotFoundError(invite_in.code)
# Check if group still exists
group = await crud_group.get_group_by_id(db, group_id=invite.group_id)
if not group:
- logger.error(f"Group {invite.group_id} not found for invite {invite_in.invite_code}")
+ logger.error(f"Group {invite.group_id} not found for invite {invite_in.code}")
raise GroupNotFoundError(invite.group_id)
# Check if user is already a member
@@ -64,11 +57,23 @@ async def accept_invite(
logger.warning(f"User {current_user.email} already a member of group {invite.group_id}")
raise GroupMembershipError(invite.group_id, "join (already a member)")
- # Add user to group and mark invite as used
- success = await crud_invite.accept_invite(db, invite=invite, user_id=current_user.id)
- if not success:
- logger.error(f"Failed to accept invite {invite_in.invite_code} for user {current_user.email}")
- raise InviteCreationError(invite.group_id)
+ # Add user to the group
+ added_to_group = await crud_group.add_user_to_group(db, group_id=invite.group_id, user_id=current_user.id)
+ if not added_to_group:
+ logger.error(f"Failed to add user {current_user.email} to group {invite.group_id} during invite acceptance.")
+ # This could be a race condition or other issue, treat as an operational error.
+ raise GroupOperationError("Failed to add user to group.")
- logger.info(f"User {current_user.email} successfully joined group {invite.group_id} via invite {invite_in.invite_code}")
- return Message(detail="Successfully joined the group")
\ No newline at end of file
+ # Deactivate the invite so it cannot be used again
+ await crud_invite.deactivate_invite(db, invite=invite)
+
+ logger.info(f"User {current_user.email} successfully joined group {invite.group_id} via invite {invite_in.code}")
+
+ # Re-fetch the group to get the updated member list
+ updated_group = await crud_group.get_group_by_id(db, group_id=invite.group_id)
+ if not updated_group:
+ # This should ideally not happen as we found it before
+ logger.error(f"Could not re-fetch group {invite.group_id} after user {current_user.email} joined.")
+ raise GroupNotFoundError(invite.group_id)
+
+ return updated_group
\ No newline at end of file
diff --git a/fe/.prettierrc.json b/fe/.prettierrc.json
index 29a2402..18b1821 100644
--- a/fe/.prettierrc.json
+++ b/fe/.prettierrc.json
@@ -2,5 +2,5 @@
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
- "printWidth": 100
-}
+ "printWidth": 150
+}
\ No newline at end of file
diff --git a/fe/src/assets/valerie-ui.scss b/fe/src/assets/valerie-ui.scss
index b13bafd..f3d916b 100644
--- a/fe/src/assets/valerie-ui.scss
+++ b/fe/src/assets/valerie-ui.scss
@@ -944,7 +944,7 @@ select.form-input {
max-width: 550px;
box-shadow: var(--shadow-lg);
position: relative;
- /* overflow: hidden; */
+ overflow-y: scroll;
/* Can cause tooltip clipping */
transform: scale(0.95) translateY(-20px);
transition: transform var(--transition-speed) var(--transition-ease-out);
diff --git a/fe/src/config/api-config.ts b/fe/src/config/api-config.ts
index 6556789..8d49ce1 100644
--- a/fe/src/config/api-config.ts
+++ b/fe/src/config/api-config.ts
@@ -2,7 +2,7 @@
export const API_VERSION = 'v1'
// API Base URL
-export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'https://mitlistbe.mohamad.dev'
+export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'http://localhost:8000'
// API Endpoints
export const API_ENDPOINTS = {
@@ -32,6 +32,8 @@ export const API_ENDPOINTS = {
LISTS: {
BASE: '/lists',
BY_ID: (id: string) => `/lists/${id}`,
+ STATUS: (id: string) => `/lists/${id}/status`,
+ STATUSES: '/lists/statuses',
ITEMS: (listId: string) => `/lists/${listId}/items`,
ITEM: (listId: string, itemId: string) => `/lists/${listId}/items/${itemId}`,
EXPENSES: (listId: string) => `/lists/${listId}/expenses`,
@@ -66,7 +68,7 @@ export const API_ENDPOINTS = {
INVITES: {
BASE: '/invites',
BY_ID: (id: string) => `/invites/${id}`,
- ACCEPT: (id: string) => `/invites/accept/${id}`,
+ ACCEPT: '/invites/accept',
DECLINE: (id: string) => `/invites/decline/${id}`,
REVOKE: (id: string) => `/invites/revoke/${id}`,
LIST: '/invites',
diff --git a/fe/src/i18n/en.json b/fe/src/i18n/en.json
index 7136cb2..62afc82 100644
--- a/fe/src/i18n/en.json
+++ b/fe/src/i18n/en.json
@@ -90,95 +90,17 @@
},
"choresPage": {
"title": "Chores",
- "tabs": {
- "overdue": "Overdue",
- "today": "Today",
- "upcoming": "Upcoming",
- "allPending": "All Pending",
- "completed": "Completed"
- },
- "viewToggle": {
- "calendarLabel": "Calendar View",
- "calendarText": "Calendar",
- "listLabel": "List View",
- "listText": "List"
- },
- "newChoreButtonLabel": "New Chore",
- "newChoreButtonText": "New Chore",
- "loadingState": {
- "loadingChores": "Loading chores..."
- },
- "calendar": {
- "prevMonthLabel": "Previous month",
- "nextMonthLabel": "Next month",
- "weekdays": {
- "sun": "Sun",
- "mon": "Mon",
- "tue": "Tue",
- "wed": "Wed",
- "thu": "Thu",
- "fri": "Fri",
- "sat": "Sat"
- },
- "addChoreToDayLabel": "Add chore to this day",
- "emptyState": "No chores to display for this period."
- },
- "listView": {
- "choreTypePersonal": "Personal",
- "choreTypeGroupFallback": "Group",
- "completedDatePrefix": "Completed:",
- "actions": {
- "doneTitle": "Mark as Done",
- "doneText": "Done",
- "undoTitle": "Mark as Not Done",
- "undoText": "Undo",
- "editTitle": "Edit",
- "editLabel": "Edit chore",
- "editText": "Edit",
- "deleteTitle": "Delete",
- "deleteLabel": "Delete chore",
- "deleteText": "Delete"
- },
- "emptyState": {
- "message": "No chores in this view. Well done!",
- "viewAllButton": "View All Pending"
- }
- },
- "choreModal": {
- "editTitle": "Edit Chore",
- "newTitle": "New Chore",
- "closeButtonLabel": "Close modal",
- "nameLabel": "Name",
- "namePlaceholder": "Enter chore name",
- "typeLabel": "Type",
- "typePersonal": "Personal",
- "typeGroup": "Group",
- "groupLabel": "Group",
- "groupSelectDefault": "Select a group",
- "descriptionLabel": "Description",
- "descriptionPlaceholder": "Add a description (optional)",
- "frequencyLabel": "Frequency",
- "intervalLabel": "Interval (days)",
- "intervalPlaceholder": "e.g. 3",
- "dueDateLabel": "Due Date",
- "quickDueDateToday": "Today",
- "quickDueDateTomorrow": "Tomorrow",
- "quickDueDateNextWeek": "Next Week",
- "cancelButton": "Cancel",
- "saveButton": "Save"
- },
- "deleteDialog": {
- "title": "Delete Chore",
- "confirmationText": "Are you sure you want to delete this chore? This action cannot be undone.",
- "deleteButton": "Delete"
- },
- "shortcutsModal": {
- "title": "Keyboard Shortcuts",
- "descNewChore": "New Chore",
- "descToggleView": "Toggle View (List/Calendar)",
- "descToggleShortcuts": "Show/Hide Shortcuts",
- "descCloseModal": "Close any open Modal/Dialog"
+ "addChore": "+",
+ "edit": "Edit",
+ "delete": "Delete",
+ "empty": {
+ "title": "No Chores Yet",
+ "message": "Get started by adding your first chore!",
+ "addFirstChore": "Add First Chore"
},
+ "today": "Today",
+ "completedToday": "Completed today",
+ "completedOn": "Completed on {date}",
"frequencyOptions": {
"oneTime": "One Time",
"daily": "Daily",
@@ -186,34 +108,44 @@
"monthly": "Monthly",
"custom": "Custom"
},
- "formatters": {
- "noDueDate": "No due date",
- "dueToday": "Due Today",
- "dueTomorrow": "Due Tomorrow",
- "overdueFull": "Overdue: {date}",
- "dueFull": "Due {date}",
- "invalidDate": "Invalid Date"
+ "frequency": {
+ "customInterval": "Every {n} day | Every {n} days"
+ },
+ "form": {
+ "name": "Name",
+ "description": "Description",
+ "dueDate": "Due Date",
+ "frequency": "Frequency",
+ "interval": "Interval (days)",
+ "type": "Type",
+ "personal": "Personal",
+ "group": "Group",
+ "assignGroup": "Assign to Group",
+ "cancel": "Cancel",
+ "save": "Save Changes",
+ "create": "Create",
+ "editChore": "Edit Chore",
+ "createChore": "Create Chore"
+ },
+ "deleteConfirm": {
+ "title": "Confirm Deletion",
+ "message": "Really want to delete? This action cannot be undone.",
+ "cancel": "Cancel",
+ "delete": "Delete"
},
"notifications": {
- "loadFailed": "Failed to load chores",
- "updateSuccess": "Chore '{name}' updated successfully",
- "createSuccess": "Chore '{name}' created successfully",
- "updateFailed": "Failed to update chore",
- "createFailed": "Failed to create chore",
- "deleteSuccess": "Chore '{name}' deleted successfully",
- "deleteFailed": "Failed to delete chore",
- "markedDone": "{name} marked as done.",
- "markedNotDone": "{name} marked as not done.",
- "statusUpdateFailed": "Failed to update chore status."
- },
- "validation": {
- "nameRequired": "Chore name is required.",
- "groupRequired": "Please select a group for group chores.",
- "intervalRequired": "Custom interval must be at least 1 day.",
- "dueDateRequired": "Due date is required.",
- "invalidDueDate": "Invalid due date format."
- },
- "unsavedChangesConfirmation": "You have unsaved changes in the chore form. Are you sure you want to leave?"
+ "loadFailed": "Failed to load chores.",
+ "loadGroupsFailed": "Failed to load groups.",
+ "updateSuccess": "Chore updated successfully!",
+ "createSuccess": "Chore created successfully!",
+ "saveFailed": "Failed to save the chore.",
+ "deleteSuccess": "Chore deleted successfully.",
+ "deleteFailed": "Failed to delete chore.",
+ "completed": "Chore marked as complete!",
+ "uncompleted": "Chore marked as incomplete.",
+ "updateFailed": "Failed to update chore status.",
+ "createAssignmentFailed": "Failed to create assignment for chore."
+ }
},
"errorNotFoundPage": {
"errorCode": "404",
diff --git a/fe/src/pages/ChoresPage.vue b/fe/src/pages/ChoresPage.vue
index bbb01ea..b4104b8 100644
--- a/fe/src/pages/ChoresPage.vue
+++ b/fe/src/pages/ChoresPage.vue
@@ -1,353 +1,10 @@
-
-
-
-
-
-
-
-
{{ t('choresPage.loadingState.loadingChores') }}
-
-
-
-
-
-
-
-
calendar_today
-
{{ t('choresPage.calendar.emptyState') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Rtask_alt
-
{{ t('choresPage.listView.emptyState.message') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ t('choresPage.deleteDialog.confirmationText') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Ctrl/Cmd + N
-
-
{{ t('choresPage.shortcutsModal.descNewChore') }}
-
-
-
- Ctrl/Cmd + /
-
-
{{ t('choresPage.shortcutsModal.descToggleView') }}
-
-
-
- Ctrl/Cmd + ?
-
-
{{ t('choresPage.shortcutsModal.descToggleShortcuts') }}
-
-
-
- Esc
-
-
{{ t('choresPage.shortcutsModal.descCloseModal') }}
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/fe/src/pages/GroupsPage.vue b/fe/src/pages/GroupsPage.vue
index 8578b7a..d08674d 100644
--- a/fe/src/pages/GroupsPage.vue
+++ b/fe/src/pages/GroupsPage.vue
@@ -85,7 +85,7 @@