185 files
This commit is contained in:
@@ -61,6 +61,8 @@ from .const import (
|
||||
ATTR_REASON,
|
||||
ATTR_REWARD_ID,
|
||||
ATTR_SOUND,
|
||||
BADGE_TIERS,
|
||||
COMPLETION_SOUND_OPTIONS,
|
||||
CONF_TASK_GROUP_CHORE_IDS,
|
||||
CONF_TASK_GROUP_ID,
|
||||
CONF_TASK_GROUP_NAME,
|
||||
@@ -1363,28 +1365,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
_safe(handle_preview_sound),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_SOUND): vol.In(
|
||||
[
|
||||
"none",
|
||||
"coin",
|
||||
"levelup",
|
||||
"fanfare",
|
||||
"chime",
|
||||
"powerup",
|
||||
"undo",
|
||||
"fart1",
|
||||
"fart2",
|
||||
"fart3",
|
||||
"fart4",
|
||||
"fart5",
|
||||
"fart6",
|
||||
"fart7",
|
||||
"fart8",
|
||||
"fart9",
|
||||
"fart10",
|
||||
"fart_random",
|
||||
]
|
||||
),
|
||||
vol.Required(ATTR_SOUND): vol.In(COMPLETION_SOUND_OPTIONS),
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -1583,7 +1564,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
vol.Required(ATTR_BADGE_NAME): cv.string,
|
||||
vol.Optional(ATTR_BADGE_DESCRIPTION, default=""): cv.string,
|
||||
vol.Optional(ATTR_BADGE_ICON, default="mdi:trophy"): cv.string,
|
||||
vol.Optional(ATTR_BADGE_TIER, default="bronze"): vol.In(["bronze", "silver", "gold", "platinum"]),
|
||||
vol.Optional(ATTR_BADGE_TIER, default="bronze"): vol.In(BADGE_TIERS),
|
||||
vol.Optional(ATTR_BADGE_POINT_BONUS, default=0): vol.Coerce(int),
|
||||
vol.Optional(ATTR_BADGE_CRITERIA, default=[]): list,
|
||||
vol.Optional(ATTR_BADGE_COMBINATOR, default="AND"): cv.string,
|
||||
@@ -1603,7 +1584,7 @@ async def _async_register_services(hass: HomeAssistant) -> None:
|
||||
vol.Optional(ATTR_BADGE_NAME): cv.string,
|
||||
vol.Optional(ATTR_BADGE_DESCRIPTION): cv.string,
|
||||
vol.Optional(ATTR_BADGE_ICON): cv.string,
|
||||
vol.Optional(ATTR_BADGE_TIER): vol.In(["bronze", "silver", "gold", "platinum"]),
|
||||
vol.Optional(ATTR_BADGE_TIER): vol.In(BADGE_TIERS),
|
||||
vol.Optional(ATTR_BADGE_POINT_BONUS): vol.Coerce(int),
|
||||
vol.Optional(ATTR_BADGE_CRITERIA): list,
|
||||
vol.Optional(ATTR_BADGE_COMBINATOR): cv.string,
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TaskMateCoordinator
|
||||
from .entity import taskmate_device_info
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -43,12 +44,7 @@ class TaskMateBaseBinarySensor(CoordinatorEntity, BinarySensorEntity):
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device info."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._entry.entry_id)},
|
||||
name="TaskMate",
|
||||
manufacturer="TaskMate",
|
||||
model="Family Chore Manager",
|
||||
)
|
||||
return taskmate_device_info(self._entry.entry_id)
|
||||
|
||||
|
||||
class HasPendingApprovalsBinarySensor(TaskMateBaseBinarySensor):
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TaskMateCoordinator
|
||||
from .entity import taskmate_device_info
|
||||
from .models import Child, Chore, Reward
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -42,8 +43,7 @@ async def async_setup_entry(
|
||||
entities.append(CompleteChoreButton(coordinator, entry, child, chore))
|
||||
|
||||
# Reward claim buttons
|
||||
for reward in rewards:
|
||||
entities.append(ClaimRewardButton(coordinator, entry, child, reward))
|
||||
entities.extend(ClaimRewardButton(coordinator, entry, child, reward) for reward in rewards)
|
||||
|
||||
# Track which entity combos already exist
|
||||
tracked_combos: set[str] = set()
|
||||
@@ -103,12 +103,7 @@ class TaskMateBaseButton(CoordinatorEntity, ButtonEntity):
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device info."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._entry.entry_id)},
|
||||
name="TaskMate",
|
||||
manufacturer="TaskMate",
|
||||
model="Family Chore Manager",
|
||||
)
|
||||
return taskmate_device_info(self._entry.entry_id)
|
||||
|
||||
|
||||
class CompleteChoreButton(TaskMateBaseButton):
|
||||
|
||||
@@ -31,6 +31,7 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TaskMateCoordinator
|
||||
from .entity import taskmate_device_info
|
||||
from .models import Child, Chore
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -127,12 +128,7 @@ class TaskMateCalendar(CoordinatorEntity, CalendarEntity):
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._entry.entry_id)},
|
||||
name="TaskMate",
|
||||
manufacturer="TaskMate",
|
||||
model="Family Chore Manager",
|
||||
)
|
||||
return taskmate_device_info(self._entry.entry_id)
|
||||
|
||||
@property
|
||||
def _child(self) -> Child | None:
|
||||
|
||||
@@ -4,49 +4,15 @@ from typing import Final
|
||||
|
||||
DOMAIN: Final = "taskmate"
|
||||
|
||||
# Configuration keys
|
||||
CONF_CHILDREN: Final = "children"
|
||||
CONF_CHORES: Final = "chores"
|
||||
CONF_REWARDS: Final = "rewards"
|
||||
CONF_POINTS_NAME: Final = "points_name"
|
||||
CONF_POINTS_ICON: Final = "points_icon"
|
||||
|
||||
# Child keys
|
||||
CONF_CHILD_NAME: Final = "name"
|
||||
CONF_CHILD_AVATAR: Final = "avatar"
|
||||
CONF_CHILD_POINTS: Final = "points"
|
||||
CONF_CHILD_ID: Final = "id"
|
||||
CONF_CHILD_AVAILABILITY_ENTITY: Final = "availability_entity"
|
||||
|
||||
# Chore keys
|
||||
CONF_CHORE_NAME: Final = "name"
|
||||
CONF_CHORE_DESCRIPTION: Final = "description"
|
||||
CONF_CHORE_POINTS: Final = "points"
|
||||
CONF_CHORE_DUE_DAYS: Final = "due_days"
|
||||
CONF_CHORE_ASSIGNED_TO: Final = "assigned_to"
|
||||
CONF_CHORE_ID: Final = "id"
|
||||
CONF_CHORE_REQUIRES_APPROVAL: Final = "requires_approval"
|
||||
CONF_CLAIM_ALLOWANCE_MINUTES: Final = "claim_allowance_minutes"
|
||||
DEFAULT_CLAIM_ALLOWANCE_MINUTES: Final = 0
|
||||
CONF_CHORE_ASSIGNMENT_MODE: Final = "assignment_mode"
|
||||
CONF_CHORE_ASSIGNMENT_ROTATION_ANCHOR: Final = "assignment_rotation_anchor"
|
||||
CONF_CHORE_PUBLISH_CALENDARS: Final = "publish_calendar_entities"
|
||||
CONF_CHORE_REQUIRE_AVAILABILITY: Final = "require_availability"
|
||||
CONF_CHORE_MANUAL_START_CHILD: Final = "manual_start_child_id"
|
||||
|
||||
# Task groups
|
||||
CONF_TASK_GROUPS: Final = "task_groups"
|
||||
CONF_TASK_GROUP_ID: Final = "group_id"
|
||||
CONF_TASK_GROUP_NAME: Final = "name"
|
||||
CONF_TASK_GROUP_POLICY: Final = "policy"
|
||||
CONF_TASK_GROUP_CHORE_IDS: Final = "chore_ids"
|
||||
TASK_GROUP_POLICIES: Final = ["sticky", "spread"]
|
||||
DEFAULT_TASK_GROUP_POLICY: Final = "sticky"
|
||||
|
||||
# Calendar projection: how many days ahead each chore's assignment is pushed
|
||||
# into the configured HA calendars. 14 days balances planning visibility with
|
||||
# calendar noise; raise via the Settings flow for longer planning horizons.
|
||||
CONF_CALENDAR_PROJECTION_DAYS: Final = "calendar_projection_days"
|
||||
DEFAULT_CALENDAR_PROJECTION_DAYS: Final = 14
|
||||
MIN_CALENDAR_PROJECTION_DAYS: Final = 1
|
||||
MAX_CALENDAR_PROJECTION_DAYS: Final = 90
|
||||
@@ -59,56 +25,28 @@ MAX_CALENDAR_PROJECTION_DAYS: Final = 90
|
||||
# "first_come" = competitive: shown to the whole pool at once; first child to complete it wins and it
|
||||
# hides for everyone else (shared quota of 1). A parent rejection reopens it for the pool.
|
||||
ASSIGNMENT_MODES: Final = ["everyone", "alternating", "random", "balanced", "first_come", "unassigned"]
|
||||
DEFAULT_ASSIGNMENT_MODE: Final = "everyone"
|
||||
|
||||
# Reward keys
|
||||
CONF_REWARD_NAME: Final = "name"
|
||||
CONF_REWARD_DESCRIPTION: Final = "description"
|
||||
CONF_REWARD_COST: Final = "cost"
|
||||
CONF_REWARD_ICON: Final = "icon"
|
||||
CONF_REWARD_ID: Final = "id"
|
||||
CONF_REWARD_QUANTITY: Final = "quantity"
|
||||
CONF_REWARD_EXPIRES_AT: Final = "expires_at"
|
||||
|
||||
# Default values
|
||||
DEFAULT_POINTS_NAME: Final = "Stars"
|
||||
DEFAULT_POINTS_ICON: Final = "mdi:star"
|
||||
|
||||
# Days of week
|
||||
DAYS_OF_WEEK: Final = [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
]
|
||||
|
||||
# Recurrence options for chores (Mode B)
|
||||
RECURRENCE_OPTIONS: Final = [
|
||||
"every_2_days",
|
||||
"weekly",
|
||||
"every_2_weeks",
|
||||
"monthly",
|
||||
"every_3_months",
|
||||
"every_6_months",
|
||||
]
|
||||
|
||||
RECURRENCE_LABELS: Final = {
|
||||
"every_2_days": "Every 2 days",
|
||||
"weekly": "Weekly",
|
||||
"every_2_weeks": "Every 2 weeks",
|
||||
"monthly": "Monthly",
|
||||
"every_3_months": "Every 3 months",
|
||||
"every_6_months": "Every 6 months",
|
||||
}
|
||||
|
||||
# Schedule modes
|
||||
SCHEDULE_MODES: Final = ["specific_days", "recurring", "one_shot"]
|
||||
|
||||
# First occurrence modes
|
||||
FIRST_OCCURRENCE_MODES: Final = ["available_immediately", "wait_for_first_occurrence"]
|
||||
# Nominal length of each Mode-B recurrence period, in days. Used for report
|
||||
# expectations and recurrence math (not for the shorter availability window
|
||||
# in coord_chores.is_chore_available_for_child, which is deliberately capped).
|
||||
RECURRENCE_PERIOD_DAYS: Final = {
|
||||
"every_2_days": 2,
|
||||
"weekly": 7,
|
||||
"every_2_weeks": 14,
|
||||
"monthly": 30,
|
||||
"every_3_months": 91,
|
||||
"every_6_months": 182,
|
||||
}
|
||||
|
||||
# Badge tiers, lowest to highest.
|
||||
BADGE_TIERS: Final = ["bronze", "silver", "gold", "platinum"]
|
||||
|
||||
# Time categories for chores
|
||||
TIME_CATEGORIES: Final = [
|
||||
@@ -140,101 +78,6 @@ DEFAULT_TIME_PERIODS: Final = [
|
||||
|
||||
MAX_TIME_PERIODS: Final = 24
|
||||
|
||||
# Avatar options
|
||||
AVATAR_OPTIONS: Final = [
|
||||
# Basic faces
|
||||
"mdi:account-circle",
|
||||
"mdi:face-man",
|
||||
"mdi:face-woman",
|
||||
"mdi:face-man-outline",
|
||||
"mdi:face-woman-outline",
|
||||
# Fun character avatars
|
||||
"mdi:robot-happy",
|
||||
"mdi:robot-excited",
|
||||
"mdi:alien",
|
||||
"mdi:ninja",
|
||||
"mdi:pirate",
|
||||
# Animals kids love
|
||||
"mdi:cat",
|
||||
"mdi:dog",
|
||||
"mdi:unicorn",
|
||||
"mdi:dragon",
|
||||
"mdi:owl",
|
||||
"mdi:penguin",
|
||||
"mdi:panda",
|
||||
"mdi:rabbit",
|
||||
"mdi:koala",
|
||||
"mdi:fox",
|
||||
"mdi:dolphin",
|
||||
"mdi:jellyfish",
|
||||
"mdi:octopus",
|
||||
"mdi:butterfly",
|
||||
# Splatoon Inkling-inspired (colorful, action-themed)
|
||||
"mdi:palette",
|
||||
"mdi:spray",
|
||||
"mdi:water-outline",
|
||||
"mdi:shimmer",
|
||||
"mdi:flare",
|
||||
"mdi:star-face",
|
||||
"mdi:emoticon-cool",
|
||||
"mdi:emoticon-excited",
|
||||
"mdi:emoticon-kiss",
|
||||
"mdi:emoticon-wink",
|
||||
"mdi:emoticon-happy",
|
||||
"mdi:emoticon-poop",
|
||||
"mdi:face-woman-shimmer",
|
||||
"mdi:face-man-shimmer",
|
||||
"mdi:account-star",
|
||||
"mdi:account-heart",
|
||||
# Gaming and action themed
|
||||
"mdi:gamepad-variant",
|
||||
"mdi:controller",
|
||||
"mdi:trophy",
|
||||
"mdi:crown",
|
||||
"mdi:lightning-bolt",
|
||||
"mdi:fire",
|
||||
"mdi:rocket",
|
||||
"mdi:sword",
|
||||
"mdi:shield",
|
||||
# Space and fantasy
|
||||
"mdi:alien-outline",
|
||||
"mdi:ufo",
|
||||
"mdi:space-invaders",
|
||||
"mdi:ghost",
|
||||
"mdi:skull-crossbones",
|
||||
"mdi:wizard-hat",
|
||||
"mdi:magic-staff",
|
||||
# Nature and colorful
|
||||
"mdi:flower-tulip",
|
||||
"mdi:flower-poppy",
|
||||
"mdi:clover",
|
||||
"mdi:mushroom",
|
||||
"mdi:star-shooting",
|
||||
"mdi:star-four-points",
|
||||
"mdi:heart",
|
||||
"mdi:heart-multiple",
|
||||
"mdi:diamond-stone",
|
||||
"mdi:puzzle-heart",
|
||||
]
|
||||
|
||||
# Reward icon options
|
||||
REWARD_ICON_OPTIONS: Final = [
|
||||
"mdi:gift",
|
||||
"mdi:ice-cream",
|
||||
"mdi:pizza",
|
||||
"mdi:movie",
|
||||
"mdi:gamepad-variant",
|
||||
"mdi:tablet",
|
||||
"mdi:television",
|
||||
"mdi:bike",
|
||||
"mdi:currency-usd",
|
||||
"mdi:shopping",
|
||||
"mdi:party-popper",
|
||||
"mdi:swim",
|
||||
"mdi:sleep",
|
||||
"mdi:candy",
|
||||
]
|
||||
|
||||
# Platforms
|
||||
PLATFORMS: Final = ["sensor", "button", "binary_sensor"]
|
||||
|
||||
@@ -331,12 +174,6 @@ ATTR_BADGE_NOTIFY_ON_EARN: Final = "notify_on_earn"
|
||||
ATTR_BADGE_ENABLED: Final = "enabled"
|
||||
ATTR_AWARDED_BADGE_ID: Final = "awarded_badge_id"
|
||||
|
||||
# States
|
||||
STATE_PENDING: Final = "pending"
|
||||
STATE_AWAITING_APPROVAL: Final = "awaiting_approval"
|
||||
STATE_COMPLETED: Final = "completed"
|
||||
STATE_CLAIMED: Final = "claimed"
|
||||
|
||||
# Completion sound options
|
||||
# Most sounds are synthesized via Web Audio API
|
||||
# Fart sounds are CC0 audio files from BigSoundBank.com and GfxSounds.com
|
||||
@@ -361,12 +198,6 @@ COMPLETION_SOUND_OPTIONS: Final = [
|
||||
"fart_random", # Random fart - picks a random fart sound each time!
|
||||
]
|
||||
|
||||
# Default completion sound
|
||||
DEFAULT_COMPLETION_SOUND: Final = "coin"
|
||||
|
||||
# Chore keys (sound-related)
|
||||
CONF_CHORE_COMPLETION_SOUND: Final = "completion_sound"
|
||||
|
||||
# --- Chore difficulty tiers ---
|
||||
# Each chore carries a difficulty tier; the points it awards are the base
|
||||
# points multiplied by the tier's multiplier. "medium" is the neutral baseline
|
||||
|
||||
@@ -389,6 +389,15 @@ class ChoresMixin:
|
||||
approved. If None/empty, every currently-unapproved completion is approved.
|
||||
Reuses async_approve_chore so all award/badge/quest/celebration side-effects
|
||||
match single approval exactly.
|
||||
|
||||
Each approval is told not to refresh (#794). ``async_approve_chore``
|
||||
normally ends in a full coordinator rebuild plus a state write for every
|
||||
TaskMate entity, which at ~0.13s a time made a 60-chore Approve All block
|
||||
the websocket call for ~8s with nothing pushed to the frontend meanwhile —
|
||||
the panel and the approvals card sit unchanged for that whole window, so
|
||||
the button reads as broken. The batch refreshes once at the end instead.
|
||||
Safe because the per-approval side-effects (badges, quests, challenges,
|
||||
the all-done check) all read `storage` directly, never `self.data`.
|
||||
"""
|
||||
pending = {c.id for c in self.storage.get_completions() if not c.approved}
|
||||
if completion_ids:
|
||||
@@ -398,8 +407,10 @@ class ChoresMixin:
|
||||
targets = [c.id for c in self.storage.get_completions() if not c.approved]
|
||||
count = 0
|
||||
for cid in targets:
|
||||
await self.async_approve_chore(cid)
|
||||
await self.async_approve_chore(cid, refresh=False)
|
||||
count += 1
|
||||
if count:
|
||||
await self.async_refresh()
|
||||
return count
|
||||
|
||||
async def async_add_chores_bulk(
|
||||
@@ -716,32 +727,37 @@ class ChoresMixin:
|
||||
# assignee. Parents (as_parent) may complete on behalf of any pool
|
||||
# member — e.g. ticking it off for the off-rotation child. first_come
|
||||
# keeps its competitive semantics (every pool member may race).
|
||||
if not as_parent and assignment_mode != "first_come":
|
||||
if child_id not in self._compute_active_children(chore):
|
||||
_LOGGER.debug(
|
||||
"complete_chore no-op: '%s' not assigned to %s today",
|
||||
chore.name,
|
||||
child.name,
|
||||
)
|
||||
return None
|
||||
if (
|
||||
not as_parent
|
||||
and assignment_mode != "first_come"
|
||||
and child_id not in self._compute_active_children(chore)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"complete_chore no-op: '%s' not assigned to %s today",
|
||||
chore.name,
|
||||
child.name,
|
||||
)
|
||||
return None
|
||||
|
||||
# Check recurrence window for Mode B chores
|
||||
if getattr(chore, "schedule_mode", "specific_days") == "recurring":
|
||||
if not self.is_chore_available_for_child(chore, child_id):
|
||||
_LOGGER.debug(
|
||||
"complete_chore no-op: '%s' not available yet (recurrence window)",
|
||||
chore.name,
|
||||
)
|
||||
return None
|
||||
if getattr(chore, "schedule_mode", "specific_days") == "recurring" and not self.is_chore_available_for_child(
|
||||
chore, child_id
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"complete_chore no-op: '%s' not available yet (recurrence window)",
|
||||
chore.name,
|
||||
)
|
||||
return None
|
||||
|
||||
# Check availability for one-shot chores
|
||||
if getattr(chore, "schedule_mode", "specific_days") == "one_shot":
|
||||
if not self.is_chore_available_for_child(chore, child_id):
|
||||
_LOGGER.debug(
|
||||
"complete_chore no-op: '%s' not available (one-shot done or expired)",
|
||||
chore.name,
|
||||
)
|
||||
return None
|
||||
if getattr(chore, "schedule_mode", "specific_days") == "one_shot" and not self.is_chore_available_for_child(
|
||||
chore, child_id
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"complete_chore no-op: '%s' not available (one-shot done or expired)",
|
||||
chore.name,
|
||||
)
|
||||
return None
|
||||
|
||||
# Check daily limit (only count parent completions, not bonus sub-tasks)
|
||||
all_completions = self.storage.get_completions()
|
||||
@@ -876,10 +892,7 @@ class ChoresMixin:
|
||||
|
||||
# Determine child pool — empty assigned_to means all children
|
||||
assigned = getattr(chore, "assigned_to", []) or []
|
||||
if assigned:
|
||||
child_ids = list(assigned)
|
||||
else:
|
||||
child_ids = [c.id for c in self.storage.get_children()]
|
||||
child_ids = list(assigned) if assigned else [c.id for c in self.storage.get_children()]
|
||||
|
||||
if not child_ids:
|
||||
raise ValueError(f"Chore '{chore.name}' has no children to suppress")
|
||||
@@ -903,14 +916,15 @@ class ChoresMixin:
|
||||
# Clear today's cached current assignee for rotation-mode chores so
|
||||
# the "Current" column / child-stats card stops pointing at the
|
||||
# original child. The pointer recomputes at the next midnight refresh.
|
||||
if getattr(chore, "assignment_mode", "everyone") != "everyone":
|
||||
if getattr(chore, "assignment_current_child_id", "") or getattr(chore, "assignment_swap_date", ""):
|
||||
chore.assignment_current_child_id = ""
|
||||
# Drop the swap override too, or it would re-point the column
|
||||
# at the swapped-to child on the next assignment pass.
|
||||
chore.assignment_swap_child_id = ""
|
||||
chore.assignment_swap_date = ""
|
||||
self.storage.update_chore(chore)
|
||||
if getattr(chore, "assignment_mode", "everyone") != "everyone" and (
|
||||
getattr(chore, "assignment_current_child_id", "") or getattr(chore, "assignment_swap_date", "")
|
||||
):
|
||||
chore.assignment_current_child_id = ""
|
||||
# Drop the swap override too, or it would re-point the column
|
||||
# at the swapped-to child on the next assignment pass.
|
||||
chore.assignment_swap_child_id = ""
|
||||
chore.assignment_swap_date = ""
|
||||
self.storage.update_chore(chore)
|
||||
|
||||
await self.storage.async_save()
|
||||
await self.async_refresh()
|
||||
@@ -988,8 +1002,13 @@ class ChoresMixin:
|
||||
await self.async_refresh()
|
||||
return completion
|
||||
|
||||
async def async_approve_chore(self, completion_id: str) -> None:
|
||||
"""Approve a chore completion."""
|
||||
async def async_approve_chore(self, completion_id: str, refresh: bool = True) -> None:
|
||||
"""Approve a chore completion.
|
||||
|
||||
``refresh=False`` is used by ``async_approve_chores_bulk`` so a batch
|
||||
pays for one coordinator rebuild rather than one per completion (#794).
|
||||
The caller must refresh once it is done.
|
||||
"""
|
||||
completions = self.storage.get_completions()
|
||||
for completion in completions:
|
||||
if completion.id == completion_id:
|
||||
@@ -1055,7 +1074,8 @@ class ChoresMixin:
|
||||
self.storage.update_chore(chore)
|
||||
|
||||
await self.storage.async_save()
|
||||
await self.async_refresh()
|
||||
if refresh:
|
||||
await self.async_refresh()
|
||||
|
||||
# Trigger badge evaluation after approval awards points/chore count/streak
|
||||
if getattr(self, "badges", None):
|
||||
@@ -1290,11 +1310,8 @@ class ChoresMixin:
|
||||
|
||||
def _check_one_shot_fully_disabled(self, chore) -> None:
|
||||
"""Check if a one-shot chore should be fully disabled (all children done)."""
|
||||
if chore.assigned_to:
|
||||
target_children = set(chore.assigned_to)
|
||||
else:
|
||||
# assigned_to=[] means all children
|
||||
target_children = {c.id for c in self.storage.get_children()}
|
||||
# assigned_to=[] means all children
|
||||
target_children = set(chore.assigned_to) or {c.id for c in self.storage.get_children()}
|
||||
|
||||
if target_children and target_children.issubset(set(chore.disabled_for)):
|
||||
chore.enabled = False
|
||||
|
||||
@@ -12,6 +12,7 @@ once this module is in place. They call self.notifications.fire(...).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
@@ -539,18 +540,20 @@ class NotificationCoordinator:
|
||||
# Scheduler — time-gated callbacks
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def cancel_schedules(self) -> None:
|
||||
"""Cancel all registered time triggers (idempotent; also used on unload)."""
|
||||
for unsub in self._scheduled_unsubs:
|
||||
with contextlib.suppress(Exception):
|
||||
unsub()
|
||||
self._scheduled_unsubs = []
|
||||
|
||||
async def async_setup_schedules(self) -> None:
|
||||
"""Cancel any existing time callbacks and register fresh ones from current config.
|
||||
|
||||
Call this on startup AND after any config change that affects schedules
|
||||
(e.g. bedtime time edited, custom notification time edited, master toggled).
|
||||
"""
|
||||
for unsub in self._scheduled_unsubs:
|
||||
try:
|
||||
unsub()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
self._scheduled_unsubs = []
|
||||
self.cancel_schedules()
|
||||
|
||||
# Bedtime — per-child time
|
||||
cfg = self.storage.get_notification_config("bedtime_reminder")
|
||||
|
||||
@@ -778,9 +778,12 @@ class PointsMixin:
|
||||
# #563: when enabled, the streak only advances once EVERY chore due that
|
||||
# day is done. The in-flight completion (chore_id) is counted as done
|
||||
# since it may not be persisted yet at this point in the flow.
|
||||
if advance_streak and self._setting_enabled("streak_requires_all_chores"):
|
||||
if not self._all_due_chores_done(child.id, effective_date, include_rotation=True, extra_done=chore_id):
|
||||
advance_streak = False
|
||||
if (
|
||||
advance_streak
|
||||
and self._setting_enabled("streak_requires_all_chores")
|
||||
and not self._all_due_chores_done(child.id, effective_date, include_rotation=True, extra_done=chore_id)
|
||||
):
|
||||
advance_streak = False
|
||||
if advance_streak:
|
||||
streak_mode = self.storage.get_setting("streak_reset_mode", "reset")
|
||||
streak_paused = getattr(child, "streak_paused", False)
|
||||
|
||||
@@ -40,9 +40,8 @@ class TimedMixin:
|
||||
|
||||
if existing and existing.state == "paused":
|
||||
# Check daily cap before resuming
|
||||
if chore.timed_max_daily_minutes > 0:
|
||||
if existing.total_seconds_today >= chore.timed_max_daily_minutes * 60:
|
||||
raise ValueError(f"Daily cap reached ({chore.timed_max_daily_minutes} min)")
|
||||
if chore.timed_max_daily_minutes > 0 and existing.total_seconds_today >= chore.timed_max_daily_minutes * 60:
|
||||
raise ValueError(f"Daily cap reached ({chore.timed_max_daily_minutes} min)")
|
||||
existing.state = "running"
|
||||
existing.segments.append({"start": now.isoformat(), "end": None})
|
||||
self.storage.save_timed_session(existing)
|
||||
|
||||
@@ -228,10 +228,11 @@ class TaskMateCoordinator(
|
||||
return True
|
||||
if self._vacation_calendar_active():
|
||||
return True
|
||||
if child is not None and getattr(child, "pause_streak_when_unavailable", False):
|
||||
if not self._is_child_available(child.id):
|
||||
return True
|
||||
return False
|
||||
return (
|
||||
child is not None
|
||||
and getattr(child, "pause_streak_when_unavailable", False)
|
||||
and not self._is_child_available(child.id)
|
||||
)
|
||||
|
||||
# ── Backup / restore ─────────────────────────────────────────────────
|
||||
EXPORT_VERSION = 1
|
||||
@@ -655,21 +656,11 @@ class TaskMateCoordinator(
|
||||
async def async_shutdown(self) -> None:
|
||||
"""Shutdown the coordinator and clean up listeners."""
|
||||
self.cancel_unlock_timers()
|
||||
if self._unsub_midnight:
|
||||
self._unsub_midnight()
|
||||
self._unsub_midnight = None
|
||||
if self._unsub_prune:
|
||||
self._unsub_prune()
|
||||
self._unsub_prune = None
|
||||
if self._unsub_availability:
|
||||
self._unsub_availability()
|
||||
self._unsub_availability = None
|
||||
if self._unsub_surprise:
|
||||
self._unsub_surprise()
|
||||
self._unsub_surprise = None
|
||||
if self._unsub_weekly:
|
||||
self._unsub_weekly()
|
||||
self._unsub_weekly = None
|
||||
self.notifications.cancel_schedules()
|
||||
for attr in ("_unsub_midnight", "_unsub_prune", "_unsub_availability", "_unsub_surprise", "_unsub_weekly"):
|
||||
if unsub := getattr(self, attr):
|
||||
unsub()
|
||||
setattr(self, attr, None)
|
||||
self.disarm_mandatory_schedules()
|
||||
# Flush any pending debounced save so an entry unload/reload can't drop
|
||||
# the last mutation (PERF-3).
|
||||
@@ -859,17 +850,3 @@ class TaskMateCoordinator(
|
||||
def get_child(self, child_id: str) -> Child | None:
|
||||
"""Get a child by ID."""
|
||||
return self.storage.get_child(child_id)
|
||||
|
||||
async def async_set_setting(self, key: str, value: str) -> None:
|
||||
"""Update a generic setting."""
|
||||
self.storage.set_setting(key, value)
|
||||
await self.storage.async_save()
|
||||
await self.async_refresh()
|
||||
|
||||
# Settings
|
||||
async def async_set_points_settings(self, name: str, icon: str) -> None:
|
||||
"""Update points settings."""
|
||||
self.storage.set_points_name(name)
|
||||
self.storage.set_points_icon(icon)
|
||||
await self.storage.async_save()
|
||||
await self.async_refresh()
|
||||
|
||||
@@ -17,5 +17,5 @@
|
||||
"iot_class": "calculated",
|
||||
"issue_tracker": "https://github.com/tempus2016/taskmate/issues",
|
||||
"requirements": [],
|
||||
"version": "5.1.1"
|
||||
"version": "5.2.0"
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ class Child:
|
||||
best_streak=data.get("best_streak", 0),
|
||||
pending_rewards=list(data.get("pending_rewards", [])),
|
||||
chore_order=list(data.get("chore_order", [])),
|
||||
last_completion_date=data.get("last_completion_date", None),
|
||||
last_completion_date=data.get("last_completion_date"),
|
||||
streak_paused=data.get("streak_paused", False),
|
||||
streak_milestones_achieved=list(data.get("streak_milestones_achieved", [])),
|
||||
awarded_perfect_weeks=list(data.get("awarded_perfect_weeks", [])),
|
||||
@@ -207,7 +207,7 @@ class Child:
|
||||
pause_streak_when_unavailable=data.get("pause_streak_when_unavailable", False),
|
||||
career_score=data.get("career_score", 0),
|
||||
total_penalties_received=data.get("total_penalties_received", 0),
|
||||
notify_service=data.get("notify_service", None),
|
||||
notify_service=data.get("notify_service"),
|
||||
linked_user_id=data.get("linked_user_id", ""),
|
||||
quiet_hours_start=data.get("quiet_hours_start", ""),
|
||||
quiet_hours_end=data.get("quiet_hours_end", ""),
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TaskMateCoordinator
|
||||
from .entity import taskmate_device_info
|
||||
|
||||
# (setting_key, translation_key, min, max, step, default, icon)
|
||||
_NUMBERS = [
|
||||
@@ -50,12 +51,7 @@ class TaskMateSettingNumber(CoordinatorEntity, NumberEntity):
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._entry.entry_id)},
|
||||
name="TaskMate",
|
||||
manufacturer="TaskMate",
|
||||
model="Family Chore Manager",
|
||||
)
|
||||
return taskmate_device_info(self._entry.entry_id)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
|
||||
@@ -10,6 +10,7 @@ served (auth-gated) at ``/api/taskmate/photo/<name>``.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
@@ -158,21 +159,23 @@ async def async_delete_photo(hass, photo_url: str) -> None:
|
||||
await hass.async_add_executor_job(_unlink)
|
||||
|
||||
|
||||
def total_photos_bytes(hass) -> int:
|
||||
"""Sum of all stored evidence-photo file sizes (0 if the dir is absent)."""
|
||||
directory = photos_path(hass)
|
||||
def matching_files_bytes(directory: Path, filename_re: re.Pattern[str]) -> int:
|
||||
"""Sum sizes of files in ``directory`` matching ``filename_re`` (0 if absent)."""
|
||||
if not directory.is_dir():
|
||||
return 0
|
||||
total = 0
|
||||
for p in directory.iterdir():
|
||||
if p.is_file() and FILENAME_RE.match(p.name):
|
||||
try:
|
||||
if p.is_file() and filename_re.match(p.name):
|
||||
with contextlib.suppress(OSError): # pragma: no cover - defensive
|
||||
total += p.stat().st_size
|
||||
except OSError: # pragma: no cover - defensive
|
||||
pass
|
||||
return total
|
||||
|
||||
|
||||
def total_photos_bytes(hass) -> int:
|
||||
"""Sum of all stored evidence-photo file sizes (0 if the dir is absent)."""
|
||||
return matching_files_bytes(photos_path(hass), FILENAME_RE)
|
||||
|
||||
|
||||
async def async_sweep_orphan_photos(hass, referenced_urls, max_age_hours: int = 24) -> int:
|
||||
"""Delete stored photos not referenced by any completion (SEC-2).
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TaskMateCoordinator
|
||||
from .entity import taskmate_device_info
|
||||
|
||||
# (setting_key, translation_key, options, default, icon)
|
||||
_SELECTS = [
|
||||
@@ -48,12 +49,7 @@ class TaskMateSettingSelect(CoordinatorEntity, SelectEntity):
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._entry.entry_id)},
|
||||
name="TaskMate",
|
||||
manufacturer="TaskMate",
|
||||
model="Family Chore Manager",
|
||||
)
|
||||
return taskmate_device_info(self._entry.entry_id)
|
||||
|
||||
@property
|
||||
def current_option(self) -> str:
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.util import dt as dt_util
|
||||
from . import images, photos
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TaskMateCoordinator
|
||||
from .entity import taskmate_device_info
|
||||
from .models import Child
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -453,7 +454,7 @@ def _build_rewards_list(common: dict) -> list[dict]:
|
||||
out = []
|
||||
for r in rewards:
|
||||
assigned = r.assigned_to if isinstance(r.assigned_to, list) and r.assigned_to else [c.id for c in children]
|
||||
calculated_costs = {child_id: r.cost for child_id in assigned}
|
||||
calculated_costs = dict.fromkeys(assigned, r.cost)
|
||||
reward_pool_allocations = {cid: pool_by_child_reward.get(cid, {}).get(r.id, 0) for cid in assigned}
|
||||
jackpot_pool_total = pool_total_by_reward.get(r.id, 0) if getattr(r, "is_jackpot", False) else None
|
||||
quantity = getattr(r, "quantity", None)
|
||||
@@ -736,12 +737,7 @@ class TaskMateBaseSensor(CoordinatorEntity, SensorEntity):
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device info."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._entry.entry_id)},
|
||||
name="TaskMate",
|
||||
manufacturer="TaskMate",
|
||||
model="Family Chore Manager",
|
||||
)
|
||||
return taskmate_device_info(self._entry.entry_id)
|
||||
|
||||
|
||||
class _CachedAttrsSensor(TaskMateBaseSensor):
|
||||
@@ -1143,12 +1139,10 @@ class ChildStatsSensor(TaskMateBaseSensor):
|
||||
if not (child.id in c.assigned_to or not c.assigned_to):
|
||||
return False
|
||||
mode = getattr(c, "assignment_mode", "everyone")
|
||||
if mode not in ("everyone", "first_come"):
|
||||
if getattr(c, "assignment_current_child_id", "") != child.id:
|
||||
return False
|
||||
if mode != "everyone":
|
||||
if self.coordinator._is_rotation_done_today(c):
|
||||
return False
|
||||
if mode not in ("everyone", "first_come") and getattr(c, "assignment_current_child_id", "") != child.id:
|
||||
return False
|
||||
if mode != "everyone" and self.coordinator._is_rotation_done_today(c):
|
||||
return False
|
||||
return True
|
||||
|
||||
assigned_chores = [c for c in chores if _included(c)]
|
||||
|
||||
@@ -378,10 +378,6 @@ class TaskMateStorage:
|
||||
if chore_id in order:
|
||||
order.remove(chore_id)
|
||||
|
||||
def get_chore_display_order(self) -> list[str]:
|
||||
"""Get the global chore display order."""
|
||||
return list(self._data.get("chore_display_order", []))
|
||||
|
||||
def set_chore_display_order(self, order: list[str]) -> None:
|
||||
"""Set the global chore display order."""
|
||||
self._data["chore_display_order"] = list(order)
|
||||
|
||||
@@ -35,6 +35,22 @@ TEMPLATE_CHORE_FIELDS = (
|
||||
_WEEKDAYS = ["monday", "tuesday", "wednesday", "thursday", "friday"]
|
||||
_WEEKENDS = ["saturday", "sunday"]
|
||||
|
||||
|
||||
def _chore(name: str, points: int, time_category: str, due_days: list[str] = _WEEKDAYS) -> dict:
|
||||
"""One built-in template chore; all built-ins share the same fixed defaults."""
|
||||
return {
|
||||
"name": name,
|
||||
"points": points,
|
||||
"time_category": time_category,
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(due_days),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
}
|
||||
|
||||
|
||||
BUILT_IN_TEMPLATES: list[dict] = [
|
||||
{
|
||||
"id": "morning_routine",
|
||||
@@ -42,50 +58,10 @@ BUILT_IN_TEMPLATES: list[dict] = [
|
||||
"icon": "mdi:weather-sunny",
|
||||
"builtin": True,
|
||||
"chores": [
|
||||
{
|
||||
"name": "Make bed",
|
||||
"points": 2,
|
||||
"time_category": "morning",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Brush teeth",
|
||||
"points": 1,
|
||||
"time_category": "morning",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Get dressed",
|
||||
"points": 1,
|
||||
"time_category": "morning",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Pack school bag",
|
||||
"points": 2,
|
||||
"time_category": "morning",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
_chore("Make bed", 2, "morning"),
|
||||
_chore("Brush teeth", 1, "morning"),
|
||||
_chore("Get dressed", 1, "morning"),
|
||||
_chore("Pack school bag", 2, "morning"),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -94,50 +70,10 @@ BUILT_IN_TEMPLATES: list[dict] = [
|
||||
"icon": "mdi:weather-night",
|
||||
"builtin": True,
|
||||
"chores": [
|
||||
{
|
||||
"name": "Brush teeth",
|
||||
"points": 1,
|
||||
"time_category": "evening",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Put on pyjamas",
|
||||
"points": 1,
|
||||
"time_category": "evening",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Tidy room",
|
||||
"points": 2,
|
||||
"time_category": "evening",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Set out clothes for tomorrow",
|
||||
"points": 1,
|
||||
"time_category": "evening",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
_chore("Brush teeth", 1, "evening"),
|
||||
_chore("Put on pyjamas", 1, "evening"),
|
||||
_chore("Tidy room", 2, "evening"),
|
||||
_chore("Set out clothes for tomorrow", 1, "evening"),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -146,50 +82,10 @@ BUILT_IN_TEMPLATES: list[dict] = [
|
||||
"icon": "mdi:silverware-fork-knife",
|
||||
"builtin": True,
|
||||
"chores": [
|
||||
{
|
||||
"name": "Set table",
|
||||
"points": 2,
|
||||
"time_category": "evening",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Clear plates",
|
||||
"points": 2,
|
||||
"time_category": "evening",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Load dishwasher",
|
||||
"points": 3,
|
||||
"time_category": "evening",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Wipe counters",
|
||||
"points": 2,
|
||||
"time_category": "evening",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
_chore("Set table", 2, "evening"),
|
||||
_chore("Clear plates", 2, "evening"),
|
||||
_chore("Load dishwasher", 3, "evening"),
|
||||
_chore("Wipe counters", 2, "evening"),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -198,50 +94,10 @@ BUILT_IN_TEMPLATES: list[dict] = [
|
||||
"icon": "mdi:broom",
|
||||
"builtin": True,
|
||||
"chores": [
|
||||
{
|
||||
"name": "Tidy bedroom",
|
||||
"points": 3,
|
||||
"time_category": "anytime",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKENDS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Hoover",
|
||||
"points": 4,
|
||||
"time_category": "anytime",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKENDS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Help with laundry",
|
||||
"points": 3,
|
||||
"time_category": "anytime",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKENDS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Take bins out",
|
||||
"points": 2,
|
||||
"time_category": "anytime",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKENDS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
_chore("Tidy bedroom", 3, "anytime", _WEEKENDS),
|
||||
_chore("Hoover", 4, "anytime", _WEEKENDS),
|
||||
_chore("Help with laundry", 3, "anytime", _WEEKENDS),
|
||||
_chore("Take bins out", 2, "anytime", _WEEKENDS),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -250,50 +106,10 @@ BUILT_IN_TEMPLATES: list[dict] = [
|
||||
"icon": "mdi:paw",
|
||||
"builtin": True,
|
||||
"chores": [
|
||||
{
|
||||
"name": "Feed pet",
|
||||
"points": 2,
|
||||
"time_category": "morning",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Fill water bowl",
|
||||
"points": 1,
|
||||
"time_category": "morning",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Walk dog",
|
||||
"points": 3,
|
||||
"time_category": "afternoon",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Clean litter tray",
|
||||
"points": 3,
|
||||
"time_category": "anytime",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
_chore("Feed pet", 2, "morning"),
|
||||
_chore("Fill water bowl", 1, "morning"),
|
||||
_chore("Walk dog", 3, "afternoon"),
|
||||
_chore("Clean litter tray", 3, "anytime"),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -302,39 +118,9 @@ BUILT_IN_TEMPLATES: list[dict] = [
|
||||
"icon": "mdi:book-open-variant",
|
||||
"builtin": True,
|
||||
"chores": [
|
||||
{
|
||||
"name": "Do homework",
|
||||
"points": 3,
|
||||
"time_category": "afternoon",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Read for 20 minutes",
|
||||
"points": 2,
|
||||
"time_category": "anytime",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
{
|
||||
"name": "Practice instrument",
|
||||
"points": 2,
|
||||
"time_category": "anytime",
|
||||
"schedule_mode": "specific_days",
|
||||
"due_days": list(_WEEKDAYS),
|
||||
"requires_approval": False,
|
||||
"assignment_mode": "everyone",
|
||||
"daily_limit": 1,
|
||||
"completion_sound": "coin",
|
||||
},
|
||||
_chore("Do homework", 3, "afternoon"),
|
||||
_chore("Read for 20 minutes", 2, "anytime"),
|
||||
_chore("Practice instrument", 2, "anytime"),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TaskMateCoordinator
|
||||
from .entity import taskmate_device_info
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
|
||||
@@ -57,12 +58,7 @@ class TaskMateChildTodoList(CoordinatorEntity, TodoListEntity):
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._entry.entry_id)},
|
||||
name="TaskMate",
|
||||
manufacturer="TaskMate",
|
||||
model="Family Chore Manager",
|
||||
)
|
||||
return taskmate_device_info(self._entry.entry_id)
|
||||
|
||||
@property
|
||||
def todo_items(self) -> list[TodoItem]:
|
||||
|
||||
@@ -59,10 +59,13 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import images, photos
|
||||
from .const import (
|
||||
ASSIGNMENT_MODES,
|
||||
DEFAULT_NOTIFICATION_NAV_URL,
|
||||
DEFAULT_TIME_PERIODS,
|
||||
DIFFICULTY_TIERS,
|
||||
DOMAIN,
|
||||
MAX_TIME_PERIODS,
|
||||
SCHEDULE_MODES,
|
||||
TIME_CATEGORY_ICONS,
|
||||
)
|
||||
from .coordinator import TaskMateCoordinator
|
||||
@@ -598,8 +601,8 @@ def _chore_payload_schema(*, require_name: bool):
|
||||
vol.Optional("completion_sound"): str,
|
||||
vol.Optional("icon"): str,
|
||||
vol.Optional("image_url"): _image_url_or_blank,
|
||||
vol.Optional("difficulty"): vol.In(["easy", "medium", "hard"]),
|
||||
vol.Optional("schedule_mode"): vol.In(["specific_days", "recurring", "one_shot"]),
|
||||
vol.Optional("difficulty"): vol.In(DIFFICULTY_TIERS),
|
||||
vol.Optional("schedule_mode"): vol.In(SCHEDULE_MODES),
|
||||
vol.Optional("due_days"): [str],
|
||||
vol.Optional("recurrence"): str,
|
||||
vol.Optional("recurrence_day"): str,
|
||||
@@ -624,9 +627,7 @@ def _chore_payload_schema(*, require_name: bool):
|
||||
vol.Optional("mandatory"): bool,
|
||||
vol.Optional("mandatory_penalty_points"): vol.All(int, vol.Range(min=0)),
|
||||
vol.Optional("require_photo"): bool,
|
||||
vol.Optional("assignment_mode"): vol.In(
|
||||
["everyone", "alternating", "random", "balanced", "first_come", "unassigned"]
|
||||
),
|
||||
vol.Optional("assignment_mode"): vol.In(ASSIGNMENT_MODES),
|
||||
vol.Optional("assignment_rotation_anchor"): str,
|
||||
vol.Optional("require_availability"): bool,
|
||||
vol.Optional("publish_calendar_entities"): [str],
|
||||
@@ -1967,7 +1968,7 @@ async def _ws_set_global_chore_order(hass, connection, msg, coordinator):
|
||||
vol.Optional("assigned_to"): [str],
|
||||
vol.Optional("requires_approval"): bool,
|
||||
vol.Optional("time_category"): str,
|
||||
vol.Optional("schedule_mode"): vol.In(["specific_days", "recurring", "one_shot"]),
|
||||
vol.Optional("schedule_mode"): vol.In(SCHEDULE_MODES),
|
||||
vol.Optional("due_days"): [str],
|
||||
vol.Optional("daily_limit"): vol.All(int, vol.Range(min=1)),
|
||||
vol.Optional("completion_sound"): str,
|
||||
@@ -2035,15 +2036,13 @@ async def _ws_templates_get(hass, connection, msg, coordinator):
|
||||
vol.Optional("time_category"): str,
|
||||
vol.Optional("daily_limit"): vol.All(int, vol.Range(min=1)),
|
||||
vol.Optional("completion_sound"): str,
|
||||
vol.Optional("schedule_mode"): vol.In(["specific_days", "recurring", "one_shot"]),
|
||||
vol.Optional("schedule_mode"): vol.In(SCHEDULE_MODES),
|
||||
vol.Optional("due_days"): [str],
|
||||
vol.Optional("recurrence"): str,
|
||||
vol.Optional("recurrence_day"): str,
|
||||
vol.Optional("recurrence_start"): str,
|
||||
vol.Optional("first_occurrence_mode"): str,
|
||||
vol.Optional("assignment_mode"): vol.In(
|
||||
["everyone", "alternating", "random", "balanced", "first_come", "unassigned"]
|
||||
),
|
||||
vol.Optional("assignment_mode"): vol.In(ASSIGNMENT_MODES),
|
||||
vol.Optional("require_availability"): bool,
|
||||
vol.Optional("visibility_entity"): str,
|
||||
vol.Optional("visibility_state"): str,
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"approvals.all_caught_up": "Alles aufgeholt!",
|
||||
"approvals.approve": "Genehmigen",
|
||||
"approvals.approve_all": "Alle genehmigen",
|
||||
"approvals.approve_all_busy": "Wird genehmigt…",
|
||||
"approvals.approve_all_confirm": "{count} ausstehende Aufgaben genehmigen?",
|
||||
"approvals.card_description": "Eine Karte zum Verwalten ausstehender Aufgabengenehmigungen für TaskMate",
|
||||
"approvals.card_name": "TaskMate-Genehmigungen",
|
||||
@@ -229,6 +230,12 @@
|
||||
"child.editor.child": "Kind",
|
||||
"child.editor.child_helper": "Für welches Kind ist diese Karte?",
|
||||
"child.editor.chores_not_due_today": "Aufgaben, die heute nicht fällig sind",
|
||||
"child.editor.dependency_mode": "Aufgaben, die auf andere warten",
|
||||
"child.editor.dependency_hide": "Ausblenden – erst nach Freischaltung zeigen",
|
||||
"child.editor.dependency_dim": "Gedimmt – ausgegraut anzeigen, nicht interaktiv",
|
||||
"child.editor.dependency_show": "Anzeigen – normal zeigen (weiterhin nicht antippbar)",
|
||||
"child.editor.dependency_helper": "Was angezeigt wird, wenn eine Aufgabe von einer anderen abhängt, die heute noch nicht genehmigt ist",
|
||||
"child.blocked_by_dependency": "Zuerst {chores} erledigen",
|
||||
"child.editor.due_days_dim": "Gedimmt – ausgegraut anzeigen, nicht interaktiv",
|
||||
"child.editor.due_days_helper": "Gilt für Aufgaben, bei denen due_days festgelegt ist, wenn der heutige Tag nicht geplant ist",
|
||||
"child.editor.due_days_hide": "Ausblenden – nicht anzeigen",
|
||||
@@ -241,7 +248,7 @@
|
||||
"child.editor.recurrence_dim": "Gedimmt – ausgegraut anzeigen, nicht interaktiv",
|
||||
"child.editor.recurrence_helper": "Was angezeigt werden soll, wenn eine wiederkehrende Aufgabe abgeschlossen wurde und das Fenster noch nicht zurückgesetzt wurde",
|
||||
"child.editor.recurrence_hide": "Ausblenden – erst wieder anzeigen, wenn es wieder verfügbar ist",
|
||||
"child.editor.recurrence_show": "Anzeigen – trotzdem normal anzeigen",
|
||||
"child.editor.recurrence_show": "Anzeigen – normal zeigen (weiterhin nicht antippbar)",
|
||||
"child.editor.recurring_when_completed": "Wiederkehrende Aufgaben – nach Abschluss",
|
||||
"child.editor.select_child": "Wählen Sie ein Kind aus...",
|
||||
"child.editor.show_countdown": "Countdown zum Zurücksetzen neben „Heutige Aufgaben“ anzeigen",
|
||||
@@ -420,6 +427,7 @@
|
||||
"overview.pending_count": "{count} ausstehend",
|
||||
"panel.activity_approve": "Genehmigen",
|
||||
"panel.activity_approve_all": "Alle genehmigen",
|
||||
"panel.activity_approve_all_busy": "Wird genehmigt…",
|
||||
"panel.activity_approve_all_confirm": "{count} ausstehende Aufgaben genehmigen?",
|
||||
"panel.activity_approve_all_done": "{count} Aufgaben genehmigt",
|
||||
"panel.activity_audit_log": "Protokoll",
|
||||
@@ -1738,5 +1746,6 @@
|
||||
"panel.chore_image_bad_type": "Wähle ein JPEG-, PNG- oder WebP-Bild.",
|
||||
"child.editor.pre_reader": "Nur-Bilder-Modus (für kleine Kinder)",
|
||||
"child.editor.pre_reader_labels": "Namen im Bildmodus anzeigen",
|
||||
"child.editor.pre_reader_points": "Punktzahl statt Sterne im Bildmodus anzeigen",
|
||||
"common.design.accessible": "Barrierefrei"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"approvals.all_caught_up": "All caught up!",
|
||||
"approvals.approve": "Approve",
|
||||
"approvals.approve_all": "Approve all",
|
||||
"approvals.approve_all_busy": "Approving…",
|
||||
"approvals.approve_all_confirm": "Approve {count} pending chores?",
|
||||
"approvals.card_description": "A card to manage pending chore approvals for TaskMate",
|
||||
"approvals.card_name": "TaskMate Approvals",
|
||||
@@ -231,6 +232,12 @@
|
||||
"child.editor.child": "Child",
|
||||
"child.editor.child_helper": "Which child is this card for?",
|
||||
"child.editor.chores_not_due_today": "Chores Not Due Today",
|
||||
"child.editor.dependency_mode": "Chores Waiting On Another",
|
||||
"child.editor.dependency_hide": "Hide — don't show until unlocked",
|
||||
"child.editor.dependency_dim": "Dim — show greyed out, non-interactive",
|
||||
"child.editor.dependency_show": "Show — show normally (still not tappable)",
|
||||
"child.editor.dependency_helper": "What to show when a chore depends on another that isn't approved yet today",
|
||||
"child.blocked_by_dependency": "Do {chores} first",
|
||||
"child.editor.due_days_dim": "Dim — show greyed out, non-interactive",
|
||||
"child.editor.due_days_helper": "Applies to chores with due_days set when today isn't scheduled",
|
||||
"child.editor.due_days_hide": "Hide — don't show",
|
||||
@@ -243,7 +250,7 @@
|
||||
"child.editor.recurrence_dim": "Dim — show greyed out, non-interactive",
|
||||
"child.editor.recurrence_helper": "What to show when a recurring chore has been completed and the window hasn't reset yet",
|
||||
"child.editor.recurrence_hide": "Hide — don't show until available again",
|
||||
"child.editor.recurrence_show": "Show — show normally regardless",
|
||||
"child.editor.recurrence_show": "Show — show normally (still not tappable)",
|
||||
"child.editor.recurring_when_completed": "Recurring Chores — When Completed",
|
||||
"child.editor.select_child": "Select a child...",
|
||||
"child.editor.show_countdown": "Show reset countdown beside \"Today's Chores\"",
|
||||
@@ -427,6 +434,7 @@
|
||||
"overview.pending_count": "{count} pending",
|
||||
"panel.activity_approve": "Approve",
|
||||
"panel.activity_approve_all": "Approve all",
|
||||
"panel.activity_approve_all_busy": "Approving…",
|
||||
"panel.activity_approve_all_confirm": "Approve {count} pending chores?",
|
||||
"panel.activity_approve_all_done": "Approved {count} chores",
|
||||
"panel.activity_audit_log": "Audit log",
|
||||
@@ -1738,5 +1746,6 @@
|
||||
"panel.chore_image_bad_type": "Choose a JPEG, PNG or WebP image.",
|
||||
"child.editor.pre_reader": "Picture-only mode (for young children)",
|
||||
"child.editor.pre_reader_labels": "Show names in picture mode",
|
||||
"child.editor.pre_reader_points": "Show point values instead of stars in picture mode",
|
||||
"common.design.accessible": "Accessible"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"approvals.all_caught_up": "All caught up!",
|
||||
"approvals.approve": "Approve",
|
||||
"approvals.approve_all": "Approve all",
|
||||
"approvals.approve_all_busy": "Approving…",
|
||||
"approvals.approve_all_confirm": "Approve {count} pending chores?",
|
||||
"approvals.card_description": "A card to manage pending chore approvals for TaskMate",
|
||||
"approvals.card_name": "TaskMate Approvals",
|
||||
@@ -231,6 +232,12 @@
|
||||
"child.editor.child": "Child",
|
||||
"child.editor.child_helper": "Which child is this card for?",
|
||||
"child.editor.chores_not_due_today": "Chores Not Due Today",
|
||||
"child.editor.dependency_mode": "Chores Waiting On Another",
|
||||
"child.editor.dependency_hide": "Hide — don't show until unlocked",
|
||||
"child.editor.dependency_dim": "Dim — show greyed out, non-interactive",
|
||||
"child.editor.dependency_show": "Show — show normally (still not tappable)",
|
||||
"child.editor.dependency_helper": "What to show when a chore depends on another that isn't approved yet today",
|
||||
"child.blocked_by_dependency": "Do {chores} first",
|
||||
"child.editor.due_days_dim": "Dim — show greyed out, non-interactive",
|
||||
"child.editor.due_days_helper": "Applies to chores with due_days set when today isn't scheduled",
|
||||
"child.editor.due_days_hide": "Hide — don't show",
|
||||
@@ -243,7 +250,7 @@
|
||||
"child.editor.recurrence_dim": "Dim — show greyed out, non-interactive",
|
||||
"child.editor.recurrence_helper": "What to show when a recurring chore has been completed and the window hasn't reset yet",
|
||||
"child.editor.recurrence_hide": "Hide — don't show until available again",
|
||||
"child.editor.recurrence_show": "Show — show normally regardless",
|
||||
"child.editor.recurrence_show": "Show — show normally (still not tappable)",
|
||||
"child.editor.recurring_when_completed": "Recurring Chores — When Completed",
|
||||
"child.editor.select_child": "Select a child...",
|
||||
"child.editor.show_countdown": "Show reset countdown beside \"Today's Chores\"",
|
||||
@@ -427,6 +434,7 @@
|
||||
"overview.pending_count": "{count} pending",
|
||||
"panel.activity_approve": "Approve",
|
||||
"panel.activity_approve_all": "Approve all",
|
||||
"panel.activity_approve_all_busy": "Approving…",
|
||||
"panel.activity_approve_all_confirm": "Approve {count} pending chores?",
|
||||
"panel.activity_approve_all_done": "Approved {count} chores",
|
||||
"panel.activity_audit_log": "Audit log",
|
||||
@@ -1738,5 +1746,6 @@
|
||||
"panel.chore_image_bad_type": "Choose a JPEG, PNG or WebP image.",
|
||||
"child.editor.pre_reader": "Picture-only mode (for young children)",
|
||||
"child.editor.pre_reader_labels": "Show names in picture mode",
|
||||
"child.editor.pre_reader_points": "Show point values instead of stars in picture mode",
|
||||
"common.design.accessible": "Accessible"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"approvals.all_caught_up": "Tout est à jour !",
|
||||
"approvals.approve": "Approuver",
|
||||
"approvals.approve_all": "Tout approuver",
|
||||
"approvals.approve_all_busy": "Approbation…",
|
||||
"approvals.approve_all_confirm": "Approuver {count} tâches en attente ?",
|
||||
"approvals.card_description": "Une carte pour gérer les approbations de tâches en attente pour TaskMate",
|
||||
"approvals.card_name": "Approbations TaskMate",
|
||||
@@ -229,6 +230,12 @@
|
||||
"child.editor.child": "Enfant",
|
||||
"child.editor.child_helper": "Pour quel enfant est cette carte ?",
|
||||
"child.editor.chores_not_due_today": "Tâches non prévues aujourd'hui",
|
||||
"child.editor.dependency_mode": "Tâches en attente d'une autre",
|
||||
"child.editor.dependency_hide": "Masquer — ne pas afficher avant déblocage",
|
||||
"child.editor.dependency_dim": "Estomper — afficher en gris, non interactif",
|
||||
"child.editor.dependency_show": "Afficher — affichage normal (toujours non cliquable)",
|
||||
"child.editor.dependency_helper": "Ce qu'il faut afficher quand une tâche dépend d'une autre non encore approuvée aujourd'hui",
|
||||
"child.blocked_by_dependency": "Fais d'abord {chores}",
|
||||
"child.editor.due_days_dim": "Estomper — afficher en gris, non interactif",
|
||||
"child.editor.due_days_helper": "S'applique aux tâches avec des jours de passage définis quand aujourd'hui n'est pas programmé",
|
||||
"child.editor.due_days_hide": "Masquer — ne pas afficher",
|
||||
@@ -241,7 +248,7 @@
|
||||
"child.editor.recurrence_dim": "Estomper — afficher en gris, non interactif",
|
||||
"child.editor.recurrence_helper": "Que montrer lorsqu'une tâche récurrente a été accomplie et que la fenêtre n'a pas encore été réinitialisée",
|
||||
"child.editor.recurrence_hide": "Masquer — ne pas afficher avant d'être à nouveau disponible",
|
||||
"child.editor.recurrence_show": "Afficher — afficher normalement malgré tout",
|
||||
"child.editor.recurrence_show": "Afficher — affichage normal (toujours non cliquable)",
|
||||
"child.editor.recurring_when_completed": "Tâches récurrentes — une fois terminées",
|
||||
"child.editor.select_child": "Sélectionner un enfant...",
|
||||
"child.editor.show_countdown": "Afficher le compte à rebours de réinitialisation à côté de \"Tâches d'aujourd'hui\"",
|
||||
@@ -420,6 +427,7 @@
|
||||
"overview.pending_count": "{count} en attente",
|
||||
"panel.activity_approve": "Approuver",
|
||||
"panel.activity_approve_all": "Tout approuver",
|
||||
"panel.activity_approve_all_busy": "Approbation…",
|
||||
"panel.activity_approve_all_confirm": "Approuver {count} tâches en attente ?",
|
||||
"panel.activity_approve_all_done": "{count} tâches approuvées",
|
||||
"panel.activity_audit_log": "Journal d'audit",
|
||||
@@ -1738,5 +1746,6 @@
|
||||
"panel.chore_image_bad_type": "Choisissez une image JPEG, PNG ou WebP.",
|
||||
"child.editor.pre_reader": "Mode images seules (jeunes enfants)",
|
||||
"child.editor.pre_reader_labels": "Afficher les noms en mode images",
|
||||
"child.editor.pre_reader_points": "Afficher les points au lieu des étoiles en mode images",
|
||||
"common.design.accessible": "Accessible"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"approvals.all_caught_up": "Alt er oppdatert!",
|
||||
"approvals.approve": "Godkjenn",
|
||||
"approvals.approve_all": "Godkjenn alle",
|
||||
"approvals.approve_all_busy": "Godkjenner…",
|
||||
"approvals.approve_all_confirm": "Godkjenne {count} ventende gjøremål?",
|
||||
"approvals.card_description": "Et kort for å håndtere ventende oppgavegodkjenninger",
|
||||
"approvals.card_name": "TaskMate Godkjenninger",
|
||||
@@ -229,6 +230,12 @@
|
||||
"child.editor.child": "Barn",
|
||||
"child.editor.child_helper": "Hvilket barn er dette kortet for?",
|
||||
"child.editor.chores_not_due_today": "Oppgaver som ikke forfaller i dag",
|
||||
"child.editor.dependency_mode": "Oppgaver som venter på en annen",
|
||||
"child.editor.dependency_hide": "Skjul — ikke vis før den er låst opp",
|
||||
"child.editor.dependency_dim": "Dim — vis gråtonet, ikke-interaktiv",
|
||||
"child.editor.dependency_show": "Vis — vis normalt (fortsatt ikke trykkbar)",
|
||||
"child.editor.dependency_helper": "Hva som vises når en oppgave avhenger av en annen som ikke er godkjent i dag ennå",
|
||||
"child.blocked_by_dependency": "Gjør {chores} først",
|
||||
"child.editor.due_days_dim": "Dim — vis gråtonet, ikke-interaktiv",
|
||||
"child.editor.due_days_helper": "Gjelder oppgaver med satte dager når i dag ikke er planlagt",
|
||||
"child.editor.due_days_hide": "Skjul — ikke vis",
|
||||
@@ -241,7 +248,7 @@
|
||||
"child.editor.recurrence_dim": "Dim — vis gråtonet, ikke-interaktiv",
|
||||
"child.editor.recurrence_helper": "Hva som vises når en gjentagende oppgave er fullført og vinduet ikke har nullstilt seg ennå",
|
||||
"child.editor.recurrence_hide": "Skjul — ikke vis før tilgjengelig igjen",
|
||||
"child.editor.recurrence_show": "Vis — vis normalt uansett",
|
||||
"child.editor.recurrence_show": "Vis — vis normalt (fortsatt ikke trykkbar)",
|
||||
"child.editor.recurring_when_completed": "Gjentagende oppgaver — når fullført",
|
||||
"child.editor.select_child": "Velg et barn...",
|
||||
"child.editor.show_countdown": "Vis nedtelling ved siden av «Dagens oppgaver»",
|
||||
@@ -420,6 +427,7 @@
|
||||
"overview.pending_count": "{count} venter",
|
||||
"panel.activity_approve": "Godkjenn",
|
||||
"panel.activity_approve_all": "Godkjenn alle",
|
||||
"panel.activity_approve_all_busy": "Godkjenner…",
|
||||
"panel.activity_approve_all_confirm": "Godkjenne {count} ventende gjøremål?",
|
||||
"panel.activity_approve_all_done": "Godkjente {count} gjøremål",
|
||||
"panel.activity_audit_log": "Revisjonslogg",
|
||||
@@ -1738,5 +1746,6 @@
|
||||
"panel.chore_image_bad_type": "Velg et JPEG-, PNG- eller WebP-bilde.",
|
||||
"child.editor.pre_reader": "Kun bilder (for små barn)",
|
||||
"child.editor.pre_reader_labels": "Vis navn i bildemodus",
|
||||
"child.editor.pre_reader_points": "Vis poengverdier i stedet for stjerner i bildemodus",
|
||||
"common.design.accessible": "Tilgjengelig"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"approvals.all_caught_up": "Alt er oppdatert!",
|
||||
"approvals.approve": "Godkjenn",
|
||||
"approvals.approve_all": "Godkjenn alle",
|
||||
"approvals.approve_all_busy": "Godkjenner…",
|
||||
"approvals.approve_all_confirm": "Godkjenne {count} ventande gjeremål?",
|
||||
"approvals.card_description": "Eit kort for å handtere ventande oppgåvegodkjenningar",
|
||||
"approvals.card_name": "TaskMate Godkjenningar",
|
||||
@@ -229,6 +230,12 @@
|
||||
"child.editor.child": "Born",
|
||||
"child.editor.child_helper": "Kva born er dette kortet for?",
|
||||
"child.editor.chores_not_due_today": "Oppgåver som ikkje forfell i dag",
|
||||
"child.editor.dependency_mode": "Oppgåver som ventar på ei anna",
|
||||
"child.editor.dependency_hide": "Skjul — ikkje vis før den er låst opp",
|
||||
"child.editor.dependency_dim": "Dim — vis gråtona, ikkje-interaktiv",
|
||||
"child.editor.dependency_show": "Vis — vis normalt (framleis ikkje trykkbar)",
|
||||
"child.editor.dependency_helper": "Kva som blir vist når ei oppgåve avheng av ei anna som ikkje er godkjend i dag enno",
|
||||
"child.blocked_by_dependency": "Gjer {chores} først",
|
||||
"child.editor.due_days_dim": "Dim — vis gråtona, ikkje-interaktiv",
|
||||
"child.editor.due_days_helper": "Gjeld oppgåver med sette dagar når i dag ikkje er planlagt",
|
||||
"child.editor.due_days_hide": "Gøym — ikkje vis",
|
||||
@@ -241,7 +248,7 @@
|
||||
"child.editor.recurrence_dim": "Dim — vis gråtona, ikkje-interaktiv",
|
||||
"child.editor.recurrence_helper": "Kva som visast når ei gjentakande oppgåve er fullført og vindauget ikkje har nullstilt seg enno",
|
||||
"child.editor.recurrence_hide": "Gøym — ikkje vis før tilgjengeleg att",
|
||||
"child.editor.recurrence_show": "Vis — vis normalt uansett",
|
||||
"child.editor.recurrence_show": "Vis — vis normalt (framleis ikkje trykkbar)",
|
||||
"child.editor.recurring_when_completed": "Gjentakande oppgåver — når fullført",
|
||||
"child.editor.select_child": "Vel eit born...",
|
||||
"child.editor.show_countdown": "Vis nedteljing ved sida av «Dagens oppgåver»",
|
||||
@@ -420,6 +427,7 @@
|
||||
"overview.pending_count": "{count} ventar",
|
||||
"panel.activity_approve": "Godkjenn",
|
||||
"panel.activity_approve_all": "Godkjenn alle",
|
||||
"panel.activity_approve_all_busy": "Godkjenner…",
|
||||
"panel.activity_approve_all_confirm": "Godkjenne {count} ventande gjeremål?",
|
||||
"panel.activity_approve_all_done": "Godkjende {count} gjeremål",
|
||||
"panel.activity_audit_log": "Revisjonslogg",
|
||||
@@ -1738,5 +1746,6 @@
|
||||
"panel.chore_image_bad_type": "Vel eit JPEG-, PNG- eller WebP-bilete.",
|
||||
"child.editor.pre_reader": "Berre bilete (for små born)",
|
||||
"child.editor.pre_reader_labels": "Vis namn i biletemodus",
|
||||
"child.editor.pre_reader_points": "Vis poengverdiar i staden for stjerner i biletemodus",
|
||||
"common.design.accessible": "Tilgjengeleg"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"approvals.all_caught_up": "Está tudo em dia!",
|
||||
"approvals.approve": "Aprovar",
|
||||
"approvals.approve_all": "Aprovar tudo",
|
||||
"approvals.approve_all_busy": "Aprovando…",
|
||||
"approvals.approve_all_confirm": "Aprovar {count} tarefas pendentes?",
|
||||
"approvals.card_description": "Cartão para gerir aprovações de tarefas pendentes no TaskMate",
|
||||
"approvals.card_name": "Aprovações TaskMate",
|
||||
@@ -229,6 +230,12 @@
|
||||
"child.editor.child": "Criança",
|
||||
"child.editor.child_helper": "Para qual criança é este cartão?",
|
||||
"child.editor.chores_not_due_today": "Tarefas Sem Prazo Hoje",
|
||||
"child.editor.dependency_mode": "Tarefas aguardando outra",
|
||||
"child.editor.dependency_hide": "Ocultar — não mostrar até desbloquear",
|
||||
"child.editor.dependency_dim": "Atenuar — mostrar em cinzento, sem interação",
|
||||
"child.editor.dependency_show": "Mostrar — mostrar normalmente (continua sem toque)",
|
||||
"child.editor.dependency_helper": "O que mostrar quando uma tarefa depende de outra ainda não aprovada hoje",
|
||||
"child.blocked_by_dependency": "Faça primeiro {chores}",
|
||||
"child.editor.due_days_dim": "Atenuar — mostrar em cinzento, sem interação",
|
||||
"child.editor.due_days_helper": "Aplica-se a tarefas com due_days definido quando hoje não está agendado",
|
||||
"child.editor.due_days_hide": "Ocultar — não mostrar",
|
||||
@@ -241,7 +248,7 @@
|
||||
"child.editor.recurrence_dim": "Atenuar — mostrar em cinzento, sem interação",
|
||||
"child.editor.recurrence_helper": "O que mostrar quando uma tarefa recorrente foi concluída e a janela ainda não reiniciou",
|
||||
"child.editor.recurrence_hide": "Ocultar — não mostrar até estar disponível novamente",
|
||||
"child.editor.recurrence_show": "Mostrar — mostrar normalmente",
|
||||
"child.editor.recurrence_show": "Mostrar — mostrar normalmente (continua sem toque)",
|
||||
"child.editor.recurring_when_completed": "Tarefas Recorrentes — Quando Concluídas",
|
||||
"child.editor.select_child": "Selecionar uma criança...",
|
||||
"child.editor.show_countdown": "Mostrar contagem regressiva de reinício junto a \"Tarefas de Hoje\"",
|
||||
@@ -420,6 +427,7 @@
|
||||
"overview.pending_count": "{count} pendente",
|
||||
"panel.activity_approve": "Aprovar",
|
||||
"panel.activity_approve_all": "Aprovar tudo",
|
||||
"panel.activity_approve_all_busy": "Aprovando…",
|
||||
"panel.activity_approve_all_confirm": "Aprovar {count} tarefas pendentes?",
|
||||
"panel.activity_approve_all_done": "{count} tarefas aprovadas",
|
||||
"panel.activity_audit_log": "Registro de auditoria",
|
||||
@@ -1738,5 +1746,6 @@
|
||||
"panel.chore_image_bad_type": "Escolha uma imagem JPEG, PNG ou WebP.",
|
||||
"child.editor.pre_reader": "Modo só imagens (crianças pequenas)",
|
||||
"child.editor.pre_reader_labels": "Mostrar nomes no modo imagens",
|
||||
"child.editor.pre_reader_points": "Mostrar pontos em vez de estrelas no modo imagens",
|
||||
"common.design.accessible": "Acessível"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"approvals.all_caught_up": "Está tudo em dia!",
|
||||
"approvals.approve": "Aprovar",
|
||||
"approvals.approve_all": "Aprovar tudo",
|
||||
"approvals.approve_all_busy": "A aprovar…",
|
||||
"approvals.approve_all_confirm": "Aprovar {count} tarefas pendentes?",
|
||||
"approvals.card_description": "Cartão para gerir aprovações de tarefas pendentes no TaskMate",
|
||||
"approvals.card_name": "Aprovações TaskMate",
|
||||
@@ -229,6 +230,12 @@
|
||||
"child.editor.child": "Criança",
|
||||
"child.editor.child_helper": "Para qual criança é este cartão?",
|
||||
"child.editor.chores_not_due_today": "Tarefas Sem Prazo Hoje",
|
||||
"child.editor.dependency_mode": "Tarefas à espera de outra",
|
||||
"child.editor.dependency_hide": "Ocultar — não mostrar até desbloquear",
|
||||
"child.editor.dependency_dim": "Atenuar — mostrar em cinzento, sem interação",
|
||||
"child.editor.dependency_show": "Mostrar — mostrar normalmente (continua sem toque)",
|
||||
"child.editor.dependency_helper": "O que mostrar quando uma tarefa depende de outra ainda não aprovada hoje",
|
||||
"child.blocked_by_dependency": "Faz primeiro {chores}",
|
||||
"child.editor.due_days_dim": "Atenuar — mostrar em cinzento, sem interação",
|
||||
"child.editor.due_days_helper": "Aplica-se a tarefas com due_days definido quando hoje não está agendado",
|
||||
"child.editor.due_days_hide": "Ocultar — não mostrar",
|
||||
@@ -241,7 +248,7 @@
|
||||
"child.editor.recurrence_dim": "Atenuar — mostrar em cinzento, sem interação",
|
||||
"child.editor.recurrence_helper": "O que mostrar quando uma tarefa recorrente foi concluída e a janela ainda não reiniciou",
|
||||
"child.editor.recurrence_hide": "Ocultar — não mostrar até estar disponível novamente",
|
||||
"child.editor.recurrence_show": "Mostrar — mostrar normalmente",
|
||||
"child.editor.recurrence_show": "Mostrar — mostrar normalmente (continua sem toque)",
|
||||
"child.editor.recurring_when_completed": "Tarefas Recorrentes — Quando Concluídas",
|
||||
"child.editor.select_child": "Selecionar uma criança...",
|
||||
"child.editor.show_countdown": "Mostrar contagem regressiva de reinício junto a \"Tarefas de Hoje\"",
|
||||
@@ -425,6 +432,7 @@
|
||||
"overview.pending_count": "{count} pendente",
|
||||
"panel.activity_approve": "Aprovar",
|
||||
"panel.activity_approve_all": "Aprovar tudo",
|
||||
"panel.activity_approve_all_busy": "A aprovar…",
|
||||
"panel.activity_approve_all_confirm": "Aprovar {count} tarefas pendentes?",
|
||||
"panel.activity_approve_all_done": "{count} tarefas aprovadas",
|
||||
"panel.activity_audit_log": "Registo de auditoria",
|
||||
@@ -1738,5 +1746,6 @@
|
||||
"panel.chore_image_bad_type": "Escolha uma imagem JPEG, PNG ou WebP.",
|
||||
"child.editor.pre_reader": "Modo só imagens (crianças pequenas)",
|
||||
"child.editor.pre_reader_labels": "Mostrar nomes no modo imagens",
|
||||
"child.editor.pre_reader_points": "Mostrar pontos em vez de estrelas no modo imagens",
|
||||
"common.design.accessible": "Acessível"
|
||||
}
|
||||
|
||||
@@ -106,6 +106,9 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
}
|
||||
|
||||
.approve-all-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.45);
|
||||
@@ -118,6 +121,18 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
}
|
||||
.approve-all-btn:hover { background: rgba(255, 255, 255, 0.3); }
|
||||
.approve-all-btn:disabled { opacity: 0.6; cursor: default; }
|
||||
/* Reuses the spin keyframes already defined for the loading spinner. */
|
||||
.btn-spinner {
|
||||
width: 12px; height: 12px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.45);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.btn-spinner { animation: none; opacity: 0.7; }
|
||||
}
|
||||
|
||||
.card-content { padding: 16px; }
|
||||
|
||||
@@ -537,7 +552,9 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
<div class="header-right">
|
||||
${filteredCompletions.length > 0
|
||||
? html`<button class="approve-all-btn" ?disabled="${this._loading['__all__']}"
|
||||
@click="${() => this._handleApproveAll(filteredCompletions)}">${this._t('approvals.approve_all')}</button>`
|
||||
@click="${() => this._handleApproveAll(filteredCompletions)}">${this._loading['__all__']
|
||||
? html`<span class="btn-spinner"></span>${this._t('approvals.approve_all_busy')}`
|
||||
: this._t('approvals.approve_all')}</button>`
|
||||
: ""}
|
||||
${totalPending > 0 ? html`<span class="pending-count">${totalPending}</span>` : ""}
|
||||
</div>
|
||||
@@ -724,7 +741,9 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
<span class="tt">${title}</span>
|
||||
${completions.length > 0
|
||||
? html`<button class="approve-all-btn" ?disabled="${this._loading['__all__']}"
|
||||
@click="${() => this._handleApproveAll(completions)}">${this._t('approvals.approve_all')}</button>`
|
||||
@click="${() => this._handleApproveAll(completions)}">${this._loading['__all__']
|
||||
? html`<span class="btn-spinner"></span>${this._t('approvals.approve_all_busy')}`
|
||||
: this._t('approvals.approve_all')}</button>`
|
||||
: ""}
|
||||
${count > 0 ? html`<span class="cnt">${count}</span>` : ""}
|
||||
</div>`;
|
||||
@@ -785,7 +804,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
<button class="btn bad" style="flex:1" ?disabled="${isLoading}" @click="${reject}">👎 ${this._t('approvals.reject')}</button>`;
|
||||
}
|
||||
|
||||
_apPhotoDesigned(it, cls, fallback) {
|
||||
_apPhotoDesigned(it, cls) {
|
||||
const photoUrl = tmSafePhotoUrl(it.photo);
|
||||
if (it.kind !== "completion" || !photoUrl) {
|
||||
return it.kind === "claim" && it.icon
|
||||
@@ -1154,8 +1173,6 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
}
|
||||
|
||||
_getDayLabel(date) {
|
||||
const timezone = this._getTimezone();
|
||||
|
||||
// Get today's date parts in HA timezone
|
||||
const now = new Date();
|
||||
const todayParts = this._getDatePartsInTimezone(now);
|
||||
@@ -1252,7 +1269,7 @@ class TaskMateApprovalsCard extends LitElement {
|
||||
_renderApprovals(groupedByDay) {
|
||||
return html`
|
||||
${groupedByDay.map(
|
||||
([dayKey, dayGroup]) => html`
|
||||
([, dayGroup]) => html`
|
||||
<div class="day-group">
|
||||
<div class="day-header">${dayGroup.label}</div>
|
||||
${this._renderTimeCategories(dayGroup.timeCategories)}
|
||||
|
||||
@@ -30,7 +30,11 @@
|
||||
// Full pending-approvals lists (chore_completions / reward_claims). These
|
||||
// are NOT filtered to today, so approval cards pointed at the overview
|
||||
// entity can still surface a completion left pending from a previous day.
|
||||
"sensor.pending_approvals",
|
||||
// Named "Pending Approvals", but it carries no _attr_has_entity_name, so
|
||||
// the TaskMate device name still prefixes its entity id. This entry was
|
||||
// missing that prefix and so matched nothing at all, silently dropping
|
||||
// chore_completions and mandatory_misses from every merge (#798).
|
||||
"sensor.taskmate_pending_approvals",
|
||||
];
|
||||
|
||||
function mergedAttributes(hass, primaryEntityId) {
|
||||
|
||||
@@ -697,7 +697,7 @@ class TaskMateCalendarCard extends LitElement {
|
||||
style="${dim ? "" : `color:var(--tmd-${tok});background:color-mix(in srgb,var(--tmd-${tok}) 16%,transparent);border-color:transparent`}">${glyph[item.state] || glyph.due} ${item.chore.name}</span>`;
|
||||
}
|
||||
|
||||
_calPlayroom(blocks, pointsIcon) {
|
||||
_calPlayroom(blocks, _pointsIcon) {
|
||||
const glyph = { approved: "✓", pending: "◌", due: "—" };
|
||||
return html`
|
||||
<div class="grid cal-grid">
|
||||
@@ -713,7 +713,7 @@ class TaskMateCalendarCard extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_calConsole(blocks, pointsIcon) {
|
||||
_calConsole(blocks, _pointsIcon) {
|
||||
const glyph = { approved: "▮", pending: "▯", due: "▢" };
|
||||
return html`
|
||||
<div class="grid cal-grid-cn">
|
||||
@@ -733,7 +733,7 @@ class TaskMateCalendarCard extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_calCleanpro(blocks, pointsIcon) {
|
||||
_calCleanpro(blocks, _pointsIcon) {
|
||||
const glyph = { approved: "●", pending: "○", due: "—" };
|
||||
return html`
|
||||
<div class="grid cal-grid-cp">
|
||||
|
||||
@@ -1144,6 +1144,23 @@ class TaskMateChildCard extends LitElement {
|
||||
|
||||
.recurrence-label ha-icon { --mdc-icon-size: 12px; }
|
||||
|
||||
/* Dependency-blocked (#793). Under dim the row stays readable so a child
|
||||
can see what unlocks next; show leaves it looking normal, but either
|
||||
way the tap is refused in the handler. */
|
||||
.chore-card.dependency-blocked {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
.dependency-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.dependency-label ha-icon { --mdc-icon-size: 12px; }
|
||||
|
||||
|
||||
/* Recurring chore not yet available — dimmed/greyed */
|
||||
.chore-card.recurrence-unavailable {
|
||||
@@ -1272,6 +1289,11 @@ class TaskMateChildCard extends LitElement {
|
||||
.pre-tile-icon ha-icon { --mdc-icon-size: 64px; color: var(--primary-color); }
|
||||
.pre-tile-stars { display: flex; gap: 2px; }
|
||||
.pre-tile-stars ha-icon { --mdc-icon-size: 18px; color: #f1c40f; }
|
||||
.pre-tile-points {
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
font-size: 1.15rem; font-weight: 700; color: var(--primary-text-color);
|
||||
}
|
||||
.pre-tile-points ha-icon { --mdc-icon-size: 20px; color: #f1c40f; }
|
||||
.pre-tile-label {
|
||||
font-size: 0.85rem; font-weight: 700; text-align: center;
|
||||
color: var(--secondary-text-color); line-height: 1.2;
|
||||
@@ -1282,6 +1304,7 @@ class TaskMateChildCard extends LitElement {
|
||||
}
|
||||
.pre-tile.done .pre-tile-icon ha-icon { opacity: 0.35; }
|
||||
.pre-tile.done .pre-tile-stars { opacity: 0.35; }
|
||||
.pre-tile.done .pre-tile-points { opacity: 0.35; }
|
||||
.pre-tile-tick {
|
||||
position: absolute; inset: 0;
|
||||
display: grid; place-items: center;
|
||||
@@ -2418,8 +2441,8 @@ class TaskMateChildCard extends LitElement {
|
||||
${this._renderRoulette(child, childChores, pointsIcon)}
|
||||
${this.config.pre_reader === true ? html`
|
||||
<div class="pre-reader-grid">
|
||||
${childChores.map((chore, index) =>
|
||||
this._renderPreReaderTile(chore, child, todaysCompletions, index))}
|
||||
${childChores.map((chore) =>
|
||||
this._renderPreReaderTile(chore, child, pointsIcon, todaysCompletions))}
|
||||
</div>
|
||||
` : childChores.map((chore, index) => html`
|
||||
${chore.task_type === 'timed'
|
||||
@@ -2626,13 +2649,21 @@ class TaskMateChildCard extends LitElement {
|
||||
|
||||
const dueDaysMode = this.config.due_days_mode || "hide";
|
||||
const elapsedTimeMode = this.config.elapsed_time_mode || "dim";
|
||||
const dependencyMode = this.config.dependency_mode || "hide";
|
||||
const recurrenceDoneMode = this.config.recurrence_done_mode || "dim";
|
||||
|
||||
// Annotate each chore with done-state + handlers, preserving the classic path's logic.
|
||||
const rows = childChores.map((chore, i) => {
|
||||
const { done, completions } = this._isChoreDone(chore, child, todaysCompletions);
|
||||
const loading = !!this._loading[chore.id];
|
||||
// Blocked chores only reach here under dim/show — hide is filtered
|
||||
// upstream. Either way the tap must be refused (#793).
|
||||
const blocked = !!chore._isDependencyBlocked && !done;
|
||||
// Recurring chore still inside its cooldown. Excluded when done so the
|
||||
// undo chip stays live, exactly as on the classic row.
|
||||
const recLocked = !!chore._isRecurrenceLocked && !done;
|
||||
const onAct = () => {
|
||||
if (loading) return;
|
||||
if (loading || blocked || recLocked) return;
|
||||
if (done) this._handleUndo(chore, child, completions);
|
||||
else this._handleComplete(chore, child);
|
||||
};
|
||||
@@ -2642,10 +2673,12 @@ class TaskMateChildCard extends LitElement {
|
||||
const dimmed = !done && (
|
||||
(notDueToday && dueDaysMode === "dim") ||
|
||||
(chore._isTimeElapsed && elapsedTimeMode === "dim") ||
|
||||
(blocked && dependencyMode === "dim") ||
|
||||
(chore._isRecurrenceLocked && recurrenceDoneMode === "dim") ||
|
||||
chore._isLockedPreview === true
|
||||
);
|
||||
return {
|
||||
chore, child, done, loading, onAct, index: i, dimmed,
|
||||
chore, child, done, loading, onAct, index: i, dimmed, blocked, recLocked,
|
||||
tone: this._designTone(i),
|
||||
glyph: this._choreGlyph(chore),
|
||||
points: chore.effective_points ?? chore.points,
|
||||
@@ -2683,15 +2716,15 @@ class TaskMateChildCard extends LitElement {
|
||||
const body = this.config.pre_reader === true
|
||||
? html`
|
||||
<div class="pre-reader-grid">
|
||||
${childChores.map((chore, index) =>
|
||||
this._renderPreReaderTile(chore, child, todaysCompletions, index))}
|
||||
${childChores.map((chore) =>
|
||||
this._renderPreReaderTile(chore, child, pointsIcon, todaysCompletions))}
|
||||
</div>`
|
||||
: design === "playroom" ? this._designPlayroom(child, rows, remaining, tone) :
|
||||
design === "console" ? this._designConsole(child, rows, remaining, tone) :
|
||||
this._designCleanpro(child, rows, remaining, tone);
|
||||
|
||||
return html`<ha-card class="tmd" style="--hd:${hd}">
|
||||
${this._designHeaderFull(child, design, remaining, rows.length, tone, pendingPoints, pointsIcon)}
|
||||
${this._designHeaderFull(child, design, remaining, rows.length, tone, pendingPoints)}
|
||||
<div class="tmd-bd">
|
||||
${attrs.vacation_active ? html`
|
||||
<div class="tmd-vacation">
|
||||
@@ -2742,7 +2775,7 @@ class TaskMateChildCard extends LitElement {
|
||||
}
|
||||
|
||||
/** Designed header: avatar/title, remaining pill, pending-points chip. */
|
||||
_designHeaderFull(child, design, remaining, total, tone, pendingPoints, pointsIcon) {
|
||||
_designHeaderFull(child, design, remaining, total, tone, pendingPoints) {
|
||||
const title = design === "console"
|
||||
? `${(child.name || "").toUpperCase()} · ${this._t("child.todays_chores").toUpperCase()}`
|
||||
: this._t("points_display.single_title", { name: child.name });
|
||||
@@ -2775,11 +2808,21 @@ class TaskMateChildCard extends LitElement {
|
||||
/** Shared meta tags (mandatory / photo / description) for a designed chore row. */
|
||||
_designChoreMeta(r) {
|
||||
const showDesc = this.config.show_description === true && r.chore.description;
|
||||
// A blocked chore says what unlocks it here too — the classic row carries
|
||||
// the same label, and a dimmed row with no reason is just confusing (#793).
|
||||
const depNames = r.blocked ? (r.chore._dependencyNames || []) : [];
|
||||
// Same reason label the classic row carries, so a dimmed designed row is
|
||||
// not just mysteriously grey (#803).
|
||||
const recLabel = r.recLocked
|
||||
? (r.chore.recurrence ? r.chore.recurrence.replace(/_/g, " ") : this._t("child.recurring"))
|
||||
: "";
|
||||
return html`
|
||||
${r.mandatory || r.photo ? html`
|
||||
${r.mandatory || r.photo || depNames.length || recLabel ? html`
|
||||
<div class="tmd-meta">
|
||||
${r.mandatory ? html`<span class="tmd-tag mandatory">⚠ ${this._t("child.mandatory")}</span>` : ""}
|
||||
${r.photo ? html`<span class="tmd-tag photo">📷 ${this._t("child.photo_needed")}</span>` : ""}
|
||||
${depNames.length ? html`<span class="tmd-tag">🔒 ${this._t("child.blocked_by_dependency", { chores: depNames.join(", ") })}</span>` : ""}
|
||||
${recLabel ? html`<span class="tmd-tag">🕒 ${recLabel}</span>` : ""}
|
||||
</div>` : ""}
|
||||
${showDesc ? html`<div class="tmd-desc">${r.chore.description}</div>` : ""}`;
|
||||
}
|
||||
@@ -2795,7 +2838,7 @@ class TaskMateChildCard extends LitElement {
|
||||
|
||||
_designDoneBtn(r, label, cls) {
|
||||
return html`<button class="btn ${cls || ""}"
|
||||
?disabled=${r.loading}
|
||||
?disabled=${r.loading || r.blocked || r.recLocked}
|
||||
@click=${(e) => { e.stopPropagation(); r.onAct(); }}>${label}</button>`;
|
||||
}
|
||||
|
||||
@@ -2820,7 +2863,7 @@ class TaskMateChildCard extends LitElement {
|
||||
return this._renderBonusSubtasks(r.chore, r.child, r.pointsIcon, r.todaysCompletions);
|
||||
}
|
||||
|
||||
_designPlayroom(child, rows, remaining, tone) {
|
||||
_designPlayroom(child, rows, _remaining, _tone) {
|
||||
if (rows.length === 0) return html`<div class="tmd-empty">${this._t("child.all_done")}</div>`;
|
||||
return html`<div class="tmd-chores">
|
||||
${rows.map(r => r.timed ? this._designTimed(r) : html`
|
||||
@@ -2840,7 +2883,7 @@ class TaskMateChildCard extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_designConsole(child, rows, remaining, tone) {
|
||||
_designConsole(child, rows, _remaining, _tone) {
|
||||
if (rows.length === 0) return html`<div class="tmd-empty">${this._t("child.all_done")}</div>`;
|
||||
return html`<div class="grid">
|
||||
${rows.map(r => r.timed ? this._designTimed(r) : html`
|
||||
@@ -2862,7 +2905,7 @@ class TaskMateChildCard extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_designCleanpro(child, rows, remaining, tone) {
|
||||
_designCleanpro(child, rows, _remaining, _tone) {
|
||||
if (rows.length === 0) return html`<div class="tmd-empty">${this._t("child.all_done")}</div>`;
|
||||
return html`<div class="tmd-checklist">
|
||||
${rows.map(r => r.timed ? this._designTimed(r) : html`
|
||||
@@ -2951,11 +2994,8 @@ class TaskMateChildCard extends LitElement {
|
||||
|
||||
_filterAndSortChores(chores, child) {
|
||||
const childId = String(child.id || "");
|
||||
const childName = child.name;
|
||||
const choreOrder = child.chore_order || [];
|
||||
|
||||
// Debug logging to diagnose assignment filtering issues
|
||||
|
||||
// Get today's day of week from sensor (set by backend) or compute client-side
|
||||
const entity = this.hass?.states?.[this.config.entity];
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity?.attributes || {};
|
||||
@@ -2965,6 +3005,18 @@ class TaskMateChildCard extends LitElement {
|
||||
const dueDaysMode = this.config.due_days_mode || 'hide';
|
||||
const showDueDaysOnly = this.config.show_due_days_only !== false;
|
||||
const elapsedTimeMode = this.config.elapsed_time_mode || 'dim';
|
||||
// Hide by default: the backend simply reports a dependency-blocked chore
|
||||
// as unavailable, so hiding is what matches it (#793).
|
||||
const dependencyMode = this.config.dependency_mode || 'hide';
|
||||
const recurrenceDoneMode = this.config.recurrence_done_mode || 'dim';
|
||||
const allCompletionsToday = attrs.todays_completions || [];
|
||||
// Availability comes from the backend's own matrix (#803). The card cannot
|
||||
// work a recurrence window out for itself: last_completed is not exposed,
|
||||
// `recurrence` is omitted from the payload when it is the default weekly,
|
||||
// and the maths is calendar-month aware. A missing matrix (older backend,
|
||||
// or a sensor that hasn't published yet) has to read as available, or the
|
||||
// card would blank itself.
|
||||
const availability = attrs.chore_availability || {};
|
||||
|
||||
// First, filter chores for this child and time category
|
||||
const filteredChores = chores.filter(chore => {
|
||||
@@ -3112,6 +3164,42 @@ class TaskMateChildCard extends LitElement {
|
||||
chore._isDueToday = isDueToday;
|
||||
chore._hasDueDays = hasDueDays;
|
||||
|
||||
// Chore dependencies (#793). Mirrors is_chore_available_for_child():
|
||||
// a prerequisite counts as satisfied only when this same child has an
|
||||
// approved, non-bonus completion of it today. todays_completions is
|
||||
// already scoped to today, so the date check is implicit. A looser rule
|
||||
// here would unlock a chore the backend then refuses to complete.
|
||||
const dependsOn = Array.isArray(chore.depends_on) ? chore.depends_on : [];
|
||||
chore._isDependencyBlocked = dependsOn.length > 0 && !dependsOn.every(
|
||||
(depId) => allCompletionsToday.some(
|
||||
(comp) => comp.chore_id === depId
|
||||
&& String(comp.child_id) === childId
|
||||
&& comp.approved
|
||||
&& !comp.bonus_subtask_id
|
||||
)
|
||||
);
|
||||
chore._dependencyNames = chore._isDependencyBlocked
|
||||
? dependsOn
|
||||
.map((depId) => (chores.find((c) => c.id === depId) || {}).name)
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
if (chore._isDependencyBlocked && dependencyMode === 'hide') return false;
|
||||
|
||||
// Recurring chores in their cooldown window (#803). Both of these are
|
||||
// read by the render path, which until now found them undefined.
|
||||
chore._isRecurring = (chore.schedule_mode || 'specific_days') === 'recurring';
|
||||
const availableNow = (availability[chore.id] || {})[childId] !== false;
|
||||
// Completing a recurring chore makes it unavailable at once, so hiding on
|
||||
// availability alone would vanish it the instant it is ticked — no done
|
||||
// state, no way to undo. The mode is about the days *after* that.
|
||||
const completedToday = allCompletionsToday.some(
|
||||
(comp) => comp.chore_id === chore.id
|
||||
&& (String(comp.child_id) === childId || comp.child_id === '__parent__')
|
||||
&& !comp.bonus_subtask_id
|
||||
);
|
||||
chore._isRecurrenceLocked = chore._isRecurring && !availableNow && !completedToday;
|
||||
if (chore._isRecurrenceLocked && recurrenceDoneMode === 'hide') return false;
|
||||
|
||||
// Mark whether this chore's time period has elapsed (grace-aware).
|
||||
chore._isTimeElapsed = this._isTimePeriodElapsed(chore);
|
||||
|
||||
@@ -3503,8 +3591,12 @@ class TaskMateChildCard extends LitElement {
|
||||
* Pre-reader mode (#683): a picture-only tile for children who can't read
|
||||
* yet. No chore name, no numbers — a big icon, a row of stars for the
|
||||
* points, and a huge tick when it's done.
|
||||
*
|
||||
* `pre_reader_points` (#795) swaps the stars for the value itself, for
|
||||
* families whose children can already read a number and who found the
|
||||
* rounded star count contradicted the balance on the rest of the dashboard.
|
||||
*/
|
||||
_renderPreReaderTile(chore, child, todaysCompletions = [], choreIndex = 0) {
|
||||
_renderPreReaderTile(chore, child, pointsIcon, todaysCompletions = []) {
|
||||
const dailyLimit = chore.daily_limit || 1;
|
||||
// Counted the same way the standard row does: pending completions hold a
|
||||
// place too, and a parent completing on behalf lands under "__parent__".
|
||||
@@ -3515,11 +3607,15 @@ class TaskMateChildCard extends LitElement {
|
||||
);
|
||||
const isDone = childCompletionsToday.length >= dailyLimit;
|
||||
const isLoading = this._loading[chore.id];
|
||||
const available = chore._isAvailableForChild !== false && !chore._isLockedPreview;
|
||||
const available = chore._isAvailableForChild !== false && !chore._isLockedPreview
|
||||
&& !chore._isDependencyBlocked;
|
||||
|
||||
// Stars, not digits: a 4-year-old can count pictures, not read "+3".
|
||||
// The count is deliberately lossy — it rounds and clamps to 1..5 — which
|
||||
// is why showing the number instead is opt-in rather than a second row.
|
||||
const points = chore.effective_points ?? chore.points ?? 0;
|
||||
const stars = Math.max(1, Math.min(5, Math.round(points / 2) || 1));
|
||||
const showPoints = this.config.pre_reader_points === true;
|
||||
|
||||
const v = window.__taskmate_chore_visual(chore);
|
||||
const icon = (v.kind === "icon" ? v.icon : "")
|
||||
@@ -3542,9 +3638,15 @@ class TaskMateChildCard extends LitElement {
|
||||
: html`<ha-icon icon="${icon}"></ha-icon>`}
|
||||
</div>
|
||||
${isDone ? html`<div class="pre-tile-tick"><ha-icon icon="mdi:check-bold"></ha-icon></div>` : ''}
|
||||
<div class="pre-tile-stars">
|
||||
${Array.from({ length: stars }, () => html`<ha-icon icon="mdi:star"></ha-icon>`)}
|
||||
</div>
|
||||
${showPoints
|
||||
? html`
|
||||
<div class="pre-tile-points">
|
||||
<ha-icon icon="${pointsIcon}"></ha-icon>+${points}
|
||||
</div>`
|
||||
: html`
|
||||
<div class="pre-tile-stars">
|
||||
${Array.from({ length: stars }, () => html`<ha-icon icon="mdi:star"></ha-icon>`)}
|
||||
</div>`}
|
||||
${this.config.pre_reader_labels === true
|
||||
? html`<div class="pre-tile-label">${chore.name}</div>` : ''}
|
||||
</button>
|
||||
@@ -3592,8 +3694,6 @@ class TaskMateChildCard extends LitElement {
|
||||
// This is defensive - we'd rather show "completed" incorrectly than allow double-completions
|
||||
// The optimistic completion will be cleaned up once we verify the server state reflects it
|
||||
if (hasOptimisticCompletion) {
|
||||
// Calculate how many optimistic completions we've tracked that aren't yet in the data
|
||||
const optimisticCount = optimisticData.count || 1;
|
||||
|
||||
// Get timestamps from actual completions for this chore/child today
|
||||
const actualTimestamps = childCompletionsToday.map(comp =>
|
||||
@@ -3616,8 +3716,6 @@ class TaskMateChildCard extends LitElement {
|
||||
|
||||
const isCompletedForToday = completionsToday >= dailyLimit;
|
||||
|
||||
// Check if the most recent completion is pending approval
|
||||
const hasPendingCompletion = childCompletionsToday.some((comp) => !comp.approved) || hasOptimisticCompletion;
|
||||
|
||||
// Chore number (1-indexed for display) and color class (cycle through 8 colors)
|
||||
const choreNumber = choreIndex + 1;
|
||||
@@ -3626,11 +3724,21 @@ class TaskMateChildCard extends LitElement {
|
||||
// Click handler for the entire row
|
||||
const notDueToday = chore._hasDueDays && !chore._isDueToday && this.config.due_days_mode === 'dim';
|
||||
const recurrenceDoneMode = this.config.recurrence_done_mode || 'dim';
|
||||
const notAvailableRecurrence = chore._isRecurring && !chore._isAvailableForChild && recurrenceDoneMode === 'dim';
|
||||
// The tap is refused whenever the chore is locked: async_complete_chore
|
||||
// no-ops a recurring chore inside its window, so a clickable row under
|
||||
// `show` would just swallow the tap (#805). Only the dim STYLING is
|
||||
// mode-gated. Same split dependency_mode uses.
|
||||
const recurrenceLocked = !!chore._isRecurrenceLocked && !isCompletedForToday;
|
||||
const notAvailableRecurrence = recurrenceLocked && recurrenceDoneMode === 'dim';
|
||||
// Elapsed: only dim incomplete chores — completed ones keep their green "done" style
|
||||
const elapsedTimeMode = this.config.elapsed_time_mode || 'dim';
|
||||
const timeElapsed = chore._isTimeElapsed && !isCompletedForToday && elapsedTimeMode === 'dim';
|
||||
const isLockedPreview = !!chore._isLockedPreview && !isCompletedForToday;
|
||||
// Under dim/show the blocked chore is still on screen, so the tap has to be
|
||||
// refused here — the backend would reject the completion anyway (#793).
|
||||
// Only `dim` greys it out; `show` is meant to look untouched.
|
||||
const depBlocked = !!chore._isDependencyBlocked && !isCompletedForToday;
|
||||
const depDimmed = depBlocked && (this.config.dependency_mode || 'hide') === 'dim';
|
||||
const periodStartHour = isLockedPreview
|
||||
? (this._getPeriodHours(chore.time_category)?.[0] ?? null)
|
||||
: null;
|
||||
@@ -3640,8 +3748,9 @@ class TaskMateChildCard extends LitElement {
|
||||
const handleRowClick = () => {
|
||||
if (isLoading) return;
|
||||
if (notDueToday) return; // Dim mode — not interactive
|
||||
if (notAvailableRecurrence) return; // Recurrence window not open — not interactive
|
||||
if (recurrenceLocked) return; // Recurrence window not open — not interactive
|
||||
if (isLockedPreview) return; // Next-period preview — not yet claimable
|
||||
if (depBlocked) return; // Prerequisite chore not approved yet
|
||||
if (timeElapsed) return; // Time period passed — not interactive
|
||||
if (isCompletedForToday) {
|
||||
this._handleUndo(chore, child, childCompletionsToday);
|
||||
@@ -3649,7 +3758,7 @@ class TaskMateChildCard extends LitElement {
|
||||
this._handleComplete(chore, child);
|
||||
}
|
||||
};
|
||||
const isInteractive = !(isLoading || notDueToday || notAvailableRecurrence || isLockedPreview || timeElapsed);
|
||||
const isInteractive = !(isLoading || notDueToday || recurrenceLocked || isLockedPreview || depBlocked || timeElapsed);
|
||||
const handleRowKeyDown = (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
@@ -3659,6 +3768,8 @@ class TaskMateChildCard extends LitElement {
|
||||
|
||||
const titleText = isLockedPreview
|
||||
? (lockedUntilLabel || this._t('child.chore_locked_until_generic'))
|
||||
: depBlocked
|
||||
? this._t('child.blocked_by_dependency', { chores: (chore._dependencyNames || []).join(', ') })
|
||||
: notDueToday
|
||||
? this._t('child.not_scheduled_for_today')
|
||||
: timeElapsed
|
||||
@@ -3669,7 +3780,7 @@ class TaskMateChildCard extends LitElement {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="chore-card ${chore.mandatory ? "mandatory" : ""} ${isLoading ? "loading" : ""} ${isCelebrating ? "celebrating" : ""} ${isCompletedForToday ? "completed" : ""} ${notDueToday ? "not-due-today" : ""} ${notAvailableRecurrence ? "recurrence-unavailable" : ""} ${timeElapsed ? "time-elapsed" : ""} ${isLockedPreview ? "chore-locked" : ""}"
|
||||
class="chore-card ${chore.mandatory ? "mandatory" : ""} ${isLoading ? "loading" : ""} ${isCelebrating ? "celebrating" : ""} ${isCompletedForToday ? "completed" : ""} ${notDueToday ? "not-due-today" : ""} ${notAvailableRecurrence ? "recurrence-unavailable" : ""} ${timeElapsed ? "time-elapsed" : ""} ${isLockedPreview ? "chore-locked" : ""} ${depDimmed ? "dependency-blocked" : ""}"
|
||||
role="button"
|
||||
tabindex="${isInteractive ? '0' : '-1'}"
|
||||
aria-disabled="${isInteractive ? 'false' : 'true'}"
|
||||
@@ -3689,7 +3800,13 @@ class TaskMateChildCard extends LitElement {
|
||||
${this.config.show_description && chore.description ? html`
|
||||
<div class="chore-description">${chore.description}</div>
|
||||
` : ''}
|
||||
${chore._isRecurring && !chore._isAvailableForChild ? html`
|
||||
${depBlocked && (chore._dependencyNames || []).length ? html`
|
||||
<div class="dependency-label">
|
||||
<ha-icon icon="mdi:lock-outline"></ha-icon>
|
||||
${this._t('child.blocked_by_dependency', { chores: chore._dependencyNames.join(', ') })}
|
||||
</div>
|
||||
` : ''}
|
||||
${chore._isRecurrenceLocked ? html`
|
||||
<div class="recurrence-label">
|
||||
<ha-icon icon="mdi:clock-outline"></ha-icon>
|
||||
${chore.recurrence ? chore.recurrence.replace(/_/g, ' ') : this._t('child.recurring')}
|
||||
@@ -3728,6 +3845,9 @@ class TaskMateChildCard extends LitElement {
|
||||
|
||||
const colorClass = `color-${choreIndex % 8}`;
|
||||
const choreNumber = choreIndex + 1;
|
||||
// A blocked timed chore must not be startable under dim/show (#793).
|
||||
// Pause/stop stay live so a session already running can still be ended.
|
||||
const depBlocked = !!chore._isDependencyBlocked;
|
||||
|
||||
// Calculate elapsed seconds
|
||||
let elapsed = session ? (session.total_seconds_today || 0) : 0;
|
||||
@@ -3759,6 +3879,7 @@ class TaskMateChildCard extends LitElement {
|
||||
const nearCap = maxMin > 0 && capPct >= 85;
|
||||
|
||||
const handleStart = (e) => {
|
||||
if (depBlocked) { e.stopPropagation(); return; }
|
||||
e.stopPropagation();
|
||||
if (isLoading) return;
|
||||
this._loading = { ...this._loading, [`timed_${chore.id}`]: true };
|
||||
@@ -3829,7 +3950,7 @@ class TaskMateChildCard extends LitElement {
|
||||
|
||||
<div class="timer-actions">
|
||||
${state === 'idle' ? html`
|
||||
<button class="timer-btn start" @click="${handleStart}" ?disabled="${isLoading}">
|
||||
<button class="timer-btn start" @click="${handleStart}" ?disabled="${isLoading || depBlocked}">
|
||||
<span class="btn-icon">▶</span>
|
||||
${this._t('child.start') || 'START'}
|
||||
</button>
|
||||
@@ -3845,7 +3966,7 @@ class TaskMateChildCard extends LitElement {
|
||||
</button>
|
||||
` : ''}
|
||||
${state === 'paused' ? html`
|
||||
<button class="timer-btn resume" @click="${handleStart}" ?disabled="${isLoading}">
|
||||
<button class="timer-btn resume" @click="${handleStart}" ?disabled="${isLoading || depBlocked}">
|
||||
<span class="btn-icon">▶</span>
|
||||
${this._t('child.resume') || 'RESUME'}
|
||||
</button>
|
||||
@@ -4708,6 +4829,19 @@ class TaskMateChildCardEditor extends LitElement {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dependency_mode',
|
||||
selector: {
|
||||
select: {
|
||||
options: [
|
||||
{ value: 'hide', label: this._t('child.editor.dependency_hide') },
|
||||
{ value: 'dim', label: this._t('child.editor.dependency_dim') },
|
||||
{ value: 'show', label: this._t('child.editor.dependency_show') },
|
||||
],
|
||||
mode: 'dropdown',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'elapsed_time_mode',
|
||||
selector: {
|
||||
@@ -4725,6 +4859,7 @@ class TaskMateChildCardEditor extends LitElement {
|
||||
{ name: 'show_description', selector: { boolean: {} } },
|
||||
{ name: 'pre_reader', selector: { boolean: {} } },
|
||||
{ name: 'pre_reader_labels', selector: { boolean: {} } },
|
||||
{ name: 'pre_reader_points', selector: { boolean: {} } },
|
||||
{ name: 'debug', selector: { boolean: {} } },
|
||||
];
|
||||
}
|
||||
@@ -4737,11 +4872,13 @@ class TaskMateChildCardEditor extends LitElement {
|
||||
card_design: this._t('common.design.field_label'),
|
||||
due_days_mode: this._t('child.editor.chores_not_due_today'),
|
||||
recurrence_done_mode: this._t('child.editor.recurring_when_completed'),
|
||||
dependency_mode: this._t('child.editor.dependency_mode'),
|
||||
elapsed_time_mode: this._t('child.editor.missed_time_chores'),
|
||||
show_countdown: this._t('child.editor.show_countdown'),
|
||||
show_description: this._t('child.editor.show_description'),
|
||||
pre_reader: this._t('child.editor.pre_reader'),
|
||||
pre_reader_labels: this._t('child.editor.pre_reader_labels'),
|
||||
pre_reader_points: this._t('child.editor.pre_reader_points'),
|
||||
debug: this._t('child.editor.show_debug'),
|
||||
};
|
||||
return labels[entry.name] ?? entry.name;
|
||||
@@ -4754,6 +4891,7 @@ class TaskMateChildCardEditor extends LitElement {
|
||||
time_category: this._t('child.editor.time_category_helper'),
|
||||
due_days_mode: this._t('child.editor.due_days_helper'),
|
||||
recurrence_done_mode: this._t('child.editor.recurrence_helper'),
|
||||
dependency_mode: this._t('child.editor.dependency_helper'),
|
||||
elapsed_time_mode: this._t('child.editor.elapsed_helper'),
|
||||
};
|
||||
return helpers[entry.name] ?? '';
|
||||
@@ -4769,11 +4907,13 @@ class TaskMateChildCardEditor extends LitElement {
|
||||
card_design: this.config.card_design || 'global',
|
||||
due_days_mode: this.config.due_days_mode || 'hide',
|
||||
recurrence_done_mode: this.config.recurrence_done_mode || 'dim',
|
||||
dependency_mode: this.config.dependency_mode || 'hide',
|
||||
elapsed_time_mode: this.config.elapsed_time_mode || 'dim',
|
||||
show_countdown: this.config.show_countdown !== false,
|
||||
show_description: this.config.show_description === true,
|
||||
pre_reader: this.config.pre_reader === true,
|
||||
pre_reader_labels: this.config.pre_reader_labels === true,
|
||||
pre_reader_points: this.config.pre_reader_points === true,
|
||||
debug: this.config.debug === true,
|
||||
};
|
||||
|
||||
|
||||
@@ -276,7 +276,6 @@ 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 || [];
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
const pointsName = attrs.points_name || this._t('common.points');
|
||||
const days = Math.max(3, Math.min(90, this.config.days || 14));
|
||||
|
||||
|
||||
@@ -919,7 +919,6 @@ export function createIncentiveCard(P) {
|
||||
|
||||
_renderDesignedRow(b, design) {
|
||||
const isEditing = this._editingBonus?.id === b.id;
|
||||
const tone = this._designTone(0);
|
||||
const accent = P.designAccent;
|
||||
const pv = P.dpts(b.points);
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ class TaskMateLeaderboardCard extends LitElement {
|
||||
|
||||
// Solo mode
|
||||
if (children.length === 1) {
|
||||
return this._renderSolo(sorted[0], weeklyPoints, pointsIcon, pointsName, periodLabel);
|
||||
return this._renderSolo(sorted[0], weeklyPoints, pointsIcon, pointsName);
|
||||
}
|
||||
|
||||
return html`
|
||||
@@ -464,8 +464,7 @@ class TaskMateLeaderboardCard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
_renderSolo(child, weeklyPoints, pointsIcon, pointsName, periodLabel) {
|
||||
const entity = this.hass.states[this.config.entity];
|
||||
_renderSolo(child, weeklyPoints, pointsIcon, pointsName) {
|
||||
const totalChores = child.total_chores_completed || 0;
|
||||
const bestStreak = child.best_streak || 0;
|
||||
const weekly = weeklyPoints[child.id] || 0;
|
||||
|
||||
@@ -631,7 +631,7 @@ class TaskMateOverviewCard extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_ovKid(design, k, isParent, pointsIcon) {
|
||||
_ovKid(design, k, isParent, _pointsIcon) {
|
||||
const cls = design === "console" ? "cn" : design === "cleanpro" ? "cp" : "";
|
||||
const nameUpper = design === "console" ? `${k.child.name.toUpperCase()} · ${k.pct}%` : k.child.name;
|
||||
const isOpen = !!this._expanded[k.child.id];
|
||||
@@ -652,7 +652,7 @@ class TaskMateOverviewCard extends LitElement {
|
||||
}
|
||||
|
||||
/** Admin "complete on behalf" panel — same handler as the classic tile. */
|
||||
_ovBehalf(k, pointsIcon) {
|
||||
_ovBehalf(k, _pointsIcon) {
|
||||
return html`
|
||||
<div class="ov-behalf" @click="${(e) => e.stopPropagation()}">
|
||||
<div class="ov-behalf-hdr">${this._t('common.complete_on_behalf_heading')} · ${k.child.name}</div>
|
||||
@@ -693,7 +693,7 @@ class TaskMateOverviewCard extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_renderChildTile(child, chores, completions, pointsIcon, pointsName) {
|
||||
_renderChildTile(child, chores, completions, pointsIcon, _pointsName) {
|
||||
// Avatar now included directly in children array from the overview sensor
|
||||
const avatar = child.avatar || "mdi:account-circle";
|
||||
|
||||
|
||||
@@ -18,24 +18,6 @@ const PANEL_VERSION = (() => {
|
||||
return el ? sanitize(new URLSearchParams(el.src.split("?")[1] || "").get("v")) : "?";
|
||||
})();
|
||||
|
||||
const TABS = [
|
||||
{ id: "children", lk: "panel.tab_children" },
|
||||
{ id: "activity", lk: "panel.tab_activity" },
|
||||
{ id: "insights", lk: "panel.tab_insights" },
|
||||
{ id: "chores", lk: "panel.tab_chores" },
|
||||
{ id: "rewards", lk: "panel.tab_rewards" },
|
||||
{ id: "penalties", lk: "panel.tab_penalties" },
|
||||
{ id: "bonuses", lk: "panel.tab_bonuses" },
|
||||
{ id: "groups", lk: "panel.tab_groups" },
|
||||
{ id: "quests", lk: "panel.tab_quests" },
|
||||
{ id: "challenges", lk: "panel.tab_challenges" },
|
||||
{ id: "badges", lk: "panel.tab_badges" },
|
||||
{ id: "templates", lk: "panel.tab_templates" },
|
||||
{ id: "notifications", lk: "panel.tab_notifications" },
|
||||
{ id: "audit", lk: "panel.tab_audit" },
|
||||
{ id: "settings", lk: "panel.tab_settings", label: "⚙" },
|
||||
];
|
||||
|
||||
const BADGE_METRICS = [
|
||||
{ v: "total_points", lk: "badge.metric_total_points" },
|
||||
{ v: "total_chores", lk: "badge.metric_total_chores" },
|
||||
@@ -1181,12 +1163,29 @@ class TaskMatePanel extends HTMLElement {
|
||||
async _doApproveAll() {
|
||||
const pending = this._state?.pending_completions || [];
|
||||
if (pending.length === 0) return;
|
||||
if (this._approvingAll) return;
|
||||
if (!confirm(this._t("panel.activity_approve_all_confirm", { count: pending.length }))) return;
|
||||
const { ok, err } = await this._callWS({
|
||||
type: "taskmate/approve_all_chores",
|
||||
completion_ids: pending.map(c => c.id),
|
||||
});
|
||||
if (!ok) { this._showToast("err", this._t("panel.toast_approve_failed", { error: err })); return; }
|
||||
// Acknowledge the tap straight away (#799). The batch is fast since #794,
|
||||
// but not instant, and a button that looks untouched is indistinguishable
|
||||
// from one that did nothing — which is how #794 came to be reported.
|
||||
this._approvingAll = true;
|
||||
this._render();
|
||||
let result;
|
||||
try {
|
||||
result = await this._callWS({
|
||||
type: "taskmate/approve_all_chores",
|
||||
completion_ids: pending.map(c => c.id),
|
||||
});
|
||||
} finally {
|
||||
// Cleared the moment the call settles, whatever it did — a button stuck
|
||||
// spinning is worse than no busy state at all.
|
||||
this._approvingAll = false;
|
||||
}
|
||||
if (!result.ok) {
|
||||
this._showToast("err", this._t("panel.toast_approve_failed", { error: result.err }));
|
||||
this._render();
|
||||
return;
|
||||
}
|
||||
await this._fetchState();
|
||||
this._showToast("ok", this._t("panel.activity_approve_all_done", { count: pending.length }));
|
||||
}
|
||||
@@ -1531,6 +1530,10 @@ class TaskMatePanel extends HTMLElement {
|
||||
image_url: d.image_url || "",
|
||||
points: Number(d.points) || 0,
|
||||
assigned_to: d.assigned_to || [],
|
||||
// The dialog tracks this via toggle-depends, but it was never sent, so a
|
||||
// dependency could neither be saved nor cleared — _ws_update_chore only
|
||||
// applies fields present in the message (#793).
|
||||
depends_on: d.depends_on || [],
|
||||
requires_approval: !!d.requires_approval,
|
||||
time_category: d.time_category || "anytime",
|
||||
claim_allowance_minutes: Math.max(0, Number(d.claim_allowance_minutes) || 0),
|
||||
@@ -3120,7 +3123,9 @@ class TaskMatePanel extends HTMLElement {
|
||||
? `<span class="tm-pill tm-pill-warn">${pendingCompletions.length + pendingClaims.length + pendingSwaps.length}</span>`
|
||||
: `<span class="tm-pill tm-pill-success">${this._t("panel.activity_pending_all_clear")}</span>`}
|
||||
${pendingCompletions.length > 0
|
||||
? `<button type="button" class="tm-btn tm-btn-raised tm-btn-sm" data-act="approve-all-chores" style="margin-left:auto">${this._t("panel.activity_approve_all")}</button>`
|
||||
? `<button type="button" class="tm-btn tm-btn-raised tm-btn-sm" data-act="approve-all-chores" style="margin-left:auto"${this._approvingAll ? " disabled" : ""}>${this._approvingAll
|
||||
? `<span class="tm-btn-spinner"></span>${this._t("panel.activity_approve_all_busy")}`
|
||||
: this._t("panel.activity_approve_all")}</button>`
|
||||
: ""}
|
||||
</h3>
|
||||
${pendingCompletions.length === 0 && pendingClaims.length === 0 && pendingSwaps.length === 0 ? `
|
||||
@@ -3434,7 +3439,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
const assigned = (q.assigned_to && q.assigned_to.length)
|
||||
? children.filter(c => q.assigned_to.includes(c.id)).map(c => c.name)
|
||||
: [this._t("panel.quest_all_children")];
|
||||
const stepNames = steps.map((sid, i) => {
|
||||
const stepNames = steps.map((sid) => {
|
||||
const name = choreById[sid] ? choreById[sid].name : this._t("panel.quest_missing_chore");
|
||||
return `<li>${this._esc(name)}</li>`;
|
||||
}).join("");
|
||||
@@ -6554,6 +6559,24 @@ class TaskMatePanel extends HTMLElement {
|
||||
background: var(--tm-accent-soft);
|
||||
border-color: var(--tm-accent);
|
||||
}
|
||||
.tm-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
@keyframes tm-spin { to { transform: rotate(360deg); } }
|
||||
.tm-btn-spinner {
|
||||
width: 12px; height: 12px;
|
||||
border: 2px solid currentColor;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: tm-spin 0.7s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
/* Still reads as busy — the ring just doesn't rotate. */
|
||||
.tm-btn-spinner { animation: none; opacity: 0.7; }
|
||||
}
|
||||
.tm-btn-raised {
|
||||
background: var(--tm-accent);
|
||||
color: var(--text-primary-color, #fff);
|
||||
|
||||
@@ -755,7 +755,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
|
||||
_dEmpty(msg) { return html`<div class="tmd-empty">${msg}</div>`; }
|
||||
|
||||
_dOverview(design, children, chores, completions, attrs, pointsIcon) {
|
||||
_dOverview(design, children, chores, completions, attrs, _pointsIcon) {
|
||||
if (!children.length) return this._dEmpty(this._t('dashboard.empty_no_children'));
|
||||
return html`${children.map((child, i) => {
|
||||
const tone = this._designTone(i);
|
||||
@@ -805,7 +805,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
})}`;
|
||||
}
|
||||
|
||||
_dApprovals(design, pending, children, chores, pointsIcon) {
|
||||
_dApprovals(design, pending, children, chores, _pointsIcon) {
|
||||
if (!pending.length) return this._dEmpty(this._t('dashboard.empty_approvals'));
|
||||
const childMap = {}; children.forEach(c => { childMap[c.id] = c; });
|
||||
const choreMap = {}; chores.forEach(c => { choreMap[c.id] = c; });
|
||||
@@ -841,7 +841,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
})}`;
|
||||
}
|
||||
|
||||
_dClaims(design, claims, pointsIcon) {
|
||||
_dClaims(design, claims, _pointsIcon) {
|
||||
if (!claims.length) return this._dEmpty(this._t('dashboard.empty_claims'));
|
||||
return html`${claims.map((claim, i) => {
|
||||
const tone = this._designTone(i);
|
||||
@@ -924,7 +924,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
})}`;
|
||||
}
|
||||
|
||||
_renderOverview(children, chores, completions, pointsIcon, pointsName) {
|
||||
_renderOverview(children, chores, completions, pointsIcon, _pointsName) {
|
||||
if (!children.length) return html`<div class="empty-section"><ha-icon icon="mdi:account-group"></ha-icon><span>${this._t('dashboard.empty_no_children')}</span></div>`;
|
||||
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config?.entity)) || {};
|
||||
|
||||
@@ -984,7 +984,7 @@ class TaskMatePointsCard extends LitElement {
|
||||
}
|
||||
|
||||
_renderDialog() {
|
||||
const { child, action, pointsIcon, pointsName } = this._dialog;
|
||||
const { child, action, pointsName } = this._dialog;
|
||||
const isAdd = action === "add";
|
||||
const isLoading = this._loading[`dialog_${child.id}`];
|
||||
|
||||
|
||||
@@ -856,7 +856,6 @@ class TaskMateReorderCard extends LitElement {
|
||||
|
||||
const chores = attrs.chores || [];
|
||||
const childChores = this._getChoresForChild(chores, child.id);
|
||||
const pointsIcon = attrs.points_icon || "mdi:star";
|
||||
|
||||
const header = html`
|
||||
<div class="tmd-hd" style="--ac:var(--tmd-c1)">
|
||||
@@ -913,7 +912,7 @@ class TaskMateReorderCard extends LitElement {
|
||||
</div>
|
||||
<div class="d-list">
|
||||
${allCategoryChores.map((chore, index) =>
|
||||
this._renderDesignedChoreItem(chore, index, allCategoryChores.length, category, pointsIcon)
|
||||
this._renderDesignedChoreItem(chore, index, allCategoryChores.length, category)
|
||||
)}
|
||||
</div>`;
|
||||
})}
|
||||
@@ -922,7 +921,7 @@ class TaskMateReorderCard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
_renderDesignedChoreItem(chore, index, total, category, pointsIcon) {
|
||||
_renderDesignedChoreItem(chore, index, total, category) {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === total - 1;
|
||||
const v = window.__taskmate_chore_visual(chore);
|
||||
|
||||
@@ -699,7 +699,7 @@ class TaskMateRewardProgressCard extends LitElement {
|
||||
|
||||
_renderDesigned(design) {
|
||||
const hd = _safeColor(this.config.header_color, '#00897b');
|
||||
const wrap = (body, icon) => html`
|
||||
const wrap = (body) => html`
|
||||
<ha-card class="tmd" style="--hd:${hd}">
|
||||
<div class="tmd-hd">
|
||||
<span class="ic">🎯</span>
|
||||
|
||||
@@ -1147,8 +1147,8 @@ class TaskMateRewardsCard extends LitElement {
|
||||
if (a.is_jackpot && !b.is_jackpot) return -1;
|
||||
if (!a.is_jackpot && b.is_jackpot) return 1;
|
||||
// Within same type, sort by calculated cost ascending
|
||||
const aCost = this._getDisplayCost(a, children);
|
||||
const bCost = this._getDisplayCost(b, children);
|
||||
const aCost = this._getDisplayCost(a);
|
||||
const bCost = this._getDisplayCost(b);
|
||||
return aCost - bCost;
|
||||
});
|
||||
|
||||
@@ -1304,7 +1304,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
// pool, so jackpots never show the single-child claim button.
|
||||
const enablePoolMode = cardForcesPool || reward.pool_enabled === true || isJackpot;
|
||||
// All costs are static — just use reward.cost
|
||||
const displayCost = this._getDisplayCost(reward, children);
|
||||
const displayCost = this._getDisplayCost(reward);
|
||||
|
||||
// Get relevant children for this reward
|
||||
const relevantChildren = assignedTo.length === 0
|
||||
@@ -1329,7 +1329,6 @@ class TaskMateRewardsCard extends LitElement {
|
||||
const sharePerChild = relevantChildren.length > 0
|
||||
? Math.round(displayCost / relevantChildren.length)
|
||||
: displayCost;
|
||||
|
||||
relevantChildren.forEach((child, index) => {
|
||||
// In pool mode, show allocated points only; in wallet mode show wallet points
|
||||
const points = enablePoolMode
|
||||
@@ -1703,7 +1702,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
* For non-jackpot rewards, each child has their own calculated cost.
|
||||
* For jackpot rewards, all children share the same cost.
|
||||
*/
|
||||
_getDisplayCost(reward, children) {
|
||||
_getDisplayCost(reward) {
|
||||
// All reward costs are static — just return reward.cost
|
||||
// calculated_costs is still provided by the sensor for jackpot display purposes
|
||||
const calculatedCosts = reward.calculated_costs || {};
|
||||
@@ -1740,9 +1739,6 @@ class TaskMateRewardsCard extends LitElement {
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate total percentage for display
|
||||
const totalPercentage = cost > 0 ? Math.min((totalStars / cost) * 100, 100) : 0;
|
||||
|
||||
return html`
|
||||
<div class="progress-section">
|
||||
<div class="progress-bar-container">
|
||||
@@ -1803,7 +1799,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
// shared jar and it's redeemed when full. Combined points only work via the
|
||||
// pool, so jackpots never show the single-child claim button.
|
||||
const enablePoolMode = cardForcesPool || reward.pool_enabled === true || isJackpot;
|
||||
const displayCost = this._getDisplayCost(reward, children);
|
||||
const displayCost = this._getDisplayCost(reward);
|
||||
|
||||
const relevantChildren = assignedTo.length === 0
|
||||
? children
|
||||
@@ -1821,9 +1817,6 @@ class TaskMateRewardsCard extends LitElement {
|
||||
const childContributions = [];
|
||||
|
||||
if (isJackpot) {
|
||||
const sharePerChild = relevantChildren.length > 0
|
||||
? Math.round(displayCost / relevantChildren.length)
|
||||
: displayCost;
|
||||
relevantChildren.forEach((child, index) => {
|
||||
const points = enablePoolMode
|
||||
? (poolAllocations[child.id] || 0)
|
||||
@@ -1906,7 +1899,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
|
||||
if (!entity) {
|
||||
return html`<ha-card class="tmd" style="--hd:${hd}">
|
||||
${this._designHeader(0, hd)}
|
||||
${this._designHeader(0)}
|
||||
<div class="tmd-bd"><div class="tmd-empty">${this._t('common.entity_not_found', { entity: this.config.entity })}</div></div>
|
||||
</ha-card>`;
|
||||
}
|
||||
@@ -1928,7 +1921,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
const sortedRewards = [...rewards].sort((a, b) => {
|
||||
if (a.is_jackpot && !b.is_jackpot) return -1;
|
||||
if (!a.is_jackpot && b.is_jackpot) return 1;
|
||||
return this._getDisplayCost(a, children) - this._getDisplayCost(b, children);
|
||||
return this._getDisplayCost(a) - this._getDisplayCost(b);
|
||||
});
|
||||
|
||||
const cardForcesPool = this.config.enable_pool_mode === true;
|
||||
@@ -1936,7 +1929,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
const activeChild = activeChildId ? children.find((c) => c.id === activeChildId) : null;
|
||||
const anyPoolReward = sortedRewards.some((r) => cardForcesPool || r.pool_enabled === true || r.is_jackpot);
|
||||
|
||||
const header = this._designHeader(rewards.length, hd);
|
||||
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>`
|
||||
@@ -1958,7 +1951,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
_designHeader(count, hd) {
|
||||
_designHeader(count) {
|
||||
return html`
|
||||
<div class="tmd-hd">
|
||||
<span class="ic">🎁</span>
|
||||
@@ -1969,7 +1962,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_designChildSelector(child, pointsIcon, pointsName, design) {
|
||||
_designChildSelector(child, _pointsIcon, _pointsName, _design) {
|
||||
const spendable = typeof child.spendable_balance === 'number'
|
||||
? child.spendable_balance : (child.points || 0);
|
||||
return html`
|
||||
@@ -1983,7 +1976,7 @@ class TaskMateRewardsCard extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_designRewardRow(reward, children, pointsIcon, pointsName, design) {
|
||||
_designRewardRow(reward, children, pointsIcon, pointsName, _design) {
|
||||
const d = this._rewardData(reward, children);
|
||||
const tone = this._designTone((reward.id || reward.name || '').length);
|
||||
const isLoading = this._loading[reward.id];
|
||||
|
||||
@@ -322,7 +322,7 @@ class TaskMateStreakCard extends LitElement {
|
||||
// Use backend-calculated streak if available, fall back to client calculation
|
||||
const streak = child.current_streak !== undefined
|
||||
? child.current_streak
|
||||
: this._calculateStreak(childCompletions, chores, child.id);
|
||||
: this._calculateStreak(childCompletions);
|
||||
const daysShown = this.config.streak_days_shown || 14;
|
||||
const dayDots = this._buildDayDots(childCompletions, chores, child.id, daysShown);
|
||||
const achievements = this._getAchievements(child, childCompletions, streak, entity_ref);
|
||||
@@ -370,7 +370,7 @@ class TaskMateStreakCard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
_calculateStreak(completions, chores, childId) {
|
||||
_calculateStreak(completions) {
|
||||
const tz = this.hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// Build set of unique days with at least one completion
|
||||
@@ -492,7 +492,7 @@ class TaskMateStreakCard extends LitElement {
|
||||
const childCompletions = completions.filter(c => c.child_id === child.id);
|
||||
const streak = child.current_streak !== undefined
|
||||
? child.current_streak
|
||||
: this._calculateStreak(childCompletions, chores, child.id);
|
||||
: this._calculateStreak(childCompletions);
|
||||
const daysShown = this.config.streak_days_shown || 14;
|
||||
const dayDots = this._buildDayDots(childCompletions, chores, child.id, daysShown);
|
||||
const achievements = this._getAchievements(child, childCompletions, streak, entity_ref);
|
||||
@@ -549,10 +549,6 @@ class TaskMateStreakCard extends LitElement {
|
||||
return html`<ha-card class="tmd" style="--hd:${hd}">${header}<div class="tmd-bd">${body}</div></ha-card>`;
|
||||
}
|
||||
|
||||
_earnedBadges(achievements) {
|
||||
return achievements.filter(a => a.earned);
|
||||
}
|
||||
|
||||
// Classic shows earned badges PLUS the next locked milestone. _getAchievements
|
||||
// already returns exactly that set, so the designed badge rows render it whole,
|
||||
// dimming the locked entry to match the classic .badge.locked treatment.
|
||||
|
||||
@@ -401,7 +401,7 @@ class TaskMateWeeklyCard extends LitElement {
|
||||
|
||||
// Max completions in a day for chart scale
|
||||
const maxPerDay = Math.max(1, ...weekDays.map(d => (approvedCompletionsByDay[d.key] || []).length));
|
||||
const weekLabel = this._getWeekLabel(weekDays, tz);
|
||||
const weekLabel = this._getWeekLabel(weekDays);
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
@@ -596,7 +596,7 @@ class TaskMateWeeklyCard extends LitElement {
|
||||
}
|
||||
|
||||
const d = this._weekData(entity, tz);
|
||||
const weekLabel = this._getWeekLabel(d.weekDays, tz);
|
||||
const weekLabel = this._getWeekLabel(d.weekDays);
|
||||
const title = this.config.title || this._t("weekly.default_title");
|
||||
|
||||
const icon = design === "playroom" ? "📅" : design === "console" ? "▦" : "▤";
|
||||
@@ -718,7 +718,7 @@ class TaskMateWeeklyCard extends LitElement {
|
||||
return days;
|
||||
}
|
||||
|
||||
_getWeekLabel(weekDays, tz) {
|
||||
_getWeekLabel(weekDays) {
|
||||
const first = weekDays[0].date;
|
||||
const last = weekDays[6].date;
|
||||
const fmt = { month: "short", day: "numeric" };
|
||||
|
||||
Reference in New Issue
Block a user