291 files

This commit is contained in:
Home Assistant Version Control
2026-10-14 18:10:18 +00:00
parent c4e0178d8a
commit 280501d2e8
291 changed files with 32540 additions and 3280 deletions
+71 -10
View File
@@ -962,7 +962,7 @@
"wizard.next_welcome": "Kom i gang",
"wizard.next_children": "Næste: pligter",
"wizard.next_chores": "Næste: belønninger",
"wizard.next_rewards": "Næste: gennemse",
"wizard.next_rewards": "Næste: dashboards",
"wizard.create": "Opret det hele",
"wizard.create_existing": "Tilføj det hele",
"wizard.creating": "Opretter…",
@@ -1005,7 +1005,7 @@
"wizard.ready_for_chores": "Klar til pligter",
"wizard.preview_chores": "Pligterne vises her",
"wizard.preview_rewards": "og belønninger at spare op til",
"wizard.preview_hint": "Farven bruges på barnets kort i dashboardet \"Familiens pligter\". Skift avatar eller tilføj et foto senere under Børn.",
"wizard.preview_hint": "Farven bruges på barnets kort i dashboardet \"Børnenes pligter\". Skift avatar eller tilføj et foto senere under Børn.",
"wizard.age_n": "{age} år",
"wizard.group_chores": "Pligter for {group}",
"wizard.toast_child_exists": "{name} er allerede på listen",
@@ -1074,27 +1074,86 @@
"wizard.appr_none": "Ingen",
"wizard.appr_big": "Store (4+)",
"wizard.appr_all": "Alle",
"wizard.dash_toggle": "Opret et dashboard \"Familiens pligter\"",
"wizard.dash_hint": "Ét børnekort pr. barn plus belønningskortet. Springes over, hvis dashboardet allerede findes.",
"wizard.step_dashboards": "Dashboards",
"wizard.next_dashboards": "Næste: gennemse",
"wizard.dash_step_title": "Vælg dine dashboards",
"wizard.dash_step_copy": "Færdige dashboards til alle i huset, med dine børn allerede udfyldt. Vælg nogle, alle eller ingen.",
"wizard.dash_step_later": "Du kan altid lave flere under Indstillinger → Færdige dashboards. Et eksisterende dashboard bliver aldrig overskrevet.",
"wizard.count_dashboards_one": "1 dashboard",
"wizard.count_dashboards_many": "{count} dashboards",
"wizard.dash_existed": "\"{title}\" findes allerede, så det blev ladt være.",
"wizard.dash_failed": "\"{title}\" kunne ikke oprettes. Alt andet blev.",
"wizard.open_named": "Åbn {title}",
"wizard.review_dashboards": "Dashboards",
"wizard.review_no_dashboards": "Ingen",
"wizard.review_dashboards_only": "Intet nyt til familien denne gang, kun de dashboards, du har valgt.",
"panel.dash_section_title": "Færdige dashboards",
"panel.dash_section_hint": "Start med et dashboard lavet til hver person i huset, med dine børn udfyldt. Bagefter er det et helt almindeligt Home Assistant-dashboard: redigér, flyt rundt på eller slet det, som du vil. Et eksisterende dashboard bliver aldrig overskrevet.",
"panel.dash_kind_kids": "Børn",
"panel.dash_kind_parents": "Forældre",
"panel.dash_kind_wall": "Vægtablet",
"panel.dash_desc_kids": "En fane til hvert barn med deres pligter, belønningsmål, stime, mærker og belønninger, plus en familiefane med rangliste og familiemål.",
"panel.dash_desc_parents": "Først godkendelser og pligttavlen, så point, bonusser og straffe, så ugeoversigter og aktivitet.",
"panel.dash_desc_wall": "Kioskkortet i fuld skærm til en fælles tablet, med pligttavlen som anden fane.",
"panel.dash_for_kids": "Til børnenes tablet eller telefoner",
"panel.dash_for_parents": "Til forældrenes telefoner",
"panel.dash_for_wall": "Til en tablet i køkkenet eller entréen",
"panel.dash_created": "Oprettet",
"panel.dash_open": "Åbn",
"panel.dash_create_btn": "Opret …",
"panel.dash_dialog_title": "Opret et dashboard",
"panel.dash_title_label": "Titel",
"panel.dash_path_label": "Adresse",
"panel.dash_path_hint": "Små bogstaver, tal og bindestreger, med mindst én bindestreg.",
"panel.dash_path_taken": "Allerede i brug: {paths}. Vælg en anden adresse.",
"panel.dash_path_invalid": "Adressen må kun have små bogstaver, tal og bindestreger, med mindst én bindestreg.",
"panel.dash_list_failed": "Kunne ikke læse dine eksisterende dashboards, så intet blev oprettet.",
"panel.dash_children_label": "Børn",
"panel.dash_layout_label": "Layout",
"panel.dash_layout_tabs": "Ét dashboard, en fane til hvert barn",
"panel.dash_layout_each": "Et dashboard til hvert barn",
"panel.dash_layout_tabs_hint": "Godt til en fælles tablet: hvert barn er kun en fane væk.",
"panel.dash_layout_each_hint": "Godt til barnets egen telefon: hvert barn får sit eget dashboard på denne adresse efterfulgt af sit navn.",
"panel.dash_hide_parent": "Skjul forældreknapper",
"panel.dash_hide_parent_hint": "Godkendelse og udførelse for et barn holdes slået fra på disse kort, også når en forælder kigger med.",
"panel.dash_admin": "Kun administratorer",
"panel.dash_admin_hint": "Skjuler det for brugere, der ikke er Home Assistant-administratorer. Lad det være slået fra, hvis en forælder ikke er administrator; kortene tjekker alligevel forældrerettigheder før hver handling.",
"panel.dash_board": "Tilføj en fane med pligttavlen",
"panel.dash_board_hint": "Hele familiens dag med ét blik, kun et swipe væk.",
"panel.dash_sidebar": "Vis i sidepanelet",
"panel.dash_preview": "Forhåndsvisning",
"panel.dash_create": "Opret dashboard",
"panel.dash_need_child": "Tilføj et barn først.",
"panel.dash_pick_child": "Vælg mindst ét barn.",
"panel.dash_need_title": "Giv dashboardet en titel.",
"panel.dash_toast_created": "Oprettet: {titles}",
"panel.dash_toast_failed": "Kunne ikke oprette: {titles}",
"panel.dash_more": "… og {count} mere",
"panel.dash_palette": "Opret et dashboard: {kind}",
"panel.dash_view_today": "I dag",
"panel.dash_view_points": "Point",
"panel.dash_view_insights": "Indsigt",
"panel.dash_view_family": "Familie",
"panel.dash_card_kiosk": "Kiosk",
"panel.dash_card_chores": "{name}s pligter",
"panel.dash_title_kids": "Børnenes pligter",
"panel.dash_title_parents": "Forældre",
"panel.dash_title_wall": "Pligtvæg",
"wizard.notify_toggle": "Giv mig besked, når noget skal godkendes",
"wizard.notify_hint": "Bruger dine eksisterende indstillinger for notifikationer.",
"wizard.rewards_n": "Belønninger ({count})",
"wizard.no_rewards": "Ingen belønninger valgt",
"wizard.dash_title": "Familiens pligter",
"wizard.done_title": "Så er I klar!",
"wizard.done_copy": "TaskMate er klar til din familie.",
"wizard.done_copy_existing": "De nye pligter og belønninger er tilføjet. Intet andet er ændret.",
"wizard.done_copy_dashboards": "Dine dashboards er klar. Intet andet blev ændret.",
"wizard.done_copy_nothing": "Alt, hvad du valgte, fandtes allerede, så intet nyt blev tilføjet. Intet andet er ændret.",
"wizard.one_dashboard": "1 dashboard",
"wizard.dash_existed": "Der findes allerede et dashboard \"Familiens pligter\", så det blev ladt være.",
"wizard.dash_failed": "Dashboardet kunne ikke oprettes. Alt andet blev.",
"wizard.next_tablet_title": "Sæt det, hvor de kan se det",
"wizard.next_tablet": "Åbn dashboardet \"Familiens pligter\" på en tablet eller deres telefoner.",
"wizard.next_tablet": "Åbn dashboardet \"Børnenes pligter\" på en tablet eller deres telefoner.",
"wizard.next_today_title": "Hold øje under I dag",
"wizard.next_today": "Godkendelser, hvem har lavet hvad og hurtige bonusser, alt på én side.",
"wizard.next_change_title": "Ændr hvad som helst",
"wizard.next_change": "Rediger pligter, point og belønninger når som helst. Kør guiden igen fra Indstillinger.",
"wizard.open_dashboard": "Åbn dashboardet",
"wizard.go_today": "Gå til I dag",
"wizard.confirm_title": "Kør opsætningsguiden?",
"wizard.confirm_have": "Du har allerede {children}, {chores} og {rewards}. Guiden vil:",
@@ -2598,6 +2657,8 @@
"kiosk.editor.show_points": "Point og niveau",
"kiosk.editor.show_streak": "Dage i træk",
"kiosk.editor.show_reward_progress": "Fremskridt mod næste belønning",
"kiosk.editor.show_bounties": "Dusørtavle",
"kiosk.editor.show_bounties_helper": "Lad hvert barn tage og klare dusører her, med et foto fra tabletten når der bedes om et",
"kiosk.editor.show_picker_progress": "Fremskridt i vælgeren",
"kiosk.editor.show_picker_progress_helper": "“3 af 6 klaret” under hvert ansigt",
"kiosk.editor.play_sounds": "Lyde",
+71 -10
View File
@@ -912,7 +912,7 @@
"wizard.next_welcome": "Los geht's",
"wizard.next_children": "Weiter: Aufgaben",
"wizard.next_chores": "Weiter: Belohnungen",
"wizard.next_rewards": "Weiter: Prüfen",
"wizard.next_rewards": "Weiter: Dashboards",
"wizard.create": "Alles erstellen",
"wizard.create_existing": "Alles hinzufügen",
"wizard.creating": "Wird erstellt…",
@@ -955,7 +955,7 @@
"wizard.ready_for_chores": "Bereit für Aufgaben",
"wizard.preview_chores": "Hier erscheinen die Aufgaben",
"wizard.preview_rewards": "und Belohnungen zum Sparen",
"wizard.preview_hint": "Die Farbe erscheint auf der Karte im Dashboard „Familienaufgaben“. Avatar oder Foto können Sie später unter Kinder ändern.",
"wizard.preview_hint": "Die Farbe erscheint auf der Karte im Dashboard „Aufgaben der Kinder“. Avatar oder Foto können Sie später unter Kinder ändern.",
"wizard.age_n": "Alter {age}",
"wizard.group_chores": "Aufgaben für {group}",
"wizard.toast_child_exists": "{name} steht bereits auf der Liste",
@@ -1024,27 +1024,86 @@
"wizard.appr_none": "Keine",
"wizard.appr_big": "Große (4+)",
"wizard.appr_all": "Alle",
"wizard.dash_toggle": "Dashboard „Familienaufgaben“ erstellen",
"wizard.dash_hint": "Eine Kinderkarte pro Kind plus die Belohnungskarte. Wird übersprungen, wenn das Dashboard schon existiert.",
"wizard.step_dashboards": "Dashboards",
"wizard.next_dashboards": "Weiter: Prüfen",
"wizard.dash_step_title": "Dashboards auswählen",
"wizard.dash_step_copy": "Fertige Dashboards für alle im Haushalt, mit deinen Kindern schon eingetragen. Wähle beliebig viele oder keins.",
"wizard.dash_step_later": "Weitere kannst du jederzeit unter Einstellungen → Fertige Dashboards erstellen. Ein vorhandenes Dashboard wird nie überschrieben.",
"wizard.count_dashboards_one": "1 Dashboard",
"wizard.count_dashboards_many": "{count} Dashboards",
"wizard.dash_existed": "„{title}“ gibt es schon, es wurde so gelassen, wie es ist.",
"wizard.dash_failed": "„{title}“ konnte nicht erstellt werden. Alles andere schon.",
"wizard.open_named": "{title} öffnen",
"wizard.review_dashboards": "Dashboards",
"wizard.review_no_dashboards": "Keine",
"wizard.review_dashboards_only": "Diesmal nichts Neues für die Familie, nur die ausgewählten Dashboards.",
"panel.dash_section_title": "Fertige Dashboards",
"panel.dash_section_hint": "Starte mit einem Dashboard für jede Person im Haushalt, mit deinen Kindern schon eingetragen. Danach ist es ein ganz normales Home-Assistant-Dashboard: bearbeiten, umstellen oder löschen, wie du willst. Ein vorhandenes Dashboard wird nie überschrieben.",
"panel.dash_kind_kids": "Kinder",
"panel.dash_kind_parents": "Eltern",
"panel.dash_kind_wall": "Wand-Tablet",
"panel.dash_desc_kids": "Ein Tab pro Kind mit seinen Aufgaben, Belohnungsziel, Serie, Abzeichen und Belohnungen, dazu ein Familien-Tab mit Rangliste und Familienziel.",
"panel.dash_desc_parents": "Zuerst Bestätigungen und die Aufgabenübersicht, dann Punkte, Boni und Strafen, dann Wochenübersichten und Aktivität.",
"panel.dash_desc_wall": "Die Kiosk-Karte im Vollbild für ein gemeinsames Tablet, mit der Aufgabenübersicht als zweitem Tab.",
"panel.dash_for_kids": "Für das Tablet oder die Handys der Kinder",
"panel.dash_for_parents": "Für die Handys der Eltern",
"panel.dash_for_wall": "Für ein Tablet in Küche oder Flur",
"panel.dash_created": "Erstellt",
"panel.dash_open": "Öffnen",
"panel.dash_create_btn": "Erstellen …",
"panel.dash_dialog_title": "Dashboard erstellen",
"panel.dash_title_label": "Titel",
"panel.dash_path_label": "Adresse",
"panel.dash_path_hint": "Kleinbuchstaben, Ziffern und Bindestriche, mit mindestens einem Bindestrich.",
"panel.dash_path_taken": "Schon vergeben: {paths}. Wähle eine andere Adresse.",
"panel.dash_path_invalid": "Die Adresse darf nur Kleinbuchstaben, Ziffern und Bindestriche enthalten, mit mindestens einem Bindestrich.",
"panel.dash_list_failed": "Deine vorhandenen Dashboards konnten nicht gelesen werden, darum wurde nichts erstellt.",
"panel.dash_children_label": "Kinder",
"panel.dash_layout_label": "Aufteilung",
"panel.dash_layout_tabs": "Ein Dashboard, ein Tab pro Kind",
"panel.dash_layout_each": "Ein Dashboard pro Kind",
"panel.dash_layout_tabs_hint": "Gut für ein gemeinsames Tablet: Jedes Kind ist nur einen Tab entfernt.",
"panel.dash_layout_each_hint": "Gut für das eigene Handy eines Kindes: Jedes Kind bekommt ein eigenes Dashboard unter dieser Adresse plus seinem Namen.",
"panel.dash_hide_parent": "Eltern-Schaltflächen ausblenden",
"panel.dash_hide_parent_hint": "Bestätigen und Erledigen für ein Kind bleiben auf diesen Karten aus, auch wenn ein Elternteil zuschaut.",
"panel.dash_admin": "Nur für Admins",
"panel.dash_admin_hint": "Blendet es für Benutzer aus, die keine Home-Assistant-Admins sind. Lass es aus, wenn ein Elternteil kein Admin ist; die Karten prüfen vor jeder Aktion trotzdem die Elternrechte.",
"panel.dash_board": "Tab mit Aufgabenübersicht hinzufügen",
"panel.dash_board_hint": "Der Tag der ganzen Familie auf einen Blick, nur einmal wischen entfernt.",
"panel.dash_sidebar": "In der Seitenleiste anzeigen",
"panel.dash_preview": "Vorschau",
"panel.dash_create": "Dashboard erstellen",
"panel.dash_need_child": "Füge zuerst ein Kind hinzu.",
"panel.dash_pick_child": "Wähle mindestens ein Kind aus.",
"panel.dash_need_title": "Gib dem Dashboard einen Titel.",
"panel.dash_toast_created": "Erstellt: {titles}",
"panel.dash_toast_failed": "Konnte nicht erstellt werden: {titles}",
"panel.dash_more": "… und {count} weitere",
"panel.dash_palette": "Dashboard erstellen: {kind}",
"panel.dash_view_today": "Heute",
"panel.dash_view_points": "Punkte",
"panel.dash_view_insights": "Einblicke",
"panel.dash_view_family": "Familie",
"panel.dash_card_kiosk": "Kiosk",
"panel.dash_card_chores": "Aufgaben von {name}",
"panel.dash_title_kids": "Aufgaben der Kinder",
"panel.dash_title_parents": "Eltern",
"panel.dash_title_wall": "Aufgabenwand",
"wizard.notify_toggle": "Mir Bescheid geben, wenn etwas bestätigt werden muss",
"wizard.notify_hint": "Nutzt Ihre bestehenden Benachrichtigungseinstellungen.",
"wizard.rewards_n": "Belohnungen ({count})",
"wizard.no_rewards": "Keine Belohnungen ausgewählt",
"wizard.dash_title": "Familienaufgaben",
"wizard.done_title": "Alles fertig!",
"wizard.done_copy": "TaskMate ist bereit für Ihre Familie.",
"wizard.done_copy_existing": "Die neuen Aufgaben und Belohnungen wurden hinzugefügt. Sonst wurde nichts geändert.",
"wizard.done_copy_dashboards": "Deine Dashboards sind fertig. Sonst wurde nichts geändert.",
"wizard.done_copy_nothing": "Alles, was Sie ausgewählt haben, war schon vorhanden, daher wurde nichts Neues hinzugefügt. Sonst wurde nichts geändert.",
"wizard.one_dashboard": "1 Dashboard",
"wizard.dash_existed": "Ein Dashboard „Familienaufgaben“ gibt es schon, es wurde so gelassen, wie es ist.",
"wizard.dash_failed": "Das Dashboard konnte nicht erstellt werden. Alles andere schon.",
"wizard.next_tablet_title": "Dort platzieren, wo sie es sehen",
"wizard.next_tablet": "Öffnen Sie das Dashboard „Familienaufgaben“ auf einem Tablet oder ihren Handys.",
"wizard.next_tablet": "Öffnen Sie das Dashboard „Aufgaben der Kinder“ auf einem Tablet oder ihren Handys.",
"wizard.next_today_title": "Unter Heute nachsehen",
"wizard.next_today": "Bestätigungen, wer was erledigt hat und schnelle Boni, alles auf einer Seite.",
"wizard.next_change_title": "Alles änderbar",
"wizard.next_change": "Aufgaben, Punkte und Belohnungen jederzeit bearbeiten. Starten Sie dies erneut über die Einstellungen.",
"wizard.open_dashboard": "Dashboard öffnen",
"wizard.go_today": "Zu Heute",
"wizard.confirm_title": "Einrichtungsassistent starten?",
"wizard.confirm_have": "Sie haben bereits {children}, {chores} und {rewards}. Der Assistent wird:",
@@ -2598,6 +2657,8 @@
"kiosk.editor.show_points": "Punkte und Stufe",
"kiosk.editor.show_streak": "Serie",
"kiosk.editor.show_reward_progress": "Fortschritt zur nächsten Belohnung",
"kiosk.editor.show_bounties": "Sonderjobs",
"kiosk.editor.show_bounties_helper": "Jedes Kind kann hier Sonderjobs übernehmen und abschließen – mit einem Foto vom Tablet, wenn eins verlangt wird",
"kiosk.editor.show_picker_progress": "Fortschritt in der Auswahl",
"kiosk.editor.show_picker_progress_helper": "„3 von 6 erledigt“ unter jedem Gesicht",
"kiosk.editor.play_sounds": "Töne",
@@ -962,7 +962,7 @@
"wizard.next_welcome": "Let's go",
"wizard.next_children": "Next: chores",
"wizard.next_chores": "Next: rewards",
"wizard.next_rewards": "Next: review",
"wizard.next_rewards": "Next: dashboards",
"wizard.create": "Create everything",
"wizard.create_existing": "Add everything",
"wizard.creating": "Creating…",
@@ -1005,7 +1005,7 @@
"wizard.ready_for_chores": "Ready for chores",
"wizard.preview_chores": "Their chores appear here",
"wizard.preview_rewards": "and rewards to save for",
"wizard.preview_hint": "The colour goes on their card in the Family chores dashboard. Change the avatar or add a photo later in Children.",
"wizard.preview_hint": "The colour goes on their card in the Kids' chores dashboard. Change the avatar or add a photo later in Children.",
"wizard.age_n": "Age {age}",
"wizard.group_chores": "{group} chores",
"wizard.toast_child_exists": "{name} is already on the list",
@@ -1074,27 +1074,86 @@
"wizard.appr_none": "None",
"wizard.appr_big": "Big jobs (4+)",
"wizard.appr_all": "All",
"wizard.dash_toggle": "Create a \"Family chores\" dashboard",
"wizard.dash_hint": "One child card per child plus the rewards card. Skipped if the dashboard already exists.",
"wizard.step_dashboards": "Dashboards",
"wizard.next_dashboards": "Next: review",
"wizard.dash_step_title": "Pick your dashboards",
"wizard.dash_step_copy": "Ready-made dashboards for everyone in the house, with your children already filled in. Pick any, all or none.",
"wizard.dash_step_later": "You can make more at any time from Settings → Ready-made dashboards. An existing dashboard is never overwritten.",
"wizard.count_dashboards_one": "1 dashboard",
"wizard.count_dashboards_many": "{count} dashboards",
"wizard.dash_existed": "\"{title}\" already exists, so it was left as it is.",
"wizard.dash_failed": "\"{title}\" couldn't be created. Everything else was.",
"wizard.open_named": "Open {title}",
"wizard.review_dashboards": "Dashboards",
"wizard.review_no_dashboards": "None",
"wizard.review_dashboards_only": "Nothing new for the family this time: just the dashboards you picked.",
"panel.dash_section_title": "Ready-made dashboards",
"panel.dash_section_hint": "Start from a dashboard built for each person in the house, filled in with your children. Afterwards it's an ordinary Home Assistant dashboard: edit, rearrange or delete it as you like. An existing dashboard is never overwritten.",
"panel.dash_kind_kids": "Kids",
"panel.dash_kind_parents": "Parents",
"panel.dash_kind_wall": "Wall tablet",
"panel.dash_desc_kids": "A tab for each child with their chores, reward goal, streak, badges and rewards, plus a family tab with the leaderboard and family goal.",
"panel.dash_desc_parents": "Approvals and the chore board first, then points, bonuses and penalties, then the weekly charts and activity.",
"panel.dash_desc_wall": "The kiosk card full screen for a shared tablet, with the chore board as a second tab.",
"panel.dash_for_kids": "For the kids' tablet or phones",
"panel.dash_for_parents": "For parents' phones",
"panel.dash_for_wall": "For a kitchen or hallway tablet",
"panel.dash_created": "Created",
"panel.dash_open": "Open",
"panel.dash_create_btn": "Create…",
"panel.dash_dialog_title": "Create a dashboard",
"panel.dash_title_label": "Title",
"panel.dash_path_label": "Address",
"panel.dash_path_hint": "Lower-case letters, numbers and dashes, with at least one dash.",
"panel.dash_path_taken": "Already in use: {paths}. Pick another address.",
"panel.dash_path_invalid": "The address must be lower-case letters, numbers and dashes, with at least one dash.",
"panel.dash_list_failed": "Couldn't read your existing dashboards, so nothing was created.",
"panel.dash_children_label": "Children",
"panel.dash_layout_label": "Layout",
"panel.dash_layout_tabs": "One dashboard, a tab each",
"panel.dash_layout_each": "A dashboard per child",
"panel.dash_layout_tabs_hint": "Good for a shared tablet: every child is a tab away.",
"panel.dash_layout_each_hint": "Good for a child's own phone: each child gets their own dashboard, at this address followed by their name.",
"panel.dash_hide_parent": "Hide parent buttons",
"panel.dash_hide_parent_hint": "Approving and completing for a child stay off these cards, even when a parent is looking.",
"panel.dash_admin": "Admins only",
"panel.dash_admin_hint": "Hides it from users who aren't Home Assistant admins. Leave off if a parent isn't an admin; the cards still check parent rights before any action.",
"panel.dash_board": "Add a chore board tab",
"panel.dash_board_hint": "The whole family's day at a glance, one swipe away.",
"panel.dash_sidebar": "Show in the sidebar",
"panel.dash_preview": "Preview",
"panel.dash_create": "Create dashboard",
"panel.dash_need_child": "Add a child first.",
"panel.dash_pick_child": "Pick at least one child.",
"panel.dash_need_title": "Give the dashboard a title.",
"panel.dash_toast_created": "Created: {titles}",
"panel.dash_toast_failed": "Couldn't create: {titles}",
"panel.dash_more": "…and {count} more",
"panel.dash_palette": "Create a dashboard: {kind}",
"panel.dash_view_today": "Today",
"panel.dash_view_points": "Points",
"panel.dash_view_insights": "Insights",
"panel.dash_view_family": "Family",
"panel.dash_card_kiosk": "Kiosk",
"panel.dash_card_chores": "{name}'s chores",
"panel.dash_title_kids": "Kids' chores",
"panel.dash_title_parents": "Parents",
"panel.dash_title_wall": "Chore wall",
"wizard.notify_toggle": "Tell me when something needs approving",
"wizard.notify_hint": "Uses your existing Notifications settings.",
"wizard.rewards_n": "Rewards ({count})",
"wizard.no_rewards": "No rewards picked",
"wizard.dash_title": "Family chores",
"wizard.done_title": "You're all set!",
"wizard.done_copy": "TaskMate is ready for your family.",
"wizard.done_copy_existing": "The new chores and rewards have been added. Nothing else was changed.",
"wizard.done_copy_dashboards": "Your dashboards are ready. Nothing else was changed.",
"wizard.done_copy_nothing": "Everything you picked was already there, so nothing new was added. Nothing else was changed.",
"wizard.one_dashboard": "1 dashboard",
"wizard.dash_existed": "A \"Family chores\" dashboard already exists, so it was left as it is.",
"wizard.dash_failed": "The dashboard couldn't be created. Everything else was.",
"wizard.next_tablet_title": "Put it where they'll see it",
"wizard.next_tablet": "Open the \"Family chores\" dashboard on a tablet or their phones.",
"wizard.next_tablet": "Open the \"Kids' chores\" dashboard on a tablet or their phones.",
"wizard.next_today_title": "Check in on Today",
"wizard.next_today": "Approvals, who's done what and quick bonuses, all on one page.",
"wizard.next_change_title": "Change anything",
"wizard.next_change": "Edit chores, points and rewards any time. Run this again from Settings.",
"wizard.open_dashboard": "Open the dashboard",
"wizard.go_today": "Go to Today",
"wizard.confirm_title": "Run the setup wizard?",
"wizard.confirm_have": "You already have {children}, {chores} and {rewards}. The wizard will:",
@@ -2598,6 +2657,8 @@
"kiosk.editor.show_points": "Points and level",
"kiosk.editor.show_streak": "Streak",
"kiosk.editor.show_reward_progress": "Next reward progress",
"kiosk.editor.show_bounties": "Bounty Board",
"kiosk.editor.show_bounties_helper": "Let each child claim and finish bounties here, with a photo from the tablet when one is asked for",
"kiosk.editor.show_picker_progress": "Progress on the picker",
"kiosk.editor.show_picker_progress_helper": "“3 of 6 done” under each face",
"kiosk.editor.play_sounds": "Sounds",
+71 -10
View File
@@ -962,7 +962,7 @@
"wizard.next_welcome": "Let's go",
"wizard.next_children": "Next: chores",
"wizard.next_chores": "Next: rewards",
"wizard.next_rewards": "Next: review",
"wizard.next_rewards": "Next: dashboards",
"wizard.create": "Create everything",
"wizard.create_existing": "Add everything",
"wizard.creating": "Creating…",
@@ -1005,7 +1005,7 @@
"wizard.ready_for_chores": "Ready for chores",
"wizard.preview_chores": "Their chores appear here",
"wizard.preview_rewards": "and rewards to save for",
"wizard.preview_hint": "The colour goes on their card in the Family chores dashboard. Change the avatar or add a photo later in Children.",
"wizard.preview_hint": "The colour goes on their card in the Kids' chores dashboard. Change the avatar or add a photo later in Children.",
"wizard.age_n": "Age {age}",
"wizard.group_chores": "{group} chores",
"wizard.toast_child_exists": "{name} is already on the list",
@@ -1074,27 +1074,86 @@
"wizard.appr_none": "None",
"wizard.appr_big": "Big jobs (4+)",
"wizard.appr_all": "All",
"wizard.dash_toggle": "Create a \"Family chores\" dashboard",
"wizard.dash_hint": "One child card per child plus the rewards card. Skipped if the dashboard already exists.",
"wizard.step_dashboards": "Dashboards",
"wizard.next_dashboards": "Next: review",
"wizard.dash_step_title": "Pick your dashboards",
"wizard.dash_step_copy": "Ready-made dashboards for everyone in the house, with your children already filled in. Pick any, all or none.",
"wizard.dash_step_later": "You can make more at any time from Settings → Ready-made dashboards. An existing dashboard is never overwritten.",
"wizard.count_dashboards_one": "1 dashboard",
"wizard.count_dashboards_many": "{count} dashboards",
"wizard.dash_existed": "\"{title}\" already exists, so it was left as it is.",
"wizard.dash_failed": "\"{title}\" couldn't be created. Everything else was.",
"wizard.open_named": "Open {title}",
"wizard.review_dashboards": "Dashboards",
"wizard.review_no_dashboards": "None",
"wizard.review_dashboards_only": "Nothing new for the family this time: just the dashboards you picked.",
"panel.dash_section_title": "Ready-made dashboards",
"panel.dash_section_hint": "Start from a dashboard built for each person in the house, filled in with your children. Afterwards it's an ordinary Home Assistant dashboard: edit, rearrange or delete it as you like. An existing dashboard is never overwritten.",
"panel.dash_kind_kids": "Kids",
"panel.dash_kind_parents": "Parents",
"panel.dash_kind_wall": "Wall tablet",
"panel.dash_desc_kids": "A tab for each child with their chores, reward goal, streak, badges and rewards, plus a family tab with the leaderboard and family goal.",
"panel.dash_desc_parents": "Approvals and the chore board first, then points, bonuses and penalties, then the weekly charts and activity.",
"panel.dash_desc_wall": "The kiosk card full screen for a shared tablet, with the chore board as a second tab.",
"panel.dash_for_kids": "For the kids' tablet or phones",
"panel.dash_for_parents": "For parents' phones",
"panel.dash_for_wall": "For a kitchen or hallway tablet",
"panel.dash_created": "Created",
"panel.dash_open": "Open",
"panel.dash_create_btn": "Create…",
"panel.dash_dialog_title": "Create a dashboard",
"panel.dash_title_label": "Title",
"panel.dash_path_label": "Address",
"panel.dash_path_hint": "Lower-case letters, numbers and dashes, with at least one dash.",
"panel.dash_path_taken": "Already in use: {paths}. Pick another address.",
"panel.dash_path_invalid": "The address must be lower-case letters, numbers and dashes, with at least one dash.",
"panel.dash_list_failed": "Couldn't read your existing dashboards, so nothing was created.",
"panel.dash_children_label": "Children",
"panel.dash_layout_label": "Layout",
"panel.dash_layout_tabs": "One dashboard, a tab each",
"panel.dash_layout_each": "A dashboard per child",
"panel.dash_layout_tabs_hint": "Good for a shared tablet: every child is a tab away.",
"panel.dash_layout_each_hint": "Good for a child's own phone: each child gets their own dashboard, at this address followed by their name.",
"panel.dash_hide_parent": "Hide parent buttons",
"panel.dash_hide_parent_hint": "Approving and completing for a child stay off these cards, even when a parent is looking.",
"panel.dash_admin": "Admins only",
"panel.dash_admin_hint": "Hides it from users who aren't Home Assistant admins. Leave off if a parent isn't an admin; the cards still check parent rights before any action.",
"panel.dash_board": "Add a chore board tab",
"panel.dash_board_hint": "The whole family's day at a glance, one swipe away.",
"panel.dash_sidebar": "Show in the sidebar",
"panel.dash_preview": "Preview",
"panel.dash_create": "Create dashboard",
"panel.dash_need_child": "Add a child first.",
"panel.dash_pick_child": "Pick at least one child.",
"panel.dash_need_title": "Give the dashboard a title.",
"panel.dash_toast_created": "Created: {titles}",
"panel.dash_toast_failed": "Couldn't create: {titles}",
"panel.dash_more": "…and {count} more",
"panel.dash_palette": "Create a dashboard: {kind}",
"panel.dash_view_today": "Today",
"panel.dash_view_points": "Points",
"panel.dash_view_insights": "Insights",
"panel.dash_view_family": "Family",
"panel.dash_card_kiosk": "Kiosk",
"panel.dash_card_chores": "{name}'s chores",
"panel.dash_title_kids": "Kids' chores",
"panel.dash_title_parents": "Parents",
"panel.dash_title_wall": "Chore wall",
"wizard.notify_toggle": "Tell me when something needs approving",
"wizard.notify_hint": "Uses your existing Notifications settings.",
"wizard.rewards_n": "Rewards ({count})",
"wizard.no_rewards": "No rewards picked",
"wizard.dash_title": "Family chores",
"wizard.done_title": "You're all set!",
"wizard.done_copy": "TaskMate is ready for your family.",
"wizard.done_copy_existing": "The new chores and rewards have been added. Nothing else was changed.",
"wizard.done_copy_dashboards": "Your dashboards are ready. Nothing else was changed.",
"wizard.done_copy_nothing": "Everything you picked was already there, so nothing new was added. Nothing else was changed.",
"wizard.one_dashboard": "1 dashboard",
"wizard.dash_existed": "A \"Family chores\" dashboard already exists, so it was left as it is.",
"wizard.dash_failed": "The dashboard couldn't be created. Everything else was.",
"wizard.next_tablet_title": "Put it where they'll see it",
"wizard.next_tablet": "Open the \"Family chores\" dashboard on a tablet or their phones.",
"wizard.next_tablet": "Open the \"Kids' chores\" dashboard on a tablet or their phones.",
"wizard.next_today_title": "Check in on Today",
"wizard.next_today": "Approvals, who's done what and quick bonuses, all on one page.",
"wizard.next_change_title": "Change anything",
"wizard.next_change": "Edit chores, points and rewards any time. Run this again from Settings.",
"wizard.open_dashboard": "Open the dashboard",
"wizard.go_today": "Go to Today",
"wizard.confirm_title": "Run the setup wizard?",
"wizard.confirm_have": "You already have {children}, {chores} and {rewards}. The wizard will:",
@@ -2598,6 +2657,8 @@
"kiosk.editor.show_points": "Points and level",
"kiosk.editor.show_streak": "Streak",
"kiosk.editor.show_reward_progress": "Next reward progress",
"kiosk.editor.show_bounties": "Bounty Board",
"kiosk.editor.show_bounties_helper": "Let each child claim and finish bounties here, with a photo from the tablet when one is asked for",
"kiosk.editor.show_picker_progress": "Progress on the picker",
"kiosk.editor.show_picker_progress_helper": "“3 of 6 done” under each face",
"kiosk.editor.play_sounds": "Sounds",
+71 -10
View File
@@ -912,7 +912,7 @@
"wizard.next_welcome": "C'est parti",
"wizard.next_children": "Suivant : tâches",
"wizard.next_chores": "Suivant : récompenses",
"wizard.next_rewards": "Suivant : vérifier",
"wizard.next_rewards": "Suivant : tableaux de bord",
"wizard.create": "Tout créer",
"wizard.create_existing": "Tout ajouter",
"wizard.creating": "Création…",
@@ -955,7 +955,7 @@
"wizard.ready_for_chores": "Prêt pour les tâches",
"wizard.preview_chores": "Ses tâches apparaissent ici",
"wizard.preview_rewards": "et les récompenses à gagner",
"wizard.preview_hint": "La couleur est utilisée pour sa carte dans le tableau de bord « Tâches de la famille ». Changez l'avatar ou ajoutez une photo plus tard dans Enfants.",
"wizard.preview_hint": "La couleur est utilisée pour sa carte dans le tableau de bord « Tâches des enfants ». Changez l'avatar ou ajoutez une photo plus tard dans Enfants.",
"wizard.age_n": "{age} ans",
"wizard.group_chores": "Tâches {group}",
"wizard.toast_child_exists": "{name} est déjà dans la liste",
@@ -1024,27 +1024,86 @@
"wizard.appr_none": "Aucune",
"wizard.appr_big": "Grosses (4+)",
"wizard.appr_all": "Toutes",
"wizard.dash_toggle": "Créer un tableau de bord « Tâches de la famille »",
"wizard.dash_hint": "Une carte par enfant plus la carte des récompenses. Ignoré si le tableau de bord existe déjà.",
"wizard.step_dashboards": "Tableaux de bord",
"wizard.next_dashboards": "Suivant : vérifier",
"wizard.dash_step_title": "Choisissez vos tableaux de bord",
"wizard.dash_step_copy": "Des tableaux de bord prêts à l'emploi pour toute la maison, avec vos enfants déjà renseignés. Choisissez-en un, plusieurs ou aucun.",
"wizard.dash_step_later": "Vous pourrez en créer d'autres à tout moment dans Paramètres → Tableaux de bord prêts à l'emploi. Un tableau de bord existant n'est jamais écrasé.",
"wizard.count_dashboards_one": "1 tableau de bord",
"wizard.count_dashboards_many": "{count} tableaux de bord",
"wizard.dash_existed": "« {title} » existe déjà ; il a été laissé tel quel.",
"wizard.dash_failed": "« {title} » n'a pas pu être créé. Tout le reste l'a été.",
"wizard.open_named": "Ouvrir {title}",
"wizard.review_dashboards": "Tableaux de bord",
"wizard.review_no_dashboards": "Aucun",
"wizard.review_dashboards_only": "Rien de nouveau pour la famille cette fois : seulement les tableaux de bord choisis.",
"panel.dash_section_title": "Tableaux de bord prêts à l'emploi",
"panel.dash_section_hint": "Partez d'un tableau de bord conçu pour chaque personne de la maison, avec vos enfants déjà renseignés. Ensuite, c'est un tableau de bord Home Assistant ordinaire : modifiez-le, réorganisez-le ou supprimez-le à votre guise. Un tableau de bord existant n'est jamais écrasé.",
"panel.dash_kind_kids": "Enfants",
"panel.dash_kind_parents": "Parents",
"panel.dash_kind_wall": "Tablette murale",
"panel.dash_desc_kids": "Un onglet par enfant avec ses tâches, son objectif de récompense, sa série, ses badges et ses récompenses, plus un onglet famille avec le classement et l'objectif familial.",
"panel.dash_desc_parents": "D'abord les validations et le tableau des tâches, puis les points, bonus et pénalités, puis les graphiques de la semaine et l'activité.",
"panel.dash_desc_wall": "La carte kiosque en plein écran pour une tablette partagée, avec le tableau des tâches en second onglet.",
"panel.dash_for_kids": "Pour la tablette ou les téléphones des enfants",
"panel.dash_for_parents": "Pour les téléphones des parents",
"panel.dash_for_wall": "Pour une tablette dans la cuisine ou l'entrée",
"panel.dash_created": "Créé",
"panel.dash_open": "Ouvrir",
"panel.dash_create_btn": "Créer…",
"panel.dash_dialog_title": "Créer un tableau de bord",
"panel.dash_title_label": "Titre",
"panel.dash_path_label": "Adresse",
"panel.dash_path_hint": "Minuscules, chiffres et tirets, avec au moins un tiret.",
"panel.dash_path_taken": "Déjà utilisée : {paths}. Choisissez une autre adresse.",
"panel.dash_path_invalid": "L'adresse ne doit contenir que des minuscules, des chiffres et des tirets, avec au moins un tiret.",
"panel.dash_list_failed": "Impossible de lire vos tableaux de bord existants, rien n'a donc été créé.",
"panel.dash_children_label": "Enfants",
"panel.dash_layout_label": "Disposition",
"panel.dash_layout_tabs": "Un tableau de bord, un onglet par enfant",
"panel.dash_layout_each": "Un tableau de bord par enfant",
"panel.dash_layout_tabs_hint": "Idéal pour une tablette partagée : chaque enfant est à un onglet de distance.",
"panel.dash_layout_each_hint": "Idéal pour le téléphone d'un enfant : chacun reçoit son propre tableau de bord, à cette adresse suivie de son prénom.",
"panel.dash_hide_parent": "Masquer les boutons parents",
"panel.dash_hide_parent_hint": "Valider et terminer pour un enfant restent désactivés sur ces cartes, même quand un parent regarde.",
"panel.dash_admin": "Administrateurs uniquement",
"panel.dash_admin_hint": "Le masque aux utilisateurs qui ne sont pas administrateurs de Home Assistant. Laissez désactivé si un parent n'est pas administrateur ; les cartes vérifient quand même les droits parent avant toute action.",
"panel.dash_board": "Ajouter un onglet tableau des tâches",
"panel.dash_board_hint": "La journée de toute la famille en un coup d'œil, à un glissement de doigt.",
"panel.dash_sidebar": "Afficher dans la barre latérale",
"panel.dash_preview": "Aperçu",
"panel.dash_create": "Créer le tableau de bord",
"panel.dash_need_child": "Ajoutez d'abord un enfant.",
"panel.dash_pick_child": "Choisissez au moins un enfant.",
"panel.dash_need_title": "Donnez un titre au tableau de bord.",
"panel.dash_toast_created": "Créé : {titles}",
"panel.dash_toast_failed": "Création impossible : {titles}",
"panel.dash_more": "… et {count} de plus",
"panel.dash_palette": "Créer un tableau de bord : {kind}",
"panel.dash_view_today": "Aujourd'hui",
"panel.dash_view_points": "Points",
"panel.dash_view_insights": "Statistiques",
"panel.dash_view_family": "Famille",
"panel.dash_card_kiosk": "Kiosque",
"panel.dash_card_chores": "Tâches de {name}",
"panel.dash_title_kids": "Tâches des enfants",
"panel.dash_title_parents": "Parents",
"panel.dash_title_wall": "Mur des tâches",
"wizard.notify_toggle": "Me prévenir quand quelque chose attend une validation",
"wizard.notify_hint": "Utilise vos réglages de Notifications existants.",
"wizard.rewards_n": "Récompenses ({count})",
"wizard.no_rewards": "Aucune récompense choisie",
"wizard.dash_title": "Tâches de la famille",
"wizard.done_title": "Tout est prêt !",
"wizard.done_copy": "TaskMate est prêt pour votre famille.",
"wizard.done_copy_existing": "Les nouvelles tâches et récompenses ont été ajoutées. Rien d'autre n'a changé.",
"wizard.done_copy_dashboards": "Vos tableaux de bord sont prêts. Rien d'autre n'a été modifié.",
"wizard.done_copy_nothing": "Tout ce que vous avez choisi existait déjà, rien de nouveau n'a donc été ajouté. Rien d'autre n'a changé.",
"wizard.one_dashboard": "1 tableau de bord",
"wizard.dash_existed": "Un tableau de bord « Tâches de la famille » existe déjà ; il a été laissé tel quel.",
"wizard.dash_failed": "Le tableau de bord n'a pas pu être créé. Tout le reste l'a été.",
"wizard.next_tablet_title": "Placez-le là où ils le verront",
"wizard.next_tablet": "Ouvrez le tableau de bord « Tâches de la famille » sur une tablette ou leurs téléphones.",
"wizard.next_tablet": "Ouvrez le tableau de bord « Tâches des enfants » sur une tablette ou leurs téléphones.",
"wizard.next_today_title": "Faites le point dans Aujourd'hui",
"wizard.next_today": "Validations, qui a fait quoi et bonus rapides, le tout sur une page.",
"wizard.next_change_title": "Tout se modifie",
"wizard.next_change": "Modifiez tâches, points et récompenses à tout moment. Relancez ceci depuis Paramètres.",
"wizard.open_dashboard": "Ouvrir le tableau de bord",
"wizard.go_today": "Aller à Aujourd'hui",
"wizard.confirm_title": "Lancer l'assistant de configuration ?",
"wizard.confirm_have": "Vous avez déjà {children}, {chores} et {rewards}. L'assistant va :",
@@ -2598,6 +2657,8 @@
"kiosk.editor.show_points": "Points et niveau",
"kiosk.editor.show_streak": "Série",
"kiosk.editor.show_reward_progress": "Progression vers la prochaine récompense",
"kiosk.editor.show_bounties": "Tableau des primes",
"kiosk.editor.show_bounties_helper": "Chaque enfant peut réserver et terminer des primes ici, avec une photo prise sur la tablette quand elle est demandée",
"kiosk.editor.show_picker_progress": "Progression sur l'écran de choix",
"kiosk.editor.show_picker_progress_helper": "« 3 sur 6 faites » sous chaque visage",
"kiosk.editor.play_sounds": "Sons",
+71 -10
View File
@@ -912,7 +912,7 @@
"wizard.next_welcome": "Sett i gang",
"wizard.next_children": "Neste: oppgaver",
"wizard.next_chores": "Neste: belønninger",
"wizard.next_rewards": "Neste: se over",
"wizard.next_rewards": "Neste: dashbord",
"wizard.create": "Opprett alt",
"wizard.create_existing": "Legg til alt",
"wizard.creating": "Oppretter…",
@@ -955,7 +955,7 @@
"wizard.ready_for_chores": "Klar for oppgaver",
"wizard.preview_chores": "Oppgavene vises her",
"wizard.preview_rewards": "og belønninger å spare til",
"wizard.preview_hint": "Fargen brukes på kortet i dashbordet «Familiens oppgaver». Bytt avatar eller legg til et bilde senere under Barn.",
"wizard.preview_hint": "Fargen brukes på kortet i dashbordet «Barnas oppgaver». Bytt avatar eller legg til et bilde senere under Barn.",
"wizard.age_n": "{age} år",
"wizard.group_chores": "Oppgaver for {group}",
"wizard.toast_child_exists": "{name} står allerede på listen",
@@ -1024,27 +1024,86 @@
"wizard.appr_none": "Ingen",
"wizard.appr_big": "Store (4+)",
"wizard.appr_all": "Alle",
"wizard.dash_toggle": "Opprett dashbordet «Familiens oppgaver»",
"wizard.dash_hint": "Ett barnekort per barn pluss belønningskortet. Hoppes over hvis dashbordet finnes fra før.",
"wizard.step_dashboards": "Dashbord",
"wizard.next_dashboards": "Neste: se over",
"wizard.dash_step_title": "Velg dashbordene dine",
"wizard.dash_step_copy": "Ferdige dashbord for alle i huset, med barna dine allerede fylt inn. Velg noen, alle eller ingen.",
"wizard.dash_step_later": "Du kan lage flere når som helst under Innstillinger → Ferdige dashbord. Et eksisterende dashbord blir aldri overskrevet.",
"wizard.count_dashboards_one": "1 dashbord",
"wizard.count_dashboards_many": "{count} dashbord",
"wizard.dash_existed": "«{title}» finnes allerede, så det ble latt være som det er.",
"wizard.dash_failed": "«{title}» kunne ikke opprettes. Alt annet ble det.",
"wizard.open_named": "Åpne {title}",
"wizard.review_dashboards": "Dashbord",
"wizard.review_no_dashboards": "Ingen",
"wizard.review_dashboards_only": "Ingenting nytt for familien denne gangen, bare dashbordene du valgte.",
"panel.dash_section_title": "Ferdige dashbord",
"panel.dash_section_hint": "Start med et dashbord laget for hver person i huset, med barna dine fylt inn. Etterpå er det et vanlig Home Assistant-dashbord: rediger, flytt rundt på eller slett det som du vil. Et eksisterende dashbord blir aldri overskrevet.",
"panel.dash_kind_kids": "Barn",
"panel.dash_kind_parents": "Foreldre",
"panel.dash_kind_wall": "Veggnettbrett",
"panel.dash_desc_kids": "En fane for hvert barn med oppgavene, belønningsmålet, rekken, merkene og belønningene deres, pluss en familiefane med toppliste og familiemål.",
"panel.dash_desc_parents": "Godkjenninger og oppgavetavla først, så poeng, bonuser og straffer, så ukeoversikter og aktivitet.",
"panel.dash_desc_wall": "Kioskkortet i fullskjerm for et felles nettbrett, med oppgavetavla som en andre fane.",
"panel.dash_for_kids": "For barnas nettbrett eller telefoner",
"panel.dash_for_parents": "For foreldrenes telefoner",
"panel.dash_for_wall": "For et nettbrett på kjøkkenet eller i gangen",
"panel.dash_created": "Opprettet",
"panel.dash_open": "Åpne",
"panel.dash_create_btn": "Opprett …",
"panel.dash_dialog_title": "Opprett et dashbord",
"panel.dash_title_label": "Tittel",
"panel.dash_path_label": "Adresse",
"panel.dash_path_hint": "Små bokstaver, tall og bindestreker, med minst én bindestrek.",
"panel.dash_path_taken": "Allerede i bruk: {paths}. Velg en annen adresse.",
"panel.dash_path_invalid": "Adressen kan bare ha små bokstaver, tall og bindestreker, med minst én bindestrek.",
"panel.dash_list_failed": "Klarte ikke å lese de eksisterende dashbordene dine, så ingenting ble opprettet.",
"panel.dash_children_label": "Barn",
"panel.dash_layout_label": "Oppsett",
"panel.dash_layout_tabs": "Ett dashbord, en fane per barn",
"panel.dash_layout_each": "Ett dashbord per barn",
"panel.dash_layout_tabs_hint": "Bra for et felles nettbrett: hvert barn er bare en fane unna.",
"panel.dash_layout_each_hint": "Bra for barnets egen telefon: hvert barn får sitt eget dashbord, på denne adressen etterfulgt av navnet sitt.",
"panel.dash_hide_parent": "Skjul foreldreknapper",
"panel.dash_hide_parent_hint": "Godkjenning og fullføring for et barn holdes av på disse kortene, også når en forelder ser på.",
"panel.dash_admin": "Bare administratorer",
"panel.dash_admin_hint": "Skjuler det for brukere som ikke er Home Assistant-administratorer. La det være av hvis en forelder ikke er administrator; kortene sjekker uansett foreldrerettigheter før hver handling.",
"panel.dash_board": "Legg til en fane med oppgavetavla",
"panel.dash_board_hint": "Hele familiens dag med ett blikk, bare et sveip unna.",
"panel.dash_sidebar": "Vis i sidepanelet",
"panel.dash_preview": "Forhåndsvisning",
"panel.dash_create": "Opprett dashbord",
"panel.dash_need_child": "Legg til et barn først.",
"panel.dash_pick_child": "Velg minst ett barn.",
"panel.dash_need_title": "Gi dashbordet en tittel.",
"panel.dash_toast_created": "Opprettet: {titles}",
"panel.dash_toast_failed": "Kunne ikke opprette: {titles}",
"panel.dash_more": "… og {count} til",
"panel.dash_palette": "Opprett et dashbord: {kind}",
"panel.dash_view_today": "I dag",
"panel.dash_view_points": "Poeng",
"panel.dash_view_insights": "Innsikt",
"panel.dash_view_family": "Familie",
"panel.dash_card_kiosk": "Kiosk",
"panel.dash_card_chores": "Oppgavene til {name}",
"panel.dash_title_kids": "Barnas oppgaver",
"panel.dash_title_parents": "Foreldre",
"panel.dash_title_wall": "Oppgavevegg",
"wizard.notify_toggle": "Gi meg beskjed når noe må godkjennes",
"wizard.notify_hint": "Bruker de eksisterende varselinnstillingene dine.",
"wizard.rewards_n": "Belønninger ({count})",
"wizard.no_rewards": "Ingen belønninger valgt",
"wizard.dash_title": "Familiens oppgaver",
"wizard.done_title": "Alt er klart!",
"wizard.done_copy": "TaskMate er klar for familien din.",
"wizard.done_copy_existing": "De nye oppgavene og belønningene er lagt til. Ingenting annet er endret.",
"wizard.done_copy_dashboards": "Dashbordene dine er klare. Ingenting annet ble endret.",
"wizard.done_copy_nothing": "Alt du valgte, fantes allerede, så ingenting nytt ble lagt til. Ingenting annet er endret.",
"wizard.one_dashboard": "1 dashbord",
"wizard.dash_existed": "Et dashbord «Familiens oppgaver» finnes allerede, så det ble latt være som det er.",
"wizard.dash_failed": "Dashbordet kunne ikke opprettes. Alt annet ble det.",
"wizard.next_tablet_title": "Sett det der de ser det",
"wizard.next_tablet": "Åpne dashbordet «Familiens oppgaver» på et nettbrett eller telefonene deres.",
"wizard.next_tablet": "Åpne dashbordet «Barnas oppgaver» på et nettbrett eller telefonene deres.",
"wizard.next_today_title": "Følg med under I dag",
"wizard.next_today": "Godkjenninger, hvem som har gjort hva og raske bonuser, alt på én side.",
"wizard.next_change_title": "Endre hva som helst",
"wizard.next_change": "Rediger oppgaver, poeng og belønninger når som helst. Kjør veiviseren igjen fra Innstillinger.",
"wizard.open_dashboard": "Åpne dashbordet",
"wizard.go_today": "Gå til I dag",
"wizard.confirm_title": "Kjøre oppsettveiviseren?",
"wizard.confirm_have": "Du har allerede {children}, {chores} og {rewards}. Veiviseren vil:",
@@ -2598,6 +2657,8 @@
"kiosk.editor.show_points": "Poeng og nivå",
"kiosk.editor.show_streak": "Rekke",
"kiosk.editor.show_reward_progress": "Fremgang mot neste belønning",
"kiosk.editor.show_bounties": "Dusørtavle",
"kiosk.editor.show_bounties_helper": "La hvert barn ta og fullføre dusører her, med bilde fra nettbrettet når det trengs",
"kiosk.editor.show_picker_progress": "Fremgang i velgeren",
"kiosk.editor.show_picker_progress_helper": "«3 av 6 ferdig» under hvert ansikt",
"kiosk.editor.play_sounds": "Lyder",
+71 -10
View File
@@ -912,7 +912,7 @@
"wizard.next_welcome": "Set i gang",
"wizard.next_children": "Neste: oppgåver",
"wizard.next_chores": "Neste: premiar",
"wizard.next_rewards": "Neste: sjå over",
"wizard.next_rewards": "Neste: dashbord",
"wizard.create": "Opprett alt",
"wizard.create_existing": "Legg til alt",
"wizard.creating": "Opprettar…",
@@ -955,7 +955,7 @@
"wizard.ready_for_chores": "Klar for oppgåver",
"wizard.preview_chores": "Oppgåvene blir viste her",
"wizard.preview_rewards": "og premiar å spare til",
"wizard.preview_hint": "Fargen vert brukt på kortet i dashbordet «Familieoppgåver». Byt avatar eller legg til eit bilete seinare under Born.",
"wizard.preview_hint": "Fargen vert brukt på kortet i dashbordet «Oppgåvene til borna». Byt avatar eller legg til eit bilete seinare under Born.",
"wizard.age_n": "{age} år",
"wizard.group_chores": "Oppgåver for {group}",
"wizard.toast_child_exists": "{name} står allereie på lista",
@@ -1024,27 +1024,86 @@
"wizard.appr_none": "Ingen",
"wizard.appr_big": "Store (4+)",
"wizard.appr_all": "Alle",
"wizard.dash_toggle": "Opprett dashbordet «Familieoppgåver»",
"wizard.dash_hint": "Eitt bornekort per born pluss premiekortet. Vert hoppa over om dashbordet finst frå før.",
"wizard.step_dashboards": "Dashbord",
"wizard.next_dashboards": "Neste: sjå over",
"wizard.dash_step_title": "Vel dashborda dine",
"wizard.dash_step_copy": "Ferdige dashbord for alle i huset, med borna dine allereie fylte inn. Vel nokre, alle eller ingen.",
"wizard.dash_step_later": "Du kan lage fleire når som helst under Innstillingar → Ferdige dashbord. Eit eksisterande dashbord vert aldri overskrive.",
"wizard.count_dashboards_one": "1 dashbord",
"wizard.count_dashboards_many": "{count} dashbord",
"wizard.dash_existed": "«{title}» finst allereie, så det vart late vere som det er.",
"wizard.dash_failed": "«{title}» kunne ikkje opprettast. Alt anna vart det.",
"wizard.open_named": "Opne {title}",
"wizard.review_dashboards": "Dashbord",
"wizard.review_no_dashboards": "Ingen",
"wizard.review_dashboards_only": "Ingenting nytt for familien denne gongen, berre dashborda du valde.",
"panel.dash_section_title": "Ferdige dashbord",
"panel.dash_section_hint": "Start med eit dashbord laga for kvar person i huset, med borna dine fylte inn. Etterpå er det eit vanleg Home Assistant-dashbord: rediger, flytt rundt på eller slett det som du vil. Eit eksisterande dashbord vert aldri overskrive.",
"panel.dash_kind_kids": "Born",
"panel.dash_kind_parents": "Foreldre",
"panel.dash_kind_wall": "Veggnettbrett",
"panel.dash_desc_kids": "Ei fane for kvart barn med oppgåvene, premiemålet, rekkja, merka og premiane deira, pluss ei familiefane med toppliste og familiemål.",
"panel.dash_desc_parents": "Godkjenningar og oppgåvetavla først, så poeng, bonusar og straffer, så vekeoversikter og aktivitet.",
"panel.dash_desc_wall": "Kioskkortet i fullskjerm for eit felles nettbrett, med oppgåvetavla som ei andre fane.",
"panel.dash_for_kids": "For nettbrettet eller telefonane til borna",
"panel.dash_for_parents": "For telefonane til foreldra",
"panel.dash_for_wall": "For eit nettbrett på kjøkenet eller i gangen",
"panel.dash_created": "Oppretta",
"panel.dash_open": "Opne",
"panel.dash_create_btn": "Opprett …",
"panel.dash_dialog_title": "Opprett eit dashbord",
"panel.dash_title_label": "Tittel",
"panel.dash_path_label": "Adresse",
"panel.dash_path_hint": "Små bokstavar, tal og bindestrekar, med minst éin bindestrek.",
"panel.dash_path_taken": "Allereie i bruk: {paths}. Vel ei anna adresse.",
"panel.dash_path_invalid": "Adressa kan berre ha små bokstavar, tal og bindestrekar, med minst éin bindestrek.",
"panel.dash_list_failed": "Klarte ikkje å lese dei eksisterande dashborda dine, så ingenting vart oppretta.",
"panel.dash_children_label": "Born",
"panel.dash_layout_label": "Oppsett",
"panel.dash_layout_tabs": "Eitt dashbord, ei fane per barn",
"panel.dash_layout_each": "Eitt dashbord per barn",
"panel.dash_layout_tabs_hint": "Bra for eit felles nettbrett: kvart barn er berre ei fane unna.",
"panel.dash_layout_each_hint": "Bra for telefonen til barnet: kvart barn får sitt eige dashbord, på denne adressa etterfølgd av namnet sitt.",
"panel.dash_hide_parent": "Gøym foreldreknappar",
"panel.dash_hide_parent_hint": "Godkjenning og fullføring for eit barn vert haldne av på desse korta, også når ein forelder ser på.",
"panel.dash_admin": "Berre administratorar",
"panel.dash_admin_hint": "Gøymer det for brukarar som ikkje er Home Assistant-administratorar. La det vere av om ein forelder ikkje er administrator; korta sjekkar uansett foreldrerettar før kvar handling.",
"panel.dash_board": "Legg til ei fane med oppgåvetavla",
"panel.dash_board_hint": "Heile dagen til familien med eitt blikk, berre eit sveip unna.",
"panel.dash_sidebar": "Vis i sidepanelet",
"panel.dash_preview": "Førehandsvising",
"panel.dash_create": "Opprett dashbord",
"panel.dash_need_child": "Legg til eit barn først.",
"panel.dash_pick_child": "Vel minst eitt barn.",
"panel.dash_need_title": "Gje dashbordet ein tittel.",
"panel.dash_toast_created": "Oppretta: {titles}",
"panel.dash_toast_failed": "Kunne ikkje opprette: {titles}",
"panel.dash_more": "… og {count} til",
"panel.dash_palette": "Opprett eit dashbord: {kind}",
"panel.dash_view_today": "I dag",
"panel.dash_view_points": "Poeng",
"panel.dash_view_insights": "Innsikt",
"panel.dash_view_family": "Familie",
"panel.dash_card_kiosk": "Kiosk",
"panel.dash_card_chores": "Oppgåvene til {name}",
"panel.dash_title_kids": "Oppgåvene til borna",
"panel.dash_title_parents": "Foreldre",
"panel.dash_title_wall": "Oppgåvevegg",
"wizard.notify_toggle": "Gje meg beskjed når noko må godkjennast",
"wizard.notify_hint": "Brukar dei eksisterande varselinnstillingane dine.",
"wizard.rewards_n": "Premiar ({count})",
"wizard.no_rewards": "Ingen premiar valde",
"wizard.dash_title": "Familieoppgåver",
"wizard.done_title": "Alt er klart!",
"wizard.done_copy": "TaskMate er klar for familien din.",
"wizard.done_copy_existing": "Dei nye oppgåvene og premiane er lagde til. Ingenting anna er endra.",
"wizard.done_copy_dashboards": "Dashborda dine er klare. Ingenting anna vart endra.",
"wizard.done_copy_nothing": "Alt du valde, fanst allereie, så ingenting nytt vart lagt til. Ingenting anna er endra.",
"wizard.one_dashboard": "1 dashbord",
"wizard.dash_existed": "Eit dashbord «Familieoppgåver» finst allereie, så det vart late vere som det er.",
"wizard.dash_failed": "Dashbordet kunne ikkje opprettast. Alt anna vart det.",
"wizard.next_tablet_title": "Set det der dei ser det",
"wizard.next_tablet": "Opne dashbordet «Familieoppgåver» på eit nettbrett eller telefonane deira.",
"wizard.next_tablet": "Opne dashbordet «Oppgåvene til borna» på eit nettbrett eller telefonane deira.",
"wizard.next_today_title": "Følg med under I dag",
"wizard.next_today": "Godkjenningar, kven som har gjort kva og raske bonusar, alt på éi side.",
"wizard.next_change_title": "Endre kva som helst",
"wizard.next_change": "Rediger oppgåver, poeng og premiar når som helst. Køyr vegvisaren att frå Innstillingar.",
"wizard.open_dashboard": "Opne dashbordet",
"wizard.go_today": "Gå til I dag",
"wizard.confirm_title": "Køyre oppsettsvegvisaren?",
"wizard.confirm_have": "Du har allereie {children}, {chores} og {rewards}. Vegvisaren vil:",
@@ -2598,6 +2657,8 @@
"kiosk.editor.show_points": "Poeng og nivå",
"kiosk.editor.show_streak": "Rekke",
"kiosk.editor.show_reward_progress": "Framgang mot neste løning",
"kiosk.editor.show_bounties": "Dusørtavle",
"kiosk.editor.show_bounties_helper": "La kvart barn ta og fullføre dusørar her, med bilete frå nettbrettet når det trengst",
"kiosk.editor.show_picker_progress": "Framgang i veljaren",
"kiosk.editor.show_picker_progress_helper": "«3 av 6 ferdig» under kvart andlet",
"kiosk.editor.play_sounds": "Lydar",
+71 -10
View File
@@ -962,7 +962,7 @@
"wizard.next_welcome": "Zaczynamy",
"wizard.next_children": "Dalej: obowiązki",
"wizard.next_chores": "Dalej: nagrody",
"wizard.next_rewards": "Dalej: podsumowanie",
"wizard.next_rewards": "Dalej: dashboardy",
"wizard.create": "Utwórz wszystko",
"wizard.create_existing": "Dodaj wszystko",
"wizard.creating": "Tworzenie…",
@@ -1005,7 +1005,7 @@
"wizard.ready_for_chores": "Gotowe do obowiązków",
"wizard.preview_chores": "Tutaj pojawią się obowiązki",
"wizard.preview_rewards": "i nagrody, na które można oszczędzać",
"wizard.preview_hint": "Kolor pojawi się na karcie dziecka na dashboardzie „Obowiązki rodzinne”. Awatar możesz zmienić lub dodać zdjęcie później w sekcji Dzieci.",
"wizard.preview_hint": "Kolor pojawi się na karcie dziecka na dashboardzie „Obowiązki dzieci”. Awatar możesz zmienić lub dodać zdjęcie później w sekcji Dzieci.",
"wizard.age_n": "Wiek: {age}",
"wizard.group_chores": "Obowiązki: {group}",
"wizard.toast_child_exists": "{name} jest już na liście",
@@ -1074,27 +1074,86 @@
"wizard.appr_none": "Żadne",
"wizard.appr_big": "Duże zadania (4+)",
"wizard.appr_all": "Wszystkie",
"wizard.dash_toggle": "Utwórz dashboard „Obowiązki rodzinne”",
"wizard.dash_hint": "Jedna karta na każde dziecko plus karta nagród. Pomijane, jeśli dashboard już istnieje.",
"wizard.step_dashboards": "Dashboardy",
"wizard.next_dashboards": "Dalej: podsumowanie",
"wizard.dash_step_title": "Wybierz dashboardy",
"wizard.dash_step_copy": "Gotowe dashboardy dla wszystkich domowników, z już uzupełnionymi dziećmi. Wybierz dowolne, wszystkie albo żaden.",
"wizard.dash_step_later": "Kolejne możesz utworzyć w każdej chwili w Ustawienia → Gotowe dashboardy. Istniejący dashboard nigdy nie zostanie nadpisany.",
"wizard.count_dashboards_one": "1 dashboard",
"wizard.count_dashboards_many": "dashboardy: {count}",
"wizard.dash_existed": "„{title}” już istnieje, więc pozostał bez zmian.",
"wizard.dash_failed": "Nie udało się utworzyć „{title}”. Cała reszta została utworzona.",
"wizard.open_named": "Otwórz {title}",
"wizard.review_dashboards": "Dashboardy",
"wizard.review_no_dashboards": "Brak",
"wizard.review_dashboards_only": "Tym razem nic nowego dla rodziny, tylko wybrane dashboardy.",
"panel.dash_section_title": "Gotowe dashboardy",
"panel.dash_section_hint": "Zacznij od dashboardu przygotowanego dla każdego domownika, z już uzupełnionymi dziećmi. Potem to zwykły dashboard Home Assistant: edytuj, przestawiaj lub usuń go według uznania. Istniejący dashboard nigdy nie zostanie nadpisany.",
"panel.dash_kind_kids": "Dzieci",
"panel.dash_kind_parents": "Rodzice",
"panel.dash_kind_wall": "Tablet na ścianie",
"panel.dash_desc_kids": "Zakładka dla każdego dziecka z jego obowiązkami, celem nagrody, serią, odznakami i nagrodami oraz zakładka rodzinna z rankingiem i celem rodzinnym.",
"panel.dash_desc_parents": "Najpierw zatwierdzenia i tablica obowiązków, potem punkty, bonusy i kary, a na końcu wykresy tygodniowe i aktywność.",
"panel.dash_desc_wall": "Karta kiosku na pełnym ekranie dla wspólnego tabletu, z tablicą obowiązków jako drugą zakładką.",
"panel.dash_for_kids": "Na tablet lub telefony dzieci",
"panel.dash_for_parents": "Na telefony rodziców",
"panel.dash_for_wall": "Na tablet w kuchni lub przedpokoju",
"panel.dash_created": "Utworzono",
"panel.dash_open": "Otwórz",
"panel.dash_create_btn": "Utwórz…",
"panel.dash_dialog_title": "Utwórz dashboard",
"panel.dash_title_label": "Tytuł",
"panel.dash_path_label": "Adres",
"panel.dash_path_hint": "Małe litery, cyfry i myślniki, z co najmniej jednym myślnikiem.",
"panel.dash_path_taken": "Już zajęte: {paths}. Wybierz inny adres.",
"panel.dash_path_invalid": "Adres może zawierać tylko małe litery, cyfry i myślniki, z co najmniej jednym myślnikiem.",
"panel.dash_list_failed": "Nie udało się odczytać istniejących dashboardów, więc nic nie utworzono.",
"panel.dash_children_label": "Dzieci",
"panel.dash_layout_label": "Układ",
"panel.dash_layout_tabs": "Jeden dashboard, zakładka dla każdego",
"panel.dash_layout_each": "Dashboard dla każdego dziecka",
"panel.dash_layout_tabs_hint": "Dobre na wspólny tablet: każde dziecko jest o jedną zakładkę dalej.",
"panel.dash_layout_each_hint": "Dobre na własny telefon dziecka: każde dziecko dostaje własny dashboard pod tym adresem z dopisanym imieniem.",
"panel.dash_hide_parent": "Ukryj przyciski rodzica",
"panel.dash_hide_parent_hint": "Zatwierdzanie i wykonywanie za dziecko pozostają wyłączone na tych kartach, nawet gdy patrzy rodzic.",
"panel.dash_admin": "Tylko administratorzy",
"panel.dash_admin_hint": "Ukrywa go przed użytkownikami, którzy nie są administratorami Home Assistant. Zostaw wyłączone, jeśli rodzic nie jest administratorem; karty i tak sprawdzają uprawnienia rodzica przed każdą akcją.",
"panel.dash_board": "Dodaj zakładkę z tablicą obowiązków",
"panel.dash_board_hint": "Dzień całej rodziny w jednym spojrzeniu, jedno przesunięcie palcem dalej.",
"panel.dash_sidebar": "Pokaż na pasku bocznym",
"panel.dash_preview": "Podgląd",
"panel.dash_create": "Utwórz dashboard",
"panel.dash_need_child": "Najpierw dodaj dziecko.",
"panel.dash_pick_child": "Wybierz co najmniej jedno dziecko.",
"panel.dash_need_title": "Nadaj dashboardowi tytuł.",
"panel.dash_toast_created": "Utworzono: {titles}",
"panel.dash_toast_failed": "Nie udało się utworzyć: {titles}",
"panel.dash_more": "…i jeszcze {count}",
"panel.dash_palette": "Utwórz dashboard: {kind}",
"panel.dash_view_today": "Dziś",
"panel.dash_view_points": "Punkty",
"panel.dash_view_insights": "Statystyki",
"panel.dash_view_family": "Rodzina",
"panel.dash_card_kiosk": "Kiosk",
"panel.dash_card_chores": "Obowiązki: {name}",
"panel.dash_title_kids": "Obowiązki dzieci",
"panel.dash_title_parents": "Rodzice",
"panel.dash_title_wall": "Ściana obowiązków",
"wizard.notify_toggle": "Powiadamiaj mnie, gdy coś wymaga zatwierdzenia",
"wizard.notify_hint": "Używa Twoich obecnych ustawień powiadomień.",
"wizard.rewards_n": "Nagrody ({count})",
"wizard.no_rewards": "Nie wybrano nagród",
"wizard.dash_title": "Obowiązki rodzinne",
"wizard.done_title": "Wszystko gotowe!",
"wizard.done_copy": "TaskMate jest gotowy dla Twojej rodziny.",
"wizard.done_copy_existing": "Nowe obowiązki i nagrody zostały dodane. Nic innego nie zostało zmienione.",
"wizard.done_copy_dashboards": "Twoje dashboardy są gotowe. Nic więcej nie zmieniono.",
"wizard.done_copy_nothing": "Wszystko, co wybrano, już istniało, więc nic nowego nie zostało dodane. Nic innego nie zostało zmienione.",
"wizard.one_dashboard": "1 dashboard",
"wizard.dash_existed": "Dashboard „Obowiązki rodzinne” już istnieje, więc pozostał bez zmian.",
"wizard.dash_failed": "Nie udało się utworzyć dashboardu. Wszystko inne zostało utworzone.",
"wizard.next_tablet_title": "Umieść go tam, gdzie dzieci go zobaczą",
"wizard.next_tablet": "Otwórz dashboard „Obowiązki rodzinne” na tablecie lub telefonach dzieci.",
"wizard.next_tablet": "Otwórz dashboard „Obowiązki dzieci” na tablecie lub telefonach dzieci.",
"wizard.next_today_title": "Zaglądaj do widoku Dziś",
"wizard.next_today": "Zatwierdzenia, kto co zrobił i szybkie premie — wszystko na jednej stronie.",
"wizard.next_change_title": "Zmieniaj, co chcesz",
"wizard.next_change": "Edytuj obowiązki, punkty i nagrody w dowolnym momencie. Kreator możesz uruchomić ponownie w Ustawieniach.",
"wizard.open_dashboard": "Otwórz dashboard",
"wizard.go_today": "Przejdź do Dziś",
"wizard.confirm_title": "Uruchomić kreator konfiguracji?",
"wizard.confirm_have": "Masz już: {children}, {chores} i {rewards}. Kreator:",
@@ -2598,6 +2657,8 @@
"kiosk.editor.show_points": "Punkty i poziom",
"kiosk.editor.show_streak": "Seria",
"kiosk.editor.show_reward_progress": "Postęp do następnej nagrody",
"kiosk.editor.show_bounties": "Tablica zleceń",
"kiosk.editor.show_bounties_helper": "Każde dziecko może tu brać i kończyć zlecenia, ze zdjęciem z tabletu, gdy jest wymagane",
"kiosk.editor.show_picker_progress": "Postęp na ekranie wyboru",
"kiosk.editor.show_picker_progress_helper": "„Zrobione: 3 z 6” pod każdą buzią",
"kiosk.editor.play_sounds": "Dźwięki",
@@ -912,7 +912,7 @@
"wizard.next_welcome": "Vamos lá",
"wizard.next_children": "Próximo: tarefas",
"wizard.next_chores": "Próximo: recompensas",
"wizard.next_rewards": "Próximo: revisar",
"wizard.next_rewards": "Próximo: painéis",
"wizard.create": "Criar tudo",
"wizard.create_existing": "Adicionar tudo",
"wizard.creating": "Criando…",
@@ -955,7 +955,7 @@
"wizard.ready_for_chores": "Pronta para tarefas",
"wizard.preview_chores": "As tarefas aparecem aqui",
"wizard.preview_rewards": "e recompensas para economizar",
"wizard.preview_hint": "A cor é usada no cartão do painel \"Tarefas da família\". Troque o avatar ou adicione uma foto depois em Crianças.",
"wizard.preview_hint": "A cor é usada no cartão do painel \"Tarefas das crianças\". Troque o avatar ou adicione uma foto depois em Crianças.",
"wizard.age_n": "{age} anos",
"wizard.group_chores": "Tarefas dos {group}",
"wizard.toast_child_exists": "{name} já está na lista",
@@ -1024,27 +1024,86 @@
"wizard.appr_none": "Nenhuma",
"wizard.appr_big": "Grandes (4+)",
"wizard.appr_all": "Todas",
"wizard.dash_toggle": "Criar um painel \"Tarefas da família\"",
"wizard.dash_hint": "Um cartão por criança mais o cartão de recompensas. Ignorado se o painel já existir.",
"wizard.step_dashboards": "Painéis",
"wizard.next_dashboards": "Próximo: revisar",
"wizard.dash_step_title": "Escolha seus painéis",
"wizard.dash_step_copy": "Painéis prontos para todo mundo da casa, com seus filhos já preenchidos. Escolha alguns, todos ou nenhum.",
"wizard.dash_step_later": "Você pode criar mais a qualquer momento em Configurações → Painéis prontos. Um painel existente nunca é substituído.",
"wizard.count_dashboards_one": "1 painel",
"wizard.count_dashboards_many": "{count} painéis",
"wizard.dash_existed": "\"{title}\" já existe, então ele foi deixado como está.",
"wizard.dash_failed": "Não foi possível criar \"{title}\". Todo o resto foi criado.",
"wizard.open_named": "Abrir {title}",
"wizard.review_dashboards": "Painéis",
"wizard.review_no_dashboards": "Nenhum",
"wizard.review_dashboards_only": "Desta vez nada de novo para a família: só os painéis que você escolheu.",
"panel.dash_section_title": "Painéis prontos",
"panel.dash_section_hint": "Comece com um painel pensado para cada pessoa da casa, com seus filhos já preenchidos. Depois ele é um painel comum do Home Assistant: edite, reorganize ou exclua como quiser. Um painel existente nunca é substituído.",
"panel.dash_kind_kids": "Crianças",
"panel.dash_kind_parents": "Pais",
"panel.dash_kind_wall": "Tablet de parede",
"panel.dash_desc_kids": "Uma aba por criança com as tarefas, a meta de recompensa, a sequência, as medalhas e as recompensas dela, mais uma aba da família com o ranking e a meta da família.",
"panel.dash_desc_parents": "Primeiro as aprovações e o quadro de tarefas, depois pontos, bônus e penalidades, e por fim os gráficos da semana e a atividade.",
"panel.dash_desc_wall": "O cartão de quiosque em tela cheia para um tablet compartilhado, com o quadro de tarefas como segunda aba.",
"panel.dash_for_kids": "Para o tablet ou os celulares das crianças",
"panel.dash_for_parents": "Para os celulares dos pais",
"panel.dash_for_wall": "Para um tablet na cozinha ou no corredor",
"panel.dash_created": "Criado",
"panel.dash_open": "Abrir",
"panel.dash_create_btn": "Criar…",
"panel.dash_dialog_title": "Criar um painel",
"panel.dash_title_label": "Título",
"panel.dash_path_label": "Endereço",
"panel.dash_path_hint": "Letras minúsculas, números e hífens, com pelo menos um hífen.",
"panel.dash_path_taken": "Já em uso: {paths}. Escolha outro endereço.",
"panel.dash_path_invalid": "O endereço só pode ter letras minúsculas, números e hífens, com pelo menos um hífen.",
"panel.dash_list_failed": "Não foi possível ler seus painéis existentes, então nada foi criado.",
"panel.dash_children_label": "Crianças",
"panel.dash_layout_label": "Layout",
"panel.dash_layout_tabs": "Um painel, uma aba por criança",
"panel.dash_layout_each": "Um painel por criança",
"panel.dash_layout_tabs_hint": "Bom para um tablet compartilhado: cada criança fica a uma aba de distância.",
"panel.dash_layout_each_hint": "Bom para o celular da própria criança: cada uma ganha o próprio painel, neste endereço seguido do nome dela.",
"panel.dash_hide_parent": "Ocultar botões dos pais",
"panel.dash_hide_parent_hint": "Aprovar e concluir por uma criança ficam desligados nesses cartões, mesmo quando um dos pais está olhando.",
"panel.dash_admin": "Somente administradores",
"panel.dash_admin_hint": "Oculta o painel de usuários que não são administradores do Home Assistant. Deixe desligado se um dos pais não for administrador; os cartões sempre verificam os direitos de pai/mãe antes de qualquer ação.",
"panel.dash_board": "Adicionar uma aba com o quadro de tarefas",
"panel.dash_board_hint": "O dia da família inteira num relance, a um deslizar de distância.",
"panel.dash_sidebar": "Mostrar na barra lateral",
"panel.dash_preview": "Prévia",
"panel.dash_create": "Criar painel",
"panel.dash_need_child": "Adicione uma criança primeiro.",
"panel.dash_pick_child": "Escolha pelo menos uma criança.",
"panel.dash_need_title": "Dê um título ao painel.",
"panel.dash_toast_created": "Criado: {titles}",
"panel.dash_toast_failed": "Não foi possível criar: {titles}",
"panel.dash_more": "…e mais {count}",
"panel.dash_palette": "Criar um painel: {kind}",
"panel.dash_view_today": "Hoje",
"panel.dash_view_points": "Pontos",
"panel.dash_view_insights": "Estatísticas",
"panel.dash_view_family": "Família",
"panel.dash_card_kiosk": "Quiosque",
"panel.dash_card_chores": "Tarefas de {name}",
"panel.dash_title_kids": "Tarefas das crianças",
"panel.dash_title_parents": "Pais",
"panel.dash_title_wall": "Mural de tarefas",
"wizard.notify_toggle": "Me avisar quando algo precisar de aprovação",
"wizard.notify_hint": "Usa suas configurações de Notificações existentes.",
"wizard.rewards_n": "Recompensas ({count})",
"wizard.no_rewards": "Nenhuma recompensa escolhida",
"wizard.dash_title": "Tarefas da família",
"wizard.done_title": "Tudo pronto!",
"wizard.done_copy": "O TaskMate está pronto para a sua família.",
"wizard.done_copy_existing": "As novas tarefas e recompensas foram adicionadas. Nada mais foi alterado.",
"wizard.done_copy_dashboards": "Seus painéis estão prontos. Nada mais foi alterado.",
"wizard.done_copy_nothing": "Tudo o que você escolheu já existia, então nada novo foi adicionado. Nada mais foi alterado.",
"wizard.one_dashboard": "1 painel",
"wizard.dash_existed": "Já existe um painel \"Tarefas da família\", então ele foi deixado como está.",
"wizard.dash_failed": "Não foi possível criar o painel. Tudo o resto foi criado.",
"wizard.next_tablet_title": "Coloque onde eles vejam",
"wizard.next_tablet": "Abra o painel \"Tarefas da família\" em um tablet ou nos celulares deles.",
"wizard.next_tablet": "Abra o painel \"Tarefas das crianças\" em um tablet ou nos celulares deles.",
"wizard.next_today_title": "Acompanhe em Hoje",
"wizard.next_today": "Aprovações, quem fez o quê e bônus rápidos, tudo em uma página.",
"wizard.next_change_title": "Mude o que quiser",
"wizard.next_change": "Edite tarefas, pontos e recompensas a qualquer momento. Execute isto de novo em Configurações.",
"wizard.open_dashboard": "Abrir o painel",
"wizard.go_today": "Ir para Hoje",
"wizard.confirm_title": "Executar o assistente de configuração?",
"wizard.confirm_have": "Você já tem {children}, {chores} e {rewards}. O assistente vai:",
@@ -2598,6 +2657,8 @@
"kiosk.editor.show_points": "Pontos e nível",
"kiosk.editor.show_streak": "Sequência",
"kiosk.editor.show_reward_progress": "Progresso até a próxima recompensa",
"kiosk.editor.show_bounties": "Tarefas extras",
"kiosk.editor.show_bounties_helper": "Cada criança pode pegar e concluir tarefas extras aqui, com uma foto do tablet quando for pedida",
"kiosk.editor.show_picker_progress": "Progresso na tela de escolha",
"kiosk.editor.show_picker_progress_helper": "“3 de 6 feitas” embaixo de cada rosto",
"kiosk.editor.play_sounds": "Sons",
+71 -10
View File
@@ -917,7 +917,7 @@
"wizard.next_welcome": "Vamos lá",
"wizard.next_children": "Seguinte: tarefas",
"wizard.next_chores": "Seguinte: recompensas",
"wizard.next_rewards": "Seguinte: rever",
"wizard.next_rewards": "Seguinte: painéis",
"wizard.create": "Criar tudo",
"wizard.create_existing": "Adicionar tudo",
"wizard.creating": "A criar…",
@@ -960,7 +960,7 @@
"wizard.ready_for_chores": "Pronta para tarefas",
"wizard.preview_chores": "As tarefas aparecem aqui",
"wizard.preview_rewards": "e recompensas para poupar",
"wizard.preview_hint": "A cor é usada no cartão do painel \"Tarefas da família\". Mude o avatar ou adicione uma foto mais tarde em Crianças.",
"wizard.preview_hint": "A cor é usada no cartão do painel \"Tarefas das crianças\". Mude o avatar ou adicione uma foto mais tarde em Crianças.",
"wizard.age_n": "{age} anos",
"wizard.group_chores": "Tarefas dos {group}",
"wizard.toast_child_exists": "{name} já está na lista",
@@ -1029,27 +1029,86 @@
"wizard.appr_none": "Nenhuma",
"wizard.appr_big": "Grandes (4+)",
"wizard.appr_all": "Todas",
"wizard.dash_toggle": "Criar um painel \"Tarefas da família\"",
"wizard.dash_hint": "Um cartão por criança mais o cartão de recompensas. Ignorado se o painel já existir.",
"wizard.step_dashboards": "Painéis",
"wizard.next_dashboards": "Seguinte: rever",
"wizard.dash_step_title": "Escolha os seus painéis",
"wizard.dash_step_copy": "Painéis prontos a usar para todos lá em casa, com os seus filhos já preenchidos. Escolha alguns, todos ou nenhum.",
"wizard.dash_step_later": "Pode criar mais a qualquer momento em Definições → Painéis prontos a usar. Um painel existente nunca é substituído.",
"wizard.count_dashboards_one": "1 painel",
"wizard.count_dashboards_many": "{count} painéis",
"wizard.dash_existed": "\"{title}\" já existe, por isso foi deixado como está.",
"wizard.dash_failed": "Não foi possível criar \"{title}\". Tudo o resto foi criado.",
"wizard.open_named": "Abrir {title}",
"wizard.review_dashboards": "Painéis",
"wizard.review_no_dashboards": "Nenhum",
"wizard.review_dashboards_only": "Desta vez nada de novo para a família: só os painéis que escolheu.",
"panel.dash_section_title": "Painéis prontos a usar",
"panel.dash_section_hint": "Comece com um painel pensado para cada pessoa lá de casa, com os seus filhos já preenchidos. Depois é um painel normal do Home Assistant: edite, reorganize ou elimine-o como quiser. Um painel existente nunca é substituído.",
"panel.dash_kind_kids": "Crianças",
"panel.dash_kind_parents": "Pais",
"panel.dash_kind_wall": "Tablet de parede",
"panel.dash_desc_kids": "Um separador por criança com as suas tarefas, objetivo de recompensa, sequência, medalhas e recompensas, e um separador da família com a classificação e o objetivo da família.",
"panel.dash_desc_parents": "Primeiro as aprovações e o quadro de tarefas, depois pontos, bónus e penalizações, e por fim os gráficos da semana e a atividade.",
"panel.dash_desc_wall": "O cartão de quiosque em ecrã inteiro para um tablet partilhado, com o quadro de tarefas num segundo separador.",
"panel.dash_for_kids": "Para o tablet ou os telemóveis das crianças",
"panel.dash_for_parents": "Para os telemóveis dos pais",
"panel.dash_for_wall": "Para um tablet na cozinha ou no corredor",
"panel.dash_created": "Criado",
"panel.dash_open": "Abrir",
"panel.dash_create_btn": "Criar…",
"panel.dash_dialog_title": "Criar um painel",
"panel.dash_title_label": "Título",
"panel.dash_path_label": "Endereço",
"panel.dash_path_hint": "Letras minúsculas, números e hífenes, com pelo menos um hífen.",
"panel.dash_path_taken": "Já em uso: {paths}. Escolha outro endereço.",
"panel.dash_path_invalid": "O endereço só pode ter letras minúsculas, números e hífenes, com pelo menos um hífen.",
"panel.dash_list_failed": "Não foi possível ler os seus painéis existentes, por isso nada foi criado.",
"panel.dash_children_label": "Crianças",
"panel.dash_layout_label": "Disposição",
"panel.dash_layout_tabs": "Um painel, um separador por criança",
"panel.dash_layout_each": "Um painel por criança",
"panel.dash_layout_tabs_hint": "Bom para um tablet partilhado: cada criança está a um separador de distância.",
"panel.dash_layout_each_hint": "Bom para o telemóvel da própria criança: cada uma recebe o seu painel, neste endereço seguido do seu nome.",
"panel.dash_hide_parent": "Ocultar botões dos pais",
"panel.dash_hide_parent_hint": "Aprovar e concluir por uma criança ficam desligados nestes cartões, mesmo quando um dos pais está a ver.",
"panel.dash_admin": "Só administradores",
"panel.dash_admin_hint": "Oculta-o de utilizadores que não são administradores do Home Assistant. Deixe desligado se um dos pais não for administrador; os cartões verificam sempre os direitos de pai/mãe antes de qualquer ação.",
"panel.dash_board": "Adicionar um separador com o quadro de tarefas",
"panel.dash_board_hint": "O dia de toda a família num relance, à distância de um deslizar.",
"panel.dash_sidebar": "Mostrar na barra lateral",
"panel.dash_preview": "Pré-visualização",
"panel.dash_create": "Criar painel",
"panel.dash_need_child": "Adicione primeiro uma criança.",
"panel.dash_pick_child": "Escolha pelo menos uma criança.",
"panel.dash_need_title": "Dê um título ao painel.",
"panel.dash_toast_created": "Criado: {titles}",
"panel.dash_toast_failed": "Não foi possível criar: {titles}",
"panel.dash_more": "…e mais {count}",
"panel.dash_palette": "Criar um painel: {kind}",
"panel.dash_view_today": "Hoje",
"panel.dash_view_points": "Pontos",
"panel.dash_view_insights": "Estatísticas",
"panel.dash_view_family": "Família",
"panel.dash_card_kiosk": "Quiosque",
"panel.dash_card_chores": "Tarefas de {name}",
"panel.dash_title_kids": "Tarefas das crianças",
"panel.dash_title_parents": "Pais",
"panel.dash_title_wall": "Mural de tarefas",
"wizard.notify_toggle": "Avisar-me quando algo precisar de aprovação",
"wizard.notify_hint": "Usa as suas definições de Notificações existentes.",
"wizard.rewards_n": "Recompensas ({count})",
"wizard.no_rewards": "Nenhuma recompensa escolhida",
"wizard.dash_title": "Tarefas da família",
"wizard.done_title": "Está tudo pronto!",
"wizard.done_copy": "O TaskMate está pronto para a sua família.",
"wizard.done_copy_existing": "As novas tarefas e recompensas foram adicionadas. Nada mais foi alterado.",
"wizard.done_copy_dashboards": "Os seus painéis estão prontos. Nada mais foi alterado.",
"wizard.done_copy_nothing": "Tudo o que escolheu já existia, por isso nada de novo foi adicionado. Nada mais foi alterado.",
"wizard.one_dashboard": "1 painel",
"wizard.dash_existed": "Já existe um painel \"Tarefas da família\", por isso foi deixado como está.",
"wizard.dash_failed": "Não foi possível criar o painel. Tudo o resto foi criado.",
"wizard.next_tablet_title": "Ponha-o onde o vejam",
"wizard.next_tablet": "Abra o painel \"Tarefas da família\" num tablet ou nos telemóveis deles.",
"wizard.next_tablet": "Abra o painel \"Tarefas das crianças\" num tablet ou nos telemóveis deles.",
"wizard.next_today_title": "Acompanhe em Hoje",
"wizard.next_today": "Aprovações, quem fez o quê e bónus rápidos, tudo numa página.",
"wizard.next_change_title": "Altere o que quiser",
"wizard.next_change": "Edite tarefas, pontos e recompensas a qualquer momento. Execute isto de novo nas Definições.",
"wizard.open_dashboard": "Abrir o painel",
"wizard.go_today": "Ir para Hoje",
"wizard.confirm_title": "Executar o assistente de configuração?",
"wizard.confirm_have": "Já tem {children}, {chores} e {rewards}. O assistente vai:",
@@ -2598,6 +2657,8 @@
"kiosk.editor.show_points": "Pontos e nível",
"kiosk.editor.show_streak": "Sequência",
"kiosk.editor.show_reward_progress": "Progresso para a próxima recompensa",
"kiosk.editor.show_bounties": "Tarefas extra",
"kiosk.editor.show_bounties_helper": "Cada criança pode reservar e concluir tarefas extra aqui, com uma fotografia do tablet quando for pedida",
"kiosk.editor.show_picker_progress": "Progresso no ecrã de escolha",
"kiosk.editor.show_picker_progress_helper": "“3 de 6 feitas” debaixo de cada cara",
"kiosk.editor.play_sounds": "Sons",
@@ -11,6 +11,11 @@
* no complete-on-behalf, no points adjustments, no reward claiming, no
* settings — there is no option to turn them on.
*
* Bounties (#1054), off unless `show_bounties: true`: the unlocked child sees
* the bounty board's child side — claim, give back, finish (taking a photo
* with the tablet when the bounty asks for one) and undo. Each call names the
* child and runs the same linked-child check as complete_chore.
*
* Each completion gets the child card's feedback (#1039): the chore's
* completion sound, confetti and a short popup — each can be switched off.
*
@@ -60,6 +65,9 @@ const POPUP_MS = 2500;
const CONFETTI_MS = 3500;
const CONFETTI_COLOURS = ["#ff6b9d", "#9b59b6", "#3498db", "#2ecc71", "#f1c40f", "#e67e22"];
const PIN_LENGTH = 4;
// The idle clock waits this long for a bounty photo: the camera covers the
// card, so no tap reaches it while the child lines up the shot.
const PHOTO_GRACE_MS = 120000;
// The same six-colour ring the child card uses for per-child tones.
const CHILD_FALLBACK = ["#ff7043", "#42a5f5", "#66bb6a", "#ab47bc", "#ffa726", "#26c6da"];
@@ -86,6 +94,7 @@ class TaskMateKioskCard extends LitElement {
_popup: { type: Object },
_confetti: { type: Array },
_fullscreen: { type: Boolean },
_bountyDlg: { type: Object },
};
}
@@ -106,6 +115,12 @@ class TaskMateKioskCard extends LitElement {
this._popup = null;
this._confetti = [];
this._fullscreen = false;
this._bountyDlg = null;
// The bounty a photo is being taken for; outlives a return to the picker
// so a photo that comes back late still lands on the right child.
this._photoFor = null;
this._photoSince = 0;
this._uploading = false;
this._now = new Date();
// Any touch or key press counts as activity and restarts the idle timer.
this._onActivity = () => { this._idle = 0; };
@@ -421,6 +436,7 @@ class TaskMateKioskCard extends LitElement {
clearTimeout(this._confettiTimer);
this._popup = null;
this._confetti = [];
this._bountyDlg = null;
this._screen = "picker";
this._kid = null;
this._pin = "";
@@ -519,6 +535,191 @@ class TaskMateKioskCard extends LitElement {
}
}
// ── Bounties (#1054) ─────────────────────────────────────────────────────
// The bounty card's child side for the unlocked child. Every call names the
// child, so the backend's linked-child check decides, as for chores.
_showBounties() { return this.config.show_bounties === true; }
/** What this child may claim, anything they hold, and what they finished. */
_bountiesFor(child) {
const id = String(child.id);
const list = this._attrs().bounties;
if (!Array.isArray(list)) return [];
const shown = list.filter(b => {
const mine = String(b.claimed_by || "") === id;
if (b.status === "completed") return mine;
return mine || !Array.isArray(b.eligible) || !b.eligible.length || b.eligible.map(String).includes(id);
});
return shown.filter(b => b.status !== "completed").concat(shown.filter(b => b.status === "completed"));
}
_msUntil(iso, now) {
const t = Date.parse(iso || "");
return Number.isFinite(t) ? t - now : null;
}
_fmtDur(ms) {
const sec = Math.max(0, Math.round(ms / 1000));
const d = Math.floor(sec / 86400);
const h = Math.floor((sec % 86400) / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = sec % 60;
if (d) return this._t("bounty.dur_days", { d, h });
if (h) return this._t("bounty.dur_hours", { h, m: String(m).padStart(2, "0") });
return this._t("bounty.dur_minutes", { m, s: String(s).padStart(2, "0") });
}
_pointsName() { return this._attrs().points_name || this._t("common.points"); }
async _bountyAction(b, run) {
if (this._busy) return false;
this._busy = `bounty:${b.id}`;
try {
await run();
return true;
} catch (err) {
this._notify(String(err?.message || err));
return false;
} finally {
this._busy = "";
this.requestUpdate();
}
}
async _claimBounty(child, b) {
const ok = await this._bountyAction(b, () =>
this.hass.callService("taskmate", "claim_bounty", { bounty_id: b.id, child_id: child.id }));
if (ok) this._bountyDlg = null;
}
_giveBackBounty(child, b) {
return this._bountyAction(b, () =>
this.hass.callService("taskmate", "give_back_bounty", { bounty_id: b.id, child_id: child.id }));
}
async _finishBounty(child, b) {
if (b.require_photo) {
this._bountyDlg = { kind: "photo", id: b.id };
return;
}
const ok = await this._bountyAction(b, () =>
this.hass.callService("taskmate", "complete_bounty", { bounty_id: b.id, child_id: child.id }));
if (ok) this._bountyDone(b);
}
_undoBounty(b) {
return this._bountyAction(b, () =>
this.hass.callService("taskmate", "undo_chore", { completion_id: b.completion_id }));
}
/** The bounty goes to a grown-up: a sound, confetti, and a note saying so. */
_bountyDone(b) {
if (this.config.play_sounds !== false) this._playSound(this.config.default_sound || "coin");
if (this.config.show_confetti !== false) this._spawnConfetti();
this._bountyDlg = { kind: "yay", id: b.id };
}
/**
* Opens the tablet's camera (or photo library). No `capture` attribute: a
* wall-mounted tablet's rear camera faces the wall, so the child picks.
*/
_pickBountyPhoto(child, b) {
this._photoFor = { childId: String(child.id), bountyId: b.id };
this._photoSince = Date.now();
this._idle = 0;
const input = this.renderRoot && this.renderRoot.querySelector("#km-photo");
if (input) input.click();
}
_photoCancelled() {
this._photoFor = null;
this._photoSince = 0;
}
/** Shrink the photo, upload it, then complete — for the child who asked. */
async _bountyPhotoPicked(e) {
const file = e.target.files && e.target.files[0];
e.target.value = "";
const target = this._photoFor;
this._photoCancelled();
if (!file || !target || this._busy) return;
// The kiosk may have gone back to the picker meanwhile: the completion
// still counts for the child who took the photo, but only their own
// screen shows the result.
const here = () => this._screen === "child" && this._kid === target.childId;
this._busy = `bounty:${target.bountyId}`;
this._uploading = true;
if (here()) this._bountyDlg = { kind: "photo", id: target.bountyId, uploading: true };
try {
const photoUrl = await this._upload(await this._shrink(file));
await this.hass.callService("taskmate", "complete_bounty", {
bounty_id: target.bountyId, child_id: target.childId, photo_url: photoUrl,
});
if (here()) this._bountyDone({ id: target.bountyId });
this._idle = 0;
} catch (err) {
if (here()) {
this._bountyDlg = { kind: "photo", id: target.bountyId, error: this._t("child.photo_upload_failed") };
} else {
this._notify(String(err?.message || err));
}
} finally {
this._busy = "";
this._uploading = false;
this.requestUpdate();
}
}
_shrink(file) {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
const max = 1280;
let { width, height } = img;
if (width > max || height > max) {
const scale = Math.min(max / width, max / height);
width = Math.round(width * scale);
height = Math.round(height * scale);
}
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
canvas.getContext("2d").drawImage(img, 0, 0, width, height);
URL.revokeObjectURL(url);
canvas.toBlob(blob => (blob ? resolve(blob) : reject(new Error("no blob"))), "image/jpeg", 0.8);
};
img.onerror = () => { URL.revokeObjectURL(url); reject(new Error("bad image")); };
img.src = url;
});
}
async _upload(blob) {
// Bearer-authenticated POST, refreshing a stale access token once on 401 —
// the same upload the bounty and child cards use.
const post = () => {
const token = this.hass?.auth?.data?.access_token;
const fd = new FormData();
fd.append("file", blob, "evidence.jpg");
return fetch("/api/taskmate/photo", {
method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : {}, body: fd,
});
};
if (this.hass?.auth?.expired && this.hass.auth.refreshAccessToken) {
try { await this.hass.auth.refreshAccessToken(); } catch (_e) { /* retried below */ }
}
let resp = await post();
if (resp.status === 401 && this.hass?.auth?.refreshAccessToken) {
await this.hass.auth.refreshAccessToken();
resp = await post();
}
if (!resp.ok) throw new Error(`upload failed (${resp.status})`);
const { photo_url: photoUrl } = await resp.json();
if (!photoUrl) throw new Error("no photo_url");
return photoUrl;
}
// ── Completion feedback (#1039) ──────────────────────────────────────────
/** The child card's reward for a tap: the chore's sound, confetti, a popup. */
@@ -578,7 +779,8 @@ class TaskMateKioskCard extends LitElement {
dirty = true;
}
if (this._screen !== "picker") {
this._idle += 1;
const photo = this._uploading || (this._photoSince && Date.now() - this._photoSince < PHOTO_GRACE_MS);
if (!photo) this._idle += 1;
if (this._idle >= this._timeout()) {
this._resetToPicker();
}
@@ -637,6 +839,9 @@ class TaskMateKioskCard extends LitElement {
</button>` : ""}
</div>
<div class="km-main">${body}</div>
${this._showBounties() ? html`
<input id="km-photo" type="file" accept="image/*" hidden
@change=${e => this._bountyPhotoPicked(e)} @cancel=${() => this._photoCancelled()}>` : ""}
${this._confetti.length ? html`
<div class="km-confetti" aria-hidden="true">
${this._confetti.map((p, i) => html`<i style="inset-inline-start:${p.x}%;animation-delay:${p.delay}s;background:${CONFETTI_COLOURS[i % CONFETTI_COLOURS.length]};border-radius:${p.round ? "50%" : "0"};width:${p.size}px;height:${p.size}px"></i>`)}
@@ -781,6 +986,7 @@ class TaskMateKioskCard extends LitElement {
<h3><ha-icon icon="mdi:cellphone"></ha-icon> ${this._t("kiosk.phone_group")}</h3>
${phoneRows.map(row => this._renderRow(child, row, icon))}
</div>` : ""}
${this._showBounties() ? this._renderBounties(child, icon) : ""}
</div>
<div class="km-side">
<div class="km-tile km-center">
@@ -807,6 +1013,7 @@ class TaskMateKioskCard extends LitElement {
</div>
</div>
${this._showBounties() ? this._renderBountyDialog(child) : ""}
${warn ? html`
<div class="km-idle">
<div class="km-idle-n" style="--p:${left / WARN_SECONDS}"><span>${left}</span></div>
@@ -875,6 +1082,123 @@ class TaskMateKioskCard extends LitElement {
`;
}
_renderBounties(child, icon) {
const list = this._bountiesFor(child);
if (!list.length) return "";
const me = String(child.id);
const now = Date.now();
const holding = list.some(b => b.status === "claimed" && String(b.claimed_by) === me);
return html`
<div class="km-bounty-group">
<h3><ha-icon icon="mdi:flag-outline"></ha-icon> ${this._t("bounty.title")}</h3>
${list.map(b => this._renderBountyRow(child, b, me, now, holding, icon))}
</div>
`;
}
_renderBountyRow(child, b, me, now, holding, icon) {
const mine = String(b.claimed_by || "") === me;
const busy = !!this._busy;
const live = b.status === "open" || b.status === "claimed";
const expLeft = live && b.expires_at ? this._msUntil(b.expires_at, now) : null;
const sibling = (this._attrs().children || []).find(c => String(c.id) === String(b.claimed_by))?.name || "?";
const box = (cls, mdi, text) =>
html`<div class="km-done ${cls}"><ha-icon icon="${mdi}"></ha-icon><span>${text}</span></div>`;
let state = b.status;
let action;
let extra = "";
if (b.status === "open") {
action = html`<button class="km-done km-claim" ?disabled=${busy || holding}
@click=${() => { this._bountyDlg = { kind: "claim", id: b.id }; }}>
<ha-icon icon="mdi:hand-back-right-outline"></ha-icon> ${this._t("bounty.claim")}</button>`;
extra = html`<span class="km-dim">${holding ? this._t("bounty.finish_first") : this._t("bounty.claim_hint", { hours: b.claim_hours })}</span>`;
} else if (b.status === "claimed" && mine) {
state = "mine";
action = html`<button class="km-done" ?disabled=${busy} @click=${() => this._finishBounty(child, b)}>
<ha-icon icon="${b.require_photo ? "mdi:camera" : "mdi:check-bold"}"></ha-icon> ${this._t("bounty.done")}</button>`;
extra = html`
<span class="km-claim-left"><ha-icon icon="mdi:timer-sand"></ha-icon> ${this._t("bounty.claimed_by_you", { time: this._fmtDur(this._msUntil(b.claim_until, now) ?? 0) })}</span>
<button class="km-undo" ?disabled=${busy} @click=${() => this._giveBackBounty(child, b)}>${this._t("bounty.give_back")}</button>`;
} else if (b.status === "claimed") {
state = "locked";
action = box("is-elsewhere", "mdi:account-clock", this._t("bounty.sibling_on_it", { name: sibling }));
extra = html`<span class="km-dim">${this._t("bounty.sibling_back_in", { time: this._fmtDur(this._msUntil(b.claim_until, now) ?? 0) })}</span>`;
} else if (b.status === "pending" && mine) {
state = "wait";
action = box("is-wait", "mdi:timer-sand", this._t("kiosk.waiting"));
if (b.undo && b.completion_id) {
extra = html`<button class="km-undo" ?disabled=${busy} aria-label="${this._t("child.undo_named", { name: b.title })}"
@click=${() => this._undoBounty(b)}>
<ha-icon class="tm-rtl-flip" icon="mdi:undo-variant"></ha-icon> ${this._t("kiosk.undo")}</button>`;
}
} else if (b.status === "pending") {
state = "locked";
action = box("is-elsewhere", "mdi:account-clock", this._t("bounty.sibling_checking", { name: sibling }));
} else {
state = "done";
action = box("is-done", "mdi:check-bold", this._t("bounty.you_earned", { points: b.points_awarded ?? b.points, pointsName: this._pointsName() }));
}
return html`
<div class="km-row km-bounty ${state}" data-bounty="${b.id}">
<div class="km-row-icon"><ha-icon icon="${b.icon || "mdi:flag-outline"}"></ha-icon></div>
<div class="km-row-body">
<div class="km-row-name">${b.title}</div>
${b.description ? html`<div class="km-row-desc">${b.description}</div>` : ""}
<div class="km-row-meta">
<span class="km-row-pts"><ha-icon icon="${icon}"></ha-icon> ${b.points}</span>
${expLeft !== null ? html`<span class="km-tag"><ha-icon icon="mdi:clock-outline"></ha-icon> ${this._t("bounty.left", { time: this._fmtDur(expLeft) })}</span>` : ""}
${b.require_photo ? html`<span class="km-tag"><ha-icon icon="mdi:camera-outline"></ha-icon> ${this._t("bounty.photo_tag")}</span>` : ""}
${extra}
</div>
</div>
${action}
</div>
`;
}
_renderBountyDialog(child) {
const d = this._bountyDlg;
if (!d) return "";
const b = this._bountiesFor(child).find(x => x.id === d.id);
if (!b) return "";
const close = () => { if (!d.uploading) this._bountyDlg = null; };
const pointsName = this._pointsName();
let inner;
if (d.kind === "claim") {
inner = html`
<div class="km-bdlg-ic"><ha-icon icon="${b.icon || "mdi:flag-outline"}"></ha-icon></div>
<h2>${this._t("bounty.claim_title", { title: b.title })}</h2>
<p>${this._t("bounty.claim_body", { hours: b.claim_hours })}</p>
<div class="km-bdlg-acts">
<button class="km-ghost" @click=${close}>${this._t("bounty.not_now")}</button>
<button class="km-primary" ?disabled=${!!this._busy} @click=${() => this._claimBounty(child, b)}>
${this._t("bounty.claim_it", { points: b.points, pointsName })}</button>
</div>`;
} else if (d.kind === "photo") {
inner = html`
<div class="km-bdlg-ic"><ha-icon icon="mdi:camera-outline"></ha-icon></div>
<h2>${this._t("bounty.photo_title")}</h2>
<p>${this._t("bounty.photo_body", { title: b.title })}</p>
${d.error ? html`<p class="km-error" role="alert">${d.error}</p>` : ""}
<div class="km-bdlg-acts">
<button class="km-ghost" ?disabled=${!!d.uploading} @click=${close}>${this._t("bounty.cancel")}</button>
<button class="km-primary" ?disabled=${!!d.uploading || !!this._busy} @click=${() => this._pickBountyPhoto(child, b)}>
<ha-icon icon="mdi:camera"></ha-icon> ${d.uploading ? this._t("bounty.uploading") : this._t("bounty.take_photo")}</button>
</div>`;
} else {
inner = html`
<div class="km-bdlg-ic good"><ha-icon icon="mdi:check-bold"></ha-icon></div>
<h2>${this._t("bounty.yay_title")}</h2>
<p>${this._t("bounty.yay_body", { title: b.title, points: b.points, pointsName })}</p>
<div class="km-bdlg-acts"><button class="km-primary" @click=${close}>${this._t("bounty.ok")}</button></div>`;
}
return html`
<div class="km-idle km-bdlg" role="dialog" aria-modal="true" @click=${e => { if (e.target === e.currentTarget) close(); }}>
<div class="km-bdlg-card">${inner}</div>
</div>
`;
}
static get styles() {
// Layout classes carry a km- prefix: the shared design kit defines its own
// .btn and .bar, and its :host([data-tm-design=...]) rules would outrank
@@ -1164,6 +1488,57 @@ class TaskMateKioskCard extends LitElement {
.km-done.is-wait { border: 3px dashed var(--tmd-warn, #f39c12); }
.km-done.is-elsewhere { border: 3px dashed var(--tmd-dim, #8a939d); }
.km-side { display: flex; flex-direction: column; gap: 14px; }
/* Bounties (#1054) */
.km-bounty-group { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
.km-bounty-group h3 { color: var(--tmd-warn, #e67e22); }
.km-row.km-bounty.mine {
border-color: var(--tmd-warn, #e67e22);
background: color-mix(in srgb, var(--tmd-warn, #e67e22) 8%, var(--tmd-surface, var(--card-background-color, #fff)));
}
.km-row.km-bounty.locked { opacity: 0.62; }
.km-row.km-bounty .km-done.is-done {
font-size: 0.9rem; line-height: 1.15; text-align: start; padding: 0 12px; min-width: 180px; max-width: 220px;
}
.km-row.km-bounty .km-row-icon {
background: color-mix(in srgb, var(--tmd-warn, #e67e22) 18%, transparent); color: var(--tmd-warn, #e67e22);
}
.km-row-desc {
font-size: 0.95rem; color: var(--tmd-dim, var(--secondary-text-color)); margin-top: 2px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.km-row-meta .km-tag { display: inline-flex; align-items: center; gap: 4px; }
.km-row-meta .km-tag ha-icon { --mdc-icon-size: 14px; }
.km-row-meta .km-dim { font-size: 0.9rem; }
.km-claim-left {
display: inline-flex; align-items: center; gap: 4px;
font-size: 0.9rem; font-weight: 700; color: var(--tmd-warn, #d68910);
}
.km-claim-left ha-icon { --mdc-icon-size: 16px; }
button.km-done.km-claim { background: var(--tmd-warn, #e67e22); }
:host([data-tm-design]:not([data-tm-design="classic"])) button.km-done.km-claim { color: #2b1700; }
/* The overlay spans the whole child view, which can run far past the
screen outside full screen: the card sticks to where the child is. */
.km-bdlg { z-index: 3; justify-content: flex-start; }
.km-bdlg-card {
position: sticky; top: 15vh; margin-top: 12vh;
max-width: 520px; display: flex; flex-direction: column; align-items: center; gap: 10px;
background: var(--tmd-surface, var(--card-background-color, #fff));
border: 1px solid var(--tmd-border, transparent);
border-radius: calc(var(--tmd-radius, 16px) + 14px); padding: 30px 36px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
}
.km-bdlg .km-bdlg-card h2 { font-size: 1.7rem; }
.km-bdlg .km-bdlg-card p { max-width: 440px; }
.km-bdlg-ic {
width: 72px; height: 72px; border-radius: 50%; display: grid; place-items: center;
background: color-mix(in srgb, var(--tmd-warn, #e67e22) 18%, transparent); color: var(--tmd-warn, #e67e22);
}
.km-bdlg-ic.good { background: color-mix(in srgb, var(--tmd-good, #27ae60) 18%, transparent); color: var(--tmd-good, #27ae60); }
.km-bdlg-ic ha-icon { --mdc-icon-size: 40px; }
.km-bdlg-acts { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 8px; }
.km-bdlg-acts .km-ghost { padding: 14px 24px; font-size: 1.1rem; border-radius: 40px; }
.km-primary:disabled, .km-ghost:disabled { opacity: 0.5; cursor: default; }
.km-tile {
background: var(--tmd-surface, var(--card-background-color, #fff));
border: 1px solid var(--tmd-border, var(--divider-color, transparent));
@@ -1315,6 +1690,7 @@ class TaskMateKioskCardEditor extends LitElement {
{ name: "show_points", selector: { boolean: {} } },
{ name: "show_streak", selector: { boolean: {} } },
{ name: "show_reward_progress", selector: { boolean: {} } },
{ name: "show_bounties", selector: { boolean: {} } },
{ name: "show_picker_progress", selector: { boolean: {} } },
{ name: "play_sounds", selector: { boolean: {} } },
{ name: "show_confetti", selector: { boolean: {} } },
@@ -1331,6 +1707,7 @@ class TaskMateKioskCardEditor extends LitElement {
show_points: this._t("kiosk.editor.show_points"),
show_streak: this._t("kiosk.editor.show_streak"),
show_reward_progress: this._t("kiosk.editor.show_reward_progress"),
show_bounties: this._t("kiosk.editor.show_bounties"),
show_picker_progress: this._t("kiosk.editor.show_picker_progress"),
play_sounds: this._t("kiosk.editor.play_sounds"),
show_confetti: this._t("kiosk.editor.show_confetti"),
@@ -1341,6 +1718,7 @@ class TaskMateKioskCardEditor extends LitElement {
title: this._t("kiosk.editor.title_helper"),
timeout: this._t("kiosk.editor.timeout_helper"),
warn_before_return: this._t("kiosk.editor.warn_before_return_helper"),
show_bounties: this._t("kiosk.editor.show_bounties_helper"),
show_picker_progress: this._t("kiosk.editor.show_picker_progress_helper"),
play_sounds: this._t("kiosk.editor.play_sounds_helper"),
show_completion_popup: this._t("kiosk.editor.show_completion_popup_helper"),
@@ -1401,6 +1779,7 @@ class TaskMateKioskCardEditor extends LitElement {
show_points: this.config.show_points !== false,
show_streak: this.config.show_streak !== false,
show_reward_progress: this.config.show_reward_progress !== false,
show_bounties: this.config.show_bounties === true,
show_picker_progress: this.config.show_picker_progress !== false,
play_sounds: this.config.play_sounds !== false,
show_confetti: this.config.show_confetti !== false,
@@ -1472,6 +1851,10 @@ class TaskMateKioskCardEditor extends LitElement {
else if (key === "timeout") {
if (clampTimeout(value) === DEFAULT_TIMEOUT) delete next[key];
else next[key] = clampTimeout(value);
} else if (key === "show_bounties") {
// The one toggle that defaults to off: store only the "on" choice.
if (value) next[key] = true;
else delete next[key];
} else if (typeof value === "boolean" && key !== "entity") {
// Every toggle defaults to on: store only the "off" choice.
if (value) delete next[key];
+474 -61
View File
@@ -176,14 +176,22 @@ const TASK_GROUP_POLICIES = [
// Setup wizard (#980). Age groups mirror AGE_GROUP_INFO in setup_catalogue.py
// (youngest age, oldest age) — tests/test_setup_wizard.py keeps them in step.
const WZ_AGE_GROUPS = [["3_5", 3, 5], ["6_8", 6, 8], ["9_12", 9, 12], ["13_plus", 13, 999]];
const WZ_STEPS = ["welcome", "children", "chores", "rewards", "review", "done"];
const WZ_STEPS = ["welcome", "children", "chores", "rewards", "dashboards", "review", "done"];
const WZ_REVIEW = WZ_STEPS.indexOf("review");
const WZ_DONE = WZ_STEPS.indexOf("done");
const WZ_KID_ICONS = ["mdi:star", "mdi:heart", "mdi:paw", "mdi:white-balance-sunny", "mdi:weather-night", "mdi:trophy",
"mdi:leaf", "mdi:creation", "mdi:fire", "mdi:medal", "mdi:book-open-variant", "mdi:car-sports"];
const WZ_KID_COLORS = ["#ff6b9d", "#3498db", "#1abc9c", "#9b59b6", "#e67e22", "#f1c40f", "#e74c3c", "#2ecc71"];
const WZ_POINT_NAMES = [["points", "mdi:star"], ["stars", "mdi:star"], ["coins", "mdi:circle-multiple"], ["gems", "mdi:diamond-stone"]];
const WZ_DRAFT_KEY = "taskmate-setup-draft";
const WZ_SKIP_KEY = "taskmate-setup-skipped";
const WZ_DASH_PATH = "family-chores";
// Ready-made dashboards (#1053).
const DASH_KINDS = ["kids", "parents", "wall"];
const DASH_PATHS = { kids: "taskmate-kids", parents: "taskmate-parents", wall: "taskmate-wall" };
const DASH_ICONS = { kids: "mdi:human-child", parents: "mdi:account-supervisor", wall: "mdi:tablet-dashboard" };
const DASH_COLORS = { kids: "#ff6b9d", parents: "#9b59b6", wall: "#e67e22" };
// Home Assistant's rule for a dashboard address: a slug with at least one dash.
const DASH_PATH_RE = /^[a-z0-9]+(-[a-z0-9]+)+$/;
class TaskMatePanel extends HTMLElement {
@@ -571,6 +579,8 @@ class TaskMatePanel extends HTMLElement {
const act = t.dataset.act;
// Setup wizard (#980) actions all start "wz-".
if (act.startsWith("wz-")) { this._wizardAct(t, e); return; }
// Ready-made dashboards (#1053) actions all start "dash-".
if (act.startsWith("dash-")) { this._dashAct(t); return; }
if (act === "new-menu") { this._newMenuOpen = !this._newMenuOpen; this._render(); return; }
if (act === "palette-open") { this._openPalette(); return; }
@@ -3535,7 +3545,7 @@ class TaskMatePanel extends HTMLElement {
if (!this._shellReady) {
if (!this._cachedStyles) {
this._cachedStyles = this._styles() + this._wizardStyles();
this._cachedStyles = this._styles() + this._wizardStyles() + this._dashStyles();
}
const existingToast = this.querySelector(".tm-toast");
this.innerHTML = `
@@ -5052,6 +5062,9 @@ class TaskMatePanel extends HTMLElement {
const actions = this._t("panel.palette_group_actions");
for (const it of this._newMenuItems()) cmds.push({ group: actions, icon: it.icon, label: it.label, data: { act: it.act } });
cmds.push({ group: actions, icon: "mdi:auto-fix", label: this._t("wizard.run_btn"), data: { act: "wz-open" } });
for (const kind of DASH_KINDS) {
cmds.push({ group: actions, icon: DASH_ICONS[kind], label: this._t("panel.dash_palette", { kind: this._t(`panel.dash_kind_${kind}`) }), data: { act: "dash-new", v: kind } });
}
if ((s.pending_completions || []).length) {
cmds.push({ group: actions, icon: "mdi:check-all", label: this._t("panel.activity_approve_all"), data: { act: "approve-all-chores" } });
}
@@ -5167,7 +5180,7 @@ class TaskMatePanel extends HTMLElement {
if (!ok) { this._showToast("err", this._t("wizard.toast_load_failed", { error: err })); return; }
this._wzCatalogue = res;
}
if (!this._wz || this._wz.step === 5) {
if (!this._wz || this._wz.step === WZ_DONE) {
const draft = this._wizardLoadDraft();
this._wz = draft || this._wizardFresh();
}
@@ -5181,7 +5194,7 @@ class TaskMatePanel extends HTMLElement {
_wizardClose(toTab = "today") {
// A finished run leaves nothing to come back to; anything else stays as a draft.
if (this._wz && this._wz.step === 5) this._wz = null;
if (this._wz && this._wz.step === WZ_DONE) this._wz = null;
else this._wizardSaveDraft();
localStorage.setItem(WZ_SKIP_KEY, "true");
this._activeTab = toTab;
@@ -5194,7 +5207,14 @@ class TaskMatePanel extends HTMLElement {
if (!d || d.v !== 1 || !Array.isArray(d.kids)) return null;
d.busy = false;
d.result = null;
d.step = Math.min(Math.max(0, Number(d.step) || 0), 4);
// Drafts from before #1053 had a single "Family chores" switch and
// Review as step 4; Review is now after the Dashboards step.
if (!d.dashOpts) {
d.dash = { kids: d.dash !== false, parents: false, wall: false };
d.dashOpts = this._wizardDashOpts();
if (Number(d.step) === 4) d.step = WZ_REVIEW;
}
d.step = Math.min(Math.max(0, Number(d.step) || 0), WZ_REVIEW);
return d;
} catch (_) {
return null;
@@ -5203,7 +5223,7 @@ class TaskMatePanel extends HTMLElement {
_wizardSaveDraft() {
const wz = this._wz;
if (!wz || wz.step === 5) return;
if (!wz || wz.step === WZ_DONE) return;
try {
const { busy: _busy, result: _result, ...keep } = wz;
localStorage.setItem(WZ_DRAFT_KEY, JSON.stringify(keep));
@@ -5219,12 +5239,17 @@ class TaskMatePanel extends HTMLElement {
pname, picon, pnameOrig: pname, piconOrig: picon, pnameCustom: false,
kids: [], form: null,
chores: {}, packs: [], rewards: {}, custom: [], rwName: "", rwCost: 20,
approval: "big", dash: true, notify: true, showAll: false,
approval: "big", notify: true, showAll: false,
dash: { kids: true, parents: false, wall: false }, dashOpts: this._wizardDashOpts(),
};
wz.pnameCustom = !WZ_POINT_NAMES.some(([k]) => this._t(`wizard.pname_${k}`) === pname);
return wz;
}
_wizardDashOpts() {
return { layout: "tabs", hide: true, admin: false, board: true };
}
// Existing children join the wizard as "Already set up", so their ages can
// steer the suggestions. Keeps any age group the parent picked in the draft.
_wizardSyncExisting() {
@@ -5439,6 +5464,11 @@ class TaskMatePanel extends HTMLElement {
return !!(plan.children.length || plan.child_updates.length || plan.chores.length || plan.rewards.length);
}
// Dashboards on their own are worth a run of the wizard too (#1053).
_wizardHasWork(plan) {
return this._wizardPlanHasWork(plan) || DASH_KINDS.some(k => this._wz.dash[k]);
}
_wizardAddKid() {
const wz = this._wz;
const f = wz.form;
@@ -5466,11 +5496,11 @@ class TaskMatePanel extends HTMLElement {
_wizardGo(step) {
const wz = this._wz;
if (wz.busy || step === 5) return;
if (wz.busy || step === WZ_DONE) return;
// Leaving Children with a name typed adds that child, as the footer says.
if (wz.step === 1 && step > 1 && (wz.form.name || "").trim() && !this._wizardAddKid()) return;
if (step >= 2 && !wz.kids.length) { this._showToast("err", this._t("wizard.toast_need_child")); wz.step = 1; this._render(); return; }
wz.step = Math.max(0, Math.min(4, step));
wz.step = Math.max(0, Math.min(WZ_REVIEW, step));
this._wizardSaveDraft();
this._render();
const body = this.querySelector(".tm-body");
@@ -5492,10 +5522,7 @@ class TaskMatePanel extends HTMLElement {
case "wz-close": this._wizardClose(); return;
case "wz-today": this._wz = null; this._activeTab = "today"; this._render(); return;
case "wz-restore": this._wizardClose("settings"); return;
case "wz-dashboard":
history.pushState(null, "", `/${WZ_DASH_PATH}`);
window.dispatchEvent(new CustomEvent("location-changed", { detail: { replace: false } }));
return;
case "wz-dashboard": this._dashOpen(v); return;
case "wz-go": this._wizardGo(Number(v)); return;
case "wz-next": this._wizardGo(wz.step + 1); return;
case "wz-back": this._wizardGo(wz.step - 1); return;
@@ -5599,7 +5626,9 @@ class TaskMatePanel extends HTMLElement {
break;
}
case "wz-appr": wz.approval = v; break;
case "wz-dash": wz.dash = !wz.dash; break;
case "wz-dash": if (DASH_KINDS.includes(v)) wz.dash[v] = !wz.dash[v]; break;
case "wz-dash-layout": wz.dashOpts.layout = v === "each" ? "each" : "tabs"; break;
case "wz-dash-opt": if (v in wz.dashOpts) wz.dashOpts[v] = !wz.dashOpts[v]; break;
case "wz-notify": wz.notify = !wz.notify; break;
default: return;
}
@@ -5637,10 +5666,13 @@ class TaskMatePanel extends HTMLElement {
const wz = this._wz;
if (wz.busy) return;
const plan = this._wizardPlan();
if (!this._wizardPlanHasWork(plan)) return;
if (!this._wizardHasWork(plan)) return;
wz.busy = true;
this._render();
const { ok, res, err, code } = await this._callWS({ type: "taskmate/setup_wizard/apply", ...plan });
// Only dashboards picked: there is nothing for the backend to add.
const { ok, res, err, code } = this._wizardPlanHasWork(plan)
? await this._callWS({ type: "taskmate/setup_wizard/apply", ...plan })
: { ok: true, res: { children: {} } };
if (!ok) {
wz.busy = false;
this._render();
@@ -5656,11 +5688,10 @@ class TaskMatePanel extends HTMLElement {
await this._callWS({ type: "taskmate/update_settings", points_name: pname.slice(0, 120), points_icon: wz.picon });
}
if (wz.notify) await this._wizardEnableApprovalAlerts();
let dashboard = "off";
if (wz.dash) dashboard = await this._wizardCreateDashboard(res.children || {});
wz.result = { ...res, dashboard, updated: plan.child_updates.length };
const dashboards = await this._wizardCreateDashboards(res.children || {});
wz.result = { ...res, dashboards, updated: plan.child_updates.length };
wz.busy = false;
wz.step = 5;
wz.step = WZ_DONE;
localStorage.removeItem(WZ_DRAFT_KEY);
await this._fetchState();
}
@@ -5685,31 +5716,19 @@ class TaskMatePanel extends HTMLElement {
return hit || "sensor.taskmate_overview";
}
// A "Family chores" dashboard: one child card per child plus the rewards
// card. Created only when the url is free — an existing dashboard (or any
// panel on that path) is never touched.
async _wizardCreateDashboard(refToId) {
if (this._hass && this._hass.panels && this._hass.panels[WZ_DASH_PATH]) return "exists";
const list = await this._callWS({ type: "lovelace/dashboards/list" });
if (!list.ok) return "failed";
if ((list.res || []).some(d => d.url_path === WZ_DASH_PATH)) return "exists";
const title = this._t("wizard.dash_title");
const created = await this._callWS({
type: "lovelace/dashboards/create", url_path: WZ_DASH_PATH, title,
icon: "mdi:checkbox-marked-circle-plus-outline", mode: "storage", show_in_sidebar: true, require_admin: false,
});
if (!created.ok) return "failed";
const entity = this._wizardOverviewEntity();
const cards = this._wz.kids.map(k => ({
type: "custom:taskmate-child-card", entity, child_id: refToId[k.key] || k.key,
time_category: "all", header_color: k.color,
}));
cards.push({ type: "custom:taskmate-rewards-card", entity });
const saved = await this._callWS({
type: "lovelace/config/save", url_path: WZ_DASH_PATH,
config: { title, views: [{ title, path: "chores", icon: "mdi:checkbox-marked-circle-outline", cards }] },
});
return saved.ok ? "created" : "failed";
// The dashboards picked on the Dashboards step (#1053), filled in with the
// children as they are once created. An address already in use is skipped.
async _wizardCreateDashboards(refToId) {
const wz = this._wz;
const kinds = DASH_KINDS.filter(k => wz.dash[k]);
if (!kinds.length) return [];
const kids = wz.kids.map(k => ({ id: refToId[k.key] || k.key, name: k.name, color: k.color, icon: k.icon }));
// Children added a moment ago get their badges sensor shortly after.
for (let i = 0; i < 10 && kinds.includes("kids") && kids.some(k => !this._dashBadgeEntities()[k.id]); i++) {
await new Promise(r => setTimeout(r, 500));
}
const specs = kinds.flatMap(kind => this._dashSpecs({ ...this._dashDefaults(kind), ...wz.dashOpts }, kids));
return this._dashCreate(specs);
}
// "Vaiha is 9 now — see new chore ideas" (#980): suggestions never move up
@@ -5773,16 +5792,17 @@ class TaskMatePanel extends HTMLElement {
return this._emptyState("🪄", this._t("wizard.title"), this._t("wizard.intro_existing"), "wz-open", this._t("wizard.run_btn"));
}
const step = wz.step;
const last = step === 5;
const last = step === WZ_DONE;
const steps = WZ_STEPS.map((id, i) => `
${i ? `<i class="tm-wz-sep ${i <= step ? "tm-wz-done" : ""}"></i>` : ""}
<button type="button" class="tm-wz-step ${i === step ? "tm-wz-on" : i < step ? "tm-wz-done" : ""}" data-act="wz-go" data-v="${i}" ${last || i === 5 ? "disabled" : ""} ${i === step ? 'aria-current="step"' : ""}>
<button type="button" class="tm-wz-step ${i === step ? "tm-wz-on" : i < step ? "tm-wz-done" : ""}" data-act="wz-go" data-v="${i}" ${last || i === WZ_DONE ? "disabled" : ""} ${i === step ? 'aria-current="step"' : ""}>
<span class="tm-wz-n">${i < step ? `<ha-icon icon="mdi:check"></ha-icon>` : i + 1}</span><span class="tm-wz-l">${this._t(`wizard.step_${id}`)}</span>
</button>`).join("");
const nextKey = ["wizard.next_welcome", "wizard.next_children", "wizard.next_chores", "wizard.next_rewards"][step];
const nextKey = ["wizard.next_welcome", "wizard.next_children", "wizard.next_chores", "wizard.next_rewards", "wizard.next_dashboards"][step];
const body = [
() => this._wizardStepWelcome(), () => this._wizardStepChildren(), () => this._wizardStepChores(),
() => this._wizardStepRewards(), () => this._wizardStepReview(), () => this._wizardStepDone(),
() => this._wizardStepRewards(), () => this._wizardStepDashboards(), () => this._wizardStepReview(),
() => this._wizardStepDone(),
][step]();
const typed = step === 1 && (wz.form.name || "").trim();
return `
@@ -5805,8 +5825,8 @@ class TaskMatePanel extends HTMLElement {
<span class="tm-wz-sp"></span>
${step === 0 && wz.mode === "fresh" ? `<button type="button" class="tm-btn tm-wz-text-btn" data-act="wz-restore">${this._t("wizard.restore")}</button>` : ""}
${step === 1 ? `<span class="tm-wz-foot-hint">${typed ? this._esc(this._t("wizard.foot_will_add", { name: typed })) : ""}</span>` : ""}
${step === 4
? `<button type="button" class="tm-btn tm-btn-raised tm-wz-create" data-act="wz-create" ${wz.busy || !this._wizardPlanHasWork(this._wizardPlan()) ? "disabled" : ""}>${wz.busy ? `<span class="tm-btn-spinner"></span>${this._t("wizard.creating")}` : this._t(wz.mode === "existing" ? "wizard.create_existing" : "wizard.create")}</button>`
${step === WZ_REVIEW
? `<button type="button" class="tm-btn tm-btn-raised tm-wz-create" data-act="wz-create" ${wz.busy || !this._wizardHasWork(this._wizardPlan()) ? "disabled" : ""}>${wz.busy ? `<span class="tm-btn-spinner"></span>${this._t("wizard.creating")}` : this._t(wz.mode === "existing" ? "wizard.create_existing" : "wizard.create")}</button>`
: `<button type="button" class="tm-btn tm-btn-raised" data-act="wz-next">${this._t(nextKey)}<ha-icon class="tm-rtl-flip" icon="mdi:chevron-right"></ha-icon></button>`}
</div>`}
</div>
@@ -6084,6 +6104,42 @@ class TaskMatePanel extends HTMLElement {
</div>`;
}
// Dashboards step (#1053): any of the three ready-made dashboards, with the
// children from the earlier steps filled in when it's all created.
_wizardStepDashboards() {
const wz = this._wz;
const o = wz.dashOpts;
const sw = (v, on, title, hint) => `
<div class="tm-wz-tog"><div class="tm-wz-grow">${title}<small>${hint}</small></div>
<button type="button" class="tm-wz-sw ${on ? "tm-wz-on" : ""}" data-act="wz-dash-opt" data-v="${v}" role="switch" aria-checked="${on}" aria-label="${this._esc(title)}"></button></div>`;
const opts = {
kids: () => `
<div class="tm-wz-tog"><div class="tm-wz-grow">${this._t("panel.dash_layout_label")}<small>${this._t(o.layout === "each" ? "panel.dash_layout_each_hint" : "panel.dash_layout_tabs_hint")}</small></div>
<div class="tm-seg tm-wz-seg" role="tablist">${["tabs", "each"].map(v => `<button type="button" role="tab" class="${o.layout === v ? "tm-seg-on" : ""}" aria-selected="${o.layout === v}" data-act="wz-dash-layout" data-v="${v}">${this._t(`panel.dash_layout_${v}`)}</button>`).join("")}</div></div>
${sw("hide", o.hide, this._t("panel.dash_hide_parent"), this._t("panel.dash_hide_parent_hint"))}`,
parents: () => sw("admin", o.admin, this._t("panel.dash_admin"), this._t("panel.dash_admin_hint")),
wall: () => sw("board", o.board, this._t("panel.dash_board"), this._t("panel.dash_board_hint")),
};
const tiles = DASH_KINDS.map(kind => {
const on = !!wz.dash[kind];
const path = kind === "kids" && o.layout === "each" ? `/${DASH_PATHS[kind]}-…` : `/${DASH_PATHS[kind]}`;
return `
<div class="tm-dash-pick ${on ? "tm-dash-on" : ""}" style="--dc:${DASH_COLORS[kind]}">
<button type="button" class="tm-dash-pick-h" data-act="wz-dash" data-v="${kind}" aria-pressed="${on}">
<span class="tm-dash-pick-ic"><ha-icon icon="${DASH_ICONS[kind]}"></ha-icon></span>
<span class="tm-wz-grow"><b>${this._t(`panel.dash_kind_${kind}`)}</b><small>${this._t(`panel.dash_desc_${kind}`)}</small><code>${this._esc(path)}</code></span>
<ha-icon class="tm-dash-tick" icon="${on ? "mdi:check-circle" : "mdi:circle-outline"}"></ha-icon>
</button>
${on ? `<div class="tm-dash-pick-o">${opts[kind]()}</div>` : ""}
</div>`;
}).join("");
return `
<h2 class="tm-wz-h">${this._t("wizard.dash_step_title")}</h2>
<p class="tm-wz-p">${this._t("wizard.dash_step_copy")}</p>
<div class="tm-dash-picks">${tiles}</div>
<p class="tm-wz-hint">${this._t("wizard.dash_step_later")}</p>`;
}
_wizardCount(noun, n) {
return this._t(`wizard.count_${noun}_${n === 1 ? "one" : "many"}`, { count: n });
}
@@ -6102,7 +6158,8 @@ class TaskMatePanel extends HTMLElement {
const plan = this._wizardPlan();
const c = this._wizardCounts(plan);
const keep = { ...c, have_chores: this._wizardCount("chores", (s.chores || []).length), have_rewards: this._wizardCount("rewards", (s.rewards || []).length) };
const intro = !this._wizardPlanHasWork(plan) ? this._t("wizard.review_nothing")
const intro = !this._wizardHasWork(plan) ? this._t("wizard.review_nothing")
: !this._wizardPlanHasWork(plan) ? this._t("wizard.review_dashboards_only")
: wz.mode !== "existing" ? this._t("wizard.review_fresh", c)
: this._t(plan.children.length ? "wizard.review_existing" : "wizard.review_existing_no_children", keep);
const packNames = plan.chores.filter(ch => !ch.assigned_to.length).map(ch => ch.name);
@@ -6134,7 +6191,7 @@ class TaskMatePanel extends HTMLElement {
<div class="tm-wz-panel tm-wz-opts"><h4>${this._t("wizard.defaults")}</h4>
<div class="tm-wz-tog"><div class="tm-wz-grow">${this._t("wizard.appr_title")}<small>${this._t("wizard.appr_hint")}</small></div>
<div class="tm-seg tm-wz-seg" role="tablist">${appr.map(([v, key]) => `<button type="button" role="tab" class="${wz.approval === v ? "tm-seg-on" : ""}" aria-selected="${wz.approval === v}" data-act="wz-appr" data-v="${v}">${this._t(key)}</button>`).join("")}</div></div>
${sw("wz-dash", wz.dash, this._t("wizard.dash_toggle"), this._t("wizard.dash_hint"))}
<div class="tm-wz-tog"><div class="tm-wz-grow">${this._t("wizard.review_dashboards")}<small>${this._esc(DASH_KINDS.filter(k => wz.dash[k]).map(k => this._t(`panel.dash_kind_${k}`)).join(", ") || this._t("wizard.review_no_dashboards"))}</small></div></div>
${sw("wz-notify", wz.notify, this._t("wizard.notify_toggle"), this._t("wizard.notify_hint"))}
</div>
<div class="tm-wz-panel"><h4>${this._t("wizard.rewards_n", { count: plan.rewards.length })}</h4>
@@ -6151,11 +6208,13 @@ class TaskMatePanel extends HTMLElement {
// Only what was actually added: never a row of zeros.
const phrase = (noun, n) => (n ? `<span>${this._wizardCount(noun, n)}</span>` : "");
const added = (r.children_added || 0) + (r.chores_added || 0) + (r.rewards_added || 0) + (r.updated || 0);
const copy = !added && r.dashboard !== "created" ? "wizard.done_copy_nothing"
const dashes = r.dashboards || [];
const made = dashes.filter(x => x.status === "created").length;
const copy = !added && !made ? "wizard.done_copy_nothing"
: !added ? "wizard.done_copy_dashboards"
: wz.mode === "existing" ? "wizard.done_copy_existing" : "wizard.done_copy";
const dash = r.dashboard === "created" || r.dashboard === "exists";
const dashNote = r.dashboard === "exists" ? this._t("wizard.dash_existed")
: r.dashboard === "failed" ? this._t("wizard.dash_failed") : "";
const dashNotes = dashes.filter(x => x.status !== "created")
.map(x => this._t(x.status === "exists" ? "wizard.dash_existed" : "wizard.dash_failed", { title: x.title }));
return `
<div class="tm-wz-finish">
<div class="tm-wz-confetti" aria-hidden="true">${conf}</div>
@@ -6166,16 +6225,16 @@ class TaskMatePanel extends HTMLElement {
${phrase("children", r.children_added || 0)}
${phrase("chores", r.chores_added || 0)}
${phrase("rewards", r.rewards_added || 0)}
${r.dashboard === "created" ? `<span>${this._t("wizard.one_dashboard")}</span>` : ""}
${phrase("dashboards", made)}
</div>
${dashNote ? `<p class="tm-wz-hint tm-wz-center">${dashNote}</p>` : ""}
${dashNotes.map(n => `<p class="tm-wz-hint tm-wz-center">${this._esc(n)}</p>`).join("")}
<div class="tm-wz-next">
<div><b><ha-icon icon="mdi:tablet"></ha-icon>${this._t("wizard.next_tablet_title")}</b>${this._t("wizard.next_tablet")}</div>
<div><b><ha-icon icon="mdi:home-outline"></ha-icon>${this._t("wizard.next_today_title")}</b>${this._t("wizard.next_today")}</div>
<div><b><ha-icon icon="mdi:auto-fix"></ha-icon>${this._t("wizard.next_change_title")}</b>${this._t("wizard.next_change")}</div>
</div>
<div class="tm-wz-done-acts">
${dash ? `<button type="button" class="tm-btn" data-act="wz-dashboard"><ha-icon icon="mdi:view-dashboard-outline"></ha-icon>${this._t("wizard.open_dashboard")}</button>` : ""}
${dashes.filter(x => x.status !== "failed").slice(0, 4).map(x => `<button type="button" class="tm-btn" data-act="wz-dashboard" data-v="${this._esc(x.url_path)}"><ha-icon icon="mdi:view-dashboard-outline"></ha-icon>${this._esc(this._t("wizard.open_named", { title: x.title }))}</button>`).join("")}
<button type="button" class="tm-btn tm-btn-raised" data-act="wz-today">${this._t("wizard.go_today")}<ha-icon class="tm-rtl-flip" icon="mdi:chevron-right"></ha-icon></button>
</div>
</div>`;
@@ -6219,6 +6278,354 @@ class TaskMatePanel extends HTMLElement {
</div>`;
}
// -- Ready-made dashboards (#1053) ----------------------------------------
// Kids, Parents and Wall tablet presets, built from the family as it is now
// and saved once as ordinary storage dashboards, which are then the user's
// to edit. Settings and the setup wizard share this. An address that is
// already a dashboard or a panel is never written to.
_dashDefaults(kind) {
return {
kind, title: this._t(`panel.dash_title_${kind}`), path: DASH_PATHS[kind],
layout: "tabs", hide: true, admin: false, board: true, sidebar: kind !== "wall",
};
}
// The children a dashboard can be made for: guests come and go, so not them.
_dashKids() {
return ((this._state && this._state.children) || []).filter(c => !c.is_guest).map(c => ({
id: c.id, name: c.name, color: this._childColor(c.id), icon: c.avatar,
}));
}
// "Zoë" -> "zoe", for a view or dashboard address.
_dashSlug(text) {
return String(text || "").normalize("NFKD").replace(/[̀-ͯ]/g, "")
.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
}
// One spec per dashboard to create: { url_path, title, icon, require_admin,
// show_in_sidebar, config }. kids: [{ id, name, color, icon }].
_dashSpecs(o, kids) {
const entity = this._wizardOverviewEntity();
const card = (type, extra = {}) => ({ type: `custom:taskmate-${type}-card`, entity, ...extra });
const view = (title, path, icon, cards, panel = false) => ({ title, path, icon, ...(panel ? { type: "panel" } : {}), cards });
const base = { title: o.title, icon: DASH_ICONS[o.kind], require_admin: o.kind === "parents" && !!o.admin, show_in_sidebar: !!o.sidebar };
const one = views => [{ ...base, url_path: o.path, config: { title: o.title, views } }];
if (o.kind === "parents") {
return one([
view(this._t("panel.dash_view_today"), "today", "mdi:calendar-today", [card("approvals"), card("chore-board"), card("parent-dashboard")]),
view(this._t("panel.dash_view_points"), "points", "mdi:star-circle-outline", [card("points-display", { mode: "multi" }), card("bonuses"), card("penalties"), card("leaderboard")]),
view(this._t("panel.dash_view_insights"), "insights", "mdi:chart-line", [card("weekly"), card("graph"), card("activity"), card("calendar")]),
]);
}
if (o.kind === "wall") {
const views = [view(this._t("panel.dash_card_kiosk"), "kiosk", "mdi:tablet", [card("kiosk")], true)];
if (o.board) views.push(view(this._t("panel.today_board_title"), "board", "mdi:view-grid-outline", [card("chore-board")], true));
return one(views);
}
const family = () => view(this._t("panel.dash_view_family"), "family", "mdi:home-heart", [card("leaderboard"), card("family-goal")]);
const badges = this._dashBadgeEntities();
const used = new Set(["family"]);
const kidViews = kids.map(k => {
let slug = this._dashSlug(k.name) || this._dashSlug(k.id) || "child";
// Two Sams, or a child called Family, still get an address each.
for (let n = 2; used.has(slug); n++) slug = `${this._dashSlug(k.name) || "child"}-${n}`;
used.add(slug);
const own = { child_id: k.id, header_color: k.color };
const chores = card("child", { ...own, time_category: "all" });
if (o.hide) chores.show_parent_actions = false;
const icon = /^mdi:/.test(k.icon || "") ? k.icon : "mdi:account";
const cards = [chores, card("reward-progress", own), card("streak", own)];
// The badges card reads the child's own badges sensor, not the overview.
if (badges[k.id]) cards.push({ ...card("badges"), entity: badges[k.id], header_color: k.color });
cards.push(card("rewards", own));
return { slug, view: view(k.name, slug, icon, cards) };
});
if (o.layout === "each") {
return kidViews.map(({ slug, view: v }) => {
const title = `${o.title} · ${v.title}`;
return { ...base, title, url_path: `${o.path}-${slug}`, config: { title, views: [v, family()] } };
});
}
return one([...kidViews.map(x => x.view), family()]);
}
// child id -> that child's badges sensor, as the overview publishes it
// (entity ids can carry an area prefix, so they are never guessed).
_dashBadgeEntities() {
const states = (this._hass && this._hass.states) || {};
const ov = states[this._wizardOverviewEntity()];
return (ov && ov.attributes && ov.attributes.badge_entities) || {};
}
// Which of these addresses are already taken; null if the list can't be read.
async _dashTaken(paths) {
const panels = (this._hass && this._hass.panels) || {};
const list = await this._callWS({ type: "lovelace/dashboards/list" });
if (!list.ok) return null;
const have = new Set((list.res || []).map(d => d.url_path));
return paths.filter(p => panels[p] || have.has(p));
}
// Creates each spec whose address is free: [{ url_path, title, status }],
// status "created", "exists" or "failed".
async _dashCreate(specs, taken = null) {
taken = taken || await this._dashTaken(specs.map(s => s.url_path));
const out = [];
for (const { config, ...meta } of specs) {
const r = { url_path: meta.url_path, title: meta.title };
if (!taken) { out.push({ ...r, status: "failed" }); continue; }
if (taken.includes(meta.url_path)) { out.push({ ...r, status: "exists" }); continue; }
const created = await this._callWS({ type: "lovelace/dashboards/create", mode: "storage", ...meta });
const saved = created.ok && await this._callWS({ type: "lovelace/config/save", url_path: meta.url_path, config });
const ok = !!(saved && saved.ok);
if (ok) (this._dashMade = this._dashMade || new Set()).add(meta.url_path);
out.push({ ...r, status: ok ? "created" : "failed" });
}
return out;
}
_dashOpen(path) {
if (!path) return;
history.pushState(null, "", `/${path}`);
window.dispatchEvent(new CustomEvent("location-changed", { detail: { replace: false } }));
}
// The dashboard a preset already made, if any: its own address, or the
// first of a per-child set.
_dashFound(kind) {
const have = new Set([...Object.keys((this._hass && this._hass.panels) || {}), ...(this._dashMade || [])]);
const path = DASH_PATHS[kind];
return have.has(path) ? path : [...have].find(p => p.startsWith(`${path}-`)) || "";
}
_dashNew(kind) {
if (!DASH_KINDS.includes(kind)) return;
const kids = this._dashKids();
if (kind === "kids" && !kids.length) { this._showToast("err", this._t("panel.dash_need_child")); return; }
this._openDialog({ kind: "dash-create", data: { ...this._dashDefaults(kind), kids: kids.map(k => k.id), err: "", busy: false } });
}
_dashAct(t) {
const act = t.dataset.act;
const v = t.dataset.v;
if (act === "dash-new") { this._dashNew(v); return; }
if (act === "dash-open") { this._dashOpen(v); return; }
const d = this._dialog && this._dialog.kind === "dash-create" ? this._dialog.data : null;
if (!d || d.busy) return;
if (act === "dash-kind") {
if (!DASH_KINDS.includes(v) || v === d.kind) return;
Object.assign(d, this._dashDefaults(v), { err: "" });
} else if (act === "dash-kid") {
const id = t.dataset.id;
d.kids = d.kids.includes(id) ? d.kids.filter(x => x !== id) : [...d.kids, id];
} else if (act === "dash-layout") {
d.layout = v === "each" ? "each" : "tabs";
} else if (act === "dash-save") {
this._dashSave();
return;
} else {
return;
}
this._render();
}
// The dialog's choices as specs, or an error key if they can't be made.
_dashDialogSpecs(d) {
const title = String(d.title || "").trim();
const path = String(d.path || "").trim().toLowerCase();
if (!title) return { err: "panel.dash_need_title" };
if (!DASH_PATH_RE.test(path)) return { err: "panel.dash_path_invalid" };
const kids = this._dashKids().filter(k => d.kids.includes(k.id));
if (d.kind === "kids" && !kids.length) return { err: "panel.dash_pick_child" };
return { specs: this._dashSpecs({ ...d, title, path }, kids) };
}
async _dashSave() {
const d = this._dialog.data;
const { specs, err } = this._dashDialogSpecs(d);
if (err) { d.err = this._t(err); this._render(); return; }
d.busy = true;
d.err = "";
this._render();
// Everything is checked before anything is written.
const taken = await this._dashTaken(specs.map(s => s.url_path));
if (!taken || taken.length) {
d.busy = false;
d.err = taken ? this._t("panel.dash_path_taken", { paths: taken.map(p => `/${p}`).join(", ") }) : this._t("panel.dash_list_failed");
this._render();
return;
}
const res = await this._dashCreate(specs, taken);
d.busy = false;
const made = res.filter(r => r.status === "created");
const failed = res.filter(r => r.status !== "created");
if (made.length) this._showToast("ok", this._t("panel.dash_toast_created", { titles: made.map(r => r.title).join(", ") }));
if (failed.length) {
this._showToast("err", this._t("panel.dash_toast_failed", { titles: failed.map(r => r.title).join(", ") }));
this._render();
return;
}
this._closeDialog(true);
}
_dashSettingsSection() {
const noKids = !this._dashKids().length;
const tiles = DASH_KINDS.map(kind => {
const found = this._dashFound(kind);
const off = kind === "kids" && noKids;
return `
<div class="tm-dash-tile" style="--dc:${DASH_COLORS[kind]}">
<div class="tm-dash-tile-h"><ha-icon icon="${DASH_ICONS[kind]}"></ha-icon>${this._t(`panel.dash_kind_${kind}`)}</div>
<div class="tm-dash-tile-b">
<p>${this._t(`panel.dash_desc_${kind}`)}</p>
<small>${this._t(off ? "panel.dash_need_child" : `panel.dash_for_${kind}`)}</small>
</div>
<div class="tm-dash-tile-f">
${found ? `<span class="tm-dash-made"><ha-icon icon="mdi:check"></ha-icon>${this._t("panel.dash_created")}</span>` : ""}
<span class="tm-dash-sp"></span>
${found ? `<button type="button" class="tm-btn tm-btn-sm" data-act="dash-open" data-v="${this._esc(found)}">${this._t("panel.dash_open")}</button>` : ""}
<button type="button" class="tm-btn tm-btn-sm ${found ? "" : "tm-btn-raised"}" data-act="dash-new" data-v="${kind}" ${off ? "disabled" : ""}>${this._t("panel.dash_create_btn")}</button>
</div>
</div>`;
}).join("");
return `
<div class="tm-section">
<div class="tm-section-head">
<div>
<h3>${this._t("panel.dash_section_title")}</h3>
<p class="tm-meta">${this._t("panel.dash_section_hint")}</p>
</div>
</div>
<div class="tm-dash-tiles">${tiles}</div>
</div>`;
}
// What each card is called in the preview.
_dashCardLabel(c, viewTitle) {
const type = c.type.replace(/^custom:taskmate-|-card$/g, "");
const keys = {
"reward-progress": "reward_progress.default_title", streak: "streak.default_title", rewards: "rewards.default_title",
leaderboard: "leaderboard.default_title", "family-goal": "family_goal.default_title", badges: "badges.default_title",
approvals: "approvals.default_title", "chore-board": "panel.today_board_title", "parent-dashboard": "dashboard.default_title",
"points-display": "points_display.default_title", bonuses: "bonuses.default_title", penalties: "penalties.default_title",
weekly: "weekly.default_title", graph: "graph.default_title", activity: "activity.default_title",
calendar: "calendar.default_title", kiosk: "panel.dash_card_kiosk",
};
if (type === "child") return this._t("panel.dash_card_chores", { name: viewTitle });
return keys[type] ? this._t(keys[type]) : type;
}
_dashPreview(specs) {
const shown = specs.slice(0, 3).map(s => `
<div class="tm-dash-pv">
<div class="tm-dash-pv-h"><ha-icon icon="${this._esc(s.icon)}"></ha-icon><b>${this._esc(s.title)}</b><code>/${this._esc(s.url_path)}</code></div>
${s.config.views.map(v => `
<div class="tm-dash-pv-v">
<span class="tm-dash-pv-tab"><ha-icon icon="${this._esc(v.icon)}"></ha-icon>${this._esc(v.title)}</span>
<span class="tm-dash-pv-cards">${v.cards.map(c => `<span style="${c.header_color ? `--cc:${this._esc(c.header_color)}` : ""}">${this._esc(this._dashCardLabel(c, v.title))}</span>`).join("")}</span>
</div>`).join("")}
</div>`).join("");
const more = specs.length > 3 ? `<p class="tm-field-hint">${this._t("panel.dash_more", { count: specs.length - 3 })}</p>` : "";
return shown + more;
}
_renderDashDialog() {
const d = this._dialog.data;
const kinds = DASH_KINDS.map(k => `
<button type="button" class="tm-dash-kind ${d.kind === k ? "tm-dash-on" : ""}" style="--dc:${DASH_COLORS[k]}" aria-pressed="${d.kind === k}" data-act="dash-kind" data-v="${k}">
<ha-icon icon="${DASH_ICONS[k]}"></ha-icon><span><b>${this._t(`panel.dash_kind_${k}`)}</b><small>${this._t(`panel.dash_for_${k}`)}</small></span>
</button>`).join("");
let opts;
if (d.kind === "kids") {
opts = `
<div class="tm-field">
<span class="tm-field-label">${this._t("panel.dash_children_label")}</span>
<div class="tm-chip-row">${this._dashKids().map(k => {
const on = d.kids.includes(k.id);
return `<button type="button" class="tm-chip-btn ${on ? "tm-chip-on" : ""}" aria-pressed="${on}" data-act="dash-kid" data-id="${this._esc(k.id)}"><i class="tm-dash-dot" style="background:${this._esc(k.color)}"></i>${this._esc(k.name)}</button>`;
}).join("")}</div>
</div>
<div class="tm-field">
<span class="tm-field-label">${this._t("panel.dash_layout_label")}</span>
<div class="tm-seg tm-dash-seg" role="tablist">${["tabs", "each"].map(v => `<button type="button" role="tab" class="${d.layout === v ? "tm-seg-on" : ""}" aria-selected="${d.layout === v}" data-act="dash-layout" data-v="${v}">${this._t(`panel.dash_layout_${v}`)}</button>`).join("")}</div>
<span class="tm-field-hint">${this._t(d.layout === "each" ? "panel.dash_layout_each_hint" : "panel.dash_layout_tabs_hint")}</span>
</div>
${this._switch(this._t("panel.dash_hide_parent"), "hide", d.hide, this._esc(this._t("panel.dash_hide_parent_hint")))}`;
} else if (d.kind === "parents") {
opts = this._switch(this._t("panel.dash_admin"), "admin", d.admin, this._esc(this._t("panel.dash_admin_hint")));
} else {
opts = this._switch(this._t("panel.dash_board"), "board", d.board, this._esc(this._t("panel.dash_board_hint")), true);
}
const pv = this._dashDialogSpecs({ ...d, title: d.title || this._t(`panel.dash_title_${d.kind}`), path: DASH_PATH_RE.test(d.path || "") ? d.path : DASH_PATHS[d.kind] });
const body = `
<div class="tm-dash-kinds">${kinds}</div>
<div class="tm-dash-two">
${this._field(this._t("panel.dash_title_label"), "title", d.title)}
${this._field(this._t("panel.dash_path_label"), "path", d.path, "text", this._esc(this._t("panel.dash_path_hint")))}
</div>
${opts}
${this._switch(this._t("panel.dash_sidebar"), "sidebar", d.sidebar)}
${d.err ? `<p class="tm-dash-err" role="alert"><ha-icon icon="mdi:alert-circle-outline"></ha-icon>${this._esc(d.err)}</p>` : ""}
<div class="tm-field">
<span class="tm-field-label">${this._t("panel.dash_preview")}</span>
${pv.specs ? this._dashPreview(pv.specs) : `<p class="tm-field-hint">${this._t(pv.err)}</p>`}
</div>`;
return this._dialogShell(this._t("panel.dash_dialog_title"), body,
`<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
<button type="button" class="tm-btn tm-btn-raised" data-act="dash-save" ${d.busy ? "disabled" : ""}>${d.busy ? `<span class="tm-btn-spinner"></span>` : `<ha-icon icon="mdi:view-dashboard-outline"></ha-icon>`}${this._t("panel.dash_create")}</button>`,
{ wide: true });
}
_dashStyles() {
return `<style>
/* Ready-made dashboards (#1053) */
.tm-dash-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; margin-top: 12px; }
.tm-dash-tile { display: flex; flex-direction: column; border: 1px solid var(--tm-border); border-radius: 12px; overflow: hidden; background: var(--tm-surface-1, var(--tm-bg)); }
.tm-dash-tile-h { display: flex; align-items: center; gap: 10px; padding: 12px 14px; background: var(--dc); color: #fff; font-weight: 600; font-size: 15px; --mdc-icon-size: 22px; }
.tm-dash-tile-b { flex: 1; padding: 12px 14px; }
.tm-dash-tile-b p { margin: 0 0 8px; font-size: 13px; color: var(--tm-text-muted); }
.tm-dash-tile-b small { color: var(--tm-text-faint); font-size: 12px; }
.tm-dash-tile-f { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--tm-border-soft); }
.tm-dash-sp { flex: 1; }
.tm-dash-made { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--tm-positive); white-space: nowrap; --mdc-icon-size: 14px; }
.tm-dash-kinds { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 14px; }
.tm-dash-kind { display: flex; align-items: center; gap: 10px; text-align: start; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--tm-border); background: var(--tm-surface-0); color: var(--tm-text); font-family: inherit; cursor: pointer; --mdc-icon-size: 22px; }
.tm-dash-kind ha-icon { color: var(--dc); flex: none; }
.tm-dash-kind b { display: block; font-size: 13.5px; }
.tm-dash-kind small { display: block; font-size: 11.5px; color: var(--tm-text-muted); }
.tm-dash-kind.tm-dash-on { border-color: var(--dc); box-shadow: inset 0 0 0 1px var(--dc); background: color-mix(in srgb, var(--dc) 10%, var(--tm-surface-0)); }
.tm-dash-two { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.tm-dash-seg { margin-inline-start: 0; align-self: flex-start; }
.tm-dash-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-inline-end: 6px; vertical-align: middle; }
.tm-dash-err { display: flex; align-items: center; gap: 8px; color: var(--tm-danger); font-size: 13px; margin: 4px 0 10px; --mdc-icon-size: 18px; }
.tm-dash-pv { border: 1px dashed var(--tm-border); border-radius: 10px; padding: 10px 12px; margin-bottom: 8px; background: var(--tm-bg); }
.tm-dash-pv-h { display: flex; align-items: center; gap: 8px; font-size: 13px; margin-bottom: 6px; --mdc-icon-size: 16px; }
.tm-dash-pv-h code { margin-inline-start: auto; color: var(--tm-text-faint); font-size: 11.5px; }
.tm-dash-pv-v { display: flex; gap: 10px; align-items: baseline; padding: 4px 0; border-top: 1px solid var(--tm-border-soft); font-size: 12px; }
.tm-dash-pv-tab { display: inline-flex; align-items: center; gap: 4px; min-width: 110px; color: var(--tm-text-muted); font-weight: 600; --mdc-icon-size: 14px; }
.tm-dash-pv-cards { display: flex; flex-wrap: wrap; gap: 4px; }
.tm-dash-pv-cards span { padding: 2px 8px; border-radius: 6px; background: var(--tm-surface-2); border-inline-start: 3px solid var(--cc, var(--tm-border-strong)); }
/* Wizard step */
.tm-dash-picks { display: grid; gap: 10px; }
.tm-dash-pick { border: 1px solid var(--tm-border); border-radius: 12px; overflow: hidden; background: var(--tm-bg); }
.tm-dash-pick.tm-dash-on { border-color: var(--dc); }
.tm-dash-pick-h { display: flex; align-items: center; gap: 14px; width: 100%; text-align: start; padding: 12px 14px; background: none; border: 0; color: var(--tm-text); font-family: inherit; cursor: pointer; }
.tm-dash-pick-h b { display: block; font-size: 15px; }
.tm-dash-pick-h small { display: block; font-size: 12.5px; color: var(--tm-text-muted); margin-top: 2px; }
.tm-dash-pick-h code { display: block; font-size: 11.5px; color: var(--tm-text-faint); margin-top: 4px; }
.tm-dash-pick-ic { width: 42px; height: 42px; border-radius: 12px; display: grid; place-items: center; background: var(--dc); color: #fff; flex: none; --mdc-icon-size: 24px; }
.tm-dash-tick { color: var(--tm-text-faint); --mdc-icon-size: 24px; flex: none; }
.tm-dash-on .tm-dash-tick { color: var(--dc); }
.tm-dash-pick-o { padding: 0 14px 4px; padding-inline-start: 70px; }
@media (max-width: 600px) {
.tm-dash-kinds, .tm-dash-two { grid-template-columns: 1fr; }
.tm-dash-pick-o { padding-inline-start: 14px; }
}
</style>`;
}
_wizardStyles() {
return `<style>
/* Setup wizard (#980) */
@@ -6408,6 +6815,10 @@ class TaskMatePanel extends HTMLElement {
.tm-wz-nudge { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 14px; margin-bottom: 16px; border-radius: 12px; background: var(--tm-accent-soft); border: 1px solid var(--tm-accent-border); }
.tm-wz-nudge-text { flex: 1; min-width: 180px; }
.tm-wz-nudge .tm-btn ha-icon { --mdc-icon-size: 16px; }
/* Seven steps outgrow a mid-width box: only the current one keeps its name. */
@container tm-wz (max-width: 960px) {
.tm-wz-step:not(.tm-wz-on) .tm-wz-l { display: none; }
}
@container tm-wz (max-width: 640px) {
.tm-wz-step .tm-wz-l, .tm-wz-sep { display: none; }
.tm-wz-steps { gap: 6px; }
@@ -8542,6 +8953,7 @@ class TaskMatePanel extends HTMLElement {
</div>
<div class="tm-settings">
${this._wizardSettingsSection()}
${this._dashSettingsSection()}
<div class="tm-section">
<div class="tm-section-head">
<div>
@@ -9339,6 +9751,7 @@ class TaskMatePanel extends HTMLElement {
if (this._dialog.kind === "wish-decline") return this._renderWishDeclineDialog();
if (this._dialog.kind === "reject") return this._renderRejectDialog();
if (this._dialog.kind === "wizard-confirm") return this._renderWizardConfirmDialog();
if (this._dialog.kind === "dash-create") return this._renderDashDialog();
if (this._dialog.kind === "insp-start") return this._renderInspectionStartDialog();
if (this._dialog.kind === "insp-pass") return this._renderInspectionPassDialog();
if (this._dialog.kind === "insp-fail") return this._renderInspectionFailDialog();