diff --git a/custom_components/choreops/engines/schedule_engine.py b/custom_components/choreops/engines/schedule_engine.py index ae3cb79..c5b96dc 100644 --- a/custom_components/choreops/engines/schedule_engine.py +++ b/custom_components/choreops/engines/schedule_engine.py @@ -1186,6 +1186,7 @@ def calculate_next_due_date_from_chore_info( interval_unit=custom_unit, delta=custom_interval, require_future=True, + reference_datetime=reference_time, return_type=const.HELPER_RETURN_DATETIME, ), ) diff --git a/custom_components/choreops/managers/chore_manager.py b/custom_components/choreops/managers/chore_manager.py index a151e3d..dc87b16 100644 --- a/custom_components/choreops/managers/chore_manager.py +++ b/custom_components/choreops/managers/chore_manager.py @@ -5679,8 +5679,14 @@ def _record_chore_missed( # Emit signal for StatisticsManager to handle period buckets # Pass missed_streak_tally for daily bucket snapshot (display purposes) # Phase 3 Step 2: Include optional due_date and reason fields (D-07) + chore_info = self.coordinator.chores_data.get(chore_id) signal_payload: dict[str, Any] = { "chore_id": chore_id, + "chore_name": str( + chore_info.get(const.DATA_CHORE_NAME, "Unknown Chore") + if chore_info + else "Unknown Chore" + ), "user_id": assignee_id, "user_name": assignee_name, "missed_streak_tally": new_missed_streak, diff --git a/custom_components/choreops/managers/notification_manager.py b/custom_components/choreops/managers/notification_manager.py index 96182d8..6b6ffdf 100644 --- a/custom_components/choreops/managers/notification_manager.py +++ b/custom_components/choreops/managers/notification_manager.py @@ -800,6 +800,22 @@ def _translate_action_buttons( return translated_actions + def _get_points_label(self) -> str: + """Return the configured label for points.""" + return str( + self.coordinator.config_entry.options.get( + const.CONF_POINTS_LABEL, const.DEFAULT_POINTS_LABEL + ) + ) + + def _build_notification_placeholders( + self, message_data: dict[str, Any] | None + ) -> dict[str, Any]: + """Build notification placeholders with shared defaults.""" + placeholders = dict(message_data or {}) + placeholders.setdefault("points_label", self._get_points_label()) + return placeholders + # ========================================================================= # Core Notification Send Method # ========================================================================= @@ -1073,17 +1089,18 @@ async def notify_assignee_translated( message_json_key = self._convert_notification_key(message_key) title_notification = translations.get(title_json_key, {}) message_notification = translations.get(message_json_key, {}) + placeholders = self._build_notification_placeholders(message_data) # Format title and message with placeholders title = self._format_notification_text( title_notification.get("title", title_key), - message_data, + placeholders, title_json_key, "title", ) message = self._format_notification_text( message_notification.get("message", message_key), - message_data, + placeholders, message_json_key, "message", ) @@ -1262,17 +1279,18 @@ async def notify_approvers_translated( message_json_key = self._convert_notification_key(message_key) title_notification = translations.get(title_json_key, {}) message_notification = translations.get(message_json_key, {}) + placeholders = self._build_notification_placeholders(message_data) # Format both title and message with placeholders title = self._format_notification_text( title_notification.get("title", title_key), - message_data, + placeholders, title_json_key, "title", ) message = self._format_notification_text( message_notification.get("message", message_key), - message_data, + placeholders, message_json_key, "message", ) @@ -1384,7 +1402,7 @@ async def broadcast_to_all_approvers( """ perf_start = time.perf_counter() notification_tasks: list[tuple[str, Any]] = [] - message_data = placeholders or {} + message_data = self._build_notification_placeholders(placeholders) for approver_id, approver_info in self.coordinator.approvers_data.items(): # Use approver's language preference, fall back to system language diff --git a/custom_components/choreops/translations_custom/en_notifications.json b/custom_components/choreops/translations_custom/en_notifications.json index 2b24c17..5f3d1b8 100644 --- a/custom_components/choreops/translations_custom/en_notifications.json +++ b/custom_components/choreops/translations_custom/en_notifications.json @@ -4,7 +4,7 @@ "_comment_assignee_footer": "══════════════════════════════════════════════════════════════════", "chore_approved_assignee": { "title": "🎉 Chore Approved!", - "message": "{chore_name} approved! You earned {points} points" + "message": "{chore_name} approved! You earned {points} {points_label}" }, "chore_disapproved_assignee": { "title": "↩️ Chore Returned", @@ -12,11 +12,11 @@ }, "chore_overdue_assignee": { "title": "⏰ Chore Overdue", - "message": "{chore_name} is overdue! Complete it now to earn {points} points" + "message": "{chore_name} is overdue! Complete it now to earn {points} {points_label}" }, "chore_overdue_steal_available": { "title": "⚡ Chore Alert", - "message": "{chore_name} is overdue and available to steal! Complete it first to earn {points} points" + "message": "{chore_name} is overdue and available to steal! Complete it first to earn {points} {points_label}" }, "chore_missed_assignee": { "title": "😔 Chore Expired", @@ -24,11 +24,11 @@ }, "chore_due_reminder_assignee": { "title": "⏰ Chore Reminder", - "message": "{chore_name} is due in {minutes} minutes! Worth {points} points" + "message": "{chore_name} is due in {minutes} minutes! Worth {points} {points_label}" }, "chore_due_window_assignee": { "title": "🎯 Chore Now Due", - "message": "{chore_name} is now due! Complete within {hours} hour(s) to earn {points} points" + "message": "{chore_name} is now due! Complete within {hours} hour(s) to earn {points} {points_label}" }, "reward_approved_assignee": { "title": "🎉 Reward Approved!", @@ -52,15 +52,15 @@ }, "penalty_applied_assignee": { "title": "⚠️ Penalty Applied", - "message": "{penalty_name}: {points} points deducted. Let's get back on track!" + "message": "{penalty_name}: {points} {points_label} deducted. Let's get back on track!" }, "bonus_applied_assignee": { "title": "⭐ Bonus Applied!", - "message": "{bonus_name}: You earned {points} bonus points! Great job" + "message": "{bonus_name}: You earned {points} bonus {points_label}! Great job" }, "multiplier_changed_assignee": { "title": "⚖️ Multiplier Updated", - "message": "Your points multiplier changed from {old_multiplier}x to {new_multiplier}x" + "message": "Your {points_label} multiplier changed from {old_multiplier}x to {new_multiplier}x" }, "_comment_approver_section": "══════════════════════════════════════════════════════════════════", "_comment_approver_desc": "APPROVER NOTIFICATIONS: Informative, third-person, actionable. Format: '{assignee_name}: Action'. NO 'ChoreOps:' prefix.", @@ -79,11 +79,11 @@ }, "pending_chores_approver": { "title": "📋 {assignee_name}: {count} Pending", - "message": "{count} chores awaiting review. Latest: {latest_chore} (+{points} pts)" + "message": "{count} chores awaiting review. Latest: {latest_chore} (+{points} {points_label})" }, "reward_claimed_approver": { "title": "🎁 {assignee_name}: Reward Request", - "message": "{assignee_name} requested {reward_name} for {points} points" + "message": "{assignee_name} requested {reward_name} for {points} {points_label}" }, "reward_reminder_approver": { "title": "⏰ {assignee_name}: Reward Pending", @@ -103,7 +103,7 @@ }, "multiplier_changed_approver": { "title": "⚖️ {assignee_name}: Multiplier Updated", - "message": "{assignee_name}'s points multiplier changed from {old_multiplier}x to {new_multiplier}x" + "message": "{assignee_name}'s {points_label} multiplier changed from {old_multiplier}x to {new_multiplier}x" }, "_comment_system_section": "══════════════════════════════════════════════════════════════════", "_comment_system_desc": "SYSTEM/ADMIN NOTIFICATIONS: Data management operations",