diff --git a/README.md b/README.md index 89ff5863..38e38fef 100755 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ 3. **Parents approve** — chores marked "requires approval" land in a pending queue 4. **Kids claim rewards** — points are only deducted once a parent approves the claim -Everything is managed from the **TaskMate panel** in the Home Assistant sidebar, and surfaced on your dashboards through 30 Lovelace cards. +Everything is managed from the **TaskMate panel** in the Home Assistant sidebar, and surfaced on your dashboards through 31 Lovelace cards. All data stays inside your Home Assistant instance. Nothing is sent to any external service. @@ -73,9 +73,9 @@ For manual installation and the first-run walkthrough, see [Installation](https: **Chores** — flexible [scheduling](https://tempus2016.github.io/taskmate/Chore-Scheduling/) (specific days, recurring down to every N days, rotation, first-come), [difficulty tiers](https://tempus2016.github.io/taskmate/Difficulty-Tiers/), [dependencies](https://tempus2016.github.io/taskmate/Chore-Dependencies/), [timed tasks](https://tempus2016.github.io/taskmate/Timed-Tasks/), [weekly targets](https://tempus2016.github.io/taskmate/Weekly-Targets/) for "three times this week, any day" with an optional bonus for hitting them, [photo proof](https://tempus2016.github.io/taskmate/Photo-Proof/), [open-ended chores](https://tempus2016.github.io/taskmate/Open-Ended-Chores/) the child describes and you price at approval, [teamwork chores](https://tempus2016.github.io/taskmate/Teamwork-Chores/) that need two or more children, [chore auctions](https://tempus2016.github.io/taskmate/Chore-Auctions/) where the lowest bid wins an unpopular job, a [bounty board](https://tempus2016.github.io/taskmate/Bounty-Board/) of one-off jobs any child can claim, [swaps](https://tempus2016.github.io/taskmate/Chore-Swaps/), [vacation mode](https://tempus2016.github.io/taskmate/Vacation-Mode/), and chores that appear or hide based on [HA entity state](https://tempus2016.github.io/taskmate/Dynamic-Chore-Visibility/) or [the weather](https://tempus2016.github.io/taskmate/Weather-Aware-Chores/). Children can [tap an NFC tag](https://tempus2016.github.io/taskmate/NFC-Tag-Completion/) to tick a chore off, and you can add, move and skip chores straight from [Home Assistant's calendar](https://tempus2016.github.io/taskmate/Two-Way-Calendar/). Missed ticking one? [Log a past job](https://tempus2016.github.io/taskmate/Logging-Past-Jobs/) on the day it was really done, up to a week back. Chores can carry an [estimated time](https://tempus2016.github.io/taskmate/Estimated-Minutes/) so children see how long is left, a [reference photo](https://tempus2016.github.io/taskmate/Reference-Photos/) of the job done well, or a [quantity](https://tempus2016.github.io/taskmate/Quantity-Chores/) that pays per item, and pending chores can [approve themselves](https://tempus2016.github.io/taskmate/Auto-Approve/) after a set number of hours. -**Points & rewards** — [fixed-cost rewards](https://tempus2016.github.io/taskmate/Rewards/) with a parent approval flow, [savings jars](https://tempus2016.github.io/taskmate/Pool-Mode-\(Savings-Jars\)/), a [wishlist](https://tempus2016.github.io/taskmate/Wishlist/) children save towards and family can pledge to, [bonuses](https://tempus2016.github.io/taskmate/Bonuses/) and [penalties](https://tempus2016.github.io/taskmate/Penalties/), [weekend multipliers and streak milestones](https://tempus2016.github.io/taskmate/Bonus-Points/), [1–3 star quality ratings](https://tempus2016.github.io/taskmate/Quality-Ratings/) that scale the points, [real-money allowance](https://tempus2016.github.io/taskmate/Allowance/), [shared family goals](https://tempus2016.github.io/taskmate/Family-Goals/), [timed device unlocks](https://tempus2016.github.io/taskmate/Timed-Unlock-Rewards/), and [time-locked rewards](https://tempus2016.github.io/taskmate/Time-Locked-Rewards/) that are only claimable on chosen days or between set hours. Children can buy tickets for a [raffle](https://tempus2016.github.io/taskmate/Raffles/) with their points, and a [make-it-right penalty](https://tempus2016.github.io/taskmate/Make-It-Right-Penalties/) can be earned back by doing a chore. +**Points & rewards** — [fixed-cost rewards](https://tempus2016.github.io/taskmate/Rewards/) with a parent approval flow, [savings jars](https://tempus2016.github.io/taskmate/Pool-Mode-\(Savings-Jars\)/), a [wishlist](https://tempus2016.github.io/taskmate/Wishlist/) children save towards and family can pledge to, [bonuses](https://tempus2016.github.io/taskmate/Bonuses/) and [penalties](https://tempus2016.github.io/taskmate/Penalties/), [weekend multipliers and streak milestones](https://tempus2016.github.io/taskmate/Bonus-Points/), [1–3 star quality ratings](https://tempus2016.github.io/taskmate/Quality-Ratings/) that scale the points, [real-money allowance](https://tempus2016.github.io/taskmate/Allowance/), [shared family goals](https://tempus2016.github.io/taskmate/Family-Goals/), [timed device unlocks](https://tempus2016.github.io/taskmate/Timed-Unlock-Rewards/), and [time-locked rewards](https://tempus2016.github.io/taskmate/Time-Locked-Rewards/) that are only claimable on chosen days or between set hours. Children can buy tickets for a [raffle](https://tempus2016.github.io/taskmate/Raffles/) with their points, a [make-it-right penalty](https://tempus2016.github.io/taskmate/Make-It-Right-Penalties/) can be earned back by doing a chore, and [reward fulfilment](https://tempus2016.github.io/taskmate/Reward-Fulfilment/) keeps a redeemed reward on an owed list until somebody hands it over. -**Gamification** — a [virtual pet](https://tempus2016.github.io/taskmate/Virtual-Pets/) that hatches and grows as chores get done, [personal weekly goals](https://tempus2016.github.io/taskmate/Weekly-Goals/) with a bonus, [levels & XP](https://tempus2016.github.io/taskmate/Levels-and-XP/), [achievement badges](https://tempus2016.github.io/taskmate/Achievement-Badges/), [quests](https://tempus2016.github.io/taskmate/Quests/), [challenges](https://tempus2016.github.io/taskmate/Challenges/), [unlockable avatars](https://tempus2016.github.io/taskmate/Avatars/), [celebrations](https://tempus2016.github.io/taskmate/Celebrations/), [leaderboard seasons](https://tempus2016.github.io/taskmate/Leaderboard-Seasons/), [streak freezes](https://tempus2016.github.io/taskmate/Streak-Freezes/) that protect a streak through a missed day, [birthday mode](https://tempus2016.github.io/taskmate/Birthday-Mode/), [surprise inspections](https://tempus2016.github.io/taskmate/Surprise-Inspections/) with a bonus, and "Wrapped"-style [recaps](https://tempus2016.github.io/taskmate/Recaps/) on a schedule you choose. +**Gamification** — a [virtual pet](https://tempus2016.github.io/taskmate/Virtual-Pets/) that hatches and grows as chores get done, [personal weekly goals](https://tempus2016.github.io/taskmate/Weekly-Goals/) with a bonus, [personal bests](https://tempus2016.github.io/taskmate/Personal-Bests/) every child can beat whatever their place on the leaderboard, [levels & XP](https://tempus2016.github.io/taskmate/Levels-and-XP/), [achievement badges](https://tempus2016.github.io/taskmate/Achievement-Badges/), [quests](https://tempus2016.github.io/taskmate/Quests/), [challenges](https://tempus2016.github.io/taskmate/Challenges/), [unlockable avatars](https://tempus2016.github.io/taskmate/Avatars/), [celebrations](https://tempus2016.github.io/taskmate/Celebrations/), [leaderboard seasons](https://tempus2016.github.io/taskmate/Leaderboard-Seasons/), [streak freezes](https://tempus2016.github.io/taskmate/Streak-Freezes/) that protect a streak through a missed day, [birthday mode](https://tempus2016.github.io/taskmate/Birthday-Mode/), [surprise inspections](https://tempus2016.github.io/taskmate/Surprise-Inspections/) with a bonus, and "Wrapped"-style [recaps](https://tempus2016.github.io/taskmate/Recaps/) on a schedule you choose. **Parent tooling** — an [admin panel](https://tempus2016.github.io/taskmate/Admin-Panel/) with a Today home page and a [setup wizard](https://tempus2016.github.io/taskmate/Setup-Wizard/) that suggests chores by age, [ready-made dashboards](https://tempus2016.github.io/taskmate/Ready-Made-Dashboards/) for the kids, the parents and a wall tablet, [notifications](https://tempus2016.github.io/taskmate/Notifications/) in your Home Assistant language, with [quiet hours](https://tempus2016.github.io/taskmate/Quiet-Hours/), [reminder escalation](https://tempus2016.github.io/taskmate/Reminder-Escalation/) and [presence-aware reminders](https://tempus2016.github.io/taskmate/Presence-Aware-Reminders/) that wait until a child is home, [reject reasons](https://tempus2016.github.io/taskmate/Reject-Reasons/) the child can see, [multi-parent approval routing](https://tempus2016.github.io/taskmate/Multi-Parent-Approval-Routing/), a [weekly digest](https://tempus2016.github.io/taskmate/Weekly-Digest/), [insight reports](https://tempus2016.github.io/taskmate/Insights/) on fairness and friction, an [audit log](https://tempus2016.github.io/taskmate/Admin-Audit-Log/), [sick days](https://tempus2016.github.io/taskmate/Sick-Days/) that pause chores and protect streaks, [event entities](https://tempus2016.github.io/taskmate/Event-Entities/) for building automations in the UI editor, [Repairs issues and a diagnostics download](https://tempus2016.github.io/taskmate/Repairs-and-Diagnostics/), [adult profiles](https://tempus2016.github.io/taskmate/Adult-Profiles/) so parents can track their own chores without approving each other's, and [backup & restore](https://tempus2016.github.io/taskmate/Backup-and-Restore/). @@ -89,7 +89,7 @@ Lovelace resources register automatically on startup. Edit your dashboard → ** | For kids | For parents | |----------|-------------| -| Child · Sticker Chart · Pet · Raffle · Rewards · Reward Progress · Points Display · Routine · Kiosk · Streak · Badges · Bounty Board · Wishlist · Recap · Chore Auction | Approvals · Parent Dashboard · Overview · Chore Board · Activity · Points · Reorder · Penalties · Bonuses · Weekly · Graph · Leaderboard · Calendar · Family Goal · Photo Gallery | +| Child · Sticker Chart · Pet · Raffle · Rewards · Reward Progress · Points Display · Routine · Kiosk · Streak · Personal Bests · Badges · Bounty Board · Wishlist · Recap · Chore Auction | Approvals · Parent Dashboard · Overview · Chore Board · Activity · Points · Reorder · Penalties · Bonuses · Weekly · Graph · Leaderboard · Calendar · Family Goal · Photo Gallery | Every card takes a `header_color` and a `card_design` option. See [Dashboard Cards](https://tempus2016.github.io/taskmate/Dashboard-Cards/) for each card's full configuration. @@ -108,7 +108,7 @@ Every card takes a `header_color` and a `card_design` option. See [Dashboard Car | [Getting Started](https://tempus2016.github.io/taskmate/Getting-Started/) | First-time setup walkthrough | | [Admin Panel](https://tempus2016.github.io/taskmate/Admin-Panel/) | The management hub in the HA sidebar | | [Settings](https://tempus2016.github.io/taskmate/Settings/) | Every configurable setting in one place | -| [Dashboard Cards](https://tempus2016.github.io/taskmate/Dashboard-Cards/) | All 30 Lovelace cards and their options | +| [Dashboard Cards](https://tempus2016.github.io/taskmate/Dashboard-Cards/) | All 31 Lovelace cards and their options | | [Services](https://tempus2016.github.io/taskmate/Services/) | Every callable service, with examples | | [Sensor Reference](https://tempus2016.github.io/taskmate/Sensor-Reference/) | All entities and their attributes | | [Automations](https://tempus2016.github.io/taskmate/Automations/) | Bus events, blueprints, and examples | @@ -134,4 +134,4 @@ Bug reports and feature requests are welcome in [Issues](https://github.com/temp MIT — see [LICENSE](LICENSE). -> Originally created by [vinnybad/choremander](https://github.com/vinnybad/choremander). This fork adds 30 Lovelace cards, an admin panel, a bonus points system, streak tracking, reward approval flow, a penalty system, and much more. +> Originally created by [vinnybad/choremander](https://github.com/vinnybad/choremander). This fork adds 31 Lovelace cards, an admin panel, a bonus points system, streak tracking, reward approval flow, a penalty system, and much more. diff --git a/custom_components/taskmate/__init__.py b/custom_components/taskmate/__init__.py index 1212e3b6..d2c4e65f 100644 --- a/custom_components/taskmate/__init__.py +++ b/custom_components/taskmate/__init__.py @@ -2030,6 +2030,7 @@ async def handle_rebuild_badges(call: ServiceCall) -> None: parent_action=_parent, get_coordinator=lambda: _get_coordinator(hass), require_linked_child=lambda *args: _async_require_linked_child(*args), + require_parent=lambda *args: _async_require_parent(*args), ) diff --git a/custom_components/taskmate/const.py b/custom_components/taskmate/const.py index 3073f065..7c6941a9 100644 --- a/custom_components/taskmate/const.py +++ b/custom_components/taskmate/const.py @@ -369,6 +369,15 @@ def is_valid_completion_sound(value: str | None) -> bool: NOTIF_TYPE_PENALTY_REDEEMED: Final = "penalty_redeemed" # Raffles: the winner was drawn. NOTIF_TYPE_RAFFLE_DRAWN: Final = "raffle_drawn" +# Reward fulfilment (#1098): a redeemed reward is still waiting to be handed over. +NOTIF_TYPE_REWARD_OWED: Final = "reward_owed" +# Personal bests (#1099): a child beat one of their own records. +NOTIF_TYPE_PERSONAL_BEST: Final = "personal_best" + +# Reward fulfilment (#1098): default days before parents are reminded a +# redeemed reward is still owed (0 = never remind), and the most allowed. +DEFAULT_REWARD_FULFILMENT_REMINDER_DAYS: Final = 3 +REWARD_FULFILMENT_REMINDER_MAX_DAYS: Final = 60 # Presence-aware reminders (#926): the default for how long a child must have # been away before arriving home earns a "you're home" nudge. diff --git a/custom_components/taskmate/coord_bests.py b/custom_components/taskmate/coord_bests.py new file mode 100644 index 00000000..59e010d7 --- /dev/null +++ b/custom_components/taskmate/coord_bests.py @@ -0,0 +1,249 @@ +"""Personal bests (#1099): a child's own records, and the moment they beat one. + +Badges and streak milestones celebrate round numbers set by a parent. A +personal best is the child's own bar: the most points they have ever earned +in a day or a week, the most chores in a day, their biggest single award and +their longest streak. Each record keeps the value and the day it was set. + +Records are checked after every approval. The first time a measure is seen +it is stored quietly as the baseline — a child's very first chore is not +"five records at once". From then on, going past a stored value fires +``taskmate_personal_best``, the opt-in ``personal_best`` notification and a +tier-2 celebration, and the record shows as new on the child's card for the +rest of the day. + +When an approval is undone or rejected the affected day or week is +recomputed from the completions still on file; a record that no longer +holds falls back to the best the kept history can show. Records older than +the kept history are left as they are. +""" + +from __future__ import annotations + +import logging +from datetime import date, timedelta +from typing import Any + +from homeassistant.util import dt as dt_util + +from . import notify_strings +from .const import NOTIF_TYPE_PERSONAL_BEST + +_LOGGER = logging.getLogger(__name__) + +RECORD_KEYS: tuple[str, ...] = ( + "longest_streak", + "best_day_points", + "best_day_chores", + "best_week_points", + "best_single_award", +) +_DAY_RECORDS = ("best_day_points", "best_day_chores") +_WEEK_RECORDS = ("best_week_points",) + + +def _week_start(day: date) -> date: + return day - timedelta(days=day.weekday()) + + +class PersonalBestsMixin: + """Per-child records with a new-record celebration.""" + + def _personal_bests(self) -> dict[str, dict[str, Any]]: + return self.storage.get_section("personal_bests", dict) + + def _child_completions(self, child_id: str) -> list: + return [c for c in self.storage.get_completions() if c.child_id == child_id and c.approved] + + def _local_day(self, comp) -> date: + return dt_util.as_local(comp.completed_at).date() + + def _measures_for(self, child, completions: list) -> dict[str, tuple[int, str]]: + """Each record's current value with the day it belongs to.""" + today = dt_util.as_local(dt_util.now()).date() + week_start = _week_start(today) + day_points = day_chores = week_points = 0 + single = (0, "") + for comp in completions: + day = self._local_day(comp) + pts = int(comp.points_awarded or 0) + if day == today: + day_points += pts + if not comp.bonus_subtask_id: + day_chores += 1 + if day >= week_start: + week_points += pts + if pts > single[0]: + single = (pts, day.isoformat()) + return { + "longest_streak": (int(getattr(child, "current_streak", 0) or 0), today.isoformat()), + "best_day_points": (day_points, today.isoformat()), + "best_day_chores": (day_chores, today.isoformat()), + "best_week_points": (week_points, week_start.isoformat()), + "best_single_award": single, + } + + def personal_bests_state(self, child) -> dict[str, Any]: + """The records board for a card, or {} while there is nothing to show. + + ``new_today`` lists the records beaten today, so a card can make a + fuss for the rest of the day. + """ + section = self._personal_bests() + records = (section.get(child.id) or {}) if isinstance(section, dict) else {} + out: dict[str, Any] = {} + today = dt_util.as_local(dt_util.now()).date().isoformat() + new_today = [] + for key in RECORD_KEYS: + rec = records.get(key) + if not rec: + continue + entry = {"value": int(rec.get("value", 0) or 0), "date": rec.get("date", "") or ""} + if rec.get("previous") is not None and str(rec.get("set_at", ""))[:10] == today: + entry["previous"] = int(rec["previous"]) + new_today.append(key) + out[key] = entry + if "longest_streak" not in out and int(getattr(child, "best_streak", 0) or 0) > 0: + # Streaks were tracked long before records were: show the old best + # so nobody starts from zero. It has no date of its own. + out["longest_streak"] = {"value": int(child.best_streak), "date": ""} + if new_today: + out["new_today"] = new_today + return out + + async def async_check_personal_bests(self, child_id: str, refresh: bool = True) -> list[str]: + """Store any measure now past its record. Returns the records beaten (not baselines). + + Runs after the approval path's own refresh, so when something changed + the cards need another push or the new record sits unseen until the + next unrelated update. Batch callers (a filled team, Approve all) pass + ``refresh=False`` and refresh once themselves (#794). + """ + child = self.get_child(child_id) + if child is None: + return [] + section = self._personal_bests() + if not isinstance(section, dict): + return [] + records = section.setdefault(child.id, {}) + measures = self._measures_for(child, self._child_completions(child.id)) + now = dt_util.now() + beaten: list[str] = [] + changed = False + for key, (value, day) in measures.items(): + if value <= 0: + continue + rec = records.get(key) + if rec is None: + if key == "longest_streak" and int(getattr(child, "best_streak", 0) or 0) > value: + # The old best streak is the bar to beat, not today's run. + records[key] = {"value": int(child.best_streak), "date": "", "set_at": now.isoformat()} + else: + records[key] = {"value": value, "date": day, "set_at": now.isoformat()} + changed = True + continue + previous = int(rec.get("value", 0) or 0) + if value <= previous: + continue + records[key] = {"value": value, "date": day, "set_at": now.isoformat(), "previous": previous} + changed = True + beaten.append(key) + if not changed: + return [] + await self.storage.async_save() + points_name = self.storage.get_points_name() + for key in beaten: + rec = records[key] + label = notify_strings.render(self.hass, f"record_{key}", {"points_name": points_name}) + payload = { + "child_id": child.id, + "child_name": child.name, + "record": key, + "value": rec["value"], + "previous": rec["previous"], + "date": rec["date"], + "timestamp": now.isoformat(), + } + self.hass.bus.async_fire("taskmate_personal_best", payload) + if getattr(self, "notifications", None): + await self.notifications.fire( + NOTIF_TYPE_PERSONAL_BEST, + {**payload, "record_label": label, "points_name": points_name}, + ) + if hasattr(self, "_celebrate"): + await self._celebrate( + child, + "personal_best", + notify_strings.render( + self.hass, + "celebrate_personal_best", + {"child_name": child.name, "record_label": label, "value": rec["value"]}, + ), + tier=2, + extra={"record": key, "value": rec["value"], "previous": rec["previous"]}, + ) + if refresh: + await self.async_refresh() + return beaten + + async def async_recompute_personal_bests(self, child_id: str) -> None: + """After a reversal: a record set on a day (or week) we still hold must still be true.""" + section = self._personal_bests() + records = section.get(child_id) if isinstance(section, dict) else None + if not records: + return + completions = self._child_completions(child_id) + if completions: + oldest = min(self._local_day(c) for c in completions) + else: + oldest = dt_util.as_local(dt_util.now()).date() + days: dict[str, dict[str, int]] = {} + single: dict[str, int] = {} + for comp in completions: + day = self._local_day(comp).isoformat() + pts = int(comp.points_awarded or 0) + bucket = days.setdefault(day, {"points": 0, "chores": 0}) + bucket["points"] += pts + if not comp.bonus_subtask_id: + bucket["chores"] += 1 + single[day] = max(single.get(day, 0), pts) + weeks: dict[str, int] = {} + for day, bucket in days.items(): + wk = _week_start(date.fromisoformat(day)).isoformat() + weeks[wk] = weeks.get(wk, 0) + bucket["points"] + + def best(table: dict[str, int]) -> tuple[int, str]: + if not table: + return (0, "") + day, value = max(table.items(), key=lambda kv: (kv[1], kv[0])) + return (value, day) + + tables = { + "best_day_points": {d: b["points"] for d, b in days.items()}, + "best_day_chores": {d: b["chores"] for d, b in days.items()}, + "best_week_points": weeks, + "best_single_award": single, + } + changed = False + for key, table in tables.items(): + rec = records.get(key) + if not rec or not rec.get("date"): + continue + rec_day = date.fromisoformat(rec["date"]) + if key in _WEEK_RECORDS: + in_history = rec_day + timedelta(days=6) >= oldest + else: + in_history = rec_day >= oldest + if not in_history: + continue + still = table.get(rec["date"], 0) + if still >= int(rec.get("value", 0) or 0): + continue + value, day = best(table) + if value > 0: + records[key] = {"value": value, "date": day, "set_at": ""} + else: + records.pop(key, None) + changed = True + if changed: + await self.storage.async_save() diff --git a/custom_components/taskmate/coord_chores.py b/custom_components/taskmate/coord_chores.py index a1be9ec7..8fb238f7 100644 --- a/custom_components/taskmate/coord_chores.py +++ b/custom_components/taskmate/coord_chores.py @@ -1162,6 +1162,12 @@ async def _async_after_completions(self, chore, recorded: list, *, auto_approve: photo_url=completion.photo_url, ) + # Personal bests (#1099) before the batch's one refresh, so a new + # record reaches the cards with it rather than costing a second one. + if auto_approve and hasattr(self, "async_check_personal_bests"): + for child, _completion, _points in recorded: + await self.async_check_personal_bests(child.id, refresh=False) + await self.async_refresh() if not auto_approve: @@ -1668,6 +1674,11 @@ async def async_approve_chore( self._check_one_shot_fully_disabled(chore) self.storage.update_chore(chore) + # Personal bests (#1099): did this approval beat a record? + # Checked before the refresh below so a new record rides it. + if hasattr(self, "async_check_personal_bests"): + await self.async_check_personal_bests(completion.child_id, refresh=False) + await self.storage.async_save() if refresh: await self.async_refresh() @@ -1939,6 +1950,8 @@ async def async_reject_chore( await self._async_reverse_challenges(target_completion.child_id) if target_completion and target_completion.approved and hasattr(self, "async_reverse_weekly_goals"): await self.async_reverse_weekly_goals(target_completion.child_id) + if target_completion and target_completion.approved and hasattr(self, "async_recompute_personal_bests"): + await self.async_recompute_personal_bests(target_completion.child_id) # The completion record is gone, so its evidence photo is now orphaned — # delete it (best-effort; foreign/blank URLs are ignored), unless some # other record still points at the same file. @@ -2035,6 +2048,8 @@ async def async_undo_chore_approval(self, completion_id: str) -> None: await self._async_reverse_challenges(target.child_id) if hasattr(self, "async_reverse_weekly_goals"): await self.async_reverse_weekly_goals(target.child_id) + if hasattr(self, "async_recompute_personal_bests"): + await self.async_recompute_personal_bests(target.child_id) await self.storage.async_save() await self.async_refresh() diff --git a/custom_components/taskmate/coord_fulfilment.py b/custom_components/taskmate/coord_fulfilment.py new file mode 100644 index 00000000..282e5724 --- /dev/null +++ b/custom_components/taskmate/coord_fulfilment.py @@ -0,0 +1,180 @@ +"""Reward fulfilment tracker (#1098): a redeemed reward stays owed until it is handed over. + +Approving a claim spends the points; nothing used to record whether the +child ever got the thing. With ``reward_fulfilment_enabled`` on, every claim +approved for a reward a person has to hand over is marked ``needs_delivery`` +and sits in the owed list until a parent marks it delivered (approvals card, +parent dashboard, panel Today page) or the child taps "Got it!" on their own +card. Parents are reminded after ``reward_fulfilment_reminder_days`` days, +and again every that-many days while it stays owed (0 = never). + +Rewards the system fulfils on its own — streak freezes and timed unlocks — +and wishlist redemptions, which have their own hand-over step, are never +tracked. Claims approved while the tracker was off are not back-filled when +it is turned on. +""" + +from __future__ import annotations + +import logging +from datetime import timedelta +from typing import Any + +from homeassistant.util import dt as dt_util + +from .const import ( + DEFAULT_REWARD_FULFILMENT_REMINDER_DAYS, + NOTIF_TYPE_REWARD_OWED, + REWARD_FULFILMENT_REMINDER_MAX_DAYS, +) + +_LOGGER = logging.getLogger(__name__) + + +class FulfilmentMixin: + """Owed rewards and the hand-over step.""" + + def reward_fulfilment_enabled(self) -> bool: + v = self.storage.get_setting("reward_fulfilment_enabled", False) + return v is True or str(v).lower() == "true" + + def reward_fulfilment_reminder_days(self) -> int: + raw = self.storage.get_setting("reward_fulfilment_reminder_days", DEFAULT_REWARD_FULFILMENT_REMINDER_DAYS) + try: + days = int(raw) + except (TypeError, ValueError): + return 0 + return max(0, min(REWARD_FULFILMENT_REMINDER_MAX_DAYS, days)) + + @staticmethod + def reward_needs_delivery(reward) -> bool: + """True for a reward a person has to hand over (not one TaskMate grants itself).""" + if reward is None: + return False + if getattr(reward, "streak_freeze", False): + return False + return not getattr(reward, "unlock_entity", "") + + def _track_fulfilment(self, claim, reward) -> None: + """Called by the reward approval path once the claim is approved.""" + if self.reward_fulfilment_enabled() and self.reward_needs_delivery(reward): + claim.needs_delivery = True + + def _owed_claims(self) -> list: + if not self.reward_fulfilment_enabled(): + return [] + return [ + c + for c in self.storage.get_reward_claims() + if c.approved and getattr(c, "needs_delivery", False) and c.delivered_at is None + ] + + def owed_rewards(self, child_id: str | None = None) -> list[dict[str, Any]]: + """Approved rewards still to be handed over, oldest first. + + Each entry is what a card needs to draw the row and mark it done; the + child's face comes from the overview's children list, not from here. + """ + now = dt_util.now() + out: list[dict[str, Any]] = [] + for claim in self._owed_claims(): + if child_id and claim.child_id != child_id: + continue + child = self.get_child(claim.child_id) + reward = self.get_reward(claim.reward_id) + if child is None or reward is None: + continue + since = claim.approved_at or claim.claimed_at + days = max(0, (dt_util.as_local(now).date() - dt_util.as_local(since).date()).days) + out.append( + { + "claim_id": claim.id, + "child_id": child.id, + "child_name": child.name, + "reward_id": reward.id, + "reward_name": reward.name, + "reward_icon": reward.icon or "mdi:gift", + "cost": claim.approved_cost if claim.approved_cost is not None else reward.cost, + "approved_at": since.isoformat(), + "days_owed": days, + } + ) + out.sort(key=lambda o: o["approved_at"]) + return out + + async def async_mark_reward_delivered(self, claim_id: str, by: str = "parent", child_id: str | None = None) -> None: + """Hand-over done. ``by`` is "parent" or "child"; a child may only confirm their own.""" + claim = next((c for c in self.storage.get_reward_claims() if c.id == claim_id), None) + if claim is None or not claim.approved or not getattr(claim, "needs_delivery", False): + raise ValueError("That reward is not waiting to be handed over.") + if claim.delivered_at is not None: + raise ValueError("That reward has already been marked as delivered.") + if by == "child" and child_id != claim.child_id: + child = self.get_child(claim.child_id) + name = child.name if child else "someone else" + raise ValueError(f"That reward belongs to {name}.") + now = dt_util.now() + claim.delivered_at = now + claim.delivered_by = "child" if by == "child" else "parent" + self.storage.update_reward_claim(claim) + await self.storage.async_save() + if getattr(self, "notifications", None): + await self.notifications.clear_approval(NOTIF_TYPE_REWARD_OWED, claim.id) + child = self.get_child(claim.child_id) + reward = self.get_reward(claim.reward_id) + self.hass.bus.async_fire( + "taskmate_reward_delivered", + { + "child_id": claim.child_id, + "child_name": getattr(child, "name", ""), + "reward_id": claim.reward_id, + "reward_name": getattr(reward, "name", ""), + "claim_id": claim.id, + "delivered_by": claim.delivered_by, + "timestamp": now.isoformat(), + }, + ) + await self.async_refresh() + + async def async_check_owed_rewards(self, refresh: bool = False) -> int: + """Remind the parents about rewards owed for N days or more. Idempotent per period. + + Runs from the minutely tick. Returns how many reminders went out. + """ + days = self.reward_fulfilment_reminder_days() + if not days: + return 0 + now = dt_util.now() + gap = timedelta(days=days) + sent = 0 + for claim in self._owed_claims(): + since = claim.approved_at or claim.claimed_at + if now - since < gap: + continue + last = claim.owed_reminded_at + if last is not None and now - last < gap: + continue + child = self.get_child(claim.child_id) + reward = self.get_reward(claim.reward_id) + if child is None or reward is None: + continue + claim.owed_reminded_at = now + self.storage.update_reward_claim(claim) + await self.storage.async_save() + await self.notifications.fire( + NOTIF_TYPE_REWARD_OWED, + { + "child_id": child.id, + "child_name": child.name, + "reward_id": reward.id, + "reward_name": reward.name, + "claim_id": claim.id, + "entry_id": claim.id, + "days": (dt_util.as_local(now).date() - dt_util.as_local(since).date()).days, + "points_name": self.storage.get_points_name(), + }, + ) + sent += 1 + if sent and refresh: + await self.async_refresh() + return sent diff --git a/custom_components/taskmate/coord_notifications.py b/custom_components/taskmate/coord_notifications.py index 9183750a..fddc19eb 100644 --- a/custom_components/taskmate/coord_notifications.py +++ b/custom_components/taskmate/coord_notifications.py @@ -49,10 +49,12 @@ NOTIF_TYPE_PENALTY_REDEEMED, NOTIF_TYPE_PENDING_CHORE_APPROVAL, NOTIF_TYPE_PENDING_REWARD_CLAIM, + NOTIF_TYPE_PERSONAL_BEST, NOTIF_TYPE_PET_EVOLVED, NOTIF_TYPE_PRESENCE_ARRIVAL, NOTIF_TYPE_RAFFLE_DRAWN, NOTIF_TYPE_RECAP_READY, + NOTIF_TYPE_REWARD_OWED, NOTIF_TYPE_SEASON_CHAMPION, NOTIF_TYPE_STREAK_AT_RISK, NOTIF_TYPE_STREAK_FREEZE_USED, @@ -186,6 +188,12 @@ class NotificationTypeMeta: NotificationTypeMeta(NOTIF_TYPE_PENALTY_REDEEMED, "both", False, False, False, False), NotificationTypeMeta(NOTIF_TYPE_RAFFLE_DRAWN, "both", False, False, False, False), NotificationTypeMeta(NOTIF_TYPE_PET_EVOLVED, "both", False, False, False, False), + # Reward fulfilment (#1098): on by default like inspection_reminder — it + # only ever fires once a parent has switched the tracker on, and a + # tracker that nags nobody is just a list. + NotificationTypeMeta(NOTIF_TYPE_REWARD_OWED, "parent", False, False, False, True), + # Personal bests (#1099): opt-in, like every celebration added after release. + NotificationTypeMeta(NOTIF_TYPE_PERSONAL_BEST, "both", False, False, False, False), ] NOTIFICATION_TYPES_BY_ID: dict[str, NotificationTypeMeta] = {t.id: t for t in NOTIFICATION_TYPES} @@ -196,6 +204,7 @@ class NotificationTypeMeta: _OWN_EVENT_TYPES: frozenset[str] = frozenset( { NOTIF_TYPE_AUCTION_OPENED, + NOTIF_TYPE_PERSONAL_BEST, NOTIF_TYPE_BADGE_EARNED, NOTIF_TYPE_BOUNTY_POSTED, NOTIF_TYPE_CELEBRATION, @@ -496,6 +505,8 @@ async def send_test(self, type_id: str) -> list[str]: "raffle_name": sample["raffle"], "winner_name": "Alex", "tickets": 12, + "record_label": notify_strings.text(self.hass, "record_longest_streak"), + "value": 12, } ctx["result_text"] = notify_strings.render( self.hass, diff --git a/custom_components/taskmate/coord_rewards.py b/custom_components/taskmate/coord_rewards.py index 50bb815a..8b41286b 100644 --- a/custom_components/taskmate/coord_rewards.py +++ b/custom_components/taskmate/coord_rewards.py @@ -645,6 +645,8 @@ async def async_approve_reward(self, claim_id: str) -> None: claim.approved = True claim.approved_at = dt_util.now() claim.approved_cost = effective_cost + # Fulfilment tracker (#1098): owed until somebody hands it over. + self._track_fulfilment(claim, reward) self.storage.update_reward_claim(claim) # One pool, one redemption. Claims stored before a jackpot was diff --git a/custom_components/taskmate/coordinator.py b/custom_components/taskmate/coordinator.py index 0971ddba..df21bfce 100644 --- a/custom_components/taskmate/coordinator.py +++ b/custom_components/taskmate/coordinator.py @@ -23,11 +23,13 @@ from .coord_auto_approve import AutoApproveMixin from .coord_avatars import AvatarsMixin from .coord_badges import BadgeCoordinator +from .coord_bests import PersonalBestsMixin from .coord_birthdays import BirthdaysMixin from .coord_bounties import BountiesMixin from .coord_calendar import CalendarMixin from .coord_challenges import ChallengesMixin from .coord_chores import ChoresMixin +from .coord_fulfilment import FulfilmentMixin from .coord_goals import WeeklyGoalsMixin from .coord_guests import GuestsMixin from .coord_inspections import InspectionsMixin @@ -108,6 +110,8 @@ class TaskMateCoordinator( TagsMixin, TeamworkMixin, WishlistMixin, + FulfilmentMixin, + PersonalBestsMixin, BountiesMixin, AuctionsMixin, RecapsMixin, @@ -507,6 +511,7 @@ async def async_run_minutely_checks(self) -> None: self.async_check_weekly_goals, self.async_draw_due_raffles, self.async_check_pet_growth, + self.async_check_owed_rewards, ): try: await step() diff --git a/custom_components/taskmate/event.py b/custom_components/taskmate/event.py index 8044bbc2..c4121817 100644 --- a/custom_components/taskmate/event.py +++ b/custom_components/taskmate/event.py @@ -62,6 +62,8 @@ "penalty_redeemed": notification_event_name(NOTIF_TYPE_PENALTY_REDEEMED), "raffle_won": "taskmate_raffle_won", "pet_evolved": notification_event_name(NOTIF_TYPE_PET_EVOLVED), + "reward_delivered": "taskmate_reward_delivered", + "personal_best": "taskmate_personal_best", } # Bus events about the whole family. diff --git a/custom_components/taskmate/family_services.py b/custom_components/taskmate/family_services.py index 6f56c355..fad30f0d 100644 --- a/custom_components/taskmate/family_services.py +++ b/custom_components/taskmate/family_services.py @@ -30,8 +30,10 @@ SERVICE_BUY_RAFFLE_TICKETS = "buy_raffle_tickets" SERVICE_DRAW_RAFFLE = "draw_raffle" SERVICE_ADOPT_PET = "adopt_pet" +SERVICE_MARK_REWARD_DELIVERED = "mark_reward_delivered" FAMILY_SERVICES: tuple[str, ...] = ( + SERVICE_MARK_REWARD_DELIVERED, SERVICE_REQUEST_HELP, SERVICE_ANSWER_HELP, SERVICE_LOG_MOOD, @@ -53,6 +55,7 @@ def async_register_family_services( parent_action: Wrapper, get_coordinator: Callable[[], object], require_linked_child: Callable[..., Awaitable[None]], + require_parent: Callable[..., Awaitable[None]] | None = None, ) -> None: """Register the services behind the caller's child and parent gates.""" @@ -94,6 +97,30 @@ async def adopt_pet(coordinator, call): async def draw_raffle(coordinator, call): await coordinator.async_draw_raffle(call.data["raffle_id"]) + # Reward fulfilment (#1098): a parent marks a reward handed over, or the + # child confirms their own with ``child_id`` — so the one service has two + # gates and picks by who is calling. + async def mark_reward_delivered(call: ServiceCall) -> None: + coordinator = get_coordinator() + if not coordinator: + _LOGGER.error("No TaskMate coordinator available") + return + child_id = call.data.get("child_id") + if child_id: + await require_linked_child(hass, call, coordinator, child_id) + await coordinator.async_mark_reward_delivered(call.data["claim_id"], by="child", child_id=child_id) + return + if require_parent is not None: + await require_parent(hass, call) + await coordinator.async_mark_reward_delivered(call.data["claim_id"]) + + hass.services.async_register( + DOMAIN, + SERVICE_MARK_REWARD_DELIVERED, + child_action(mark_reward_delivered), + schema=vol.Schema({vol.Required("claim_id"): cv.string, vol.Optional("child_id"): cv.string}), + ) + registrations = ( ( SERVICE_REQUEST_HELP, diff --git a/custom_components/taskmate/frontend.py b/custom_components/taskmate/frontend.py index 04411fce..d73c1edd 100644 --- a/custom_components/taskmate/frontend.py +++ b/custom_components/taskmate/frontend.py @@ -58,6 +58,7 @@ "taskmate-chore-board-card.js", "taskmate-raffle-card.js", "taskmate-pet-card.js", + "taskmate-personal-bests-card.js", ] # Cards that USED to ship but were removed. Their files no longer exist, so any diff --git a/custom_components/taskmate/models.py b/custom_components/taskmate/models.py index 2d93a3e1..aacfcce3 100644 --- a/custom_components/taskmate/models.py +++ b/custom_components/taskmate/models.py @@ -1308,6 +1308,16 @@ class RewardClaim: # Set when the claim redeems a wishlist wish (#932) rather than a reward. # reward_id then holds the wish id too, and no Reward record exists. wish_id: str = "" + # Reward fulfilment (#1098): True on a claim approved while the tracker + # was on, for a reward somebody has to hand over. It stays "owed" until + # delivered_at is set — by a parent, or by the child saying "got it". + # Claims from before the feature, or approved while it was off, keep + # False and never show up as owed. + needs_delivery: bool = False + delivered_at: datetime | None = None + delivered_by: str = "" # "parent" | "child" | "" + # When the parents were last reminded this is still owed. + owed_reminded_at: datetime | None = None @classmethod def from_dict(cls, data: dict[str, Any]) -> RewardClaim: @@ -1324,6 +1334,10 @@ def from_dict(cls, data: dict[str, Any]) -> RewardClaim: id=data.get("id") or generate_id(), approved_cost=parse_optional_points(data.get("approved_cost")), wish_id=str(data.get("wish_id", "") or ""), + needs_delivery=bool(data.get("needs_delivery", False)), + delivered_at=parse_datetime(data.get("delivered_at")), + delivered_by=str(data.get("delivered_by", "") or ""), + owed_reminded_at=parse_datetime(data.get("owed_reminded_at")), ) def to_dict(self) -> dict[str, Any]: @@ -1340,6 +1354,15 @@ def to_dict(self) -> dict[str, Any]: # Only a wishlist redemption carries it, so ordinary claims keep their shape. if self.wish_id: out["wish_id"] = self.wish_id + # Fulfilment (#1098) fields likewise only appear on tracked claims. + if self.needs_delivery: + out["needs_delivery"] = True + if self.delivered_at is not None: + out["delivered_at"] = format_datetime(self.delivered_at) + if self.delivered_by: + out["delivered_by"] = self.delivered_by + if self.owed_reminded_at is not None: + out["owed_reminded_at"] = format_datetime(self.owed_reminded_at) return out diff --git a/custom_components/taskmate/notify_strings.py b/custom_components/taskmate/notify_strings.py index d70f609f..132d8e60 100644 --- a/custom_components/taskmate/notify_strings.py +++ b/custom_components/taskmate/notify_strings.py @@ -74,6 +74,14 @@ "help_requested": "🙋 {child_name} needs help with '{chore_name}'{message_text}", "mood_low": "{emoji} {child_name} had a tough day today. Maybe check in?", "weekly_goal_reached": "🎯 {child_name} reached their weekly goal of {target} {points_name} — +{bonus} bonus!", + "reward_owed": "🎁 {child_name} is still waiting for '{reward_name}' — redeemed {days} days ago.", + "personal_best": "🏆 New personal best! {child_name}: {record_label} — {value}.", + # Personal best record names, used inside the personal_best message. + "record_longest_streak": "longest streak (days)", + "record_best_day_points": "most {points_name} in a day", + "record_best_day_chores": "most chores in a day", + "record_best_week_points": "most {points_name} in a week", + "record_best_single_award": "biggest single award", "pet_evolved": "{news}", "pet_news_stage": "🐣 {pet_name} just grew up! Have a look at {child_name}'s pet.", "pet_news_legendary": "✨ {pet_name} turned legendary! {child_name}'s pet is shining gold.", @@ -115,6 +123,7 @@ "celebrate_weekly_target": "{child_name} reached the weekly target for '{chore_name}' — +{bonus}!", "celebrate_quest_completed": "{child_name} completed the quest '{quest_name}'!", "celebrate_streak_milestone": "{child_name} hit a {days}-day streak!", + "celebrate_personal_best": "{child_name} set a new personal best: {record_label} — {value}!", # Recap announcements: the single-recap child push is recap_ready above. "recap_ready_many": "✨ {count} new recaps are ready, {child_name}! Tap to watch them.", "recap_parent_one": "✨ New {period} recaps are ready for {names}. Tap to see them.", diff --git a/custom_components/taskmate/sensor.py b/custom_components/taskmate/sensor.py index 26769599..1c1aad22 100644 --- a/custom_components/taskmate/sensor.py +++ b/custom_components/taskmate/sensor.py @@ -261,6 +261,18 @@ def _build_children_summary(coordinator: TaskMateCoordinator, common: dict) -> l **({"pet": pet} if (pet := coordinator.pet_state(c)) is not None else {}), # Personal weekly goal, while the feature is on. **({"weekly_goal": goal} if (goal := coordinator.weekly_goal_state(c.id)) is not None else {}), + # Reward fulfilment (#1098): what this child is still owed. + **( + { + "owed_rewards": [ + {k: o[k] for k in ("claim_id", "reward_name", "reward_icon", "days_owed")} for o in owed + ] + } + if (owed := coordinator.owed_rewards(c.id)) + else {} + ), + # Personal bests (#1099): the records board, once there is one. + **({"personal_bests": bests} if (bests := coordinator.personal_bests_state(c)) else {}), # Make it right: open offers to earn a penalty back. **( { @@ -1960,7 +1972,7 @@ def extra_state_attributes(self) -> dict: class PendingApprovalsSensor(TaskMateBaseSensor): """Sensor for pending approvals.""" - _unrecorded_attributes = frozenset({"chore_completions", "reward_claims", "mandatory_misses"}) + _unrecorded_attributes = frozenset({"chore_completions", "reward_claims", "mandatory_misses", "owed_rewards"}) def __init__( self, @@ -2084,6 +2096,10 @@ def extra_state_attributes(self) -> dict: "reward_claims": reward_details, "mandatory_misses": mandatory_misses, } + # Reward fulfilment (#1098): redeemed rewards still to hand over, only + # while the tracker is on and something is owed. + if owed := self.coordinator.owed_rewards(): + attrs["owed_rewards"] = owed # Quality rating (#927): tells approval cards to draw the star picker, # and what each star pays. Absent entirely while the feature is off. if self.coordinator.quality_rating_enabled(): diff --git a/custom_components/taskmate/services.yaml b/custom_components/taskmate/services.yaml index b4278091..80c7407b 100644 --- a/custom_components/taskmate/services.yaml +++ b/custom_components/taskmate/services.yaml @@ -1856,6 +1856,23 @@ set_weekly_goal: max: 100000 mode: box +mark_reward_delivered: + name: Mark reward delivered + description: A redeemed reward has been handed over, so it leaves the owed list. Give child_id when the child is confirming their own. + fields: + claim_id: + name: Claim ID + description: The approved reward claim + required: true + selector: + text: + child_id: + name: Child ID + description: The child confirming they received it (leave empty when a parent marks it) + required: false + selector: + text: + buy_raffle_tickets: name: Buy raffle tickets description: A child spends points on tickets for a raffle. Each ticket is one chance in the draw. diff --git a/custom_components/taskmate/strings.json b/custom_components/taskmate/strings.json index 1b22315f..d515d579 100644 --- a/custom_components/taskmate/strings.json +++ b/custom_components/taskmate/strings.json @@ -1448,6 +1448,20 @@ "description": "Hatch the egg the grown-up pet laid into a new pet. The old pet joins the pet family." } } + }, + "mark_reward_delivered": { + "name": "Mark reward delivered", + "description": "A redeemed reward has been handed over, so it leaves the owed list. Give child_id when the child is confirming their own.", + "fields": { + "claim_id": { + "name": "Claim ID", + "description": "The approved reward claim" + }, + "child_id": { + "name": "Child ID", + "description": "The child confirming they received it (leave empty when a parent marks it)" + } + } } }, "entity": { @@ -1516,7 +1530,9 @@ "chore_auto_approved": "Chore auto-approved", "penalty_redeemed": "Penalty earned back", "raffle_won": "Raffle won", - "pet_evolved": "Pet grew up" + "pet_evolved": "Pet grew up", + "reward_delivered": "Reward handed over", + "personal_best": "Personal best" } } } diff --git a/custom_components/taskmate/translations/da.json b/custom_components/taskmate/translations/da.json index 0a53e22b..452e2ca7 100644 --- a/custom_components/taskmate/translations/da.json +++ b/custom_components/taskmate/translations/da.json @@ -1448,6 +1448,20 @@ "description": "Udklæk ægget, som det voksne kæledyr lagde, til et nyt kæledyr. Det gamle kæledyr kommer med i kæledyrsfamilien." } } + }, + "mark_reward_delivered": { + "name": "Markér belønning som givet", + "description": "En indløst belønning er givet og forlader listen over udeståender. Angiv child_id, når barnet selv bekræfter.", + "fields": { + "claim_id": { + "name": "Krav-ID", + "description": "Det godkendte belønningskrav" + }, + "child_id": { + "name": "Barn-ID", + "description": "Barnet, der bekræfter at have fået den (lad stå tomt, når en forælder markerer)" + } + } } }, "entity": { @@ -1516,7 +1530,9 @@ "chore_auto_approved": "Opgave godkendt automatisk", "penalty_redeemed": "Straf tjent tilbage", "raffle_won": "Lodtrækning vundet", - "pet_evolved": "Kæledyr voksede" + "pet_evolved": "Kæledyr voksede", + "reward_delivered": "Belønning givet", + "personal_best": "Personlig rekord" } } } diff --git a/custom_components/taskmate/translations/de.json b/custom_components/taskmate/translations/de.json index 7da2f9af..3d2d5cf5 100644 --- a/custom_components/taskmate/translations/de.json +++ b/custom_components/taskmate/translations/de.json @@ -1448,6 +1448,20 @@ "description": "Das Ei des ausgewachsenen Haustiers zu einem neuen Haustier ausbrüten. Das alte Haustier kommt in die Haustierfamilie." } } + }, + "mark_reward_delivered": { + "name": "Belohnung als übergeben markieren", + "description": "Eine eingelöste Belohnung wurde übergeben und verlässt die Liste der ausstehenden. child_id angeben, wenn das Kind selbst bestätigt.", + "fields": { + "claim_id": { + "name": "Anspruchs-ID", + "description": "Der genehmigte Belohnungsanspruch" + }, + "child_id": { + "name": "Kind-ID", + "description": "Das Kind, das den Erhalt bestätigt (leer lassen, wenn ein Elternteil markiert)" + } + } } }, "entity": { @@ -1516,7 +1530,9 @@ "chore_auto_approved": "Aufgabe automatisch genehmigt", "penalty_redeemed": "Strafe wieder verdient", "raffle_won": "Verlosung gewonnen", - "pet_evolved": "Haustier gewachsen" + "pet_evolved": "Haustier gewachsen", + "reward_delivered": "Belohnung übergeben", + "personal_best": "Persönliche Bestleistung" } } } diff --git a/custom_components/taskmate/translations/en-GB.json b/custom_components/taskmate/translations/en-GB.json index 1b22315f..d515d579 100644 --- a/custom_components/taskmate/translations/en-GB.json +++ b/custom_components/taskmate/translations/en-GB.json @@ -1448,6 +1448,20 @@ "description": "Hatch the egg the grown-up pet laid into a new pet. The old pet joins the pet family." } } + }, + "mark_reward_delivered": { + "name": "Mark reward delivered", + "description": "A redeemed reward has been handed over, so it leaves the owed list. Give child_id when the child is confirming their own.", + "fields": { + "claim_id": { + "name": "Claim ID", + "description": "The approved reward claim" + }, + "child_id": { + "name": "Child ID", + "description": "The child confirming they received it (leave empty when a parent marks it)" + } + } } }, "entity": { @@ -1516,7 +1530,9 @@ "chore_auto_approved": "Chore auto-approved", "penalty_redeemed": "Penalty earned back", "raffle_won": "Raffle won", - "pet_evolved": "Pet grew up" + "pet_evolved": "Pet grew up", + "reward_delivered": "Reward handed over", + "personal_best": "Personal best" } } } diff --git a/custom_components/taskmate/translations/en.json b/custom_components/taskmate/translations/en.json index 1b22315f..d515d579 100644 --- a/custom_components/taskmate/translations/en.json +++ b/custom_components/taskmate/translations/en.json @@ -1448,6 +1448,20 @@ "description": "Hatch the egg the grown-up pet laid into a new pet. The old pet joins the pet family." } } + }, + "mark_reward_delivered": { + "name": "Mark reward delivered", + "description": "A redeemed reward has been handed over, so it leaves the owed list. Give child_id when the child is confirming their own.", + "fields": { + "claim_id": { + "name": "Claim ID", + "description": "The approved reward claim" + }, + "child_id": { + "name": "Child ID", + "description": "The child confirming they received it (leave empty when a parent marks it)" + } + } } }, "entity": { @@ -1516,7 +1530,9 @@ "chore_auto_approved": "Chore auto-approved", "penalty_redeemed": "Penalty earned back", "raffle_won": "Raffle won", - "pet_evolved": "Pet grew up" + "pet_evolved": "Pet grew up", + "reward_delivered": "Reward handed over", + "personal_best": "Personal best" } } } diff --git a/custom_components/taskmate/translations/fr.json b/custom_components/taskmate/translations/fr.json index e3e59ee5..9487970e 100644 --- a/custom_components/taskmate/translations/fr.json +++ b/custom_components/taskmate/translations/fr.json @@ -1448,6 +1448,20 @@ "description": "Faire éclore l’œuf pondu par l’animal adulte pour obtenir un nouvel animal. L’ancien rejoint la famille des animaux." } } + }, + "mark_reward_delivered": { + "name": "Marquer la récompense comme remise", + "description": "Une récompense échangée a été remise : elle quitte la liste des récompenses dues. Indiquez child_id quand c’est l’enfant qui confirme.", + "fields": { + "claim_id": { + "name": "ID de la demande", + "description": "La demande de récompense approuvée" + }, + "child_id": { + "name": "ID de l’enfant", + "description": "L’enfant qui confirme l’avoir reçue (laisser vide quand un parent la marque)" + } + } } }, "entity": { @@ -1516,7 +1530,9 @@ "chore_auto_approved": "Tâche approuvée automatiquement", "penalty_redeemed": "Pénalité regagnée", "raffle_won": "Tombola gagnée", - "pet_evolved": "L’animal a grandi" + "pet_evolved": "L’animal a grandi", + "reward_delivered": "Récompense remise", + "personal_best": "Record personnel" } } } diff --git a/custom_components/taskmate/translations/nb.json b/custom_components/taskmate/translations/nb.json index 6a9b398d..d5f76fd4 100644 --- a/custom_components/taskmate/translations/nb.json +++ b/custom_components/taskmate/translations/nb.json @@ -1448,6 +1448,20 @@ "description": "Klekk egget det voksne kjæledyret la, til et nytt kjæledyr. Det gamle kjæledyret blir med i kjæledyrfamilien." } } + }, + "mark_reward_delivered": { + "name": "Merk belønning som gitt", + "description": "En innløst belønning er gitt og forlater listen over utestående. Oppgi child_id når barnet selv bekrefter.", + "fields": { + "claim_id": { + "name": "Krav-ID", + "description": "Det godkjente belønningskravet" + }, + "child_id": { + "name": "Barn-ID", + "description": "Barnet som bekrefter å ha fått den (la stå tom når en forelder merker)" + } + } } }, "entity": { @@ -1516,7 +1530,9 @@ "chore_auto_approved": "Oppgave godkjent automatisk", "penalty_redeemed": "Straff tjent inn igjen", "raffle_won": "Loddtrekning vunnet", - "pet_evolved": "Kjæledyret vokste" + "pet_evolved": "Kjæledyret vokste", + "reward_delivered": "Belønning gitt", + "personal_best": "Personlig rekord" } } } diff --git a/custom_components/taskmate/translations/nl.json b/custom_components/taskmate/translations/nl.json index e5bf1141..4adf6bed 100644 --- a/custom_components/taskmate/translations/nl.json +++ b/custom_components/taskmate/translations/nl.json @@ -1448,6 +1448,20 @@ "description": "Broed het ei dat het volgroeide huisdier legde uit tot een nieuw huisdier. Het oude huisdier komt bij de huisdierenfamilie." } } + }, + "mark_reward_delivered": { + "name": "Beloning als gegeven markeren", + "description": "Een ingewisselde beloning is overhandigd en verdwijnt van de lijst met nog te geven beloningen. Geef child_id op als het kind het zelf bevestigt.", + "fields": { + "claim_id": { + "name": "Aanvraag-ID", + "description": "De goedgekeurde beloningsaanvraag" + }, + "child_id": { + "name": "Kind-ID", + "description": "Het kind dat bevestigt dat het de beloning heeft gekregen (leeg laten als een ouder het markeert)" + } + } } }, "entity": { @@ -1516,7 +1530,9 @@ "chore_auto_approved": "Taak automatisch goedgekeurd", "penalty_redeemed": "Puntenaftrek goedgemaakt", "raffle_won": "Loterij gewonnen", - "pet_evolved": "Huisdier gegroeid" + "pet_evolved": "Huisdier gegroeid", + "reward_delivered": "Beloning overhandigd", + "personal_best": "Persoonlijk record" } } } diff --git a/custom_components/taskmate/translations/nn.json b/custom_components/taskmate/translations/nn.json index e43e7ca9..5b0b2be7 100644 --- a/custom_components/taskmate/translations/nn.json +++ b/custom_components/taskmate/translations/nn.json @@ -1448,6 +1448,20 @@ "description": "Klekk egget det vaksne kjæledyret la, til eit nytt kjæledyr. Det gamle kjæledyret blir med i kjæledyrfamilien." } } + }, + "mark_reward_delivered": { + "name": "Merk løning som gjeven", + "description": "Ei innløyst løning er gjeven og forlèt lista over uteståande. Oppgje child_id når barnet sjølv stadfestar.", + "fields": { + "claim_id": { + "name": "Krav-ID", + "description": "Det godkjende løningskravet" + }, + "child_id": { + "name": "Barn-ID", + "description": "Barnet som stadfestar å ha fått henne (la stå tom når ein forelder merkjer)" + } + } } }, "entity": { @@ -1516,7 +1530,9 @@ "chore_auto_approved": "Oppgåve godkjend automatisk", "penalty_redeemed": "Straff tent inn att", "raffle_won": "Loddtrekning vunnen", - "pet_evolved": "Kjæledyret voks" + "pet_evolved": "Kjæledyret voks", + "reward_delivered": "Løning gjeven", + "personal_best": "Personleg rekord" } } } diff --git a/custom_components/taskmate/translations/pl.json b/custom_components/taskmate/translations/pl.json index 677a21c4..cb911a4c 100644 --- a/custom_components/taskmate/translations/pl.json +++ b/custom_components/taskmate/translations/pl.json @@ -1448,6 +1448,20 @@ "description": "Wykluj z jajka dorosłego zwierzaka nowego zwierzaka. Stary dołącza do rodziny zwierzaków." } } + }, + "mark_reward_delivered": { + "name": "Oznacz nagrodę jako wręczoną", + "description": "Odebrana nagroda została wręczona i znika z listy należnych. Podaj child_id, gdy potwierdza samo dziecko.", + "fields": { + "claim_id": { + "name": "ID zgłoszenia", + "description": "Zatwierdzone zgłoszenie nagrody" + }, + "child_id": { + "name": "ID dziecka", + "description": "Dziecko potwierdzające odbiór (zostaw puste, gdy oznacza rodzic)" + } + } } }, "entity": { @@ -1516,7 +1530,9 @@ "chore_auto_approved": "Zadanie zatwierdzone automatycznie", "penalty_redeemed": "Odrobiono karę", "raffle_won": "Wygrano losowanie", - "pet_evolved": "Zwierzak urósł" + "pet_evolved": "Zwierzak urósł", + "reward_delivered": "Nagroda wręczona", + "personal_best": "Rekord życiowy" } } } diff --git a/custom_components/taskmate/translations/pt-BR.json b/custom_components/taskmate/translations/pt-BR.json index 90baefa2..d8e317f2 100644 --- a/custom_components/taskmate/translations/pt-BR.json +++ b/custom_components/taskmate/translations/pt-BR.json @@ -1448,6 +1448,20 @@ "description": "Chocar o ovo que o bichinho adulto botou para ter um bichinho novo. O antigo entra para a família de bichinhos." } } + }, + "mark_reward_delivered": { + "name": "Marcar recompensa como entregue", + "description": "Uma recompensa resgatada foi entregue e sai da lista de pendentes. Informe child_id quando for a criança confirmando.", + "fields": { + "claim_id": { + "name": "ID do pedido", + "description": "O pedido de recompensa aprovado" + }, + "child_id": { + "name": "ID da criança", + "description": "A criança que confirma ter recebido (deixe vazio quando um dos pais marca)" + } + } } }, "entity": { @@ -1516,7 +1530,9 @@ "chore_auto_approved": "Tarefa aprovada automaticamente", "penalty_redeemed": "Penalidade recuperada", "raffle_won": "Sorteio ganho", - "pet_evolved": "O bichinho cresceu" + "pet_evolved": "O bichinho cresceu", + "reward_delivered": "Recompensa entregue", + "personal_best": "Recorde pessoal" } } } diff --git a/custom_components/taskmate/translations/pt.json b/custom_components/taskmate/translations/pt.json index ac5072d8..33e70701 100644 --- a/custom_components/taskmate/translations/pt.json +++ b/custom_components/taskmate/translations/pt.json @@ -1448,6 +1448,20 @@ "description": "Chocar o ovo posto pelo animal adulto para ter um animal novo. O antigo junta-se à família de animais." } } + }, + "mark_reward_delivered": { + "name": "Marcar recompensa como entregue", + "description": "Uma recompensa resgatada foi entregue e sai da lista de pendentes. Indica child_id quando é a criança a confirmar.", + "fields": { + "claim_id": { + "name": "ID do pedido", + "description": "O pedido de recompensa aprovado" + }, + "child_id": { + "name": "ID da criança", + "description": "A criança que confirma ter recebido (deixar vazio quando é um dos pais a marcar)" + } + } } }, "entity": { @@ -1516,7 +1530,9 @@ "chore_auto_approved": "Tarefa aprovada automaticamente", "penalty_redeemed": "Penalização recuperada", "raffle_won": "Sorteio ganho", - "pet_evolved": "O animal cresceu" + "pet_evolved": "O animal cresceu", + "reward_delivered": "Recompensa entregue", + "personal_best": "Recorde pessoal" } } } diff --git a/custom_components/taskmate/websocket.py b/custom_components/taskmate/websocket.py index 770a53df..19b89425 100644 --- a/custom_components/taskmate/websocket.py +++ b/custom_components/taskmate/websocket.py @@ -78,6 +78,7 @@ MAX_RECURRENCE_INTERVAL_DAYS, MAX_TIME_PERIODS, MIN_RECURRENCE_INTERVAL_DAYS, + REWARD_FULFILMENT_REMINDER_MAX_DAYS, SCHEDULE_MODES, TAG_ID_MAX_LENGTH, TEAM_POINTS_MODES, @@ -494,6 +495,8 @@ def _build_state_snapshot(coordinator: TaskMateCoordinator) -> dict[str, Any]: "inspection_bed_rule": coordinator.inspection_bed_rule(), # Today page: open help requests, who is off sick and today's moods. "help_requests": coordinator.open_help_requests(), + # Reward fulfilment (#1098): redeemed rewards still to be handed over. + "owed_rewards": coordinator.owed_rewards(), "sick_today": [c.id for c in coordinator.storage.get_children() if coordinator.is_sick_day(c.id)], "moods_today": {c.id: m for c in coordinator.storage.get_children() if (m := coordinator.mood_on(c.id))}, "raffles": coordinator.all_raffles(), @@ -1985,6 +1988,8 @@ async def _ws_remove_custom_sound(hass, connection, msg, coordinator): "auto_approve_hours", "weekly_goal_enabled", "weekly_goal_bonus", + "reward_fulfilment_enabled", + "reward_fulfilment_reminder_days", "pets_enabled", *PET_INT_SETTINGS, *PET_BOOL_SETTINGS, @@ -2277,6 +2282,10 @@ def _validate_chore_undo_seconds(value: Any) -> int: }, **{vol.Optional(key): bool for key in PET_BOOL_SETTINGS}, vol.Optional("weekly_goal_bonus"): vol.All(vol.Coerce(int), vol.Range(min=0, max=100000)), + vol.Optional("reward_fulfilment_enabled"): bool, + vol.Optional("reward_fulfilment_reminder_days"): vol.All( + vol.Coerce(int), vol.Range(min=0, max=REWARD_FULFILMENT_REMINDER_MAX_DAYS) + ), vol.Optional("recap_frequencies"): _validate_recap_frequencies, vol.Optional("recap_child_frequencies"): _validate_recap_child_frequencies, vol.Optional("recap_notify_children"): bool, diff --git a/custom_components/taskmate/www/locales/da.json b/custom_components/taskmate/www/locales/da.json index 6f5bdae9..1beedc15 100644 --- a/custom_components/taskmate/www/locales/da.json +++ b/custom_components/taskmate/www/locales/da.json @@ -3425,5 +3425,60 @@ "panel.pet_level_every": "Nyt niveau hver", "panel.pet_reset_defaults": "Nulstil tal til standard", "panel.pet_settings_invalid": "Kæledyrstrinene skal stige i rækkefølge, og legendarisk og ægget skal komme efter fuldvoksen.", - "panel.pet_about_one_chore": "Cirka 1 opgave (opgaver til 5 point)" + "panel.pet_about_one_chore": "Cirka 1 opgave (opgaver til 5 point)", + "notify.reward_owed": "🎁 {child_name} venter stadig på '{reward_name}' — indløst for {days} dage siden.", + "notify.personal_best": "🏆 Ny personlig rekord! {child_name}: {record_label} — {value}.", + "notify.record_longest_streak": "længste stime (dage)", + "notify.record_best_day_points": "flest {points_name} på en dag", + "notify.record_best_day_chores": "flest opgaver på en dag", + "notify.record_best_week_points": "flest {points_name} på en uge", + "notify.record_best_single_award": "største enkelte belønning", + "notify.celebrate_personal_best": "{child_name} satte ny personlig rekord: {record_label} — {value}!", + "notification.reward_owed.name": "Belønning mangler stadig", + "notification.reward_owed.description": "Minder forældre om, at en indløst belønning endnu ikke er givet. Sendes kun, mens belønningsopfølgning er slået til.", + "notification.personal_best.name": "Personlig rekord", + "notification.personal_best.description": "Et barn slog en af sine egne rekorder: længste stime, bedste dag, bedste uge eller største belønning.", + "approvals.owed_section": "Mangler at blive givet", + "approvals.owed_delivered": "Givet", + "approvals.owed_today": "indløst i dag", + "approvals.owed_days_one": "udestået i 1 dag", + "approvals.owed_days": "udestået i {count} dage", + "panel.today_owed_title": "Belønninger der skal gives", + "panel.today_owed_delivered": "Givet", + "panel.today_owed_marked": "Markeret som givet", + "panel.today_owed_today": "indløst i dag", + "panel.today_owed_days_one": "udestået i 1 dag", + "panel.today_owed_days": "udestået i {count} dage", + "panel.fulfilment_title": "Belønningsopfølgning", + "panel.fulfilment_hint": "Når en belønning godkendes, bruges pointene; her bliver den stående på en liste over udeståender, indtil nogen giver den. Barnet kan trykke „Fået!“ på sit kort.", + "panel.fulfilment_enabled": "Følg belønninger, indtil de er givet", + "panel.fulfilment_reminder_days": "Mind mig om det efter", + "panel.fulfilment_reminder_days_hint": "Dage før en besked siger, at en belønning stadig mangler, gentaget hver så mange dage. 0 = aldrig.", + "child.owed_title": "Du har en belønning til gode", + "child.owed_got_it": "Fået!", + "child.owed_today": "indløst i dag", + "child.owed_days_one": "for 1 dag siden", + "child.owed_days": "for {count} dage siden", + "child.record_title": "Ny personlig rekord!", + "child.record_longest_streak": "Længste stime", + "child.record_best_day_points": "Flest {points_name} på en dag", + "child.record_best_day_chores": "Flest opgaver på en dag", + "child.record_best_week_points": "Flest {points_name} på en uge", + "child.record_best_single_award": "Største enkelte belønning", + "child.editor.show_personal_bests": "Vis nye personlige rekorder", + "bests.default_title": "Personlige rekorder", + "bests.error.entity_required": "Angiv en enhed", + "bests.subtitle": "Dine egne rekorder at slå", + "bests.none_yet": "Ingen rekorder endnu — de første opgaver sætter niveauet", + "bests.not_yet": "ikke endnu", + "bests.new_tag": "Ny", + "bests.new_count": "{count} nye i dag", + "bests.beat_previous": "slog {previous}", + "bests.editor.title": "Titel", + "bests.editor.child_helper": "Vis kun ét barns rekorder", + "bests.record_longest_streak": "Længste stime", + "bests.record_best_day_points": "Bedste dag ({points_name})", + "bests.record_best_day_chores": "Flest opgaver på en dag", + "bests.record_best_week_points": "Bedste uge ({points_name})", + "bests.record_best_single_award": "Største belønning" } diff --git a/custom_components/taskmate/www/locales/de.json b/custom_components/taskmate/www/locales/de.json index 6679a767..e697ec13 100644 --- a/custom_components/taskmate/www/locales/de.json +++ b/custom_components/taskmate/www/locales/de.json @@ -3425,5 +3425,60 @@ "panel.pet_level_every": "Neues Level alle", "panel.pet_reset_defaults": "Zahlen auf Standard zurücksetzen", "panel.pet_settings_invalid": "Die Stufen müssen der Reihe nach steigen, und legendär und das Ei müssen nach »ausgewachsen« kommen.", - "panel.pet_about_one_chore": "Etwa 1 Aufgabe (Aufgaben mit 5 Punkten)" + "panel.pet_about_one_chore": "Etwa 1 Aufgabe (Aufgaben mit 5 Punkten)", + "notify.reward_owed": "🎁 {child_name} wartet noch auf „{reward_name}“ – vor {days} Tagen eingelöst.", + "notify.personal_best": "🏆 Neue persönliche Bestleistung! {child_name}: {record_label} – {value}.", + "notify.record_longest_streak": "längste Serie (Tage)", + "notify.record_best_day_points": "meiste {points_name} an einem Tag", + "notify.record_best_day_chores": "meiste Aufgaben an einem Tag", + "notify.record_best_week_points": "meiste {points_name} in einer Woche", + "notify.record_best_single_award": "größte Einzelbelohnung", + "notify.celebrate_personal_best": "{child_name} hat eine neue Bestleistung: {record_label} – {value}!", + "notification.reward_owed.name": "Belohnung noch ausstehend", + "notification.reward_owed.description": "Erinnert Eltern daran, dass eine eingelöste Belohnung noch nicht übergeben wurde. Nur aktiv, wenn die Belohnungsübergabe eingeschaltet ist.", + "notification.personal_best.name": "Persönliche Bestleistung", + "notification.personal_best.description": "Ein Kind hat einen eigenen Rekord gebrochen: längste Serie, bester Tag, beste Woche oder größte Belohnung.", + "approvals.owed_section": "Noch zu übergeben", + "approvals.owed_delivered": "Übergeben", + "approvals.owed_today": "heute eingelöst", + "approvals.owed_days_one": "seit 1 Tag ausstehend", + "approvals.owed_days": "seit {count} Tagen ausstehend", + "panel.today_owed_title": "Belohnungen zum Übergeben", + "panel.today_owed_delivered": "Übergeben", + "panel.today_owed_marked": "Als übergeben markiert", + "panel.today_owed_today": "heute eingelöst", + "panel.today_owed_days_one": "seit 1 Tag ausstehend", + "panel.today_owed_days": "seit {count} Tagen ausstehend", + "panel.fulfilment_title": "Belohnungsübergabe", + "panel.fulfilment_hint": "Das Genehmigen einer Belohnung zieht die Punkte ab; hier bleibt sie auf einer „Ausstehend“-Liste, bis jemand sie übergibt. Das Kind kann auf seiner Karte „Hab ich!“ antippen.", + "panel.fulfilment_enabled": "Belohnungen bis zur Übergabe verfolgen", + "panel.fulfilment_reminder_days": "Erinnern nach", + "panel.fulfilment_reminder_days_hint": "Tage, bis eine Push-Nachricht an die ausstehende Belohnung erinnert, danach alle so viele Tage wieder. 0 = nie.", + "child.owed_title": "Dir steht eine Belohnung zu", + "child.owed_got_it": "Hab ich!", + "child.owed_today": "heute eingelöst", + "child.owed_days_one": "vor 1 Tag", + "child.owed_days": "vor {count} Tagen", + "child.record_title": "Neue persönliche Bestleistung!", + "child.record_longest_streak": "Längste Serie", + "child.record_best_day_points": "Meiste {points_name} an einem Tag", + "child.record_best_day_chores": "Meiste Aufgaben an einem Tag", + "child.record_best_week_points": "Meiste {points_name} in einer Woche", + "child.record_best_single_award": "Größte Einzelbelohnung", + "child.editor.show_personal_bests": "Neue Bestleistungen anzeigen", + "bests.default_title": "Bestleistungen", + "bests.error.entity_required": "Bitte eine Entität angeben", + "bests.subtitle": "Deine eigenen Rekorde zum Knacken", + "bests.none_yet": "Noch keine Rekorde – die ersten Aufgaben setzen die Messlatte", + "bests.not_yet": "noch nicht", + "bests.new_tag": "Neu", + "bests.new_count": "{count} neu heute", + "bests.beat_previous": "vorher {previous}", + "bests.editor.title": "Titel", + "bests.editor.child_helper": "Nur die Rekorde eines Kindes anzeigen", + "bests.record_longest_streak": "Längste Serie", + "bests.record_best_day_points": "Bester Tag ({points_name})", + "bests.record_best_day_chores": "Meiste Aufgaben an einem Tag", + "bests.record_best_week_points": "Beste Woche ({points_name})", + "bests.record_best_single_award": "Größte Belohnung" } diff --git a/custom_components/taskmate/www/locales/en-GB.json b/custom_components/taskmate/www/locales/en-GB.json index ba20f279..b8475062 100644 --- a/custom_components/taskmate/www/locales/en-GB.json +++ b/custom_components/taskmate/www/locales/en-GB.json @@ -3425,5 +3425,60 @@ "panel.pet_level_every": "New level every", "panel.pet_reset_defaults": "Reset numbers to defaults", "panel.pet_settings_invalid": "Pet stages must go up in order, and legendary and the egg must come after fully grown.", - "panel.pet_about_one_chore": "About 1 chore (5-point chores)" + "panel.pet_about_one_chore": "About 1 chore (5-point chores)", + "notify.reward_owed": "🎁 {child_name} is still waiting for '{reward_name}' — redeemed {days} days ago.", + "notify.personal_best": "🏆 New personal best! {child_name}: {record_label} — {value}.", + "notify.record_longest_streak": "longest streak (days)", + "notify.record_best_day_points": "most {points_name} in a day", + "notify.record_best_day_chores": "most chores in a day", + "notify.record_best_week_points": "most {points_name} in a week", + "notify.record_best_single_award": "biggest single award", + "notify.celebrate_personal_best": "{child_name} set a new personal best: {record_label} — {value}!", + "notification.reward_owed.name": "Reward still owed", + "notification.reward_owed.description": "Reminds parents that a redeemed reward has not been handed over yet. Only fires while reward fulfilment is on.", + "notification.personal_best.name": "Personal best", + "notification.personal_best.description": "A child beat one of their own records: longest streak, best day, best week or biggest award.", + "approvals.owed_section": "Still to hand over", + "approvals.owed_delivered": "Handed over", + "approvals.owed_today": "redeemed today", + "approvals.owed_days_one": "owed for 1 day", + "approvals.owed_days": "owed for {count} days", + "panel.today_owed_title": "Rewards to hand over", + "panel.today_owed_delivered": "Handed over", + "panel.today_owed_marked": "Marked as handed over", + "panel.today_owed_today": "redeemed today", + "panel.today_owed_days_one": "owed for 1 day", + "panel.today_owed_days": "owed for {count} days", + "panel.fulfilment_title": "Reward fulfilment", + "panel.fulfilment_hint": "Approving a reward spends the points; this keeps it on an \"owed\" list until somebody hands it over. The child can tap \"Got it!\" on their card.", + "panel.fulfilment_enabled": "Track rewards until they are handed over", + "panel.fulfilment_reminder_days": "Remind me after", + "panel.fulfilment_reminder_days_hint": "Days before a push says a reward is still owed, repeated every that many days. 0 = never.", + "child.owed_title": "You're owed a reward", + "child.owed_got_it": "Got it!", + "child.owed_today": "redeemed today", + "child.owed_days_one": "1 day ago", + "child.owed_days": "{count} days ago", + "child.record_title": "New personal best!", + "child.record_longest_streak": "Longest streak", + "child.record_best_day_points": "Most {points_name} in a day", + "child.record_best_day_chores": "Most chores in a day", + "child.record_best_week_points": "Most {points_name} in a week", + "child.record_best_single_award": "Biggest single award", + "child.editor.show_personal_bests": "Show new personal bests", + "bests.default_title": "Personal Bests", + "bests.error.entity_required": "Please define an entity", + "bests.subtitle": "Your own records to beat", + "bests.none_yet": "No records yet — the first chores set the bar", + "bests.not_yet": "not yet", + "bests.new_tag": "New", + "bests.new_count": "{count} new today", + "bests.beat_previous": "beat {previous}", + "bests.editor.title": "Title", + "bests.editor.child_helper": "Show one child's records only", + "bests.record_longest_streak": "Longest streak", + "bests.record_best_day_points": "Best day ({points_name})", + "bests.record_best_day_chores": "Most chores in a day", + "bests.record_best_week_points": "Best week ({points_name})", + "bests.record_best_single_award": "Biggest award" } diff --git a/custom_components/taskmate/www/locales/en.json b/custom_components/taskmate/www/locales/en.json index ba20f279..b8475062 100644 --- a/custom_components/taskmate/www/locales/en.json +++ b/custom_components/taskmate/www/locales/en.json @@ -3425,5 +3425,60 @@ "panel.pet_level_every": "New level every", "panel.pet_reset_defaults": "Reset numbers to defaults", "panel.pet_settings_invalid": "Pet stages must go up in order, and legendary and the egg must come after fully grown.", - "panel.pet_about_one_chore": "About 1 chore (5-point chores)" + "panel.pet_about_one_chore": "About 1 chore (5-point chores)", + "notify.reward_owed": "🎁 {child_name} is still waiting for '{reward_name}' — redeemed {days} days ago.", + "notify.personal_best": "🏆 New personal best! {child_name}: {record_label} — {value}.", + "notify.record_longest_streak": "longest streak (days)", + "notify.record_best_day_points": "most {points_name} in a day", + "notify.record_best_day_chores": "most chores in a day", + "notify.record_best_week_points": "most {points_name} in a week", + "notify.record_best_single_award": "biggest single award", + "notify.celebrate_personal_best": "{child_name} set a new personal best: {record_label} — {value}!", + "notification.reward_owed.name": "Reward still owed", + "notification.reward_owed.description": "Reminds parents that a redeemed reward has not been handed over yet. Only fires while reward fulfilment is on.", + "notification.personal_best.name": "Personal best", + "notification.personal_best.description": "A child beat one of their own records: longest streak, best day, best week or biggest award.", + "approvals.owed_section": "Still to hand over", + "approvals.owed_delivered": "Handed over", + "approvals.owed_today": "redeemed today", + "approvals.owed_days_one": "owed for 1 day", + "approvals.owed_days": "owed for {count} days", + "panel.today_owed_title": "Rewards to hand over", + "panel.today_owed_delivered": "Handed over", + "panel.today_owed_marked": "Marked as handed over", + "panel.today_owed_today": "redeemed today", + "panel.today_owed_days_one": "owed for 1 day", + "panel.today_owed_days": "owed for {count} days", + "panel.fulfilment_title": "Reward fulfilment", + "panel.fulfilment_hint": "Approving a reward spends the points; this keeps it on an \"owed\" list until somebody hands it over. The child can tap \"Got it!\" on their card.", + "panel.fulfilment_enabled": "Track rewards until they are handed over", + "panel.fulfilment_reminder_days": "Remind me after", + "panel.fulfilment_reminder_days_hint": "Days before a push says a reward is still owed, repeated every that many days. 0 = never.", + "child.owed_title": "You're owed a reward", + "child.owed_got_it": "Got it!", + "child.owed_today": "redeemed today", + "child.owed_days_one": "1 day ago", + "child.owed_days": "{count} days ago", + "child.record_title": "New personal best!", + "child.record_longest_streak": "Longest streak", + "child.record_best_day_points": "Most {points_name} in a day", + "child.record_best_day_chores": "Most chores in a day", + "child.record_best_week_points": "Most {points_name} in a week", + "child.record_best_single_award": "Biggest single award", + "child.editor.show_personal_bests": "Show new personal bests", + "bests.default_title": "Personal Bests", + "bests.error.entity_required": "Please define an entity", + "bests.subtitle": "Your own records to beat", + "bests.none_yet": "No records yet — the first chores set the bar", + "bests.not_yet": "not yet", + "bests.new_tag": "New", + "bests.new_count": "{count} new today", + "bests.beat_previous": "beat {previous}", + "bests.editor.title": "Title", + "bests.editor.child_helper": "Show one child's records only", + "bests.record_longest_streak": "Longest streak", + "bests.record_best_day_points": "Best day ({points_name})", + "bests.record_best_day_chores": "Most chores in a day", + "bests.record_best_week_points": "Best week ({points_name})", + "bests.record_best_single_award": "Biggest award" } diff --git a/custom_components/taskmate/www/locales/fr.json b/custom_components/taskmate/www/locales/fr.json index 8b11847a..c9a112ed 100644 --- a/custom_components/taskmate/www/locales/fr.json +++ b/custom_components/taskmate/www/locales/fr.json @@ -3425,5 +3425,60 @@ "panel.pet_level_every": "Nouveau niveau tous les", "panel.pet_reset_defaults": "Rétablir les nombres par défaut", "panel.pet_settings_invalid": "Les étapes doivent augmenter dans l’ordre, et légendaire et l’œuf doivent venir après adulte.", - "panel.pet_about_one_chore": "Environ 1 tâche (tâches à 5 points)" + "panel.pet_about_one_chore": "Environ 1 tâche (tâches à 5 points)", + "notify.reward_owed": "🎁 {child_name} attend toujours « {reward_name} » — échangée il y a {days} jours.", + "notify.personal_best": "🏆 Nouveau record personnel ! {child_name} : {record_label} — {value}.", + "notify.record_longest_streak": "plus longue série (jours)", + "notify.record_best_day_points": "le plus de {points_name} en une journée", + "notify.record_best_day_chores": "le plus de tâches en une journée", + "notify.record_best_week_points": "le plus de {points_name} en une semaine", + "notify.record_best_single_award": "plus gros gain d’un coup", + "notify.celebrate_personal_best": "{child_name} a battu un record personnel : {record_label} — {value} !", + "notification.reward_owed.name": "Récompense toujours due", + "notification.reward_owed.description": "Rappelle aux parents qu’une récompense échangée n’a pas encore été remise. Actif seulement quand le suivi des récompenses est activé.", + "notification.personal_best.name": "Record personnel", + "notification.personal_best.description": "Un enfant a battu l’un de ses propres records : plus longue série, meilleure journée, meilleure semaine ou plus gros gain.", + "approvals.owed_section": "À remettre", + "approvals.owed_delivered": "Remise", + "approvals.owed_today": "échangée aujourd’hui", + "approvals.owed_days_one": "due depuis 1 jour", + "approvals.owed_days": "due depuis {count} jours", + "panel.today_owed_title": "Récompenses à remettre", + "panel.today_owed_delivered": "Remise", + "panel.today_owed_marked": "Marquée comme remise", + "panel.today_owed_today": "échangée aujourd’hui", + "panel.today_owed_days_one": "due depuis 1 jour", + "panel.today_owed_days": "due depuis {count} jours", + "panel.fulfilment_title": "Remise des récompenses", + "panel.fulfilment_hint": "Approuver une récompense dépense les points ; ici elle reste sur une liste « à remettre » jusqu’à ce que quelqu’un la donne. L’enfant peut appuyer sur « Reçue ! » sur sa carte.", + "panel.fulfilment_enabled": "Suivre les récompenses jusqu’à leur remise", + "panel.fulfilment_reminder_days": "Me rappeler après", + "panel.fulfilment_reminder_days_hint": "Jours avant qu’une notification rappelle qu’une récompense est due, puis tous les autant de jours. 0 = jamais.", + "child.owed_title": "On te doit une récompense", + "child.owed_got_it": "Reçue !", + "child.owed_today": "échangée aujourd’hui", + "child.owed_days_one": "il y a 1 jour", + "child.owed_days": "il y a {count} jours", + "child.record_title": "Nouveau record personnel !", + "child.record_longest_streak": "Plus longue série", + "child.record_best_day_points": "Le plus de {points_name} en une journée", + "child.record_best_day_chores": "Le plus de tâches en une journée", + "child.record_best_week_points": "Le plus de {points_name} en une semaine", + "child.record_best_single_award": "Plus gros gain d’un coup", + "child.editor.show_personal_bests": "Afficher les nouveaux records personnels", + "bests.default_title": "Records personnels", + "bests.error.entity_required": "Veuillez définir une entité", + "bests.subtitle": "Tes propres records à battre", + "bests.none_yet": "Pas encore de record — les premières tâches fixent la barre", + "bests.not_yet": "pas encore", + "bests.new_tag": "Nouveau", + "bests.new_count": "{count} nouveau(x) aujourd’hui", + "bests.beat_previous": "avant : {previous}", + "bests.editor.title": "Titre", + "bests.editor.child_helper": "N’afficher que les records d’un enfant", + "bests.record_longest_streak": "Plus longue série", + "bests.record_best_day_points": "Meilleure journée ({points_name})", + "bests.record_best_day_chores": "Le plus de tâches en un jour", + "bests.record_best_week_points": "Meilleure semaine ({points_name})", + "bests.record_best_single_award": "Plus gros gain" } diff --git a/custom_components/taskmate/www/locales/nb.json b/custom_components/taskmate/www/locales/nb.json index 19ee294b..ffaa5570 100644 --- a/custom_components/taskmate/www/locales/nb.json +++ b/custom_components/taskmate/www/locales/nb.json @@ -3425,5 +3425,60 @@ "panel.pet_level_every": "Nytt nivå hver", "panel.pet_reset_defaults": "Tilbakestill tall til standard", "panel.pet_settings_invalid": "Stadiene må stige i rekkefølge, og legendarisk og egget må komme etter fullvoksen.", - "panel.pet_about_one_chore": "Omtrent 1 oppgave (oppgaver på 5 poeng)" + "panel.pet_about_one_chore": "Omtrent 1 oppgave (oppgaver på 5 poeng)", + "notify.reward_owed": "🎁 {child_name} venter fortsatt på «{reward_name}» – innløst for {days} dager siden.", + "notify.personal_best": "🏆 Ny personlig rekord! {child_name}: {record_label} – {value}.", + "notify.record_longest_streak": "lengste rekke (dager)", + "notify.record_best_day_points": "flest {points_name} på én dag", + "notify.record_best_day_chores": "flest oppgaver på én dag", + "notify.record_best_week_points": "flest {points_name} på én uke", + "notify.record_best_single_award": "største enkeltbelønning", + "notify.celebrate_personal_best": "{child_name} satte ny personlig rekord: {record_label} – {value}!", + "notification.reward_owed.name": "Belønning mangler fortsatt", + "notification.reward_owed.description": "Minner foreldre på at en innløst belønning ikke er gitt enda. Sendes bare når belønningsoppfølging er på.", + "notification.personal_best.name": "Personlig rekord", + "notification.personal_best.description": "Et barn slo en av sine egne rekorder: lengste rekke, beste dag, beste uke eller største belønning.", + "approvals.owed_section": "Gjenstår å gi", + "approvals.owed_delivered": "Gitt", + "approvals.owed_today": "innløst i dag", + "approvals.owed_days_one": "utestående i 1 dag", + "approvals.owed_days": "utestående i {count} dager", + "panel.today_owed_title": "Belønninger som skal gis", + "panel.today_owed_delivered": "Gitt", + "panel.today_owed_marked": "Merket som gitt", + "panel.today_owed_today": "innløst i dag", + "panel.today_owed_days_one": "utestående i 1 dag", + "panel.today_owed_days": "utestående i {count} dager", + "panel.fulfilment_title": "Belønningsoppfølging", + "panel.fulfilment_hint": "Når en belønning godkjennes, brukes poengene; her blir den stående på en liste over utestående til noen gir den. Barnet kan trykke «Fått!» på kortet sitt.", + "panel.fulfilment_enabled": "Følg belønninger til de er gitt", + "panel.fulfilment_reminder_days": "Minn meg på det etter", + "panel.fulfilment_reminder_days_hint": "Dager før et varsel sier at en belønning fortsatt mangler, gjentatt hver så mange dager. 0 = aldri.", + "child.owed_title": "Du har en belønning til gode", + "child.owed_got_it": "Fått!", + "child.owed_today": "innløst i dag", + "child.owed_days_one": "for 1 dag siden", + "child.owed_days": "for {count} dager siden", + "child.record_title": "Ny personlig rekord!", + "child.record_longest_streak": "Lengste rekke", + "child.record_best_day_points": "Flest {points_name} på én dag", + "child.record_best_day_chores": "Flest oppgaver på én dag", + "child.record_best_week_points": "Flest {points_name} på én uke", + "child.record_best_single_award": "Største enkeltbelønning", + "child.editor.show_personal_bests": "Vis nye personlige rekorder", + "bests.default_title": "Personlige rekorder", + "bests.error.entity_required": "Angi en enhet", + "bests.subtitle": "Dine egne rekorder å slå", + "bests.none_yet": "Ingen rekorder enda – de første oppgavene setter nivået", + "bests.not_yet": "ikke enda", + "bests.new_tag": "Ny", + "bests.new_count": "{count} nye i dag", + "bests.beat_previous": "slo {previous}", + "bests.editor.title": "Tittel", + "bests.editor.child_helper": "Vis bare ett barns rekorder", + "bests.record_longest_streak": "Lengste rekke", + "bests.record_best_day_points": "Beste dag ({points_name})", + "bests.record_best_day_chores": "Flest oppgaver på én dag", + "bests.record_best_week_points": "Beste uke ({points_name})", + "bests.record_best_single_award": "Største belønning" } diff --git a/custom_components/taskmate/www/locales/nl.json b/custom_components/taskmate/www/locales/nl.json index 54737ce9..dd188c36 100644 --- a/custom_components/taskmate/www/locales/nl.json +++ b/custom_components/taskmate/www/locales/nl.json @@ -3425,5 +3425,60 @@ "panel.pet_level_every": "Nieuw level om de", "panel.pet_reset_defaults": "Standaardgetallen terugzetten", "panel.pet_settings_invalid": "De groei per fase moet steeds hoger worden, en legendarisch en het ei komen pas na volgroeid.", - "panel.pet_about_one_chore": "Ongeveer 1 taak (taken van 5 punten)" + "panel.pet_about_one_chore": "Ongeveer 1 taak (taken van 5 punten)", + "notify.reward_owed": "🎁 {child_name} wacht nog steeds op '{reward_name}' — {days} dagen geleden ingewisseld.", + "notify.personal_best": "🏆 Nieuw persoonlijk record! {child_name}: {record_label} — {value}.", + "notify.record_longest_streak": "langste reeks (dagen)", + "notify.record_best_day_points": "meeste {points_name} op één dag", + "notify.record_best_day_chores": "meeste taken op één dag", + "notify.record_best_week_points": "meeste {points_name} in één week", + "notify.record_best_single_award": "grootste beloning in één keer", + "notify.celebrate_personal_best": "{child_name} heeft een nieuw persoonlijk record: {record_label} — {value}!", + "notification.reward_owed.name": "Beloning nog te geven", + "notification.reward_owed.description": "Herinnert ouders eraan dat een ingewisselde beloning nog niet is overhandigd. Werkt alleen als het bijhouden van beloningen aanstaat.", + "notification.personal_best.name": "Persoonlijk record", + "notification.personal_best.description": "Een kind heeft een eigen record verbroken: langste reeks, beste dag, beste week of grootste beloning.", + "approvals.owed_section": "Nog te overhandigen", + "approvals.owed_delivered": "Overhandigd", + "approvals.owed_today": "vandaag ingewisseld", + "approvals.owed_days_one": "1 dag tegoed", + "approvals.owed_days": "{count} dagen tegoed", + "panel.today_owed_title": "Beloningen om te overhandigen", + "panel.today_owed_delivered": "Overhandigd", + "panel.today_owed_marked": "Gemarkeerd als overhandigd", + "panel.today_owed_today": "vandaag ingewisseld", + "panel.today_owed_days_one": "1 dag tegoed", + "panel.today_owed_days": "{count} dagen tegoed", + "panel.fulfilment_title": "Beloningen overhandigen", + "panel.fulfilment_hint": "Bij het goedkeuren van een beloning worden de punten uitgegeven; hiermee blijft de beloning op een lijst met nog te geven beloningen staan tot iemand hem overhandigt. Het kind kan op zijn kaart op \"Gekregen!\" tikken.", + "panel.fulfilment_enabled": "Beloningen bijhouden tot ze zijn overhandigd", + "panel.fulfilment_reminder_days": "Herinner me na", + "panel.fulfilment_reminder_days_hint": "Aantal dagen voordat een melding laat weten dat een beloning nog gegeven moet worden; elke zoveel dagen herhaald. 0 = nooit.", + "child.owed_title": "Je krijgt nog een beloning", + "child.owed_got_it": "Gekregen!", + "child.owed_today": "vandaag ingewisseld", + "child.owed_days_one": "1 dag geleden", + "child.owed_days": "{count} dagen geleden", + "child.record_title": "Nieuw persoonlijk record!", + "child.record_longest_streak": "Langste reeks", + "child.record_best_day_points": "Meeste {points_name} op één dag", + "child.record_best_day_chores": "Meeste taken op één dag", + "child.record_best_week_points": "Meeste {points_name} in één week", + "child.record_best_single_award": "Grootste beloning in één keer", + "child.editor.show_personal_bests": "Nieuwe persoonlijke records tonen", + "bests.default_title": "Persoonlijke records", + "bests.error.entity_required": "Kies eerst een entiteit", + "bests.subtitle": "Je eigen records om te verbreken", + "bests.none_yet": "Nog geen records — de eerste taken leggen de lat", + "bests.not_yet": "nog niet", + "bests.new_tag": "Nieuw", + "bests.new_count": "{count} nieuw vandaag", + "bests.beat_previous": "vorig record: {previous}", + "bests.editor.title": "Titel", + "bests.editor.child_helper": "Alleen de records van één kind tonen", + "bests.record_longest_streak": "Langste reeks", + "bests.record_best_day_points": "Beste dag ({points_name})", + "bests.record_best_day_chores": "Meeste taken op één dag", + "bests.record_best_week_points": "Beste week ({points_name})", + "bests.record_best_single_award": "Grootste beloning" } diff --git a/custom_components/taskmate/www/locales/nn.json b/custom_components/taskmate/www/locales/nn.json index 412667dd..c24a3958 100644 --- a/custom_components/taskmate/www/locales/nn.json +++ b/custom_components/taskmate/www/locales/nn.json @@ -3425,5 +3425,60 @@ "panel.pet_level_every": "Nytt nivå kvar", "panel.pet_reset_defaults": "Tilbakestill tal til standard", "panel.pet_settings_invalid": "Stadia må stige i rekkjefølgje, og legendarisk og egget må kome etter fullvaksen.", - "panel.pet_about_one_chore": "Om lag 1 oppgåve (oppgåver på 5 poeng)" + "panel.pet_about_one_chore": "Om lag 1 oppgåve (oppgåver på 5 poeng)", + "notify.reward_owed": "🎁 {child_name} ventar framleis på «{reward_name}» – innløyst for {days} dagar sidan.", + "notify.personal_best": "🏆 Ny personleg rekord! {child_name}: {record_label} – {value}.", + "notify.record_longest_streak": "lengste rekkje (dagar)", + "notify.record_best_day_points": "flest {points_name} på éin dag", + "notify.record_best_day_chores": "flest oppgåver på éin dag", + "notify.record_best_week_points": "flest {points_name} på éi veke", + "notify.record_best_single_award": "største enkeltløning", + "notify.celebrate_personal_best": "{child_name} sette ny personleg rekord: {record_label} – {value}!", + "notification.reward_owed.name": "Løning manglar framleis", + "notification.reward_owed.description": "Minner foreldre på at ei innløyst løning ikkje er gjeven enno. Blir berre sendt når løningsoppfølging er på.", + "notification.personal_best.name": "Personleg rekord", + "notification.personal_best.description": "Eit barn slo ein av sine eigne rekordar: lengste rekkje, beste dag, beste veke eller største løning.", + "approvals.owed_section": "Står att å gje", + "approvals.owed_delivered": "Gjeven", + "approvals.owed_today": "innløyst i dag", + "approvals.owed_days_one": "uteståande i 1 dag", + "approvals.owed_days": "uteståande i {count} dagar", + "panel.today_owed_title": "Løningar som skal gjevast", + "panel.today_owed_delivered": "Gjeven", + "panel.today_owed_marked": "Merkt som gjeven", + "panel.today_owed_today": "innløyst i dag", + "panel.today_owed_days_one": "uteståande i 1 dag", + "panel.today_owed_days": "uteståande i {count} dagar", + "panel.fulfilment_title": "Løningsoppfølging", + "panel.fulfilment_hint": "Når ei løning blir godkjend, blir poenga brukte; her blir ho ståande på ei liste over uteståande til nokon gjev henne. Barnet kan trykkje «Fått!» på kortet sitt.", + "panel.fulfilment_enabled": "Følg løningar til dei er gjevne", + "panel.fulfilment_reminder_days": "Minn meg på det etter", + "panel.fulfilment_reminder_days_hint": "Dagar før eit varsel seier at ei løning framleis manglar, gjenteke kvar så mange dagar. 0 = aldri.", + "child.owed_title": "Du har ei løning til gode", + "child.owed_got_it": "Fått!", + "child.owed_today": "innløyst i dag", + "child.owed_days_one": "for 1 dag sidan", + "child.owed_days": "for {count} dagar sidan", + "child.record_title": "Ny personleg rekord!", + "child.record_longest_streak": "Lengste rekkje", + "child.record_best_day_points": "Flest {points_name} på éin dag", + "child.record_best_day_chores": "Flest oppgåver på éin dag", + "child.record_best_week_points": "Flest {points_name} på éi veke", + "child.record_best_single_award": "Største enkeltløning", + "child.editor.show_personal_bests": "Vis nye personlege rekordar", + "bests.default_title": "Personlege rekordar", + "bests.error.entity_required": "Oppgje ein entitet", + "bests.subtitle": "Dine eigne rekordar å slå", + "bests.none_yet": "Ingen rekordar enno – dei første oppgåvene set nivået", + "bests.not_yet": "ikkje enno", + "bests.new_tag": "Ny", + "bests.new_count": "{count} nye i dag", + "bests.beat_previous": "slo {previous}", + "bests.editor.title": "Tittel", + "bests.editor.child_helper": "Vis berre rekordane til eitt barn", + "bests.record_longest_streak": "Lengste rekkje", + "bests.record_best_day_points": "Beste dag ({points_name})", + "bests.record_best_day_chores": "Flest oppgåver på éin dag", + "bests.record_best_week_points": "Beste veke ({points_name})", + "bests.record_best_single_award": "Største løning" } diff --git a/custom_components/taskmate/www/locales/pl.json b/custom_components/taskmate/www/locales/pl.json index 67cde070..6769ef48 100644 --- a/custom_components/taskmate/www/locales/pl.json +++ b/custom_components/taskmate/www/locales/pl.json @@ -3425,5 +3425,60 @@ "panel.pet_level_every": "Nowy poziom co", "panel.pet_reset_defaults": "Przywróć domyślne liczby", "panel.pet_settings_invalid": "Etapy muszą rosnąć po kolei, a legendarność i jajko muszą być po dorośnięciu.", - "panel.pet_about_one_chore": "Około 1 zadania (zadania za 5 punktów)" + "panel.pet_about_one_chore": "Około 1 zadania (zadania za 5 punktów)", + "notify.reward_owed": "🎁 {child_name} nadal czeka na „{reward_name}” — odebrano {days} dni temu.", + "notify.personal_best": "🏆 Nowy rekord życiowy! {child_name}: {record_label} — {value}.", + "notify.record_longest_streak": "najdłuższa seria (dni)", + "notify.record_best_day_points": "najwięcej {points_name} w jeden dzień", + "notify.record_best_day_chores": "najwięcej obowiązków w jeden dzień", + "notify.record_best_week_points": "najwięcej {points_name} w jeden tydzień", + "notify.record_best_single_award": "największa pojedyncza nagroda", + "notify.celebrate_personal_best": "{child_name} ma nowy rekord życiowy: {record_label} — {value}!", + "notification.reward_owed.name": "Nagroda wciąż należna", + "notification.reward_owed.description": "Przypomina rodzicom, że odebrana nagroda nie została jeszcze wręczona. Działa tylko, gdy włączone jest śledzenie wręczania nagród.", + "notification.personal_best.name": "Rekord życiowy", + "notification.personal_best.description": "Dziecko pobiło jeden ze swoich rekordów: najdłuższą serię, najlepszy dzień, najlepszy tydzień lub największą nagrodę.", + "approvals.owed_section": "Do wręczenia", + "approvals.owed_delivered": "Wręczono", + "approvals.owed_today": "odebrano dzisiaj", + "approvals.owed_days_one": "należna od 1 dnia", + "approvals.owed_days": "należna od {count} dni", + "panel.today_owed_title": "Nagrody do wręczenia", + "panel.today_owed_delivered": "Wręczono", + "panel.today_owed_marked": "Oznaczono jako wręczoną", + "panel.today_owed_today": "odebrano dzisiaj", + "panel.today_owed_days_one": "należna od 1 dnia", + "panel.today_owed_days": "należna od {count} dni", + "panel.fulfilment_title": "Wręczanie nagród", + "panel.fulfilment_hint": "Zatwierdzenie nagrody zużywa punkty; tutaj nagroda zostaje na liście „należne”, aż ktoś ją wręczy. Dziecko może nacisnąć „Mam!” na swojej karcie.", + "panel.fulfilment_enabled": "Śledź nagrody do momentu wręczenia", + "panel.fulfilment_reminder_days": "Przypomnij mi po", + "panel.fulfilment_reminder_days_hint": "Liczba dni, po których powiadomienie przypomni o należnej nagrodzie, powtarzane co tyle dni. 0 = nigdy.", + "child.owed_title": "Należy ci się nagroda", + "child.owed_got_it": "Mam!", + "child.owed_today": "odebrano dzisiaj", + "child.owed_days_one": "1 dzień temu", + "child.owed_days": "{count} dni temu", + "child.record_title": "Nowy rekord życiowy!", + "child.record_longest_streak": "Najdłuższa seria", + "child.record_best_day_points": "Najwięcej {points_name} w jeden dzień", + "child.record_best_day_chores": "Najwięcej obowiązków w jeden dzień", + "child.record_best_week_points": "Najwięcej {points_name} w jeden tydzień", + "child.record_best_single_award": "Największa pojedyncza nagroda", + "child.editor.show_personal_bests": "Pokaż nowe rekordy życiowe", + "bests.default_title": "Rekordy życiowe", + "bests.error.entity_required": "Podaj encję", + "bests.subtitle": "Twoje własne rekordy do pobicia", + "bests.none_yet": "Jeszcze brak rekordów — pierwsze obowiązki ustawią poprzeczkę", + "bests.not_yet": "jeszcze nie", + "bests.new_tag": "Nowy", + "bests.new_count": "{count} nowe dzisiaj", + "bests.beat_previous": "poprzednio {previous}", + "bests.editor.title": "Tytuł", + "bests.editor.child_helper": "Pokaż rekordy tylko jednego dziecka", + "bests.record_longest_streak": "Najdłuższa seria", + "bests.record_best_day_points": "Najlepszy dzień ({points_name})", + "bests.record_best_day_chores": "Najwięcej obowiązków w jeden dzień", + "bests.record_best_week_points": "Najlepszy tydzień ({points_name})", + "bests.record_best_single_award": "Największa nagroda" } diff --git a/custom_components/taskmate/www/locales/pt-BR.json b/custom_components/taskmate/www/locales/pt-BR.json index e94a4764..075a31fd 100644 --- a/custom_components/taskmate/www/locales/pt-BR.json +++ b/custom_components/taskmate/www/locales/pt-BR.json @@ -3425,5 +3425,60 @@ "panel.pet_level_every": "Novo nível a cada", "panel.pet_reset_defaults": "Restaurar os números padrão", "panel.pet_settings_invalid": "As fases precisam subir em ordem, e o lendário e o ovo precisam vir depois de adulto.", - "panel.pet_about_one_chore": "Cerca de 1 tarefa (tarefas de 5 pontos)" + "panel.pet_about_one_chore": "Cerca de 1 tarefa (tarefas de 5 pontos)", + "notify.reward_owed": "🎁 {child_name} ainda está esperando por '{reward_name}' — resgatada há {days} dias.", + "notify.personal_best": "🏆 Novo recorde pessoal! {child_name}: {record_label} — {value}.", + "notify.record_longest_streak": "sequência mais longa (dias)", + "notify.record_best_day_points": "mais {points_name} em um dia", + "notify.record_best_day_chores": "mais tarefas em um dia", + "notify.record_best_week_points": "mais {points_name} em uma semana", + "notify.record_best_single_award": "maior prêmio de uma vez", + "notify.celebrate_personal_best": "{child_name} bateu um recorde pessoal: {record_label} — {value}!", + "notification.reward_owed.name": "Recompensa ainda não entregue", + "notification.reward_owed.description": "Lembra os pais de que uma recompensa resgatada ainda não foi entregue. Só é enviada enquanto a entrega de recompensas estiver ligada.", + "notification.personal_best.name": "Recorde pessoal", + "notification.personal_best.description": "Uma criança bateu um dos seus próprios recordes: sequência mais longa, melhor dia, melhor semana ou maior prêmio.", + "approvals.owed_section": "Para entregar", + "approvals.owed_delivered": "Entregue", + "approvals.owed_today": "resgatada hoje", + "approvals.owed_days_one": "pendente há 1 dia", + "approvals.owed_days": "pendente há {count} dias", + "panel.today_owed_title": "Recompensas para entregar", + "panel.today_owed_delivered": "Entregue", + "panel.today_owed_marked": "Marcada como entregue", + "panel.today_owed_today": "resgatada hoje", + "panel.today_owed_days_one": "pendente há 1 dia", + "panel.today_owed_days": "pendente há {count} dias", + "panel.fulfilment_title": "Entrega de recompensas", + "panel.fulfilment_hint": "Aprovar uma recompensa gasta os pontos; aqui ela fica numa lista de \"pendentes\" até alguém entregá-la. A criança pode tocar em \"Recebi!\" no seu cartão.", + "panel.fulfilment_enabled": "Acompanhar as recompensas até serem entregues", + "panel.fulfilment_reminder_days": "Me lembrar após", + "panel.fulfilment_reminder_days_hint": "Dias até uma notificação avisar que uma recompensa continua pendente, repetida a cada tantos dias. 0 = nunca.", + "child.owed_title": "Você tem uma recompensa para receber", + "child.owed_got_it": "Recebi!", + "child.owed_today": "resgatada hoje", + "child.owed_days_one": "há 1 dia", + "child.owed_days": "há {count} dias", + "child.record_title": "Novo recorde pessoal!", + "child.record_longest_streak": "Sequência mais longa", + "child.record_best_day_points": "Mais {points_name} em um dia", + "child.record_best_day_chores": "Mais tarefas em um dia", + "child.record_best_week_points": "Mais {points_name} em uma semana", + "child.record_best_single_award": "Maior prêmio de uma vez", + "child.editor.show_personal_bests": "Mostrar novos recordes pessoais", + "bests.default_title": "Recordes pessoais", + "bests.error.entity_required": "Defina uma entidade", + "bests.subtitle": "Seus próprios recordes para bater", + "bests.none_yet": "Ainda sem recordes — as primeiras tarefas definem a meta", + "bests.not_yet": "ainda não", + "bests.new_tag": "Novo", + "bests.new_count": "{count} novos hoje", + "bests.beat_previous": "antes: {previous}", + "bests.editor.title": "Título", + "bests.editor.child_helper": "Mostrar apenas os recordes de uma criança", + "bests.record_longest_streak": "Sequência mais longa", + "bests.record_best_day_points": "Melhor dia ({points_name})", + "bests.record_best_day_chores": "Mais tarefas em um dia", + "bests.record_best_week_points": "Melhor semana ({points_name})", + "bests.record_best_single_award": "Maior prêmio" } diff --git a/custom_components/taskmate/www/locales/pt.json b/custom_components/taskmate/www/locales/pt.json index 717bcbd5..cb648ed6 100644 --- a/custom_components/taskmate/www/locales/pt.json +++ b/custom_components/taskmate/www/locales/pt.json @@ -3425,5 +3425,60 @@ "panel.pet_level_every": "Novo nível a cada", "panel.pet_reset_defaults": "Repor os números predefinidos", "panel.pet_settings_invalid": "As fases têm de subir por ordem, e o lendário e o ovo têm de vir depois de adulto.", - "panel.pet_about_one_chore": "Cerca de 1 tarefa (tarefas de 5 pontos)" + "panel.pet_about_one_chore": "Cerca de 1 tarefa (tarefas de 5 pontos)", + "notify.reward_owed": "🎁 {child_name} ainda está à espera de '{reward_name}' — resgatada há {days} dias.", + "notify.personal_best": "🏆 Novo recorde pessoal! {child_name}: {record_label} — {value}.", + "notify.record_longest_streak": "sequência mais longa (dias)", + "notify.record_best_day_points": "mais {points_name} num dia", + "notify.record_best_day_chores": "mais tarefas num dia", + "notify.record_best_week_points": "mais {points_name} numa semana", + "notify.record_best_single_award": "maior prémio de uma vez", + "notify.celebrate_personal_best": "{child_name} bateu um recorde pessoal: {record_label} — {value}!", + "notification.reward_owed.name": "Recompensa ainda por entregar", + "notification.reward_owed.description": "Lembra os pais de que uma recompensa resgatada ainda não foi entregue. Só é enviada enquanto a entrega de recompensas estiver ativa.", + "notification.personal_best.name": "Recorde pessoal", + "notification.personal_best.description": "Uma criança bateu um dos seus próprios recordes: sequência mais longa, melhor dia, melhor semana ou maior prémio.", + "approvals.owed_section": "Por entregar", + "approvals.owed_delivered": "Entregue", + "approvals.owed_today": "resgatada hoje", + "approvals.owed_days_one": "por entregar há 1 dia", + "approvals.owed_days": "por entregar há {count} dias", + "panel.today_owed_title": "Recompensas por entregar", + "panel.today_owed_delivered": "Entregue", + "panel.today_owed_marked": "Marcada como entregue", + "panel.today_owed_today": "resgatada hoje", + "panel.today_owed_days_one": "por entregar há 1 dia", + "panel.today_owed_days": "por entregar há {count} dias", + "panel.fulfilment_title": "Entrega de recompensas", + "panel.fulfilment_hint": "Aprovar uma recompensa gasta os pontos; aqui ela fica numa lista de \"por entregar\" até alguém a dar. A criança pode tocar em \"Recebi!\" no seu cartão.", + "panel.fulfilment_enabled": "Acompanhar as recompensas até serem entregues", + "panel.fulfilment_reminder_days": "Lembrar-me após", + "panel.fulfilment_reminder_days_hint": "Dias até uma notificação avisar que uma recompensa continua por entregar, repetida a cada tantos dias. 0 = nunca.", + "child.owed_title": "Tens uma recompensa por receber", + "child.owed_got_it": "Recebi!", + "child.owed_today": "resgatada hoje", + "child.owed_days_one": "há 1 dia", + "child.owed_days": "há {count} dias", + "child.record_title": "Novo recorde pessoal!", + "child.record_longest_streak": "Sequência mais longa", + "child.record_best_day_points": "Mais {points_name} num dia", + "child.record_best_day_chores": "Mais tarefas num dia", + "child.record_best_week_points": "Mais {points_name} numa semana", + "child.record_best_single_award": "Maior prémio de uma vez", + "child.editor.show_personal_bests": "Mostrar novos recordes pessoais", + "bests.default_title": "Recordes pessoais", + "bests.error.entity_required": "Define uma entidade", + "bests.subtitle": "Os teus próprios recordes a bater", + "bests.none_yet": "Ainda sem recordes — as primeiras tarefas definem a meta", + "bests.not_yet": "ainda não", + "bests.new_tag": "Novo", + "bests.new_count": "{count} novos hoje", + "bests.beat_previous": "antes: {previous}", + "bests.editor.title": "Título", + "bests.editor.child_helper": "Mostrar apenas os recordes de uma criança", + "bests.record_longest_streak": "Sequência mais longa", + "bests.record_best_day_points": "Melhor dia ({points_name})", + "bests.record_best_day_chores": "Mais tarefas num dia", + "bests.record_best_week_points": "Melhor semana ({points_name})", + "bests.record_best_single_award": "Maior prémio" } diff --git a/custom_components/taskmate/www/taskmate-approvals-card.js b/custom_components/taskmate/www/taskmate-approvals-card.js index 4ed56935..8ed8a01a 100644 --- a/custom_components/taskmate/www/taskmate-approvals-card.js +++ b/custom_components/taskmate/www/taskmate-approvals-card.js @@ -536,6 +536,10 @@ class TaskMateApprovalsCard extends LitElement { color: white; } + .approval-item.owed { border-inline-start: 3px solid #8e44ad; } + .approval-item.owed .owed-since { font-size: 0.8rem; color: var(--secondary-text-color); } + .action-button.delivered { width: auto; border-radius: 999px; padding: 0 12px; gap: 6px; } + .action-button.delivered .delivered-label { font-size: 0.8rem; font-weight: 600; } .approval-item.mandatory { border-inline-start: 5px solid var(--fun-red, #e74c3c); } @@ -770,6 +774,9 @@ class TaskMateApprovalsCard extends LitElement { // Missed mandatory chores awaiting parent review (#532) const misses = this._filterMissesByChild(attrs.mandatory_misses || []); + // Reward fulfilment (#1098): redeemed rewards still to hand over. Not an + // approval, so it never counts in the header badge. + const owed = this._owedRewards(entity, attrs); const totalPending = filteredCompletions.length + filteredClaims.length + misses.length; @@ -793,12 +800,13 @@ class TaskMateApprovalsCard extends LitElement {