251 files
This commit is contained in:
@@ -545,6 +545,16 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
chore_id = call.data[ATTR_CHORE_ID]
|
||||
child_id = call.data[ATTR_CHILD_ID]
|
||||
as_parent = call.data.get(ATTR_AS_PARENT, False)
|
||||
completed_date = call.data.get("completed_date")
|
||||
if completed_date is not None:
|
||||
# Logging a job for an earlier day is a grown-up's correction, never
|
||||
# a child's: it is always parent-gated and always approved.
|
||||
await _async_require_parent(hass, call)
|
||||
try:
|
||||
await coordinator.async_complete_chore_on_date(chore_id, child_id, completed_date)
|
||||
except ValueError as err:
|
||||
raise ServiceValidationError(str(err)) from err
|
||||
return
|
||||
if as_parent:
|
||||
# Completing on behalf of a child (auto-approve + instant award) is a
|
||||
# parent privilege. Enforce it on the backend, not just by hiding the
|
||||
@@ -1302,6 +1312,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
vol.Required(ATTR_CHILD_ID): cv.string,
|
||||
vol.Optional(ATTR_AS_PARENT, default=False): cv.boolean,
|
||||
vol.Optional("photo_url"): cv.string,
|
||||
vol.Optional("completed_date"): cv.date,
|
||||
vol.Optional("note"): cv.string,
|
||||
vol.Optional("suggested_points"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=CHORE_SUGGESTED_POINTS_MAX)
|
||||
|
||||
@@ -53,6 +53,14 @@ class AssignmentsMixin:
|
||||
visibility.add(chore.weather_entity)
|
||||
self._tracked_visibility_entities = visibility
|
||||
|
||||
# A child's photo is read from their picture entity, so a photo changed
|
||||
# there has to reach the sensors without waiting for a TaskMate edit.
|
||||
# Trimmed the same way child_avatar_image reads it, so a stray space in
|
||||
# a stored id can't leave the photo untracked.
|
||||
self._tracked_picture_entities = {
|
||||
entity for c in self.storage.get_children() if (entity := (getattr(c, "picture_entity", "") or "").strip())
|
||||
}
|
||||
|
||||
@callback
|
||||
def _availability_state_changed(self, event: Any) -> None:
|
||||
"""Cheap bus-filter: react only when a tracked entity changes.
|
||||
@@ -66,6 +74,9 @@ class AssignmentsMixin:
|
||||
if not entity_id:
|
||||
return
|
||||
|
||||
if entity_id in getattr(self, "_tracked_picture_entities", set()):
|
||||
self._picture_entity_changed(data)
|
||||
|
||||
is_availability = entity_id in getattr(self, "_tracked_availability_entities", set())
|
||||
is_visibility = entity_id in getattr(self, "_tracked_visibility_entities", set())
|
||||
if not (is_availability or is_visibility):
|
||||
@@ -79,6 +90,22 @@ class AssignmentsMixin:
|
||||
if is_availability:
|
||||
self.hass.async_create_task(self._async_reevaluate_availability())
|
||||
|
||||
def _picture_entity_changed(self, data: dict) -> None:
|
||||
"""Push a child's new photo out as soon as the entity's picture changes.
|
||||
|
||||
A person's state flips between home and away all day without touching
|
||||
its picture, so only a real change of ``entity_picture`` (or the entity
|
||||
appearing or vanishing) is worth invalidating the sensors for.
|
||||
"""
|
||||
|
||||
def picture(state: Any) -> str:
|
||||
return str((getattr(state, "attributes", None) or {}).get("entity_picture") or "") if state else ""
|
||||
|
||||
if picture(data.get("old_state")) == picture(data.get("new_state")):
|
||||
return
|
||||
self.external_state_version += 1
|
||||
self.async_update_listeners()
|
||||
|
||||
async def _async_reevaluate_availability(self) -> None:
|
||||
"""Re-run assignment for require_availability chores when availability flips.
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Shipped defaults so the feature is useful out of the box. Parents can replace
|
||||
@@ -36,6 +40,31 @@ class AvatarsMixin:
|
||||
return [a for a in raw if isinstance(a, dict) and a.get("icon")]
|
||||
return list(DEFAULT_AVATAR_CATALOG)
|
||||
|
||||
def child_avatar_image(self, child) -> str:
|
||||
"""The picture of the entity chosen for a child's face, or ``""``.
|
||||
|
||||
Read from that entity on every update rather than stored, so a photo
|
||||
changed in HA appears here with nothing to re-save, and a deleted
|
||||
entity simply falls back to the MDI avatar instead of a broken image.
|
||||
|
||||
Empty when no entity is chosen, it no longer exists, or it has no
|
||||
picture — every call site then shows the MDI avatar, so a child is
|
||||
never left with a blank face.
|
||||
|
||||
TaskMate's own child sensors are refused: they show this very photo,
|
||||
so choosing one would make the face its own source and leave it stale.
|
||||
"""
|
||||
entity_id = (getattr(child, "picture_entity", "") or "").strip()
|
||||
if not entity_id:
|
||||
return ""
|
||||
entry = er.async_get(self.hass).async_get(entity_id)
|
||||
if entry is not None and entry.platform == DOMAIN:
|
||||
return ""
|
||||
state = self.hass.states.get(entity_id)
|
||||
if state is None:
|
||||
return ""
|
||||
return str(state.attributes.get("entity_picture") or "")
|
||||
|
||||
def _avatar_unlocked(self, entry: dict, child) -> bool:
|
||||
kind = entry.get("unlock_type", "free")
|
||||
try:
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from calendar import monthrange
|
||||
from datetime import date, datetime, timedelta
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -20,7 +20,7 @@ from .const import (
|
||||
)
|
||||
from .coord_rejections import clean_reject_reason
|
||||
from .coord_teamwork import teamwork_config_error
|
||||
from .models import Chore, ChoreCompletion, PointsTransaction
|
||||
from .models import Chore, ChoreCompletion, PointsTransaction, parse_datetime
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
@@ -929,7 +929,10 @@ class ChoresMixin:
|
||||
return await self._async_join_team_chore(chore, child, now, as_parent=as_parent, photo_url=photo_url)
|
||||
# Open-ended completions always go to a parent: the child suggested the
|
||||
# point value, so self-awarding it would let them set their own pay.
|
||||
auto_approve = as_parent or (not chore.requires_approval and not requires_photo and not is_open_ended)
|
||||
# A grown-up's own completion never waits for approval (#1061).
|
||||
auto_approve = (
|
||||
as_parent or child.is_adult or (not chore.requires_approval and not requires_photo and not is_open_ended)
|
||||
)
|
||||
# Won at auction (#982): the winner is paid their winning bid for this
|
||||
# occurrence instead of the chore's own points — exactly what they bid.
|
||||
auction_price = self.auction_price_for(chore, child_id, today)
|
||||
@@ -996,12 +999,18 @@ class ChoresMixin:
|
||||
photo_url: str = "",
|
||||
note: str = "",
|
||||
suggested_points: int = 0,
|
||||
backdated: bool = False,
|
||||
) -> ChoreCompletion:
|
||||
"""Store one completion and, when auto-approved, pay it.
|
||||
|
||||
The part of completing a chore that is per child. The caller has already
|
||||
run every eligibility gate and then calls _async_after_completions once
|
||||
for the whole batch (one child normally, a whole team for #928).
|
||||
|
||||
``backdated`` is a parent logging a job for an earlier day: ``now`` is
|
||||
then that day's timestamp. It is paid for that day (weekend and birthday
|
||||
bonuses follow the day) but leaves the streak alone, and only moves the
|
||||
recurrence window forward, never back.
|
||||
"""
|
||||
completion = ChoreCompletion(
|
||||
chore_id=chore.id,
|
||||
@@ -1017,6 +1026,7 @@ class ChoresMixin:
|
||||
# window (#918); one a parent made on their behalf may not, and
|
||||
# nothing made while the window is 0 (off) ever can.
|
||||
child_undo_allowed=not as_parent and undo_window_seconds(self.storage) > 0,
|
||||
backdated=backdated,
|
||||
)
|
||||
|
||||
# Record the completion before awarding anything. Awarding can suspend
|
||||
@@ -1027,7 +1037,16 @@ class ChoresMixin:
|
||||
self.storage.add_completion(completion)
|
||||
|
||||
if auto_approve:
|
||||
total_awarded = await self._award_points(child, points, chore_id=chore.id)
|
||||
if backdated:
|
||||
total_awarded = await self._award_points(
|
||||
child,
|
||||
points,
|
||||
completion_date=dt_util.as_local(now).date(),
|
||||
skip_streak=True,
|
||||
chore_id=chore.id,
|
||||
)
|
||||
else:
|
||||
total_awarded = await self._award_points(child, points, chore_id=chore.id)
|
||||
completion.approved = True
|
||||
completion.approved_at = dt_util.now()
|
||||
completion.points_awarded = total_awarded
|
||||
@@ -1043,12 +1062,17 @@ class ChoresMixin:
|
||||
"chore_name": chore.name,
|
||||
"points": points,
|
||||
"difficulty": getattr(chore, "difficulty", "medium"),
|
||||
"timestamp": dt_util.now().isoformat(),
|
||||
"timestamp": now.isoformat() if backdated else dt_util.now().isoformat(),
|
||||
**({"backdated": True} if backdated else {}),
|
||||
},
|
||||
)
|
||||
|
||||
# Update last_completed store (window starts at completion time, midnight-rounded)
|
||||
self.storage.set_last_completed(chore.id, child.id, now.isoformat())
|
||||
# Update last_completed store (window starts at completion time, midnight-rounded).
|
||||
# A job logged for an earlier day must not pull the window back behind a
|
||||
# completion the child made since, so it only records when it is the latest.
|
||||
current = parse_datetime((self.storage.get_last_completed(chore.id, child.id) or {}).get("current"))
|
||||
if not backdated or current is None or now > current:
|
||||
self.storage.set_last_completed(chore.id, child.id, now.isoformat())
|
||||
|
||||
# One-shot: if auto-approved, disable for this child immediately
|
||||
if getattr(chore, "schedule_mode", "specific_days") == "one_shot" and auto_approve:
|
||||
@@ -1097,6 +1121,175 @@ class ChoresMixin:
|
||||
if hasattr(self, "_async_evaluate_challenges"):
|
||||
await self._async_evaluate_challenges(child.id)
|
||||
|
||||
# How far back a grown-up may log a job that was done but not ticked.
|
||||
BACKDATE_MAX_DAYS = 7
|
||||
|
||||
async def async_complete_chore_on_date(self, chore_id: str, child_id: str, day: date) -> ChoreCompletion:
|
||||
"""Log a job a child did on an earlier day but nobody ticked at the time.
|
||||
|
||||
Parent-only (the service gates it). The completion is stamped midday
|
||||
on ``day`` so it lands in that day's (and that week's) totals, and it
|
||||
is approved straight away — the grown-up logging it is the approver. It
|
||||
is recorded and followed up by the same code a parent ticking a job
|
||||
for a child uses, so badges, quest steps, challenges, the recurrence
|
||||
window and one-shot disabling all behave as they do for any approval.
|
||||
|
||||
It must be a day in the last ``BACKDATE_MAX_DAYS`` days, not today
|
||||
(today is ticked the normal way), for a chore that was assigned to the
|
||||
child and due that day, with that day's daily limit — and, for a
|
||||
weekly-target chore, that week's quota — not yet used up.
|
||||
|
||||
Refused, because they can't be reconstructed or priced after the fact:
|
||||
rotation and teamwork chores (whose turn it was), a day won at auction,
|
||||
and open-ended chores (they need the child's note and the parent's price).
|
||||
|
||||
Points are the chore's base points (with its difficulty) plus the
|
||||
weekend multiplier when ``day`` was a Saturday or Sunday; the time-of-day,
|
||||
speed and roulette adjustments are left out, as there was no live tap.
|
||||
The streak is left alone: rewriting ``last_completion_date`` to a past
|
||||
day would break a streak the child is on today.
|
||||
|
||||
Every refusal raises ValueError with a message meant for the parent.
|
||||
"""
|
||||
chore = self.get_chore(chore_id)
|
||||
if not chore:
|
||||
raise ValueError(f"Chore {chore_id} not found")
|
||||
child = self.get_child(child_id)
|
||||
if not child:
|
||||
raise ValueError(f"Child {child_id} not found")
|
||||
|
||||
today = dt_util.as_local(dt_util.now()).date()
|
||||
if not (today - timedelta(days=self.BACKDATE_MAX_DAYS) <= day < today):
|
||||
raise ValueError(f"Pick a day in the last {self.BACKDATE_MAX_DAYS} days, before today.")
|
||||
|
||||
refusal = self._backdate_refusal(chore, child, day)
|
||||
if refusal:
|
||||
raise ValueError(refusal)
|
||||
|
||||
points = self.effective_chore_points(chore)
|
||||
completed_at = datetime.combine(day, time(12, 0), tzinfo=dt_util.DEFAULT_TIME_ZONE)
|
||||
completion = await self._async_record_completion(
|
||||
chore,
|
||||
child,
|
||||
completed_at,
|
||||
auto_approve=True,
|
||||
points=points,
|
||||
as_parent=True,
|
||||
backdated=True,
|
||||
)
|
||||
await self._async_after_completions(chore, [(child, completion, points)], auto_approve=True)
|
||||
return completion
|
||||
|
||||
def completions_on_date(self, day: date) -> list[dict]:
|
||||
"""Every chore completion made on ``day`` (local time), sub-tasks excluded.
|
||||
|
||||
Shaped like the sensors' ``todays_completions`` rows, so the sticker
|
||||
chart can show a past day with the same code it uses for today.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"completion_id": comp.id,
|
||||
"chore_id": comp.chore_id,
|
||||
"child_id": comp.child_id,
|
||||
"approved": comp.approved,
|
||||
"points": comp.points_awarded,
|
||||
}
|
||||
for comp in self.storage.get_completions()
|
||||
if not comp.bonus_subtask_id and dt_util.as_local(comp.completed_at).date() == day
|
||||
]
|
||||
|
||||
def _backdate_refusal(self, chore, child, day: date, *, quota: bool = True) -> str | None:
|
||||
"""Why ``chore`` can't be logged for ``child`` on ``day``, or None if it can.
|
||||
|
||||
The one place the past-day rules live: logging a job refuses with this
|
||||
message, and the panel's day lookup offers exactly the chores it lets
|
||||
through, so the dialog never offers a tap TaskMate would then refuse.
|
||||
|
||||
``quota=False`` leaves out what that day and week have already used
|
||||
(the daily limit and a weekly target's quota). The sticker chart shows
|
||||
those itself, as done tiles that can still be undone.
|
||||
"""
|
||||
child_id = child.id
|
||||
if not getattr(chore, "enabled", True):
|
||||
return f"'{chore.name}' is switched off."
|
||||
assigned = chore.assigned_to or []
|
||||
if (assigned and child_id not in assigned) or child_id in (getattr(chore, "disabled_for", None) or []):
|
||||
return f"'{chore.name}' isn't one of {child.name}'s jobs."
|
||||
if (getattr(chore, "assignment_mode", "everyone") or "everyone") != "everyone" or self.teamwork_size(chore):
|
||||
return f"'{chore.name}' rotates or needs a team, so it can't be logged for a past day."
|
||||
if getattr(chore, "open_ended", False):
|
||||
return f"'{chore.name}' is open-ended, so it needs the child's note and a price. Use the child's card."
|
||||
if self.is_vacation_day(day):
|
||||
return f"{day.strftime('%A')} was a holiday, so no jobs were due."
|
||||
# A birthday with "chores off" is a day off for everything but mandatory
|
||||
# chores, the same rule today's chore list follows.
|
||||
if not getattr(chore, "mandatory", False) and self.is_birthday_day_off(child, day):
|
||||
return f"{day.strftime('%A')} was {child.name}'s birthday, a day off from jobs."
|
||||
if self.auction_winner(chore, day):
|
||||
return f"'{chore.name}' was won at auction that day, so it can't be logged afterwards."
|
||||
|
||||
# A weekly-target chore (#883) has no fixed days — the child picks them —
|
||||
# so it isn't "due" on any one day; its quota is the gate instead.
|
||||
weekly_target = int(getattr(chore, "weekly_target", 0) or 0)
|
||||
if weekly_target:
|
||||
if quota and self.weekly_target_met(chore, child_id, on=day):
|
||||
return f"'{chore.name}' already reached its {weekly_target} for that week."
|
||||
elif not self._is_chore_scheduled_for_date(chore, day):
|
||||
return f"'{chore.name}' wasn't due on {day.strftime('%A')}."
|
||||
|
||||
if not quota:
|
||||
return None
|
||||
redo_ids = self.inspection_redo_completion_ids()
|
||||
done_that_day = sum(
|
||||
1
|
||||
for comp in self.storage.get_completions()
|
||||
if comp.id not in redo_ids
|
||||
and comp.chore_id == chore.id
|
||||
and comp.child_id == child_id
|
||||
and not comp.bonus_subtask_id
|
||||
and dt_util.as_local(comp.completed_at).date() == day
|
||||
)
|
||||
daily_limit = max(1, int(getattr(chore, "daily_limit", 1) or 1))
|
||||
if done_that_day >= daily_limit:
|
||||
return f"'{chore.name}' was already done {done_that_day}/{daily_limit} times on {day.strftime('%A')}."
|
||||
return None
|
||||
|
||||
def loggable_on(self, day: date) -> dict[str, list[str]]:
|
||||
"""The chores each child could have had logged for ``day``.
|
||||
|
||||
Everything ``_backdate_refusal`` allows apart from the day's and week's
|
||||
quotas, which the sticker chart shows itself. The panel uses it so its
|
||||
list and the past-day chart follow every schedule TaskMate knows —
|
||||
recurrences, calendar moves, holidays — not just fixed weekdays.
|
||||
"""
|
||||
loggable: dict[str, list[str]] = {}
|
||||
for child in self.storage.get_children():
|
||||
ids = [
|
||||
chore.id
|
||||
for chore in self.storage.get_chores()
|
||||
if self._backdate_refusal(chore, child, day, quota=False) is None
|
||||
]
|
||||
if ids:
|
||||
loggable[child.id] = ids
|
||||
return loggable
|
||||
|
||||
def week_progress_on(self, day: date) -> dict[str, dict[str, int]]:
|
||||
"""Each child's count for every weekly-target chore, in the week of ``day``.
|
||||
|
||||
The past-day sticker chart caps a weekly-target chore at what is left
|
||||
of that week's quota, the way today's chart reads ``weekly_chore_progress``,
|
||||
so it never offers a tick the backend would refuse.
|
||||
"""
|
||||
progress: dict[str, dict[str, int]] = {}
|
||||
for chore in self.storage.get_chores():
|
||||
if int(getattr(chore, "weekly_target", 0) or 0) <= 0:
|
||||
continue
|
||||
for child in self.storage.get_children():
|
||||
count = self.weekly_completion_count(chore.id, child.id, on=day)
|
||||
if count:
|
||||
progress.setdefault(child.id, {})[chore.id] = count
|
||||
return progress
|
||||
|
||||
async def async_parent_complete_chore(self, chore_id: str) -> ChoreCompletion:
|
||||
"""Mark a chore as completed by the parent — zero points, advances recurrence."""
|
||||
chore = self.get_chore(chore_id)
|
||||
@@ -1211,12 +1404,14 @@ class ChoresMixin:
|
||||
if already_done:
|
||||
raise ValueError(f"Bonus sub-task '{subtask.name}' already completed today.")
|
||||
|
||||
# A grown-up's own sub-task never waits for approval (#1061).
|
||||
needs_approval = chore.requires_approval and not child.is_adult
|
||||
completion = ChoreCompletion(
|
||||
chore_id=chore_id,
|
||||
child_id=child_id,
|
||||
completed_at=now,
|
||||
approved=not chore.requires_approval,
|
||||
points_awarded=subtask.points if not chore.requires_approval else 0,
|
||||
approved=not needs_approval,
|
||||
points_awarded=subtask.points if not needs_approval else 0,
|
||||
submitted_points=subtask.points,
|
||||
bonus_subtask_id=bonus_subtask_id,
|
||||
child_undo_allowed=by_child and undo_window_seconds(self.storage) > 0,
|
||||
@@ -1227,7 +1422,7 @@ class ChoresMixin:
|
||||
# awarding can suspend.
|
||||
self.storage.add_completion(completion)
|
||||
|
||||
if not chore.requires_approval:
|
||||
if not needs_approval:
|
||||
total_awarded = await self._award_points(child, subtask.points, skip_streak=True)
|
||||
completion.approved = True
|
||||
completion.approved_at = dt_util.now()
|
||||
@@ -1235,7 +1430,7 @@ class ChoresMixin:
|
||||
self.storage.update_completion(completion)
|
||||
await self.storage.async_save()
|
||||
|
||||
if chore.requires_approval:
|
||||
if needs_approval:
|
||||
await self._async_notify_pending_approval(
|
||||
child.name, f"{chore.name} › {subtask.name}", subtask.points, completion_id=completion.id
|
||||
)
|
||||
@@ -1455,8 +1650,9 @@ class ChoresMixin:
|
||||
|
||||
# Only reverse streak for parent completions, and only
|
||||
# when this was the child's sole completion that day —
|
||||
# streaks are day-based, not per-completion
|
||||
if not completion.bonus_subtask_id:
|
||||
# streaks are day-based, not per-completion. A backdated one
|
||||
# never advanced the streak, so it has nothing to give back.
|
||||
if not completion.bonus_subtask_id and not completion.backdated:
|
||||
reject_date = dt_util.as_local(completion.completed_at).date()
|
||||
other_same_day = any(
|
||||
c.id != completion.id
|
||||
@@ -1508,8 +1704,10 @@ class ChoresMixin:
|
||||
)
|
||||
)
|
||||
child.streak_milestones_achieved = sorted(d for d in achieved if d <= child.current_streak)
|
||||
# The weekly target bonus this completion helped earn (#1044).
|
||||
self._reverse_weekly_target_bonus(completion, child)
|
||||
# The weekly target bonus this completion helped earn (#1044).
|
||||
# Outside the streak block: a backdated job never moved the
|
||||
# streak, but it can complete a week's target like any other.
|
||||
self._reverse_weekly_target_bonus(completion, child)
|
||||
|
||||
bonus_completions: list = []
|
||||
is_parent = not target_completion.bonus_subtask_id
|
||||
@@ -1532,8 +1730,17 @@ class ChoresMixin:
|
||||
child.total_points_earned = max(0, child.total_points_earned - bc.points_awarded)
|
||||
child.total_chores_completed = max(0, child.total_chores_completed - 1)
|
||||
|
||||
# Undo last_completed store so recurrence window resets correctly
|
||||
self.storage.undo_last_completed(target_completion.chore_id, target_completion.child_id)
|
||||
# Undo last_completed store so recurrence window resets correctly.
|
||||
# A backdated completion only owns the current entry if it was the
|
||||
# latest when logged; otherwise a later completion does, and popping
|
||||
# it would reopen that chore's window.
|
||||
lc_current = parse_datetime(
|
||||
(self.storage.get_last_completed(target_completion.chore_id, target_completion.child_id) or {}).get(
|
||||
"current"
|
||||
)
|
||||
)
|
||||
if not target_completion.backdated or lc_current == target_completion.completed_at:
|
||||
self.storage.undo_last_completed(target_completion.chore_id, target_completion.child_id)
|
||||
|
||||
# One-shot: re-enable for this child
|
||||
chore = self.get_chore(target_completion.chore_id)
|
||||
@@ -1900,33 +2107,40 @@ class ChoresMixin:
|
||||
days_since = (today - last_dt).days
|
||||
return days_since >= window_days
|
||||
|
||||
def weekly_completion_count(self, chore_id: str, child_id: str) -> int:
|
||||
def weekly_completion_count(self, chore_id: str, child_id: str, on: date | None = None) -> int:
|
||||
"""How many times ``child_id`` has completed ``chore_id`` this week.
|
||||
|
||||
Monday-anchored, matching the Challenges period convention. Bonus
|
||||
sub-tasks don't count; pending completions do, so a chore can't be
|
||||
submitted a fourth time just because the parent hasn't got round to
|
||||
approving the third yet.
|
||||
|
||||
``on`` asks about the week containing that day instead of this one, for
|
||||
a job being logged against a past week.
|
||||
"""
|
||||
today = dt_util.as_local(dt_util.now()).date()
|
||||
week_start = today - timedelta(days=today.weekday())
|
||||
anchor = on or dt_util.as_local(dt_util.now()).date()
|
||||
week_start = anchor - timedelta(days=anchor.weekday())
|
||||
week_end = week_start + timedelta(days=6)
|
||||
count = 0
|
||||
for comp in self._cached_completions_for_chore(chore_id):
|
||||
if comp.child_id != child_id or getattr(comp, "bonus_subtask_id", ""):
|
||||
continue
|
||||
try:
|
||||
if dt_util.as_local(comp.completed_at).date() >= week_start:
|
||||
if week_start <= dt_util.as_local(comp.completed_at).date() <= week_end:
|
||||
count += 1
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
continue
|
||||
return count
|
||||
|
||||
def weekly_target_met(self, chore, child_id: str) -> bool:
|
||||
"""True when a weekly-target chore has had its quota filled this week."""
|
||||
def weekly_target_met(self, chore, child_id: str, on: date | None = None) -> bool:
|
||||
"""True when a weekly-target chore has had its quota filled this week.
|
||||
|
||||
``on`` checks the week containing that day (see ``weekly_completion_count``).
|
||||
"""
|
||||
target = int(getattr(chore, "weekly_target", 0) or 0)
|
||||
if target <= 0:
|
||||
return False
|
||||
return self.weekly_completion_count(chore.id, child_id) >= target
|
||||
return self.weekly_completion_count(chore.id, child_id, on) >= target
|
||||
|
||||
@staticmethod
|
||||
def _week_key(when) -> str:
|
||||
|
||||
@@ -20,6 +20,84 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class PointsMixin:
|
||||
"""Mixin providing points, streaks, penalties, bonuses, and notifications."""
|
||||
|
||||
def week_progress(self, children, chores, completions) -> dict[str, dict[str, int]]:
|
||||
"""Points each child has earned this week, and the most they could have.
|
||||
|
||||
The week runs Monday to Sunday, like the weekly digest. ``earned`` is
|
||||
the approved chore points since Monday (bonus sub-tasks excluded, as
|
||||
they are extras on top of the jobs). ``available`` is every scheduled
|
||||
job's points at its daily limit across all seven days, with the weekend
|
||||
multiplier on Saturday and Sunday and the family's planned vacation days
|
||||
left out (a child's live away-sensor is not used: it says nothing about
|
||||
the rest of the week). Rotation
|
||||
chores are skipped — who is active on a future day can't be known — so
|
||||
``earned`` may run past ``available``; cards cap the display.
|
||||
|
||||
A weekly-target chore (#883) is picked up on whichever days the child
|
||||
chooses, so it adds its quota (target × points) once rather than a slot
|
||||
per scheduled day. An open-ended chore has no price of its own, so it
|
||||
adds nothing to ``available`` (what was paid for it still counts in
|
||||
``earned``). A teamwork chore counts each member's share, as paid.
|
||||
"""
|
||||
today = dt_util.now().date()
|
||||
week_start = today - timedelta(days=today.weekday())
|
||||
days = [week_start + timedelta(days=i) for i in range(7)]
|
||||
try:
|
||||
multiplier = float(self.storage.get_setting("weekend_multiplier", "2.0"))
|
||||
except (ValueError, TypeError):
|
||||
multiplier = 2.0
|
||||
|
||||
earned: dict[str, int] = {}
|
||||
for comp in completions:
|
||||
if not comp.approved or comp.bonus_subtask_id:
|
||||
continue
|
||||
if dt_util.as_local(comp.completed_at).date() < week_start:
|
||||
continue
|
||||
earned[comp.child_id] = earned.get(comp.child_id, 0) + (comp.points_awarded or 0)
|
||||
|
||||
celebration_multiplier = self.birthday_multiplier()
|
||||
|
||||
progress: dict[str, dict[str, int]] = {}
|
||||
for child in children:
|
||||
available = 0
|
||||
for chore in chores:
|
||||
if not getattr(chore, "enabled", True):
|
||||
continue
|
||||
assigned = chore.assigned_to or []
|
||||
if assigned and child.id not in assigned:
|
||||
continue
|
||||
if child.id in (getattr(chore, "disabled_for", None) or []):
|
||||
continue
|
||||
mode = getattr(chore, "assignment_mode", "everyone") or "everyone"
|
||||
if mode not in ("everyone", "first_come"):
|
||||
continue
|
||||
if getattr(chore, "open_ended", False):
|
||||
continue
|
||||
limit = 1 if mode == "first_come" else max(1, int(getattr(chore, "daily_limit", 1) or 1))
|
||||
# A teamwork chore (#928) pays each member their share (plus any
|
||||
# team bonus), so that is what it can add to a child's week.
|
||||
points = self.team_share_points(chore)
|
||||
weekly_target = int(getattr(chore, "weekly_target", 0) or 0)
|
||||
if weekly_target:
|
||||
available += weekly_target * points
|
||||
continue
|
||||
for day in days:
|
||||
if self.is_vacation_day(day) or not self._is_chore_scheduled_for_date(chore, day):
|
||||
continue
|
||||
# A birthday off (#924) only excuses non-mandatory chores.
|
||||
birthday = self.is_birthday(child, day)
|
||||
if birthday and not getattr(chore, "mandatory", False) and self.birthday_chores_off():
|
||||
continue
|
||||
per = points
|
||||
if day.weekday() in (5, 6) and multiplier > 1.0:
|
||||
per += round(points * (multiplier - 1.0))
|
||||
# The birthday multiplier stacks on the weekend one, as paid.
|
||||
if birthday and celebration_multiplier > 1.0:
|
||||
per += round(per * (celebration_multiplier - 1.0))
|
||||
available += per * limit
|
||||
progress[child.id] = {"earned": earned.get(child.id, 0), "available": available}
|
||||
return progress
|
||||
|
||||
async def _async_check_perfect_week(self) -> None:
|
||||
"""Award perfect week bonus to children who completed at least one chore every day last week."""
|
||||
perfect_week_enabled = self.storage.get_setting("perfect_week_enabled", "true") == "true"
|
||||
|
||||
@@ -123,19 +123,21 @@ class TimedMixin:
|
||||
rate_seconds = chore.timed_rate_minutes * 60
|
||||
pts = (total_seconds // rate_seconds) * chore.timed_rate_points if rate_seconds > 0 else 0
|
||||
|
||||
# A grown-up's own session never waits for approval (#1061).
|
||||
needs_approval = chore.requires_approval and not child.is_adult
|
||||
completion = ChoreCompletion(
|
||||
chore_id=chore_id,
|
||||
child_id=child_id,
|
||||
completed_at=now,
|
||||
approved=not chore.requires_approval,
|
||||
points_awarded=pts if not chore.requires_approval else 0,
|
||||
approved=not needs_approval,
|
||||
points_awarded=pts if not needs_approval else 0,
|
||||
submitted_points=pts,
|
||||
timed_duration_seconds=total_seconds,
|
||||
# The child's own stopped session: theirs to undo in the window (#918).
|
||||
child_undo_allowed=undo_window_seconds(self.storage) > 0,
|
||||
)
|
||||
|
||||
if not chore.requires_approval:
|
||||
if not needs_approval:
|
||||
total_awarded = await self._award_points(child, pts)
|
||||
completion.approved = True
|
||||
completion.approved_at = dt_util.now()
|
||||
@@ -147,7 +149,7 @@ class TimedMixin:
|
||||
|
||||
await self.storage.async_save()
|
||||
|
||||
if chore.requires_approval:
|
||||
if needs_approval:
|
||||
await self._async_notify_pending_approval(
|
||||
child.name,
|
||||
chore.name,
|
||||
@@ -252,17 +254,18 @@ class TimedMixin:
|
||||
rate_seconds = chore.timed_rate_minutes * 60
|
||||
pts = (total_seconds // rate_seconds) * chore.timed_rate_points if rate_seconds > 0 else 0
|
||||
|
||||
needs_approval = chore.requires_approval and not child.is_adult
|
||||
if total_seconds > 0:
|
||||
completion = ChoreCompletion(
|
||||
chore_id=session.chore_id,
|
||||
child_id=session.child_id,
|
||||
completed_at=dt_util.now(),
|
||||
approved=not chore.requires_approval,
|
||||
points_awarded=pts if not chore.requires_approval else 0,
|
||||
approved=not needs_approval,
|
||||
points_awarded=pts if not needs_approval else 0,
|
||||
submitted_points=pts,
|
||||
timed_duration_seconds=total_seconds,
|
||||
)
|
||||
if not chore.requires_approval:
|
||||
if not needs_approval:
|
||||
total_awarded = await self._award_points(child, pts)
|
||||
completion.approved = True
|
||||
completion.approved_at = dt_util.now()
|
||||
|
||||
@@ -986,9 +986,11 @@ class TaskMateCoordinator(
|
||||
unavailability_entity: str = "",
|
||||
pause_streak_when_unavailable: bool = False,
|
||||
linked_user_id: str = "",
|
||||
picture_entity: str = "",
|
||||
birthday: str = "",
|
||||
presence_entity: str = "",
|
||||
age_group: str = "",
|
||||
is_adult: bool = False,
|
||||
refresh: bool = True,
|
||||
) -> Child:
|
||||
"""Add a new child.
|
||||
@@ -1004,9 +1006,11 @@ class TaskMateCoordinator(
|
||||
unavailability_entity=unavailability_entity,
|
||||
pause_streak_when_unavailable=pause_streak_when_unavailable,
|
||||
linked_user_id=linked_user_id,
|
||||
picture_entity=picture_entity,
|
||||
birthday=birthday,
|
||||
presence_entity=presence_entity,
|
||||
age_group=age_group,
|
||||
is_adult=is_adult,
|
||||
)
|
||||
self.storage.add_child(child)
|
||||
await self.storage.async_save()
|
||||
|
||||
@@ -49,6 +49,7 @@ CARDS: Final = [
|
||||
"taskmate-calendar-card.js",
|
||||
"taskmate-photo-gallery-card.js",
|
||||
"taskmate-family-goal-card.js",
|
||||
"taskmate-sticker-chart-card.js",
|
||||
"taskmate-wishlist-card.js",
|
||||
"taskmate-kiosk-card.js",
|
||||
"taskmate-bounty-card.js",
|
||||
|
||||
@@ -17,5 +17,5 @@
|
||||
"iot_class": "calculated",
|
||||
"issue_tracker": "https://github.com/tempus2016/taskmate/issues",
|
||||
"requirements": [],
|
||||
"version": "6.3.0"
|
||||
"version": "6.4.0"
|
||||
}
|
||||
|
||||
@@ -235,6 +235,12 @@ class Child:
|
||||
total_penalties_received: int = 0
|
||||
notify_service: str | None = None
|
||||
linked_user_id: str = "" # HA user id; when set, only that user (or an admin) may self-serve as this child
|
||||
# Entity whose picture replaces the MDI avatar — normally the child's
|
||||
# person.* entity. Deliberately independent of linked_user_id: a young
|
||||
# child often has a person (with a photo) but no HA login of their own.
|
||||
# Only the entity id is stored; the picture is read from it on every
|
||||
# update, so a new photo set in HA shows up with nothing to re-save here.
|
||||
picture_entity: str = ""
|
||||
quiet_hours_start: str = "" # "HH:MM" — start of do-not-disturb window; empty = no quiet hours
|
||||
quiet_hours_end: str = "" # "HH:MM" — end of do-not-disturb window; start>end means overnight
|
||||
presence_entity: str = "" # person./device_tracker.; while not home, reminders wait for arrival (#926)
|
||||
@@ -248,6 +254,9 @@ class Child:
|
||||
# Setup wizard age group (#980): one of AGE_GROUPS, or "" when none was
|
||||
# picked (every child that predates the wizard). Only steers suggestions.
|
||||
age_group: str = ""
|
||||
# A grown-up's profile (#1061): their own completions never wait for
|
||||
# approval, and cards leave them out unless a card opts in (show_adults).
|
||||
is_adult: bool = False
|
||||
id: str = field(default_factory=generate_id)
|
||||
|
||||
@classmethod
|
||||
@@ -274,6 +283,7 @@ class Child:
|
||||
guest_expires_on=str(data.get("guest_expires_on", "") or ""),
|
||||
birthday=str(data.get("birthday", "") or ""),
|
||||
age_group=normalize_age_group(data.get("age_group")),
|
||||
is_adult=bool(data.get("is_adult", False)),
|
||||
availability_entity=data.get("availability_entity", ""),
|
||||
availability_inverted=data.get("availability_inverted", False),
|
||||
unavailability_entity=data.get("unavailability_entity", ""),
|
||||
@@ -282,6 +292,7 @@ class Child:
|
||||
total_penalties_received=data.get("total_penalties_received", 0),
|
||||
notify_service=data.get("notify_service"),
|
||||
linked_user_id=data.get("linked_user_id", ""),
|
||||
picture_entity=str(data.get("picture_entity", "") or ""),
|
||||
quiet_hours_start=data.get("quiet_hours_start", ""),
|
||||
quiet_hours_end=data.get("quiet_hours_end", ""),
|
||||
presence_entity=str(data.get("presence_entity", "") or ""),
|
||||
@@ -312,6 +323,7 @@ class Child:
|
||||
"guest_expires_on": self.guest_expires_on,
|
||||
"birthday": self.birthday,
|
||||
"age_group": self.age_group,
|
||||
"is_adult": self.is_adult,
|
||||
"availability_entity": self.availability_entity,
|
||||
"availability_inverted": self.availability_inverted,
|
||||
"unavailability_entity": self.unavailability_entity,
|
||||
@@ -320,6 +332,7 @@ class Child:
|
||||
"total_penalties_received": self.total_penalties_received,
|
||||
"notify_service": self.notify_service,
|
||||
"linked_user_id": self.linked_user_id,
|
||||
"picture_entity": self.picture_entity,
|
||||
"quiet_hours_start": self.quiet_hours_start,
|
||||
"quiet_hours_end": self.quiet_hours_end,
|
||||
"presence_entity": self.presence_entity,
|
||||
@@ -1112,6 +1125,11 @@ class ChoreCompletion:
|
||||
# Set when this completion is a bounty's (#931). chore_id then carries the
|
||||
# bounty id too, so nothing that groups completions by chore mixes the two.
|
||||
bounty_id: str = ""
|
||||
# True on a completion a parent logged afterwards for an earlier day. It was
|
||||
# paid without touching the child's streak (rewriting last_completion_date
|
||||
# to a past day would break a streak they are on today), so undoing it must
|
||||
# not take a day off the streak either.
|
||||
backdated: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> ChoreCompletion:
|
||||
@@ -1136,6 +1154,7 @@ class ChoreCompletion:
|
||||
quality_rating=parse_quality_rating(data.get("quality_rating")),
|
||||
child_undo_allowed=data.get("child_undo_allowed") is True,
|
||||
bounty_id=str(data.get("bounty_id", "") or ""),
|
||||
backdated=data.get("backdated") is True,
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
@@ -1162,6 +1181,8 @@ class ChoreCompletion:
|
||||
data["child_undo_allowed"] = True
|
||||
if self.bounty_id:
|
||||
data["bounty_id"] = self.bounty_id
|
||||
if self.backdated:
|
||||
data["backdated"] = True
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -68,8 +68,11 @@ def _compute_common(coordinator: TaskMateCoordinator) -> dict:
|
||||
"""
|
||||
data = coordinator.data
|
||||
data_id = id(data)
|
||||
# External entities TaskMate reads (a child's photo among them) are not part
|
||||
# of ``data``, so their version is part of the key.
|
||||
external = getattr(coordinator, "external_state_version", 0)
|
||||
cached = getattr(coordinator, _COMMON_CACHE_ATTR, None)
|
||||
if cached and cached.get("data_id") == data_id:
|
||||
if cached and cached.get("data_id") == data_id and cached.get("external") == external:
|
||||
return cached["common"]
|
||||
|
||||
children = data.get("children", [])
|
||||
@@ -142,10 +145,15 @@ def _compute_common(coordinator: TaskMateCoordinator) -> dict:
|
||||
"pool_by_child_reward": pool_by_child_reward,
|
||||
"pool_total_by_reward": pool_total_by_reward,
|
||||
"total_allocated_by_child": total_allocated_by_child,
|
||||
# Each child's photo (from their chosen picture entity), resolved once
|
||||
# per update: every call site that draws a child's face reads this.
|
||||
"child_avatar_images": {c.id: coordinator.child_avatar_image(c) for c in children},
|
||||
# This week's points earned vs. the most on offer, per child.
|
||||
"child_week_progress": coordinator.week_progress(children, chores, all_completions),
|
||||
# Sent back to redo by an inspection (#981): not "done" on the card.
|
||||
"inspection_redo_ids": _inspection_redo_ids(coordinator),
|
||||
}
|
||||
setattr(coordinator, _COMMON_CACHE_ATTR, {"data_id": data_id, "common": common})
|
||||
setattr(coordinator, _COMMON_CACHE_ATTR, {"data_id": data_id, "external": external, "common": common})
|
||||
return common
|
||||
|
||||
|
||||
@@ -193,6 +201,8 @@ def _build_children_summary(coordinator: TaskMateCoordinator, common: dict) -> l
|
||||
if getattr(c, "is_guest", False)
|
||||
else {}
|
||||
),
|
||||
# Grown-ups (#1061): cards leave these out unless show_adults is set.
|
||||
**({"is_adult": True} if getattr(c, "is_adult", False) else {}),
|
||||
# Chore roulette (#677): today's pick + spins left, so the card can
|
||||
# show the result and disable the button once the allowance is used.
|
||||
**(
|
||||
@@ -217,6 +227,10 @@ def _build_children_summary(coordinator: TaskMateCoordinator, common: dict) -> l
|
||||
"total_points_earned": getattr(c, "total_points_earned", 0) or 0,
|
||||
"total_chores_completed": getattr(c, "total_chores_completed", 0) or 0,
|
||||
"avatar": getattr(c, "avatar", "mdi:account-circle") or "mdi:account-circle",
|
||||
# The child's photo, or "" — cards fall back to `avatar`.
|
||||
"avatar_image": common.get("child_avatar_images", {}).get(c.id, ""),
|
||||
"week_points_earned": common.get("child_week_progress", {}).get(c.id, {}).get("earned", 0),
|
||||
"week_points_available": common.get("child_week_progress", {}).get(c.id, {}).get("available", 0),
|
||||
"last_completion_date": getattr(c, "last_completion_date", None),
|
||||
"streak_paused": getattr(c, "streak_paused", False),
|
||||
"on_vacation": coordinator._is_child_on_vacation(c),
|
||||
@@ -698,6 +712,9 @@ def _build_pending_reward_claims(common: dict) -> list[dict]:
|
||||
"reward_id": rc.reward_id,
|
||||
"child_id": rc.child_id,
|
||||
"child_name": child.name,
|
||||
# No avatar_image here on purpose: the cards resolve a claim's
|
||||
# face from the overview sensor's children[], and a copy per
|
||||
# claim pushed this sensor past the 16KB recorder limit.
|
||||
"child_avatar": getattr(child, "avatar", "mdi:account-circle") or "mdi:account-circle",
|
||||
"reward_name": reward.name,
|
||||
"reward_icon": reward.icon or "mdi:gift",
|
||||
@@ -1575,6 +1592,7 @@ class ChildPointsSensor(TaskMateBaseSensor):
|
||||
"child_id": child.id,
|
||||
"child_name": child.name,
|
||||
"avatar": child.avatar,
|
||||
"avatar_image": self.coordinator.child_avatar_image(child),
|
||||
"total_points_earned": child.total_points_earned,
|
||||
"total_chores_completed": child.total_chores_completed,
|
||||
"current_streak": child.current_streak,
|
||||
@@ -1615,6 +1633,18 @@ class ChildStatsSensor(TaskMateBaseSensor):
|
||||
child = self.coordinator.get_child(self.child_id)
|
||||
return child.avatar if child else "mdi:account-circle"
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str | None:
|
||||
"""The child's photo, so HA's own UI shows their face too.
|
||||
|
||||
HA prefers a picture over ``icon`` when both are set, so returning
|
||||
None (the default) leaves the MDI avatar above in charge.
|
||||
"""
|
||||
child = self.coordinator.get_child(self.child_id)
|
||||
if not child:
|
||||
return None
|
||||
return self.coordinator.child_avatar_image(child) or None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict:
|
||||
"""Return additional attributes."""
|
||||
@@ -1650,6 +1680,7 @@ class ChildStatsSensor(TaskMateBaseSensor):
|
||||
"child_id": child.id,
|
||||
"child_name": child.name,
|
||||
"avatar": child.avatar,
|
||||
"avatar_image": self.coordinator.child_avatar_image(child),
|
||||
"points": child.points,
|
||||
"total_points_earned": child.total_points_earned,
|
||||
"total_chores_completed": child.total_chores_completed,
|
||||
@@ -1869,6 +1900,10 @@ class ChildBadgesSensor(TaskMateBaseSensor):
|
||||
earned.sort(key=lambda e: e.get("earned_at") or "", reverse=True)
|
||||
|
||||
return {
|
||||
# Who the badges belong to, so the card can put a face to them.
|
||||
"child_name": child.name,
|
||||
"child_avatar": getattr(child, "avatar", "mdi:account-circle") or "mdi:account-circle",
|
||||
"avatar_image": self.coordinator.child_avatar_image(child),
|
||||
"earned": earned,
|
||||
"available": available,
|
||||
"total_badges": len(applicable),
|
||||
|
||||
@@ -27,6 +27,12 @@ complete_chore:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
completed_date:
|
||||
name: Done on
|
||||
description: Log the job for an earlier day (up to 7 days back, not today) that was done but not ticked. Parents only; approved straight away. The day's daily limit still applies.
|
||||
required: false
|
||||
selector:
|
||||
date:
|
||||
note:
|
||||
name: Note
|
||||
description: What the child did, in their own words. Required when the chore is open-ended, and shown to the parent during approval.
|
||||
|
||||
@@ -173,6 +173,7 @@ WS_SET_CHORE_ORDER: Final = "taskmate/set_chore_order"
|
||||
WS_SET_GLOBAL_CHORE_ORDER: Final = "taskmate/set_global_chore_order"
|
||||
WS_ADD_CHORES_BULK: Final = "taskmate/add_chores_bulk"
|
||||
WS_PARENT_COMPLETE_CHORE: Final = "taskmate/parent_complete_chore"
|
||||
WS_DAY_COMPLETIONS: Final = "taskmate/day_completions"
|
||||
|
||||
# Templates
|
||||
WS_TEMPLATES_LIST: Final = "taskmate/templates/list"
|
||||
@@ -490,9 +491,11 @@ async def _ws_get_state(hass, connection, msg, coordinator):
|
||||
vol.Optional("unavailability_entity", default=""): str,
|
||||
vol.Optional("pause_streak_when_unavailable", default=False): bool,
|
||||
vol.Optional("linked_user_id", default=""): str,
|
||||
vol.Optional("picture_entity", default=""): str,
|
||||
vol.Optional("birthday", default=""): vol.All(str, vol.Length(max=10)),
|
||||
vol.Optional("presence_entity", default=""): _validate_presence_entity,
|
||||
vol.Optional("age_group", default=""): vol.In(("", *AGE_GROUPS)),
|
||||
vol.Optional("is_adult", default=False): bool,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@@ -511,9 +514,11 @@ async def _ws_add_child(hass, connection, msg, coordinator):
|
||||
unavailability_entity=_opt_str(msg.get("unavailability_entity")),
|
||||
pause_streak_when_unavailable=bool(msg.get("pause_streak_when_unavailable", False)),
|
||||
linked_user_id=_opt_str(msg.get("linked_user_id")),
|
||||
picture_entity=_opt_str(msg.get("picture_entity")),
|
||||
birthday=birthday,
|
||||
presence_entity=msg.get("presence_entity", ""),
|
||||
age_group=msg.get("age_group", ""),
|
||||
is_adult=bool(msg.get("is_adult", False)),
|
||||
)
|
||||
connection.send_result(msg["id"], {"id": child.id})
|
||||
|
||||
@@ -529,11 +534,13 @@ async def _ws_add_child(hass, connection, msg, coordinator):
|
||||
vol.Optional("unavailability_entity"): str,
|
||||
vol.Optional("pause_streak_when_unavailable"): bool,
|
||||
vol.Optional("linked_user_id"): str,
|
||||
vol.Optional("picture_entity"): str,
|
||||
vol.Optional("presence_entity"): _validate_presence_entity,
|
||||
vol.Optional("is_guest"): bool,
|
||||
vol.Optional("guest_expires_on"): str,
|
||||
vol.Optional("birthday"): vol.All(str, vol.Length(max=10)),
|
||||
vol.Optional("age_group"): vol.In(("", *AGE_GROUPS)),
|
||||
vol.Optional("is_adult"): bool,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@@ -557,6 +564,8 @@ async def _ws_update_child(hass, connection, msg, coordinator):
|
||||
existing.pause_streak_when_unavailable = bool(msg["pause_streak_when_unavailable"])
|
||||
if "linked_user_id" in msg:
|
||||
existing.linked_user_id = _opt_str(msg["linked_user_id"])
|
||||
if "picture_entity" in msg:
|
||||
existing.picture_entity = _opt_str(msg["picture_entity"])
|
||||
if "birthday" in msg:
|
||||
try:
|
||||
existing.birthday = normalize_birthday(msg["birthday"])
|
||||
@@ -567,6 +576,8 @@ async def _ws_update_child(hass, connection, msg, coordinator):
|
||||
existing.presence_entity = msg["presence_entity"]
|
||||
if "age_group" in msg:
|
||||
existing.age_group = msg["age_group"]
|
||||
if "is_adult" in msg:
|
||||
existing.is_adult = bool(msg["is_adult"])
|
||||
if "is_guest" in msg or "guest_expires_on" in msg:
|
||||
# Routed through the coordinator so the expiry is validated and an
|
||||
# archived guest is un-archived when promoted to a family member.
|
||||
@@ -2410,6 +2421,26 @@ async def _ws_parent_complete_chore(hass, connection, msg, coordinator):
|
||||
connection.send_result(msg["id"], {"ok": True})
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): WS_DAY_COMPLETIONS,
|
||||
vol.Required("date"): vol.All(str, vol.Coerce(date.fromisoformat)),
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@_admin_only
|
||||
async def _ws_day_completions(hass, connection, msg, coordinator):
|
||||
"""What was done on one day — for logging past jobs from the panel."""
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
"completions": coordinator.completions_on_date(msg["date"]),
|
||||
"week_progress": coordinator.week_progress_on(msg["date"]),
|
||||
"loggable": coordinator.loggable_on(msg["date"]),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): WS_SET_CHORE_ORDER,
|
||||
@@ -3183,6 +3214,7 @@ _COMMANDS = (
|
||||
_ws_approve_reward,
|
||||
_ws_reject_reward,
|
||||
_ws_parent_complete_chore,
|
||||
_ws_day_completions,
|
||||
_ws_set_chore_order,
|
||||
_ws_set_global_chore_order,
|
||||
_ws_add_chores_bulk,
|
||||
|
||||
@@ -363,6 +363,8 @@
|
||||
"common.editor.filter_by_child_all": "Alle børn",
|
||||
"common.editor.header_colour": "Overskriftsfarve",
|
||||
"common.editor.header_colour_helper": "Baggrundsfarve på kortets overskrift",
|
||||
"common.editor.show_adults": "Vis voksne",
|
||||
"common.editor.show_adults_helper": "Medtag voksenprofiler på dette kort",
|
||||
"common.editor.overview_entity": "Oversigtsentitet",
|
||||
"common.editor.overview_entity_helper": "TaskMates oversigtssensor",
|
||||
"common.editor.show_parent_actions": "Vis forældrehandlinger",
|
||||
@@ -778,6 +780,8 @@
|
||||
"panel.child_inverted": "(omvendt)",
|
||||
"panel.child_link_user_label": "Knyt til Home Assistant-bruger (valgfrit)",
|
||||
"panel.child_link_user_hint": "Når det er sat, kan kun denne Home Assistant-bruger (eller en admin) fuldføre pligter, indløse belønninger eller køre timere som dette barn via service-kald. Vælg \"Alle\" til en fælles familie-tablet.",
|
||||
"panel.child_picture_entity_label": "Foto fra",
|
||||
"panel.child_picture_entity_hint": "Vælg en entitet med et billede, typisk barnets person, for at vise det i stedet for ikonet ovenfor. En persons foto indstilles under Indstillinger › Personer; uden billede vises ikonet.",
|
||||
"panel.child_kiosk_pin_label": "Kiosk-PIN-kode",
|
||||
"panel.child_kiosk_pin_hint": "Valgfri. 4 cifre, som barnet taster på en delt kiosk-tablet. Gemmes som hash og kan ikke læses igen — lad feltet stå tomt for at beholde den nuværende kode.",
|
||||
"panel.child_kiosk_pin_set": "Der er sat en PIN-kode.",
|
||||
@@ -939,6 +943,9 @@
|
||||
"panel.child_age_group_label": "Aldersgruppe",
|
||||
"panel.child_age_group_none": "Ikke valgt",
|
||||
"panel.child_age_group_hint": "Bestemmer, hvilke pligter opsætningsguiden foreslår. Ændrer ikke andet.",
|
||||
"panel.child_adult_label": "Voksenprofil",
|
||||
"panel.child_adult_hint": "Til en forælder eller anden voksen. Pligter, de markerer som udført, tæller med det samme uden godkendelse, og kort viser dem kun, når kortets indstilling \"Vis voksne\" er slået til.",
|
||||
"panel.child_adult_pill": "Voksen",
|
||||
"wizard.title": "Opsætningsguide",
|
||||
"wizard.run_btn": "Kør opsætningsguiden",
|
||||
"wizard.open_btn": "Åbn guiden",
|
||||
@@ -3055,5 +3062,66 @@
|
||||
"notify.sample_pledger": "Mormor",
|
||||
"notify.sample_bounty": "Vask bilen",
|
||||
"notify.sample_reason": "Ikke færdig",
|
||||
"notify.sample_note": "Flot klaret!"
|
||||
"notify.sample_note": "Flot klaret!",
|
||||
"sticker_chart.error.entity_required": "Angiv en entitet",
|
||||
"sticker_chart.default_title": "{name}s klistermærkeskema",
|
||||
"sticker_chart.today": "I dag: {done} / {total} opgaver klaret",
|
||||
"sticker_chart.all_done": "Alt klaret, {name}!",
|
||||
"sticker_chart.day_done_message": "Alle opgaver er klaret i dag!",
|
||||
"sticker_chart.no_stickers": "Ingen klistermærker endnu - lav en opgave for at få dit første!",
|
||||
"sticker_chart.stickers_alt": "{count} {points} optjent",
|
||||
"sticker_chart.goal": "{points} / {goal} {unit} til belønningen",
|
||||
"sticker_chart.goal_reached": "MÅLET ER NÅET! Tid til din belønning!",
|
||||
"sticker_chart.week_goal": "{points} / {goal} {unit} denne uge",
|
||||
"sticker_chart.week_perfect": "PERFEKT UGE! Alle stjerner er optjent!",
|
||||
"sticker_chart.block_key": "Hver blok = {count} {unit}",
|
||||
"sticker_chart.day_count": "{day}: {done} / {total} opgaver klaret",
|
||||
"sticker_chart.no_jobs_day": "Ingen opgaver skulle laves {day}.",
|
||||
"sticker_chart.editor.goal": "Hvad klistermærkerne tæller mod",
|
||||
"sticker_chart.editor.goal_reward": "Belønningen",
|
||||
"sticker_chart.editor.goal_week": "Ugens opgaver",
|
||||
"sticker_chart.editor.goal_helper": "Ugens opgaver: blokkene fyldes med de stjerner, der er optjent siden mandag, ud af alle de stjerner, ugens opgaver kan give. Belønningen og knappen til at kræve den forbliver uændrede.",
|
||||
"sticker_chart.no_reward": "Ingen belønning at spare op til endnu - vælg en i kortets indstillinger.",
|
||||
"sticker_chart.no_child": "Ingen børn fundet på denne entitet",
|
||||
"sticker_chart.editor.title": "Titel",
|
||||
"sticker_chart.editor.reward": "Belønning at spare op til",
|
||||
"sticker_chart.editor.reward_helper": "Belønningens pris bliver målet. Lad stå på automatisk for at sigte mod den billigste, de endnu ikke har optjent.",
|
||||
"sticker_chart.editor.reward_auto": "Automatisk (den næste, der kan optjenes)",
|
||||
"sticker_chart.editor.show_today": "Vis dagens opgaver",
|
||||
"sticker_chart.editor.show_today_helper": "Vis linjen \"I dag: x / y opgaver klaret\" over klistermærkerne.",
|
||||
"sticker_chart.claim": "Kræv {reward}",
|
||||
"sticker_chart.claim_confirm": "Tryk igen for at kræve {reward}",
|
||||
"sticker_chart.claim_pending": "Venter på, at en voksen siger ja",
|
||||
"sticker_chart.claim_short": "{count} mere {unit} igen",
|
||||
"sticker_chart.claimed": "{reward} krævet for {name}!",
|
||||
"sticker_chart.complete_failed": "Det virkede ikke: {error}",
|
||||
"sticker_chart.editor.allow_undo": "Hold et klistermærke nede for at fortryde",
|
||||
"sticker_chart.editor.allow_undo_helper": "Tryk og hold på et klistermærke for at tage det tilbage. TaskMate lader kun en forælder gøre dette.",
|
||||
"sticker_chart.editor.child": "Barn",
|
||||
"sticker_chart.editor.child_all": "Alle børn, side om side",
|
||||
"sticker_chart.editor.columns": "Klistermærker pr. række",
|
||||
"sticker_chart.editor.show_claim": "Vis knappen til at kræve belønningen",
|
||||
"sticker_chart.editor.show_claim_helper": "En knap under målet, der kræver belønningen, når der er optjent nok (tryk to gange for at bekræfte).",
|
||||
"sticker_chart.editor.sound": "Afspil opgavens lyd",
|
||||
"sticker_chart.editor.title_helper": "Lad stå tomt for at bruge hvert barns navn.",
|
||||
"sticker_chart.goal_tile": "Mål",
|
||||
"sticker_chart.needs_photo": "Denne opgave kræver et foto - åbn den på TaskMates børnekort.",
|
||||
"sticker_chart.no_jobs_today": "Ingen opgaver i dag - nyd din dag!",
|
||||
"sticker_chart.state.done": "klaret",
|
||||
"sticker_chart.state.partial": "påbegyndt",
|
||||
"sticker_chart.state.pending": "venter på en voksen",
|
||||
"sticker_chart.state.todo": "skal gøres",
|
||||
"sticker_chart.not_allowed": "Denne tablet må ikke sætte flueben ved {name}s opgaver. En voksen kan rette det i TaskMate: knyt {name} til den konto, tabletten bruger, eller tilføj kontoen som forælder.",
|
||||
"sticker_chart.undo_denied": "Kun en voksen kan fortryde et klistermærke.",
|
||||
"sticker_chart.undone": "Klistermærket er taget tilbage.",
|
||||
"panel.backdate_button": "Registrer en tidligere opgave",
|
||||
"panel.backdate_title": "Registrer en tidligere opgave for {name}",
|
||||
"panel.backdate_hint": "Vælg dagen, og tryk derefter på de opgaver, der blev lavet, ligesom på klistermærkeskemaet. De tæller for den dag og uge og godkendes med det samme. Hold et klistermærke nede for at tage det tilbage.",
|
||||
"panel.backdate_hint_list": "Til en opgave, der blev lavet en tidligere dag, men ikke blev afkrydset. Den tæller for den dag og uge og godkendes med det samme.",
|
||||
"panel.backdate_finish": "Færdig",
|
||||
"panel.backdate_day": "Dagen den blev lavet",
|
||||
"panel.backdate_chore": "Opgave",
|
||||
"panel.backdate_none": "Ingen opgaver skulle laves for {name} den dag.",
|
||||
"panel.backdate_save": "Registrer",
|
||||
"panel.backdate_done": "{chore} registreret for {name} d. {day}"
|
||||
}
|
||||
|
||||
@@ -353,6 +353,8 @@
|
||||
"common.editor.filter_by_child_all": "Alle Kinder",
|
||||
"common.editor.header_colour": "Kopfzeilenfarbe",
|
||||
"common.editor.header_colour_helper": "Hintergrundfarbe der Kartenkopfzeile",
|
||||
"common.editor.show_adults": "Erwachsene anzeigen",
|
||||
"common.editor.show_adults_helper": "Erwachsenenprofile auf dieser Karte anzeigen",
|
||||
"common.editor.overview_entity": "Übersichtseinheit",
|
||||
"common.editor.overview_entity_helper": "Die TaskMate-Übersichtssensoreinheit",
|
||||
"common.editor.show_parent_actions": "Elternaktionen anzeigen",
|
||||
@@ -889,6 +891,9 @@
|
||||
"panel.child_age_group_label": "Altersgruppe",
|
||||
"panel.child_age_group_none": "Nicht festgelegt",
|
||||
"panel.child_age_group_hint": "Bestimmt, welche Aufgaben der Einrichtungsassistent vorschlägt. Ändert sonst nichts.",
|
||||
"panel.child_adult_label": "Erwachsenenprofil",
|
||||
"panel.child_adult_hint": "Für ein Elternteil oder einen anderen Erwachsenen. Aufgaben, die sie als erledigt markieren, zählen sofort ohne Genehmigung, und Karten zeigen sie nur, wenn die Kartenoption „Erwachsene anzeigen“ aktiviert ist.",
|
||||
"panel.child_adult_pill": "Erwachsen",
|
||||
"wizard.title": "Einrichtungsassistent",
|
||||
"wizard.run_btn": "Einrichtungsassistent starten",
|
||||
"wizard.open_btn": "Assistent öffnen",
|
||||
@@ -1886,6 +1891,57 @@
|
||||
"streak.editor.days_shown_helper": "Wie viele Tage der Punkte werden angezeigt (Standard: 14)",
|
||||
"streak.editor.title": "Titel",
|
||||
"streak.error.entity_required": "Bitte definieren Sie eine Entität",
|
||||
"sticker_chart.error.entity_required": "Bitte definieren Sie eine Entität",
|
||||
"sticker_chart.default_title": "{name}s Stickertafel",
|
||||
"sticker_chart.today": "Heute: {done} / {total} Aufgaben erledigt",
|
||||
"sticker_chart.all_done": "Alles geschafft, {name}!",
|
||||
"sticker_chart.day_done_message": "Heute ist jede Aufgabe erledigt!",
|
||||
"sticker_chart.no_stickers": "Noch keine Sticker - erledige eine Aufgabe für den ersten!",
|
||||
"sticker_chart.stickers_alt": "{count} {points} gesammelt",
|
||||
"sticker_chart.goal": "{points} / {goal} {unit} bis zur Belohnung",
|
||||
"sticker_chart.goal_reached": "ZIEL ERREICHT! Zeit für die Belohnung!",
|
||||
"sticker_chart.week_goal": "{points} / {goal} {unit} diese Woche",
|
||||
"sticker_chart.week_perfect": "PERFEKTE WOCHE! Jeden Stern verdient!",
|
||||
"sticker_chart.block_key": "Jedes Kästchen = {count} {unit}",
|
||||
"sticker_chart.day_count": "{day}: {done} / {total} Aufgaben erledigt",
|
||||
"sticker_chart.no_jobs_day": "Am {day} war keine Aufgabe fällig.",
|
||||
"sticker_chart.editor.goal": "Worauf die Sticker zählen",
|
||||
"sticker_chart.editor.goal_reward": "Die Belohnung",
|
||||
"sticker_chart.editor.goal_week": "Die Aufgaben dieser Woche",
|
||||
"sticker_chart.editor.goal_helper": "Aufgaben dieser Woche: Die Kästchen füllen sich mit den seit Montag verdienten Sternen, von allen Sternen, die die Aufgaben der Woche bringen können. Belohnung und Einlöse-Knopf bleiben, wie sie sind.",
|
||||
"sticker_chart.no_reward": "Noch keine Belohnung zum Sparen - wähle eine in den Karteneinstellungen.",
|
||||
"sticker_chart.no_child": "Keine Kinder für diese Entität gefunden",
|
||||
"sticker_chart.editor.title": "Titel",
|
||||
"sticker_chart.editor.reward": "Belohnung zum Sparen",
|
||||
"sticker_chart.editor.reward_helper": "Die Kosten der Belohnung werden zum Ziel. Auf automatisch belassen, um die günstigste noch nicht verdiente anzupeilen.",
|
||||
"sticker_chart.editor.reward_auto": "Automatisch (die nächste)",
|
||||
"sticker_chart.editor.show_today": "Heutige Aufgaben anzeigen",
|
||||
"sticker_chart.editor.show_today_helper": "Zeigt die Zeile \"Heute: x / y Aufgaben erledigt\" über den Stickern.",
|
||||
"sticker_chart.claim": "{reward} einlösen",
|
||||
"sticker_chart.claim_confirm": "Nochmal tippen, um {reward} einzulösen",
|
||||
"sticker_chart.claim_pending": "Warte auf das OK von einem Erwachsenen",
|
||||
"sticker_chart.claim_short": "Noch {count} {unit}",
|
||||
"sticker_chart.claimed": "{reward} für {name} eingelöst!",
|
||||
"sticker_chart.complete_failed": "Das hat nicht geklappt: {error}",
|
||||
"sticker_chart.editor.allow_undo": "Sticker gedrückt halten zum Rückgängigmachen",
|
||||
"sticker_chart.editor.allow_undo_helper": "Einen Sticker gedrückt halten, um ihn zurückzunehmen. TaskMate erlaubt das nur Eltern.",
|
||||
"sticker_chart.editor.child": "Kind",
|
||||
"sticker_chart.editor.child_all": "Alle Kinder nebeneinander",
|
||||
"sticker_chart.editor.columns": "Sticker pro Reihe",
|
||||
"sticker_chart.editor.show_claim": "Einlöse-Knopf anzeigen",
|
||||
"sticker_chart.editor.show_claim_helper": "Ein Knopf unter dem Ziel, der die Belohnung einlöst, sobald genug gesammelt ist (zum Bestätigen zweimal tippen).",
|
||||
"sticker_chart.editor.sound": "Ton der Aufgabe abspielen",
|
||||
"sticker_chart.editor.title_helper": "Leer lassen, um den Namen jedes Kindes zu verwenden.",
|
||||
"sticker_chart.goal_tile": "Ziel",
|
||||
"sticker_chart.needs_photo": "Diese Aufgabe braucht ein Foto - öffne sie auf der TaskMate-Kinderkarte.",
|
||||
"sticker_chart.no_jobs_today": "Heute keine Aufgaben - genieß den Tag!",
|
||||
"sticker_chart.state.done": "erledigt",
|
||||
"sticker_chart.state.partial": "angefangen",
|
||||
"sticker_chart.state.pending": "wartet auf einen Erwachsenen",
|
||||
"sticker_chart.state.todo": "zu erledigen",
|
||||
"sticker_chart.not_allowed": "Dieses Tablet darf {name}s Aufgaben nicht abhaken. Ein Erwachsener kann das in TaskMate ändern: {name} mit dem Konto dieses Tablets verknüpfen oder dieses Konto als Elternteil eintragen.",
|
||||
"sticker_chart.undo_denied": "Nur ein Erwachsener kann einen Sticker zurücknehmen.",
|
||||
"sticker_chart.undone": "Sticker zurückgenommen.",
|
||||
"visibility.description_hidden": "Durch Sichtbarkeitsbedingung ausgeblendet",
|
||||
"visibility.description_shown": "Wird nach Sichtbarkeitsbedingung angezeigt",
|
||||
"visibility.label_entity": "Sichtbarkeitseinheit",
|
||||
@@ -1980,6 +2036,8 @@
|
||||
"panel.btn_clone_chore": "Duplizieren",
|
||||
"panel.child_link_user_label": "Mit Home-Assistant-Benutzer verknüpfen (optional)",
|
||||
"panel.child_link_user_hint": "Wenn gesetzt, kann nur dieser Home-Assistant-Benutzer (oder ein Admin) per Dienstaufruf Aufgaben erledigen, Belohnungen einlösen oder Timer für dieses Kind starten. Für ein gemeinsames Familien-Tablet auf \"Beliebig\" lassen.",
|
||||
"panel.child_picture_entity_label": "Foto von",
|
||||
"panel.child_picture_entity_hint": "Wähle eine beliebige Entität mit Bild, meist die Person des Kindes, um es anstelle des Symbols oben zu zeigen. Das Foto einer Person wird unter Einstellungen › Personen gesetzt; ohne Bild wird das Symbol angezeigt.",
|
||||
"panel.child_kiosk_pin_label": "Kiosk-PIN",
|
||||
"panel.child_kiosk_pin_hint": "Optional. 4 Ziffern, die das Kind auf einem gemeinsamen Kiosk-Tablet eintippt. Wird als Hash gespeichert und kann nicht ausgelesen werden — leer lassen, um die aktuelle PIN zu behalten.",
|
||||
"panel.child_kiosk_pin_set": "Eine PIN ist gesetzt.",
|
||||
@@ -2085,6 +2143,16 @@
|
||||
"panel.adjust_custom_title": "Punkte von {child} um einen eigenen Betrag anpassen",
|
||||
"panel.adjust_dialog_title": "Punkte anpassen — {name}",
|
||||
"panel.adjust_done": "{sign}{amount} {points} für {child}",
|
||||
"panel.backdate_button": "Erledigte Aufgabe nachtragen",
|
||||
"panel.backdate_title": "Aufgabe für {name} nachtragen",
|
||||
"panel.backdate_hint": "Wähle den Tag und tippe dann die erledigten Aufgaben an, genau wie auf dem Stickerplan. Sie zählen für diesen Tag und diese Woche und sind sofort bestätigt. Einen Sticker gedrückt halten, um ihn zurückzunehmen.",
|
||||
"panel.backdate_hint_list": "Für eine Aufgabe, die an einem früheren Tag erledigt, aber nicht abgehakt wurde. Sie zählt für diesen Tag und diese Woche und ist sofort bestätigt.",
|
||||
"panel.backdate_finish": "Fertig",
|
||||
"panel.backdate_day": "Tag der Erledigung",
|
||||
"panel.backdate_chore": "Aufgabe",
|
||||
"panel.backdate_none": "An diesem Tag war für {name} keine Aufgabe fällig.",
|
||||
"panel.backdate_save": "Nachtragen",
|
||||
"panel.backdate_done": "{chore} für {name} am {day} nachgetragen",
|
||||
"panel.adjust_err_amount": "Gib einen Betrag zwischen 1 und 10000 ein.",
|
||||
"panel.adjust_reason": "Grund (optional)",
|
||||
"panel.adjust_remove": "Abziehen",
|
||||
|
||||
@@ -363,6 +363,8 @@
|
||||
"common.editor.filter_by_child_all": "All Children",
|
||||
"common.editor.header_colour": "Header Colour",
|
||||
"common.editor.header_colour_helper": "Card header background colour",
|
||||
"common.editor.show_adults": "Show adults",
|
||||
"common.editor.show_adults_helper": "Include adult profiles on this card",
|
||||
"common.editor.overview_entity": "Overview Entity",
|
||||
"common.editor.overview_entity_helper": "The TaskMate overview sensor entity",
|
||||
"common.editor.show_parent_actions": "Show parent actions",
|
||||
@@ -778,6 +780,8 @@
|
||||
"panel.child_inverted": "(inverted)",
|
||||
"panel.child_link_user_label": "Link to Home Assistant user (optional)",
|
||||
"panel.child_link_user_hint": "When set, only this Home Assistant user (or an admin) can complete chores, claim rewards, or run timers as this child via service calls. Leave as \"Anyone\" for a shared family tablet.",
|
||||
"panel.child_picture_entity_label": "Photo from",
|
||||
"panel.child_picture_entity_hint": "Pick any entity with a picture, usually the child's person, to show it instead of the icon above. A person's photo is set under Settings › People; with no picture, the icon is shown.",
|
||||
"panel.child_kiosk_pin_label": "Kiosk PIN",
|
||||
"panel.child_kiosk_pin_hint": "Optional. 4 digits this child types on a shared kiosk tablet. Stored hashed, so it can't be read back — leave blank to keep the current PIN.",
|
||||
"panel.child_kiosk_pin_set": "A PIN is set.",
|
||||
@@ -939,6 +943,9 @@
|
||||
"panel.child_age_group_label": "Age group",
|
||||
"panel.child_age_group_none": "Not set",
|
||||
"panel.child_age_group_hint": "Decides which chores the setup wizard suggests. Changes nothing else.",
|
||||
"panel.child_adult_label": "Adult profile",
|
||||
"panel.child_adult_hint": "For a parent or other grown-up. Chores they mark done count straight away with no approval, and cards leave them out unless the card's \"Show adults\" option is on.",
|
||||
"panel.child_adult_pill": "Adult",
|
||||
"wizard.title": "Setup wizard",
|
||||
"wizard.run_btn": "Run setup wizard",
|
||||
"wizard.open_btn": "Open wizard",
|
||||
@@ -1951,6 +1958,57 @@
|
||||
"streak.editor.days_shown_helper": "How many days of dots to show (default: 14)",
|
||||
"streak.editor.title": "Title",
|
||||
"streak.error.entity_required": "Please define an entity",
|
||||
"sticker_chart.error.entity_required": "Please define an entity",
|
||||
"sticker_chart.default_title": "{name}'s Sticker Chart",
|
||||
"sticker_chart.today": "Today: {done} / {total} jobs done",
|
||||
"sticker_chart.all_done": "All done, {name}!",
|
||||
"sticker_chart.day_done_message": "Every job finished today!",
|
||||
"sticker_chart.no_stickers": "No stickers yet - do a chore to earn your first!",
|
||||
"sticker_chart.stickers_alt": "{count} {points} earned",
|
||||
"sticker_chart.goal": "{points} / {goal} {unit} to the reward",
|
||||
"sticker_chart.goal_reached": "GOAL REACHED! Time for your reward!",
|
||||
"sticker_chart.week_goal": "{points} / {goal} {unit} this week",
|
||||
"sticker_chart.week_perfect": "PERFECT WEEK! Every star earned!",
|
||||
"sticker_chart.block_key": "Each block = {count} {unit}",
|
||||
"sticker_chart.day_count": "{day}: {done} / {total} jobs done",
|
||||
"sticker_chart.no_jobs_day": "No jobs were due on {day}.",
|
||||
"sticker_chart.editor.goal": "What the stickers count towards",
|
||||
"sticker_chart.editor.goal_reward": "The reward",
|
||||
"sticker_chart.editor.goal_week": "This week's jobs",
|
||||
"sticker_chart.editor.goal_helper": "This week's jobs: the blocks fill with the stars earned since Monday, out of every star the week's jobs can give. The reward and claim button stay as they are.",
|
||||
"sticker_chart.no_reward": "No reward to save up for yet - pick one in the card settings.",
|
||||
"sticker_chart.no_child": "No children found on this entity",
|
||||
"sticker_chart.editor.title": "Title",
|
||||
"sticker_chart.editor.reward": "Reward to save up for",
|
||||
"sticker_chart.editor.reward_helper": "The reward's cost becomes the goal. Leave on automatic to aim at the cheapest one they haven't earned yet.",
|
||||
"sticker_chart.editor.reward_auto": "Automatic (next one to earn)",
|
||||
"sticker_chart.editor.show_today": "Show today's jobs",
|
||||
"sticker_chart.editor.show_today_helper": "Show the \"Today: x / y jobs done\" line above the stickers.",
|
||||
"sticker_chart.claim": "Claim {reward}",
|
||||
"sticker_chart.claim_confirm": "Tap again to claim {reward}",
|
||||
"sticker_chart.claim_pending": "Waiting for a grown-up to say yes",
|
||||
"sticker_chart.claim_short": "{count} more {unit} to go",
|
||||
"sticker_chart.claimed": "{reward} claimed for {name}!",
|
||||
"sticker_chart.complete_failed": "That didn't work: {error}",
|
||||
"sticker_chart.editor.allow_undo": "Hold a sticker to undo it",
|
||||
"sticker_chart.editor.allow_undo_helper": "Press and hold a sticker to take it back. TaskMate only lets a parent do this.",
|
||||
"sticker_chart.editor.child": "Child",
|
||||
"sticker_chart.editor.child_all": "All children, side by side",
|
||||
"sticker_chart.editor.columns": "Stickers per row",
|
||||
"sticker_chart.editor.show_claim": "Show the claim button",
|
||||
"sticker_chart.editor.show_claim_helper": "A button under the goal that claims the reward once enough is earned (tap twice to confirm).",
|
||||
"sticker_chart.editor.sound": "Play the chore's sound",
|
||||
"sticker_chart.editor.title_helper": "Leave empty to use each child's name.",
|
||||
"sticker_chart.goal_tile": "Goal",
|
||||
"sticker_chart.needs_photo": "This job needs a photo - open it on the TaskMate child card.",
|
||||
"sticker_chart.no_jobs_today": "No jobs today - enjoy your day!",
|
||||
"sticker_chart.state.done": "done",
|
||||
"sticker_chart.state.partial": "started",
|
||||
"sticker_chart.state.pending": "waiting for a grown-up",
|
||||
"sticker_chart.state.todo": "to do",
|
||||
"sticker_chart.not_allowed": "This tablet isn't allowed to tick {name}'s jobs. A grown-up can fix that in TaskMate: link {name} to the account this tablet uses, or add that account as a parent.",
|
||||
"sticker_chart.undo_denied": "Only a grown-up can undo a sticker.",
|
||||
"sticker_chart.undone": "Sticker taken back.",
|
||||
"visibility.description_hidden": "Hidden by visibility condition",
|
||||
"visibility.description_shown": "Shown by visibility condition",
|
||||
"visibility.label_entity": "Visibility Entity",
|
||||
@@ -2085,6 +2143,16 @@
|
||||
"panel.adjust_custom_title": "Adjust {child}'s points by a custom amount",
|
||||
"panel.adjust_dialog_title": "Adjust points — {name}",
|
||||
"panel.adjust_done": "{sign}{amount} {points} for {child}",
|
||||
"panel.backdate_button": "Log a past job",
|
||||
"panel.backdate_title": "Log a past job for {name}",
|
||||
"panel.backdate_hint": "Pick the day, then tap the jobs that were done, just like on the sticker chart. They count for that day and week and are approved straight away. Hold a sticker to take it back.",
|
||||
"panel.backdate_hint_list": "For a job done on an earlier day that nobody ticked. It counts for that day and week, and it's approved straight away.",
|
||||
"panel.backdate_finish": "Done",
|
||||
"panel.backdate_day": "Day it was done",
|
||||
"panel.backdate_chore": "Job",
|
||||
"panel.backdate_none": "No jobs were due for {name} that day.",
|
||||
"panel.backdate_save": "Log it",
|
||||
"panel.backdate_done": "{chore} logged for {name} on {day}",
|
||||
"panel.adjust_err_amount": "Enter an amount between 1 and 10000.",
|
||||
"panel.adjust_reason": "Reason (optional)",
|
||||
"panel.adjust_remove": "Remove",
|
||||
|
||||
@@ -363,6 +363,8 @@
|
||||
"common.editor.filter_by_child_all": "All Children",
|
||||
"common.editor.header_colour": "Header Colour",
|
||||
"common.editor.header_colour_helper": "Card header background colour",
|
||||
"common.editor.show_adults": "Show adults",
|
||||
"common.editor.show_adults_helper": "Include adult profiles on this card",
|
||||
"common.editor.overview_entity": "Overview Entity",
|
||||
"common.editor.overview_entity_helper": "The TaskMate overview sensor entity",
|
||||
"common.editor.show_parent_actions": "Show parent actions",
|
||||
@@ -778,6 +780,8 @@
|
||||
"panel.child_inverted": "(inverted)",
|
||||
"panel.child_link_user_label": "Link to Home Assistant user (optional)",
|
||||
"panel.child_link_user_hint": "When set, only this Home Assistant user (or an admin) can complete chores, claim rewards, or run timers as this child via service calls. Leave as \"Anyone\" for a shared family tablet.",
|
||||
"panel.child_picture_entity_label": "Photo from",
|
||||
"panel.child_picture_entity_hint": "Pick any entity with a picture, usually the child's person, to show it instead of the icon above. A person's photo is set under Settings › People; with no picture, the icon is shown.",
|
||||
"panel.child_kiosk_pin_label": "Kiosk PIN",
|
||||
"panel.child_kiosk_pin_hint": "Optional. 4 digits this child types on a shared kiosk tablet. Stored hashed, so it can't be read back — leave blank to keep the current PIN.",
|
||||
"panel.child_kiosk_pin_set": "A PIN is set.",
|
||||
@@ -939,6 +943,9 @@
|
||||
"panel.child_age_group_label": "Age group",
|
||||
"panel.child_age_group_none": "Not set",
|
||||
"panel.child_age_group_hint": "Decides which chores the setup wizard suggests. Changes nothing else.",
|
||||
"panel.child_adult_label": "Adult profile",
|
||||
"panel.child_adult_hint": "For a parent or other grown-up. Chores they mark done count straight away with no approval, and cards leave them out unless the card's \"Show adults\" option is on.",
|
||||
"panel.child_adult_pill": "Adult",
|
||||
"wizard.title": "Setup wizard",
|
||||
"wizard.run_btn": "Run setup wizard",
|
||||
"wizard.open_btn": "Open wizard",
|
||||
@@ -1951,6 +1958,57 @@
|
||||
"streak.editor.days_shown_helper": "How many days of dots to show (default: 14)",
|
||||
"streak.editor.title": "Title",
|
||||
"streak.error.entity_required": "Please define an entity",
|
||||
"sticker_chart.error.entity_required": "Please define an entity",
|
||||
"sticker_chart.default_title": "{name}'s Sticker Chart",
|
||||
"sticker_chart.today": "Today: {done} / {total} jobs done",
|
||||
"sticker_chart.all_done": "All done, {name}!",
|
||||
"sticker_chart.day_done_message": "Every job finished today!",
|
||||
"sticker_chart.no_stickers": "No stickers yet - do a chore to earn your first!",
|
||||
"sticker_chart.stickers_alt": "{count} {points} earned",
|
||||
"sticker_chart.goal": "{points} / {goal} {unit} to the reward",
|
||||
"sticker_chart.goal_reached": "GOAL REACHED! Time for your reward!",
|
||||
"sticker_chart.week_goal": "{points} / {goal} {unit} this week",
|
||||
"sticker_chart.week_perfect": "PERFECT WEEK! Every star earned!",
|
||||
"sticker_chart.block_key": "Each block = {count} {unit}",
|
||||
"sticker_chart.day_count": "{day}: {done} / {total} jobs done",
|
||||
"sticker_chart.no_jobs_day": "No jobs were due on {day}.",
|
||||
"sticker_chart.editor.goal": "What the stickers count towards",
|
||||
"sticker_chart.editor.goal_reward": "The reward",
|
||||
"sticker_chart.editor.goal_week": "This week's jobs",
|
||||
"sticker_chart.editor.goal_helper": "This week's jobs: the blocks fill with the stars earned since Monday, out of every star the week's jobs can give. The reward and claim button stay as they are.",
|
||||
"sticker_chart.no_reward": "No reward to save up for yet - pick one in the card settings.",
|
||||
"sticker_chart.no_child": "No children found on this entity",
|
||||
"sticker_chart.editor.title": "Title",
|
||||
"sticker_chart.editor.reward": "Reward to save up for",
|
||||
"sticker_chart.editor.reward_helper": "The reward's cost becomes the goal. Leave on automatic to aim at the cheapest one they haven't earned yet.",
|
||||
"sticker_chart.editor.reward_auto": "Automatic (next one to earn)",
|
||||
"sticker_chart.editor.show_today": "Show today's jobs",
|
||||
"sticker_chart.editor.show_today_helper": "Show the \"Today: x / y jobs done\" line above the stickers.",
|
||||
"sticker_chart.claim": "Claim {reward}",
|
||||
"sticker_chart.claim_confirm": "Tap again to claim {reward}",
|
||||
"sticker_chart.claim_pending": "Waiting for a grown-up to say yes",
|
||||
"sticker_chart.claim_short": "{count} more {unit} to go",
|
||||
"sticker_chart.claimed": "{reward} claimed for {name}!",
|
||||
"sticker_chart.complete_failed": "That didn't work: {error}",
|
||||
"sticker_chart.editor.allow_undo": "Hold a sticker to undo it",
|
||||
"sticker_chart.editor.allow_undo_helper": "Press and hold a sticker to take it back. TaskMate only lets a parent do this.",
|
||||
"sticker_chart.editor.child": "Child",
|
||||
"sticker_chart.editor.child_all": "All children, side by side",
|
||||
"sticker_chart.editor.columns": "Stickers per row",
|
||||
"sticker_chart.editor.show_claim": "Show the claim button",
|
||||
"sticker_chart.editor.show_claim_helper": "A button under the goal that claims the reward once enough is earned (tap twice to confirm).",
|
||||
"sticker_chart.editor.sound": "Play the chore's sound",
|
||||
"sticker_chart.editor.title_helper": "Leave empty to use each child's name.",
|
||||
"sticker_chart.goal_tile": "Goal",
|
||||
"sticker_chart.needs_photo": "This job needs a photo - open it on the TaskMate child card.",
|
||||
"sticker_chart.no_jobs_today": "No jobs today - enjoy your day!",
|
||||
"sticker_chart.state.done": "done",
|
||||
"sticker_chart.state.partial": "started",
|
||||
"sticker_chart.state.pending": "waiting for a grown-up",
|
||||
"sticker_chart.state.todo": "to do",
|
||||
"sticker_chart.not_allowed": "This tablet isn't allowed to tick {name}'s jobs. A grown-up can fix that in TaskMate: link {name} to the account this tablet uses, or add that account as a parent.",
|
||||
"sticker_chart.undo_denied": "Only a grown-up can undo a sticker.",
|
||||
"sticker_chart.undone": "Sticker taken back.",
|
||||
"visibility.description_hidden": "Hidden by visibility condition",
|
||||
"visibility.description_shown": "Shown by visibility condition",
|
||||
"visibility.label_entity": "Visibility Entity",
|
||||
@@ -2085,6 +2143,16 @@
|
||||
"panel.adjust_custom_title": "Adjust {child}'s points by a custom amount",
|
||||
"panel.adjust_dialog_title": "Adjust points — {name}",
|
||||
"panel.adjust_done": "{sign}{amount} {points} for {child}",
|
||||
"panel.backdate_button": "Log a past job",
|
||||
"panel.backdate_title": "Log a past job for {name}",
|
||||
"panel.backdate_hint": "Pick the day, then tap the jobs that were done, just like on the sticker chart. They count for that day and week and are approved straight away. Hold a sticker to take it back.",
|
||||
"panel.backdate_hint_list": "For a job done on an earlier day that nobody ticked. It counts for that day and week, and it's approved straight away.",
|
||||
"panel.backdate_finish": "Done",
|
||||
"panel.backdate_day": "Day it was done",
|
||||
"panel.backdate_chore": "Job",
|
||||
"panel.backdate_none": "No jobs were due for {name} that day.",
|
||||
"panel.backdate_save": "Log it",
|
||||
"panel.backdate_done": "{chore} logged for {name} on {day}",
|
||||
"panel.adjust_err_amount": "Enter an amount between 1 and 10000.",
|
||||
"panel.adjust_reason": "Reason (optional)",
|
||||
"panel.adjust_remove": "Remove",
|
||||
|
||||
@@ -353,6 +353,8 @@
|
||||
"common.editor.filter_by_child_all": "Tous les enfants",
|
||||
"common.editor.header_colour": "Couleur de l'en-tête",
|
||||
"common.editor.header_colour_helper": "Couleur d'arrière-plan de l'en-tête de la carte",
|
||||
"common.editor.show_adults": "Afficher les adultes",
|
||||
"common.editor.show_adults_helper": "Inclure les profils adultes sur cette carte",
|
||||
"common.editor.overview_entity": "Entité de vue d'ensemble",
|
||||
"common.editor.overview_entity_helper": "L'entité du capteur de vue d'ensemble TaskMate",
|
||||
"common.editor.show_parent_actions": "Afficher les actions parentales",
|
||||
@@ -889,6 +891,9 @@
|
||||
"panel.child_age_group_label": "Tranche d'âge",
|
||||
"panel.child_age_group_none": "Non définie",
|
||||
"panel.child_age_group_hint": "Détermine les tâches suggérées par l'assistant de configuration. Ne change rien d'autre.",
|
||||
"panel.child_adult_label": "Profil adulte",
|
||||
"panel.child_adult_hint": "Pour un parent ou un autre adulte. Les tâches qu'il marque comme faites comptent tout de suite, sans validation, et les cartes ne l'affichent que si l'option « Afficher les adultes » de la carte est activée.",
|
||||
"panel.child_adult_pill": "Adulte",
|
||||
"wizard.title": "Assistant de configuration",
|
||||
"wizard.run_btn": "Lancer l'assistant de configuration",
|
||||
"wizard.open_btn": "Ouvrir l'assistant",
|
||||
@@ -1886,6 +1891,57 @@
|
||||
"streak.editor.days_shown_helper": "Combien de jours de points afficher (par défaut : 14)",
|
||||
"streak.editor.title": "Titre",
|
||||
"streak.error.entity_required": "Veuillez définir une entité",
|
||||
"sticker_chart.error.entity_required": "Veuillez définir une entité",
|
||||
"sticker_chart.default_title": "Tableau d'autocollants de {name}",
|
||||
"sticker_chart.today": "Aujourd'hui : {done} / {total} tâches faites",
|
||||
"sticker_chart.all_done": "Tout est fait, {name} !",
|
||||
"sticker_chart.day_done_message": "Toutes les tâches du jour sont faites !",
|
||||
"sticker_chart.no_stickers": "Pas encore d'autocollants - fais une tâche pour gagner le premier !",
|
||||
"sticker_chart.stickers_alt": "{count} {points} gagnées",
|
||||
"sticker_chart.goal": "{points} / {goal} {unit} avant la récompense",
|
||||
"sticker_chart.goal_reached": "OBJECTIF ATTEINT ! C'est l'heure de la récompense !",
|
||||
"sticker_chart.week_goal": "{points} / {goal} {unit} cette semaine",
|
||||
"sticker_chart.week_perfect": "SEMAINE PARFAITE ! Toutes les étoiles gagnées !",
|
||||
"sticker_chart.block_key": "Chaque case = {count} {unit}",
|
||||
"sticker_chart.day_count": "{day} : {done} / {total} tâches faites",
|
||||
"sticker_chart.no_jobs_day": "Aucune tâche n'était prévue le {day}.",
|
||||
"sticker_chart.editor.goal": "Ce que comptent les autocollants",
|
||||
"sticker_chart.editor.goal_reward": "La récompense",
|
||||
"sticker_chart.editor.goal_week": "Les tâches de la semaine",
|
||||
"sticker_chart.editor.goal_helper": "Tâches de la semaine : les cases se remplissent avec les étoiles gagnées depuis lundi, sur toutes celles que les tâches de la semaine peuvent rapporter. La récompense et son bouton ne changent pas.",
|
||||
"sticker_chart.no_reward": "Aucune récompense à viser pour l'instant - choisissez-en une dans les réglages de la carte.",
|
||||
"sticker_chart.no_child": "Aucun enfant trouvé pour cette entité",
|
||||
"sticker_chart.editor.title": "Titre",
|
||||
"sticker_chart.editor.reward": "Récompense à viser",
|
||||
"sticker_chart.editor.reward_helper": "Le coût de la récompense devient l'objectif. Laissez sur automatique pour viser la moins chère pas encore gagnée.",
|
||||
"sticker_chart.editor.reward_auto": "Automatique (la prochaine à gagner)",
|
||||
"sticker_chart.editor.show_today": "Afficher les tâches du jour",
|
||||
"sticker_chart.editor.show_today_helper": "Affiche la ligne « Aujourd'hui : x / y tâches faites » au-dessus des autocollants.",
|
||||
"sticker_chart.claim": "Obtenir {reward}",
|
||||
"sticker_chart.claim_confirm": "Touchez encore pour obtenir {reward}",
|
||||
"sticker_chart.claim_pending": "En attente de l'accord d'un adulte",
|
||||
"sticker_chart.claim_short": "Encore {count} {unit}",
|
||||
"sticker_chart.claimed": "{reward} obtenu pour {name} !",
|
||||
"sticker_chart.complete_failed": "Ça n'a pas marché : {error}",
|
||||
"sticker_chart.editor.allow_undo": "Maintenir un autocollant pour l'annuler",
|
||||
"sticker_chart.editor.allow_undo_helper": "Maintenez un autocollant appuyé pour le reprendre. TaskMate ne l'autorise qu'aux parents.",
|
||||
"sticker_chart.editor.child": "Enfant",
|
||||
"sticker_chart.editor.child_all": "Tous les enfants côte à côte",
|
||||
"sticker_chart.editor.columns": "Autocollants par ligne",
|
||||
"sticker_chart.editor.show_claim": "Afficher le bouton de récompense",
|
||||
"sticker_chart.editor.show_claim_helper": "Un bouton sous l'objectif qui obtient la récompense une fois assez gagné (touchez deux fois pour confirmer).",
|
||||
"sticker_chart.editor.sound": "Jouer le son de la tâche",
|
||||
"sticker_chart.editor.title_helper": "Laissez vide pour utiliser le prénom de chaque enfant.",
|
||||
"sticker_chart.goal_tile": "Objectif",
|
||||
"sticker_chart.needs_photo": "Cette tâche demande une photo - ouvrez-la sur la carte enfant TaskMate.",
|
||||
"sticker_chart.no_jobs_today": "Pas de tâches aujourd'hui - profite de ta journée !",
|
||||
"sticker_chart.state.done": "fait",
|
||||
"sticker_chart.state.partial": "commencé",
|
||||
"sticker_chart.state.pending": "attend un adulte",
|
||||
"sticker_chart.state.todo": "à faire",
|
||||
"sticker_chart.not_allowed": "Cette tablette n'a pas le droit de cocher les tâches de {name}. Un adulte peut le corriger dans TaskMate : associer {name} au compte de cette tablette, ou ajouter ce compte comme parent.",
|
||||
"sticker_chart.undo_denied": "Seul un adulte peut reprendre un autocollant.",
|
||||
"sticker_chart.undone": "Autocollant repris.",
|
||||
"visibility.description_hidden": "Masqué par la condition de visibilité",
|
||||
"visibility.description_shown": "Affiché par la condition de visibilité",
|
||||
"visibility.label_entity": "Entité de visibilité",
|
||||
@@ -1980,6 +2036,8 @@
|
||||
"panel.btn_clone_chore": "Dupliquer",
|
||||
"panel.child_link_user_label": "Lier à un utilisateur Home Assistant (facultatif)",
|
||||
"panel.child_link_user_hint": "Si défini, seul cet utilisateur Home Assistant (ou un admin) peut terminer des tâches, échanger des récompenses ou lancer des minuteurs pour cet enfant via les services. Laissez sur « N'importe qui » pour une tablette familiale partagée.",
|
||||
"panel.child_picture_entity_label": "Photo de",
|
||||
"panel.child_picture_entity_hint": "Choisissez n'importe quelle entité avec une image, généralement la personne de l'enfant, pour l'afficher à la place de l'icône ci-dessus. La photo d'une personne se définit dans Paramètres › Personnes ; sans image, l'icône est affichée.",
|
||||
"panel.child_kiosk_pin_label": "Code PIN du kiosque",
|
||||
"panel.child_kiosk_pin_hint": "Facultatif. 4 chiffres que l'enfant tape sur une tablette kiosque partagée. Stocké haché, il ne peut pas être relu — laissez vide pour garder le code actuel.",
|
||||
"panel.child_kiosk_pin_set": "Un code PIN est défini.",
|
||||
@@ -2085,6 +2143,16 @@
|
||||
"panel.adjust_custom_title": "Ajuster les points de {child} d'un montant personnalisé",
|
||||
"panel.adjust_dialog_title": "Ajuster les points — {name}",
|
||||
"panel.adjust_done": "{sign}{amount} {points} pour {child}",
|
||||
"panel.backdate_button": "Ajouter une tâche passée",
|
||||
"panel.backdate_title": "Ajouter une tâche passée pour {name}",
|
||||
"panel.backdate_hint": "Choisissez le jour, puis touchez les tâches faites, comme sur le tableau d'autocollants. Elles comptent pour ce jour et cette semaine, et sont validées tout de suite. Maintenez un autocollant pour l'annuler.",
|
||||
"panel.backdate_hint_list": "Pour une tâche faite un jour précédent mais jamais cochée. Elle compte pour ce jour et cette semaine, et elle est validée tout de suite.",
|
||||
"panel.backdate_finish": "Terminé",
|
||||
"panel.backdate_day": "Jour où elle a été faite",
|
||||
"panel.backdate_chore": "Tâche",
|
||||
"panel.backdate_none": "Aucune tâche n'était prévue pour {name} ce jour-là.",
|
||||
"panel.backdate_save": "Ajouter",
|
||||
"panel.backdate_done": "{chore} ajoutée pour {name} le {day}",
|
||||
"panel.adjust_err_amount": "Saisissez un montant entre 1 et 10000.",
|
||||
"panel.adjust_reason": "Motif (facultatif)",
|
||||
"panel.adjust_remove": "Retirer",
|
||||
|
||||
@@ -353,6 +353,8 @@
|
||||
"common.editor.filter_by_child_all": "Alle barn",
|
||||
"common.editor.header_colour": "Toppfarge",
|
||||
"common.editor.header_colour_helper": "Bakgrunnsfarge for korttoppen",
|
||||
"common.editor.show_adults": "Vis voksne",
|
||||
"common.editor.show_adults_helper": "Ta med voksenprofiler på dette kortet",
|
||||
"common.editor.overview_entity": "Oversiktsentitet",
|
||||
"common.editor.overview_entity_helper": "TaskMate oversiktssensor-entiteten",
|
||||
"common.editor.show_parent_actions": "Vis foreldrehandlinger",
|
||||
@@ -889,6 +891,9 @@
|
||||
"panel.child_age_group_label": "Aldersgruppe",
|
||||
"panel.child_age_group_none": "Ikke valgt",
|
||||
"panel.child_age_group_hint": "Bestemmer hvilke oppgaver oppsettveiviseren foreslår. Endrer ikke noe annet.",
|
||||
"panel.child_adult_label": "Voksenprofil",
|
||||
"panel.child_adult_hint": "For en forelder eller en annen voksen. Oppgaver de markerer som gjort, teller med en gang uten godkjenning, og kort viser dem bare når kortets valg «Vis voksne» er slått på.",
|
||||
"panel.child_adult_pill": "Voksen",
|
||||
"wizard.title": "Oppsettveiviser",
|
||||
"wizard.run_btn": "Kjør oppsettveiviseren",
|
||||
"wizard.open_btn": "Åpne veiviseren",
|
||||
@@ -1886,6 +1891,57 @@
|
||||
"streak.editor.days_shown_helper": "Hvor mange dager med prikker som skal vises (standard: 14)",
|
||||
"streak.editor.title": "Tittel",
|
||||
"streak.error.entity_required": "Vennligst definer en entitet",
|
||||
"sticker_chart.error.entity_required": "Vennligst definer en entitet",
|
||||
"sticker_chart.default_title": "{name} sin klistremerketavle",
|
||||
"sticker_chart.today": "I dag: {done} / {total} oppgaver gjort",
|
||||
"sticker_chart.all_done": "Alt er gjort, {name}!",
|
||||
"sticker_chart.day_done_message": "Alle dagens oppgaver er gjort!",
|
||||
"sticker_chart.no_stickers": "Ingen klistremerker ennå - gjør en oppgave for å få det første!",
|
||||
"sticker_chart.stickers_alt": "{count} {points} opptjent",
|
||||
"sticker_chart.goal": "{points} / {goal} {unit} til belønningen",
|
||||
"sticker_chart.goal_reached": "MÅLET ER NÅDD! På tide med belønning!",
|
||||
"sticker_chart.week_goal": "{points} / {goal} {unit} denne uken",
|
||||
"sticker_chart.week_perfect": "PERFEKT UKE! Hver eneste stjerne tjent!",
|
||||
"sticker_chart.block_key": "Hver rute = {count} {unit}",
|
||||
"sticker_chart.day_count": "{day}: {done} / {total} oppgaver gjort",
|
||||
"sticker_chart.no_jobs_day": "Ingen oppgaver den {day}.",
|
||||
"sticker_chart.editor.goal": "Hva klistremerkene teller mot",
|
||||
"sticker_chart.editor.goal_reward": "Belønningen",
|
||||
"sticker_chart.editor.goal_week": "Ukens oppgaver",
|
||||
"sticker_chart.editor.goal_helper": "Ukens oppgaver: rutene fylles med stjernene tjent siden mandag, av alle stjernene ukens oppgaver kan gi. Belønningen og knappen for å hente den er som før.",
|
||||
"sticker_chart.no_reward": "Ingen belønning å spare til ennå - velg en i kortinnstillingene.",
|
||||
"sticker_chart.no_child": "Fant ingen barn på denne entiteten",
|
||||
"sticker_chart.editor.title": "Tittel",
|
||||
"sticker_chart.editor.reward": "Belønning å spare til",
|
||||
"sticker_chart.editor.reward_helper": "Prisen på belønningen blir målet. La stå på automatisk for å sikte mot den billigste som ikke er opptjent ennå.",
|
||||
"sticker_chart.editor.reward_auto": "Automatisk (den neste å tjene)",
|
||||
"sticker_chart.editor.show_today": "Vis dagens oppgaver",
|
||||
"sticker_chart.editor.show_today_helper": "Viser linjen «I dag: x / y oppgaver gjort» over klistremerkene.",
|
||||
"sticker_chart.claim": "Hent {reward}",
|
||||
"sticker_chart.claim_confirm": "Trykk igjen for å hente {reward}",
|
||||
"sticker_chart.claim_pending": "Venter på at en voksen sier ja",
|
||||
"sticker_chart.claim_short": "{count} {unit} igjen",
|
||||
"sticker_chart.claimed": "{reward} hentet for {name}!",
|
||||
"sticker_chart.complete_failed": "Det gikk ikke: {error}",
|
||||
"sticker_chart.editor.allow_undo": "Hold inne et klistremerke for å angre",
|
||||
"sticker_chart.editor.allow_undo_helper": "Hold inne et klistremerke for å ta det tilbake. TaskMate lar bare foreldre gjøre dette.",
|
||||
"sticker_chart.editor.child": "Barn",
|
||||
"sticker_chart.editor.child_all": "Alle barn side om side",
|
||||
"sticker_chart.editor.columns": "Klistremerker per rad",
|
||||
"sticker_chart.editor.show_claim": "Vis hent-knappen",
|
||||
"sticker_chart.editor.show_claim_helper": "En knapp under målet som henter belønningen når nok er opptjent (trykk to ganger for å bekrefte).",
|
||||
"sticker_chart.editor.sound": "Spill oppgavens lyd",
|
||||
"sticker_chart.editor.title_helper": "La stå tom for å bruke navnet til hvert barn.",
|
||||
"sticker_chart.goal_tile": "Mål",
|
||||
"sticker_chart.needs_photo": "Denne oppgaven trenger et bilde - åpne den på TaskMate-barnekortet.",
|
||||
"sticker_chart.no_jobs_today": "Ingen oppgaver i dag - kos deg!",
|
||||
"sticker_chart.state.done": "gjort",
|
||||
"sticker_chart.state.partial": "påbegynt",
|
||||
"sticker_chart.state.pending": "venter på en voksen",
|
||||
"sticker_chart.state.todo": "ikke gjort",
|
||||
"sticker_chart.not_allowed": "Dette nettbrettet får ikke krysse av oppgavene til {name}. En voksen kan fikse det i TaskMate: koble {name} til kontoen nettbrettet bruker, eller legge til den kontoen som forelder.",
|
||||
"sticker_chart.undo_denied": "Bare en voksen kan ta tilbake et klistremerke.",
|
||||
"sticker_chart.undone": "Klistremerket er tatt tilbake.",
|
||||
"visibility.description_hidden": "Skjult av synlighetsbetingelse",
|
||||
"visibility.description_shown": "Vist av synlighetsbetingelse",
|
||||
"visibility.label_entity": "Synlighetsentitet",
|
||||
@@ -1980,6 +2036,8 @@
|
||||
"panel.btn_clone_chore": "Dupliser",
|
||||
"panel.child_link_user_label": "Koble til Home Assistant-bruker (valgfritt)",
|
||||
"panel.child_link_user_hint": "Når satt, kan bare denne Home Assistant-brukeren (eller en admin) fullføre gjøremål, løse inn belønninger eller starte tidtakere for dette barnet via tjenestekall. La stå på «Hvem som helst» for et delt familiebrett.",
|
||||
"panel.child_picture_entity_label": "Bilde fra",
|
||||
"panel.child_picture_entity_hint": "Velg en entitet med bilde, vanligvis barnets person, for å vise det i stedet for ikonet over. Bildet til en person settes under Innstillinger › Personer; uten bilde vises ikonet.",
|
||||
"panel.child_kiosk_pin_label": "Kiosk-PIN",
|
||||
"panel.child_kiosk_pin_hint": "Valgfritt. 4 sifre barnet taster på et delt kiosk-nettbrett. Lagres som hash og kan ikke leses tilbake — la feltet stå tomt for å beholde gjeldende kode.",
|
||||
"panel.child_kiosk_pin_set": "En PIN-kode er satt.",
|
||||
@@ -2085,6 +2143,16 @@
|
||||
"panel.adjust_custom_title": "Juster poengene til {child} med et eget beløp",
|
||||
"panel.adjust_dialog_title": "Juster poeng — {name}",
|
||||
"panel.adjust_done": "{sign}{amount} {points} til {child}",
|
||||
"panel.backdate_button": "Før opp en tidligere oppgave",
|
||||
"panel.backdate_title": "Før opp en tidligere oppgave for {name}",
|
||||
"panel.backdate_hint": "Velg dagen, og trykk så på oppgavene som ble gjort, akkurat som på klistremerketavla. De teller for den dagen og uken, og godkjennes med en gang. Hold inne et klistremerke for å ta det tilbake.",
|
||||
"panel.backdate_hint_list": "For en oppgave som ble gjort en tidligere dag, men ikke krysset av. Den teller for den dagen og uken, og godkjennes med en gang.",
|
||||
"panel.backdate_finish": "Ferdig",
|
||||
"panel.backdate_day": "Dagen den ble gjort",
|
||||
"panel.backdate_chore": "Oppgave",
|
||||
"panel.backdate_none": "{name} hadde ingen oppgaver den dagen.",
|
||||
"panel.backdate_save": "Før opp",
|
||||
"panel.backdate_done": "{chore} ført opp for {name} {day}",
|
||||
"panel.adjust_err_amount": "Skriv inn et beløp mellom 1 og 10000.",
|
||||
"panel.adjust_reason": "Årsak (valgfritt)",
|
||||
"panel.adjust_remove": "Trekk fra",
|
||||
|
||||
@@ -353,6 +353,8 @@
|
||||
"common.editor.filter_by_child_all": "Alle born",
|
||||
"common.editor.header_colour": "Toppfarge",
|
||||
"common.editor.header_colour_helper": "Bakgrunnsfarge for korttoppen",
|
||||
"common.editor.show_adults": "Vis vaksne",
|
||||
"common.editor.show_adults_helper": "Ta med vaksenprofilar på dette kortet",
|
||||
"common.editor.overview_entity": "Oversiktsentitet",
|
||||
"common.editor.overview_entity_helper": "TaskMate oversiktssensor-entiteten",
|
||||
"common.editor.show_parent_actions": "Vis foreldrehandlingar",
|
||||
@@ -889,6 +891,9 @@
|
||||
"panel.child_age_group_label": "Aldersgruppe",
|
||||
"panel.child_age_group_none": "Ikkje vald",
|
||||
"panel.child_age_group_hint": "Avgjer kva oppgåver oppsettsvegvisaren føreslår. Endrar ikkje noko anna.",
|
||||
"panel.child_adult_label": "Vaksenprofil",
|
||||
"panel.child_adult_hint": "For ein forelder eller ein annan vaksen. Oppgåver dei markerer som gjort, tel med ein gong utan godkjenning, og kort viser dei berre når kortet sitt val «Vis vaksne» er slått på.",
|
||||
"panel.child_adult_pill": "Vaksen",
|
||||
"wizard.title": "Oppsettsvegvisar",
|
||||
"wizard.run_btn": "Køyr oppsettsvegvisaren",
|
||||
"wizard.open_btn": "Opne vegvisaren",
|
||||
@@ -1886,6 +1891,57 @@
|
||||
"streak.editor.days_shown_helper": "Kor mange dagar med prikkar som skal visast (standard: 14)",
|
||||
"streak.editor.title": "Tittel",
|
||||
"streak.error.entity_required": "Ver venleg og definer ein entitet",
|
||||
"sticker_chart.error.entity_required": "Ver venleg og definer ein entitet",
|
||||
"sticker_chart.default_title": "{name} si klistremerketavle",
|
||||
"sticker_chart.today": "I dag: {done} / {total} oppgåver gjorde",
|
||||
"sticker_chart.all_done": "Alt er gjort, {name}!",
|
||||
"sticker_chart.day_done_message": "Alle oppgåvene i dag er gjorde!",
|
||||
"sticker_chart.no_stickers": "Ingen klistremerke enno - gjer ei oppgåve for å få det første!",
|
||||
"sticker_chart.stickers_alt": "{count} {points} opptente",
|
||||
"sticker_chart.goal": "{points} / {goal} {unit} til løninga",
|
||||
"sticker_chart.goal_reached": "MÅLET ER NÅDD! På tide med løn!",
|
||||
"sticker_chart.week_goal": "{points} / {goal} {unit} denne veka",
|
||||
"sticker_chart.week_perfect": "PERFEKT VEKE! Kvar einaste stjerne tent!",
|
||||
"sticker_chart.block_key": "Kvar rute = {count} {unit}",
|
||||
"sticker_chart.day_count": "{day}: {done} / {total} oppgåver gjorde",
|
||||
"sticker_chart.no_jobs_day": "Ingen oppgåver den {day}.",
|
||||
"sticker_chart.editor.goal": "Kva klistremerka tel mot",
|
||||
"sticker_chart.editor.goal_reward": "Løna",
|
||||
"sticker_chart.editor.goal_week": "Oppgåvene denne veka",
|
||||
"sticker_chart.editor.goal_helper": "Oppgåvene denne veka: rutene blir fylte med stjernene tente sidan måndag, av alle stjernene oppgåvene i veka kan gi. Løna og knappen for å hente henne er som før.",
|
||||
"sticker_chart.no_reward": "Inga løn å spare til enno - vel ei i kortinnstillingane.",
|
||||
"sticker_chart.no_child": "Fann ingen born på denne entiteten",
|
||||
"sticker_chart.editor.title": "Tittel",
|
||||
"sticker_chart.editor.reward": "Løn å spare til",
|
||||
"sticker_chart.editor.reward_helper": "Prisen på løna blir målet. Lat stå på automatisk for å sikte mot den billegaste som ikkje er opptent enno.",
|
||||
"sticker_chart.editor.reward_auto": "Automatisk (den neste å tene)",
|
||||
"sticker_chart.editor.show_today": "Vis oppgåvene i dag",
|
||||
"sticker_chart.editor.show_today_helper": "Viser linja «I dag: x / y oppgåver gjorde» over klistremerka.",
|
||||
"sticker_chart.claim": "Hent {reward}",
|
||||
"sticker_chart.claim_confirm": "Trykk igjen for å hente {reward}",
|
||||
"sticker_chart.claim_pending": "Ventar på at ein vaksen seier ja",
|
||||
"sticker_chart.claim_short": "{count} {unit} att",
|
||||
"sticker_chart.claimed": "{reward} henta for {name}!",
|
||||
"sticker_chart.complete_failed": "Det gjekk ikkje: {error}",
|
||||
"sticker_chart.editor.allow_undo": "Hald inne eit klistremerke for å angre",
|
||||
"sticker_chart.editor.allow_undo_helper": "Hald inne eit klistremerke for å ta det tilbake. TaskMate lèt berre foreldre gjere dette.",
|
||||
"sticker_chart.editor.child": "Barn",
|
||||
"sticker_chart.editor.child_all": "Alle born side om side",
|
||||
"sticker_chart.editor.columns": "Klistremerke per rad",
|
||||
"sticker_chart.editor.show_claim": "Vis hent-knappen",
|
||||
"sticker_chart.editor.show_claim_helper": "Ein knapp under målet som hentar løna når nok er opptent (trykk to gonger for å stadfeste).",
|
||||
"sticker_chart.editor.sound": "Spel lyden til oppgåva",
|
||||
"sticker_chart.editor.title_helper": "La stå tom for å bruke namnet til kvart barn.",
|
||||
"sticker_chart.goal_tile": "Mål",
|
||||
"sticker_chart.needs_photo": "Denne oppgåva treng eit bilete - opne ho på TaskMate-barnekortet.",
|
||||
"sticker_chart.no_jobs_today": "Ingen oppgåver i dag - kos deg!",
|
||||
"sticker_chart.state.done": "gjort",
|
||||
"sticker_chart.state.partial": "byrja",
|
||||
"sticker_chart.state.pending": "ventar på ein vaksen",
|
||||
"sticker_chart.state.todo": "ikkje gjort",
|
||||
"sticker_chart.not_allowed": "Dette nettbrettet får ikkje krysse av oppgåvene til {name}. Ein vaksen kan fikse det i TaskMate: kople {name} til kontoen nettbrettet brukar, eller leggje til den kontoen som forelder.",
|
||||
"sticker_chart.undo_denied": "Berre ein vaksen kan ta tilbake eit klistremerke.",
|
||||
"sticker_chart.undone": "Klistremerket er teke tilbake.",
|
||||
"visibility.description_hidden": "Skjult av synlegheitskondisjon",
|
||||
"visibility.description_shown": "Vist av synlegheitskondisjon",
|
||||
"visibility.label_entity": "Synlegheitsentitet",
|
||||
@@ -1980,6 +2036,8 @@
|
||||
"panel.btn_clone_chore": "Dupliser",
|
||||
"panel.child_link_user_label": "Kople til Home Assistant-brukar (valfritt)",
|
||||
"panel.child_link_user_hint": "Når sett, kan berre denne Home Assistant-brukaren (eller ein admin) fullføre gjeremål, løyse inn løningar eller starte tidtakarar for dette barnet via tenestekall. Lat stå på «Kven som helst» for eit delt familiebrett.",
|
||||
"panel.child_picture_entity_label": "Bilete frå",
|
||||
"panel.child_picture_entity_hint": "Vel ein entitet med bilete, vanlegvis personen til barnet, for å vise det i staden for ikonet over. Biletet til ein person vert sett under Innstillingar › Personar; utan bilete vert ikonet vist.",
|
||||
"panel.child_kiosk_pin_label": "Kiosk-PIN",
|
||||
"panel.child_kiosk_pin_hint": "Valfritt. 4 siffer barnet tastar på eit delt kiosk-nettbrett. Vert lagra som hash og kan ikkje lesast tilbake — la feltet stå tomt for å halde på gjeldande kode.",
|
||||
"panel.child_kiosk_pin_set": "Ein PIN-kode er sett.",
|
||||
@@ -2085,6 +2143,16 @@
|
||||
"panel.adjust_custom_title": "Juster poenga til {child} med eit eige beløp",
|
||||
"panel.adjust_dialog_title": "Juster poeng — {name}",
|
||||
"panel.adjust_done": "{sign}{amount} {points} til {child}",
|
||||
"panel.backdate_button": "Før opp ei tidlegare oppgåve",
|
||||
"panel.backdate_title": "Før opp ei tidlegare oppgåve for {name}",
|
||||
"panel.backdate_hint": "Vel dagen, og trykk så på oppgåvene som vart gjorde, akkurat som på klistremerketavla. Dei tel for den dagen og veka, og blir godkjende med ein gong. Hald inne eit klistremerke for å ta det tilbake.",
|
||||
"panel.backdate_hint_list": "For ei oppgåve som vart gjord ein tidlegare dag, men ikkje kryssa av. Ho tel for den dagen og veka, og blir godkjend med ein gong.",
|
||||
"panel.backdate_finish": "Ferdig",
|
||||
"panel.backdate_day": "Dagen ho vart gjord",
|
||||
"panel.backdate_chore": "Oppgåve",
|
||||
"panel.backdate_none": "{name} hadde ingen oppgåver den dagen.",
|
||||
"panel.backdate_save": "Før opp",
|
||||
"panel.backdate_done": "{chore} ført opp for {name} {day}",
|
||||
"panel.adjust_err_amount": "Skriv inn eit beløp mellom 1 og 10000.",
|
||||
"panel.adjust_reason": "Årsak (valfritt)",
|
||||
"panel.adjust_remove": "Trekk frå",
|
||||
|
||||
@@ -363,6 +363,8 @@
|
||||
"common.editor.filter_by_child_all": "Wszystkie dzieci",
|
||||
"common.editor.header_colour": "Kolor nagłówka",
|
||||
"common.editor.header_colour_helper": "Kolor tła nagłówka karty",
|
||||
"common.editor.show_adults": "Pokaż dorosłych",
|
||||
"common.editor.show_adults_helper": "Uwzględnij profile dorosłych na tej karcie",
|
||||
"common.editor.overview_entity": "Encja przeglądu",
|
||||
"common.editor.overview_entity_helper": "Encja sensora przeglądu TaskMate",
|
||||
"common.editor.show_parent_actions": "Pokaż działania rodzica",
|
||||
@@ -778,6 +780,8 @@
|
||||
"panel.child_inverted": "(odwrócone)",
|
||||
"panel.child_link_user_label": "Powiąż z użytkownikiem Home Assistant (opcjonalnie)",
|
||||
"panel.child_link_user_hint": "Po ustawieniu tylko ten użytkownik Home Assistant (lub administrator) może wykonywać obowiązki, odbierać nagrody i uruchamiać stopery jako to dziecko przez wywołania usług. Pozostaw „Każdy” dla wspólnego tabletu rodzinnego.",
|
||||
"panel.child_picture_entity_label": "Zdjęcie z",
|
||||
"panel.child_picture_entity_hint": "Wybierz dowolną encję z obrazem, zwykle osobę dziecka, aby pokazać go zamiast ikony powyżej. Zdjęcie osoby ustawisz w Ustawienia › Osoby; bez obrazu pokazywana jest ikona.",
|
||||
"panel.child_kiosk_pin_label": "PIN kiosku",
|
||||
"panel.child_kiosk_pin_hint": "Opcjonalny. 4 cyfry, które dziecko wpisuje na wspólnym tablecie w trybie kiosku. Przechowywany jako hash, więc nie da się go odczytać — pozostaw puste, aby zachować obecny PIN.",
|
||||
"panel.child_kiosk_pin_set": "PIN jest ustawiony.",
|
||||
@@ -939,6 +943,9 @@
|
||||
"panel.child_age_group_label": "Grupa wiekowa",
|
||||
"panel.child_age_group_none": "Nie ustawiono",
|
||||
"panel.child_age_group_hint": "Decyduje, jakie obowiązki proponuje kreator. Nie zmienia niczego innego.",
|
||||
"panel.child_adult_label": "Profil dorosłego",
|
||||
"panel.child_adult_hint": "Dla rodzica lub innej dorosłej osoby. Obowiązki, które oznaczy jako wykonane, liczą się od razu bez zatwierdzania, a karty pokazują tę osobę tylko wtedy, gdy w karcie włączona jest opcja „Pokaż dorosłych”.",
|
||||
"panel.child_adult_pill": "Dorosły",
|
||||
"wizard.title": "Kreator konfiguracji",
|
||||
"wizard.run_btn": "Uruchom kreator konfiguracji",
|
||||
"wizard.open_btn": "Otwórz kreator",
|
||||
@@ -3055,5 +3062,66 @@
|
||||
"notify.sample_pledger": "Babcia",
|
||||
"notify.sample_bounty": "Umyj samochód",
|
||||
"notify.sample_reason": "Niedokończone",
|
||||
"notify.sample_note": "Świetna robota!"
|
||||
"notify.sample_note": "Świetna robota!",
|
||||
"sticker_chart.error.entity_required": "Zdefiniuj encję",
|
||||
"sticker_chart.default_title": "Tabela naklejek: {name}",
|
||||
"sticker_chart.today": "Dziś: {done} / {total} zadań zrobionych",
|
||||
"sticker_chart.all_done": "Wszystko zrobione, {name}!",
|
||||
"sticker_chart.day_done_message": "Wszystkie zadania na dziś gotowe!",
|
||||
"sticker_chart.no_stickers": "Jeszcze nie ma naklejek – zrób obowiązek, aby zdobyć pierwszą!",
|
||||
"sticker_chart.stickers_alt": "Zdobyte: {count} {points}",
|
||||
"sticker_chart.goal": "{points} / {goal} {unit} do nagrody",
|
||||
"sticker_chart.goal_reached": "CEL OSIĄGNIĘTY! Czas na nagrodę!",
|
||||
"sticker_chart.week_goal": "{points} / {goal} {unit} w tym tygodniu",
|
||||
"sticker_chart.week_perfect": "IDEALNY TYDZIEŃ! Wszystkie gwiazdki zdobyte!",
|
||||
"sticker_chart.block_key": "Każdy blok = {count} {unit}",
|
||||
"sticker_chart.day_count": "{day}: {done} / {total} zadań zrobionych",
|
||||
"sticker_chart.no_jobs_day": "W dniu {day} nie było żadnych zadań.",
|
||||
"sticker_chart.editor.goal": "Do czego liczą się naklejki",
|
||||
"sticker_chart.editor.goal_reward": "Nagroda",
|
||||
"sticker_chart.editor.goal_week": "Zadania z tego tygodnia",
|
||||
"sticker_chart.editor.goal_helper": "Zadania z tego tygodnia: bloki wypełniają się gwiazdkami zdobytymi od poniedziałku, z wszystkich gwiazdek, jakie mogą dać zadania tygodnia. Nagroda i przycisk odbioru pozostają bez zmian.",
|
||||
"sticker_chart.no_reward": "Nie ma jeszcze nagrody do zbierania – wybierz ją w ustawieniach karty.",
|
||||
"sticker_chart.no_child": "Nie znaleziono dzieci dla tej encji",
|
||||
"sticker_chart.editor.title": "Tytuł",
|
||||
"sticker_chart.editor.reward": "Nagroda, na którą się zbiera",
|
||||
"sticker_chart.editor.reward_helper": "Koszt nagrody staje się celem. Zostaw automatycznie, aby celować w najtańszą, jeszcze niezdobytą nagrodę.",
|
||||
"sticker_chart.editor.reward_auto": "Automatycznie (następna do zdobycia)",
|
||||
"sticker_chart.editor.show_today": "Pokaż dzisiejsze zadania",
|
||||
"sticker_chart.editor.show_today_helper": "Pokaż wiersz „Dziś: x / y zadań zrobionych” nad naklejkami.",
|
||||
"sticker_chart.claim": "Odbierz: {reward}",
|
||||
"sticker_chart.claim_confirm": "Dotknij ponownie, aby odebrać: {reward}",
|
||||
"sticker_chart.claim_pending": "Czeka na zgodę dorosłego",
|
||||
"sticker_chart.claim_short": "Jeszcze {count} {unit}",
|
||||
"sticker_chart.claimed": "Odebrano {reward} dla {name}!",
|
||||
"sticker_chart.complete_failed": "Nie udało się: {error}",
|
||||
"sticker_chart.editor.allow_undo": "Przytrzymaj naklejkę, aby cofnąć",
|
||||
"sticker_chart.editor.allow_undo_helper": "Naciśnij i przytrzymaj naklejkę, aby ją zabrać. TaskMate pozwala na to tylko rodzicowi.",
|
||||
"sticker_chart.editor.child": "Dziecko",
|
||||
"sticker_chart.editor.child_all": "Wszystkie dzieci obok siebie",
|
||||
"sticker_chart.editor.columns": "Naklejek w rzędzie",
|
||||
"sticker_chart.editor.show_claim": "Pokaż przycisk odbioru",
|
||||
"sticker_chart.editor.show_claim_helper": "Przycisk pod celem, który odbiera nagrodę po zdobyciu wystarczającej liczby (dotknij dwa razy, aby potwierdzić).",
|
||||
"sticker_chart.editor.sound": "Odtwarzaj dźwięk obowiązku",
|
||||
"sticker_chart.editor.title_helper": "Zostaw puste, aby użyć imienia każdego dziecka.",
|
||||
"sticker_chart.goal_tile": "Cel",
|
||||
"sticker_chart.needs_photo": "To zadanie wymaga zdjęcia – otwórz je na karcie dziecka TaskMate.",
|
||||
"sticker_chart.no_jobs_today": "Dziś brak zadań – miłego dnia!",
|
||||
"sticker_chart.state.done": "zrobione",
|
||||
"sticker_chart.state.partial": "rozpoczęte",
|
||||
"sticker_chart.state.pending": "czeka na dorosłego",
|
||||
"sticker_chart.state.todo": "do zrobienia",
|
||||
"sticker_chart.not_allowed": "Ten tablet nie może odznaczać zadań dziecka {name}. Dorosły może to zmienić w TaskMate: połącz {name} z kontem używanym na tym tablecie lub dodaj to konto jako rodzica.",
|
||||
"sticker_chart.undo_denied": "Tylko dorosły może cofnąć naklejkę.",
|
||||
"sticker_chart.undone": "Naklejka zabrana.",
|
||||
"panel.backdate_button": "Zapisz minione zadanie",
|
||||
"panel.backdate_title": "Zapisz minione zadanie dla: {name}",
|
||||
"panel.backdate_hint": "Wybierz dzień, a potem dotknij zadań, które zostały zrobione – tak jak w tabeli naklejek. Liczą się do tego dnia i tygodnia i są zatwierdzane od razu. Przytrzymaj naklejkę, aby ją cofnąć.",
|
||||
"panel.backdate_hint_list": "Dla zadania zrobionego wcześniej, którego nikt nie odznaczył. Liczy się do tamtego dnia i tygodnia i jest zatwierdzane od razu.",
|
||||
"panel.backdate_finish": "Gotowe",
|
||||
"panel.backdate_day": "Dzień wykonania",
|
||||
"panel.backdate_chore": "Zadanie",
|
||||
"panel.backdate_none": "W tym dniu nie było zadań dla: {name}.",
|
||||
"panel.backdate_save": "Zapisz",
|
||||
"panel.backdate_done": "Zapisano {chore} dla: {name} w dniu {day}"
|
||||
}
|
||||
|
||||
@@ -353,6 +353,8 @@
|
||||
"common.editor.filter_by_child_all": "Todas as crianças",
|
||||
"common.editor.header_colour": "Cor do Cabeçalho",
|
||||
"common.editor.header_colour_helper": "Cor de fundo do cabeçalho do cartão",
|
||||
"common.editor.show_adults": "Mostrar adultos",
|
||||
"common.editor.show_adults_helper": "Incluir perfis de adultos neste cartão",
|
||||
"common.editor.overview_entity": "Entidade de Visão Geral",
|
||||
"common.editor.overview_entity_helper": "A entidade sensor de visão geral do TaskMate",
|
||||
"common.editor.show_parent_actions": "Mostrar ações dos pais",
|
||||
@@ -889,6 +891,9 @@
|
||||
"panel.child_age_group_label": "Faixa etária",
|
||||
"panel.child_age_group_none": "Não definida",
|
||||
"panel.child_age_group_hint": "Define quais tarefas o assistente de configuração sugere. Não muda mais nada.",
|
||||
"panel.child_adult_label": "Perfil de adulto",
|
||||
"panel.child_adult_hint": "Para um pai, mãe ou outro adulto. As tarefas que ele marcar como feitas contam na hora, sem aprovação, e os cartões só o mostram quando a opção \"Mostrar adultos\" do cartão está ativada.",
|
||||
"panel.child_adult_pill": "Adulto",
|
||||
"wizard.title": "Assistente de configuração",
|
||||
"wizard.run_btn": "Executar assistente de configuração",
|
||||
"wizard.open_btn": "Abrir assistente",
|
||||
@@ -1886,6 +1891,57 @@
|
||||
"streak.editor.days_shown_helper": "Quantos dias de pontos mostrar (padrão: 14)",
|
||||
"streak.editor.title": "Título",
|
||||
"streak.error.entity_required": "Por favor define uma entidade",
|
||||
"sticker_chart.error.entity_required": "Por favor define uma entidade",
|
||||
"sticker_chart.default_title": "Quadro de adesivos da {name}",
|
||||
"sticker_chart.today": "Hoje: {done} / {total} tarefas feitas",
|
||||
"sticker_chart.all_done": "Tudo pronto, {name}!",
|
||||
"sticker_chart.day_done_message": "Todas as tarefas de hoje foram feitas!",
|
||||
"sticker_chart.no_stickers": "Ainda sem adesivos - faça uma tarefa para ganhar o primeiro!",
|
||||
"sticker_chart.stickers_alt": "{count} {points} ganhas",
|
||||
"sticker_chart.goal": "{points} / {goal} {unit} até a recompensa",
|
||||
"sticker_chart.goal_reached": "OBJETIVO ALCANÇADO! Hora da recompensa!",
|
||||
"sticker_chart.week_goal": "{points} / {goal} {unit} nesta semana",
|
||||
"sticker_chart.week_perfect": "SEMANA PERFEITA! Todas as estrelas conquistadas!",
|
||||
"sticker_chart.block_key": "Cada bloco = {count} {unit}",
|
||||
"sticker_chart.day_count": "{day}: {done} / {total} tarefas feitas",
|
||||
"sticker_chart.no_jobs_day": "Não havia tarefas em {day}.",
|
||||
"sticker_chart.editor.goal": "Para onde contam os adesivos",
|
||||
"sticker_chart.editor.goal_reward": "A recompensa",
|
||||
"sticker_chart.editor.goal_week": "As tarefas desta semana",
|
||||
"sticker_chart.editor.goal_helper": "Tarefas desta semana: os blocos se enchem com as estrelas ganhas desde segunda-feira, de todas as que as tarefas da semana podem dar. A recompensa e o botão de resgate continuam iguais.",
|
||||
"sticker_chart.no_reward": "Ainda não há recompensa para juntar - escolha uma nas configurações do cartão.",
|
||||
"sticker_chart.no_child": "Nenhuma criança encontrada nesta entidade",
|
||||
"sticker_chart.editor.title": "Título",
|
||||
"sticker_chart.editor.reward": "Recompensa a ser conquistada",
|
||||
"sticker_chart.editor.reward_helper": "O custo da recompensa vira o objetivo. Deixe em automático para mirar a mais barata ainda não conquistada.",
|
||||
"sticker_chart.editor.reward_auto": "Automático (a próxima a conquistar)",
|
||||
"sticker_chart.editor.show_today": "Mostrar tarefas de hoje",
|
||||
"sticker_chart.editor.show_today_helper": "Mostra a linha \"Hoje: x / y tarefas feitas\" acima dos adesivos.",
|
||||
"sticker_chart.claim": "Resgatar {reward}",
|
||||
"sticker_chart.claim_confirm": "Toque de novo para resgatar {reward}",
|
||||
"sticker_chart.claim_pending": "Esperando um adulto dizer sim",
|
||||
"sticker_chart.claim_short": "Faltam {count} {unit}",
|
||||
"sticker_chart.claimed": "{reward} resgatado por {name}!",
|
||||
"sticker_chart.complete_failed": "Não deu certo: {error}",
|
||||
"sticker_chart.editor.allow_undo": "Segurar um adesivo para desfazer",
|
||||
"sticker_chart.editor.allow_undo_helper": "Segure um adesivo para retirá-lo. O TaskMate só deixa os pais fazerem isso.",
|
||||
"sticker_chart.editor.child": "Criança",
|
||||
"sticker_chart.editor.child_all": "Todas as crianças lado a lado",
|
||||
"sticker_chart.editor.columns": "Adesivos por linha",
|
||||
"sticker_chart.editor.show_claim": "Mostrar o botão de resgate",
|
||||
"sticker_chart.editor.show_claim_helper": "Um botão abaixo do objetivo que resgata a recompensa quando já ganhou o suficiente (toque duas vezes para confirmar).",
|
||||
"sticker_chart.editor.sound": "Tocar o som da tarefa",
|
||||
"sticker_chart.editor.title_helper": "Deixe vazio para usar o nome de cada criança.",
|
||||
"sticker_chart.goal_tile": "Objetivo",
|
||||
"sticker_chart.needs_photo": "Esta tarefa precisa de uma foto - abra no cartão da criança do TaskMate.",
|
||||
"sticker_chart.no_jobs_today": "Nenhuma tarefa hoje - aproveite o dia!",
|
||||
"sticker_chart.state.done": "feito",
|
||||
"sticker_chart.state.partial": "começado",
|
||||
"sticker_chart.state.pending": "esperando um adulto",
|
||||
"sticker_chart.state.todo": "a fazer",
|
||||
"sticker_chart.not_allowed": "Este tablet não pode marcar as tarefas de {name}. Um adulto pode resolver no TaskMate: vincular {name} à conta que este tablet usa, ou adicionar essa conta como pai/mãe.",
|
||||
"sticker_chart.undo_denied": "Só um adulto pode retirar um adesivo.",
|
||||
"sticker_chart.undone": "Adesivo retirado.",
|
||||
"visibility.description_hidden": "Ocultado por condição de visibilidade",
|
||||
"visibility.description_shown": "Mostrado por condição de visibilidade",
|
||||
"visibility.label_entity": "Entidade de Visibilidade",
|
||||
@@ -1980,6 +2036,8 @@
|
||||
"panel.btn_clone_chore": "Duplicar",
|
||||
"panel.child_link_user_label": "Vincular a um usuário do Home Assistant (opcional)",
|
||||
"panel.child_link_user_hint": "Quando definido, apenas este usuário do Home Assistant (ou um admin) pode concluir tarefas, resgatar recompensas ou iniciar timers por esta criança via serviços. Deixe em \"Qualquer pessoa\" para um tablet familiar compartilhado.",
|
||||
"panel.child_picture_entity_label": "Foto de",
|
||||
"panel.child_picture_entity_hint": "Escolha qualquer entidade com imagem, geralmente a pessoa da criança, para mostrá-la em vez do ícone acima. A foto de uma pessoa é definida em Configurações › Pessoas; sem imagem, o ícone é exibido.",
|
||||
"panel.child_kiosk_pin_label": "PIN do quiosque",
|
||||
"panel.child_kiosk_pin_hint": "Opcional. 4 dígitos que a criança digita em um tablet de quiosque compartilhado. Salvo com hash, não pode ser lido de volta — deixe em branco para manter o PIN atual.",
|
||||
"panel.child_kiosk_pin_set": "Há um PIN definido.",
|
||||
@@ -2085,6 +2143,16 @@
|
||||
"panel.adjust_custom_title": "Ajustar os pontos de {child} com um valor personalizado",
|
||||
"panel.adjust_dialog_title": "Ajustar pontos — {name}",
|
||||
"panel.adjust_done": "{sign}{amount} {points} para {child}",
|
||||
"panel.backdate_button": "Registrar uma tarefa passada",
|
||||
"panel.backdate_title": "Registrar uma tarefa passada para {name}",
|
||||
"panel.backdate_hint": "Escolha o dia e toque nas tarefas que foram feitas, igual ao quadro de adesivos. Elas contam para esse dia e essa semana, e já ficam aprovadas. Segure um adesivo para desfazê-lo.",
|
||||
"panel.backdate_hint_list": "Para uma tarefa feita em um dia anterior que ninguém marcou. Conta para esse dia e essa semana, e já fica aprovada.",
|
||||
"panel.backdate_finish": "Concluir",
|
||||
"panel.backdate_day": "Dia em que foi feita",
|
||||
"panel.backdate_chore": "Tarefa",
|
||||
"panel.backdate_none": "Não havia tarefas para {name} nesse dia.",
|
||||
"panel.backdate_save": "Registrar",
|
||||
"panel.backdate_done": "{chore} registrada para {name} em {day}",
|
||||
"panel.adjust_err_amount": "Digite um valor entre 1 e 10000.",
|
||||
"panel.adjust_reason": "Motivo (opcional)",
|
||||
"panel.adjust_remove": "Remover",
|
||||
|
||||
@@ -357,6 +357,8 @@
|
||||
"common.editor.filter_by_child_all": "Todas as crianças",
|
||||
"common.editor.header_colour": "Cor do Cabeçalho",
|
||||
"common.editor.header_colour_helper": "Cor de fundo do cabeçalho do cartão",
|
||||
"common.editor.show_adults": "Mostrar adultos",
|
||||
"common.editor.show_adults_helper": "Incluir perfis de adultos neste cartão",
|
||||
"common.editor.overview_entity": "Entidade de Visão Geral",
|
||||
"common.editor.overview_entity_helper": "A entidade sensor de visão geral do TaskMate",
|
||||
"common.editor.show_parent_actions": "Mostrar ações dos pais",
|
||||
@@ -894,6 +896,9 @@
|
||||
"panel.child_age_group_label": "Faixa etária",
|
||||
"panel.child_age_group_none": "Não definida",
|
||||
"panel.child_age_group_hint": "Decide que tarefas o assistente de configuração sugere. Não altera mais nada.",
|
||||
"panel.child_adult_label": "Perfil de adulto",
|
||||
"panel.child_adult_hint": "Para um pai, mãe ou outro adulto. As tarefas que marcar como feitas contam logo, sem aprovação, e os cartões só o mostram quando a opção \"Mostrar adultos\" do cartão está ativa.",
|
||||
"panel.child_adult_pill": "Adulto",
|
||||
"wizard.title": "Assistente de configuração",
|
||||
"wizard.run_btn": "Executar assistente de configuração",
|
||||
"wizard.open_btn": "Abrir assistente",
|
||||
@@ -1891,6 +1896,57 @@
|
||||
"streak.editor.days_shown_helper": "Quantos dias de pontos mostrar (padrão: 14)",
|
||||
"streak.editor.title": "Título",
|
||||
"streak.error.entity_required": "Por favor define uma entidade",
|
||||
"sticker_chart.error.entity_required": "Por favor define uma entidade",
|
||||
"sticker_chart.default_title": "Quadro de autocolantes da {name}",
|
||||
"sticker_chart.today": "Hoje: {done} / {total} tarefas feitas",
|
||||
"sticker_chart.all_done": "Está tudo feito, {name}!",
|
||||
"sticker_chart.day_done_message": "Todas as tarefas de hoje estão feitas!",
|
||||
"sticker_chart.no_stickers": "Ainda sem autocolantes - faz uma tarefa para ganhar o primeiro!",
|
||||
"sticker_chart.stickers_alt": "{count} {points} ganhas",
|
||||
"sticker_chart.goal": "{points} / {goal} {unit} até à recompensa",
|
||||
"sticker_chart.goal_reached": "OBJETIVO ALCANÇADO! Hora da recompensa!",
|
||||
"sticker_chart.week_goal": "{points} / {goal} {unit} esta semana",
|
||||
"sticker_chart.week_perfect": "SEMANA PERFEITA! Todas as estrelas ganhas!",
|
||||
"sticker_chart.block_key": "Cada bloco = {count} {unit}",
|
||||
"sticker_chart.day_count": "{day}: {done} / {total} tarefas feitas",
|
||||
"sticker_chart.no_jobs_day": "Não havia tarefas em {day}.",
|
||||
"sticker_chart.editor.goal": "Para onde contam os autocolantes",
|
||||
"sticker_chart.editor.goal_reward": "A recompensa",
|
||||
"sticker_chart.editor.goal_week": "As tarefas desta semana",
|
||||
"sticker_chart.editor.goal_helper": "Tarefas desta semana: os blocos enchem-se com as estrelas ganhas desde segunda-feira, de todas as que as tarefas da semana podem dar. A recompensa e o botão para a levantar ficam como estão.",
|
||||
"sticker_chart.no_reward": "Ainda não há recompensa para juntar - escolhe uma nas definições do cartão.",
|
||||
"sticker_chart.no_child": "Não foram encontradas crianças nesta entidade",
|
||||
"sticker_chart.editor.title": "Título",
|
||||
"sticker_chart.editor.reward": "Recompensa a que se destina",
|
||||
"sticker_chart.editor.reward_helper": "O custo da recompensa passa a ser o objetivo. Deixa em automático para apontar à mais barata ainda não ganha.",
|
||||
"sticker_chart.editor.reward_auto": "Automático (a próxima a ganhar)",
|
||||
"sticker_chart.editor.show_today": "Mostrar tarefas de hoje",
|
||||
"sticker_chart.editor.show_today_helper": "Mostra a linha \"Hoje: x / y tarefas feitas\" por cima dos autocolantes.",
|
||||
"sticker_chart.claim": "Levantar {reward}",
|
||||
"sticker_chart.claim_confirm": "Toca outra vez para levantar {reward}",
|
||||
"sticker_chart.claim_pending": "À espera que um adulto diga que sim",
|
||||
"sticker_chart.claim_short": "Faltam {count} {unit}",
|
||||
"sticker_chart.claimed": "{reward} levantado por {name}!",
|
||||
"sticker_chart.complete_failed": "Não resultou: {error}",
|
||||
"sticker_chart.editor.allow_undo": "Manter premido um autocolante para anular",
|
||||
"sticker_chart.editor.allow_undo_helper": "Mantém premido um autocolante para o retirar. O TaskMate só permite isto aos pais.",
|
||||
"sticker_chart.editor.child": "Criança",
|
||||
"sticker_chart.editor.child_all": "Todas as crianças lado a lado",
|
||||
"sticker_chart.editor.columns": "Autocolantes por linha",
|
||||
"sticker_chart.editor.show_claim": "Mostrar o botão de levantar",
|
||||
"sticker_chart.editor.show_claim_helper": "Um botão por baixo do objetivo que levanta a recompensa quando já se ganhou o suficiente (toca duas vezes para confirmar).",
|
||||
"sticker_chart.editor.sound": "Tocar o som da tarefa",
|
||||
"sticker_chart.editor.title_helper": "Deixa vazio para usar o nome de cada criança.",
|
||||
"sticker_chart.goal_tile": "Objetivo",
|
||||
"sticker_chart.needs_photo": "Esta tarefa precisa de uma fotografia - abre-a no cartão da criança do TaskMate.",
|
||||
"sticker_chart.no_jobs_today": "Hoje não há tarefas - aproveita o dia!",
|
||||
"sticker_chart.state.done": "feito",
|
||||
"sticker_chart.state.partial": "começado",
|
||||
"sticker_chart.state.pending": "à espera de um adulto",
|
||||
"sticker_chart.state.todo": "por fazer",
|
||||
"sticker_chart.not_allowed": "Este tablet não pode marcar as tarefas de {name}. Um adulto pode resolver no TaskMate: associar {name} à conta que este tablet usa, ou adicionar essa conta como pai/mãe.",
|
||||
"sticker_chart.undo_denied": "Só um adulto pode retirar um autocolante.",
|
||||
"sticker_chart.undone": "Autocolante retirado.",
|
||||
"visibility.description_hidden": "Ocultado por condição de visibilidade",
|
||||
"visibility.description_shown": "Mostrado por condição de visibilidade",
|
||||
"visibility.label_entity": "Entidade de Visibilidade",
|
||||
@@ -1980,6 +2036,8 @@
|
||||
"panel.btn_clone_chore": "Duplicar",
|
||||
"panel.child_link_user_label": "Associar a um utilizador do Home Assistant (opcional)",
|
||||
"panel.child_link_user_hint": "Quando definido, apenas este utilizador do Home Assistant (ou um admin) pode concluir tarefas, resgatar recompensas ou iniciar temporizadores por esta criança via serviços. Deixe em \"Qualquer pessoa\" para um tablet familiar partilhado.",
|
||||
"panel.child_picture_entity_label": "Fotografia de",
|
||||
"panel.child_picture_entity_hint": "Escolhe qualquer entidade com imagem, normalmente a pessoa da criança, para a mostrar em vez do ícone acima. A fotografia de uma pessoa define-se em Definições › Pessoas; sem imagem, é mostrado o ícone.",
|
||||
"panel.child_kiosk_pin_label": "PIN do quiosque",
|
||||
"panel.child_kiosk_pin_hint": "Opcional. 4 dígitos que a criança escreve num tablet de quiosque partilhado. Guardado com hash, não pode ser lido de volta — deixe em branco para manter o PIN atual.",
|
||||
"panel.child_kiosk_pin_set": "Há um PIN definido.",
|
||||
@@ -2085,6 +2143,16 @@
|
||||
"panel.adjust_custom_title": "Ajustar os pontos de {child} com um valor personalizado",
|
||||
"panel.adjust_dialog_title": "Ajustar pontos — {name}",
|
||||
"panel.adjust_done": "{sign}{amount} {points} para {child}",
|
||||
"panel.backdate_button": "Registar uma tarefa passada",
|
||||
"panel.backdate_title": "Registar uma tarefa passada para {name}",
|
||||
"panel.backdate_hint": "Escolhe o dia e toca nas tarefas que foram feitas, tal como no quadro de autocolantes. Contam para esse dia e essa semana, e ficam logo aprovadas. Mantém um autocolante premido para o retirar.",
|
||||
"panel.backdate_hint_list": "Para uma tarefa feita num dia anterior que ninguém marcou. Conta para esse dia e essa semana, e fica logo aprovada.",
|
||||
"panel.backdate_finish": "Concluído",
|
||||
"panel.backdate_day": "Dia em que foi feita",
|
||||
"panel.backdate_chore": "Tarefa",
|
||||
"panel.backdate_none": "Não havia tarefas para {name} nesse dia.",
|
||||
"panel.backdate_save": "Registar",
|
||||
"panel.backdate_done": "{chore} registada para {name} em {day}",
|
||||
"panel.adjust_err_amount": "Introduza um valor entre 1 e 10000.",
|
||||
"panel.adjust_reason": "Motivo (opcional)",
|
||||
"panel.adjust_remove": "Retirar",
|
||||
|
||||
@@ -789,23 +789,26 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
_av(name, avatar, tone, size) {
|
||||
const a = avatar || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${name || ''}">`
|
||||
// `source` is whatever _childAvatar returned: a child object, or a bare
|
||||
// avatar string from an older payload. The shared helper accepts either.
|
||||
_av(name, source, tone, size) {
|
||||
const face = window.__taskmate_child_visual(source);
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${name || ''}">`
|
||||
: (name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
|
||||
// Resolve a child's avatar from the overview sensor's children list (the
|
||||
// completion/claim payloads don't carry an avatar field).
|
||||
// Resolve a child's face from the overview sensor's children list (the
|
||||
// completion/claim payloads don't carry one). Returns the whole child so the
|
||||
// child's photo travels with the avatar, not just the icon.
|
||||
_childAvatar(childId) {
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity))
|
||||
|| this.hass?.states?.[this.config.entity]?.attributes || {};
|
||||
const child = (attrs.children || []).find(c => c.id === childId);
|
||||
return child ? child.avatar : null;
|
||||
return child || null;
|
||||
}
|
||||
|
||||
_pointsAttrs() {
|
||||
|
||||
@@ -143,6 +143,34 @@
|
||||
|
||||
window.__taskmate_attrs = resolveAttrs;
|
||||
|
||||
/**
|
||||
* The children a family-wide card lists (#1061). Grown-ups' profiles stay
|
||||
* out unless the card sets show_adults, and when shown they follow the
|
||||
* kids. A card pinned to one child_id gets the full list back, so an adult
|
||||
* can still have a card of their own.
|
||||
*/
|
||||
function familyChildren(children, config) {
|
||||
const list = Array.isArray(children) ? children : [];
|
||||
if (!list.some(c => c && c.is_adult) || (config && config.child_id)) return list;
|
||||
const kids = list.filter(c => !c.is_adult);
|
||||
return config && config.show_adults === true ? kids.concat(list.filter(c => c.is_adult)) : kids;
|
||||
}
|
||||
|
||||
window.__taskmate_family_children = familyChildren;
|
||||
|
||||
/**
|
||||
* The editor's "Show grown-ups" toggle, as ha-form schema entries. Only
|
||||
* offered once the family has an adult profile (or the card already has
|
||||
* the option set), so everyone else's editor stays as it was.
|
||||
*/
|
||||
function adultsEditorField(hass, config) {
|
||||
const attrs = (config && config.entity && resolveAttrs(hass, config.entity)) || {};
|
||||
const hasAdult = (attrs.children || []).some(c => c && c.is_adult);
|
||||
return hasAdult || (config && config.show_adults) ? [{ name: "show_adults", selector: { boolean: {} } }] : [];
|
||||
}
|
||||
|
||||
window.__taskmate_adults_field = adultsEditorField;
|
||||
|
||||
/**
|
||||
* A child's badges sensor state, by child id, from the overview's entity
|
||||
* map (#1018). Null when the map doesn't list one — callers fall back to
|
||||
|
||||
@@ -183,7 +183,11 @@ class TaskMateAuctionCard extends LitElement {
|
||||
const idx = Math.max(0, children.findIndex(c => String(c.id) === String(childId)));
|
||||
const name = this._childName(childId);
|
||||
const colour = `var(--tmd-c${(idx % 6) + 1}, ${CHILD_COLOURS[idx % CHILD_COLOURS.length]})`;
|
||||
return html`<span class="ac-av" style="--ac-ac:${colour};--ac-s:${size}px" title="${name}">${name.charAt(0).toUpperCase()}</span>`;
|
||||
// The child's photo when they have one (the shared face helper), else their initial.
|
||||
const child = children.find(c => String(c.id) === String(childId));
|
||||
const face = child && window.__taskmate_child_visual ? window.__taskmate_child_visual(child) : null;
|
||||
const inner = face && face.kind === "image" ? html`<img src="${face.url}" alt="">` : name.charAt(0).toUpperCase();
|
||||
return html`<span class="ac-av" style="--ac-ac:${colour};--ac-s:${size}px" title="${name}">${inner}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -623,8 +627,9 @@ class TaskMateAuctionCard extends LitElement {
|
||||
background: var(--ac-ac, #9b59b6); color: #fff;
|
||||
display: inline-grid; place-items: center;
|
||||
font-weight: 700; font-size: calc(var(--ac-s, 30px) * .42); line-height: 1;
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box; overflow: hidden;
|
||||
}
|
||||
.ac-av img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
|
||||
.ac-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||||
|
||||
@@ -145,6 +145,8 @@ class TaskMateBadgesCard extends LitElement {
|
||||
--mdc-icon-size: 26px;
|
||||
color: white;
|
||||
}
|
||||
.child-avatar { overflow: hidden; }
|
||||
.child-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.badges-title {
|
||||
font-size: 1.1rem;
|
||||
@@ -479,6 +481,17 @@ class TaskMateBadgesCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// The child's photo when they have one (the shared face helper), else the
|
||||
// avatar icon the sensor sends.
|
||||
_face(attrs, avatar) {
|
||||
const face = window.__taskmate_child_visual
|
||||
? window.__taskmate_child_visual({ avatar, avatar_image: attrs.avatar_image })
|
||||
: { kind: "icon", icon: avatar };
|
||||
return face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="">`
|
||||
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : avatar}"></ha-icon>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.hass || !this.config) return html``;
|
||||
|
||||
@@ -528,9 +541,7 @@ class TaskMateBadgesCard extends LitElement {
|
||||
<ha-card>
|
||||
<div class="badges-head">
|
||||
<div class="badges-head-left">
|
||||
<div class="child-avatar">
|
||||
<ha-icon icon="${childAvatar}"></ha-icon>
|
||||
</div>
|
||||
<div class="child-avatar">${this._face(attrs, childAvatar)}</div>
|
||||
<div>
|
||||
<div class="badges-title">${title}</div>
|
||||
<div class="badges-count">${countLabel}</div>
|
||||
|
||||
@@ -127,6 +127,20 @@ class TaskMateBountyCard extends LitElement {
|
||||
return c ? c.name : "?";
|
||||
}
|
||||
|
||||
/**
|
||||
* Who an open-to-everyone bounty shows as able to claim it (#1071). That
|
||||
* avatar row is family-wide even though the card belongs to one child, so
|
||||
* adult profiles stay out of it unless show_adults is set. The card's own
|
||||
* child is always in it.
|
||||
*/
|
||||
_everyoneIds() {
|
||||
const all = this._children();
|
||||
const cfg = { ...this.config, child_id: undefined };
|
||||
const listed = (window.__taskmate_family_children ? window.__taskmate_family_children(all, cfg) : all);
|
||||
const me = String(this.config.child_id);
|
||||
return listed.concat(all.filter(c => String(c.id) === me && !listed.includes(c))).map(c => c.id);
|
||||
}
|
||||
|
||||
_eligible(b, childId) {
|
||||
return !Array.isArray(b.eligible) || b.eligible.length === 0 || b.eligible.map(String).includes(String(childId));
|
||||
}
|
||||
@@ -175,7 +189,11 @@ class TaskMateBountyCard extends LitElement {
|
||||
const idx = Math.max(0, children.findIndex(c => String(c.id) === String(childId)));
|
||||
const name = this._childName(childId);
|
||||
const colour = `var(--tmd-c${(idx % 6) + 1}, ${CHILD_COLOURS[idx % CHILD_COLOURS.length]})`;
|
||||
return html`<span class="bb-av" style="--bb-ac:${colour};--bb-s:${size}px" title="${name}">${name.charAt(0).toUpperCase()}</span>`;
|
||||
// The child's photo when they have one (the shared face helper), else their initial.
|
||||
const child = children.find(c => String(c.id) === String(childId));
|
||||
const face = child && window.__taskmate_child_visual ? window.__taskmate_child_visual(child) : null;
|
||||
const inner = face && face.kind === "image" ? html`<img src="${face.url}" alt="">` : name.charAt(0).toUpperCase();
|
||||
return html`<span class="bb-av" style="--bb-ac:${colour};--bb-s:${size}px" title="${name}">${inner}</span>`;
|
||||
}
|
||||
|
||||
// ── actions ─────────────────────────────────────────────────────────────
|
||||
@@ -345,7 +363,7 @@ class TaskMateBountyCard extends LitElement {
|
||||
|
||||
_row(b, me, now, holdingClaim) {
|
||||
const mine = String(b.claimed_by || "") === me;
|
||||
const eligibleIds = Array.isArray(b.eligible) && b.eligible.length ? b.eligible : this._children().map(c => c.id);
|
||||
const eligibleIds = Array.isArray(b.eligible) && b.eligible.length ? b.eligible : this._everyoneIds();
|
||||
const expLeft = b.expires_at ? this._msUntil(b.expires_at, now) : null;
|
||||
const showMeta = b.status === "open" || b.status === "claimed";
|
||||
let cls = "";
|
||||
@@ -578,8 +596,9 @@ class TaskMateBountyCard extends LitElement {
|
||||
display: inline-grid; place-items: center;
|
||||
font-weight: 700; font-size: calc(var(--bb-s, 22px) * .45); line-height: 1;
|
||||
border: 2px solid var(--tmd-surface, var(--ha-card-background, var(--card-background-color, #fff)));
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box; overflow: hidden;
|
||||
}
|
||||
.bb-av img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.bb-act { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
|
||||
.bb-hint { font-size: 12px; color: var(--tmd-dim, var(--secondary-text-color)); }
|
||||
.bb-claim-line {
|
||||
@@ -708,11 +727,13 @@ class TaskMateBountyCardEditor extends LitElement {
|
||||
child_id: this._t("bounty.editor.child"),
|
||||
title: this._t("common.editor.card_title"),
|
||||
card_design: this._t("common.design.field_label"),
|
||||
show_adults: this._t("common.editor.show_adults"),
|
||||
}[entry.name] ?? entry.name);
|
||||
|
||||
_computeHelper = (entry) => ({
|
||||
entity: this._t("common.editor.overview_entity_helper"),
|
||||
child_id: this._t("bounty.editor.child_helper"),
|
||||
show_adults: this._t("common.editor.show_adults_helper"),
|
||||
}[entry.name] ?? "");
|
||||
|
||||
render() {
|
||||
@@ -722,12 +743,13 @@ class TaskMateBountyCardEditor extends LitElement {
|
||||
child_id: this.config.child_id || "",
|
||||
title: this.config.title || "",
|
||||
card_design: this.config.card_design || "global",
|
||||
show_adults: this.config.show_adults === true,
|
||||
};
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
@@ -756,7 +778,8 @@ class TaskMateBountyCardEditor extends LitElement {
|
||||
const newConfig = { ...this.config };
|
||||
for (const [key, value] of Object.entries(newValues)) {
|
||||
// "global" is the absence of a per-card override, not a value to store.
|
||||
if (value === "" || value === null || value === undefined || (key === "card_design" && value === "global")) {
|
||||
if (value === "" || value === null || value === undefined || (key === "card_design" && value === "global")
|
||||
|| (key === "show_adults" && value === false)) {
|
||||
delete newConfig[key];
|
||||
} else {
|
||||
newConfig[key] = value;
|
||||
|
||||
@@ -470,7 +470,7 @@ class TaskMateCalendarCard extends LitElement {
|
||||
|
||||
const tz = this.hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
let children = attrs.children || [];
|
||||
let children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
const chores = attrs.chores || [];
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
|
||||
@@ -606,7 +606,12 @@ class TaskMateCalendarCard extends LitElement {
|
||||
<div class="child-block">
|
||||
<div class="child-head">
|
||||
<div class="child-avatar">
|
||||
<ha-icon icon="${child.avatar || "mdi:account-circle"}"></ha-icon>
|
||||
${(() => {
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
return face.kind === "image"
|
||||
? html`<img class="tm-face-img" src="${face.url}" alt="" loading="lazy">`
|
||||
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : "mdi:account-circle"}"></ha-icon>`;
|
||||
})()}
|
||||
</div>
|
||||
<span class="child-name">${child.name}</span>
|
||||
<span class="child-summary">${summary}</span>
|
||||
@@ -626,11 +631,11 @@ class TaskMateCalendarCard extends LitElement {
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
_av(child, tone, size) {
|
||||
const a = child.avatar || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${child.name}">`
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: (child.name || "?").split(" ").map((w) => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
@@ -658,7 +663,7 @@ class TaskMateCalendarCard extends LitElement {
|
||||
}
|
||||
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
let children = attrs.children || [];
|
||||
let children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
const chores = attrs.chores || [];
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
if (this.config.child_id) children = children.filter((c) => c.id === this.config.child_id);
|
||||
@@ -856,6 +861,7 @@ class TaskMateCalendarCardEditor extends LitElement {
|
||||
|
||||
_computeLabel = (entry) => {
|
||||
const labels = {
|
||||
show_adults: this._t("common.editor.show_adults"),
|
||||
entity: this._t("common.editor.overview_entity"),
|
||||
title: this._t("calendar.editor.title"),
|
||||
card_design: this._t("common.design.field_label"),
|
||||
@@ -866,6 +872,7 @@ class TaskMateCalendarCardEditor extends LitElement {
|
||||
|
||||
_computeHelper = (entry) => {
|
||||
const helpers = {
|
||||
show_adults: this._t("common.editor.show_adults_helper"),
|
||||
entity: this._t("common.editor.overview_entity_helper"),
|
||||
child_id: this._t("calendar.editor.child_helper"),
|
||||
};
|
||||
@@ -883,8 +890,8 @@ class TaskMateCalendarCardEditor extends LitElement {
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.data=${this.config.show_adults === true ? { ...data, show_adults: true } : data}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
|
||||
@@ -192,6 +192,20 @@ class TaskMateChildCard extends LitElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Classic-header avatar: child's photo → MDI avatar → default.
|
||||
*
|
||||
* The picker below is unaffected: it still compares catalogue icons against
|
||||
* `child.avatar`, so a child shown with their photo keeps whichever MDI
|
||||
* avatar they unlocked, and gets it back by switching the photo off.
|
||||
*/
|
||||
_avatarGlyph(child) {
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
return face.kind === "image"
|
||||
? html`<img class="tm-face-img" src="${face.url}" alt="" loading="lazy">`
|
||||
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : "mdi:account-circle"}"></ha-icon>`;
|
||||
}
|
||||
|
||||
_renderAvatarPicker(child, options) {
|
||||
return html`
|
||||
<div class="avatar-picker">
|
||||
@@ -2250,9 +2264,6 @@ class TaskMateChildCard extends LitElement {
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
const pointsName = attrs.points_name || this._t('common.stars');
|
||||
|
||||
// Avatar now in children array directly
|
||||
const avatar = child.avatar || "mdi:account-circle";
|
||||
|
||||
// Resolve badges sensor entity for this child
|
||||
const badgesEntity = this._resolveBadgesEntity(child);
|
||||
const earnedBadges = (badgesEntity?.attributes?.earned) || [];
|
||||
@@ -2281,7 +2292,7 @@ class TaskMateChildCard extends LitElement {
|
||||
<div class="avatar-container ${canChange ? 'avatar-clickable' : ''}"
|
||||
@click=${canChange ? () => this._toggleAvatarPicker() : null}
|
||||
title="${canChange ? this._t('child.avatar_change') : ''}">
|
||||
<ha-icon icon="${avatar}"></ha-icon>
|
||||
${this._avatarGlyph(child)}
|
||||
${canChange ? html`<span class="avatar-edit-dot"><ha-icon icon="mdi:pencil"></ha-icon></span>` : ''}
|
||||
</div>
|
||||
${canChange && this._avatarPickerOpen ? this._renderAvatarPicker(child, opts) : ''}
|
||||
@@ -2521,11 +2532,11 @@ class TaskMateChildCard extends LitElement {
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
_av(child, tone, size) {
|
||||
const a = child.avatar || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${child.name}">`
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: (child.name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
@@ -2554,11 +2565,12 @@ class TaskMateChildCard extends LitElement {
|
||||
<span class="tm-team-avs">${team.joined.map((id) => {
|
||||
const i = kids.findIndex((k) => String(k.id) === id);
|
||||
const kid = kids[i] || { name: "?" };
|
||||
const a = kid.avatar || "";
|
||||
// The shared face helper: the child's photo, else their MDI avatar.
|
||||
const face = window.__taskmate_child_visual ? window.__taskmate_child_visual(kid) : { kind: "none" };
|
||||
const tone = `var(--tmd-c${((i < 0 ? 0 : i) % 6) + 1}, ${fallback[(i < 0 ? 0 : i) % 6]})`;
|
||||
return html`<span class="tm-team-av" style="--ac:${tone}" title="${kid.name || ""}">${a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a ? html`<img src="${a}" alt="">` : (kid.name || "?").slice(0, 1).toUpperCase()}</span>`;
|
||||
return html`<span class="tm-team-av" style="--ac:${tone}" title="${kid.name || ""}">${face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image" ? html`<img src="${face.url}" alt="">` : (kid.name || "?").slice(0, 1).toUpperCase()}</span>`;
|
||||
})}</span>
|
||||
<span class="tm-team-txt">👥 ${label}${team.mine ? html` · ${this._t("child.team_you_joined")}` : ""}</span>
|
||||
</span>`;
|
||||
|
||||
@@ -105,7 +105,7 @@ class TaskMateChoreBoardCard extends LitElement {
|
||||
_kids(attrs) {
|
||||
const all = attrs.children || [];
|
||||
const picked = Array.isArray(this.config.children) ? this.config.children.map(String) : [];
|
||||
return picked.length ? all.filter(c => picked.includes(String(c.id))) : all;
|
||||
return picked.length ? all.filter(c => picked.includes(String(c.id))) : (window.__taskmate_family_children ? window.__taskmate_family_children(all, this.config) : all);
|
||||
}
|
||||
|
||||
_kidIndex(attrs, id) {
|
||||
@@ -317,10 +317,11 @@ class TaskMateChoreBoardCard extends LitElement {
|
||||
}
|
||||
|
||||
_avatar(attrs, kid) {
|
||||
const a = kid.avatar || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a ? html`<img src="${a}" alt="">` : (kid.name || "?").charAt(0).toUpperCase();
|
||||
// The shared face helper: the child's photo, else their MDI avatar.
|
||||
const face = window.__taskmate_child_visual ? window.__taskmate_child_visual(kid) : { kind: "none" };
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image" ? html`<img src="${face.url}" alt="">` : (kid.name || "?").charAt(0).toUpperCase();
|
||||
return html`<span class="cb-av" style="--cb-kc:${this._kidColour(attrs, kid.id)}">${inner}</span>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
const KIT = `
|
||||
.tmd{overflow:hidden;font-family:var(--tmd-font-body);color:var(--tmd-text);background:var(--tmd-surface);border-radius:var(--tmd-radius);box-shadow:var(--tmd-shadow)}
|
||||
.tmd-hd{display:flex;align-items:center;gap:11px;padding:13px 15px;background:var(--hd,var(--tmd-accent));color:#fff}
|
||||
:host([data-tm-design="playroom"]) .tmd-hd{background:linear-gradient(135deg,var(--hd,var(--tmd-accent)),color-mix(in srgb,var(--hd,var(--tmd-accent)) 72%,#fff))}
|
||||
:host([data-tm-design="playroom"]) .tmd-hd{background:var(--hd,var(--tmd-accent));background:linear-gradient(135deg,var(--hd,var(--tmd-accent)),color-mix(in srgb,var(--hd,var(--tmd-accent)) 72%,#fff))}
|
||||
.tmd-hd .ic{width:32px;height:32px;border-radius:9px;display:grid;place-items:center;background:rgba(255,255,255,.22);font-size:17px;flex:none}
|
||||
.tmd-hd .tt{font-family:var(--tmd-font-display);font-weight:800;font-size:16px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.tmd-hd .tt small{display:block;font-family:var(--tmd-font-body);font-weight:600;font-size:11px;opacity:.85;margin-top:1px}
|
||||
@@ -159,6 +159,11 @@
|
||||
:host([data-tm-design="console"]) .av{box-shadow:0 0 0 1px color-mix(in srgb,var(--ac,var(--tmd-accent)) 70%,transparent),0 0 14px color-mix(in srgb,var(--ac,var(--tmd-accent)) 35%,transparent)}
|
||||
.av ha-icon{--mdc-icon-size:calc(var(--av,42px)*.55);color:#fff}
|
||||
.av img{width:100%;height:100%;object-fit:cover;border-radius:50%}
|
||||
/* A child's photo, for the classic (non-designed) avatar wells.
|
||||
Every one of them is a sized, centred circle, so filling it is right; the
|
||||
chip variant is for the few places the icon sits inline beside text. */
|
||||
.tm-face-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block}
|
||||
.tm-face-chip{width:18px;height:18px;border-radius:50%;object-fit:cover;display:block;flex:none}
|
||||
.chip{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:700;padding:4px 9px;border-radius:999px;background:var(--tmd-surface-2);color:var(--tmd-text);border:1px solid var(--tmd-border)}
|
||||
.chip.soft{background:color-mix(in srgb,var(--tmd-accent) 14%,transparent);border-color:transparent;color:var(--tmd-accent)}
|
||||
.num{font-family:var(--tmd-font-mono);font-weight:800;letter-spacing:-.01em}
|
||||
@@ -172,7 +177,7 @@
|
||||
.lead{color:var(--tmd-accent)}
|
||||
.lead-dot{color:var(--tmd-gold)}
|
||||
.btn{font:inherit;font-family:var(--tmd-font-display);font-weight:800;font-size:13.5px;line-height:1;border:0;cursor:pointer;padding:9px 14px;border-radius:var(--tmd-radius-sm);background:var(--tmd-accent);color:#fff;display:inline-flex;align-items:center;justify-content:center;gap:6px}
|
||||
:host([data-tm-design="console"]) .btn{color:#06101c;background:linear-gradient(135deg,var(--tmd-accent),color-mix(in srgb,var(--tmd-accent) 60%,var(--tmd-accent2)));box-shadow:0 0 14px color-mix(in srgb,var(--tmd-accent) 45%,transparent)}
|
||||
:host([data-tm-design="console"]) .btn{color:#06101c;background:linear-gradient(135deg,var(--tmd-accent),var(--tmd-accent2));background:linear-gradient(135deg,var(--tmd-accent),color-mix(in srgb,var(--tmd-accent) 60%,var(--tmd-accent2)));box-shadow:0 0 14px color-mix(in srgb,var(--tmd-accent) 45%,transparent)}
|
||||
.btn.ghost{background:var(--tmd-surface-2);color:var(--tmd-text);border:1px solid var(--tmd-border)}
|
||||
:host([data-tm-design="console"]) .btn.ghost{box-shadow:none}
|
||||
.btn.good{background:var(--tmd-good);color:#06301f}
|
||||
@@ -563,6 +568,28 @@
|
||||
return { kind: "none" };
|
||||
};
|
||||
|
||||
/**
|
||||
* The same idea for a child's face.
|
||||
*
|
||||
* Precedence: child's photo → MDI avatar → the call site's fallback.
|
||||
*
|
||||
* `avatar_image` is resolved server-side from the picture entity chosen for
|
||||
* the child in TaskMate (empty when none is), so a card needs no knowledge
|
||||
* of person entities. A non-"mdi:" `avatar` is still treated as a picture URL,
|
||||
* which is what the points-display and approvals cards already did.
|
||||
*
|
||||
* Accepts a child object or a bare avatar string, because a few payloads
|
||||
* (reward claims, badges) carry only the string.
|
||||
*/
|
||||
window.__taskmate_child_visual = window.__taskmate_child_visual || function (child) {
|
||||
const c = typeof child === "string" ? { avatar: child } : child || {};
|
||||
if (c.avatar_image) return { kind: "image", url: c.avatar_image };
|
||||
const av = c.avatar || "";
|
||||
if (av.startsWith("mdi:")) return { kind: "icon", icon: av };
|
||||
if (av) return { kind: "image", url: av };
|
||||
return { kind: "none" };
|
||||
};
|
||||
|
||||
// ── Card-picker entity suggestions (HA 2026.6+) ────────────────────────────
|
||||
// Custom cards may add getEntitySuggestion(hass, entityId) to their
|
||||
// window.customCards entry; when a user picks an entity in the card picker,
|
||||
|
||||
@@ -277,7 +277,7 @@ class TaskMateGraphCard extends LitElement {
|
||||
|
||||
const tz = this.hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
let children = attrs.children || [];
|
||||
let children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
const pointsName = attrs.points_name || this._t('common.points');
|
||||
const days = Math.max(3, Math.min(90, this.config.days || 14));
|
||||
|
||||
@@ -394,7 +394,7 @@ class TaskMateGraphCard extends LitElement {
|
||||
const entity = this.hass.states[this.config.entity];
|
||||
const tz = this.hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
let children = attrs.children || [];
|
||||
let children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
const pointsName = attrs.points_name || this._t('common.points');
|
||||
const days = Math.max(3, Math.min(90, this.config.days || 14));
|
||||
|
||||
@@ -986,6 +986,7 @@ class TaskMateGraphCardEditor extends LitElement {
|
||||
|
||||
_computeLabel = (entry) => {
|
||||
const labels = {
|
||||
show_adults: this._t('common.editor.show_adults'),
|
||||
entity: this._t('common.editor.overview_entity'),
|
||||
title: this._t('common.editor.card_title'),
|
||||
card_design: this._t('common.design.field_label'),
|
||||
@@ -997,6 +998,7 @@ class TaskMateGraphCardEditor extends LitElement {
|
||||
|
||||
_computeHelper = (entry) => {
|
||||
const helpers = {
|
||||
show_adults: this._t('common.editor.show_adults_helper'),
|
||||
entity: this._t('common.editor.overview_entity_helper'),
|
||||
days: this._t('graph.editor.days_helper'),
|
||||
child_id: this._t('graph.editor.child_filter_helper'),
|
||||
@@ -1016,8 +1018,8 @@ class TaskMateGraphCardEditor extends LitElement {
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.data=${this.config.show_adults === true ? { ...data, show_adults: true } : data}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
|
||||
@@ -43,6 +43,14 @@ export function createIncentiveCard(P) {
|
||||
return fn ? fn(this.hass, key, params) : key;
|
||||
}
|
||||
|
||||
/** Avatar: child's photo → MDI avatar → default icon. */
|
||||
_avatarGlyph(source, fallback = "mdi:account-circle", cls = "tm-face-img") {
|
||||
const face = window.__taskmate_child_visual(source);
|
||||
return face.kind === "image"
|
||||
? html`<img class="${cls}" src="${face.url}" alt="" loading="lazy">`
|
||||
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : fallback}"></ha-icon>`;
|
||||
}
|
||||
|
||||
_tp(key, params) {
|
||||
return this._t(P.i18n + "." + key, params);
|
||||
}
|
||||
@@ -448,7 +456,9 @@ export function createIncentiveCard(P) {
|
||||
}
|
||||
|
||||
_getChildren() {
|
||||
return this._getAttrs().children || [];
|
||||
// Adult profiles stay off the picker unless show_adults is set (#1071).
|
||||
const all = this._getAttrs().children || [];
|
||||
return (window.__taskmate_family_children ? window.__taskmate_family_children(all, this.config) : all);
|
||||
}
|
||||
|
||||
_getBonuses() {
|
||||
@@ -617,7 +627,7 @@ export function createIncentiveCard(P) {
|
||||
${children.map(c => html`
|
||||
<div class="child-tab ${selected?.id === c.id ? "selected" : ""}"
|
||||
@click=${() => this._selectChild(c.id)}>
|
||||
<ha-icon icon="${c.avatar || "mdi:account-circle"}"></ha-icon>
|
||||
${this._avatarGlyph(c, "mdi:account-circle", "tm-face-chip")}
|
||||
${c.name}
|
||||
</div>
|
||||
`)}
|
||||
@@ -821,11 +831,11 @@ export function createIncentiveCard(P) {
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
_av(child, tone, size) {
|
||||
const a = child.avatar || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${child.name}">`
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: (child.name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
@@ -1087,6 +1097,7 @@ export function createIncentiveCard(P) {
|
||||
title: this._t('common.editor.card_title'),
|
||||
show_parent_actions: this._t('common.editor.show_parent_actions'),
|
||||
card_design: this._t('common.design.field_label'),
|
||||
show_adults: this._t('common.editor.show_adults'),
|
||||
};
|
||||
return labels[entry.name] ?? entry.name;
|
||||
};
|
||||
@@ -1095,6 +1106,7 @@ export function createIncentiveCard(P) {
|
||||
const helpers = {
|
||||
entity: this._t('common.editor.overview_entity_helper'),
|
||||
show_parent_actions: this._t('common.editor.show_parent_actions_helper'),
|
||||
show_adults: this._t('common.editor.show_adults_helper'),
|
||||
};
|
||||
return helpers[entry.name] ?? '';
|
||||
};
|
||||
@@ -1106,6 +1118,7 @@ export function createIncentiveCard(P) {
|
||||
title: this.config.title || '',
|
||||
show_parent_actions: this.config.show_parent_actions !== false,
|
||||
card_design: this.config.card_design || 'global',
|
||||
show_adults: this.config.show_adults === true,
|
||||
};
|
||||
return html`
|
||||
<div class="info-note">
|
||||
@@ -1115,7 +1128,7 @@ export function createIncentiveCard(P) {
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
@@ -1145,6 +1158,7 @@ export function createIncentiveCard(P) {
|
||||
for (const [key, value] of Object.entries(newValues)) {
|
||||
if (value === '' || value === null || value === undefined) delete newConfig[key];
|
||||
else if (key === 'card_design' && value === 'global') delete newConfig[key];
|
||||
else if (key === 'show_adults' && value === false) delete newConfig[key];
|
||||
else newConfig[key] = value;
|
||||
}
|
||||
this._fire(newConfig);
|
||||
|
||||
@@ -233,7 +233,7 @@ class TaskMateKioskCard extends LitElement {
|
||||
};
|
||||
const configured = Array.isArray(this.config.children) ? this.config.children : null;
|
||||
if (!configured) {
|
||||
return all.filter(c => !c.is_guest).map(c => ({ child: c, requirePin: true, tone: tone(c) }));
|
||||
return (window.__taskmate_family_children ? window.__taskmate_family_children(all, this.config) : all).filter(c => !c.is_guest).map(c => ({ child: c, requirePin: true, tone: tone(c) }));
|
||||
}
|
||||
const out = [];
|
||||
for (const entry of configured) {
|
||||
@@ -798,7 +798,12 @@ class TaskMateKioskCard extends LitElement {
|
||||
// ── Render ───────────────────────────────────────────────────────────────
|
||||
|
||||
_face(child, tone, size = "") {
|
||||
const avatar = child.avatar || "";
|
||||
// The child's photo first (the shared face helper), then their avatar.
|
||||
const face = window.__taskmate_child_visual ? window.__taskmate_child_visual(child) : { kind: "none" };
|
||||
if (face.kind === "image") {
|
||||
return html`<div class="km-face ${size}" style="--kc:${tone}"><img src="${face.url}" alt=""></div>`;
|
||||
}
|
||||
const avatar = face.kind === "icon" ? face.icon : "";
|
||||
// The default avatar is a generic silhouette, so a child who never picked
|
||||
// one gets their initial instead — easier to tell apart on a picker.
|
||||
const icon = avatar.startsWith("mdi:") && avatar !== "mdi:account-circle";
|
||||
@@ -1256,6 +1261,7 @@ class TaskMateKioskCard extends LitElement {
|
||||
box-shadow: 0 0 0 6px color-mix(in srgb, var(--kc) 25%, transparent);
|
||||
}
|
||||
.km-face ha-icon { --mdc-icon-size: 72px; }
|
||||
.km-face img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
|
||||
.km-face.big { width: 160px; height: 160px; font-size: 76px; }
|
||||
.km-face.big ha-icon { --mdc-icon-size: 90px; }
|
||||
.km-face.head {
|
||||
@@ -1729,7 +1735,7 @@ class TaskMateKioskCardEditor extends LitElement {
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity || "sensor.taskmate_overview")) || {};
|
||||
const all = attrs.children || [];
|
||||
const configured = Array.isArray(this.config.children) ? this.config.children : null;
|
||||
if (!configured) return all.map(c => ({ child: c, shown: !c.is_guest, requirePin: true }));
|
||||
if (!configured) return all.map(c => ({ child: c, shown: !c.is_guest && (!c.is_adult || this.config.show_adults === true), requirePin: true }));
|
||||
const rows = [];
|
||||
for (const entry of configured) {
|
||||
const child = all.find(c => String(c.id) === String(entry && (entry.child_id ?? entry)));
|
||||
|
||||
@@ -36,6 +36,14 @@ class TaskMateLeaderboardCard extends LitElement {
|
||||
return fn ? fn(this.hass, key, params) : key;
|
||||
}
|
||||
|
||||
/** Avatar: child's photo → MDI avatar → default icon. */
|
||||
_avatarGlyph(source, fallback = "mdi:account-circle", cls = "tm-face-img") {
|
||||
const face = window.__taskmate_child_visual(source);
|
||||
return face.kind === "image"
|
||||
? html`<img class="${cls}" src="${face.url}" alt="" loading="lazy">`
|
||||
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : fallback}"></ha-icon>`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
const base = css`
|
||||
:host { display: block; }
|
||||
@@ -320,7 +328,7 @@ class TaskMateLeaderboardCard extends LitElement {
|
||||
if (entity.state === "unavailable" || entity.state === "unknown") return html`<ha-card><div class="error-state"><ha-icon icon="mdi:alert-circle"></ha-icon><div>${this._t('common.unavailable')}</div></div></ha-card>`;
|
||||
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
const children = [...(attrs.children || [])].filter(c => !c.is_guest);
|
||||
const children = [...(window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || [])].filter(c => !c.is_guest);
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
const pointsName = attrs.points_name || this._t('common.points');
|
||||
|
||||
@@ -424,7 +432,7 @@ class TaskMateLeaderboardCard extends LitElement {
|
||||
: html`<div class="rank-number">${this._t('leaderboard.ordinal_' + rankNum)}</div>`}
|
||||
|
||||
<div class="child-avatar" style="background: linear-gradient(135deg, ${avatarColour} 0%, ${avatarColour}cc 100%);">
|
||||
<ha-icon icon="${child.avatar || 'mdi:account-circle'}"></ha-icon>
|
||||
${this._avatarGlyph(child)}
|
||||
</div>
|
||||
|
||||
<div class="rank-info">
|
||||
@@ -483,7 +491,7 @@ class TaskMateLeaderboardCard extends LitElement {
|
||||
<div class="rank-row first">
|
||||
<div class="rank-badge">🥇</div>
|
||||
<div class="child-avatar" style="background: linear-gradient(135deg, #f1c40f 0%, #e67e22 100%);">
|
||||
<ha-icon icon="${child.avatar || 'mdi:account-circle'}"></ha-icon>
|
||||
${this._avatarGlyph(child)}
|
||||
</div>
|
||||
<div class="rank-info">
|
||||
<div class="rank-name">${child.name}</div>
|
||||
@@ -566,11 +574,11 @@ class TaskMateLeaderboardCard extends LitElement {
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
_av(child, tone, size) {
|
||||
const a = child.avatar || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${child.name}">`
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: (child.name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
@@ -593,7 +601,7 @@ class TaskMateLeaderboardCard extends LitElement {
|
||||
}
|
||||
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
const children = [...(attrs.children || [])].filter(c => !c.is_guest);
|
||||
const children = [...(window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || [])].filter(c => !c.is_guest);
|
||||
const pointsName = attrs.points_name || this._t('common.points');
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
|
||||
@@ -833,6 +841,7 @@ class TaskMateLeaderboardCardEditor extends LitElement {
|
||||
|
||||
_computeLabel = (entry) => {
|
||||
const labels = {
|
||||
show_adults: this._t('common.editor.show_adults'),
|
||||
entity: this._t('leaderboard.editor.entity_label'),
|
||||
title: this._t('leaderboard.editor.title_label'),
|
||||
sort_by: this._t('leaderboard.editor.rank_by_label'),
|
||||
@@ -846,6 +855,7 @@ class TaskMateLeaderboardCardEditor extends LitElement {
|
||||
|
||||
_computeHelper = (entry) => {
|
||||
const helpers = {
|
||||
show_adults: this._t('common.editor.show_adults_helper'),
|
||||
entity: this._t('leaderboard.editor.entity_helper'),
|
||||
sort_by: this._t('leaderboard.editor.rank_by_helper'),
|
||||
};
|
||||
@@ -866,8 +876,8 @@ class TaskMateLeaderboardCardEditor extends LitElement {
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.data=${this.config.show_adults === true ? { ...data, show_adults: true } : data}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
|
||||
@@ -56,6 +56,14 @@ class TaskMateOverviewCard extends LitElement {
|
||||
return fn ? fn(this.hass, key, params) : key;
|
||||
}
|
||||
|
||||
/** Avatar: child's photo → MDI avatar → default icon. */
|
||||
_avatarGlyph(source, fallback = "mdi:account-circle", cls = "tm-face-img") {
|
||||
const face = window.__taskmate_child_visual(source);
|
||||
return face.kind === "image"
|
||||
? html`<img class="${cls}" src="${face.url}" alt="" loading="lazy">`
|
||||
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : fallback}"></ha-icon>`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
const base = css`
|
||||
:host {
|
||||
@@ -409,7 +417,7 @@ class TaskMateOverviewCard extends LitElement {
|
||||
if (design !== "classic") return this._renderDesigned(design, entity);
|
||||
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
const children = attrs.children || [];
|
||||
const children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
const chores = attrs.chores || [];
|
||||
const completions = [...(attrs.todays_completions || [])];
|
||||
const chorePointsMap = {};
|
||||
@@ -493,11 +501,11 @@ class TaskMateOverviewCard extends LitElement {
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
_av(child, tone, size) {
|
||||
const a = child.avatar || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${child.name}">`
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: (child.name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
@@ -526,7 +534,7 @@ class TaskMateOverviewCard extends LitElement {
|
||||
|
||||
_renderDesigned(design, entity) {
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
const children = attrs.children || [];
|
||||
const children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
const chores = attrs.chores || [];
|
||||
const completions = [...(attrs.todays_completions || [])];
|
||||
const hd = _safeColor(this.config.header_color, "#8e44ad");
|
||||
@@ -723,9 +731,6 @@ class TaskMateOverviewCard extends LitElement {
|
||||
}
|
||||
|
||||
_renderChildTile(child, chores, completions, pointsIcon, _pointsName) {
|
||||
// Avatar now included directly in children array from the overview sensor
|
||||
const avatar = child.avatar || "mdi:account-circle";
|
||||
|
||||
// Get today's day of week from sensor (e.g. "monday")
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config?.entity)) || {};
|
||||
const todayDow = attrs.today_day_of_week ||
|
||||
@@ -782,7 +787,7 @@ class TaskMateOverviewCard extends LitElement {
|
||||
return html`
|
||||
<div class="child-tile">
|
||||
<div class="child-avatar">
|
||||
<ha-icon icon="${avatar}"></ha-icon>
|
||||
${this._avatarGlyph(child)}
|
||||
</div>
|
||||
<div class="child-main ${isParent ? 'tm-expandable' : ''}"
|
||||
@click="${isParent ? () => { this._expanded = { ...this._expanded, [child.id]: !isOpen }; this.requestUpdate(); } : null}">
|
||||
@@ -931,6 +936,7 @@ class TaskMateOverviewCardEditor extends LitElement {
|
||||
|
||||
_computeLabel = (entry) => {
|
||||
const labels = {
|
||||
show_adults: this._t('common.editor.show_adults'),
|
||||
entity: this._t('overview.editor.entity_label'),
|
||||
title: this._t('overview.editor.title_label'),
|
||||
approvals_entity: this._t('overview.editor.approvals_entity_label'),
|
||||
@@ -942,6 +948,7 @@ class TaskMateOverviewCardEditor extends LitElement {
|
||||
|
||||
_computeHelper = (entry) => {
|
||||
const helpers = {
|
||||
show_adults: this._t('common.editor.show_adults_helper'),
|
||||
entity: this._t('overview.editor.entity_helper'),
|
||||
approvals_entity: this._t('overview.editor.approvals_entity_helper'),
|
||||
show_parent_actions: this._t('common.editor.show_parent_actions_helper'),
|
||||
@@ -961,8 +968,8 @@ class TaskMateOverviewCardEditor extends LitElement {
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.data=${this.config.show_adults === true ? { ...data, show_adults: true } : data}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
|
||||
@@ -292,6 +292,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
// to lean on) and our shell was stripped out in the meantime.
|
||||
if (!this._rendered || !this.querySelector(".tm-shell")) this._render();
|
||||
this._bindHaPickers(false);
|
||||
if (this._backdateCard) this._backdateCard.hass = value;
|
||||
}
|
||||
get hass() { return this._hass; }
|
||||
set narrow(_v) {}
|
||||
@@ -646,6 +647,9 @@ class TaskMatePanel extends HTMLElement {
|
||||
if (act === "save-gift") { this._doGiftPoints(); return; }
|
||||
if (act === "adjust-points") { this._doAdjustPoints(t.dataset.id, Number(t.dataset.delta)); return; }
|
||||
if (act === "adjust-points-custom") { this._openAdjustDialog(t.dataset.id); return; }
|
||||
if (act === "backdate-open") { this._openBackdateDialog(t.dataset.id); return; }
|
||||
if (act === "save-backdate") { this._saveBackdateDialog(); return; }
|
||||
if (act === "close-backdate") { this._closeDialog(true); this._fetchState(); return; }
|
||||
if (act === "sound-preview-field") {
|
||||
const sel = t.closest(".tm-sound-row")?.querySelector('select[data-field="completion_sound"]');
|
||||
if (sel) this._previewSound(sel.value);
|
||||
@@ -1973,8 +1977,10 @@ class TaskMatePanel extends HTMLElement {
|
||||
unavailability_entity: c.unavailability_entity || "",
|
||||
pause_streak_when_unavailable: !!c.pause_streak_when_unavailable,
|
||||
linked_user_id: c.linked_user_id || "",
|
||||
picture_entity: c.picture_entity || "",
|
||||
birthday: c.birthday || "",
|
||||
age_group: c.age_group || "",
|
||||
is_adult: !!c.is_adult,
|
||||
presence_entity: c.presence_entity || "",
|
||||
// Kiosk PIN (#930): write-only. The panel only knows whether one is set.
|
||||
kiosk_pin: "", kiosk_pin_clear: false,
|
||||
@@ -1985,8 +1991,8 @@ class TaskMatePanel extends HTMLElement {
|
||||
name: "", avatar: "mdi:account-circle", availability_entity: "",
|
||||
availability_inverted: false, unavailability_entity: "",
|
||||
pause_streak_when_unavailable: false,
|
||||
linked_user_id: "", presence_entity: "",
|
||||
birthday: "", age_group: "",
|
||||
linked_user_id: "", picture_entity: "", presence_entity: "",
|
||||
birthday: "", age_group: "", is_adult: false,
|
||||
kiosk_pin: "", kiosk_pin_clear: false, has_kiosk_pin: false,
|
||||
} });
|
||||
}
|
||||
@@ -2035,14 +2041,16 @@ class TaskMatePanel extends HTMLElement {
|
||||
availability_entity: d.availability_entity || "", availability_inverted: !!d.availability_inverted,
|
||||
unavailability_entity: d.unavailability_entity || "",
|
||||
pause_streak_when_unavailable: !!d.pause_streak_when_unavailable, linked_user_id: d.linked_user_id || "",
|
||||
birthday: (d.birthday || "").trim(), age_group: d.age_group || "",
|
||||
presence_entity: d.presence_entity || "" }
|
||||
birthday: (d.birthday || "").trim(), age_group: d.age_group || "", is_adult: !!d.is_adult,
|
||||
presence_entity: d.presence_entity || "",
|
||||
picture_entity: d.picture_entity || "" }
|
||||
: { type: "taskmate/update_child", child_id: d.id, name: d.name.trim(), avatar: d.avatar || "mdi:account-circle",
|
||||
availability_entity: d.availability_entity || "", availability_inverted: !!d.availability_inverted,
|
||||
unavailability_entity: d.unavailability_entity || "",
|
||||
pause_streak_when_unavailable: !!d.pause_streak_when_unavailable, linked_user_id: d.linked_user_id || "",
|
||||
birthday: (d.birthday || "").trim(), age_group: d.age_group || "",
|
||||
presence_entity: d.presence_entity || "" };
|
||||
birthday: (d.birthday || "").trim(), age_group: d.age_group || "", is_adult: !!d.is_adult,
|
||||
presence_entity: d.presence_entity || "",
|
||||
picture_entity: d.picture_entity || "" };
|
||||
const { ok, err, res } = await this._callWS(payload);
|
||||
if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
|
||||
// The PIN goes through its own command so it is hashed server-side and
|
||||
@@ -3572,6 +3580,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
this._zoneCache = {};
|
||||
this._applyDesign();
|
||||
this._bindHaPickers();
|
||||
this._mountBackdateCard();
|
||||
if (this._palette) this._focusPalette();
|
||||
return;
|
||||
}
|
||||
@@ -3632,6 +3641,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
}
|
||||
|
||||
this._applyDesign();
|
||||
this._mountBackdateCard();
|
||||
}
|
||||
|
||||
// A dialog plays its opening animation only when it first appears (#974).
|
||||
@@ -4492,7 +4502,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
<div class="tm-child-head">
|
||||
<div class="tm-avatar">${this._mdi(child.avatar)}</div>
|
||||
<div class="tm-child-name">
|
||||
<h3>${this._esc(child.name || this._t("panel.child_unnamed"))}</h3>
|
||||
<h3>${this._esc(child.name || this._t("panel.child_unnamed"))}${child.is_adult ? ` <span class="tm-pill tm-pill-accent">${this._t("panel.child_adult_pill")}</span>` : ""}</h3>
|
||||
${this._idBadge(child.id)}
|
||||
<div class="tm-meta">${child.availability_entity ? `<code title="${this._esc(child.availability_entity)}">${this._esc(child.availability_entity)}</code>${child.availability_inverted ? ` <em>${this._t("panel.child_inverted")}</em>` : ""}` : `${this._t("panel.child_no_availability")}`}${child.unavailability_entity ? ` · ${this._t("panel.child_busy_label")} <code title="${this._esc(child.unavailability_entity)}">${this._esc(child.unavailability_entity)}</code>` : ""}</div>
|
||||
</div>
|
||||
@@ -4507,6 +4517,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
<div class="tm-card-foot">
|
||||
<button type="button" class="tm-btn tm-btn-sm" data-act="edit-child" data-id="${this._esc(child.id)}">${this._t("panel.btn_edit")}</button>
|
||||
<button type="button" class="tm-btn tm-btn-sm" data-act="reorder-chores-for-child" data-id="${this._esc(child.id)}" title="${this._t("panel.chore_order_title")}">⇅ ${this._t("panel.chore_order_title")}</button>
|
||||
<button type="button" class="tm-btn tm-btn-sm" data-act="backdate-open" data-id="${this._esc(child.id)}" title="${this._esc(this._t("panel.backdate_hint"))}">📅 ${this._t("panel.backdate_button")}</button>
|
||||
<button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="delete-child" data-id="${this._esc(child.id)}">${this._t("panel.btn_delete")}</button>
|
||||
</div>
|
||||
</article>
|
||||
@@ -6484,9 +6495,10 @@ class TaskMatePanel extends HTMLElement {
|
||||
</div>
|
||||
<div class="tm-dash-tile-f">
|
||||
${found ? `<span class="tm-dash-made"><ha-icon icon="mdi:check"></ha-icon>${this._t("panel.dash_created")}</span>` : ""}
|
||||
<span class="tm-dash-sp"></span>
|
||||
${found ? `<button type="button" class="tm-btn tm-btn-sm" data-act="dash-open" data-v="${this._esc(found)}">${this._t("panel.dash_open")}</button>` : ""}
|
||||
<button type="button" class="tm-btn tm-btn-sm ${found ? "" : "tm-btn-raised"}" data-act="dash-new" data-v="${kind}" ${off ? "disabled" : ""}>${this._t("panel.dash_create_btn")}</button>
|
||||
<span class="tm-dash-acts">
|
||||
${found ? `<button type="button" class="tm-btn tm-btn-sm" data-act="dash-open" data-v="${this._esc(found)}">${this._t("panel.dash_open")}</button>` : ""}
|
||||
<button type="button" class="tm-btn tm-btn-sm ${found ? "" : "tm-btn-raised"}" data-act="dash-new" data-v="${kind}" ${off ? "disabled" : ""}>${this._t("panel.dash_create_btn")}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
@@ -6587,8 +6599,11 @@ class TaskMatePanel extends HTMLElement {
|
||||
.tm-dash-tile-b { flex: 1; padding: 12px 14px; }
|
||||
.tm-dash-tile-b p { margin: 0 0 8px; font-size: 13px; color: var(--tm-text-muted); }
|
||||
.tm-dash-tile-b small { color: var(--tm-text-faint); font-size: 12px; }
|
||||
.tm-dash-tile-f { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--tm-border-soft); }
|
||||
.tm-dash-sp { flex: 1; }
|
||||
/* Wraps rather than squeezing: with "Created" + Open + Create… a narrow
|
||||
tile drops the buttons to their own right-aligned line (#1059). */
|
||||
.tm-dash-tile-f { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--tm-border-soft); }
|
||||
.tm-dash-acts { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-inline-start: auto; }
|
||||
.tm-dash-acts .tm-btn { flex: none; }
|
||||
.tm-dash-made { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--tm-positive); white-space: nowrap; --mdc-icon-size: 14px; }
|
||||
.tm-dash-kinds { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 14px; }
|
||||
.tm-dash-kind { display: flex; align-items: center; gap: 10px; text-align: start; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--tm-border); background: var(--tm-surface-0); color: var(--tm-text); font-family: inherit; cursor: pointer; --mdc-icon-size: 22px; }
|
||||
@@ -9688,6 +9703,242 @@ class TaskMatePanel extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Log a past job: one done on an earlier day that nobody ticked. The
|
||||
// backend (complete_chore with completed_date) re-checks every rule here —
|
||||
// the last 7 days, the child's own non-rotating job, due that day, that
|
||||
// day's limit — so this list is a convenience, not the gate.
|
||||
// "Today" is Home Assistant's today: the backend checks the day against its
|
||||
// own clock and zone, so a browser in another zone (or a tablet with a wrong
|
||||
// clock) must not offer a day the backend would call today or too old.
|
||||
_backdateDays() {
|
||||
const tz = this._hass?.config?.time_zone;
|
||||
const lang = this._hass?.locale?.language || this._hass?.language || undefined;
|
||||
let todayKey;
|
||||
try {
|
||||
todayKey = new Date().toLocaleDateString("en-CA", tz ? { timeZone: tz } : undefined);
|
||||
} catch (err) {
|
||||
todayKey = new Date().toLocaleDateString("en-CA");
|
||||
}
|
||||
const [y, m, d0] = todayKey.split("-").map(Number);
|
||||
const days = [];
|
||||
for (let back = 1; back <= 7; back++) {
|
||||
// Plain calendar arithmetic on the date parts, at noon UTC so no zone
|
||||
// can push it onto a neighbouring day.
|
||||
const d = new Date(Date.UTC(y, m - 1, d0 - back, 12));
|
||||
days.push({
|
||||
v: d.toISOString().slice(0, 10),
|
||||
l: d.toLocaleDateString(lang, { weekday: "long", day: "numeric", month: "short", timeZone: "UTC" }),
|
||||
dow: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][d.getUTCDay()],
|
||||
});
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
_backdateChores(childId, dow, dayKey) {
|
||||
// Once TaskMate has described the day, offer exactly what it would accept
|
||||
// that hasn't already used up that week's quota. Until then, a first guess
|
||||
// from the weekday alone (logging still checks everything).
|
||||
const info = this._backdateInfo;
|
||||
if (info && info.day === dayKey) {
|
||||
const ids = (info.loggable && info.loggable[childId]) || [];
|
||||
const week = (info.week_progress && info.week_progress[childId]) || {};
|
||||
return (this._state.chores || []).filter(c => {
|
||||
if (!ids.includes(c.id)) return false;
|
||||
const target = Number(c.weekly_target) || 0;
|
||||
return !target || (Number(week[c.id]) || 0) < target;
|
||||
});
|
||||
}
|
||||
return (this._state.chores || []).filter(c => {
|
||||
if (c.enabled === false) return false;
|
||||
if ((c.disabled_for || []).includes(childId)) return false;
|
||||
if ((c.assigned_to || []).length && !(c.assigned_to || []).includes(childId)) return false;
|
||||
if ((c.assignment_mode || "everyone") !== "everyone") return false;
|
||||
// TaskMate refuses these for a past day: an open-ended job needs the
|
||||
// child's note and a price, and a team's members can't be reconstructed.
|
||||
if (c.open_ended || c.team_size >= 2) return false;
|
||||
// A weekly-target job has no fixed days: the child picks them.
|
||||
if (Number(c.weekly_target) > 0) return true;
|
||||
if ((c.schedule_mode || "specific_days") === "specific_days") {
|
||||
const due = c.due_days || [];
|
||||
return !due.length || due.includes(dow);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async _openBackdateDialog(childId) {
|
||||
const child = (this._state.children || []).find(c => c.id === childId);
|
||||
if (!child) return;
|
||||
const day = this._backdateDays()[0];
|
||||
const first = this._backdateChores(childId, day.dow, day.v)[0];
|
||||
this._backdateKey = "";
|
||||
this._backdateUseList = false;
|
||||
this._backdateCardReady = undefined;
|
||||
this._backdateInfo = null;
|
||||
this._openDialog({ kind: "backdate", data: { child_id: childId, day: day.v, chore_id: first ? first.id : "" } });
|
||||
this._backdateCardReady = await this._loadBackdateCard();
|
||||
if (this._dialog && this._dialog.kind === "backdate") this._render();
|
||||
}
|
||||
|
||||
// The day is shown with the dashboard's own sticker chart card, so jobs are
|
||||
// ticked exactly as the children tick them. The panel isn't a dashboard and
|
||||
// never loads dashboard resources, so the card (and the attribute helper it
|
||||
// reads the sensors through) are imported here, at the same versioned URLs
|
||||
// a dashboard uses — a browser that has both open runs each module once.
|
||||
// If the card can't load, the dialog falls back to a plain job list.
|
||||
async _loadBackdateCard() {
|
||||
if (customElements.get("taskmate-sticker-chart-card")) return true;
|
||||
try {
|
||||
await import(`/taskmate/taskmate-attr-resolver.js?v=${PANEL_VERSION}`);
|
||||
await import(`/taskmate/taskmate-sticker-chart-card.js?v=${PANEL_VERSION}`);
|
||||
return !!customElements.get("taskmate-sticker-chart-card");
|
||||
} catch (err) {
|
||||
console.warn("TaskMate: sticker chart card unavailable in the panel", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_overviewEntity() {
|
||||
const states = (this._hass && this._hass.states) || {};
|
||||
return Object.keys(states).find(id => {
|
||||
const a = id.startsWith("sensor.") && states[id].attributes;
|
||||
return a && a.points_name !== undefined && Array.isArray(a.children);
|
||||
}) || "";
|
||||
}
|
||||
|
||||
// Called after every render: the card element is created once and moved
|
||||
// into the dialog's placeholder, so a re-render never loses its state.
|
||||
//
|
||||
// Nothing here may throw: it runs inside the panel's render, so an error
|
||||
// would leave the whole panel dead until a reload. If the card can't be
|
||||
// built (its module hasn't defined it yet, or the overview sensor is
|
||||
// missing), the dialog falls back to the plain job list instead.
|
||||
_mountBackdateCard() {
|
||||
const host = this.querySelector(".tm-backdate-host");
|
||||
if (!host || !this._dialog || this._dialog.kind !== "backdate") return;
|
||||
// Not defined yet: _openBackdateDialog re-renders once the import settles.
|
||||
if (!customElements.get("taskmate-sticker-chart-card")) return;
|
||||
try {
|
||||
const entity = this._overviewEntity();
|
||||
if (!entity) throw new Error("no TaskMate overview sensor");
|
||||
const d = this._dialog.data;
|
||||
if (!this._backdateCard) {
|
||||
this._backdateCard = document.createElement("taskmate-sticker-chart-card");
|
||||
this._backdateCard.addEventListener("taskmate-day-changed", () => this._refreshBackdateDay());
|
||||
}
|
||||
const card = this._backdateCard;
|
||||
const key = `${d.child_id}|${d.day}`;
|
||||
if (this._backdateKey !== key) {
|
||||
card.setConfig({ entity, child_id: d.child_id, date: d.day, show_claim: false });
|
||||
this._backdateKey = key;
|
||||
// Nothing from the previous day may linger while this one loads.
|
||||
card.dayLoggable = undefined;
|
||||
card.dayWeekProgress = {};
|
||||
card.dayCompletions = [];
|
||||
this._refreshBackdateDay();
|
||||
// Changing the day isn't an unsaved edit: every tick is saved as it's made.
|
||||
this._dialogInitialHash = this._hashDialog();
|
||||
}
|
||||
card.hass = this._hass;
|
||||
if (card.parentNode !== host) host.appendChild(card);
|
||||
} catch (err) {
|
||||
console.warn("TaskMate: sticker chart card couldn't be shown in the panel", err);
|
||||
if (!this._backdateUseList) {
|
||||
this._backdateUseList = true;
|
||||
this._backdateCard = null;
|
||||
// Re-render on the next turn rather than from inside a render.
|
||||
setTimeout(() => { if (this._dialog && this._dialog.kind === "backdate") this._render(); }, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What TaskMate says about the chosen day: its completions, that week's
|
||||
// weekly-target progress and the chores it would accept. Feeds both the
|
||||
// card and the plain-list fallback.
|
||||
async _refreshBackdateDay() {
|
||||
const d = this._dialog && this._dialog.data;
|
||||
if (!d) return;
|
||||
const day = d.day;
|
||||
this._backdateInfoPending = day;
|
||||
const { ok, res, err } = await this._callWS({ type: "taskmate/day_completions", date: day });
|
||||
if (this._backdateInfoPending === day) this._backdateInfoPending = null;
|
||||
if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
|
||||
if (!this._dialog || this._dialog.kind !== "backdate" || this._dialog.data.day !== day) return;
|
||||
this._backdateInfo = {
|
||||
day,
|
||||
loggable: res.loggable || {},
|
||||
week_progress: res.week_progress || {},
|
||||
};
|
||||
const card = this._backdateCard;
|
||||
if (card) {
|
||||
card.dayLoggable = this._backdateInfo.loggable;
|
||||
card.dayWeekProgress = this._backdateInfo.week_progress;
|
||||
card.dayCompletions = res.completions || [];
|
||||
} else {
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
|
||||
_renderBackdateDialog() {
|
||||
const d = this._dialog.data;
|
||||
const child = (this._state.children || []).find(c => c.id === d.child_id) || {};
|
||||
const name = child.name || this._t("panel.child_unnamed");
|
||||
const days = this._backdateDays();
|
||||
const day = days.find(x => x.v === d.day) || days[0];
|
||||
if (this._backdateCardReady !== false && !this._backdateUseList) {
|
||||
return this._dialogShell(
|
||||
this._t("panel.backdate_title", { name }),
|
||||
[
|
||||
`<p class="tm-field-hint">${this._esc(this._t("panel.backdate_hint"))}</p>`,
|
||||
this._select(this._t("panel.backdate_day"), "day", day.v, days, "", true),
|
||||
this._backdateCardReady ? `<div class="tm-backdate-host"></div>` : `<p class="tm-field-hint">…</p>`,
|
||||
].join(""),
|
||||
`<button type="button" class="tm-btn tm-btn-raised" data-act="close-backdate">${this._t("panel.backdate_finish")}</button>`
|
||||
);
|
||||
}
|
||||
// The plain list needs the day described too; fetched once per day.
|
||||
if ((!this._backdateInfo || this._backdateInfo.day !== day.v) && this._backdateInfoPending !== day.v) {
|
||||
setTimeout(() => this._refreshBackdateDay(), 0);
|
||||
}
|
||||
const chores = this._backdateChores(d.child_id, day.dow, day.v);
|
||||
if (!chores.some(c => c.id === d.chore_id)) d.chore_id = chores[0] ? chores[0].id : "";
|
||||
return this._dialogShell(
|
||||
this._t("panel.backdate_title", { name }),
|
||||
[
|
||||
`<p class="tm-field-hint">${this._esc(this._t("panel.backdate_hint_list"))}</p>`,
|
||||
this._select(this._t("panel.backdate_day"), "day", day.v, days, "", true),
|
||||
chores.length
|
||||
? this._select(this._t("panel.backdate_chore"), "chore_id", d.chore_id,
|
||||
chores.map(c => ({ v: c.id, l: `${c.name} (${c.points})` })))
|
||||
: `<p class="tm-field-hint">${this._esc(this._t("panel.backdate_none", { name }))}</p>`,
|
||||
].join(""),
|
||||
`<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
|
||||
<button type="button" class="tm-btn tm-btn-raised" data-act="save-backdate" ${chores.length ? "" : "disabled"}>${this._t("panel.backdate_save")}</button>`
|
||||
);
|
||||
}
|
||||
|
||||
async _saveBackdateDialog() {
|
||||
const d = this._dialog.data;
|
||||
if (!d.child_id || !d.chore_id || !d.day || this._backdateBusy) return;
|
||||
this._backdateBusy = true;
|
||||
try {
|
||||
const { ok, err } = await this._callService("complete_chore", {
|
||||
chore_id: d.chore_id, child_id: d.child_id, as_parent: true, completed_date: d.day,
|
||||
});
|
||||
if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
|
||||
const child = (this._state.children || []).find(c => c.id === d.child_id) || {};
|
||||
const chore = (this._state.chores || []).find(c => c.id === d.chore_id) || {};
|
||||
const day = this._backdateDays().find(x => x.v === d.day);
|
||||
this._closeDialog(true);
|
||||
await this._fetchState();
|
||||
this._showToast("ok", this._t("panel.backdate_done", {
|
||||
chore: chore.name || "", name: child.name || "", day: day ? day.l : d.day,
|
||||
}));
|
||||
} finally {
|
||||
this._backdateBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
_openSwapDialog(choreId) {
|
||||
const c = (this._state.chores || []).find(x => x.id === choreId);
|
||||
if (!c) return;
|
||||
@@ -9729,6 +9980,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
if (this._dialog.kind === "swap") return this._renderSwapDialog();
|
||||
if (this._dialog.kind === "gift") return this._renderGiftDialog();
|
||||
if (this._dialog.kind === "adjust") return this._renderAdjustDialog();
|
||||
if (this._dialog.kind === "backdate") return this._renderBackdateDialog();
|
||||
if (this._dialog.kind === "child") return this._renderChildDialog();
|
||||
if (this._dialog.kind === "chore") return this._renderChoreDialog();
|
||||
if (this._dialog.kind === "reward") return this._renderRewardDialog();
|
||||
@@ -9820,6 +10072,9 @@ class TaskMatePanel extends HTMLElement {
|
||||
[
|
||||
this._field(this._t("panel.child_name_label"), "name", d.name, "text", this._t("panel.child_name_placeholder")),
|
||||
this._iconPickerField(this._t("panel.child_avatar_label"), "avatar", d.avatar),
|
||||
// A grown-up's profile (#1061): auto-approved, hidden from cards by default.
|
||||
this._switch(this._t("panel.child_adult_label"), "is_adult", !!d.is_adult,
|
||||
this._esc(this._t("panel.child_adult_hint"))),
|
||||
this._field(this._t("panel.child_birthday_label"), "birthday", d.birthday, "text",
|
||||
this._esc(this._t("panel.child_birthday_hint"))),
|
||||
// Setup wizard age group (#980): only steers chore suggestions.
|
||||
@@ -9842,6 +10097,10 @@ class TaskMatePanel extends HTMLElement {
|
||||
(this._haUsers || []).map(u => ({ v: u.id, l: u.is_admin ? `${u.name} (admin)` : u.name }))
|
||||
),
|
||||
this._t("panel.child_link_user_hint")),
|
||||
// Any entity with a picture: normally the child's person, but an image
|
||||
// or camera entity works the same way.
|
||||
this._entityPickerField(this._t("panel.child_picture_entity_label"), "picture_entity", d.picture_entity,
|
||||
[], this._t("panel.child_picture_entity_hint"), { withPicture: true }),
|
||||
this._kioskPinField(d),
|
||||
].join(""),
|
||||
`<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
|
||||
@@ -10912,13 +11171,13 @@ class TaskMatePanel extends HTMLElement {
|
||||
else { (this._settingsEntityDraft = this._settingsEntityDraft || {})[field] = value; }
|
||||
}
|
||||
|
||||
_entityPickerField(label, name, value, domains, hintHtml = "") {
|
||||
_entityPickerField(label, name, value, domains, hintHtml = "", { withPicture = false } = {}) {
|
||||
const friendly = value && this._hass?.states?.[value]?.attributes?.friendly_name || "";
|
||||
const icon = value ? (this._hass?.states?.[value]?.attributes?.icon || this._entityDomainIcon(value)) : "";
|
||||
return `
|
||||
<div class="tm-field">
|
||||
<span class="tm-field-label">${this._esc(label)}</span>
|
||||
<div class="tm-ep" data-ep-field="${name}" data-ep-domains="${(domains || []).join(",")}">
|
||||
<div class="tm-ep" data-ep-field="${name}" data-ep-domains="${(domains || []).join(",")}"${withPicture ? ' data-ep-picture="1"' : ""}>
|
||||
${value
|
||||
? `<div class="tm-ep-selected">
|
||||
<ha-icon icon="${this._esc(icon)}" class="tm-ep-selected-icon"></ha-icon>
|
||||
@@ -10945,6 +11204,13 @@ class TaskMatePanel extends HTMLElement {
|
||||
const entities = Object.entries(this._hass.states)
|
||||
.filter(([id, st]) => {
|
||||
if (domains.length && !domains.some(d => id.startsWith(d + "."))) return false;
|
||||
// A photo picker lists only entities that have a picture to show, and
|
||||
// never TaskMate's own child sensors: they show the photo chosen here,
|
||||
// so picking one would make the face its own source.
|
||||
if (picker.dataset.epPicture) {
|
||||
if (!st.attributes?.entity_picture) return false;
|
||||
if (this._hass.entities?.[id]?.platform === "taskmate") return false;
|
||||
}
|
||||
if (!query) return true;
|
||||
const fn = st.attributes?.friendly_name || "";
|
||||
return id.toLowerCase().includes(query) || fn.toLowerCase().includes(query);
|
||||
|
||||
@@ -57,6 +57,14 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
return fn ? fn(this.hass, key, params) : key;
|
||||
}
|
||||
|
||||
/** Avatar: child's photo → MDI avatar → default icon. */
|
||||
_avatarGlyph(source, fallback = "mdi:account-circle", cls = "tm-face-img") {
|
||||
const face = window.__taskmate_child_visual(source);
|
||||
return face.kind === "image"
|
||||
? html`<img class="${cls}" src="${face.url}" alt="" loading="lazy">`
|
||||
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : fallback}"></ha-icon>`;
|
||||
}
|
||||
|
||||
// Build "Chore · Child · date" for the lightbox caption (missing parts drop).
|
||||
_photoCaption(chore, child, iso) {
|
||||
let when = "";
|
||||
@@ -587,6 +595,9 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
const children = attrs.children || [];
|
||||
// Grown-ups (#1061) only drop out of the per-child lists; approvals and
|
||||
// rotation still need every profile to name whoever did the job.
|
||||
const family = (window.__taskmate_family_children ? window.__taskmate_family_children(children, this.config) : children);
|
||||
const chores = attrs.chores || [];
|
||||
const completions = attrs.todays_completions || [];
|
||||
// Pending approvals come from the full pending list (not filtered to
|
||||
@@ -646,11 +657,11 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
${this._activeSection === "overview" ? this._renderOverview(children, chores, completions, pointsIcon, pointsName) : ''}
|
||||
${this._activeSection === "overview" ? this._renderOverview(family, chores, completions, pointsIcon, pointsName) : ''}
|
||||
${this._activeSection === "rotation" ? this._renderRotation(rotationChores, children, attrs) : ''}
|
||||
${this._activeSection === "approvals" ? this._renderApprovals(pendingCompletions, children, chores, pointsIcon) : ''}
|
||||
${this._activeSection === "claims" ? this._renderClaims(pendingRewardClaims, pointsIcon) : ''}
|
||||
${this._activeSection === "points" ? this._renderPoints(children, pointsIcon, pointsName) : ''}
|
||||
${this._activeSection === "points" ? this._renderPoints(family, pointsIcon, pointsName) : ''}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -665,12 +676,14 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
_av(name, avatar, tone, size) {
|
||||
const a = avatar || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${name || ''}">`
|
||||
// `source` is a child object (so the child's photo travels with
|
||||
// the avatar) or a bare icon string; the shared helper accepts either.
|
||||
_av(name, source, tone, size) {
|
||||
const face = window.__taskmate_child_visual(source);
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${name || ''}">`
|
||||
: (name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
@@ -728,6 +741,9 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
const children = attrs.children || [];
|
||||
// Grown-ups (#1061) only drop out of the per-child lists; approvals and
|
||||
// rotation still need every profile to name whoever did the job.
|
||||
const family = (window.__taskmate_family_children ? window.__taskmate_family_children(children, this.config) : children);
|
||||
const chores = attrs.chores || [];
|
||||
const completions = attrs.todays_completions || [];
|
||||
const pendingCompletions = attrs.chore_completions || completions.filter(c => !c.approved);
|
||||
@@ -762,11 +778,11 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
|
||||
let body;
|
||||
const sec = this._activeSection;
|
||||
if (sec === "overview") body = this._dOverview(design, children, chores, completions, attrs, pointsIcon);
|
||||
if (sec === "overview") body = this._dOverview(design, family, chores, completions, attrs, pointsIcon);
|
||||
else if (sec === "rotation") body = this._dRotation(design, rotationChores, children, attrs);
|
||||
else if (sec === "approvals") body = this._dApprovals(design, pendingCompletions, children, chores, pointsIcon);
|
||||
else if (sec === "claims") body = this._dClaims(design, pendingRewardClaims, pointsIcon);
|
||||
else if (sec === "points") body = this._dPoints(design, children, pointsIcon, pointsName);
|
||||
else if (sec === "points") body = this._dPoints(design, family, pointsIcon, pointsName);
|
||||
|
||||
return html`<ha-card class="tmd" style="--hd:${hd}">
|
||||
${this._dHeader(hd, totalPending)}
|
||||
@@ -832,7 +848,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
if (design === "console") {
|
||||
return html`
|
||||
<div class="row pd-ov-cn" style="--ac:${tone}">
|
||||
${this._av(child.name, child.avatar, tone, 34)}
|
||||
${this._av(child.name, child, tone, 34)}
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="pd-name">${child.name} <span class="muted pd-mini">${approved}/${total}</span></div>
|
||||
<div class="bar" style="margin-top:6px"><i style="width:${pct}%"></i></div>
|
||||
@@ -844,7 +860,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
const isParent = window.__taskmate_is_parent(this.hass);
|
||||
return html`
|
||||
<div class="row pd-ov-cp" style="--ac:${tone}">
|
||||
${this._av(child.name, child.avatar, tone, 36)}
|
||||
${this._av(child.name, child, tone, 36)}
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<span class="pd-name-cp">${child.name}</span>
|
||||
@@ -862,7 +878,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
return html`
|
||||
<div class="pd-ov-pl" style="--ac:${tone}">
|
||||
<div class="row">
|
||||
${this._av(child.name, child.avatar, tone, 42)}
|
||||
${this._av(child.name, child, tone, 42)}
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="pd-name">${child.name}</div>
|
||||
<div class="muted pd-sub">${_ltrNums(this._t('dashboard.chores_done', { done: approved, total }, `${approved} / ${total} chores done`))}</div>
|
||||
@@ -899,7 +915,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
@click="${() => this._handleReject(comp.completion_id)}">✕</button>`;
|
||||
return html`
|
||||
<div class="row pd-item ${isLoading ? 'loading' : ''}" style="--ac:${tone}">
|
||||
${this._av(child?.name || '?', child?.avatar, tone, 36)}
|
||||
${this._av(child?.name || '?', child, tone, 36)}
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="pd-item-title">${comp.chore_name || choreMap[comp.chore_id]?.name || this._t('common.unknown')}</div>
|
||||
<div class="muted pd-item-sub">${child?.name || this._t('common.unknown')}${time ? ` · ${time}` : ""} · +${pts}</div>
|
||||
@@ -955,7 +971,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
const loading = !!this._loading[key];
|
||||
return html`
|
||||
<div class="row pd-item ${loading ? 'loading' : ''}" style="--ac:${tone}">
|
||||
${this._av(currentChild?.name || '?', currentChild?.avatar || 'mdi:rotate-3d-variant', tone, 36)}
|
||||
${this._av(currentChild?.name || '?', currentChild?.avatar ? currentChild : 'mdi:rotate-3d-variant', tone, 36)}
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="pd-item-title">${chore.name}</div>
|
||||
<div class="muted pd-item-sub">${currentChild?.name || unknown} · ${chore.assignment_mode}${group ? ` · ${group.policy === 'sticky' ? '🔗' : '🔀'} ${group.name}` : ""}</div>
|
||||
@@ -976,7 +992,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
const tone = this._designTone(i);
|
||||
return html`
|
||||
<div class="row pd-item" style="--ac:${tone}">
|
||||
${this._av(child.name, child.avatar, tone, 36)}
|
||||
${this._av(child.name, child, tone, 36)}
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="pd-item-title">${child.name}</div>
|
||||
<div class="muted pd-item-sub">${child.points} ${pointsName}</div>
|
||||
@@ -1078,7 +1094,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
return html`
|
||||
<div class="child-tile ${isParent ? 'tm-expandable' : ''}">
|
||||
<div class="child-avatar">
|
||||
<ha-icon icon="${child.avatar || 'mdi:account-circle'}"></ha-icon>
|
||||
${this._avatarGlyph(child)}
|
||||
</div>
|
||||
<div class="child-tile-main"
|
||||
@click="${isParent ? () => { this._expanded = { ...this._expanded, [child.id]: !isOpen }; this.requestUpdate(); } : null}">
|
||||
@@ -1160,7 +1176,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
return html`
|
||||
<div class="approval-item ${loading ? 'loading' : ''}">
|
||||
<div class="approval-child-avatar" style="background: linear-gradient(135deg, #16a085, #1abc9c);">
|
||||
<ha-icon icon="${currentChild?.avatar || 'mdi:rotate-3d-variant'}"></ha-icon>
|
||||
${this._avatarGlyph(currentChild, "mdi:rotate-3d-variant")}
|
||||
</div>
|
||||
<div class="approval-info">
|
||||
<span class="approval-chore">${chore.name}</span>
|
||||
@@ -1211,7 +1227,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
return html`
|
||||
<div class="approval-item ${isLoading ? 'loading' : ''}">
|
||||
<div class="approval-child-avatar">
|
||||
<ha-icon icon="${child?.avatar || 'mdi:account-circle'}"></ha-icon>
|
||||
${this._avatarGlyph(child)}
|
||||
</div>
|
||||
<div class="approval-info">
|
||||
<div class="approval-chore">${comp.chore_name || chore?.name || this._t('common.unknown')}</div>
|
||||
@@ -1294,7 +1310,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
${children.map(child => html`
|
||||
<div class="quick-points-row">
|
||||
<div class="qp-avatar">
|
||||
<ha-icon icon="${child.avatar || 'mdi:account-circle'}"></ha-icon>
|
||||
${this._avatarGlyph(child)}
|
||||
</div>
|
||||
<span class="qp-name">${child.name}</span>
|
||||
<span class="qp-points">
|
||||
@@ -1490,6 +1506,7 @@ class TaskMateParentDashboardCardEditor extends LitElement {
|
||||
|
||||
_computeLabel = (entry) => {
|
||||
const labels = {
|
||||
show_adults: this._t('common.editor.show_adults'),
|
||||
entity: this._t('dashboard.editor.entity_label'),
|
||||
title: this._t('dashboard.editor.title_label'),
|
||||
card_design: this._t('common.design.field_label'),
|
||||
@@ -1501,6 +1518,7 @@ class TaskMateParentDashboardCardEditor extends LitElement {
|
||||
|
||||
_computeHelper = (entry) => {
|
||||
const helpers = {
|
||||
show_adults: this._t('common.editor.show_adults_helper'),
|
||||
entity: this._t('dashboard.editor.entity_helper'),
|
||||
quick_points_amount: this._t('dashboard.editor.quick_points_helper'),
|
||||
};
|
||||
@@ -1519,8 +1537,8 @@ class TaskMateParentDashboardCardEditor extends LitElement {
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.data=${this.config.show_adults === true ? { ...data, show_adults: true } : data}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
|
||||
@@ -725,7 +725,7 @@ class TaskMatePointsCard extends LitElement {
|
||||
}
|
||||
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
const children = attrs.children || [];
|
||||
const children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
const pointsName = attrs.points_name || this._t("common.stars");
|
||||
|
||||
@@ -783,20 +783,24 @@ class TaskMatePointsCard extends LitElement {
|
||||
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
/** Avatar + photo, read off the child's own sensor. */
|
||||
_childAvatarValue(child) {
|
||||
const childEntityId = Object.keys(this.hass.states).find(
|
||||
(eid) => this.hass.states[eid].attributes?.child_id === child.id
|
||||
);
|
||||
const childEntity = childEntityId ? this.hass.states[childEntityId] : null;
|
||||
return childEntity?.attributes?.avatar || "mdi:account-circle";
|
||||
const attrs = childEntityId ? this.hass.states[childEntityId].attributes : null;
|
||||
return {
|
||||
avatar: attrs?.avatar || "mdi:account-circle",
|
||||
avatar_image: attrs?.avatar_image || "",
|
||||
};
|
||||
}
|
||||
|
||||
_av(child, tone, size) {
|
||||
const a = this._childAvatarValue(child);
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${child.name}">`
|
||||
const face = window.__taskmate_child_visual(this._childAvatarValue(child));
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: (child.name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
@@ -925,17 +929,15 @@ class TaskMatePointsCard extends LitElement {
|
||||
const isLoading = this._loading[child.id];
|
||||
|
||||
// Get child entity for avatar
|
||||
const childEntityId = Object.keys(this.hass.states).find(
|
||||
(eid) => this.hass.states[eid].attributes?.child_id === child.id
|
||||
);
|
||||
const childEntity = childEntityId ? this.hass.states[childEntityId] : null;
|
||||
const avatar = childEntity?.attributes?.avatar || "mdi:account-circle";
|
||||
const face = window.__taskmate_child_visual(this._childAvatarValue(child));
|
||||
|
||||
return html`
|
||||
<div class="child-row ${isLoading ? "loading" : ""}">
|
||||
<div class="child-info">
|
||||
<div class="child-avatar">
|
||||
<ha-icon icon="${avatar}"></ha-icon>
|
||||
${face.kind === "image"
|
||||
? html`<img class="tm-face-img" src="${face.url}" alt="" loading="lazy">`
|
||||
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : "mdi:account-circle"}"></ha-icon>`}
|
||||
</div>
|
||||
<div class="child-details">
|
||||
<div class="child-name">${child.name}</div>
|
||||
@@ -1276,6 +1278,7 @@ class TaskMatePointsCardEditor extends LitElement {
|
||||
|
||||
_computeLabel = (entry) => {
|
||||
const labels = {
|
||||
show_adults: this._t('common.editor.show_adults'),
|
||||
entity: this._t('points_card.editor.entity_label'),
|
||||
title: this._t('points_card.editor.title_label'),
|
||||
card_design: this._t('common.design.field_label'),
|
||||
@@ -1288,6 +1291,7 @@ class TaskMatePointsCardEditor extends LitElement {
|
||||
|
||||
_computeHelper = (entry) => {
|
||||
const helpers = {
|
||||
show_adults: this._t('common.editor.show_adults_helper'),
|
||||
entity: this._t('points_card.editor.entity_helper'),
|
||||
quick_add_amounts: this._t('points_card.editor.add_buttons_helper'),
|
||||
quick_remove_amounts: this._t('points_card.editor.remove_buttons_helper'),
|
||||
@@ -1313,8 +1317,8 @@ class TaskMatePointsCardEditor extends LitElement {
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.data=${this.config.show_adults === true ? { ...data, show_adults: true } : data}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
|
||||
@@ -59,8 +59,7 @@ function weeklyPoints(child, tz) {
|
||||
}
|
||||
|
||||
function childAvatar(child, colour) {
|
||||
const av = child.avatar || "mdi:account-circle";
|
||||
const isIcon = av.startsWith("mdi:");
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
const initials = (child.name || "?")
|
||||
.split(" ")
|
||||
.map(w => w[0])
|
||||
@@ -69,10 +68,10 @@ function childAvatar(child, colour) {
|
||||
.toUpperCase();
|
||||
return html`
|
||||
<div class="avatar" style="background:${colour}">
|
||||
${isIcon
|
||||
? html`<ha-icon icon="${av}"></ha-icon>`
|
||||
: av
|
||||
? html`<img src="${av}" alt="${child.name}">`
|
||||
${face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: initials}
|
||||
</div>`;
|
||||
}
|
||||
@@ -625,7 +624,8 @@ class TaskMatePointsDisplayCard extends LitElement {
|
||||
|
||||
_allChildren() {
|
||||
const s = this._stateObj();
|
||||
return s ? getChildren(s) : [];
|
||||
const all = s ? getChildren(s) : [];
|
||||
return window.__taskmate_family_children ? window.__taskmate_family_children(all, this.config) : all;
|
||||
}
|
||||
|
||||
_isCareerMode() {
|
||||
@@ -961,11 +961,11 @@ class TaskMatePointsDisplayCard extends LitElement {
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
_av(child, tone, size) {
|
||||
const a = child.avatar || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${child.name}">`
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: (child.name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
@@ -1242,6 +1242,7 @@ class TaskMatePointsDisplayCardEditor extends LitElement {
|
||||
|
||||
_computeLabel = (entry) => {
|
||||
const labels = {
|
||||
show_adults: this._t('common.editor.show_adults'),
|
||||
entity: this._t('points_display.editor.entity_label'),
|
||||
title: this._t('common.editor.card_title'),
|
||||
mode: this._t('points_display.editor.mode_label'),
|
||||
@@ -1255,7 +1256,7 @@ class TaskMatePointsDisplayCardEditor extends LitElement {
|
||||
return labels[entry.name] ?? entry.name;
|
||||
};
|
||||
|
||||
_computeHelper = () => '';
|
||||
_computeHelper = (entry) => (entry.name === 'show_adults' ? this._t('common.editor.show_adults_helper') : '');
|
||||
|
||||
render() {
|
||||
if (!this.config) return html``;
|
||||
@@ -1274,8 +1275,8 @@ class TaskMatePointsDisplayCardEditor extends LitElement {
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.data=${this.config.show_adults === true ? { ...data, show_adults: true } : data}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
|
||||
@@ -430,11 +430,12 @@ class TaskMateRecapCard extends LitElement {
|
||||
}
|
||||
|
||||
_avatar(child) {
|
||||
const a = child?.avatar || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: /^(https?:|\/)/.test(a)
|
||||
? html`<img src="${a}" alt="">`
|
||||
// The shared face helper: the child's photo, else their MDI avatar.
|
||||
const face = window.__taskmate_child_visual ? window.__taskmate_child_visual(child) : { kind: "none" };
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image" && /^(https?:|\/)/.test(face.url)
|
||||
? html`<img src="${face.url}" alt="">`
|
||||
: (child?.name || "?").slice(0, 1).toUpperCase();
|
||||
return html`<span class="who-av">${inner}</span>`;
|
||||
}
|
||||
|
||||
@@ -818,12 +818,12 @@ class TaskMateReorderCard extends LitElement {
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
_designAv(child, size) {
|
||||
const a = child.avatar || "";
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
const tone = "var(--tmd-c1)";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${child.name}">`
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: (child.name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<span class="av" style="--av:${size}px;--ac:${tone}">${inner}</span>`;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,14 @@ class TaskMateRewardProgressCard extends LitElement {
|
||||
return fn ? fn(this.hass, key, params) : key;
|
||||
}
|
||||
|
||||
/** Avatar: child's photo → MDI avatar → default icon. */
|
||||
_avatarGlyph(source, fallback = "mdi:account-circle", cls = "tm-face-img") {
|
||||
const face = window.__taskmate_child_visual(source);
|
||||
return face.kind === "image"
|
||||
? html`<img class="${cls}" src="${face.url}" alt="" loading="lazy">`
|
||||
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : fallback}"></ha-icon>`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
const base = css`
|
||||
:host { display: block; }
|
||||
@@ -489,7 +497,7 @@ class TaskMateRewardProgressCard extends LitElement {
|
||||
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
const rewards = attrs.rewards || [];
|
||||
const children = attrs.children || [];
|
||||
const children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
const pointsName = attrs.points_name || this._t("common.points");
|
||||
|
||||
@@ -601,7 +609,7 @@ class TaskMateRewardProgressCard extends LitElement {
|
||||
<div class="child-progress-header">
|
||||
<div class="child-progress-left">
|
||||
<div class="child-avatar">
|
||||
<ha-icon icon="${child.avatar || 'mdi:account-circle'}"></ha-icon>
|
||||
${this._avatarGlyph(child)}
|
||||
</div>
|
||||
<div>
|
||||
<div class="child-progress-name">${child.name}</div>
|
||||
@@ -661,7 +669,7 @@ class TaskMateRewardProgressCard extends LitElement {
|
||||
${children.map(child => html`
|
||||
<div class="jackpot-contributor">
|
||||
<div class="mini-avatar">
|
||||
<ha-icon icon="${child.avatar || 'mdi:account-circle'}"></ha-icon>
|
||||
${this._avatarGlyph(child)}
|
||||
</div>
|
||||
<span>${child.name}: ${child.points}</span>
|
||||
</div>
|
||||
@@ -716,11 +724,11 @@ class TaskMateRewardProgressCard extends LitElement {
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
_av(child, tone, size) {
|
||||
const a = (child && child.avatar) || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${child.name}">`
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: ((child && child.name) || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
@@ -743,7 +751,7 @@ class TaskMateRewardProgressCard extends LitElement {
|
||||
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
const rewards = attrs.rewards || [];
|
||||
const children = attrs.children || [];
|
||||
const children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
const pointsName = attrs.points_name || this._t("common.points");
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
|
||||
@@ -930,6 +938,7 @@ class TaskMateRewardProgressCardEditor extends LitElement {
|
||||
|
||||
_computeLabel = (entry) => {
|
||||
const labels = {
|
||||
show_adults: this._t('common.editor.show_adults'),
|
||||
entity: this._t('common.editor.overview_entity'),
|
||||
title: this._t('common.editor.card_title'),
|
||||
reward_id: this._t('reward_progress.editor.reward_to_display'),
|
||||
@@ -941,6 +950,7 @@ class TaskMateRewardProgressCardEditor extends LitElement {
|
||||
|
||||
_computeHelper = (entry) => {
|
||||
const helpers = {
|
||||
show_adults: this._t('common.editor.show_adults_helper'),
|
||||
entity: this._t('common.editor.overview_entity_helper'),
|
||||
reward_id: this._t('reward_progress.editor.reward_helper'),
|
||||
child_id: this._t('reward_progress.editor.child_helper'),
|
||||
@@ -960,8 +970,8 @@ class TaskMateRewardProgressCardEditor extends LitElement {
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.data=${this.config.show_adults === true ? { ...data, show_adults: true } : data}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
|
||||
@@ -66,6 +66,14 @@ class TaskMateRewardsCard extends LitElement {
|
||||
return fn ? fn(this.hass, key, params) : key;
|
||||
}
|
||||
|
||||
/** Avatar: child's photo → MDI avatar → default icon. */
|
||||
_avatarGlyph(source, fallback = "mdi:account-circle", cls = "tm-face-img") {
|
||||
const face = window.__taskmate_child_visual(source);
|
||||
return face.kind === "image"
|
||||
? html`<img class="${cls}" src="${face.url}" alt="" loading="lazy">`
|
||||
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : fallback}"></ha-icon>`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
const base = css`
|
||||
:host {
|
||||
@@ -1104,14 +1112,58 @@ class TaskMateRewardsCard extends LitElement {
|
||||
// in-card selection to the first child so the claim button renders (and the
|
||||
// child tabs have a selection). Without this, childId stays null and no claim
|
||||
// button is ever shown, making rewards impossible to claim.
|
||||
if (this.hass && this.config && !this.config.child_id && !this._selectedChildId) {
|
||||
// Adult profiles (#1071): the default is the first family child, and a
|
||||
// selection that's no longer listed (show_adults switched off) moves back.
|
||||
if (this.hass && this.config && !this.config.child_id) {
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity))
|
||||
|| this.hass.states?.[this.config.entity]?.attributes || {};
|
||||
const kids = attrs.children || [];
|
||||
if (kids.length) this._selectedChildId = kids[0].id;
|
||||
const kids = this._family(attrs.children || []);
|
||||
if (kids.length && !kids.some((c) => c.id === this._selectedChildId)) this._selectedChildId = kids[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
// The children a family-wide view lists (#1061/#1071): adults only with
|
||||
// show_adults, and everyone when the card is pinned to a child_id.
|
||||
_family(children) {
|
||||
return (window.__taskmate_family_children ? window.__taskmate_family_children(children, this.config) : children);
|
||||
}
|
||||
|
||||
/**
|
||||
* Who a reward row lists (#1071). Non-jackpot rewards and the child badges
|
||||
* follow the family filter. A jackpot's breakdown does too, except that an
|
||||
* adult who has put points into it stays listed — dropping them would leave
|
||||
* the segments and the total not adding up.
|
||||
*/
|
||||
_rewardPeople(reward, children) {
|
||||
const assignedTo = reward.assigned_to || [];
|
||||
const family = this._family(children);
|
||||
const assigned = (c) => assignedTo.length === 0 || assignedTo.includes(c.id);
|
||||
const relevantChildren = family.filter(assigned);
|
||||
const listed = new Set(family.map((c) => c.id));
|
||||
const hidden = new Set(children.filter((c) => !listed.has(c.id)).map((c) => c.id));
|
||||
const allocations = reward.pool_allocations || {};
|
||||
const contributors = relevantChildren.concat(
|
||||
children.filter((c) => hidden.has(c.id) && assigned(c) && (allocations[c.id] || 0) !== 0)
|
||||
);
|
||||
return {
|
||||
relevantChildren,
|
||||
contributors,
|
||||
badgeIds: assignedTo.filter((cid) => !hidden.has(cid)),
|
||||
};
|
||||
}
|
||||
|
||||
// The rewards the card lists: those open to everyone plus those assigned to
|
||||
// the pinned child, or to the selected tab when nothing is pinned (#1074).
|
||||
// Without the tab filter a child was offered a sibling's reward to claim.
|
||||
_rewardsFor(allRewards) {
|
||||
const childId = this.config.child_id || this._selectedChildId;
|
||||
if (!childId) return allRewards;
|
||||
return allRewards.filter((reward) => {
|
||||
const assignedTo = reward.assigned_to || [];
|
||||
return assignedTo.length === 0 || assignedTo.includes(childId);
|
||||
});
|
||||
}
|
||||
|
||||
_selectChild(id) {
|
||||
this._selectedChildId = id;
|
||||
}
|
||||
@@ -1125,7 +1177,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
${children.map((c) => html`
|
||||
<div class="child-tab ${activeId === c.id ? 'selected' : ''}"
|
||||
@click=${() => this._selectChild(c.id)}>
|
||||
<ha-icon icon="${c.avatar || 'mdi:account-circle'}"></ha-icon>
|
||||
${this._avatarGlyph(c, "mdi:account-circle", "tm-face-chip")}
|
||||
${c.name}
|
||||
</div>
|
||||
`)}
|
||||
@@ -1176,15 +1228,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
const pointsName = attrs.points_name || this._t("common.stars");
|
||||
|
||||
// Filter rewards based on child_id if configured
|
||||
let rewards = allRewards;
|
||||
if (this.config.child_id) {
|
||||
rewards = allRewards.filter((reward) => {
|
||||
const assignedTo = reward.assigned_to || [];
|
||||
// Show reward if assigned_to is empty (available to all) OR contains the child_id
|
||||
return assignedTo.length === 0 || assignedTo.includes(this.config.child_id);
|
||||
});
|
||||
}
|
||||
const rewards = this._rewardsFor(allRewards);
|
||||
|
||||
// Sort rewards: jackpot first, then by calculated cost ascending within each group
|
||||
const sortedRewards = [...rewards].sort((a, b) => {
|
||||
@@ -1226,7 +1270,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
${this._renderChildTabs(children)}
|
||||
${this._renderChildTabs(this._family(children))}
|
||||
|
||||
${anyPoolReward && activeChild ? this._renderSpendableBanner(activeChild, pointsIcon, pointsName) : ''}
|
||||
|
||||
@@ -1353,10 +1397,8 @@ class TaskMateRewardsCard extends LitElement {
|
||||
// All costs are static — just use reward.cost
|
||||
const displayCost = this._getDisplayCost(reward);
|
||||
|
||||
// Get relevant children for this reward
|
||||
const relevantChildren = assignedTo.length === 0
|
||||
? children
|
||||
: children.filter(c => assignedTo.includes(c.id));
|
||||
// Get relevant children for this reward (family-filtered, #1071)
|
||||
const { relevantChildren, contributors, badgeIds } = this._rewardPeople(reward, children);
|
||||
|
||||
// Determine active child context
|
||||
const childId = this.config?.child_id || this._selectedChildId;
|
||||
@@ -1373,7 +1415,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
let childContributions = [];
|
||||
|
||||
if (isJackpot) {
|
||||
relevantChildren.forEach((child, index) => {
|
||||
contributors.forEach((child, index) => {
|
||||
// In pool mode, show allocated points only; in wallet mode show wallet points
|
||||
const points = enablePoolMode
|
||||
? (poolAllocations[child.id] || 0)
|
||||
@@ -1390,11 +1432,9 @@ class TaskMateRewardsCard extends LitElement {
|
||||
if (enablePoolMode) {
|
||||
// Pool mode: show this child's allocation for this reward
|
||||
currentStars = childId ? (poolAllocations[childId] || 0) : 0;
|
||||
} else if (this.config.child_id) {
|
||||
const filteredChild = children.find(c => c.id === this.config.child_id);
|
||||
currentStars = filteredChild ? (filteredChild.points || 0) : 0;
|
||||
} else if (relevantChildren.length > 0) {
|
||||
currentStars = relevantChildren[0].points || 0;
|
||||
} else {
|
||||
// The pinned or selected child, so switching tabs moves the bar (#1068)
|
||||
currentStars = relevantChild ? (relevantChild.points || 0) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1478,7 +1518,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
<div class="assigned-children">
|
||||
${assignedTo.length === 0
|
||||
? html`<span class="child-badge all-children">${this._t('rewards.all_children')}</span>`
|
||||
: assignedTo.map(
|
||||
: badgeIds.map(
|
||||
(cid) =>
|
||||
html`<span class="child-badge">${childMap[cid] || cid}</span>`
|
||||
)}
|
||||
@@ -1837,11 +1877,11 @@ class TaskMateRewardsCard extends LitElement {
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
_av(child, tone, size) {
|
||||
const a = (child && child.avatar) || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${child.name}">`
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: ((child && child.name) || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
@@ -1852,7 +1892,6 @@ class TaskMateRewardsCard extends LitElement {
|
||||
* _getDisplayCost — static only). Returns everything the designed rows need.
|
||||
*/
|
||||
_rewardData(reward, children) {
|
||||
const assignedTo = reward.assigned_to || [];
|
||||
const isJackpot = reward.is_jackpot || false;
|
||||
const cardForcesPool = this.config.enable_pool_mode === true;
|
||||
// Jackpot rewards are always pool-mode (#552): everyone deposits into the
|
||||
@@ -1861,9 +1900,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
const enablePoolMode = cardForcesPool || reward.pool_enabled === true || isJackpot;
|
||||
const displayCost = this._getDisplayCost(reward);
|
||||
|
||||
const relevantChildren = assignedTo.length === 0
|
||||
? children
|
||||
: children.filter(c => assignedTo.includes(c.id));
|
||||
const { relevantChildren, contributors } = this._rewardPeople(reward, children);
|
||||
|
||||
const childId = this.config?.child_id || this._selectedChildId;
|
||||
const relevantChild = childId
|
||||
@@ -1877,7 +1914,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
const childContributions = [];
|
||||
|
||||
if (isJackpot) {
|
||||
relevantChildren.forEach((child, index) => {
|
||||
contributors.forEach((child, index) => {
|
||||
const points = enablePoolMode
|
||||
? (poolAllocations[child.id] || 0)
|
||||
: (child.points || 0);
|
||||
@@ -1891,11 +1928,9 @@ class TaskMateRewardsCard extends LitElement {
|
||||
});
|
||||
} else if (enablePoolMode) {
|
||||
currentStars = childId ? (poolAllocations[childId] || 0) : 0;
|
||||
} else if (this.config.child_id) {
|
||||
const fc = children.find(c => c.id === this.config.child_id);
|
||||
currentStars = fc ? (fc.points || 0) : 0;
|
||||
} else if (relevantChildren.length > 0) {
|
||||
currentStars = relevantChildren[0].points || 0;
|
||||
} else {
|
||||
// The pinned or selected child, so switching tabs moves the bar (#1068)
|
||||
currentStars = relevantChild ? (relevantChild.points || 0) : 0;
|
||||
}
|
||||
|
||||
const percentage = displayCost > 0 ? Math.min((currentStars / displayCost) * 100, 100) : 0;
|
||||
@@ -1978,13 +2013,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
const pointsName = attrs.points_name || this._t("common.stars");
|
||||
|
||||
let rewards = allRewards;
|
||||
if (this.config.child_id) {
|
||||
rewards = allRewards.filter((reward) => {
|
||||
const assignedTo = reward.assigned_to || [];
|
||||
return assignedTo.length === 0 || assignedTo.includes(this.config.child_id);
|
||||
});
|
||||
}
|
||||
const rewards = this._rewardsFor(allRewards);
|
||||
|
||||
const sortedRewards = [...rewards].sort((a, b) => {
|
||||
if (a.is_jackpot && !b.is_jackpot) return -1;
|
||||
@@ -1999,16 +2028,20 @@ class TaskMateRewardsCard extends LitElement {
|
||||
|
||||
const header = this._designHeader(rewards.length);
|
||||
|
||||
const body = sortedRewards.length === 0
|
||||
? html`<div class="tmd-empty">${this._t('rewards.empty_title')}<br>${this._t('rewards.empty_subtitle')}</div>`
|
||||
const empty = html`<div class="tmd-empty">${this._t('rewards.empty_title')}<br>${this._t('rewards.empty_subtitle')}</div>`;
|
||||
// The tabs stay up when only the selected child has nothing, so the other
|
||||
// children can still be picked (#1074).
|
||||
const body = allRewards.length === 0
|
||||
? empty
|
||||
: html`
|
||||
${this._designChildTabs(children)}
|
||||
${this._designChildTabs(this._family(children))}
|
||||
${(anyPoolReward && activeChild) || activeChild
|
||||
? this._designChildSelector(activeChild, pointsIcon, pointsName, design)
|
||||
: ''}
|
||||
${sortedRewards.length === 0 ? empty : html`
|
||||
<div class="rw-list ${this.config.expand_to_fit === false ? 'rw-scroll' : ''}">
|
||||
${sortedRewards.map((r) => this._designRewardRow(r, children, pointsIcon, pointsName, design))}
|
||||
</div>`;
|
||||
</div>`}`;
|
||||
|
||||
return html`
|
||||
<ha-card class="tmd" style="--hd:${hd}">
|
||||
@@ -2060,7 +2093,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
const badges = showChildBadges && !d.isJackpot
|
||||
? (assignedTo.length === 0
|
||||
? html`<span class="chip soft rw-badge rw-badge-all">${this._t('rewards.all_children')}</span>`
|
||||
: assignedTo.map((cid) => html`<span class="chip rw-badge">${childMap[cid] || cid}</span>`))
|
||||
: this._rewardPeople(reward, children).badgeIds.map((cid) => html`<span class="chip rw-badge">${childMap[cid] || cid}</span>`))
|
||||
: '';
|
||||
|
||||
const costBadge = html`<span class="chip rw-cost"><ha-icon style="--mdc-icon-size:1.1em;vertical-align:-0.15em" icon="${pointsIcon}"></ha-icon> ${d.displayCost} ${pointsName}</span>`;
|
||||
@@ -2349,6 +2382,7 @@ class TaskMateRewardsCardEditor extends LitElement {
|
||||
show_child_badges: this._t('rewards.editor.show_child_badges'),
|
||||
enable_pool_mode: this._t('rewards.editor.enable_pool_mode'),
|
||||
expand_to_fit: this._t('rewards.editor.expand_to_fit'),
|
||||
show_adults: this._t('common.editor.show_adults'),
|
||||
};
|
||||
return labels[schemaEntry.name] ?? schemaEntry.name;
|
||||
};
|
||||
@@ -2361,6 +2395,7 @@ class TaskMateRewardsCardEditor extends LitElement {
|
||||
show_child_badges: this._t('rewards.editor.show_child_badges_helper'),
|
||||
enable_pool_mode: this._t('rewards.editor.enable_pool_mode_helper'),
|
||||
expand_to_fit: this._t('rewards.editor.expand_to_fit_helper'),
|
||||
show_adults: this._t('common.editor.show_adults_helper'),
|
||||
};
|
||||
return helpers[schemaEntry.name] ?? '';
|
||||
};
|
||||
@@ -2378,13 +2413,14 @@ class TaskMateRewardsCardEditor extends LitElement {
|
||||
show_child_badges: this.config.show_child_badges !== false,
|
||||
enable_pool_mode: this.config.enable_pool_mode === true,
|
||||
expand_to_fit: this.config.expand_to_fit !== false,
|
||||
show_adults: this.config.show_adults === true,
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
@@ -2423,7 +2459,7 @@ class TaskMateRewardsCardEditor extends LitElement {
|
||||
} else if (key === 'show_child_badges' && value === true) {
|
||||
// Default is true — omit to keep config minimal
|
||||
delete newConfig[key];
|
||||
} else if (key === 'enable_pool_mode' && value === false) {
|
||||
} else if ((key === 'enable_pool_mode' || key === 'show_adults') && value === false) {
|
||||
delete newConfig[key];
|
||||
} else if (key === 'expand_to_fit' && value === true) {
|
||||
// Default is true (fluid) — omit to keep config minimal
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,14 @@ class TaskMateStreakCard extends LitElement {
|
||||
return fn ? fn(this.hass, key, params) : key;
|
||||
}
|
||||
|
||||
/** Avatar: child's photo → MDI avatar → default icon. */
|
||||
_avatarGlyph(source, fallback = "mdi:account-circle", cls = "tm-face-img") {
|
||||
const face = window.__taskmate_child_visual(source);
|
||||
return face.kind === "image"
|
||||
? html`<img class="${cls}" src="${face.url}" alt="" loading="lazy">`
|
||||
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : fallback}"></ha-icon>`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
const base = css`
|
||||
:host {
|
||||
@@ -302,7 +310,7 @@ class TaskMateStreakCard extends LitElement {
|
||||
}
|
||||
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
let children = attrs.children || [];
|
||||
let children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
// Use recent_completions (all-time history, last 50) for streak/achievement calculation
|
||||
const completions = [...(attrs.recent_completions || attrs.todays_completions || [])];
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
@@ -342,16 +350,13 @@ class TaskMateStreakCard extends LitElement {
|
||||
const dayDots = this._buildDayDots(childCompletions, chores, child.id, daysShown);
|
||||
const achievements = this._getAchievements(child, childCompletions, streak, entity_ref);
|
||||
|
||||
// Avatar now included directly in children array from the overview sensor
|
||||
const avatar = child.avatar || "mdi:account-circle";
|
||||
|
||||
const streakClass = streak >= 7 ? "hot" : streak >= 3 ? "warm" : "cold";
|
||||
const streakEmoji = streak >= 14 ? "🔥🔥" : streak >= 7 ? "🔥" : streak >= 3 ? "⚡" : streak >= 1 ? "✨" : "💤";
|
||||
|
||||
return html`
|
||||
<div class="streak-tile">
|
||||
<div class="streak-header">
|
||||
<div class="child-avatar"><ha-icon icon="${avatar}"></ha-icon></div>
|
||||
<div class="child-avatar">${this._avatarGlyph(child)}</div>
|
||||
<div class="streak-info">
|
||||
<div class="child-name">${child.name}</div>
|
||||
<div class="streak-count-row">
|
||||
@@ -493,11 +498,11 @@ class TaskMateStreakCard extends LitElement {
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
_av(child, tone, size) {
|
||||
const a = child.avatar || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${child.name}">`
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: (child.name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
@@ -550,7 +555,7 @@ class TaskMateStreakCard extends LitElement {
|
||||
}
|
||||
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
let children = attrs.children || [];
|
||||
let children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
const completions = [...(attrs.recent_completions || attrs.todays_completions || [])];
|
||||
const chores = attrs.chores || [];
|
||||
|
||||
@@ -732,6 +737,7 @@ class TaskMateStreakCardEditor extends LitElement {
|
||||
|
||||
_computeLabel = (entry) => {
|
||||
const labels = {
|
||||
show_adults: this._t('common.editor.show_adults'),
|
||||
entity: this._t('common.editor.overview_entity'),
|
||||
title: this._t('streak.editor.title'),
|
||||
child_id: this._t('common.editor.filter_by_child'),
|
||||
@@ -743,6 +749,7 @@ class TaskMateStreakCardEditor extends LitElement {
|
||||
|
||||
_computeHelper = (entry) => {
|
||||
const helpers = {
|
||||
show_adults: this._t('common.editor.show_adults_helper'),
|
||||
entity: this._t('common.editor.overview_entity_helper'),
|
||||
child_id: this._t('streak.editor.child_helper'),
|
||||
streak_days_shown: this._t('streak.editor.days_shown_helper'),
|
||||
@@ -762,8 +769,8 @@ class TaskMateStreakCardEditor extends LitElement {
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.data=${this.config.show_adults === true ? { ...data, show_adults: true } : data}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
|
||||
@@ -38,6 +38,14 @@ class TaskMateWeeklyCard extends LitElement {
|
||||
return fn ? fn(this.hass, key, params) : key;
|
||||
}
|
||||
|
||||
/** Avatar: child's photo → MDI avatar → default icon. */
|
||||
_avatarGlyph(source, fallback = "mdi:account-circle", cls = "tm-face-img") {
|
||||
const face = window.__taskmate_child_visual(source);
|
||||
return face.kind === "image"
|
||||
? html`<img class="${cls}" src="${face.url}" alt="" loading="lazy">`
|
||||
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : fallback}"></ha-icon>`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
const base = css`
|
||||
:host {
|
||||
@@ -338,7 +346,7 @@ class TaskMateWeeklyCard extends LitElement {
|
||||
|
||||
const tz = this.hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
let children = attrs.children || [];
|
||||
let children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
const chores = attrs.chores || [];
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
const pointsName = attrs.points_name || this._t("common.stars");
|
||||
@@ -466,14 +474,11 @@ class TaskMateWeeklyCard extends LitElement {
|
||||
new Date(c.completed_at).toLocaleDateString("en-CA", { timeZone: tz })
|
||||
)).size;
|
||||
|
||||
// Avatar now included directly in children array from the overview sensor
|
||||
const avatar = child.avatar || "mdi:account-circle";
|
||||
|
||||
const pct = Math.min((childDaysActive / 7) * 100, 100);
|
||||
|
||||
return html`
|
||||
<div class="child-row">
|
||||
<div class="child-avatar"><ha-icon icon="${avatar}"></ha-icon></div>
|
||||
<div class="child-avatar">${this._avatarGlyph(child)}</div>
|
||||
<div class="child-info">
|
||||
<div class="child-name">${child.name}</div>
|
||||
<div class="child-week-stats">
|
||||
@@ -503,18 +508,18 @@ class TaskMateWeeklyCard extends LitElement {
|
||||
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
|
||||
|
||||
_av(child, tone, size) {
|
||||
const a = child.avatar || "";
|
||||
const inner = a.startsWith("mdi:")
|
||||
? html`<ha-icon icon="${a}"></ha-icon>`
|
||||
: a
|
||||
? html`<img src="${a}" alt="${child.name}">`
|
||||
const face = window.__taskmate_child_visual(child);
|
||||
const inner = face.kind === "icon"
|
||||
? html`<ha-icon icon="${face.icon}"></ha-icon>`
|
||||
: face.kind === "image"
|
||||
? html`<img src="${face.url}" alt="${child.name}">`
|
||||
: (child.name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||||
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
|
||||
}
|
||||
|
||||
_weekData(entity, tz) {
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
|
||||
let children = attrs.children || [];
|
||||
let children = (window.__taskmate_family_children ? window.__taskmate_family_children(attrs.children || [], this.config) : attrs.children || []);
|
||||
const chores = attrs.chores || [];
|
||||
const pointsName = attrs.points_name || this._t("common.stars");
|
||||
const rewardsClaimed = (attrs.rewards_claimed || attrs.recent_rewards || []);
|
||||
@@ -796,6 +801,7 @@ class TaskMateWeeklyCardEditor extends LitElement {
|
||||
|
||||
_computeLabel = (entry) => {
|
||||
const labels = {
|
||||
show_adults: this._t('common.editor.show_adults'),
|
||||
entity: this._t('common.editor.overview_entity'),
|
||||
title: this._t('weekly.editor.title'),
|
||||
child_id: this._t('common.editor.filter_by_child'),
|
||||
@@ -806,6 +812,7 @@ class TaskMateWeeklyCardEditor extends LitElement {
|
||||
|
||||
_computeHelper = (entry) => {
|
||||
const helpers = {
|
||||
show_adults: this._t('common.editor.show_adults_helper'),
|
||||
entity: this._t('common.editor.overview_entity_helper'),
|
||||
child_id: this._t('weekly.editor.child_helper'),
|
||||
};
|
||||
@@ -823,8 +830,8 @@ class TaskMateWeeklyCardEditor extends LitElement {
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._buildSchema()}
|
||||
.data=${this.config.show_adults === true ? { ...data, show_adults: true } : data}
|
||||
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._formChanged}
|
||||
|
||||
Reference in New Issue
Block a user