feat(i18n): Internationalize remaining app pages
This commit completes the internationalization (i18n) for several key pages within the frontend application. The following pages have been updated to support multiple languages: - AccountPage.vue - SignupPage.vue - ListDetailPage.vue (including items, OCR, expenses, and cost summary) - MyChoresPage.vue - PersonalChoresPage.vue - IndexPage.vue Key changes include: - Extraction of all user-facing strings from these Vue components. - Addition of new translation keys and their English values to `fe/src/i18n/en.json`. - Modification of the Vue components to use the Vue I18n plugin's `$t()` (template) and `t()` (script) functions for displaying translated strings. - Dynamic messages, notifications, and form validation messages are now also internationalized. - The language files `de.json`, `es.json`, and `fr.json` have been updated with the new keys, using the English text as placeholders for future translation. This effort significantly expands the i18n coverage of the application, making it more accessible to a wider audience.
This commit is contained in:
parent
5c9ba3f38c
commit
4effbf5c03
@ -270,5 +270,292 @@
|
||||
"removeMemberSuccess": "DE: Member removed successfully",
|
||||
"removeMemberFailed": "DE: Failed to remove member"
|
||||
}
|
||||
},
|
||||
"accountPage": {
|
||||
"title": "Account Settings",
|
||||
"loadingProfile": "Loading profile...",
|
||||
"retryButton": "Retry",
|
||||
"profileSection": {
|
||||
"header": "Profile Information",
|
||||
"nameLabel": "Name",
|
||||
"emailLabel": "Email",
|
||||
"saveButton": "Save Changes"
|
||||
},
|
||||
"passwordSection": {
|
||||
"header": "Change Password",
|
||||
"currentPasswordLabel": "Current Password",
|
||||
"newPasswordLabel": "New Password",
|
||||
"changeButton": "Change Password"
|
||||
},
|
||||
"notificationsSection": {
|
||||
"header": "Notification Preferences",
|
||||
"emailNotificationsLabel": "Email Notifications",
|
||||
"emailNotificationsDescription": "Receive email notifications for important updates",
|
||||
"listUpdatesLabel": "List Updates",
|
||||
"listUpdatesDescription": "Get notified when lists are updated",
|
||||
"groupActivitiesLabel": "Group Activities",
|
||||
"groupActivitiesDescription": "Receive notifications for group activities"
|
||||
},
|
||||
"notifications": {
|
||||
"profileLoadFailed": "Failed to load profile",
|
||||
"profileUpdateSuccess": "Profile updated successfully",
|
||||
"profileUpdateFailed": "Failed to update profile",
|
||||
"passwordFieldsRequired": "Please fill in both current and new password fields.",
|
||||
"passwordTooShort": "New password must be at least 8 characters long.",
|
||||
"passwordChangeSuccess": "Password changed successfully",
|
||||
"passwordChangeFailed": "Failed to change password",
|
||||
"preferencesUpdateSuccess": "Preferences updated successfully",
|
||||
"preferencesUpdateFailed": "Failed to update preferences"
|
||||
},
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"signupPage": {
|
||||
"header": "Sign Up",
|
||||
"fullNameLabel": "Full Name",
|
||||
"emailLabel": "Email",
|
||||
"passwordLabel": "Password",
|
||||
"confirmPasswordLabel": "Confirm Password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"submitButton": "Sign Up",
|
||||
"loginLink": "Already have an account? Login",
|
||||
"validation": {
|
||||
"nameRequired": "Name is required",
|
||||
"emailRequired": "Email is required",
|
||||
"emailInvalid": "Invalid email format",
|
||||
"passwordRequired": "Password is required",
|
||||
"passwordLength": "Password must be at least 8 characters",
|
||||
"confirmPasswordRequired": "Please confirm your password",
|
||||
"passwordsNoMatch": "Passwords do not match"
|
||||
},
|
||||
"notifications": {
|
||||
"signupFailed": "Signup failed. Please try again.",
|
||||
"signupSuccess": "Account created successfully. Please login."
|
||||
}
|
||||
},
|
||||
"listDetailPage": {
|
||||
"loading": {
|
||||
"list": "Loading list...",
|
||||
"items": "Loading items...",
|
||||
"ocrProcessing": "Processing image...",
|
||||
"addingOcrItems": "Adding OCR items...",
|
||||
"costSummary": "Loading summary...",
|
||||
"expenses": "Loading expenses...",
|
||||
"settlement": "Processing settlement..."
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "Failed to load list details.",
|
||||
"genericLoadFailure": "Group not found or an error occurred.",
|
||||
"ocrNoItems": "No items extracted from the image.",
|
||||
"ocrFailed": "Failed to process image.",
|
||||
"addItemFailed": "Failed to add item.",
|
||||
"updateItemFailed": "Failed to update item.",
|
||||
"updateItemPriceFailed": "Failed to update item price.",
|
||||
"deleteItemFailed": "Failed to delete item.",
|
||||
"addOcrItemsFailed": "Failed to add OCR items.",
|
||||
"fetchItemsFailed": "Failed to load items: {errorMessage}",
|
||||
"loadCostSummaryFailed": "Failed to load cost summary."
|
||||
},
|
||||
"retryButton": "Retry",
|
||||
"buttons": {
|
||||
"addViaOcr": "Add via OCR",
|
||||
"addItem": "Add",
|
||||
"addItems": "Add Items",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"saveChanges": "Save Changes",
|
||||
"close": "Close",
|
||||
"costSummary": "Cost Summary"
|
||||
},
|
||||
"badges": {
|
||||
"groupList": "Group List",
|
||||
"personalList": "Personal List"
|
||||
},
|
||||
"items": {
|
||||
"emptyState": {
|
||||
"title": "No Items Yet!",
|
||||
"message": "Add some items using the form below."
|
||||
},
|
||||
"addItemForm": {
|
||||
"placeholder": "Add a new item",
|
||||
"quantityPlaceholder": "Qty",
|
||||
"itemNameSrLabel": "New item name",
|
||||
"quantitySrLabel": "Quantity"
|
||||
},
|
||||
"pricePlaceholder": "Price",
|
||||
"editItemAriaLabel": "Edit item",
|
||||
"deleteItemAriaLabel": "Delete item"
|
||||
},
|
||||
"modals": {
|
||||
"ocr": {
|
||||
"title": "Add Items via OCR",
|
||||
"uploadLabel": "Upload Image"
|
||||
},
|
||||
"confirmation": {
|
||||
"title": "Confirmation"
|
||||
},
|
||||
"editItem": {
|
||||
"title": "Edit Item",
|
||||
"nameLabel": "Item Name",
|
||||
"quantityLabel": "Quantity"
|
||||
},
|
||||
"costSummary": {
|
||||
"title": "List Cost Summary",
|
||||
"totalCostLabel": "Total List Cost:",
|
||||
"equalShareLabel": "Equal Share Per User:",
|
||||
"participantsLabel": "Participating Users:",
|
||||
"userBalancesHeader": "User Balances",
|
||||
"tableHeaders": {
|
||||
"user": "User",
|
||||
"itemsAddedValue": "Items Added Value",
|
||||
"amountDue": "Amount Due",
|
||||
"balance": "Balance"
|
||||
},
|
||||
"emptyState": "No cost summary available."
|
||||
},
|
||||
"settleShare": {
|
||||
"title": "Settle Share",
|
||||
"settleAmountFor": "Settle amount for {userName}:",
|
||||
"amountLabel": "Amount",
|
||||
"errors": {
|
||||
"enterAmount": "Please enter an amount.",
|
||||
"positiveAmount": "Please enter a positive amount.",
|
||||
"exceedsRemaining": "Amount cannot exceed remaining: {amount}.",
|
||||
"noSplitSelected": "Error: No split selected."
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirmations": {
|
||||
"updateMessage": "Mark '{itemName}' as {status}?",
|
||||
"statusComplete": "complete",
|
||||
"statusIncomplete": "incomplete",
|
||||
"deleteMessage": "Delete '{itemName}'? This cannot be undone."
|
||||
},
|
||||
"notifications": {
|
||||
"itemAddedSuccess": "Item added successfully.",
|
||||
"itemsAddedSuccessOcr": "{count} item(s) added successfully from OCR.",
|
||||
"itemUpdatedSuccess": "Item updated successfully.",
|
||||
"itemDeleteSuccess": "Item deleted successfully.",
|
||||
"enterItemName": "Please enter an item name.",
|
||||
"costSummaryLoadFailed": "Failed to load cost summary.",
|
||||
"cannotSettleOthersShares": "You can only settle your own shares.",
|
||||
"settlementDataMissing": "Cannot process settlement: missing data.",
|
||||
"settleShareSuccess": "Share settled successfully!",
|
||||
"settleShareFailed": "Failed to settle share."
|
||||
},
|
||||
"expensesSection": {
|
||||
"title": "Expenses",
|
||||
"addExpenseButton": "Add Expense",
|
||||
"loading": "Loading expenses...",
|
||||
"emptyState": "No expenses recorded for this list yet.",
|
||||
"paidBy": "Paid by:",
|
||||
"onDate": "on",
|
||||
"owes": "owes",
|
||||
"paidAmount": "Paid:",
|
||||
"activityLabel": "Activity:",
|
||||
"byUser": "by",
|
||||
"settleShareButton": "Settle My Share",
|
||||
"retryButton": "Retry"
|
||||
},
|
||||
"status": {
|
||||
"settled": "Settled",
|
||||
"partiallySettled": "Partially Settled",
|
||||
"unsettled": "Unsettled",
|
||||
"paid": "Paid",
|
||||
"partiallyPaid": "Partially Paid",
|
||||
"unpaid": "Unpaid",
|
||||
"unknown": "Unknown Status"
|
||||
}
|
||||
},
|
||||
"myChoresPage": {
|
||||
"title": "My Assigned Chores",
|
||||
"showCompletedToggle": "Show Completed",
|
||||
"timelineHeaders": {
|
||||
"overdue": "Overdue",
|
||||
"today": "Due Today",
|
||||
"thisWeek": "This Week",
|
||||
"later": "Later",
|
||||
"completed": "Completed"
|
||||
},
|
||||
"choreCard": {
|
||||
"personal": "Personal",
|
||||
"group": "Group",
|
||||
"duePrefix": "Due",
|
||||
"completedPrefix": "Completed",
|
||||
"dueToday": "Due Today",
|
||||
"markCompleteButton": "Mark Complete"
|
||||
},
|
||||
"frequencies": {
|
||||
"one_time": "One Time",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"custom": "Custom",
|
||||
"unknown": "Unknown Frequency"
|
||||
},
|
||||
"dates": {
|
||||
"invalidDate": "Invalid Date",
|
||||
"unknownDate": "Unknown Date"
|
||||
},
|
||||
"emptyState": {
|
||||
"title": "No Assignments Yet!",
|
||||
"noAssignmentsPending": "You have no pending chore assignments.",
|
||||
"noAssignmentsAll": "You have no chore assignments (completed or pending).",
|
||||
"viewAllChoresButton": "View All Chores"
|
||||
},
|
||||
"notifications": {
|
||||
"loadFailed": "Failed to load assignments",
|
||||
"markedComplete": "Marked \"{choreName}\" as complete!",
|
||||
"markCompleteFailed": "Failed to mark assignment as complete"
|
||||
}
|
||||
},
|
||||
"personalChoresPage": {
|
||||
"title": "Personal Chores",
|
||||
"newChoreButton": "New Chore",
|
||||
"editButton": "Edit",
|
||||
"deleteButton": "Delete",
|
||||
"cancelButton": "Cancel",
|
||||
"saveButton": "Save",
|
||||
"modals": {
|
||||
"editChoreTitle": "Edit Chore",
|
||||
"newChoreTitle": "New Chore",
|
||||
"deleteChoreTitle": "Delete Chore"
|
||||
},
|
||||
"form": {
|
||||
"nameLabel": "Name",
|
||||
"descriptionLabel": "Description",
|
||||
"frequencyLabel": "Frequency",
|
||||
"intervalLabel": "Interval (days)",
|
||||
"dueDateLabel": "Next Due Date"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"confirmationText": "Are you sure you want to delete this chore?"
|
||||
},
|
||||
"frequencies": {
|
||||
"one_time": "One Time",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"custom": "Custom",
|
||||
"unknown": "Unknown Frequency"
|
||||
},
|
||||
"dates": {
|
||||
"invalidDate": "Invalid Date",
|
||||
"duePrefix": "Due"
|
||||
},
|
||||
"notifications": {
|
||||
"loadFailed": "Failed to load personal chores",
|
||||
"updateSuccess": "Personal chore updated successfully",
|
||||
"createSuccess": "Personal chore created successfully",
|
||||
"saveFailed": "Failed to save personal chore",
|
||||
"deleteSuccess": "Personal chore deleted successfully",
|
||||
"deleteFailed": "Failed to delete personal chore"
|
||||
}
|
||||
},
|
||||
"indexPage": {
|
||||
"welcomeMessage": "Welcome to Valerie UI App",
|
||||
"mainPageInfo": "This is the main index page.",
|
||||
"sampleTodosHeader": "Sample Todos (from IndexPage data)",
|
||||
"totalCountLabel": "Total count from meta:",
|
||||
"noTodos": "No todos to display."
|
||||
}
|
||||
}
|
||||
|
@ -270,5 +270,292 @@
|
||||
"removeMemberSuccess": "Member removed successfully",
|
||||
"removeMemberFailed": "Failed to remove member"
|
||||
}
|
||||
},
|
||||
"accountPage": {
|
||||
"title": "Account Settings",
|
||||
"loadingProfile": "Loading profile...",
|
||||
"retryButton": "Retry",
|
||||
"profileSection": {
|
||||
"header": "Profile Information",
|
||||
"nameLabel": "Name",
|
||||
"emailLabel": "Email",
|
||||
"saveButton": "Save Changes"
|
||||
},
|
||||
"passwordSection": {
|
||||
"header": "Change Password",
|
||||
"currentPasswordLabel": "Current Password",
|
||||
"newPasswordLabel": "New Password",
|
||||
"changeButton": "Change Password"
|
||||
},
|
||||
"notificationsSection": {
|
||||
"header": "Notification Preferences",
|
||||
"emailNotificationsLabel": "Email Notifications",
|
||||
"emailNotificationsDescription": "Receive email notifications for important updates",
|
||||
"listUpdatesLabel": "List Updates",
|
||||
"listUpdatesDescription": "Get notified when lists are updated",
|
||||
"groupActivitiesLabel": "Group Activities",
|
||||
"groupActivitiesDescription": "Receive notifications for group activities"
|
||||
},
|
||||
"notifications": {
|
||||
"profileLoadFailed": "Failed to load profile",
|
||||
"profileUpdateSuccess": "Profile updated successfully",
|
||||
"profileUpdateFailed": "Failed to update profile",
|
||||
"passwordFieldsRequired": "Please fill in both current and new password fields.",
|
||||
"passwordTooShort": "New password must be at least 8 characters long.",
|
||||
"passwordChangeSuccess": "Password changed successfully",
|
||||
"passwordChangeFailed": "Failed to change password",
|
||||
"preferencesUpdateSuccess": "Preferences updated successfully",
|
||||
"preferencesUpdateFailed": "Failed to update preferences"
|
||||
},
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"signupPage": {
|
||||
"header": "Sign Up",
|
||||
"fullNameLabel": "Full Name",
|
||||
"emailLabel": "Email",
|
||||
"passwordLabel": "Password",
|
||||
"confirmPasswordLabel": "Confirm Password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"submitButton": "Sign Up",
|
||||
"loginLink": "Already have an account? Login",
|
||||
"validation": {
|
||||
"nameRequired": "Name is required",
|
||||
"emailRequired": "Email is required",
|
||||
"emailInvalid": "Invalid email format",
|
||||
"passwordRequired": "Password is required",
|
||||
"passwordLength": "Password must be at least 8 characters",
|
||||
"confirmPasswordRequired": "Please confirm your password",
|
||||
"passwordsNoMatch": "Passwords do not match"
|
||||
},
|
||||
"notifications": {
|
||||
"signupFailed": "Signup failed. Please try again.",
|
||||
"signupSuccess": "Account created successfully. Please login."
|
||||
}
|
||||
},
|
||||
"listDetailPage": {
|
||||
"loading": {
|
||||
"list": "Loading list...",
|
||||
"items": "Loading items...",
|
||||
"ocrProcessing": "Processing image...",
|
||||
"addingOcrItems": "Adding OCR items...",
|
||||
"costSummary": "Loading summary...",
|
||||
"expenses": "Loading expenses...",
|
||||
"settlement": "Processing settlement..."
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "Failed to load list details.",
|
||||
"genericLoadFailure": "Group not found or an error occurred.",
|
||||
"ocrNoItems": "No items extracted from the image.",
|
||||
"ocrFailed": "Failed to process image.",
|
||||
"addItemFailed": "Failed to add item.",
|
||||
"updateItemFailed": "Failed to update item.",
|
||||
"updateItemPriceFailed": "Failed to update item price.",
|
||||
"deleteItemFailed": "Failed to delete item.",
|
||||
"addOcrItemsFailed": "Failed to add OCR items.",
|
||||
"fetchItemsFailed": "Failed to load items: {errorMessage}",
|
||||
"loadCostSummaryFailed": "Failed to load cost summary."
|
||||
},
|
||||
"retryButton": "Retry",
|
||||
"buttons": {
|
||||
"addViaOcr": "Add via OCR",
|
||||
"addItem": "Add",
|
||||
"addItems": "Add Items",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"saveChanges": "Save Changes",
|
||||
"close": "Close",
|
||||
"costSummary": "Cost Summary"
|
||||
},
|
||||
"badges": {
|
||||
"groupList": "Group List",
|
||||
"personalList": "Personal List"
|
||||
},
|
||||
"items": {
|
||||
"emptyState": {
|
||||
"title": "No Items Yet!",
|
||||
"message": "Add some items using the form below."
|
||||
},
|
||||
"addItemForm": {
|
||||
"placeholder": "Add a new item",
|
||||
"quantityPlaceholder": "Qty",
|
||||
"itemNameSrLabel": "New item name",
|
||||
"quantitySrLabel": "Quantity"
|
||||
},
|
||||
"pricePlaceholder": "Price",
|
||||
"editItemAriaLabel": "Edit item",
|
||||
"deleteItemAriaLabel": "Delete item"
|
||||
},
|
||||
"modals": {
|
||||
"ocr": {
|
||||
"title": "Add Items via OCR",
|
||||
"uploadLabel": "Upload Image"
|
||||
},
|
||||
"confirmation": {
|
||||
"title": "Confirmation"
|
||||
},
|
||||
"editItem": {
|
||||
"title": "Edit Item",
|
||||
"nameLabel": "Item Name",
|
||||
"quantityLabel": "Quantity"
|
||||
},
|
||||
"costSummary": {
|
||||
"title": "List Cost Summary",
|
||||
"totalCostLabel": "Total List Cost:",
|
||||
"equalShareLabel": "Equal Share Per User:",
|
||||
"participantsLabel": "Participating Users:",
|
||||
"userBalancesHeader": "User Balances",
|
||||
"tableHeaders": {
|
||||
"user": "User",
|
||||
"itemsAddedValue": "Items Added Value",
|
||||
"amountDue": "Amount Due",
|
||||
"balance": "Balance"
|
||||
},
|
||||
"emptyState": "No cost summary available."
|
||||
},
|
||||
"settleShare": {
|
||||
"title": "Settle Share",
|
||||
"settleAmountFor": "Settle amount for {userName}:",
|
||||
"amountLabel": "Amount",
|
||||
"errors": {
|
||||
"enterAmount": "Please enter an amount.",
|
||||
"positiveAmount": "Please enter a positive amount.",
|
||||
"exceedsRemaining": "Amount cannot exceed remaining: {amount}.",
|
||||
"noSplitSelected": "Error: No split selected."
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirmations": {
|
||||
"updateMessage": "Mark '{itemName}' as {status}?",
|
||||
"statusComplete": "complete",
|
||||
"statusIncomplete": "incomplete",
|
||||
"deleteMessage": "Delete '{itemName}'? This cannot be undone."
|
||||
},
|
||||
"notifications": {
|
||||
"itemAddedSuccess": "Item added successfully.",
|
||||
"itemsAddedSuccessOcr": "{count} item(s) added successfully from OCR.",
|
||||
"itemUpdatedSuccess": "Item updated successfully.",
|
||||
"itemDeleteSuccess": "Item deleted successfully.",
|
||||
"enterItemName": "Please enter an item name.",
|
||||
"costSummaryLoadFailed": "Failed to load cost summary.",
|
||||
"cannotSettleOthersShares": "You can only settle your own shares.",
|
||||
"settlementDataMissing": "Cannot process settlement: missing data.",
|
||||
"settleShareSuccess": "Share settled successfully!",
|
||||
"settleShareFailed": "Failed to settle share."
|
||||
},
|
||||
"expensesSection": {
|
||||
"title": "Expenses",
|
||||
"addExpenseButton": "Add Expense",
|
||||
"loading": "Loading expenses...",
|
||||
"emptyState": "No expenses recorded for this list yet.",
|
||||
"paidBy": "Paid by:",
|
||||
"onDate": "on",
|
||||
"owes": "owes",
|
||||
"paidAmount": "Paid:",
|
||||
"activityLabel": "Activity:",
|
||||
"byUser": "by",
|
||||
"settleShareButton": "Settle My Share",
|
||||
"retryButton": "Retry"
|
||||
},
|
||||
"status": {
|
||||
"settled": "Settled",
|
||||
"partiallySettled": "Partially Settled",
|
||||
"unsettled": "Unsettled",
|
||||
"paid": "Paid",
|
||||
"partiallyPaid": "Partially Paid",
|
||||
"unpaid": "Unpaid",
|
||||
"unknown": "Unknown Status"
|
||||
}
|
||||
},
|
||||
"myChoresPage": {
|
||||
"title": "My Assigned Chores",
|
||||
"showCompletedToggle": "Show Completed",
|
||||
"timelineHeaders": {
|
||||
"overdue": "Overdue",
|
||||
"today": "Due Today",
|
||||
"thisWeek": "This Week",
|
||||
"later": "Later",
|
||||
"completed": "Completed"
|
||||
},
|
||||
"choreCard": {
|
||||
"personal": "Personal",
|
||||
"group": "Group",
|
||||
"duePrefix": "Due",
|
||||
"completedPrefix": "Completed",
|
||||
"dueToday": "Due Today",
|
||||
"markCompleteButton": "Mark Complete"
|
||||
},
|
||||
"frequencies": {
|
||||
"one_time": "One Time",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"custom": "Custom",
|
||||
"unknown": "Unknown Frequency"
|
||||
},
|
||||
"dates": {
|
||||
"invalidDate": "Invalid Date",
|
||||
"unknownDate": "Unknown Date"
|
||||
},
|
||||
"emptyState": {
|
||||
"title": "No Assignments Yet!",
|
||||
"noAssignmentsPending": "You have no pending chore assignments.",
|
||||
"noAssignmentsAll": "You have no chore assignments (completed or pending).",
|
||||
"viewAllChoresButton": "View All Chores"
|
||||
},
|
||||
"notifications": {
|
||||
"loadFailed": "Failed to load assignments",
|
||||
"markedComplete": "Marked \"{choreName}\" as complete!",
|
||||
"markCompleteFailed": "Failed to mark assignment as complete"
|
||||
}
|
||||
},
|
||||
"personalChoresPage": {
|
||||
"title": "Personal Chores",
|
||||
"newChoreButton": "New Chore",
|
||||
"editButton": "Edit",
|
||||
"deleteButton": "Delete",
|
||||
"cancelButton": "Cancel",
|
||||
"saveButton": "Save",
|
||||
"modals": {
|
||||
"editChoreTitle": "Edit Chore",
|
||||
"newChoreTitle": "New Chore",
|
||||
"deleteChoreTitle": "Delete Chore"
|
||||
},
|
||||
"form": {
|
||||
"nameLabel": "Name",
|
||||
"descriptionLabel": "Description",
|
||||
"frequencyLabel": "Frequency",
|
||||
"intervalLabel": "Interval (days)",
|
||||
"dueDateLabel": "Next Due Date"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"confirmationText": "Are you sure you want to delete this chore?"
|
||||
},
|
||||
"frequencies": {
|
||||
"one_time": "One Time",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"custom": "Custom",
|
||||
"unknown": "Unknown Frequency"
|
||||
},
|
||||
"dates": {
|
||||
"invalidDate": "Invalid Date",
|
||||
"duePrefix": "Due"
|
||||
},
|
||||
"notifications": {
|
||||
"loadFailed": "Failed to load personal chores",
|
||||
"updateSuccess": "Personal chore updated successfully",
|
||||
"createSuccess": "Personal chore created successfully",
|
||||
"saveFailed": "Failed to save personal chore",
|
||||
"deleteSuccess": "Personal chore deleted successfully",
|
||||
"deleteFailed": "Failed to delete personal chore"
|
||||
}
|
||||
},
|
||||
"indexPage": {
|
||||
"welcomeMessage": "Welcome to Valerie UI App",
|
||||
"mainPageInfo": "This is the main index page.",
|
||||
"sampleTodosHeader": "Sample Todos (from IndexPage data)",
|
||||
"totalCountLabel": "Total count from meta:",
|
||||
"noTodos": "No todos to display."
|
||||
}
|
||||
}
|
||||
|
@ -270,5 +270,292 @@
|
||||
"removeMemberSuccess": "ES: Member removed successfully",
|
||||
"removeMemberFailed": "ES: Failed to remove member"
|
||||
}
|
||||
},
|
||||
"accountPage": {
|
||||
"title": "Account Settings",
|
||||
"loadingProfile": "Loading profile...",
|
||||
"retryButton": "Retry",
|
||||
"profileSection": {
|
||||
"header": "Profile Information",
|
||||
"nameLabel": "Name",
|
||||
"emailLabel": "Email",
|
||||
"saveButton": "Save Changes"
|
||||
},
|
||||
"passwordSection": {
|
||||
"header": "Change Password",
|
||||
"currentPasswordLabel": "Current Password",
|
||||
"newPasswordLabel": "New Password",
|
||||
"changeButton": "Change Password"
|
||||
},
|
||||
"notificationsSection": {
|
||||
"header": "Notification Preferences",
|
||||
"emailNotificationsLabel": "Email Notifications",
|
||||
"emailNotificationsDescription": "Receive email notifications for important updates",
|
||||
"listUpdatesLabel": "List Updates",
|
||||
"listUpdatesDescription": "Get notified when lists are updated",
|
||||
"groupActivitiesLabel": "Group Activities",
|
||||
"groupActivitiesDescription": "Receive notifications for group activities"
|
||||
},
|
||||
"notifications": {
|
||||
"profileLoadFailed": "Failed to load profile",
|
||||
"profileUpdateSuccess": "Profile updated successfully",
|
||||
"profileUpdateFailed": "Failed to update profile",
|
||||
"passwordFieldsRequired": "Please fill in both current and new password fields.",
|
||||
"passwordTooShort": "New password must be at least 8 characters long.",
|
||||
"passwordChangeSuccess": "Password changed successfully",
|
||||
"passwordChangeFailed": "Failed to change password",
|
||||
"preferencesUpdateSuccess": "Preferences updated successfully",
|
||||
"preferencesUpdateFailed": "Failed to update preferences"
|
||||
},
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"signupPage": {
|
||||
"header": "Sign Up",
|
||||
"fullNameLabel": "Full Name",
|
||||
"emailLabel": "Email",
|
||||
"passwordLabel": "Password",
|
||||
"confirmPasswordLabel": "Confirm Password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"submitButton": "Sign Up",
|
||||
"loginLink": "Already have an account? Login",
|
||||
"validation": {
|
||||
"nameRequired": "Name is required",
|
||||
"emailRequired": "Email is required",
|
||||
"emailInvalid": "Invalid email format",
|
||||
"passwordRequired": "Password is required",
|
||||
"passwordLength": "Password must be at least 8 characters",
|
||||
"confirmPasswordRequired": "Please confirm your password",
|
||||
"passwordsNoMatch": "Passwords do not match"
|
||||
},
|
||||
"notifications": {
|
||||
"signupFailed": "Signup failed. Please try again.",
|
||||
"signupSuccess": "Account created successfully. Please login."
|
||||
}
|
||||
},
|
||||
"listDetailPage": {
|
||||
"loading": {
|
||||
"list": "Loading list...",
|
||||
"items": "Loading items...",
|
||||
"ocrProcessing": "Processing image...",
|
||||
"addingOcrItems": "Adding OCR items...",
|
||||
"costSummary": "Loading summary...",
|
||||
"expenses": "Loading expenses...",
|
||||
"settlement": "Processing settlement..."
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "Failed to load list details.",
|
||||
"genericLoadFailure": "Group not found or an error occurred.",
|
||||
"ocrNoItems": "No items extracted from the image.",
|
||||
"ocrFailed": "Failed to process image.",
|
||||
"addItemFailed": "Failed to add item.",
|
||||
"updateItemFailed": "Failed to update item.",
|
||||
"updateItemPriceFailed": "Failed to update item price.",
|
||||
"deleteItemFailed": "Failed to delete item.",
|
||||
"addOcrItemsFailed": "Failed to add OCR items.",
|
||||
"fetchItemsFailed": "Failed to load items: {errorMessage}",
|
||||
"loadCostSummaryFailed": "Failed to load cost summary."
|
||||
},
|
||||
"retryButton": "Retry",
|
||||
"buttons": {
|
||||
"addViaOcr": "Add via OCR",
|
||||
"addItem": "Add",
|
||||
"addItems": "Add Items",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"saveChanges": "Save Changes",
|
||||
"close": "Close",
|
||||
"costSummary": "Cost Summary"
|
||||
},
|
||||
"badges": {
|
||||
"groupList": "Group List",
|
||||
"personalList": "Personal List"
|
||||
},
|
||||
"items": {
|
||||
"emptyState": {
|
||||
"title": "No Items Yet!",
|
||||
"message": "Add some items using the form below."
|
||||
},
|
||||
"addItemForm": {
|
||||
"placeholder": "Add a new item",
|
||||
"quantityPlaceholder": "Qty",
|
||||
"itemNameSrLabel": "New item name",
|
||||
"quantitySrLabel": "Quantity"
|
||||
},
|
||||
"pricePlaceholder": "Price",
|
||||
"editItemAriaLabel": "Edit item",
|
||||
"deleteItemAriaLabel": "Delete item"
|
||||
},
|
||||
"modals": {
|
||||
"ocr": {
|
||||
"title": "Add Items via OCR",
|
||||
"uploadLabel": "Upload Image"
|
||||
},
|
||||
"confirmation": {
|
||||
"title": "Confirmation"
|
||||
},
|
||||
"editItem": {
|
||||
"title": "Edit Item",
|
||||
"nameLabel": "Item Name",
|
||||
"quantityLabel": "Quantity"
|
||||
},
|
||||
"costSummary": {
|
||||
"title": "List Cost Summary",
|
||||
"totalCostLabel": "Total List Cost:",
|
||||
"equalShareLabel": "Equal Share Per User:",
|
||||
"participantsLabel": "Participating Users:",
|
||||
"userBalancesHeader": "User Balances",
|
||||
"tableHeaders": {
|
||||
"user": "User",
|
||||
"itemsAddedValue": "Items Added Value",
|
||||
"amountDue": "Amount Due",
|
||||
"balance": "Balance"
|
||||
},
|
||||
"emptyState": "No cost summary available."
|
||||
},
|
||||
"settleShare": {
|
||||
"title": "Settle Share",
|
||||
"settleAmountFor": "Settle amount for {userName}:",
|
||||
"amountLabel": "Amount",
|
||||
"errors": {
|
||||
"enterAmount": "Please enter an amount.",
|
||||
"positiveAmount": "Please enter a positive amount.",
|
||||
"exceedsRemaining": "Amount cannot exceed remaining: {amount}.",
|
||||
"noSplitSelected": "Error: No split selected."
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirmations": {
|
||||
"updateMessage": "Mark '{itemName}' as {status}?",
|
||||
"statusComplete": "complete",
|
||||
"statusIncomplete": "incomplete",
|
||||
"deleteMessage": "Delete '{itemName}'? This cannot be undone."
|
||||
},
|
||||
"notifications": {
|
||||
"itemAddedSuccess": "Item added successfully.",
|
||||
"itemsAddedSuccessOcr": "{count} item(s) added successfully from OCR.",
|
||||
"itemUpdatedSuccess": "Item updated successfully.",
|
||||
"itemDeleteSuccess": "Item deleted successfully.",
|
||||
"enterItemName": "Please enter an item name.",
|
||||
"costSummaryLoadFailed": "Failed to load cost summary.",
|
||||
"cannotSettleOthersShares": "You can only settle your own shares.",
|
||||
"settlementDataMissing": "Cannot process settlement: missing data.",
|
||||
"settleShareSuccess": "Share settled successfully!",
|
||||
"settleShareFailed": "Failed to settle share."
|
||||
},
|
||||
"expensesSection": {
|
||||
"title": "Expenses",
|
||||
"addExpenseButton": "Add Expense",
|
||||
"loading": "Loading expenses...",
|
||||
"emptyState": "No expenses recorded for this list yet.",
|
||||
"paidBy": "Paid by:",
|
||||
"onDate": "on",
|
||||
"owes": "owes",
|
||||
"paidAmount": "Paid:",
|
||||
"activityLabel": "Activity:",
|
||||
"byUser": "by",
|
||||
"settleShareButton": "Settle My Share",
|
||||
"retryButton": "Retry"
|
||||
},
|
||||
"status": {
|
||||
"settled": "Settled",
|
||||
"partiallySettled": "Partially Settled",
|
||||
"unsettled": "Unsettled",
|
||||
"paid": "Paid",
|
||||
"partiallyPaid": "Partially Paid",
|
||||
"unpaid": "Unpaid",
|
||||
"unknown": "Unknown Status"
|
||||
}
|
||||
},
|
||||
"myChoresPage": {
|
||||
"title": "My Assigned Chores",
|
||||
"showCompletedToggle": "Show Completed",
|
||||
"timelineHeaders": {
|
||||
"overdue": "Overdue",
|
||||
"today": "Due Today",
|
||||
"thisWeek": "This Week",
|
||||
"later": "Later",
|
||||
"completed": "Completed"
|
||||
},
|
||||
"choreCard": {
|
||||
"personal": "Personal",
|
||||
"group": "Group",
|
||||
"duePrefix": "Due",
|
||||
"completedPrefix": "Completed",
|
||||
"dueToday": "Due Today",
|
||||
"markCompleteButton": "Mark Complete"
|
||||
},
|
||||
"frequencies": {
|
||||
"one_time": "One Time",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"custom": "Custom",
|
||||
"unknown": "Unknown Frequency"
|
||||
},
|
||||
"dates": {
|
||||
"invalidDate": "Invalid Date",
|
||||
"unknownDate": "Unknown Date"
|
||||
},
|
||||
"emptyState": {
|
||||
"title": "No Assignments Yet!",
|
||||
"noAssignmentsPending": "You have no pending chore assignments.",
|
||||
"noAssignmentsAll": "You have no chore assignments (completed or pending).",
|
||||
"viewAllChoresButton": "View All Chores"
|
||||
},
|
||||
"notifications": {
|
||||
"loadFailed": "Failed to load assignments",
|
||||
"markedComplete": "Marked \"{choreName}\" as complete!",
|
||||
"markCompleteFailed": "Failed to mark assignment as complete"
|
||||
}
|
||||
},
|
||||
"personalChoresPage": {
|
||||
"title": "Personal Chores",
|
||||
"newChoreButton": "New Chore",
|
||||
"editButton": "Edit",
|
||||
"deleteButton": "Delete",
|
||||
"cancelButton": "Cancel",
|
||||
"saveButton": "Save",
|
||||
"modals": {
|
||||
"editChoreTitle": "Edit Chore",
|
||||
"newChoreTitle": "New Chore",
|
||||
"deleteChoreTitle": "Delete Chore"
|
||||
},
|
||||
"form": {
|
||||
"nameLabel": "Name",
|
||||
"descriptionLabel": "Description",
|
||||
"frequencyLabel": "Frequency",
|
||||
"intervalLabel": "Interval (days)",
|
||||
"dueDateLabel": "Next Due Date"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"confirmationText": "Are you sure you want to delete this chore?"
|
||||
},
|
||||
"frequencies": {
|
||||
"one_time": "One Time",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"custom": "Custom",
|
||||
"unknown": "Unknown Frequency"
|
||||
},
|
||||
"dates": {
|
||||
"invalidDate": "Invalid Date",
|
||||
"duePrefix": "Due"
|
||||
},
|
||||
"notifications": {
|
||||
"loadFailed": "Failed to load personal chores",
|
||||
"updateSuccess": "Personal chore updated successfully",
|
||||
"createSuccess": "Personal chore created successfully",
|
||||
"saveFailed": "Failed to save personal chore",
|
||||
"deleteSuccess": "Personal chore deleted successfully",
|
||||
"deleteFailed": "Failed to delete personal chore"
|
||||
}
|
||||
},
|
||||
"indexPage": {
|
||||
"welcomeMessage": "Welcome to Valerie UI App",
|
||||
"mainPageInfo": "This is the main index page.",
|
||||
"sampleTodosHeader": "Sample Todos (from IndexPage data)",
|
||||
"totalCountLabel": "Total count from meta:",
|
||||
"noTodos": "No todos to display."
|
||||
}
|
||||
}
|
||||
|
@ -270,5 +270,292 @@
|
||||
"removeMemberSuccess": "FR: Member removed successfully",
|
||||
"removeMemberFailed": "FR: Failed to remove member"
|
||||
}
|
||||
},
|
||||
"accountPage": {
|
||||
"title": "Account Settings",
|
||||
"loadingProfile": "Loading profile...",
|
||||
"retryButton": "Retry",
|
||||
"profileSection": {
|
||||
"header": "Profile Information",
|
||||
"nameLabel": "Name",
|
||||
"emailLabel": "Email",
|
||||
"saveButton": "Save Changes"
|
||||
},
|
||||
"passwordSection": {
|
||||
"header": "Change Password",
|
||||
"currentPasswordLabel": "Current Password",
|
||||
"newPasswordLabel": "New Password",
|
||||
"changeButton": "Change Password"
|
||||
},
|
||||
"notificationsSection": {
|
||||
"header": "Notification Preferences",
|
||||
"emailNotificationsLabel": "Email Notifications",
|
||||
"emailNotificationsDescription": "Receive email notifications for important updates",
|
||||
"listUpdatesLabel": "List Updates",
|
||||
"listUpdatesDescription": "Get notified when lists are updated",
|
||||
"groupActivitiesLabel": "Group Activities",
|
||||
"groupActivitiesDescription": "Receive notifications for group activities"
|
||||
},
|
||||
"notifications": {
|
||||
"profileLoadFailed": "Failed to load profile",
|
||||
"profileUpdateSuccess": "Profile updated successfully",
|
||||
"profileUpdateFailed": "Failed to update profile",
|
||||
"passwordFieldsRequired": "Please fill in both current and new password fields.",
|
||||
"passwordTooShort": "New password must be at least 8 characters long.",
|
||||
"passwordChangeSuccess": "Password changed successfully",
|
||||
"passwordChangeFailed": "Failed to change password",
|
||||
"preferencesUpdateSuccess": "Preferences updated successfully",
|
||||
"preferencesUpdateFailed": "Failed to update preferences"
|
||||
},
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"signupPage": {
|
||||
"header": "Sign Up",
|
||||
"fullNameLabel": "Full Name",
|
||||
"emailLabel": "Email",
|
||||
"passwordLabel": "Password",
|
||||
"confirmPasswordLabel": "Confirm Password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"submitButton": "Sign Up",
|
||||
"loginLink": "Already have an account? Login",
|
||||
"validation": {
|
||||
"nameRequired": "Name is required",
|
||||
"emailRequired": "Email is required",
|
||||
"emailInvalid": "Invalid email format",
|
||||
"passwordRequired": "Password is required",
|
||||
"passwordLength": "Password must be at least 8 characters",
|
||||
"confirmPasswordRequired": "Please confirm your password",
|
||||
"passwordsNoMatch": "Passwords do not match"
|
||||
},
|
||||
"notifications": {
|
||||
"signupFailed": "Signup failed. Please try again.",
|
||||
"signupSuccess": "Account created successfully. Please login."
|
||||
}
|
||||
},
|
||||
"listDetailPage": {
|
||||
"loading": {
|
||||
"list": "Loading list...",
|
||||
"items": "Loading items...",
|
||||
"ocrProcessing": "Processing image...",
|
||||
"addingOcrItems": "Adding OCR items...",
|
||||
"costSummary": "Loading summary...",
|
||||
"expenses": "Loading expenses...",
|
||||
"settlement": "Processing settlement..."
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "Failed to load list details.",
|
||||
"genericLoadFailure": "Group not found or an error occurred.",
|
||||
"ocrNoItems": "No items extracted from the image.",
|
||||
"ocrFailed": "Failed to process image.",
|
||||
"addItemFailed": "Failed to add item.",
|
||||
"updateItemFailed": "Failed to update item.",
|
||||
"updateItemPriceFailed": "Failed to update item price.",
|
||||
"deleteItemFailed": "Failed to delete item.",
|
||||
"addOcrItemsFailed": "Failed to add OCR items.",
|
||||
"fetchItemsFailed": "Failed to load items: {errorMessage}",
|
||||
"loadCostSummaryFailed": "Failed to load cost summary."
|
||||
},
|
||||
"retryButton": "Retry",
|
||||
"buttons": {
|
||||
"addViaOcr": "Add via OCR",
|
||||
"addItem": "Add",
|
||||
"addItems": "Add Items",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"saveChanges": "Save Changes",
|
||||
"close": "Close",
|
||||
"costSummary": "Cost Summary"
|
||||
},
|
||||
"badges": {
|
||||
"groupList": "Group List",
|
||||
"personalList": "Personal List"
|
||||
},
|
||||
"items": {
|
||||
"emptyState": {
|
||||
"title": "No Items Yet!",
|
||||
"message": "Add some items using the form below."
|
||||
},
|
||||
"addItemForm": {
|
||||
"placeholder": "Add a new item",
|
||||
"quantityPlaceholder": "Qty",
|
||||
"itemNameSrLabel": "New item name",
|
||||
"quantitySrLabel": "Quantity"
|
||||
},
|
||||
"pricePlaceholder": "Price",
|
||||
"editItemAriaLabel": "Edit item",
|
||||
"deleteItemAriaLabel": "Delete item"
|
||||
},
|
||||
"modals": {
|
||||
"ocr": {
|
||||
"title": "Add Items via OCR",
|
||||
"uploadLabel": "Upload Image"
|
||||
},
|
||||
"confirmation": {
|
||||
"title": "Confirmation"
|
||||
},
|
||||
"editItem": {
|
||||
"title": "Edit Item",
|
||||
"nameLabel": "Item Name",
|
||||
"quantityLabel": "Quantity"
|
||||
},
|
||||
"costSummary": {
|
||||
"title": "List Cost Summary",
|
||||
"totalCostLabel": "Total List Cost:",
|
||||
"equalShareLabel": "Equal Share Per User:",
|
||||
"participantsLabel": "Participating Users:",
|
||||
"userBalancesHeader": "User Balances",
|
||||
"tableHeaders": {
|
||||
"user": "User",
|
||||
"itemsAddedValue": "Items Added Value",
|
||||
"amountDue": "Amount Due",
|
||||
"balance": "Balance"
|
||||
},
|
||||
"emptyState": "No cost summary available."
|
||||
},
|
||||
"settleShare": {
|
||||
"title": "Settle Share",
|
||||
"settleAmountFor": "Settle amount for {userName}:",
|
||||
"amountLabel": "Amount",
|
||||
"errors": {
|
||||
"enterAmount": "Please enter an amount.",
|
||||
"positiveAmount": "Please enter a positive amount.",
|
||||
"exceedsRemaining": "Amount cannot exceed remaining: {amount}.",
|
||||
"noSplitSelected": "Error: No split selected."
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirmations": {
|
||||
"updateMessage": "Mark '{itemName}' as {status}?",
|
||||
"statusComplete": "complete",
|
||||
"statusIncomplete": "incomplete",
|
||||
"deleteMessage": "Delete '{itemName}'? This cannot be undone."
|
||||
},
|
||||
"notifications": {
|
||||
"itemAddedSuccess": "Item added successfully.",
|
||||
"itemsAddedSuccessOcr": "{count} item(s) added successfully from OCR.",
|
||||
"itemUpdatedSuccess": "Item updated successfully.",
|
||||
"itemDeleteSuccess": "Item deleted successfully.",
|
||||
"enterItemName": "Please enter an item name.",
|
||||
"costSummaryLoadFailed": "Failed to load cost summary.",
|
||||
"cannotSettleOthersShares": "You can only settle your own shares.",
|
||||
"settlementDataMissing": "Cannot process settlement: missing data.",
|
||||
"settleShareSuccess": "Share settled successfully!",
|
||||
"settleShareFailed": "Failed to settle share."
|
||||
},
|
||||
"expensesSection": {
|
||||
"title": "Expenses",
|
||||
"addExpenseButton": "Add Expense",
|
||||
"loading": "Loading expenses...",
|
||||
"emptyState": "No expenses recorded for this list yet.",
|
||||
"paidBy": "Paid by:",
|
||||
"onDate": "on",
|
||||
"owes": "owes",
|
||||
"paidAmount": "Paid:",
|
||||
"activityLabel": "Activity:",
|
||||
"byUser": "by",
|
||||
"settleShareButton": "Settle My Share",
|
||||
"retryButton": "Retry"
|
||||
},
|
||||
"status": {
|
||||
"settled": "Settled",
|
||||
"partiallySettled": "Partially Settled",
|
||||
"unsettled": "Unsettled",
|
||||
"paid": "Paid",
|
||||
"partiallyPaid": "Partially Paid",
|
||||
"unpaid": "Unpaid",
|
||||
"unknown": "Unknown Status"
|
||||
}
|
||||
},
|
||||
"myChoresPage": {
|
||||
"title": "My Assigned Chores",
|
||||
"showCompletedToggle": "Show Completed",
|
||||
"timelineHeaders": {
|
||||
"overdue": "Overdue",
|
||||
"today": "Due Today",
|
||||
"thisWeek": "This Week",
|
||||
"later": "Later",
|
||||
"completed": "Completed"
|
||||
},
|
||||
"choreCard": {
|
||||
"personal": "Personal",
|
||||
"group": "Group",
|
||||
"duePrefix": "Due",
|
||||
"completedPrefix": "Completed",
|
||||
"dueToday": "Due Today",
|
||||
"markCompleteButton": "Mark Complete"
|
||||
},
|
||||
"frequencies": {
|
||||
"one_time": "One Time",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"custom": "Custom",
|
||||
"unknown": "Unknown Frequency"
|
||||
},
|
||||
"dates": {
|
||||
"invalidDate": "Invalid Date",
|
||||
"unknownDate": "Unknown Date"
|
||||
},
|
||||
"emptyState": {
|
||||
"title": "No Assignments Yet!",
|
||||
"noAssignmentsPending": "You have no pending chore assignments.",
|
||||
"noAssignmentsAll": "You have no chore assignments (completed or pending).",
|
||||
"viewAllChoresButton": "View All Chores"
|
||||
},
|
||||
"notifications": {
|
||||
"loadFailed": "Failed to load assignments",
|
||||
"markedComplete": "Marked \"{choreName}\" as complete!",
|
||||
"markCompleteFailed": "Failed to mark assignment as complete"
|
||||
}
|
||||
},
|
||||
"personalChoresPage": {
|
||||
"title": "Personal Chores",
|
||||
"newChoreButton": "New Chore",
|
||||
"editButton": "Edit",
|
||||
"deleteButton": "Delete",
|
||||
"cancelButton": "Cancel",
|
||||
"saveButton": "Save",
|
||||
"modals": {
|
||||
"editChoreTitle": "Edit Chore",
|
||||
"newChoreTitle": "New Chore",
|
||||
"deleteChoreTitle": "Delete Chore"
|
||||
},
|
||||
"form": {
|
||||
"nameLabel": "Name",
|
||||
"descriptionLabel": "Description",
|
||||
"frequencyLabel": "Frequency",
|
||||
"intervalLabel": "Interval (days)",
|
||||
"dueDateLabel": "Next Due Date"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"confirmationText": "Are you sure you want to delete this chore?"
|
||||
},
|
||||
"frequencies": {
|
||||
"one_time": "One Time",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"custom": "Custom",
|
||||
"unknown": "Unknown Frequency"
|
||||
},
|
||||
"dates": {
|
||||
"invalidDate": "Invalid Date",
|
||||
"duePrefix": "Due"
|
||||
},
|
||||
"notifications": {
|
||||
"loadFailed": "Failed to load personal chores",
|
||||
"updateSuccess": "Personal chore updated successfully",
|
||||
"createSuccess": "Personal chore created successfully",
|
||||
"saveFailed": "Failed to save personal chore",
|
||||
"deleteSuccess": "Personal chore deleted successfully",
|
||||
"deleteFailed": "Failed to delete personal chore"
|
||||
}
|
||||
},
|
||||
"indexPage": {
|
||||
"welcomeMessage": "Welcome to Valerie UI App",
|
||||
"mainPageInfo": "This is the main index page.",
|
||||
"sampleTodosHeader": "Sample Todos (from IndexPage data)",
|
||||
"totalCountLabel": "Total count from meta:",
|
||||
"noTodos": "No todos to display."
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,32 @@
|
||||
<template>
|
||||
<main class="container page-padding">
|
||||
<VHeading level="1" text="Account Settings" class="mb-3" />
|
||||
<VHeading level="1" :text="$t('accountPage.title')" class="mb-3" />
|
||||
|
||||
<div v-if="loading" class="text-center">
|
||||
<VSpinner label="Loading profile..." />
|
||||
<VSpinner :label="$t('accountPage.loadingProfile')" />
|
||||
</div>
|
||||
|
||||
<VAlert v-else-if="error" type="error" :message="error" class="mb-3">
|
||||
<template #actions>
|
||||
<VButton variant="danger" size="sm" @click="fetchProfile">Retry</VButton>
|
||||
<VButton variant="danger" size="sm" @click="fetchProfile">{{ $t('accountPage.retryButton') }}</VButton>
|
||||
</template>
|
||||
</VAlert>
|
||||
|
||||
<form v-else @submit.prevent="onSubmitProfile">
|
||||
<!-- Profile Section -->
|
||||
<VCard class="mb-3">
|
||||
<template #header><VHeading level="3">Profile Information</VHeading></template>
|
||||
<template #header><VHeading level="3">{{ $t('accountPage.profileSection.header') }}</VHeading></template>
|
||||
<div class="flex flex-wrap" style="gap: 1rem;">
|
||||
<VFormField label="Name" class="flex-grow">
|
||||
<VFormField :label="$t('accountPage.profileSection.nameLabel')" class="flex-grow">
|
||||
<VInput id="profileName" v-model="profile.name" required />
|
||||
</VFormField>
|
||||
<VFormField label="Email" class="flex-grow">
|
||||
<VFormField :label="$t('accountPage.profileSection.emailLabel')" class="flex-grow">
|
||||
<VInput type="email" id="profileEmail" v-model="profile.email" required readonly />
|
||||
</VFormField>
|
||||
</div>
|
||||
<template #footer>
|
||||
<VButton type="submit" variant="primary" :disabled="saving">
|
||||
<VSpinner v-if="saving" size="sm" /> Save Changes
|
||||
<VSpinner v-if="saving" size="sm" /> {{ $t('accountPage.profileSection.saveButton') }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VCard>
|
||||
@ -35,18 +35,18 @@
|
||||
<!-- Password Section -->
|
||||
<form @submit.prevent="onChangePassword">
|
||||
<VCard class="mb-3">
|
||||
<template #header><VHeading level="3">Change Password</VHeading></template>
|
||||
<template #header><VHeading level="3">{{ $t('accountPage.passwordSection.header') }}</VHeading></template>
|
||||
<div class="flex flex-wrap" style="gap: 1rem;">
|
||||
<VFormField label="Current Password" class="flex-grow">
|
||||
<VFormField :label="$t('accountPage.passwordSection.currentPasswordLabel')" class="flex-grow">
|
||||
<VInput type="password" id="currentPassword" v-model="password.current" required />
|
||||
</VFormField>
|
||||
<VFormField label="New Password" class="flex-grow">
|
||||
<VFormField :label="$t('accountPage.passwordSection.newPasswordLabel')" class="flex-grow">
|
||||
<VInput type="password" id="newPassword" v-model="password.newPassword" required />
|
||||
</VFormField>
|
||||
</div>
|
||||
<template #footer>
|
||||
<VButton type="submit" variant="primary" :disabled="changingPassword">
|
||||
<VSpinner v-if="changingPassword" size="sm" /> Change Password
|
||||
<VSpinner v-if="changingPassword" size="sm" /> {{ $t('accountPage.passwordSection.changeButton') }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VCard>
|
||||
@ -54,28 +54,28 @@
|
||||
|
||||
<!-- Notifications Section -->
|
||||
<VCard>
|
||||
<template #header><VHeading level="3">Notification Preferences</VHeading></template>
|
||||
<template #header><VHeading level="3">{{ $t('accountPage.notificationsSection.header') }}</VHeading></template>
|
||||
<VList class="preference-list">
|
||||
<VListItem class="preference-item">
|
||||
<div class="preference-label">
|
||||
<span>Email Notifications</span>
|
||||
<small>Receive email notifications for important updates</small>
|
||||
<span>{{ $t('accountPage.notificationsSection.emailNotificationsLabel') }}</span>
|
||||
<small>{{ $t('accountPage.notificationsSection.emailNotificationsDescription') }}</small>
|
||||
</div>
|
||||
<VToggleSwitch v-model="preferences.emailNotifications" @change="onPreferenceChange" label="Email Notifications" id="emailNotificationsToggle" />
|
||||
<VToggleSwitch v-model="preferences.emailNotifications" @change="onPreferenceChange" :label="$t('accountPage.notificationsSection.emailNotificationsLabel')" id="emailNotificationsToggle" />
|
||||
</VListItem>
|
||||
<VListItem class="preference-item">
|
||||
<div class="preference-label">
|
||||
<span>List Updates</span>
|
||||
<small>Get notified when lists are updated</small>
|
||||
<span>{{ $t('accountPage.notificationsSection.listUpdatesLabel') }}</span>
|
||||
<small>{{ $t('accountPage.notificationsSection.listUpdatesDescription') }}</small>
|
||||
</div>
|
||||
<VToggleSwitch v-model="preferences.listUpdates" @change="onPreferenceChange" label="List Updates" id="listUpdatesToggle"/>
|
||||
<VToggleSwitch v-model="preferences.listUpdates" @change="onPreferenceChange" :label="$t('accountPage.notificationsSection.listUpdatesLabel')" id="listUpdatesToggle"/>
|
||||
</VListItem>
|
||||
<VListItem class="preference-item">
|
||||
<div class="preference-label">
|
||||
<span>Group Activities</span>
|
||||
<small>Receive notifications for group activities</small>
|
||||
<span>{{ $t('accountPage.notificationsSection.groupActivitiesLabel') }}</span>
|
||||
<small>{{ $t('accountPage.notificationsSection.groupActivitiesDescription') }}</small>
|
||||
</div>
|
||||
<VToggleSwitch v-model="preferences.groupActivities" @change="onPreferenceChange" label="Group Activities" id="groupActivitiesToggle"/>
|
||||
<VToggleSwitch v-model="preferences.groupActivities" @change="onPreferenceChange" :label="$t('accountPage.notificationsSection.groupActivitiesLabel')" id="groupActivitiesToggle"/>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
@ -84,6 +84,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { apiClient, API_ENDPOINTS } from '@/config/api'; // Assuming path
|
||||
import { useNotificationStore } from '@/stores/notifications';
|
||||
import VHeading from '@/components/valerie/VHeading.vue';
|
||||
@ -97,6 +98,8 @@ import VToggleSwitch from '@/components/valerie/VToggleSwitch.vue';
|
||||
import VList from '@/components/valerie/VList.vue';
|
||||
import VListItem from '@/components/valerie/VListItem.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Profile {
|
||||
name: string;
|
||||
email: string;
|
||||
@ -136,10 +139,11 @@ const fetchProfile = async () => {
|
||||
// Assume preferences are also fetched or part of profile
|
||||
// preferences.value = response.data.preferences || preferences.value;
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load profile';
|
||||
error.value = message;
|
||||
const apiMessage = err instanceof Error ? err.message : t('accountPage.notifications.profileLoadFailed');
|
||||
error.value = apiMessage; // Show translated or API error message in the VAlert
|
||||
console.error('Failed to fetch profile:', err);
|
||||
notificationStore.addNotification({ message, type: 'error' });
|
||||
// For the notification pop-up, always use the translated generic message
|
||||
notificationStore.addNotification({ message: t('accountPage.notifications.profileLoadFailed'), type: 'error' });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -149,11 +153,11 @@ const onSubmitProfile = async () => {
|
||||
saving.value = true;
|
||||
try {
|
||||
await apiClient.put(API_ENDPOINTS.USERS.UPDATE_PROFILE, profile.value);
|
||||
notificationStore.addNotification({ message: 'Profile updated successfully', type: 'success' });
|
||||
notificationStore.addNotification({ message: t('accountPage.notifications.profileUpdateSuccess'), type: 'success' });
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to update profile';
|
||||
const message = err instanceof Error ? err.message : t('accountPage.notifications.profileUpdateFailed');
|
||||
console.error('Failed to update profile:', err);
|
||||
notificationStore.addNotification({ message, type: 'error' });
|
||||
notificationStore.addNotification({ message: t('accountPage.notifications.profileUpdateFailed'), type: 'error' });
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
@ -161,11 +165,11 @@ const onSubmitProfile = async () => {
|
||||
|
||||
const onChangePassword = async () => {
|
||||
if (!password.value.current || !password.value.newPassword) {
|
||||
notificationStore.addNotification({ message: 'Please fill in both current and new password fields.', type: 'warning' });
|
||||
notificationStore.addNotification({ message: t('accountPage.notifications.passwordFieldsRequired'), type: 'warning' });
|
||||
return;
|
||||
}
|
||||
if (password.value.newPassword.length < 8) {
|
||||
notificationStore.addNotification({ message: 'New password must be at least 8 characters long.', type: 'warning' });
|
||||
notificationStore.addNotification({ message: t('accountPage.notifications.passwordTooShort'), type: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -177,11 +181,11 @@ const onChangePassword = async () => {
|
||||
new: password.value.newPassword
|
||||
});
|
||||
password.value = { current: '', newPassword: '' };
|
||||
notificationStore.addNotification({ message: 'Password changed successfully', type: 'success' });
|
||||
notificationStore.addNotification({ message: t('accountPage.notifications.passwordChangeSuccess'), type: 'success' });
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to change password';
|
||||
const message = err instanceof Error ? err.message : t('accountPage.notifications.passwordChangeFailed');
|
||||
console.error('Failed to change password:', err);
|
||||
notificationStore.addNotification({ message, type: 'error' });
|
||||
notificationStore.addNotification({ message: t('accountPage.notifications.passwordChangeFailed'), type: 'error' });
|
||||
} finally {
|
||||
changingPassword.value = false;
|
||||
}
|
||||
@ -192,11 +196,11 @@ const onPreferenceChange = async () => {
|
||||
// Consider debouncing or providing a "Save Preferences" button if API calls are expensive.
|
||||
try {
|
||||
await apiClient.put(API_ENDPOINTS.USERS.PREFERENCES, preferences.value);
|
||||
notificationStore.addNotification({ message: 'Preferences updated successfully', type: 'success' });
|
||||
notificationStore.addNotification({ message: t('accountPage.notifications.preferencesUpdateSuccess'), type: 'success' });
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to update preferences';
|
||||
const message = err instanceof Error ? err.message : t('accountPage.notifications.preferencesUpdateFailed');
|
||||
console.error('Failed to update preferences:', err);
|
||||
notificationStore.addNotification({ message, type: 'error' });
|
||||
notificationStore.addNotification({ message: t('accountPage.notifications.preferencesUpdateFailed'), type: 'error' });
|
||||
// Optionally revert the toggle if the API call fails
|
||||
// await fetchProfile(); // Or manage state more granularly
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<main class="container page-padding text-center">
|
||||
<h1>Welcome to Valerie UI App</h1>
|
||||
<p class="mb-3">This is the main index page.</p>
|
||||
<h1>{{ $t('indexPage.welcomeMessage') }}</h1>
|
||||
<p class="mb-3">{{ $t('indexPage.mainPageInfo') }}</p>
|
||||
|
||||
<!-- The ExampleComponent is not provided, so this section is a placeholder -->
|
||||
<div v-if="todos.length" class="card">
|
||||
<div class="card-header">
|
||||
<h3>Sample Todos (from IndexPage data)</h3>
|
||||
<h3>{{ $t('indexPage.sampleTodosHeader') }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="item-list">
|
||||
@ -16,18 +16,21 @@
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="mt-2">Total count from meta: {{ meta.totalCount }}</p>
|
||||
<p class="mt-2">{{ $t('indexPage.totalCountLabel') }} {{ meta.totalCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else>No todos to display.</p>
|
||||
<p v-else>{{ $t('indexPage.noTodos') }}</p>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { Todo, Meta } from '@/components/models'; // Adjusted path if models.ts is in the same directory
|
||||
// import ExampleComponent from 'components/ExampleComponent.vue'; // This component is not provided for conversion
|
||||
|
||||
const { t } = useI18n(); // Added for consistency, though not strictly needed if only $t in template
|
||||
|
||||
const todos = ref<Todo[]>([
|
||||
{ id: 1, content: 'ct1' },
|
||||
{ id: 2, content: 'ct2' },
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<main class="neo-container page-padding">
|
||||
<div v-if="pageInitialLoad && !list && !error" class="text-center py-10">
|
||||
<VSpinner label="Loading list..." size="lg" />
|
||||
<VSpinner :label="$t('listDetailPage.loading.list')" size="lg" />
|
||||
</div>
|
||||
|
||||
<VAlert v-else-if="error && !list" type="error" :message="error" class="mb-4">
|
||||
<template #actions>
|
||||
<VButton @click="fetchListDetails">Retry</VButton>
|
||||
<VButton @click="fetchListDetails">{{ $t('listDetailPage.retryButton') }}</VButton>
|
||||
</template>
|
||||
</VAlert>
|
||||
|
||||
@ -15,10 +15,10 @@
|
||||
<div class="neo-list-header">
|
||||
<VHeading :level="1" :text="list.name" class="mb-3 neo-title" />
|
||||
<div class="neo-header-actions">
|
||||
<VButton @click="showCostSummaryDialog = true" :disabled="!isOnline" icon-left="clipboard">Cost Summary
|
||||
<VButton @click="showCostSummaryDialog = true" :disabled="!isOnline" icon-left="clipboard">{{ $t('listDetailPage.buttons.costSummary') }}
|
||||
</VButton>
|
||||
<VButton @click="openOcrDialog" :disabled="!isOnline" icon-left="plus">Add via OCR</VButton>
|
||||
<VBadge :text="list.group_id ? 'Group List' : 'Personal List'" :variant="list.group_id ? 'accent' : 'settled'"
|
||||
<VButton @click="openOcrDialog" :disabled="!isOnline" icon-left="plus">{{ $t('listDetailPage.buttons.addViaOcr') }}</VButton>
|
||||
<VBadge :text="list.group_id ? $t('listDetailPage.badges.groupList') : $t('listDetailPage.badges.personalList')" :variant="list.group_id ? 'accent' : 'settled'"
|
||||
class="neo-status" />
|
||||
</div>
|
||||
</div>
|
||||
@ -26,10 +26,10 @@
|
||||
|
||||
<!-- Items List Section -->
|
||||
<VCard v-if="itemsAreLoading" class="py-10 text-center mt-4">
|
||||
<VSpinner label="Loading items..." size="lg" />
|
||||
<VSpinner :label="$t('listDetailPage.loading.items')" size="lg" />
|
||||
</VCard>
|
||||
<VCard v-else-if="!itemsAreLoading && list.items.length === 0" variant="empty-state" empty-icon="clipboard"
|
||||
empty-title="No Items Yet!" empty-message="Add some items using the form below." class="mt-4" />
|
||||
:empty-title="$t('listDetailPage.items.emptyState.title')" :empty-message="$t('listDetailPage.items.emptyState.message')" class="mt-4" />
|
||||
<div v-else class="neo-item-list-container mt-4">
|
||||
<ul class="neo-item-list">
|
||||
<li v-for="item in list.items" :key="item.id" class="neo-list-item"
|
||||
@ -42,18 +42,18 @@
|
||||
<span v-if="item.quantity" class="text-sm text-gray-500 ml-1">× {{ item.quantity }}</span>
|
||||
</label>
|
||||
<div class="neo-item-actions">
|
||||
<button class="neo-icon-button neo-edit-button" @click.stop="editItem(item)" aria-label="Edit item">
|
||||
<button class="neo-icon-button neo-edit-button" @click.stop="editItem(item)" :aria-label="$t('listDetailPage.items.editItemAriaLabel')">
|
||||
<VIcon name="edit" />
|
||||
</button>
|
||||
<button class="neo-icon-button neo-delete-button" @click.stop="confirmDeleteItem(item)"
|
||||
:disabled="item.deleting" aria-label="Delete item">
|
||||
:disabled="item.deleting" :aria-label="$t('listDetailPage.items.deleteItemAriaLabel')">
|
||||
<VIcon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.is_complete" class="neo-price-input">
|
||||
<VInput type="number" :model-value="item.priceInput || ''" @update:modelValue="item.priceInput = $event"
|
||||
placeholder="Price" size="sm" class="w-24" step="0.01" @blur="updateItemPrice(item)"
|
||||
:placeholder="$t('listDetailPage.items.pricePlaceholder')" size="sm" class="w-24" step="0.01" @blur="updateItemPrice(item)"
|
||||
@keydown.enter.prevent="($event.target as HTMLInputElement).blur()" />
|
||||
</div>
|
||||
</li>
|
||||
@ -63,16 +63,16 @@
|
||||
<!-- Add New Item Form -->
|
||||
<form @submit.prevent="onAddItem" class="add-item-form mt-4 p-4 border rounded-lg shadow flex items-center gap-2">
|
||||
<VIcon name="plus-circle" class="text-gray-400 shrink-0" />
|
||||
<VFormField class="flex-grow" label="New item name" :label-sr-only="true">
|
||||
<VInput v-model="newItem.name" placeholder="Add a new item" required ref="itemNameInputRef" />
|
||||
<VFormField class="flex-grow" :label="$t('listDetailPage.items.addItemForm.itemNameSrLabel')" :label-sr-only="true">
|
||||
<VInput v-model="newItem.name" :placeholder="$t('listDetailPage.items.addItemForm.placeholder')" required ref="itemNameInputRef" />
|
||||
</VFormField>
|
||||
<VFormField label="Quantity" :label-sr-only="true" class="w-24 shrink-0">
|
||||
<VFormField :label="$t('listDetailPage.items.addItemForm.quantitySrLabel')" :label-sr-only="true" class="w-24 shrink-0">
|
||||
<VInput type="number" :model-value="newItem.quantity || ''" @update:modelValue="newItem.quantity = $event"
|
||||
placeholder="Qty" min="1" />
|
||||
:placeholder="$t('listDetailPage.items.addItemForm.quantityPlaceholder')" min="1" />
|
||||
</VFormField>
|
||||
<VButton type="submit" :disabled="addingItem" class="shrink-0">
|
||||
<VSpinner v-if="addingItem" size="sm" />
|
||||
<span v-else>Add</span>
|
||||
<span v-else>{{ $t('listDetailPage.buttons.addItem') }}</span>
|
||||
</VButton>
|
||||
</form>
|
||||
</template>
|
||||
@ -80,24 +80,24 @@
|
||||
<!-- Expenses Section (Original Content - Part 3 will refactor this) -->
|
||||
<section v-if="list && !itemsAreLoading" class="neo-expenses-section">
|
||||
<div class="neo-expenses-header">
|
||||
<h2 class="neo-expenses-title">Expenses</h2>
|
||||
<h2 class="neo-expenses-title">{{ $t('listDetailPage.expensesSection.title') }}</h2>
|
||||
<button class="neo-action-button" @click="showCreateExpenseForm = true">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-plus" />
|
||||
</svg>
|
||||
Add Expense
|
||||
{{ $t('listDetailPage.expensesSection.addExpenseButton') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="listDetailStore.isLoading && expenses.length === 0" class="neo-loading-state">
|
||||
<div class="spinner-dots" role="status"><span /><span /><span /></div>
|
||||
<p>Loading expenses...</p>
|
||||
<p>{{ $t('listDetailPage.expensesSection.loading') }}</p>
|
||||
</div>
|
||||
<div v-else-if="listDetailStore.error" class="neo-error-state">
|
||||
<p>{{ listDetailStore.error }}</p>
|
||||
<button class="neo-button" @click="listDetailStore.fetchListWithExpenses(String(list?.id))">Retry</button>
|
||||
<p>{{ listDetailStore.error }}</p> <!-- Assuming listDetailStore.error is a backend message or already translated if generic -->
|
||||
<button class="neo-button" @click="listDetailStore.fetchListWithExpenses(String(list?.id))">{{ $t('listDetailPage.expensesSection.retryButton') }}</button>
|
||||
</div>
|
||||
<div v-else-if="!expenses || expenses.length === 0" class="neo-empty-state">
|
||||
<p>No expenses recorded for this list yet.</p>
|
||||
<p>{{ $t('listDetailPage.expensesSection.emptyState') }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="expense in expenses" :key="expense.id" class="neo-expense-card">
|
||||
@ -108,34 +108,34 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="neo-expense-details">
|
||||
Paid by: <strong>{{ expense.paid_by_user?.name || expense.paid_by_user?.email || `User ID:
|
||||
{{ $t('listDetailPage.expensesSection.paidBy') }} <strong>{{ expense.paid_by_user?.name || expense.paid_by_user?.email || `User ID:
|
||||
${expense.paid_by_user_id}` }}</strong>
|
||||
on {{ new Date(expense.expense_date).toLocaleDateString() }}
|
||||
{{ $t('listDetailPage.expensesSection.onDate') }} {{ new Date(expense.expense_date).toLocaleDateString() }}
|
||||
</div>
|
||||
|
||||
<div class="neo-splits-list">
|
||||
<div v-for="split in expense.splits" :key="split.id" class="neo-split-item">
|
||||
<div class="neo-split-details">
|
||||
<strong>{{ split.user?.name || split.user?.email || `User ID: ${split.user_id}` }}</strong> owes {{
|
||||
<strong>{{ split.user?.name || split.user?.email || `User ID: ${split.user_id}` }}</strong> {{ $t('listDetailPage.expensesSection.owes') }} {{
|
||||
formatCurrency(split.owed_amount) }}
|
||||
<span class="neo-expense-status" :class="getStatusClass(split.status)">
|
||||
{{ getSplitStatusText(split.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="neo-split-details">
|
||||
Paid: {{ getPaidAmountForSplitDisplay(split) }}
|
||||
<span v-if="split.paid_at"> on {{ new Date(split.paid_at).toLocaleDateString() }}</span>
|
||||
{{ $t('listDetailPage.expensesSection.paidAmount') }} {{ getPaidAmountForSplitDisplay(split) }}
|
||||
<span v-if="split.paid_at"> {{ $t('listDetailPage.expensesSection.onDate') }} {{ new Date(split.paid_at).toLocaleDateString() }}</span>
|
||||
</div>
|
||||
<button v-if="split.user_id === authStore.user?.id && split.status !== ExpenseSplitStatusEnum.PAID"
|
||||
class="neo-button neo-button-primary" @click="openSettleShareModal(expense, split)"
|
||||
:disabled="isSettlementLoading">
|
||||
Settle My Share
|
||||
{{ $t('listDetailPage.expensesSection.settleShareButton') }}
|
||||
</button>
|
||||
<ul v-if="split.settlement_activities && split.settlement_activities.length > 0"
|
||||
class="neo-settlement-activities">
|
||||
<li v-for="activity in split.settlement_activities" :key="activity.id">
|
||||
Activity: {{ formatCurrency(activity.amount_paid) }} by {{ activity.payer?.name || `User
|
||||
${activity.paid_by_user_id}` }} on {{ new Date(activity.paid_at).toLocaleDateString() }}
|
||||
{{ $t('listDetailPage.expensesSection.activityLabel') }} {{ formatCurrency(activity.amount_paid) }} {{ $t('listDetailPage.expensesSection.byUser') }} {{ activity.payer?.name || `User
|
||||
${activity.paid_by_user_id}` }} {{ $t('listDetailPage.expensesSection.onDate') }} {{ new Date(activity.paid_at).toLocaleDateString() }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -149,10 +149,10 @@
|
||||
@close="showCreateExpenseForm = false" @created="handleExpenseCreated" />
|
||||
|
||||
<!-- OCR Dialog -->
|
||||
<VModal v-model="showOcrDialogState" title="Add Items via OCR" @update:modelValue="!$event && closeOcrDialog()">
|
||||
<VModal v-model="showOcrDialogState" :title="$t('listDetailPage.modals.ocr.title')" @update:modelValue="!$event && closeOcrDialog()">
|
||||
<template #default>
|
||||
<div v-if="ocrLoading" class="text-center">
|
||||
<VSpinner label="Processing image..." />
|
||||
<VSpinner :label="$t('listDetailPage.loading.ocrProcessing')" />
|
||||
</div>
|
||||
<VList v-else-if="ocrItems.length > 0">
|
||||
<VListItem v-for="(ocrItem, index) in ocrItems" :key="index">
|
||||
@ -163,22 +163,22 @@
|
||||
</div>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VFormField v-else label="Upload Image" :error-message="ocrError || undefined">
|
||||
<VFormField v-else :label="$t('listDetailPage.modals.ocr.uploadLabel')" :error-message="ocrError || undefined">
|
||||
<VInput type="file" id="ocrFile" accept="image/*" @change="handleOcrFileUpload" ref="ocrFileInputRef"
|
||||
:model-value="''" />
|
||||
</VFormField>
|
||||
</template>
|
||||
<template #footer>
|
||||
<VButton variant="neutral" @click="closeOcrDialog">Cancel</VButton>
|
||||
<VButton variant="neutral" @click="closeOcrDialog">{{ $t('listDetailPage.buttons.cancel') }}</VButton>
|
||||
<VButton v-if="ocrItems.length > 0" type="button" variant="primary" @click="addOcrItems"
|
||||
:disabled="addingOcrItems">
|
||||
<VSpinner v-if="addingOcrItems" size="sm" /> Add Items
|
||||
<VSpinner v-if="addingOcrItems" size="sm" :label="$t('listDetailPage.loading.addingOcrItems')" /> {{ $t('listDetailPage.buttons.addItems') }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VModal>
|
||||
|
||||
<!-- Confirmation Dialog -->
|
||||
<VModal v-model="showConfirmDialogState" title="Confirmation" @update:modelValue="!$event && cancelConfirmation()"
|
||||
<VModal v-model="showConfirmDialogState" :title="$t('listDetailPage.modals.confirmation.title')" @update:modelValue="!$event && cancelConfirmation()"
|
||||
size="sm">
|
||||
<template #default>
|
||||
<div class="text-center">
|
||||
@ -187,34 +187,34 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<VButton variant="neutral" @click="cancelConfirmation">Cancel</VButton>
|
||||
<VButton variant="primary" @click="handleConfirmedAction">Confirm</VButton>
|
||||
<VButton variant="neutral" @click="cancelConfirmation">{{ $t('listDetailPage.buttons.cancel') }}</VButton>
|
||||
<VButton variant="primary" @click="handleConfirmedAction">{{ $t('listDetailPage.buttons.confirm') }}</VButton>
|
||||
</template>
|
||||
</VModal>
|
||||
|
||||
<!-- Cost Summary Dialog -->
|
||||
<VModal v-model="showCostSummaryDialog" title="List Cost Summary" @update:modelValue="showCostSummaryDialog = false"
|
||||
<VModal v-model="showCostSummaryDialog" :title="$t('listDetailPage.modals.costSummary.title')" @update:modelValue="showCostSummaryDialog = false"
|
||||
size="lg">
|
||||
<template #default>
|
||||
<div v-if="costSummaryLoading" class="text-center">
|
||||
<VSpinner label="Loading summary..." />
|
||||
<VSpinner :label="$t('listDetailPage.loading.costSummary')" />
|
||||
</div>
|
||||
<VAlert v-else-if="costSummaryError" type="error" :message="costSummaryError" />
|
||||
<div v-else-if="listCostSummary">
|
||||
<div class="mb-3 cost-overview">
|
||||
<p><strong>Total List Cost:</strong> {{ formatCurrency(listCostSummary.total_list_cost) }}</p>
|
||||
<p><strong>Equal Share Per User:</strong> {{ formatCurrency(listCostSummary.equal_share_per_user) }}</p>
|
||||
<p><strong>Participating Users:</strong> {{ listCostSummary.num_participating_users }}</p>
|
||||
<p><strong>{{ $t('listDetailPage.costSummaryModal.totalCostLabel') }}</strong> {{ formatCurrency(listCostSummary.total_list_cost) }}</p>
|
||||
<p><strong>{{ $t('listDetailPage.costSummaryModal.equalShareLabel') }}</strong> {{ formatCurrency(listCostSummary.equal_share_per_user) }}</p>
|
||||
<p><strong>{{ $t('listDetailPage.costSummaryModal.participantsLabel') }}</strong> {{ listCostSummary.num_participating_users }}</p>
|
||||
</div>
|
||||
<h4>User Balances</h4>
|
||||
<h4>{{ $t('listDetailPage.costSummaryModal.userBalancesHeader') }}</h4>
|
||||
<div class="table-container mt-2">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th class="text-right">Items Added Value</th>
|
||||
<th class="text-right">Amount Due</th>
|
||||
<th class="text-right">Balance</th>
|
||||
<th>{{ $t('listDetailPage.costSummaryModal.tableHeaders.user') }}</th>
|
||||
<th class="text-right">{{ $t('listDetailPage.costSummaryModal.tableHeaders.itemsAddedValue') }}</th>
|
||||
<th class="text-right">{{ $t('listDetailPage.costSummaryModal.tableHeaders.amountDue') }}</th>
|
||||
<th class="text-right">{{ $t('listDetailPage.costSummaryModal.tableHeaders.balance') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -231,60 +231,60 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else>No cost summary available.</p>
|
||||
<p v-else>{{ $t('listDetailPage.costSummaryModal.emptyState') }}</p>
|
||||
</template>
|
||||
<template #footer>
|
||||
<VButton variant="primary" @click="showCostSummaryDialog = false">Close</VButton>
|
||||
<VButton variant="primary" @click="showCostSummaryDialog = false">{{ $t('listDetailPage.buttons.close') }}</VButton>
|
||||
</template>
|
||||
</VModal>
|
||||
|
||||
<!-- Settle Share Modal -->
|
||||
<VModal v-model="showSettleModal" title="Settle Share" @update:modelValue="!$event && closeSettleShareModal()"
|
||||
<VModal v-model="showSettleModal" :title="$t('listDetailPage.settleShareModal.title')" @update:modelValue="!$event && closeSettleShareModal()"
|
||||
size="md">
|
||||
<template #default>
|
||||
<div v-if="isSettlementLoading" class="text-center">
|
||||
<VSpinner label="Processing settlement..." />
|
||||
<VSpinner :label="$t('listDetailPage.loading.settlement')" />
|
||||
</div>
|
||||
<VAlert v-else-if="settleAmountError" type="error" :message="settleAmountError" />
|
||||
<div v-else>
|
||||
<p>Settle amount for {{ selectedSplitForSettlement?.user?.name || selectedSplitForSettlement?.user?.email ||
|
||||
`User ID: ${selectedSplitForSettlement?.user_id}` }}:</p>
|
||||
<VFormField label="Amount" :error-message="settleAmountError || undefined">
|
||||
<p>{{ $t('listDetailPage.settleShareModal.settleAmountFor', { userName: selectedSplitForSettlement?.user?.name || selectedSplitForSettlement?.user?.email || `User ID: ${selectedSplitForSettlement?.user_id}` }) }}</p>
|
||||
<VFormField :label="$t('listDetailPage.settleShareModal.amountLabel')" :error-message="settleAmountError || undefined">
|
||||
<VInput type="number" v-model="settleAmount" id="settleAmount" required />
|
||||
</VFormField>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<VButton variant="neutral" @click="closeSettleShareModal">Cancel</VButton>
|
||||
<VButton variant="primary" @click="handleConfirmSettle">Confirm</VButton>
|
||||
<VButton variant="neutral" @click="closeSettleShareModal">{{ $t('listDetailPage.settleShareModal.cancelButton') }}</VButton>
|
||||
<VButton variant="primary" @click="handleConfirmSettle">{{ $t('listDetailPage.settleShareModal.confirmButton') }}</VButton>
|
||||
</template>
|
||||
</VModal>
|
||||
|
||||
<!-- Edit Item Dialog -->
|
||||
<VModal v-model="showEditDialog" title="Edit Item" @update:modelValue="!$event && closeEditDialog()">
|
||||
<VModal v-model="showEditDialog" :title="$t('listDetailPage.modals.editItem.title')" @update:modelValue="!$event && closeEditDialog()">
|
||||
<template #default>
|
||||
<VFormField v-if="editingItem" label="Item Name" class="mb-4">
|
||||
<VFormField v-if="editingItem" :label="$t('listDetailPage.modals.editItem.nameLabel')" class="mb-4">
|
||||
<VInput type="text" id="editItemName" v-model="editingItem.name" required />
|
||||
</VFormField>
|
||||
<VFormField v-if="editingItem" label="Quantity">
|
||||
<VFormField v-if="editingItem" :label="$t('listDetailPage.modals.editItem.quantityLabel')">
|
||||
<VInput type="number" id="editItemQuantity" :model-value="editingItem.quantity || ''"
|
||||
@update:modelValue="editingItem.quantity = $event" min="1" />
|
||||
</VFormField>
|
||||
</template>
|
||||
<template #footer>
|
||||
<VButton variant="neutral" @click="closeEditDialog">Cancel</VButton>
|
||||
<VButton variant="primary" @click="handleConfirmEdit" :disabled="!editingItem?.name.trim()">Save Changes
|
||||
<VButton variant="neutral" @click="closeEditDialog">{{ $t('listDetailPage.buttons.cancel') }}</VButton>
|
||||
<VButton variant="primary" @click="handleConfirmEdit" :disabled="!editingItem?.name.trim()">{{ $t('listDetailPage.buttons.saveChanges') }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VModal>
|
||||
|
||||
<VAlert v-if="!list && !pageInitialLoad" type="info" message="Group not found or an error occurred." />
|
||||
<VAlert v-if="!list && !pageInitialLoad" type="info" :message="$t('listDetailPage.errors.genericLoadFailure')" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { apiClient, API_ENDPOINTS } from '@/config/api'; // Keep for item management
|
||||
import { useEventListener, useFileDialog, useNetwork } from '@vueuse/core'; // onClickOutside removed
|
||||
import { useNotificationStore } from '@/stores/notifications';
|
||||
@ -313,6 +313,7 @@ import VListItem from '@/components/valerie/VListItem.vue';
|
||||
import VCheckbox from '@/components/valerie/VCheckbox.vue';
|
||||
// VTextarea and VSelect are not used in this part of the refactor for ListDetailPage
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// UI-specific properties that we add to items
|
||||
interface ItemWithUI extends Item {
|
||||
@ -433,31 +434,23 @@ const processListItems = (items: Item[]): ItemWithUI[] => {
|
||||
};
|
||||
|
||||
const fetchListDetails = async () => {
|
||||
// If pageInitialLoad is still true here, it means no shell was loaded.
|
||||
// The main spinner might be showing. We're about to fetch details, so turn off main spinner.
|
||||
if (pageInitialLoad.value) {
|
||||
pageInitialLoad.value = false;
|
||||
}
|
||||
itemsAreLoading.value = true;
|
||||
|
||||
// Check for pre-fetched full data first
|
||||
const routeId = String(route.params.id);
|
||||
const cachedFullData = sessionStorage.getItem(`listDetailFull_${routeId}`);
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (cachedFullData) {
|
||||
// Use cached data
|
||||
response = { data: JSON.parse(cachedFullData) };
|
||||
// Clear the cache after using it
|
||||
sessionStorage.removeItem(`listDetailFull_${routeId}`);
|
||||
} else {
|
||||
// Fetch fresh data
|
||||
response = await apiClient.get(API_ENDPOINTS.LISTS.BY_ID(routeId));
|
||||
}
|
||||
|
||||
const rawList = response.data as ListWithExpenses;
|
||||
// Map API response to local List type
|
||||
const localList: List = {
|
||||
id: rawList.id,
|
||||
name: rawList.name,
|
||||
@ -477,17 +470,15 @@ const fetchListDetails = async () => {
|
||||
await fetchListCostSummary();
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = (err instanceof Error ? err.message : String(err)) || 'Failed to load list details.';
|
||||
if (!list.value) { // If there was no shell AND this fetch failed
|
||||
error.value = errorMessage; // This error is for the whole page
|
||||
const apiErrorMessage = err instanceof Error ? err.message : String(err);
|
||||
const fallbackErrorMessage = t('listDetailPage.errors.fetchFailed');
|
||||
if (!list.value) {
|
||||
error.value = apiErrorMessage || fallbackErrorMessage;
|
||||
} else {
|
||||
// We have a shell, but items failed to load.
|
||||
// Show a notification for item loading failure. list.items will remain as per shell (empty).
|
||||
notificationStore.addNotification({ message: `Failed to load items: ${errorMessage}`, type: 'error' });
|
||||
notificationStore.addNotification({ message: t('listDetailPage.errors.fetchItemsFailed', { errorMessage: apiErrorMessage }), type: 'error' });
|
||||
}
|
||||
} finally {
|
||||
itemsAreLoading.value = false;
|
||||
// If list is still null and no error was set (e.g. silent failure), ensure pageInitialLoad is false.
|
||||
if (!list.value && !error.value) {
|
||||
pageInitialLoad.value = false;
|
||||
}
|
||||
@ -532,7 +523,7 @@ const isItemPendingSync = (item: Item) => {
|
||||
|
||||
const onAddItem = async () => {
|
||||
if (!list.value || !newItem.value.name.trim()) {
|
||||
notificationStore.addNotification({ message: 'Please enter an item name.', type: 'warning' });
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.enterItemName'), type: 'warning' });
|
||||
if (itemNameInputRef.value?.$el) {
|
||||
(itemNameInputRef.value.$el as HTMLElement).focus();
|
||||
}
|
||||
@ -541,7 +532,7 @@ const onAddItem = async () => {
|
||||
addingItem.value = true;
|
||||
|
||||
if (!isOnline.value) {
|
||||
const offlinePayload: any = {
|
||||
const offlinePayload: any = { // Define explicit type later if needed
|
||||
name: newItem.value.name
|
||||
};
|
||||
if (typeof newItem.value.quantity !== 'undefined') {
|
||||
@ -555,12 +546,12 @@ const onAddItem = async () => {
|
||||
}
|
||||
});
|
||||
const optimisticItem: ItemWithUI = {
|
||||
id: Date.now(),
|
||||
id: Date.now(), // Temporary ID for offline
|
||||
name: newItem.value.name,
|
||||
quantity: typeof newItem.value.quantity === 'string' ? Number(newItem.value.quantity) : (newItem.value.quantity || null),
|
||||
is_complete: false,
|
||||
price: null,
|
||||
version: 1,
|
||||
version: 1, // Assuming initial version
|
||||
updated_at: new Date().toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
list_id: list.value.id,
|
||||
@ -575,6 +566,7 @@ const onAddItem = async () => {
|
||||
(itemNameInputRef.value.$el as HTMLElement).focus();
|
||||
}
|
||||
addingItem.value = false;
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.itemAddedSuccess'), type: 'success' }); // Optimistic UI
|
||||
return;
|
||||
}
|
||||
|
||||
@ -592,8 +584,9 @@ const onAddItem = async () => {
|
||||
if (itemNameInputRef.value?.$el) {
|
||||
(itemNameInputRef.value.$el as HTMLElement).focus();
|
||||
}
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.itemAddedSuccess'), type: 'success' });
|
||||
} catch (err) {
|
||||
notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || 'Failed to add item.', type: 'error' });
|
||||
notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || t('listDetailPage.errors.addItemFailed'), type: 'error' });
|
||||
} finally {
|
||||
addingItem.value = false;
|
||||
}
|
||||
@ -618,6 +611,7 @@ const updateItem = async (item: ItemWithUI, newCompleteStatus: boolean) => {
|
||||
}
|
||||
});
|
||||
item.updating = false;
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.itemUpdatedSuccess'), type: 'success' }); // Optimistic UI
|
||||
return;
|
||||
}
|
||||
|
||||
@ -627,9 +621,10 @@ const updateItem = async (item: ItemWithUI, newCompleteStatus: boolean) => {
|
||||
{ completed: newCompleteStatus, version: item.version }
|
||||
);
|
||||
item.version++;
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.itemUpdatedSuccess'), type: 'success' });
|
||||
} catch (err) {
|
||||
item.is_complete = originalCompleteStatus;
|
||||
notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || 'Failed to update item.', type: 'error' });
|
||||
item.is_complete = originalCompleteStatus; // Revert optimistic update
|
||||
notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || t('listDetailPage.errors.updateItemFailed'), type: 'error' });
|
||||
} finally {
|
||||
item.updating = false;
|
||||
}
|
||||
@ -638,11 +633,12 @@ const updateItem = async (item: ItemWithUI, newCompleteStatus: boolean) => {
|
||||
const updateItemPrice = async (item: ItemWithUI) => {
|
||||
if (!list.value || !item.is_complete) return;
|
||||
const newPrice = item.priceInput !== undefined && String(item.priceInput).trim() !== '' ? parseFloat(String(item.priceInput)) : null;
|
||||
if (item.price === newPrice?.toString()) return;
|
||||
if (item.price === newPrice?.toString()) return; // No change
|
||||
item.updating = true;
|
||||
const originalPrice = item.price;
|
||||
const originalPriceInput = item.priceInput;
|
||||
item.price = newPrice?.toString() || null;
|
||||
item.price = newPrice?.toString() || null; // Optimistic update
|
||||
|
||||
if (!isOnline.value) {
|
||||
offlineStore.addAction({
|
||||
type: 'update_list_item',
|
||||
@ -650,13 +646,14 @@ const updateItemPrice = async (item: ItemWithUI) => {
|
||||
listId: String(list.value.id),
|
||||
itemId: String(item.id),
|
||||
data: {
|
||||
price: newPrice ?? null,
|
||||
completed: item.is_complete
|
||||
price: newPrice ?? null, // Ensure null is sent if cleared
|
||||
completed: item.is_complete // Keep completion status
|
||||
},
|
||||
version: item.version
|
||||
}
|
||||
});
|
||||
item.updating = false;
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.itemUpdatedSuccess'), type: 'success' }); // Optimistic UI
|
||||
return;
|
||||
}
|
||||
|
||||
@ -666,10 +663,11 @@ const updateItemPrice = async (item: ItemWithUI) => {
|
||||
{ price: newPrice?.toString(), completed: item.is_complete, version: item.version }
|
||||
);
|
||||
item.version++;
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.itemUpdatedSuccess'), type: 'success' });
|
||||
} catch (err) {
|
||||
item.price = originalPrice;
|
||||
item.price = originalPrice; // Revert optimistic update
|
||||
item.priceInput = originalPriceInput;
|
||||
notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || 'Failed to update item price.', type: 'error' });
|
||||
notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || t('listDetailPage.errors.updateItemPriceFailed'), type: 'error' });
|
||||
} finally {
|
||||
item.updating = false;
|
||||
}
|
||||
@ -678,6 +676,7 @@ const updateItemPrice = async (item: ItemWithUI) => {
|
||||
const deleteItem = async (item: ItemWithUI) => {
|
||||
if (!list.value) return;
|
||||
item.deleting = true;
|
||||
const originalItems = [...list.value.items]; // For potential revert
|
||||
|
||||
if (!isOnline.value) {
|
||||
offlineStore.addAction({
|
||||
@ -687,29 +686,35 @@ const deleteItem = async (item: ItemWithUI) => {
|
||||
itemId: String(item.id)
|
||||
}
|
||||
});
|
||||
list.value.items = list.value.items.filter(i => i.id !== item.id);
|
||||
list.value.items = list.value.items.filter(i => i.id !== item.id); // Optimistic UI
|
||||
item.deleting = false;
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.itemDeleteSuccess'), type: 'success' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.delete(API_ENDPOINTS.LISTS.ITEM(String(list.value.id), String(item.id)));
|
||||
list.value.items = list.value.items.filter(i => i.id !== item.id);
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.itemDeleteSuccess'), type: 'success' });
|
||||
} catch (err) {
|
||||
notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || 'Failed to delete item.', type: 'error' });
|
||||
list.value.items = originalItems; // Revert optimistic UI
|
||||
notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || t('listDetailPage.errors.deleteItemFailed'), type: 'error' });
|
||||
} finally {
|
||||
item.deleting = false;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmUpdateItem = (item: ItemWithUI, newCompleteStatus: boolean) => {
|
||||
confirmDialogMessage.value = `Mark "${item.name}" as ${newCompleteStatus ? 'complete' : 'incomplete'}?`;
|
||||
confirmDialogMessage.value = t('listDetailPage.confirmations.updateMessage', {
|
||||
itemName: item.name,
|
||||
status: newCompleteStatus ? t('listDetailPage.confirmations.statusComplete') : t('listDetailPage.confirmations.statusIncomplete')
|
||||
});
|
||||
pendingAction.value = () => updateItem(item, newCompleteStatus);
|
||||
showConfirmDialogState.value = true;
|
||||
};
|
||||
|
||||
const confirmDeleteItem = (item: ItemWithUI) => {
|
||||
confirmDialogMessage.value = `Delete "${item.name}"? This cannot be undone.`;
|
||||
confirmDialogMessage.value = t('listDetailPage.confirmations.deleteMessage', { itemName: item.name });
|
||||
pendingAction.value = () => deleteItem(item);
|
||||
showConfirmDialogState.value = true;
|
||||
};
|
||||
@ -723,20 +728,19 @@ const handleConfirmedAction = async () => {
|
||||
const cancelConfirmation = () => {
|
||||
showConfirmDialogState.value = false;
|
||||
pendingAction.value = null;
|
||||
confirmDialogMessage.value = ''; // Clear message
|
||||
};
|
||||
|
||||
const openOcrDialog = () => {
|
||||
ocrItems.value = [];
|
||||
ocrError.value = null;
|
||||
resetOcrFileDialog();
|
||||
resetOcrFileDialog(); // From useFileDialog
|
||||
showOcrDialogState.value = true;
|
||||
nextTick(() => {
|
||||
// For VInput type file, direct .value = '' might not work or be needed.
|
||||
// VInput should handle its own reset if necessary, or this ref might target the native input inside.
|
||||
if (ocrFileInputRef.value && ocrFileInputRef.value.$el) { // Assuming VInput exposes $el
|
||||
if (ocrFileInputRef.value && ocrFileInputRef.value.$el) {
|
||||
const inputElement = ocrFileInputRef.value.$el.querySelector('input[type="file"]') || ocrFileInputRef.value.$el;
|
||||
if (inputElement) (inputElement as HTMLInputElement).value = '';
|
||||
} else if (ocrFileInputRef.value) { // Fallback if ref is native input
|
||||
} else if (ocrFileInputRef.value) { // Native input
|
||||
(ocrFileInputRef.value as any).value = '';
|
||||
}
|
||||
});
|
||||
@ -774,17 +778,18 @@ const handleOcrUpload = async (file: File) => {
|
||||
});
|
||||
ocrItems.value = response.data.extracted_items.map((nameStr: string) => ({ name: nameStr.trim() })).filter((item: { name: string }) => item.name);
|
||||
if (ocrItems.value.length === 0) {
|
||||
ocrError.value = "No items extracted from the image.";
|
||||
ocrError.value = t('listDetailPage.errors.ocrNoItems');
|
||||
}
|
||||
} catch (err) {
|
||||
ocrError.value = (err instanceof Error ? err.message : String(err)) || 'Failed to process image.';
|
||||
ocrError.value = (err instanceof Error ? err.message : String(err)) || t('listDetailPage.errors.ocrFailed');
|
||||
} finally {
|
||||
ocrLoading.value = false;
|
||||
// Reset file input
|
||||
if (ocrFileInputRef.value && ocrFileInputRef.value.$el) {
|
||||
const inputElement = ocrFileInputRef.value.$el.querySelector('input[type="file"]') || ocrFileInputRef.value.$el;
|
||||
if (inputElement) (inputElement as HTMLInputElement).value = '';
|
||||
} else if (ocrFileInputRef.value) {
|
||||
(ocrFileInputRef.value as any).value = '';
|
||||
} else if (ocrFileInputRef.value) { // Native input
|
||||
(ocrFileInputRef.value as any).value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -798,16 +803,18 @@ const addOcrItems = async () => {
|
||||
if (!item.name.trim()) continue;
|
||||
const response = await apiClient.post(
|
||||
API_ENDPOINTS.LISTS.ITEMS(String(list.value.id)),
|
||||
{ name: item.name, quantity: "1" } // Assuming default quantity 1 for OCR items
|
||||
{ name: item.name, quantity: "1" } // Default quantity 1
|
||||
);
|
||||
const addedItem = response.data as Item;
|
||||
list.value.items.push(processListItems([addedItem])[0]);
|
||||
successCount++;
|
||||
}
|
||||
if (successCount > 0) notificationStore.addNotification({ message: `${successCount} item(s) added successfully from OCR.`, type: 'success' });
|
||||
if (successCount > 0) {
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.itemsAddedSuccessOcr', { count: successCount }), type: 'success' });
|
||||
}
|
||||
closeOcrDialog();
|
||||
} catch (err) {
|
||||
notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || 'Failed to add OCR items.', type: 'error' });
|
||||
notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || t('listDetailPage.errors.addOcrItemsFailed'), type: 'error' });
|
||||
} finally {
|
||||
addingOcrItems.value = false;
|
||||
}
|
||||
@ -821,7 +828,7 @@ const fetchListCostSummary = async () => {
|
||||
const response = await apiClient.get(API_ENDPOINTS.COSTS.LIST_SUMMARY(list.value.id));
|
||||
listCostSummary.value = response.data;
|
||||
} catch (err) {
|
||||
costSummaryError.value = (err instanceof Error ? err.message : String(err)) || 'Failed to load cost summary.';
|
||||
costSummaryError.value = (err instanceof Error ? err.message : String(err)) || t('listDetailPage.errors.loadCostSummaryFailed');
|
||||
listCostSummary.value = null;
|
||||
} finally {
|
||||
costSummaryLoading.value = false;
|
||||
@ -844,19 +851,19 @@ const getPaidAmountForSplitDisplay = (split: ExpenseSplit): string => {
|
||||
|
||||
const getSplitStatusText = (status: ExpenseSplitStatusEnum): string => {
|
||||
switch (status) {
|
||||
case ExpenseSplitStatusEnum.PAID: return 'Paid';
|
||||
case ExpenseSplitStatusEnum.PARTIALLY_PAID: return 'Partially Paid';
|
||||
case ExpenseSplitStatusEnum.UNPAID: return 'Unpaid';
|
||||
default: return status;
|
||||
case ExpenseSplitStatusEnum.PAID: return t('listDetailPage.status.paid');
|
||||
case ExpenseSplitStatusEnum.PARTIALLY_PAID: return t('listDetailPage.status.partiallyPaid');
|
||||
case ExpenseSplitStatusEnum.UNPAID: return t('listDetailPage.status.unpaid');
|
||||
default: return t('listDetailPage.status.unknown');
|
||||
}
|
||||
};
|
||||
|
||||
const getOverallExpenseStatusText = (status: ExpenseOverallStatusEnum): string => {
|
||||
switch (status) {
|
||||
case ExpenseOverallStatusEnum.PAID: return 'Settled';
|
||||
case ExpenseOverallStatusEnum.PARTIALLY_PAID: return 'Partially Settled';
|
||||
case ExpenseOverallStatusEnum.UNPAID: return 'Unsettled';
|
||||
default: return status;
|
||||
case ExpenseOverallStatusEnum.PAID: return t('listDetailPage.status.settled');
|
||||
case ExpenseOverallStatusEnum.PARTIALLY_PAID: return t('listDetailPage.status.partiallySettled');
|
||||
case ExpenseOverallStatusEnum.UNPAID: return t('listDetailPage.status.unsettled');
|
||||
default: return t('listDetailPage.status.unknown');
|
||||
}
|
||||
};
|
||||
|
||||
@ -872,76 +879,74 @@ useEventListener(window, 'keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === 'n' && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
|
||||
return;
|
||||
return; // Don't interfere with typing
|
||||
}
|
||||
if (showOcrDialogState.value || showCostSummaryDialog.value || showConfirmDialogState.value) {
|
||||
// Check if any modal is open, if so, don't trigger
|
||||
if (showOcrDialogState.value || showCostSummaryDialog.value || showConfirmDialogState.value || showEditDialog.value || showSettleModal.value || showCreateExpenseForm.value) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (itemNameInputRef.value?.$el) {
|
||||
if (itemNameInputRef.value?.$el) { // Focus the add item input
|
||||
(itemNameInputRef.value.$el as HTMLElement).focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let touchStartX = 0;
|
||||
const SWIPE_THRESHOLD = 50;
|
||||
const SWIPE_THRESHOLD = 50; // Pixels
|
||||
|
||||
const handleTouchStart = (event: TouchEvent) => {
|
||||
touchStartX = event.changedTouches[0].clientX;
|
||||
};
|
||||
|
||||
const handleTouchMove = () => {
|
||||
const handleTouchMove = (event: TouchEvent, item: ItemWithUI) => {
|
||||
// This function might be used for swipe-to-reveal actions in the future
|
||||
// For now, it's a placeholder or can be removed if not used.
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
const handleTouchEnd = (event: TouchEvent, item: ItemWithUI) => {
|
||||
// This function might be used for swipe-to-reveal actions in the future
|
||||
// For now, it's a placeholder or can be removed if not used.
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
pageInitialLoad.value = true;
|
||||
itemsAreLoading.value = false;
|
||||
error.value = null; // Clear stale errors on mount
|
||||
error.value = null;
|
||||
|
||||
if (!route.params.id) {
|
||||
error.value = 'No list ID provided';
|
||||
pageInitialLoad.value = false; // Stop initial load phase, show error
|
||||
listDetailStore.setError('No list ID provided for expenses.'); // Set error in expense store
|
||||
error.value = t('listDetailPage.errors.fetchFailed'); // Generic error if no ID
|
||||
pageInitialLoad.value = false;
|
||||
listDetailStore.setError(t('listDetailPage.errors.fetchFailed'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to load shell data from sessionStorage
|
||||
const listShellJSON = sessionStorage.getItem('listDetailShell');
|
||||
const routeId = String(route.params.id);
|
||||
|
||||
if (listShellJSON) {
|
||||
const shellData = JSON.parse(listShellJSON);
|
||||
// Ensure the shell data is for the current list
|
||||
if (shellData.id === parseInt(routeId, 10)) {
|
||||
list.value = {
|
||||
id: shellData.id,
|
||||
name: shellData.name,
|
||||
description: shellData.description,
|
||||
is_complete: false, // Assume not complete until full data loaded
|
||||
items: [], // Start with no items, they will be fetched by fetchListDetails
|
||||
version: 0, // Placeholder, will be updated
|
||||
updated_at: new Date().toISOString(), // Placeholder
|
||||
is_complete: false,
|
||||
items: [],
|
||||
version: 0,
|
||||
updated_at: new Date().toISOString(),
|
||||
group_id: shellData.group_id,
|
||||
};
|
||||
pageInitialLoad.value = false; // Shell loaded, main page spinner can go
|
||||
// Optionally, clear the sessionStorage item after use
|
||||
// sessionStorage.removeItem('listDetailShell');
|
||||
pageInitialLoad.value = false;
|
||||
} else {
|
||||
// Shell data is for a different list, clear it
|
||||
sessionStorage.removeItem('listDetailShell');
|
||||
// pageInitialLoad remains true, will be set to false by fetchListDetails
|
||||
}
|
||||
}
|
||||
|
||||
fetchListDetails().then(() => { // Fetches items
|
||||
fetchListDetails().then(() => {
|
||||
startPolling();
|
||||
});
|
||||
// Fetch expenses using the store when component is mounted
|
||||
const routeParamsId = route.params.id;
|
||||
listDetailStore.fetchListWithExpenses(String(routeParamsId));
|
||||
});
|
||||
@ -951,7 +956,7 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
const editItem = (item: Item) => {
|
||||
editingItem.value = { ...item };
|
||||
editingItem.value = { ...item }; // Clone item for editing
|
||||
showEditDialog.value = true;
|
||||
};
|
||||
|
||||
@ -963,25 +968,22 @@ const closeEditDialog = () => {
|
||||
const handleConfirmEdit = async () => {
|
||||
if (!editingItem.value || !list.value) return;
|
||||
|
||||
const item = editingItem.value;
|
||||
const originalItem = list.value.items.find(i => i.id === item.id);
|
||||
if (!originalItem) return;
|
||||
const itemToUpdate = editingItem.value; // Already a clone
|
||||
|
||||
try {
|
||||
const response = await apiClient.put(
|
||||
API_ENDPOINTS.LISTS.ITEM(String(list.value.id), String(item.id)),
|
||||
API_ENDPOINTS.LISTS.ITEM(String(list.value.id), String(itemToUpdate.id)),
|
||||
{
|
||||
name: item.name,
|
||||
quantity: item.quantity?.toString(),
|
||||
version: item.version
|
||||
name: itemToUpdate.name,
|
||||
quantity: itemToUpdate.quantity?.toString(), // Ensure quantity is string or null
|
||||
version: itemToUpdate.version
|
||||
}
|
||||
);
|
||||
|
||||
// Update the item in the list
|
||||
const updatedItem = response.data as Item;
|
||||
const index = list.value.items.findIndex(i => i.id === item.id);
|
||||
const updatedItemFromApi = response.data as Item;
|
||||
const index = list.value.items.findIndex(i => i.id === updatedItemFromApi.id);
|
||||
if (index !== -1) {
|
||||
list.value.items[index] = processListItems([updatedItem])[0];
|
||||
list.value.items[index] = processListItems([updatedItemFromApi])[0];
|
||||
}
|
||||
|
||||
notificationStore.addNotification({
|
||||
@ -991,7 +993,7 @@ const handleConfirmEdit = async () => {
|
||||
closeEditDialog();
|
||||
} catch (err) {
|
||||
notificationStore.addNotification({
|
||||
message: (err instanceof Error ? err.message : String(err)) || 'Failed to update item',
|
||||
message: (err instanceof Error ? err.message : String(err)) || t('listDetailPage.errors.updateItemFailed'),
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
@ -999,7 +1001,7 @@ const handleConfirmEdit = async () => {
|
||||
|
||||
const openSettleShareModal = (expense: Expense, split: ExpenseSplit) => {
|
||||
if (split.user_id !== authStore.user?.id) {
|
||||
notificationStore.addNotification({ message: "You can only settle your own shares.", type: 'warning' });
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.cannotSettleOthersShares'), type: 'warning' });
|
||||
return;
|
||||
}
|
||||
selectedSplitForSettlement.value = split;
|
||||
@ -1023,24 +1025,24 @@ const closeSettleShareModal = () => {
|
||||
const validateSettleAmount = (): boolean => {
|
||||
settleAmountError.value = null;
|
||||
if (!settleAmount.value.trim()) {
|
||||
settleAmountError.value = 'Please enter an amount.';
|
||||
settleAmountError.value = t('listDetailPage.settleShareModal.errors.enterAmount');
|
||||
return false;
|
||||
}
|
||||
const amount = new Decimal(settleAmount.value);
|
||||
if (amount.isNaN() || amount.isNegative() || amount.isZero()) {
|
||||
settleAmountError.value = 'Please enter a positive amount.';
|
||||
settleAmountError.value = t('listDetailPage.settleShareModal.errors.positiveAmount');
|
||||
return false;
|
||||
}
|
||||
if (selectedSplitForSettlement.value) {
|
||||
const alreadyPaid = new Decimal(listDetailStore.getPaidAmountForSplit(selectedSplitForSettlement.value.id));
|
||||
const owed = new Decimal(selectedSplitForSettlement.value.owed_amount);
|
||||
const remaining = owed.minus(alreadyPaid);
|
||||
if (amount.greaterThan(remaining.plus(new Decimal('0.001')))) { // Epsilon for float issues
|
||||
settleAmountError.value = `Amount cannot exceed remaining: ${formatCurrency(remaining.toFixed(2))}.`;
|
||||
if (amount.greaterThan(remaining.plus(new Decimal('0.001')))) {
|
||||
settleAmountError.value = t('listDetailPage.settleShareModal.errors.exceedsRemaining', { amount: formatCurrency(remaining.toFixed(2)) });
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
settleAmountError.value = 'Error: No split selected.'; // Should not happen
|
||||
settleAmountError.value = t('listDetailPage.settleShareModal.errors.noSplitSelected');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -1050,13 +1052,13 @@ const currentListIdForRefetch = computed(() => listDetailStore.currentList?.id |
|
||||
|
||||
const handleConfirmSettle = async () => {
|
||||
if (!selectedSplitForSettlement.value || !authStore.user?.id || !currentListIdForRefetch.value) {
|
||||
notificationStore.addNotification({ message: 'Cannot process settlement: missing data.', type: 'error' });
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.settlementDataMissing'), type: 'error' });
|
||||
return;
|
||||
}
|
||||
// Use settleAmount.value which is the confirmed amount (remaining amount for MVP)
|
||||
|
||||
const activityData: SettlementActivityCreate = {
|
||||
expense_split_id: selectedSplitForSettlement.value.id,
|
||||
paid_by_user_id: Number(authStore.user.id), // Convert to number
|
||||
paid_by_user_id: Number(authStore.user.id),
|
||||
amount_paid: new Decimal(settleAmount.value).toString(),
|
||||
paid_at: new Date().toISOString(),
|
||||
};
|
||||
@ -1068,15 +1070,14 @@ const handleConfirmSettle = async () => {
|
||||
});
|
||||
|
||||
if (success) {
|
||||
notificationStore.addNotification({ message: 'Share settled successfully!', type: 'success' });
|
||||
notificationStore.addNotification({ message: t('listDetailPage.notifications.settleShareSuccess'), type: 'success' });
|
||||
closeSettleShareModal();
|
||||
} else {
|
||||
notificationStore.addNotification({ message: listDetailStore.error || 'Failed to settle share.', type: 'error' });
|
||||
notificationStore.addNotification({ message: listDetailStore.error || t('listDetailPage.notifications.settleShareFailed'), type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleExpenseCreated = (expense: any) => {
|
||||
// Refresh the expenses list
|
||||
if (list.value?.id) {
|
||||
listDetailStore.fetchListWithExpenses(String(list.value.id));
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<main class="container page-padding">
|
||||
<div class="page-header">
|
||||
<h1 class="mb-3">My Assigned Chores</h1>
|
||||
<h1 class="mb-3">{{ $t('myChoresPage.title') }}</h1>
|
||||
<div class="header-controls">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" v-model="showCompleted" @change="loadAssignments">
|
||||
<span class="toggle-slider"></span>
|
||||
Show Completed
|
||||
{{ $t('myChoresPage.showCompletedToggle') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -17,7 +17,7 @@
|
||||
<div v-if="assignmentsByTimeline.overdue.length > 0" class="timeline-section overdue">
|
||||
<div class="timeline-header">
|
||||
<div class="timeline-dot overdue"></div>
|
||||
<h2 class="timeline-title">Overdue</h2>
|
||||
<h2 class="timeline-title">{{ $t('myChoresPage.timelineHeaders.overdue') }}</h2>
|
||||
<span class="timeline-count">{{ assignmentsByTimeline.overdue.length }}</span>
|
||||
</div>
|
||||
<div class="timeline-items">
|
||||
@ -29,7 +29,7 @@
|
||||
<h3>{{ assignment.chore?.name }}</h3>
|
||||
<div class="assignment-tags">
|
||||
<span class="chore-type-tag" :class="assignment.chore?.type">
|
||||
{{ assignment.chore?.type === 'personal' ? 'Personal' : 'Group' }}
|
||||
{{ assignment.chore?.type === 'personal' ? $t('myChoresPage.choreCard.personal') : $t('myChoresPage.choreCard.group') }}
|
||||
</span>
|
||||
<span class="chore-frequency-tag" :class="assignment.chore?.frequency">
|
||||
{{ formatFrequency(assignment.chore?.frequency) }}
|
||||
@ -39,7 +39,7 @@
|
||||
<div class="assignment-meta">
|
||||
<div class="assignment-due-date overdue">
|
||||
<span class="material-icons">schedule</span>
|
||||
Due {{ formatDate(assignment.due_date) }}
|
||||
{{ $t('myChoresPage.choreCard.duePrefix') }} {{ formatDate(assignment.due_date) }}
|
||||
</div>
|
||||
<div v-if="assignment.chore?.description" class="assignment-description">
|
||||
{{ assignment.chore.description }}
|
||||
@ -49,7 +49,7 @@
|
||||
<button class="btn btn-success btn-sm" @click="completeAssignment(assignment)"
|
||||
:disabled="isCompleting">
|
||||
<span class="material-icons">check_circle</span>
|
||||
Mark Complete
|
||||
{{ $t('myChoresPage.choreCard.markCompleteButton') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -61,7 +61,7 @@
|
||||
<div v-if="assignmentsByTimeline.today.length > 0" class="timeline-section today">
|
||||
<div class="timeline-header">
|
||||
<div class="timeline-dot today"></div>
|
||||
<h2 class="timeline-title">Due Today</h2>
|
||||
<h2 class="timeline-title">{{ $t('myChoresPage.timelineHeaders.today') }}</h2>
|
||||
<span class="timeline-count">{{ assignmentsByTimeline.today.length }}</span>
|
||||
</div>
|
||||
<div class="timeline-items">
|
||||
@ -73,7 +73,7 @@
|
||||
<h3>{{ assignment.chore?.name }}</h3>
|
||||
<div class="assignment-tags">
|
||||
<span class="chore-type-tag" :class="assignment.chore?.type">
|
||||
{{ assignment.chore?.type === 'personal' ? 'Personal' : 'Group' }}
|
||||
{{ assignment.chore?.type === 'personal' ? $t('myChoresPage.choreCard.personal') : $t('myChoresPage.choreCard.group') }}
|
||||
</span>
|
||||
<span class="chore-frequency-tag" :class="assignment.chore?.frequency">
|
||||
{{ formatFrequency(assignment.chore?.frequency) }}
|
||||
@ -83,7 +83,7 @@
|
||||
<div class="assignment-meta">
|
||||
<div class="assignment-due-date today">
|
||||
<span class="material-icons">today</span>
|
||||
Due Today
|
||||
{{ $t('myChoresPage.choreCard.dueToday') }}
|
||||
</div>
|
||||
<div v-if="assignment.chore?.description" class="assignment-description">
|
||||
{{ assignment.chore.description }}
|
||||
@ -93,7 +93,7 @@
|
||||
<button class="btn btn-success btn-sm" @click="completeAssignment(assignment)"
|
||||
:disabled="isCompleting">
|
||||
<span class="material-icons">check_circle</span>
|
||||
Mark Complete
|
||||
{{ $t('myChoresPage.choreCard.markCompleteButton') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -105,7 +105,7 @@
|
||||
<div v-if="assignmentsByTimeline.thisWeek.length > 0" class="timeline-section this-week">
|
||||
<div class="timeline-header">
|
||||
<div class="timeline-dot this-week"></div>
|
||||
<h2 class="timeline-title">This Week</h2>
|
||||
<h2 class="timeline-title">{{ $t('myChoresPage.timelineHeaders.thisWeek') }}</h2>
|
||||
<span class="timeline-count">{{ assignmentsByTimeline.thisWeek.length }}</span>
|
||||
</div>
|
||||
<div class="timeline-items">
|
||||
@ -117,7 +117,7 @@
|
||||
<h3>{{ assignment.chore?.name }}</h3>
|
||||
<div class="assignment-tags">
|
||||
<span class="chore-type-tag" :class="assignment.chore?.type">
|
||||
{{ assignment.chore?.type === 'personal' ? 'Personal' : 'Group' }}
|
||||
{{ assignment.chore?.type === 'personal' ? $t('myChoresPage.choreCard.personal') : $t('myChoresPage.choreCard.group') }}
|
||||
</span>
|
||||
<span class="chore-frequency-tag" :class="assignment.chore?.frequency">
|
||||
{{ formatFrequency(assignment.chore?.frequency) }}
|
||||
@ -127,7 +127,7 @@
|
||||
<div class="assignment-meta">
|
||||
<div class="assignment-due-date this-week">
|
||||
<span class="material-icons">date_range</span>
|
||||
Due {{ formatDate(assignment.due_date) }}
|
||||
{{ $t('myChoresPage.choreCard.duePrefix') }} {{ formatDate(assignment.due_date) }}
|
||||
</div>
|
||||
<div v-if="assignment.chore?.description" class="assignment-description">
|
||||
{{ assignment.chore.description }}
|
||||
@ -137,7 +137,7 @@
|
||||
<button class="btn btn-success btn-sm" @click="completeAssignment(assignment)"
|
||||
:disabled="isCompleting">
|
||||
<span class="material-icons">check_circle</span>
|
||||
Mark Complete
|
||||
{{ $t('myChoresPage.choreCard.markCompleteButton') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -149,7 +149,7 @@
|
||||
<div v-if="assignmentsByTimeline.later.length > 0" class="timeline-section later">
|
||||
<div class="timeline-header">
|
||||
<div class="timeline-dot later"></div>
|
||||
<h2 class="timeline-title">Later</h2>
|
||||
<h2 class="timeline-title">{{ $t('myChoresPage.timelineHeaders.later') }}</h2>
|
||||
<span class="timeline-count">{{ assignmentsByTimeline.later.length }}</span>
|
||||
</div>
|
||||
<div class="timeline-items">
|
||||
@ -161,7 +161,7 @@
|
||||
<h3>{{ assignment.chore?.name }}</h3>
|
||||
<div class="assignment-tags">
|
||||
<span class="chore-type-tag" :class="assignment.chore?.type">
|
||||
{{ assignment.chore?.type === 'personal' ? 'Personal' : 'Group' }}
|
||||
{{ assignment.chore?.type === 'personal' ? $t('myChoresPage.choreCard.personal') : $t('myChoresPage.choreCard.group') }}
|
||||
</span>
|
||||
<span class="chore-frequency-tag" :class="assignment.chore?.frequency">
|
||||
{{ formatFrequency(assignment.chore?.frequency) }}
|
||||
@ -171,7 +171,7 @@
|
||||
<div class="assignment-meta">
|
||||
<div class="assignment-due-date later">
|
||||
<span class="material-icons">schedule</span>
|
||||
Due {{ formatDate(assignment.due_date) }}
|
||||
{{ $t('myChoresPage.choreCard.duePrefix') }} {{ formatDate(assignment.due_date) }}
|
||||
</div>
|
||||
<div v-if="assignment.chore?.description" class="assignment-description">
|
||||
{{ assignment.chore.description }}
|
||||
@ -181,7 +181,7 @@
|
||||
<button class="btn btn-success btn-sm" @click="completeAssignment(assignment)"
|
||||
:disabled="isCompleting">
|
||||
<span class="material-icons">check_circle</span>
|
||||
Mark Complete
|
||||
{{ $t('myChoresPage.choreCard.markCompleteButton') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -193,7 +193,7 @@
|
||||
<div v-if="showCompleted && assignmentsByTimeline.completed.length > 0" class="timeline-section completed">
|
||||
<div class="timeline-header">
|
||||
<div class="timeline-dot completed"></div>
|
||||
<h2 class="timeline-title">Completed</h2>
|
||||
<h2 class="timeline-title">{{ $t('myChoresPage.timelineHeaders.completed') }}</h2>
|
||||
<span class="timeline-count">{{ assignmentsByTimeline.completed.length }}</span>
|
||||
</div>
|
||||
<div class="timeline-items">
|
||||
@ -205,7 +205,7 @@
|
||||
<h3>{{ assignment.chore?.name }}</h3>
|
||||
<div class="assignment-tags">
|
||||
<span class="chore-type-tag" :class="assignment.chore?.type">
|
||||
{{ assignment.chore?.type === 'personal' ? 'Personal' : 'Group' }}
|
||||
{{ assignment.chore?.type === 'personal' ? $t('myChoresPage.choreCard.personal') : $t('myChoresPage.choreCard.group') }}
|
||||
</span>
|
||||
<span class="chore-frequency-tag" :class="assignment.chore?.frequency">
|
||||
{{ formatFrequency(assignment.chore?.frequency) }}
|
||||
@ -215,7 +215,7 @@
|
||||
<div class="assignment-meta">
|
||||
<div class="assignment-due-date completed">
|
||||
<span class="material-icons">check_circle</span>
|
||||
Completed {{ formatDate(assignment.completed_at || assignment.updated_at) }}
|
||||
{{ $t('myChoresPage.choreCard.completedPrefix') }} {{ formatDate(assignment.completed_at || assignment.updated_at) }}
|
||||
</div>
|
||||
<div v-if="assignment.chore?.description" class="assignment-description">
|
||||
{{ assignment.chore.description }}
|
||||
@ -231,14 +231,14 @@
|
||||
<svg class="icon icon-lg" aria-hidden="true">
|
||||
<use xlink:href="#icon-clipboard" />
|
||||
</svg>
|
||||
<h3>No Assignments Yet!</h3>
|
||||
<p v-if="showCompleted">You have no chore assignments (completed or pending).</p>
|
||||
<p v-else>You have no pending chore assignments.</p>
|
||||
<h3>{{ $t('myChoresPage.emptyState.title') }}</h3>
|
||||
<p v-if="showCompleted">{{ $t('myChoresPage.emptyState.noAssignmentsAll') }}</p>
|
||||
<p v-else>{{ $t('myChoresPage.emptyState.noAssignmentsPending') }}</p>
|
||||
<router-link to="/chores" class="btn btn-primary mt-2">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-eye" />
|
||||
</svg>
|
||||
View All Chores
|
||||
{{ $t('myChoresPage.emptyState.viewAllChoresButton') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</main>
|
||||
@ -246,11 +246,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { format } from 'date-fns'
|
||||
import { choreService } from '../services/choreService'
|
||||
import { useNotificationStore } from '../stores/notifications'
|
||||
import type { ChoreAssignment, ChoreFrequency } from '../types/chore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
// State
|
||||
@ -310,13 +312,14 @@ const assignmentsByTimeline = computed(() => {
|
||||
return timeline
|
||||
})
|
||||
|
||||
const frequencyOptions = [
|
||||
{ label: 'One Time', value: 'one_time' as ChoreFrequency },
|
||||
{ label: 'Daily', value: 'daily' as ChoreFrequency },
|
||||
{ label: 'Weekly', value: 'weekly' as ChoreFrequency },
|
||||
{ label: 'Monthly', value: 'monthly' as ChoreFrequency },
|
||||
{ label: 'Custom', value: 'custom' as ChoreFrequency }
|
||||
]
|
||||
// frequencyOptions is not directly used for display labels anymore, but can be kept for logic if needed elsewhere.
|
||||
// const frequencyOptions = [
|
||||
// { label: 'One Time', value: 'one_time' as ChoreFrequency },
|
||||
// { label: 'Daily', value: 'daily' as ChoreFrequency },
|
||||
// { label: 'Weekly', value: 'weekly' as ChoreFrequency },
|
||||
// { label: 'Monthly', value: 'monthly' as ChoreFrequency },
|
||||
// { label: 'Custom', value: 'custom' as ChoreFrequency }
|
||||
// ]
|
||||
|
||||
// Methods
|
||||
const loadAssignments = async () => {
|
||||
@ -325,7 +328,7 @@ const loadAssignments = async () => {
|
||||
} catch (error) {
|
||||
console.error('Failed to load assignments:', error)
|
||||
notificationStore.addNotification({
|
||||
message: 'Failed to load assignments',
|
||||
message: t('myChoresPage.notifications.loadFailed'),
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
@ -338,7 +341,7 @@ const completeAssignment = async (assignment: ChoreAssignment) => {
|
||||
try {
|
||||
await choreService.completeAssignment(assignment.id)
|
||||
notificationStore.addNotification({
|
||||
message: `Marked "${assignment.chore?.name}" as complete!`,
|
||||
message: t('myChoresPage.notifications.markedComplete', { choreName: assignment.chore?.name || '' }),
|
||||
type: 'success'
|
||||
})
|
||||
// Reload assignments to show updated state
|
||||
@ -346,7 +349,7 @@ const completeAssignment = async (assignment: ChoreAssignment) => {
|
||||
} catch (error) {
|
||||
console.error('Failed to complete assignment:', error)
|
||||
notificationStore.addNotification({
|
||||
message: 'Failed to mark assignment as complete',
|
||||
message: t('myChoresPage.notifications.markCompleteFailed'),
|
||||
type: 'error'
|
||||
})
|
||||
} finally {
|
||||
@ -355,23 +358,34 @@ const completeAssignment = async (assignment: ChoreAssignment) => {
|
||||
}
|
||||
|
||||
const formatDate = (date: string | undefined) => {
|
||||
if (!date) return 'Unknown'
|
||||
if (!date) return t('myChoresPage.dates.unknownDate');
|
||||
|
||||
if (date.includes('T')) {
|
||||
return format(new Date(date), 'MMM d, yyyy')
|
||||
} else {
|
||||
const parts = date.split('-')
|
||||
if (parts.length === 3) {
|
||||
return format(new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2])), 'MMM d, yyyy')
|
||||
// Attempt to parse and format; date-fns handles various ISO and other formats.
|
||||
try {
|
||||
const parsedDate = new Date(date);
|
||||
// Check if parsedDate is valid
|
||||
if (isNaN(parsedDate.getTime())) {
|
||||
// Handle cases like "YYYY-MM-DD" which might be parsed as UTC midnight
|
||||
// and then potentially displayed incorrectly depending on timezone.
|
||||
// If the input is just a date string without time, ensure it's treated as local.
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
||||
const [year, month, day] = date.split('-').map(Number);
|
||||
return format(new Date(year, month - 1, day), 'MMM d, yyyy');
|
||||
}
|
||||
return t('myChoresPage.dates.invalidDate');
|
||||
}
|
||||
return format(parsedDate, 'MMM d, yyyy');
|
||||
} catch (e) {
|
||||
// Catch any error during parsing (though Date constructor is quite forgiving)
|
||||
return t('myChoresPage.dates.invalidDate');
|
||||
}
|
||||
return 'Invalid Date'
|
||||
}
|
||||
|
||||
const formatFrequency = (frequency: ChoreFrequency | undefined) => {
|
||||
if (!frequency) return 'Unknown'
|
||||
const option = frequencyOptions.find(opt => opt.value === frequency)
|
||||
return option ? option.label : frequency
|
||||
if (!frequency) return t('myChoresPage.frequencies.unknown');
|
||||
// Assuming keys like myChoresPage.frequencies.one_time, myChoresPage.frequencies.daily
|
||||
// The ChoreFrequency enum values ('one_time', 'daily', etc.) match the last part of the key.
|
||||
return t(`myChoresPage.frequencies.${frequency}`);
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
|
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<main class="container page-padding">
|
||||
<div class="row q-mb-md items-center justify-between">
|
||||
<h1 class="mb-3">Personal Chores</h1>
|
||||
<h1 class="mb-3">{{ $t('personalChoresPage.title') }}</h1>
|
||||
<button class="btn btn-primary" @click="openCreateChoreModal">
|
||||
<span class="material-icons">add</span>
|
||||
New Chore
|
||||
{{ $t('personalChoresPage.newChoreButton') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
<div class="neo-card-body">
|
||||
<div class="neo-chore-info">
|
||||
<div class="neo-chore-due">
|
||||
Due: {{ formatDate(chore.next_due_date) }}
|
||||
{{ $t('personalChoresPage.dates.duePrefix') }}: {{ formatDate(chore.next_due_date) }}
|
||||
</div>
|
||||
<div v-if="chore.description" class="neo-chore-description">
|
||||
{{ chore.description }}
|
||||
@ -31,11 +31,11 @@
|
||||
<div class="neo-card-actions">
|
||||
<button class="btn btn-primary btn-sm" @click="openEditChoreModal(chore)">
|
||||
<span class="material-icons">edit</span>
|
||||
Edit
|
||||
{{ $t('personalChoresPage.editButton') }}
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" @click="confirmDeleteChore(chore)">
|
||||
<span class="material-icons">delete</span>
|
||||
Delete
|
||||
{{ $t('personalChoresPage.deleteButton') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,7 +46,7 @@
|
||||
<div v-if="showChoreModal" class="neo-modal">
|
||||
<div class="neo-modal-content">
|
||||
<div class="neo-modal-header">
|
||||
<h3>{{ isEditing ? 'Edit Chore' : 'New Chore' }}</h3>
|
||||
<h3>{{ isEditing ? $t('personalChoresPage.modals.editChoreTitle') : $t('personalChoresPage.modals.newChoreTitle') }}</h3>
|
||||
<button class="btn btn-neutral btn-icon-only" @click="showChoreModal = false">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
@ -54,7 +54,7 @@
|
||||
<div class="neo-modal-body">
|
||||
<form @submit.prevent="onSubmit" class="neo-form">
|
||||
<div class="neo-form-group">
|
||||
<label for="name">Name</label>
|
||||
<label for="name">{{ $t('personalChoresPage.form.nameLabel') }}</label>
|
||||
<input
|
||||
id="name"
|
||||
v-model="choreForm.name"
|
||||
@ -65,7 +65,7 @@
|
||||
</div>
|
||||
|
||||
<div class="neo-form-group">
|
||||
<label for="description">Description</label>
|
||||
<label for="description">{{ $t('personalChoresPage.form.descriptionLabel') }}</label>
|
||||
<textarea
|
||||
id="description"
|
||||
v-model="choreForm.description"
|
||||
@ -75,7 +75,7 @@
|
||||
</div>
|
||||
|
||||
<div class="neo-form-group">
|
||||
<label for="frequency">Frequency</label>
|
||||
<label for="frequency">{{ $t('personalChoresPage.form.frequencyLabel') }}</label>
|
||||
<select
|
||||
id="frequency"
|
||||
v-model="choreForm.frequency"
|
||||
@ -89,7 +89,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="choreForm.frequency === 'custom'" class="neo-form-group">
|
||||
<label for="interval">Interval (days)</label>
|
||||
<label for="interval">{{ $t('personalChoresPage.form.intervalLabel') }}</label>
|
||||
<input
|
||||
id="interval"
|
||||
v-model.number="choreForm.custom_interval_days"
|
||||
@ -101,7 +101,7 @@
|
||||
</div>
|
||||
|
||||
<div class="neo-form-group">
|
||||
<label for="dueDate">Next Due Date</label>
|
||||
<label for="dueDate">{{ $t('personalChoresPage.form.dueDateLabel') }}</label>
|
||||
<input
|
||||
id="dueDate"
|
||||
v-model="choreForm.next_due_date"
|
||||
@ -113,8 +113,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="neo-modal-footer">
|
||||
<button class="btn btn-neutral" @click="showChoreModal = false">Cancel</button>
|
||||
<button class="btn btn-primary" @click="onSubmit">Save</button>
|
||||
<button class="btn btn-neutral" @click="showChoreModal = false">{{ $t('personalChoresPage.cancelButton') }}</button>
|
||||
<button class="btn btn-primary" @click="onSubmit">{{ $t('personalChoresPage.saveButton') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -123,17 +123,17 @@
|
||||
<div v-if="showDeleteDialog" class="neo-modal">
|
||||
<div class="neo-modal-content">
|
||||
<div class="neo-modal-header">
|
||||
<h3>Delete Chore</h3>
|
||||
<h3>{{ $t('personalChoresPage.modals.deleteChoreTitle') }}</h3>
|
||||
<button class="btn btn-neutral btn-icon-only" @click="showDeleteDialog = false">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="neo-modal-body">
|
||||
<p>Are you sure you want to delete this chore?</p>
|
||||
<p>{{ $t('personalChoresPage.deleteDialog.confirmationText') }}</p>
|
||||
</div>
|
||||
<div class="neo-modal-footer">
|
||||
<button class="btn btn-neutral" @click="showDeleteDialog = false">Cancel</button>
|
||||
<button class="btn btn-danger" @click="deleteChore">Delete</button>
|
||||
<button class="btn btn-neutral" @click="showDeleteDialog = false">{{ $t('personalChoresPage.cancelButton') }}</button>
|
||||
<button class="btn btn-danger" @click="deleteChore">{{ $t('personalChoresPage.deleteButton') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -141,12 +141,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { format } from 'date-fns'
|
||||
import { choreService } from '../services/choreService'
|
||||
import { useNotificationStore } from '../stores/notifications'
|
||||
import type { Chore, ChoreCreate, ChoreUpdate, ChoreFrequency } from '../types/chore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
// State
|
||||
@ -165,13 +167,13 @@ const choreForm = ref<ChoreCreate>({
|
||||
type: 'personal'
|
||||
})
|
||||
|
||||
const frequencyOptions = [
|
||||
{ label: 'One Time', value: 'one_time' as ChoreFrequency },
|
||||
{ label: 'Daily', value: 'daily' as ChoreFrequency },
|
||||
{ label: 'Weekly', value: 'weekly' as ChoreFrequency },
|
||||
{ label: 'Monthly', value: 'monthly' as ChoreFrequency },
|
||||
{ label: 'Custom', value: 'custom' as ChoreFrequency }
|
||||
]
|
||||
const frequencyOptions = computed(() => [
|
||||
{ label: t('personalChoresPage.frequencies.one_time'), value: 'one_time' as ChoreFrequency },
|
||||
{ label: t('personalChoresPage.frequencies.daily'), value: 'daily' as ChoreFrequency },
|
||||
{ label: t('personalChoresPage.frequencies.weekly'), value: 'weekly' as ChoreFrequency },
|
||||
{ label: t('personalChoresPage.frequencies.monthly'), value: 'monthly' as ChoreFrequency },
|
||||
{ label: t('personalChoresPage.frequencies.custom'), value: 'custom' as ChoreFrequency }
|
||||
])
|
||||
|
||||
// Methods
|
||||
const loadChores = async () => {
|
||||
@ -180,7 +182,7 @@ const loadChores = async () => {
|
||||
} catch (error) {
|
||||
console.error('Failed to load personal chores:', error)
|
||||
notificationStore.addNotification({
|
||||
message: 'Failed to load personal chores',
|
||||
message: t('personalChoresPage.notifications.loadFailed'),
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
@ -216,13 +218,13 @@ const onSubmit = async () => {
|
||||
if (isEditing.value && selectedChore.value) {
|
||||
await choreService.updatePersonalChore(selectedChore.value.id, payload as ChoreUpdate)
|
||||
notificationStore.addNotification({
|
||||
message: 'Personal chore updated successfully',
|
||||
message: t('personalChoresPage.notifications.updateSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
await choreService.createPersonalChore(payload as ChoreCreate)
|
||||
notificationStore.addNotification({
|
||||
message: 'Personal chore created successfully',
|
||||
message: t('personalChoresPage.notifications.createSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
@ -231,7 +233,7 @@ const onSubmit = async () => {
|
||||
} catch (error) {
|
||||
console.error('Failed to save personal chore:', error)
|
||||
notificationStore.addNotification({
|
||||
message: `Failed to ${isEditing.value ? 'update' : 'create'} personal chore`,
|
||||
message: t('personalChoresPage.notifications.saveFailed'), // Generic message
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
@ -249,34 +251,43 @@ const deleteChore = async () => {
|
||||
await choreService.deletePersonalChore(selectedChore.value.id)
|
||||
showDeleteDialog.value = false
|
||||
notificationStore.addNotification({
|
||||
message: 'Personal chore deleted successfully',
|
||||
message: t('personalChoresPage.notifications.deleteSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
loadChores()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete personal chore:', error)
|
||||
notificationStore.addNotification({
|
||||
message: 'Failed to delete personal chore',
|
||||
message: t('personalChoresPage.notifications.deleteFailed'),
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
if (date && date.includes('T')) {
|
||||
return format(new Date(date), 'MMM d, yyyy');
|
||||
} else if (date) {
|
||||
const parts = date.split('-');
|
||||
if (parts.length === 3) {
|
||||
return format(new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2])), 'MMM d, yyyy');
|
||||
const formatDate = (date: string | undefined) => {
|
||||
if (!date) return ''; // Or perhaps a specific 'Unknown Date' string if desired: t('personalChoresPage.dates.unknownDate')
|
||||
try {
|
||||
// Handles both 'YYYY-MM-DD' and full ISO with 'T'
|
||||
const parsedDate = new Date(date);
|
||||
if (isNaN(parsedDate.getTime())) {
|
||||
// Explicitly handle 'YYYY-MM-DD' if new Date() struggles with it directly as local time
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
||||
const [year, month, day] = date.split('-').map(Number);
|
||||
return format(new Date(year, month - 1, day), 'MMM d, yyyy');
|
||||
}
|
||||
return t('personalChoresPage.dates.invalidDate');
|
||||
}
|
||||
return format(parsedDate, 'MMM d, yyyy');
|
||||
} catch (e) {
|
||||
return t('personalChoresPage.dates.invalidDate');
|
||||
}
|
||||
}
|
||||
return 'Invalid Date';
|
||||
}
|
||||
|
||||
const formatFrequency = (frequency: ChoreFrequency) => {
|
||||
const option = frequencyOptions.find(opt => opt.value === frequency)
|
||||
return option ? option.label : frequency
|
||||
const formatFrequency = (frequency: ChoreFrequency | undefined) => {
|
||||
if (!frequency) return t('personalChoresPage.frequencies.unknown');
|
||||
// Use the value from frequencyOptions which is now translated
|
||||
const option = frequencyOptions.value.find(opt => opt.value === frequency);
|
||||
return option ? option.label : t(`personalChoresPage.frequencies.${frequency}`); // Fallback if somehow not in options
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
|
@ -2,29 +2,29 @@
|
||||
<main class="flex items-center justify-center page-container">
|
||||
<div class="card signup-card">
|
||||
<div class="card-header">
|
||||
<h3>Sign Up</h3>
|
||||
<h3>{{ $t('signupPage.header') }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form @submit.prevent="onSubmit" class="form-layout">
|
||||
<div class="form-group mb-2">
|
||||
<label for="name" class="form-label">Full Name</label>
|
||||
<label for="name" class="form-label">{{ $t('signupPage.fullNameLabel') }}</label>
|
||||
<input type="text" id="name" v-model="name" class="form-input" required autocomplete="name" />
|
||||
<p v-if="formErrors.name" class="form-error-text">{{ formErrors.name }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-2">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<label for="email" class="form-label">{{ $t('signupPage.emailLabel') }}</label>
|
||||
<input type="email" id="email" v-model="email" class="form-input" required autocomplete="email" />
|
||||
<p v-if="formErrors.email" class="form-error-text">{{ formErrors.email }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-2">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<label for="password" class="form-label">{{ $t('signupPage.passwordLabel') }}</label>
|
||||
<div class="input-with-icon-append">
|
||||
<input :type="isPwdVisible ? 'text' : 'password'" id="password" v-model="password" class="form-input"
|
||||
required autocomplete="new-password" />
|
||||
<button type="button" @click="isPwdVisible = !isPwdVisible" class="icon-append-btn"
|
||||
aria-label="Toggle password visibility">
|
||||
:aria-label="$t('signupPage.togglePasswordVisibility')">
|
||||
<svg class="icon icon-sm">
|
||||
<use :xlink:href="isPwdVisible ? '#icon-settings' : '#icon-settings'"></use>
|
||||
</svg> <!-- Placeholder for visibility icons -->
|
||||
@ -34,7 +34,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label for="confirmPassword" class="form-label">Confirm Password</label>
|
||||
<label for="confirmPassword" class="form-label">{{ $t('signupPage.confirmPasswordLabel') }}</label>
|
||||
<input :type="isPwdVisible ? 'text' : 'password'" id="confirmPassword" v-model="confirmPassword"
|
||||
class="form-input" required autocomplete="new-password" />
|
||||
<p v-if="formErrors.confirmPassword" class="form-error-text">{{ formErrors.confirmPassword }}</p>
|
||||
@ -44,11 +44,11 @@
|
||||
|
||||
<button type="submit" class="btn btn-primary w-full mt-2" :disabled="loading">
|
||||
<span v-if="loading" class="spinner-dots-sm" role="status"><span /><span /><span /></span>
|
||||
Sign Up
|
||||
{{ $t('signupPage.submitButton') }}
|
||||
</button>
|
||||
|
||||
<div class="text-center mt-2">
|
||||
<router-link to="auth/login" class="link-styled">Already have an account? Login</router-link>
|
||||
<router-link to="auth/login" class="link-styled">{{ $t('signupPage.loginLink') }}</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -59,9 +59,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAuthStore } from '@/stores/auth'; // Assuming path is correct
|
||||
import { useNotificationStore } from '@/stores/notifications';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
@ -82,22 +84,22 @@ const isValidEmail = (val: string): boolean => {
|
||||
const validateForm = (): boolean => {
|
||||
formErrors.value = {};
|
||||
if (!name.value.trim()) {
|
||||
formErrors.value.name = 'Name is required';
|
||||
formErrors.value.name = t('signupPage.validation.nameRequired');
|
||||
}
|
||||
if (!email.value.trim()) {
|
||||
formErrors.value.email = 'Email is required';
|
||||
formErrors.value.email = t('signupPage.validation.emailRequired');
|
||||
} else if (!isValidEmail(email.value)) {
|
||||
formErrors.value.email = 'Invalid email format';
|
||||
formErrors.value.email = t('signupPage.validation.emailInvalid');
|
||||
}
|
||||
if (!password.value) {
|
||||
formErrors.value.password = 'Password is required';
|
||||
formErrors.value.password = t('signupPage.validation.passwordRequired');
|
||||
} else if (password.value.length < 8) {
|
||||
formErrors.value.password = 'Password must be at least 8 characters';
|
||||
formErrors.value.password = t('signupPage.validation.passwordLength');
|
||||
}
|
||||
if (!confirmPassword.value) {
|
||||
formErrors.value.confirmPassword = 'Please confirm your password';
|
||||
formErrors.value.confirmPassword = t('signupPage.validation.confirmPasswordRequired');
|
||||
} else if (password.value !== confirmPassword.value) {
|
||||
formErrors.value.confirmPassword = 'Passwords do not match';
|
||||
formErrors.value.confirmPassword = t('signupPage.validation.passwordsNoMatch');
|
||||
}
|
||||
return Object.keys(formErrors.value).length === 0;
|
||||
};
|
||||
@ -114,13 +116,17 @@ const onSubmit = async () => {
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
});
|
||||
notificationStore.addNotification({ message: 'Account created successfully. Please login.', type: 'success' });
|
||||
notificationStore.addNotification({ message: t('signupPage.notifications.signupSuccess'), type: 'success' });
|
||||
router.push('auth/login');
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : 'Signup failed. Please try again.';
|
||||
formErrors.value.general = message;
|
||||
console.error(message, error);
|
||||
notificationStore.addNotification({ message, type: 'error' });
|
||||
// Prefer API error message if available, otherwise use generic translated message for the form
|
||||
const errorMessageForForm = error instanceof Error ? error.message : t('signupPage.notifications.signupFailed');
|
||||
formErrors.value.general = errorMessageForForm;
|
||||
|
||||
// For the notification pop-up, always use the generic translated message if API message is not specific enough or not an Error
|
||||
const notificationMessage = error instanceof Error && error.message ? error.message : t('signupPage.notifications.signupFailed');
|
||||
console.error("Signup error:", error); // Keep detailed log for developers
|
||||
notificationStore.addNotification({ message: notificationMessage, type: 'error' });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user