ph4 #49
@ -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