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 @@ - - - - - \ 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 @@